diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 678e712c7d..dfdb5fe9ed 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -33,6 +33,7 @@ impl ChannelStore { ) -> Self { let rpc_subscription = client.add_message_handler(cx.handle(), Self::handle_update_channels); + Self { channels: vec![], channel_invitations: vec![], diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index f87b68c1ec..12e02b06ed 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3214,6 +3214,44 @@ impl Database { .await } + pub async fn get_channel_invites(&self, user_id: UserId) -> Result> { + self.transaction(|tx| async move { + let tx = tx; + + let channel_invites = channel_member::Entity::find() + .filter( + channel_member::Column::UserId + .eq(user_id) + .and(channel_member::Column::Accepted.eq(false)), + ) + .all(&*tx) + .await?; + + let channels = channel::Entity::find() + .filter( + channel::Column::Id.is_in( + channel_invites + .into_iter() + .map(|channel_member| channel_member.channel_id), + ), + ) + .all(&*tx) + .await?; + + let channels = channels + .into_iter() + .map(|channel| Channel { + id: channel.id, + name: channel.name, + parent_id: None, + }) + .collect(); + + Ok(channels) + }) + .await + } + pub async fn get_channels(&self, user_id: UserId) -> Result> { self.transaction(|tx| async move { let tx = tx; diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 719e8693d4..64ab03e02d 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1023,6 +1023,96 @@ test_both_dbs!( } ); +test_both_dbs!( + test_channel_invites_postgres, + test_channel_invites_sqlite, + db, + { + let owner_id = db.create_server("test").await.unwrap().0 as u32; + + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user_3 = db + .create_user( + "user3@example.com", + false, + NewUserParams { + github_login: "user3".into(), + github_user_id: 7, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let channel_1_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); + + let channel_1_2 = db + .create_root_channel("channel_2", "2", user_1) + .await + .unwrap(); + + db.invite_channel_member(channel_1_1, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_2, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_1, user_3, user_1, false) + .await + .unwrap(); + + let user_2_invites = db + .get_channel_invites(user_2) // -> [channel_1_1, channel_1_2] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); + + assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); + + let user_3_invites = db + .get_channel_invites(user_3) // -> [channel_1_1] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); + + assert_eq!(user_3_invites, &[channel_1_1]) + } +); + #[gpui::test] async fn test_multiple_signup_overwrite() { let test_db = TestDb::postgres(build_background_executor()); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0abf2c44a7..6461f67c38 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -516,15 +516,19 @@ impl Server { this.app_state.db.set_user_connected_once(user_id, true).await?; } - let (contacts, invite_code) = future::try_join( + let (contacts, invite_code, channels, channel_invites) = future::try_join4( this.app_state.db.get_contacts(user_id), - this.app_state.db.get_invite_code_for_user(user_id) + this.app_state.db.get_invite_code_for_user(user_id), + this.app_state.db.get_channels(user_id), + this.app_state.db.get_channel_invites(user_id) ).await?; { let mut pool = this.connection_pool.lock(); pool.add_connection(connection_id, user_id, user.admin); this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; + this.peer.send(connection_id, build_initial_channels_update(channels, channel_invites))?; + if let Some((code, count)) = invite_code { this.peer.send(connection_id, proto::UpdateInviteInfo { @@ -2097,6 +2101,7 @@ async fn create_channel( response: Response, session: Session, ) -> Result<()> { + dbg!(&request); let db = session.db().await; let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); @@ -2307,6 +2312,31 @@ fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage { } } +fn build_initial_channels_update( + channels: Vec, + channel_invites: Vec, +) -> proto::UpdateChannels { + let mut update = proto::UpdateChannels::default(); + + for channel in channels { + update.channels.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + parent_id: None, + }); + } + + for channel in channel_invites { + update.channel_invitations.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + parent_id: None, + }); + } + + update +} + fn build_initial_contacts_update( contacts: Vec, pool: &ConnectionPool, diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index bdd01e4299..bfaa414a27 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -32,11 +32,10 @@ use theme::IconButton; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, + item::ItemHandle, Workspace, }; -use self::channel_modal::ChannelModal; - actions!(collab_panel, [ToggleFocus]); const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; @@ -52,6 +51,11 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::confirm); } +#[derive(Debug, Default)] +pub struct ChannelEditingState { + root_channel: bool, +} + pub struct CollabPanel { width: Option, fs: Arc, @@ -59,6 +63,8 @@ pub struct CollabPanel { pending_serialization: Task>, context_menu: ViewHandle, filter_editor: ViewHandle, + channel_name_editor: ViewHandle, + channel_editing_state: Option, entries: Vec, selection: Option, user_store: ModelHandle, @@ -93,7 +99,7 @@ enum Section { Offline, } -#[derive(Clone)] +#[derive(Clone, Debug)] enum ContactEntry { Header(Section, usize), CallParticipant { @@ -157,6 +163,23 @@ impl CollabPanel { }) .detach(); + let channel_name_editor = cx.add_view(|cx| { + Editor::single_line( + Some(Arc::new(|theme| { + theme.collab_panel.user_query_editor.clone() + })), + cx, + ) + }); + + cx.subscribe(&channel_name_editor, |this, _, event, cx| { + if let editor::Event::Blurred = event { + this.take_editing_state(cx); + cx.notify(); + } + }) + .detach(); + let list_state = ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { let theme = theme::current(cx).clone(); @@ -166,7 +189,7 @@ impl CollabPanel { match &this.entries[ix] { ContactEntry::Header(section, depth) => { let is_collapsed = this.collapsed_sections.contains(section); - Self::render_header( + this.render_header( *section, &theme, *depth, @@ -250,8 +273,10 @@ impl CollabPanel { fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), + channel_name_editor, filter_editor, entries: Vec::default(), + channel_editing_state: None, selection: None, user_store: workspace.user_store().clone(), channel_store: workspace.app_state().channel_store.clone(), @@ -333,6 +358,13 @@ impl CollabPanel { ); } + fn is_editing_root_channel(&self) -> bool { + self.channel_editing_state + .as_ref() + .map(|state| state.root_channel) + .unwrap_or(false) + } + fn update_entries(&mut self, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); let user_store = self.user_store.read(cx); @@ -944,7 +976,23 @@ impl CollabPanel { .into_any() } + fn take_editing_state( + &mut self, + cx: &mut ViewContext, + ) -> Option<(ChannelEditingState, String)> { + let result = self + .channel_editing_state + .take() + .map(|state| (state, self.channel_name_editor.read(cx).text(cx))); + + self.channel_name_editor + .update(cx, |editor, cx| editor.set_text("", cx)); + + result + } + fn render_header( + &self, section: Section, theme: &theme::Theme, depth: usize, @@ -1014,7 +1062,13 @@ impl CollabPanel { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, this, cx| { - this.toggle_channel_finder(cx); + if this.channel_editing_state.is_none() { + this.channel_editing_state = + Some(ChannelEditingState { root_channel: true }); + } + + cx.focus(this.channel_name_editor.as_any()); + cx.notify(); }) .with_tooltip::( 0, @@ -1027,6 +1081,13 @@ impl CollabPanel { _ => None, }; + let addition = match section { + Section::Channels if self.is_editing_root_channel() => { + Some(ChildView::new(self.channel_name_editor.as_any(), cx)) + } + _ => None, + }; + let can_collapse = depth > 0; let icon_size = (&theme.collab_panel).section_icon_size; MouseEventHandler::::new(section as usize, cx, |state, _| { @@ -1040,40 +1101,44 @@ impl CollabPanel { &theme.collab_panel.header_row }; - Flex::row() - .with_children(if can_collapse { - Some( - Svg::new(if is_collapsed { - "icons/chevron_right_8.svg" - } else { - "icons/chevron_down_8.svg" - }) - .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 - }) + Flex::column() .with_child( - Label::new(text, header_style.text.clone()) - .aligned() - .left() - .flex(1., true), + Flex::row() + .with_children(if can_collapse { + Some( + Svg::new(if is_collapsed { + "icons/chevron_right_8.svg" + } else { + "icons/chevron_down_8.svg" + }) + .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 + }) + .with_child( + Label::new(text, header_style.text.clone()) + .aligned() + .left() + .flex(1., true), + ) + .with_children(button.map(|button| button.aligned().right())) + .constrained() + .with_height(theme.collab_panel.row_height) + .contained() + .with_style(header_style.container), ) - .with_children(button.map(|button| button.aligned().right())) - .constrained() - .with_height(theme.collab_panel.row_height) - .contained() - .with_style(header_style.container) + .with_children(addition) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { @@ -1189,7 +1254,7 @@ impl CollabPanel { cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; - MouseEventHandler::::new(channel.id as usize, cx, |state, cx| { + MouseEventHandler::::new(channel.id as usize, cx, |state, _cx| { Flex::row() .with_child({ Svg::new("icons/hash") @@ -1218,7 +1283,7 @@ impl CollabPanel { fn render_channel_invite( channel: Arc, - user_store: ModelHandle, + channel_store: ModelHandle, theme: &theme::CollabPanel, is_selected: bool, cx: &mut ViewContext, @@ -1227,7 +1292,7 @@ impl CollabPanel { enum Accept {} let channel_id = channel.id; - let is_invite_pending = user_store.read(cx).is_channel_invite_pending(&channel); + let is_invite_pending = channel_store.read(cx).is_channel_invite_pending(&channel); let button_spacing = theme.contact_button_spacing; Flex::row() @@ -1401,7 +1466,7 @@ impl CollabPanel { } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - let did_clear = self.filter_editor.update(cx, |editor, cx| { + let mut did_clear = self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { editor.set_text("", cx); true @@ -1410,6 +1475,8 @@ impl CollabPanel { } }); + did_clear |= self.take_editing_state(cx).is_some(); + if !did_clear { cx.emit(Event::Dismissed); } @@ -1496,6 +1563,17 @@ impl CollabPanel { _ => {} } } + } else if let Some((_editing_state, channel_name)) = self.take_editing_state(cx) { + dbg!(&channel_name); + let create_channel = self.channel_store.update(cx, |channel_store, cx| { + channel_store.create_channel(&channel_name, None) + }); + + cx.foreground() + .spawn(async move { + dbg!(create_channel.await).ok(); + }) + .detach(); } } @@ -1522,14 +1600,6 @@ impl CollabPanel { } } - fn toggle_channel_finder(&mut self, cx: &mut ViewContext) { - if let Some(workspace) = self.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(cx, |_, cx| cx.add_view(|cx| ChannelModal::new(cx))); - }); - } - } - fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { let user_store = self.user_store.clone(); let prompt_message = format!( diff --git a/script/zed-with-local-servers b/script/zed-with-local-servers index f1de38adcf..c47b0e3de0 100755 --- a/script/zed-with-local-servers +++ b/script/zed-with-local-servers @@ -1,3 +1,3 @@ #!/bin/bash -ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:3000 cargo run $@ +ZED_ADMIN_API_TOKEN=secret ZED_IMPERSONATE=as-cii ZED_SERVER_URL=http://localhost:8080 cargo run $@