Detailed changes
@@ -30,6 +30,11 @@ impl Entity for ChannelStore {
type Event = ();
}
+pub enum ChannelMemberStatus {
+ Invited,
+ Member,
+}
+
impl ChannelStore {
pub fn new(
client: Arc<Client>,
@@ -115,6 +120,26 @@ impl ChannelStore {
}
}
+ pub fn get_channel_members(
+ &self,
+ channel_id: ChannelId,
+ ) -> impl Future<Output = Result<HashMap<UserId, ChannelMemberStatus>>> {
+ let client = self.client.clone();
+ async move {
+ let response = client
+ .request(proto::GetChannelMembers { channel_id })
+ .await?;
+ let mut result = HashMap::default();
+ for member_id in response.members {
+ result.insert(member_id, ChannelMemberStatus::Member);
+ }
+ for invitee_id in response.invited_members {
+ result.insert(invitee_id, ChannelMemberStatus::Invited);
+ }
+ Ok(result)
+ }
+ }
+
pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
let client = self.client.clone();
async move {
@@ -42,7 +42,7 @@ use workspace::{
use crate::face_pile::FacePile;
-use self::channel_modal::ChannelModal;
+use self::channel_modal::build_channel_modal;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct RemoveChannel {
@@ -1682,7 +1682,12 @@ impl CollabPanel {
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, |_, cx| {
cx.add_view(|cx| {
- ChannelModal::new(action.channel_id, self.channel_store.clone(), cx)
+ build_channel_modal(
+ self.user_store.clone(),
+ self.channel_store.clone(),
+ action.channel_id,
+ cx,
+ )
})
})
});
@@ -0,0 +1,178 @@
+use client::{
+ ChannelId, ChannelMemberStatus, ChannelStore, ContactRequestStatus, User, UserId, UserStore,
+};
+use collections::HashMap;
+use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
+use picker::{Picker, PickerDelegate, PickerEvent};
+use std::sync::Arc;
+use util::TryFutureExt;
+
+pub fn init(cx: &mut AppContext) {
+ Picker::<ChannelModalDelegate>::init(cx);
+}
+
+pub type ChannelModal = Picker<ChannelModalDelegate>;
+
+pub fn build_channel_modal(
+ user_store: ModelHandle<UserStore>,
+ channel_store: ModelHandle<ChannelStore>,
+ channel: ChannelId,
+ cx: &mut ViewContext<ChannelModal>,
+) -> ChannelModal {
+ Picker::new(
+ ChannelModalDelegate {
+ potential_contacts: Arc::from([]),
+ selected_index: 0,
+ user_store,
+ channel_store,
+ channel_id: channel,
+ member_statuses: Default::default(),
+ },
+ cx,
+ )
+ .with_theme(|theme| theme.picker.clone())
+}
+
+pub struct ChannelModalDelegate {
+ potential_contacts: Arc<[Arc<User>]>,
+ user_store: ModelHandle<UserStore>,
+ channel_store: ModelHandle<ChannelStore>,
+ channel_id: ChannelId,
+ selected_index: usize,
+ member_statuses: HashMap<UserId, ChannelMemberStatus>,
+}
+
+impl PickerDelegate for ChannelModalDelegate {
+ fn placeholder_text(&self) -> Arc<str> {
+ "Search collaborator by username...".into()
+ }
+
+ fn match_count(&self) -> usize {
+ self.potential_contacts.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+ self.selected_index = ix;
+ }
+
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+ let search_users = self
+ .user_store
+ .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
+
+ cx.spawn(|picker, mut cx| async move {
+ async {
+ let potential_contacts = search_users.await?;
+ picker.update(&mut cx, |picker, cx| {
+ picker.delegate_mut().potential_contacts = potential_contacts.into();
+ cx.notify();
+ })?;
+ anyhow::Ok(())
+ }
+ .log_err()
+ .await;
+ })
+ }
+
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
+ if let Some(user) = self.potential_contacts.get(self.selected_index) {
+ let user_store = self.user_store.read(cx);
+ match user_store.contact_request_status(user) {
+ ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
+ self.user_store
+ .update(cx, |store, cx| store.request_contact(user.id, cx))
+ .detach();
+ }
+ ContactRequestStatus::RequestSent => {
+ self.user_store
+ .update(cx, |store, cx| store.remove_contact(user.id, cx))
+ .detach();
+ }
+ _ => {}
+ }
+ }
+ }
+
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+ cx.emit(PickerEvent::Dismiss);
+ }
+
+ fn render_header(
+ &self,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<AnyElement<Picker<Self>>> {
+ let theme = &theme::current(cx).collab_panel.channel_modal;
+
+ self.channel_store
+ .read(cx)
+ .channel_for_id(self.channel_id)
+ .map(|channel| {
+ Label::new(
+ format!("Add members for #{}", channel.name),
+ theme.picker.item.default_style().label.clone(),
+ )
+ .into_any()
+ })
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ mouse_state: &mut MouseState,
+ selected: bool,
+ cx: &gpui::AppContext,
+ ) -> AnyElement<Picker<Self>> {
+ let theme = &theme::current(cx).collab_panel.channel_modal;
+ let user = &self.potential_contacts[ix];
+ let request_status = self.member_statuses.get(&user.id);
+
+ let icon_path = match request_status {
+ Some(ChannelMemberStatus::Member) => Some("icons/check_8.svg"),
+ Some(ChannelMemberStatus::Invited) => Some("icons/x_mark_8.svg"),
+ None => None,
+ };
+ let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
+ &theme.disabled_contact_button
+ } else {
+ &theme.contact_button
+ };
+ let style = theme.picker.item.in_state(selected).style_for(mouse_state);
+ Flex::row()
+ .with_children(user.avatar.clone().map(|avatar| {
+ Image::from_data(avatar)
+ .with_style(theme.contact_avatar)
+ .aligned()
+ .left()
+ }))
+ .with_child(
+ Label::new(user.github_login.clone(), style.label.clone())
+ .contained()
+ .with_style(theme.contact_username)
+ .aligned()
+ .left(),
+ )
+ .with_children(icon_path.map(|icon_path| {
+ Svg::new(icon_path)
+ .with_color(button_style.color)
+ .constrained()
+ .with_width(button_style.icon_width)
+ .aligned()
+ .contained()
+ .with_style(button_style.container)
+ .constrained()
+ .with_width(button_style.button_width)
+ .with_height(button_style.button_width)
+ .aligned()
+ .flex_float()
+ }))
+ .contained()
+ .with_style(style.container)
+ .constrained()
+ .with_height(theme.row_height)
+ .into_any()
+ }
+}
@@ -1,9 +1,9 @@
+pub mod collab_panel;
mod collab_titlebar_item;
mod contact_notification;
mod face_pile;
mod incoming_call_notification;
mod notifications;
-pub mod panel;
mod project_shared_notification;
mod sharing_status_indicator;
@@ -22,7 +22,7 @@ actions!(
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
vcs_menu::init(cx);
collab_titlebar_item::init(cx);
- panel::init(app_state.client.clone(), cx);
+ collab_panel::init(app_state.client.clone(), cx);
incoming_call_notification::init(&app_state, cx);
project_shared_notification::init(&app_state, cx);
sharing_status_indicator::init(cx);
@@ -1,119 +0,0 @@
-use client::{ChannelId, ChannelStore};
-use editor::Editor;
-use gpui::{
- elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
-};
-use menu::Cancel;
-use workspace::{item::ItemHandle, Modal};
-
-pub fn init(cx: &mut AppContext) {
- cx.add_action(ChannelModal::cancel)
-}
-
-pub struct ChannelModal {
- has_focus: bool,
- filter_editor: ViewHandle<Editor>,
- selection: usize,
- list_state: ListState<Self>,
- channel_store: ModelHandle<ChannelStore>,
- channel_id: ChannelId,
-}
-
-pub enum Event {
- Dismiss,
-}
-
-impl Entity for ChannelModal {
- type Event = Event;
-}
-
-impl ChannelModal {
- pub fn new(
- channel_id: ChannelId,
- channel_store: ModelHandle<ChannelStore>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- let input_editor = cx.add_view(|cx| {
- let mut editor = Editor::single_line(None, cx);
- editor.set_placeholder_text("Add a member", cx);
- editor
- });
-
- let list_state = ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
- Empty::new().into_any()
- });
-
- ChannelModal {
- has_focus: false,
- filter_editor: input_editor,
- selection: 0,
- list_state,
- channel_id,
- channel_store,
- }
- }
-
- pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
- self.dismiss(cx);
- }
-
- fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
- cx.emit(Event::Dismiss)
- }
-}
-
-impl View for ChannelModal {
- fn ui_name() -> &'static str {
- "Channel Modal"
- }
-
- fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
- let theme = theme::current(cx).clone();
- let style = &theme.collab_panel.modal;
- let modal_container = theme::current(cx).picker.container.clone();
-
- enum ChannelModal {}
- MouseEventHandler::<ChannelModal, _>::new(0, cx, |_, cx| {
- Flex::column()
- .with_child(ChildView::new(self.filter_editor.as_any(), cx))
- .with_child(
- List::new(self.list_state.clone())
- .constrained()
- .with_width(style.width)
- .flex(1., true)
- .into_any(),
- )
- .contained()
- .with_style(modal_container)
- .constrained()
- .with_max_width(540.)
- .with_max_height(420.)
- })
- .on_click(gpui::platform::MouseButton::Left, |_, _, _| {}) // Capture click and down events
- .on_down_out(gpui::platform::MouseButton::Left, |_, v, cx| v.dismiss(cx))
- .into_any_named("channel modal")
- }
-
- fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- self.has_focus = true;
- if cx.is_self_focused() {
- cx.focus(&self.filter_editor);
- }
- }
-
- fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = false;
- }
-}
-
-impl Modal for ChannelModal {
- fn has_focus(&self) -> bool {
- self.has_focus
- }
-
- fn dismiss_on_event(event: &Self::Event) -> bool {
- match event {
- Event::Dismiss => true,
- }
- }
-}
@@ -138,6 +138,8 @@ message Envelope {
UpdateChannels update_channels = 124;
JoinChannel join_channel = 126;
RemoveChannel remove_channel = 127;
+ GetChannelMembers get_channel_members = 128;
+ GetChannelMembersResponse get_channel_members_response = 129;
}
}
@@ -886,6 +888,14 @@ message RemoveChannel {
uint64 channel_id = 1;
}
+message GetChannelMembers {
+ uint64 channel_id = 1;
+}
+
+message GetChannelMembersResponse {
+ repeated uint64 members = 1;
+ repeated uint64 invited_members = 2;
+}
message CreateChannel {
string name = 1;
@@ -244,7 +244,9 @@ messages!(
(UpdateWorktreeSettings, Foreground),
(UpdateDiffBase, Foreground),
(GetPrivateUserInfo, Foreground),
- (GetPrivateUserInfoResponse, Foreground)
+ (GetPrivateUserInfoResponse, Foreground),
+ (GetChannelMembers, Foreground),
+ (GetChannelMembersResponse, Foreground)
);
request_messages!(
@@ -296,6 +298,7 @@ request_messages!(
(RemoveContact, Ack),
(RespondToContactRequest, Ack),
(RespondToChannelInvite, Ack),
+ (GetChannelMembers, GetChannelMembersResponse),
(JoinChannel, JoinRoomResponse),
(RemoveChannel, Ack),
(RenameProjectEntry, ProjectEntryResponse),
@@ -220,7 +220,7 @@ pub struct CopilotAuthAuthorized {
pub struct CollabPanel {
#[serde(flatten)]
pub container: ContainerStyle,
- pub modal: ChannelModal,
+ pub channel_modal: ChannelModal,
pub user_query_editor: FieldEditor,
pub user_query_editor_height: f32,
pub leave_call_button: IconButton,
@@ -247,7 +247,12 @@ pub struct CollabPanel {
#[derive(Deserialize, Default, JsonSchema)]
pub struct ChannelModal {
- pub width: f32,
+ pub picker: Picker,
+ pub row_height: f32,
+ pub contact_avatar: ImageStyle,
+ pub contact_username: ContainerStyle,
+ pub contact_button: IconButton,
+ pub disabled_contact_button: IconButton,
}
#[derive(Deserialize, Default, JsonSchema)]
@@ -209,9 +209,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
);
cx.add_action(
|workspace: &mut Workspace,
- _: &collab_ui::panel::ToggleFocus,
+ _: &collab_ui::collab_panel::ToggleFocus,
cx: &mut ViewContext<Workspace>| {
- workspace.toggle_panel_focus::<collab_ui::panel::CollabPanel>(cx);
+ workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
},
);
cx.add_action(
@@ -333,7 +333,7 @@ pub fn initialize_workspace(
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
let channels_panel =
- collab_ui::panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
+ collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!(
project_panel,
terminal_panel,
@@ -1,9 +1,74 @@
import { useTheme } from "../theme"
+import { background, border, foreground, text } from "./components"
+import picker from "./picker"
export default function contacts_panel(): any {
const theme = useTheme()
+ const side_margin = 6
+ const contact_button = {
+ background: background(theme.middle, "variant"),
+ color: foreground(theme.middle, "variant"),
+ icon_width: 8,
+ button_width: 16,
+ corner_radius: 8,
+ }
+
+ const picker_style = picker()
+ const picker_input = {
+ background: background(theme.middle, "on"),
+ corner_radius: 6,
+ text: text(theme.middle, "mono"),
+ placeholder_text: text(theme.middle, "mono", "on", "disabled", {
+ size: "xs",
+ }),
+ selection: theme.players[0],
+ border: border(theme.middle),
+ padding: {
+ bottom: 4,
+ left: 8,
+ right: 8,
+ top: 4,
+ },
+ margin: {
+ left: side_margin,
+ right: side_margin,
+ },
+ }
+
return {
- width: 100,
+ picker: {
+ empty_container: {},
+ item: {
+ ...picker_style.item,
+ margin: { left: side_margin, right: side_margin },
+ },
+ no_matches: picker_style.no_matches,
+ input_editor: picker_input,
+ empty_input_editor: picker_input,
+ header: picker_style.header,
+ footer: picker_style.footer,
+ },
+ row_height: 28,
+ contact_avatar: {
+ corner_radius: 10,
+ width: 18,
+ },
+ contact_username: {
+ padding: {
+ left: 8,
+ },
+ },
+ contact_button: {
+ ...contact_button,
+ hover: {
+ background: background(theme.middle, "variant", "hovered"),
+ },
+ },
+ disabled_contact_button: {
+ ...contact_button,
+ background: background(theme.middle, "disabled"),
+ color: foreground(theme.middle, "disabled"),
+ },
}
}
@@ -52,7 +52,7 @@ export default function contacts_panel(): any {
}
return {
- modal: channel_modal(),
+ channel_modal: channel_modal(),
background: background(layer),
padding: {
top: 12,