Wire through public access toggle

Conrad Irwin created

Change summary

crates/channel/src/channel_store.rs                | 23 ++++
crates/channel/src/channel_store/channel_index.rs  |  2 
crates/collab/src/db.rs                            |  1 
crates/collab/src/db/ids.rs                        |  6 
crates/collab/src/db/queries/channels.rs           | 19 ++-
crates/collab/src/db/tests.rs                      |  1 
crates/collab/src/rpc.rs                           | 69 +++++++++----
crates/collab/src/tests/channel_buffer_tests.rs    |  6 
crates/collab_ui/src/collab_panel/channel_modal.rs | 83 +++++++++++++++
crates/rpc/proto/zed.proto                         | 10 +
crates/rpc/src/proto.rs                            |  2 
crates/theme/src/theme.rs                          |  2 
styles/src/style_tree/collab_modals.ts             | 23 ++++
13 files changed, 209 insertions(+), 38 deletions(-)

Detailed changes

crates/channel/src/channel_store.rs 🔗

@@ -9,7 +9,7 @@ use db::RELEASE_CHANNEL;
 use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
 use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
 use rpc::{
-    proto::{self, ChannelEdge, ChannelPermission, ChannelRole},
+    proto::{self, ChannelEdge, ChannelPermission, ChannelRole, ChannelVisibility},
     TypedEnvelope,
 };
 use serde_derive::{Deserialize, Serialize};
@@ -49,6 +49,7 @@ pub type ChannelData = (Channel, ChannelPath);
 pub struct Channel {
     pub id: ChannelId,
     pub name: String,
+    pub visibility: proto::ChannelVisibility,
     pub unseen_note_version: Option<(u64, clock::Global)>,
     pub unseen_message_id: Option<u64>,
 }
@@ -508,6 +509,25 @@ impl ChannelStore {
         })
     }
 
