@@ -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::<Vec<_>>();
+
+ 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::<Vec<_>>();
+
+ assert_eq!(user_3_invites, &[channel_1_1])
+ }
+);
+
#[gpui::test]
async fn test_multiple_signup_overwrite() {
let test_db = TestDb::postgres(build_background_executor());
@@ -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<proto::CreateChannel>,
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<db::Channel>,
+ channel_invites: Vec<db::Channel>,
+) -> 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<db::Contact>,
pool: &ConnectionPool,
@@ -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<Client>, cx: &mut AppContext) {
cx.add_action(CollabPanel::confirm);
}
+#[derive(Debug, Default)]
+pub struct ChannelEditingState {
+ root_channel: bool,
+}
+
pub struct CollabPanel {
width: Option<f32>,
fs: Arc<dyn Fs>,
@@ -59,6 +63,8 @@ pub struct CollabPanel {
pending_serialization: Task<Option<()>>,
context_menu: ViewHandle<ContextMenu>,
filter_editor: ViewHandle<Editor>,
+ channel_name_editor: ViewHandle<Editor>,
+ channel_editing_state: Option<ChannelEditingState>,
entries: Vec<ContactEntry>,
selection: Option<usize>,
user_store: ModelHandle<UserStore>,
@@ -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::<Self>::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<Self>) {
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<Self>,
+ ) -> 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::<AddChannel>(
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::<Header, Self>::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"
+ Flex::column()
+ .with_child(
+ 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 {
- "icons/chevron_down_8.svg"
+ None
})
- .with_color(header_style.text.color)
- .constrained()
- .with_max_width(icon_size)
- .with_max_height(icon_size)
- .aligned()
+ .with_child(
+ Label::new(text, header_style.text.clone())
+ .aligned()
+ .left()
+ .flex(1., true),
+ )
+ .with_children(button.map(|button| button.aligned().right()))
.constrained()
- .with_width(icon_size)
+ .with_height(theme.collab_panel.row_height)
.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_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<Self>,
) -> AnyElement<Self> {
let channel_id = channel.id;
- MouseEventHandler::<Channel, Self>::new(channel.id as usize, cx, |state, cx| {
+ MouseEventHandler::<Channel, Self>::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<Channel>,
- user_store: ModelHandle<ChannelStore>,
+ channel_store: ModelHandle<ChannelStore>,
theme: &theme::CollabPanel,
is_selected: bool,
cx: &mut ViewContext<Self>,
@@ -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<Self>) {
- 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<Self>) {
- 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<Self>) {
let user_store = self.user_store.clone();
let prompt_message = format!(