+    pub fn set_channel_visibility(
+        &mut self,
+        channel_id: ChannelId,
+        visibility: ChannelVisibility,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let client = self.client.clone();
+        cx.spawn(|_, _| async move {
+            let _ = client
+                .request(proto::SetChannelVisibility {
+                    channel_id,
+                    visibility: visibility.into(),
+                })
+                .await?;
+
+            Ok(())
+        })
+    }
+
     pub fn invite_member(
         &mut self,
         channel_id: ChannelId,
@@ -869,6 +889,7 @@ impl ChannelStore {
                     ix,
                     Arc::new(Channel {
                         id: channel.id,
+                        visibility: channel.visibility(),
                         name: channel.name,
                         unseen_note_version: None,
                         unseen_message_id: None,

crates/channel/src/channel_store/channel_index.rs 🔗

@@ -123,12 +123,14 @@ impl<'a> ChannelPathsInsertGuard<'a> {
 
     pub fn insert(&mut self, channel_proto: proto::Channel) {
         if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
+            Arc::make_mut(existing_channel).visibility = channel_proto.visibility();
             Arc::make_mut(existing_channel).name = channel_proto.name;
         } else {
             self.channels_by_id.insert(
                 channel_proto.id,
                 Arc::new(Channel {
                     id: channel_proto.id,
+                    visibility: channel_proto.visibility(),
                     name: channel_proto.name,
                     unseen_note_version: None,
                     unseen_message_id: None,

crates/collab/src/db.rs 🔗

@@ -432,6 +432,7 @@ pub struct NewUserResult {
 pub struct Channel {
     pub id: ChannelId,
     pub name: String,
+    pub visibility: ChannelVisibility,
 }
 
 #[derive(Debug, PartialEq)]

crates/collab/src/db/ids.rs 🔗

@@ -137,7 +137,7 @@ impl Into<i32> for ChannelRole {
     }
 }
 
-#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default)]
+#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
 #[sea_orm(rs_type = "String", db_type = "String(None)")]
 pub enum ChannelVisibility {
     #[sea_orm(string_value = "public")]
@@ -151,7 +151,7 @@ impl From<proto::ChannelVisibility> for ChannelVisibility {
     fn from(value: proto::ChannelVisibility) -> Self {
         match value {
             proto::ChannelVisibility::Public => ChannelVisibility::Public,
-            proto::ChannelVisibility::ChannelMembers => ChannelVisibility::Members,
+            proto::ChannelVisibility::Members => ChannelVisibility::Members,
         }
     }
 }
@@ -160,7 +160,7 @@ impl Into<proto::ChannelVisibility> for ChannelVisibility {
     fn into(self) -> proto::ChannelVisibility {
         match self {
             ChannelVisibility::Public => proto::ChannelVisibility::Public,
-            ChannelVisibility::Members => proto::ChannelVisibility::ChannelMembers,
+            ChannelVisibility::Members => proto::ChannelVisibility::Members,
         }
     }
 }

crates/collab/src/db/queries/channels.rs 🔗

@@ -93,12 +93,12 @@ impl Database {
         channel_id: ChannelId,
         visibility: ChannelVisibility,
         user_id: UserId,
-    ) -> Result<()> {
+    ) -> Result<channel::Model> {
         self.transaction(move |tx| async move {
             self.check_user_is_channel_admin(channel_id, user_id, &*tx)
                 .await?;
 
-            channel::ActiveModel {
+            let channel = channel::ActiveModel {
                 id: ActiveValue::Unchanged(channel_id),
                 visibility: ActiveValue::Set(visibility),
                 ..Default::default()
@@ -106,7 +106,7 @@ impl Database {
             .update(&*tx)
             .await?;
 
-            Ok(())
+            Ok(channel)
         })
         .await
     }
@@ -219,14 +219,14 @@ impl Database {
         channel_id: ChannelId,
         user_id: UserId,
         new_name: &str,
-    ) -> Result<String> {
+    ) -> Result<Channel> {
         self.transaction(move |tx| async move {
             let new_name = Self::sanitize_channel_name(new_name)?.to_string();
 
             self.check_user_is_channel_admin(channel_id, user_id, &*tx)
                 .await?;
 
-            channel::ActiveModel {
+            let channel = channel::ActiveModel {
                 id: ActiveValue::Unchanged(channel_id),
                 name: ActiveValue::Set(new_name.clone()),
                 ..Default::default()
@@ -234,7 +234,11 @@ impl Database {
             .update(&*tx)
             .await?;
 
-            Ok(new_name)
+            Ok(Channel {
+                id: channel.id,
+                name: channel.name,
+                visibility: channel.visibility,
+            })
         })
         .await
     }
@@ -336,6 +340,7 @@ impl Database {
                 .map(|channel| Channel {
                     id: channel.id,
                     name: channel.name,
+                    visibility: channel.visibility,
                 })
                 .collect();
 
@@ -443,6 +448,7 @@ impl Database {
             channels.push(Channel {
                 id: channel.id,
                 name: channel.name,
+                visibility: channel.visibility,
             });
 
             if role == ChannelRole::Admin {
@@ -963,6 +969,7 @@ impl Database {
                 Ok(Some((
                     Channel {
                         id: channel.id,
+                        visibility: channel.visibility,
                         name: channel.name,
                     },
                     is_accepted,

crates/collab/src/db/tests.rs 🔗

@@ -159,6 +159,7 @@ fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)
         graph.channels.push(Channel {
             id: *id,
             name: name.to_string(),
+            visibility: ChannelVisibility::Members,
         })
     }
 

crates/collab/src/rpc.rs 🔗

@@ -3,8 +3,8 @@ mod connection_pool;
 use crate::{
     auth,
     db::{
-        self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId,
-        ServerId, User, UserId,
+        self, BufferId, ChannelId, ChannelVisibility, ChannelsForUser, Database, MessageId,
+        ProjectId, RoomId, ServerId, User, UserId,
     },
     executor::Executor,
     AppState, Result,
@@ -38,8 +38,8 @@ use lazy_static::lazy_static;
 use prometheus::{register_int_gauge, IntGauge};
 use rpc::{
     proto::{
-        self, Ack, AnyTypedEnvelope, ChannelEdge, ChannelVisibility, EntityMessage,
-        EnvelopedMessage, LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators,
+        self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage,
+        LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators,
     },
     Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
 };
@@ -255,6 +255,7 @@ impl Server {
             .add_request_handler(invite_channel_member)
             .add_request_handler(remove_channel_member)
             .add_request_handler(set_channel_member_role)
+            .add_request_handler(set_channel_visibility)
             .add_request_handler(rename_channel)
             .add_request_handler(join_channel_buffer)
             .add_request_handler(leave_channel_buffer)
@@ -2210,8 +2211,7 @@ async fn create_channel(
     let channel = proto::Channel {
         id: id.to_proto(),
         name: request.name,
-        // TODO: Visibility
-        visibility: proto::ChannelVisibility::ChannelMembers as i32,
+        visibility: proto::ChannelVisibility::Members as i32,
     };
 
     response.send(proto::CreateChannelResponse {
@@ -2300,9 +2300,8 @@ async fn invite_channel_member(
     let mut update = proto::UpdateChannels::default();
     update.channel_invitations.push(proto::Channel {
         id: channel.id.to_proto(),
+        visibility: channel.visibility.into(),
         name: channel.name,
-        // TODO: Visibility
-        visibility: proto::ChannelVisibility::ChannelMembers as i32,
     });
     for connection_id in session
         .connection_pool()
@@ -2343,6 +2342,39 @@ async fn remove_channel_member(
     Ok(())
 }
 
+async fn set_channel_visibility(
+    request: proto::SetChannelVisibility,
+    response: Response<proto::SetChannelVisibility>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let visibility = request.visibility().into();
+
+    let channel = db
+        .set_channel_visibility(channel_id, visibility, session.user_id)
+        .await?;
+
+    let mut update = proto::UpdateChannels::default();
+    update.channels.push(proto::Channel {
+        id: channel.id.to_proto(),
+        name: channel.name,
+        visibility: channel.visibility.into(),
+    });
+
+    let member_ids = db.get_channel_members(channel_id).await?;
+
+    let connection_pool = session.connection_pool().await;
+    for member_id in member_ids {
+        for connection_id in connection_pool.user_connection_ids(member_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
+    }
+
+    response.send(proto::Ack {})?;
+    Ok(())
+}
+
 async fn set_channel_member_role(
     request: proto::SetChannelMemberRole,
     response: Response<proto::SetChannelMemberRole>,
@@ -2391,15 +2423,14 @@ async fn rename_channel(
 ) -> Result<()> {
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
-    let new_name = db
+    let channel = db
         .rename_channel(channel_id, session.user_id, &request.name)
         .await?;
 
     let channel = proto::Channel {
-        id: request.channel_id,
-        name: new_name,
-        // TODO: Visibility
-        visibility: proto::ChannelVisibility::ChannelMembers as i32,
+        id: channel.id.to_proto(),
+        name: channel.name,
+        visibility: channel.visibility.into(),
     };
     response.send(proto::RenameChannelResponse {
         channel: Some(channel.clone()),
@@ -2437,9 +2468,8 @@ async fn link_channel(
             .into_iter()
             .map(|channel| proto::Channel {
                 id: channel.id.to_proto(),
+                visibility: channel.visibility.into(),
                 name: channel.name,
-                // TODO: Visibility
-                visibility: proto::ChannelVisibility::ChannelMembers as i32,
             })
             .collect(),
         insert_edge: channels_to_send.edges,
@@ -2530,9 +2560,8 @@ async fn move_channel(
             .into_iter()
             .map(|channel| proto::Channel {
                 id: channel.id.to_proto(),
+                visibility: channel.visibility.into(),
                 name: channel.name,
-                // TODO: Visibility
-                visibility: proto::ChannelVisibility::ChannelMembers as i32,
             })
             .collect(),
         insert_edge: channels_to_send.edges,
@@ -2588,9 +2617,8 @@ async fn respond_to_channel_invite(
                     .into_iter()
                     .map(|channel| proto::Channel {
                         id: channel.id.to_proto(),
+                        visibility: channel.visibility.into(),
                         name: channel.name,
-                        // TODO: Visibility
-                        visibility: ChannelVisibility::ChannelMembers.into(),
                     }),
             );
         update.unseen_channel_messages = result.channel_messages;
@@ -3094,8 +3122,7 @@ fn build_initial_channels_update(
         update.channels.push(proto::Channel {
             id: channel.id.to_proto(),
             name: channel.name,
-            // TODO: Visibility
-            visibility: ChannelVisibility::Public.into(),
+            visibility: channel.visibility.into(),
         });
     }
 

crates/collab/src/tests/channel_buffer_tests.rs 🔗

@@ -11,7 +11,10 @@ use collections::HashMap;
 use editor::{Anchor, Editor, ToOffset};
 use futures::future;
 use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
-use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
+use rpc::{
+    proto::{self, PeerId},
+    RECEIVE_TIMEOUT,
+};
 use serde_json::json;
 use std::{ops::Range, sync::Arc};
 
@@ -445,6 +448,7 @@ fn channel(id: u64, name: &'static str) -> Channel {
     Channel {
         id,
         name: name.to_string(),
+        visibility: proto::ChannelVisibility::Members,
         unseen_note_version: None,
         unseen_message_id: None,
     }

crates/collab_ui/src/collab_panel/channel_modal.rs 🔗

@@ -1,6 +1,6 @@
-use channel::{ChannelId, ChannelMembership, ChannelStore};
+use channel::{Channel, ChannelId, ChannelMembership, ChannelStore};
 use client::{
-    proto::{self, ChannelRole},
+    proto::{self, ChannelRole, ChannelVisibility},
     User, UserId, UserStore,
 };
 use context_menu::{ContextMenu, ContextMenuItem};
@@ -9,7 +9,8 @@ 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;
@@ -185,6 +186,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()
@@ -193,6 +269,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,

crates/rpc/proto/zed.proto 🔗

@@ -170,7 +170,8 @@ message Envelope {
 
         LinkChannel link_channel = 140;
         UnlinkChannel unlink_channel = 141;
-        MoveChannel move_channel = 142; // current max: 145
+        MoveChannel move_channel = 142;
+        SetChannelVisibility set_channel_visibility = 146; // current max: 146
     }
 }
 
@@ -1049,6 +1050,11 @@ message SetChannelMemberRole {
     ChannelRole role = 3;
 }
 
+message SetChannelVisibility {
+    uint64 channel_id = 1;
+    ChannelVisibility visibility = 2;
+}
+
 message RenameChannel {
     uint64 channel_id = 1;
     string name = 2;
@@ -1542,7 +1548,7 @@ message Nonce {
 
 enum ChannelVisibility {
     Public = 0;
-    ChannelMembers = 1;
+    Members = 1;
 }
 
 message Channel {

crates/rpc/src/proto.rs 🔗

@@ -231,6 +231,7 @@ messages!(
     (RenameChannel, Foreground),
     (RenameChannelResponse, Foreground),
     (SetChannelMemberRole, Foreground),
+    (SetChannelVisibility, Foreground),
     (SearchProject, Background),
     (SearchProjectResponse, Background),
     (ShareProject, Foreground),
@@ -327,6 +328,7 @@ request_messages!(
     (RespondToContactRequest, Ack),
     (RespondToChannelInvite, Ack),
     (SetChannelMemberRole, Ack),
+    (SetChannelVisibility, Ack),
     (SendChannelMessage, SendChannelMessageResponse),
     (GetChannelMessages, GetChannelMessagesResponse),
     (GetChannelMembers, GetChannelMembersResponse),

crates/theme/src/theme.rs 🔗

@@ -286,6 +286,8 @@ pub struct TabbedModal {
     pub header: ContainerStyle,
     pub body: ContainerStyle,
     pub title: ContainedText,
+    pub visibility_toggle: Interactive<ContainedText>,
+    pub channel_link: Interactive<ContainedText>,
     pub picker: Picker,
     pub max_height: f32,
     pub max_width: f32,

styles/src/style_tree/collab_modals.ts 🔗

@@ -1,10 +1,11 @@
-import { useTheme } from "../theme"
+import { StyleSet, StyleSets, Styles, useTheme } from "../theme"
 import { background, border, foreground, text } from "./components"
 import picker from "./picker"
 import { input } from "../component/input"
 import contact_finder from "./contact_finder"
 import { tab } from "../component/tab"
 import { icon_button } from "../component/icon_button"
+import { interactive } from "../element/interactive"
 
 export default function channel_modal(): any {
     const theme = useTheme()
@@ -27,6 +28,24 @@ export default function channel_modal(): any {
 
     const picker_input = input()
 
+    const interactive_text = (styleset: StyleSets) =>
+        interactive({
+            base: {
+                padding: {
+                    left: 8,
+                    top: 8
+                },
+                ...text(theme.middle, "sans", styleset, "default"),
+            }, state: {
+                hovered: {
+                    ...text(theme.middle, "sans", styleset, "hovered"),
+                },
+                clicked: {
+                    ...text(theme.middle, "sans", styleset, "active"),
+                }
+            }
+        });
+
     const member_icon_style = icon_button({
         variant: "ghost",
         size: "sm",
@@ -88,6 +107,8 @@ export default function channel_modal(): any {
                     left: BUTTON_OFFSET,
                 },
             },
+            visibility_toggle: interactive_text("base"),
+            channel_link: interactive_text("accent"),
             picker: {
                 empty_container: {},
                 item: {