channel projects (#8456)

Conrad Irwin created

Add plumbing for hosted projects. This will currently show them if they
exist
but provides no UX to create/rename/delete them.

Also changed the `ChannelId` type to not auto-cast to u64; this avoids
type
confusion if you have multiple id types.


Release Notes:

- N/A

Change summary

crates/call/src/call.rs                                        |  10 
crates/call/src/room.rs                                        |  22 
crates/channel/src/channel.rs                                  |   2 
crates/channel/src/channel_buffer.rs                           |  12 
crates/channel/src/channel_chat.rs                             |  30 
crates/channel/src/channel_store.rs                            | 180 +++
crates/channel/src/channel_store/channel_index.rs              |  24 
crates/client/src/telemetry.rs                                 |   6 
crates/client/src/user.rs                                      |   9 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql |  10 
crates/collab/migrations/20240226163408_hosted_projects.sql    |  11 
crates/collab/src/db.rs                                        |   1 
crates/collab/src/db/ids.rs                                    |   1 
crates/collab/src/db/queries.rs                                |   1 
crates/collab/src/db/queries/channels.rs                       |   5 
crates/collab/src/db/queries/hosted_projects.rs                |  42 
crates/collab/src/db/tables.rs                                 |   1 
crates/collab/src/db/tables/hosted_project.rs                  |  18 
crates/collab/src/rpc.rs                                       |   3 
crates/collab/src/tests.rs                                     |   3 
crates/collab/src/tests/channel_guest_tests.rs                 |   2 
crates/collab/src/tests/channel_message_tests.rs               |   4 
crates/collab/src/tests/channel_tests.rs                       |   8 
crates/collab/src/tests/following_tests.rs                     |   3 
crates/collab/src/tests/notification_tests.rs                  |   2 
crates/collab/src/tests/random_channel_buffer_tests.rs         |   2 
crates/collab/src/tests/test_server.rs                         |  19 
crates/collab_ui/src/channel_view.rs                           |   9 
crates/collab_ui/src/chat_panel.rs                             |   6 
crates/collab_ui/src/chat_panel/message_editor.rs              |   6 
crates/collab_ui/src/collab_panel.rs                           |  76 +
crates/collab_ui/src/collab_panel/channel_modal.rs             |   4 
crates/collab_ui/src/notification_panel.rs                     |  10 
crates/notifications/src/notification_store.rs                 |   4 
crates/rpc/proto/zed.proto                                     |  16 
crates/workspace/src/workspace.rs                              |   6 
crates/zed/src/main.rs                                         |  22 
37 files changed, 446 insertions(+), 144 deletions(-)

Detailed changes

crates/call/src/call.rs 🔗

@@ -5,7 +5,7 @@ pub mod room;
 use anyhow::{anyhow, Result};
 use audio::Audio;
 use call_settings::CallSettings;
-use client::{proto, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
+use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
 use collections::HashSet;
 use futures::{channel::oneshot, future::Shared, Future, FutureExt};
 use gpui::{
@@ -107,7 +107,7 @@ impl ActiveCall {
         }
     }
 
-    pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
+    pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
         self.room()?.read(cx).channel_id()
     }
 
@@ -336,7 +336,7 @@ impl ActiveCall {
 
     pub fn join_channel(
         &mut self,
-        channel_id: u64,
+        channel_id: ChannelId,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Model<Room>>>> {
         if let Some(room) = self.room().cloned() {
@@ -487,7 +487,7 @@ impl ActiveCall {
 pub fn report_call_event_for_room(
     operation: &'static str,
     room_id: u64,
-    channel_id: Option<u64>,
+    channel_id: Option<ChannelId>,
     client: &Arc<Client>,
 ) {
     let telemetry = client.telemetry();
@@ -497,7 +497,7 @@ pub fn report_call_event_for_room(
 
 pub fn report_call_event_for_channel(
     operation: &'static str,
-    channel_id: u64,
+    channel_id: ChannelId,
     client: &Arc<Client>,
     cx: &AppContext,
 ) {

crates/call/src/room.rs 🔗

@@ -6,7 +6,7 @@ use anyhow::{anyhow, Result};
 use audio::{Audio, Sound};
 use client::{
     proto::{self, PeerId},
-    Client, ParticipantIndex, TypedEnvelope, User, UserStore,
+    ChannelId, Client, ParticipantIndex, TypedEnvelope, User, UserStore,
 };
 use collections::{BTreeMap, HashMap, HashSet};
 use fs::Fs;
@@ -27,7 +27,7 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum Event {
     RoomJoined {
-        channel_id: Option<u64>,
+        channel_id: Option<ChannelId>,
     },
     ParticipantLocationChanged {
         participant_id: proto::PeerId,
@@ -53,13 +53,13 @@ pub enum Event {
         project_id: u64,
     },
     Left {
-        channel_id: Option<u64>,
+        channel_id: Option<ChannelId>,
     },
 }
 
 pub struct Room {
     id: u64,
-    channel_id: Option<u64>,
+    channel_id: Option<ChannelId>,
     live_kit: Option<LiveKitRoom>,
     status: RoomStatus,
     shared_projects: HashSet<WeakModel<Project>>,
@@ -84,7 +84,7 @@ pub struct Room {
 impl EventEmitter<Event> for Room {}
 
 impl Room {
-    pub fn channel_id(&self) -> Option<u64> {
+    pub fn channel_id(&self) -> Option<ChannelId> {
         self.channel_id
     }
 
@@ -106,7 +106,7 @@ impl Room {
 
     fn new(
         id: u64,
-        channel_id: Option<u64>,
+        channel_id: Option<ChannelId>,
         live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
         client: Arc<Client>,
         user_store: Model<UserStore>,
@@ -273,13 +273,17 @@ impl Room {
     }
 
     pub(crate) async fn join_channel(
-        channel_id: u64,
+        channel_id: ChannelId,
         client: Arc<Client>,
         user_store: Model<UserStore>,
         cx: AsyncAppContext,
     ) -> Result<Model<Self>> {
         Self::from_join_response(
-            client.request(proto::JoinChannel { channel_id }).await?,
+            client
+                .request(proto::JoinChannel {
+                    channel_id: channel_id.0,
+                })
+                .await?,
             client,
             user_store,
             cx,
@@ -337,7 +341,7 @@ impl Room {
         let room = cx.new_model(|cx| {
             Self::new(
                 room_proto.id,
-                response.channel_id,
+                response.channel_id.map(ChannelId),
                 response.live_kit_connection_info,
                 client,
                 user_store,

crates/channel/src/channel.rs 🔗

@@ -11,7 +11,7 @@ pub use channel_chat::{
     mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
     MessageParams,
 };
-pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore};
+pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore, HostedProjectId};
 
 #[cfg(test)]
 mod channel_store_tests;

crates/channel/src/channel_buffer.rs 🔗

@@ -1,6 +1,6 @@
-use crate::{Channel, ChannelId, ChannelStore};
+use crate::{Channel, ChannelStore};
 use anyhow::Result;
-use client::{Client, Collaborator, UserStore, ZED_ALWAYS_ACTIVE};
+use client::{ChannelId, Client, Collaborator, UserStore, ZED_ALWAYS_ACTIVE};
 use collections::HashMap;
 use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
 use language::proto::serialize_version;
@@ -51,7 +51,7 @@ impl ChannelBuffer {
     ) -> Result<Model<Self>> {
         let response = client
             .request(proto::JoinChannelBuffer {
-                channel_id: channel.id,
+                channel_id: channel.id.0,
             })
             .await?;
         let buffer_id = BufferId::new(response.buffer_id)?;
@@ -68,7 +68,7 @@ impl ChannelBuffer {
         })?;
         buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))??;
 
-        let subscription = client.subscribe_to_entity(channel.id)?;
+        let subscription = client.subscribe_to_entity(channel.id.0)?;
 
         anyhow::Ok(cx.new_model(|cx| {
             cx.subscribe(&buffer, Self::on_buffer_update).detach();
@@ -97,7 +97,7 @@ impl ChannelBuffer {
             }
             self.client
                 .send(proto::LeaveChannelBuffer {
-                    channel_id: self.channel_id,
+                    channel_id: self.channel_id.0,
                 })
                 .log_err();
         }
@@ -191,7 +191,7 @@ impl ChannelBuffer {
                 let operation = language::proto::serialize_operation(operation);
                 self.client
                     .send(proto::UpdateChannelBuffer {
-                        channel_id: self.channel_id,
+                        channel_id: self.channel_id.0,
                         operations: vec![operation],
                     })
                     .log_err();

crates/channel/src/channel_chat.rs 🔗

@@ -1,9 +1,9 @@
-use crate::{Channel, ChannelId, ChannelStore};
+use crate::{Channel, ChannelStore};
 use anyhow::{anyhow, Result};
 use client::{
     proto,
     user::{User, UserStore},
-    Client, Subscription, TypedEnvelope, UserId,
+    ChannelId, Client, Subscription, TypedEnvelope, UserId,
 };
 use collections::HashSet;
 use futures::lock::Mutex;
@@ -104,10 +104,12 @@ impl ChannelChat {
         mut cx: AsyncAppContext,
     ) -> Result<Model<Self>> {
         let channel_id = channel.id;
-        let subscription = client.subscribe_to_entity(channel_id).unwrap();
+        let subscription = client.subscribe_to_entity(channel_id.0).unwrap();
 
         let response = client
-            .request(proto::JoinChannelChat { channel_id })
+            .request(proto::JoinChannelChat {
+                channel_id: channel_id.0,
+            })
             .await?;
 
         let handle = cx.new_model(|cx| {
@@ -143,7 +145,7 @@ impl ChannelChat {
     fn release(&mut self, _: &mut AppContext) {
         self.rpc
             .send(proto::LeaveChannelChat {
-                channel_id: self.channel_id,
+                channel_id: self.channel_id.0,
             })
             .log_err();
     }
@@ -200,7 +202,7 @@ impl ChannelChat {
         Ok(cx.spawn(move |this, mut cx| async move {
             let outgoing_message_guard = outgoing_messages_lock.lock().await;
             let request = rpc.request(proto::SendChannelMessage {
-                channel_id,
+                channel_id: channel_id.0,
                 body: message.text,
                 nonce: Some(nonce.into()),
                 mentions: mentions_to_proto(&message.mentions),
@@ -220,7 +222,7 @@ impl ChannelChat {
 
     pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         let response = self.rpc.request(proto::RemoveChannelMessage {
-            channel_id: self.channel_id,
+            channel_id: self.channel_id.0,
             message_id: id,
         });
         cx.spawn(move |this, mut cx| async move {
@@ -245,7 +247,7 @@ impl ChannelChat {
             async move {
                 let response = rpc
                     .request(proto::GetChannelMessages {
-                        channel_id,
+                        channel_id: channel_id.0,
                         before_message_id,
                     })
                     .await?;
@@ -323,7 +325,7 @@ impl ChannelChat {
             {
                 self.rpc
                     .send(proto::AckChannelMessage {
-                        channel_id: self.channel_id,
+                        channel_id: self.channel_id.0,
                         message_id: latest_message_id,
                     })
                     .ok();
@@ -401,7 +403,11 @@ impl ChannelChat {
         let channel_id = self.channel_id;
         cx.spawn(move |this, mut cx| {
             async move {
-                let response = rpc.request(proto::JoinChannelChat { channel_id }).await?;
+                let response = rpc
+                    .request(proto::JoinChannelChat {
+                        channel_id: channel_id.0,
+                    })
+                    .await?;
                 Self::handle_loaded_messages(
                     this.clone(),
                     user_store.clone(),
@@ -418,7 +424,7 @@ impl ChannelChat {
 
                 for pending_message in pending_messages {
                     let request = rpc.request(proto::SendChannelMessage {
-                        channel_id,
+                        channel_id: channel_id.0,
                         body: pending_message.body,
                         mentions: mentions_to_proto(&pending_message.mentions),
                         nonce: Some(pending_message.nonce.into()),
@@ -461,7 +467,7 @@ impl ChannelChat {
         if self.acknowledged_message_ids.insert(id) {
             self.rpc
                 .send(proto::AckChannelMessage {
-                    channel_id: self.channel_id,
+                    channel_id: self.channel_id.0,
                     message_id: id,
                 })
                 .ok();

crates/channel/src/channel_store.rs 🔗

@@ -3,7 +3,7 @@ mod channel_index;
 use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
 use anyhow::{anyhow, Result};
 use channel_index::ChannelIndex;
-use client::{Client, Subscription, User, UserId, UserStore};
+use client::{ChannelId, Client, Subscription, User, UserId, UserStore};
 use collections::{hash_map, HashMap, HashSet};
 use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
 use gpui::{
@@ -19,15 +19,16 @@ use rpc::{
 use std::{mem, sync::Arc, time::Duration};
 use util::{async_maybe, maybe, ResultExt};
 
+pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
+
 pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
     let channel_store =
         cx.new_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
     cx.set_global(GlobalChannelStore(channel_store));
 }
 
-pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
-
-pub type ChannelId = u64;
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub struct HostedProjectId(pub u64);
 
 #[derive(Debug, Clone, Default)]
 struct NotesVersion {
@@ -35,11 +36,31 @@ struct NotesVersion {
     version: clock::Global,
 }
 
+#[derive(Debug, Clone)]
+pub struct HostedProject {
+    id: HostedProjectId,
+    channel_id: ChannelId,
+    name: SharedString,
+    _visibility: proto::ChannelVisibility,
+}
+
+impl From<proto::HostedProject> for HostedProject {
+    fn from(project: proto::HostedProject) -> Self {
+        Self {
+            id: HostedProjectId(project.id),
+            channel_id: ChannelId(project.channel_id),
+            _visibility: project.visibility(),
+            name: project.name.into(),
+        }
+    }
+}
+
 pub struct ChannelStore {
     pub channel_index: ChannelIndex,
     channel_invitations: Vec<Arc<Channel>>,
     channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
     channel_states: HashMap<ChannelId, ChannelState>,
+    hosted_projects: HashMap<HostedProjectId, HostedProject>,
 
     outgoing_invites: HashSet<(ChannelId, UserId)>,
     update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
@@ -58,7 +79,7 @@ pub struct Channel {
     pub id: ChannelId,
     pub name: SharedString,
     pub visibility: proto::ChannelVisibility,
-    pub parent_path: Vec<u64>,
+    pub parent_path: Vec<ChannelId>,
 }
 
 #[derive(Default)]
@@ -68,6 +89,7 @@ pub struct ChannelState {
     observed_chat_message: Option<u64>,
     observed_notes_versions: Option<NotesVersion>,
     role: Option<ChannelRole>,
+    projects: HashSet<HostedProjectId>,
 }
 
 impl Channel {
@@ -92,10 +114,7 @@ impl Channel {
     }
 
     pub fn root_id(&self) -> ChannelId {
-        self.parent_path
-            .first()
-            .map(|id| *id as ChannelId)
-            .unwrap_or(self.id)
+        self.parent_path.first().copied().unwrap_or(self.id)
     }
 
     pub fn slug(str: &str) -> String {
@@ -199,6 +218,7 @@ impl ChannelStore {
             channel_invitations: Vec::default(),
             channel_index: ChannelIndex::default(),
             channel_participants: Default::default(),
+            hosted_projects: Default::default(),
             outgoing_invites: Default::default(),
             opened_buffers: Default::default(),
             opened_chats: Default::default(),
@@ -285,6 +305,19 @@ impl ChannelStore {
         self.channel_index.by_id().get(&channel_id)
     }
 
+    pub fn projects_for_id(&self, channel_id: ChannelId) -> Vec<(SharedString, HostedProjectId)> {
+        let mut projects: Vec<(SharedString, HostedProjectId)> = self
+            .channel_states
+            .get(&channel_id)
+            .map(|state| state.projects.clone())
+            .unwrap_or_default()
+            .into_iter()
+            .flat_map(|id| Some((self.hosted_projects.get(&id)?.name.clone(), id)))
+            .collect();
+        projects.sort();
+        projects
+    }
+
     pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
         if let Some(buffer) = self.opened_buffers.get(&channel_id) {
             if let OpenedModelHandle::Open(buffer) = buffer {
@@ -562,13 +595,16 @@ impl ChannelStore {
         let name = name.trim_start_matches("#").to_owned();
         cx.spawn(move |this, mut cx| async move {
             let response = client
-                .request(proto::CreateChannel { name, parent_id })
+                .request(proto::CreateChannel {
+                    name,
+                    parent_id: parent_id.map(|cid| cid.0),
+                })
                 .await?;
 
             let channel = response
                 .channel
                 .ok_or_else(|| anyhow!("missing channel in response"))?;
-            let channel_id = channel.id;
+            let channel_id = ChannelId(channel.id);
 
             this.update(&mut cx, |this, cx| {
                 let task = this.update_channels(
@@ -600,7 +636,10 @@ impl ChannelStore {
         let client = self.client.clone();
         cx.spawn(move |_, _| async move {
             let _ = client
-                .request(proto::MoveChannel { channel_id, to })
+                .request(proto::MoveChannel {
+                    channel_id: channel_id.0,
+                    to: to.0,
+                })
                 .await?;
 
             Ok(())
@@ -617,7 +656,7 @@ impl ChannelStore {
         cx.spawn(move |_, _| async move {
             let _ = client
                 .request(proto::SetChannelVisibility {
-                    channel_id,
+                    channel_id: channel_id.0,
                     visibility: visibility.into(),
                 })
                 .await?;
@@ -642,7 +681,7 @@ impl ChannelStore {
         cx.spawn(move |this, mut cx| async move {
             let result = client
                 .request(proto::InviteChannelMember {
-                    channel_id,
+                    channel_id: channel_id.0,
                     user_id,
                     role: role.into(),
                 })
@@ -674,7 +713,7 @@ impl ChannelStore {
         cx.spawn(move |this, mut cx| async move {
             let result = client
                 .request(proto::RemoveChannelMember {
-                    channel_id,
+                    channel_id: channel_id.0,
                     user_id,
                 })
                 .await;
@@ -704,7 +743,7 @@ impl ChannelStore {
         cx.spawn(move |this, mut cx| async move {
             let result = client
                 .request(proto::SetChannelMemberRole {
-                    channel_id,
+                    channel_id: channel_id.0,
                     user_id,
                     role: role.into(),
                 })
@@ -730,7 +769,10 @@ impl ChannelStore {
         let name = new_name.to_string();
         cx.spawn(move |this, mut cx| async move {
             let channel = client
-                .request(proto::RenameChannel { channel_id, name })
+                .request(proto::RenameChannel {
+                    channel_id: channel_id.0,
+                    name,
+                })
                 .await?
                 .channel
                 .ok_or_else(|| anyhow!("missing channel in response"))?;
@@ -763,7 +805,10 @@ impl ChannelStore {
         let client = self.client.clone();
         cx.background_executor().spawn(async move {
             client
-                .request(proto::RespondToChannelInvite { channel_id, accept })
+                .request(proto::RespondToChannelInvite {
+                    channel_id: channel_id.0,
+                    accept,
+                })
                 .await?;
             Ok(())
         })
@@ -778,7 +823,9 @@ impl ChannelStore {
         let user_store = self.user_store.downgrade();
         cx.spawn(move |_, mut cx| async move {
             let response = client
-                .request(proto::GetChannelMembers { channel_id })
+                .request(proto::GetChannelMembers {
+                    channel_id: channel_id.0,
+                })
                 .await?;
 
             let user_ids = response.members.iter().map(|m| m.user_id).collect();
@@ -806,7 +853,11 @@ impl ChannelStore {
     pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
         let client = self.client.clone();
         async move {
-            client.request(proto::DeleteChannel { channel_id }).await?;
+            client
+                .request(proto::DeleteChannel {
+                    channel_id: channel_id.0,
+                })
+                .await?;
             Ok(())
         }
     }
@@ -843,19 +894,23 @@ impl ChannelStore {
             for buffer_version in message.payload.observed_channel_buffer_version {
                 let version = language::proto::deserialize_version(&buffer_version.version);
                 this.acknowledge_notes_version(
-                    buffer_version.channel_id,
+                    ChannelId(buffer_version.channel_id),
                     buffer_version.epoch,
                     &version,
                     cx,
                 );
             }
             for message_id in message.payload.observed_channel_message_id {
-                this.acknowledge_message_id(message_id.channel_id, message_id.message_id, cx);
+                this.acknowledge_message_id(
+                    ChannelId(message_id.channel_id),
+                    message_id.message_id,
+                    cx,
+                );
             }
             for membership in message.payload.channel_memberships {
                 if let Some(role) = ChannelRole::from_i32(membership.role) {
                     this.channel_states
-                        .entry(membership.channel_id)
+                        .entry(ChannelId(membership.channel_id))
                         .or_insert_with(|| ChannelState::default())
                         .set_role(role)
                 }
@@ -888,7 +943,7 @@ impl ChannelStore {
                     let channel_buffer = buffer.read(cx);
                     let buffer = channel_buffer.buffer().read(cx);
                     buffer_versions.push(proto::ChannelBufferVersion {
-                        channel_id: channel_buffer.channel_id,
+                        channel_id: channel_buffer.channel_id.0,
                         epoch: channel_buffer.epoch(),
                         version: language::proto::serialize_version(&buffer.version()),
                     });
@@ -919,7 +974,7 @@ impl ChannelStore {
                             if let Some(remote_buffer) = response
                                 .buffers
                                 .iter_mut()
-                                .find(|buffer| buffer.channel_id == channel_id)
+                                .find(|buffer| buffer.channel_id == channel_id.0)
                             {
                                 let channel_id = channel_buffer.channel_id;
                                 let remote_version =
@@ -955,7 +1010,7 @@ impl ChannelStore {
                                             {
                                                 client
                                                     .send(proto::UpdateChannelBuffer {
-                                                        channel_id,
+                                                        channel_id: channel_id.0,
                                                         operations: chunk,
                                                     })
                                                     .ok();
@@ -1010,12 +1065,12 @@ impl ChannelStore {
     ) -> Option<Task<Result<()>>> {
         if !payload.remove_channel_invitations.is_empty() {
             self.channel_invitations
-                .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id));
+                .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id.0));
         }
         for channel in payload.channel_invitations {
             match self
                 .channel_invitations
-                .binary_search_by_key(&channel.id, |c| c.id)
+                .binary_search_by_key(&channel.id, |c| c.id.0)
             {
                 Ok(ix) => {
                     Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into()
@@ -1023,10 +1078,14 @@ impl ChannelStore {
                 Err(ix) => self.channel_invitations.insert(
                     ix,
                     Arc::new(Channel {
-                        id: channel.id,
+                        id: ChannelId(channel.id),
                         visibility: channel.visibility(),
                         name: channel.name.into(),
-                        parent_path: channel.parent_path,
+                        parent_path: channel
+                            .parent_path
+                            .into_iter()
+                            .map(|cid| ChannelId(cid))
+                            .collect(),
                     }),
                 ),
             }
@@ -1035,20 +1094,27 @@ impl ChannelStore {
         let channels_changed = !payload.channels.is_empty()
             || !payload.delete_channels.is_empty()
             || !payload.latest_channel_message_ids.is_empty()
-            || !payload.latest_channel_buffer_versions.is_empty();
+            || !payload.latest_channel_buffer_versions.is_empty()
+            || !payload.hosted_projects.is_empty()
+            || !payload.deleted_hosted_projects.is_empty();
 
         if channels_changed {
             if !payload.delete_channels.is_empty() {
-                self.channel_index.delete_channels(&payload.delete_channels);
+                let delete_channels: Vec<ChannelId> = payload
+                    .delete_channels
+                    .into_iter()
+                    .map(|cid| ChannelId(cid))
+                    .collect();
+                self.channel_index.delete_channels(&delete_channels);
                 self.channel_participants
-                    .retain(|channel_id, _| !&payload.delete_channels.contains(channel_id));
+                    .retain(|channel_id, _| !delete_channels.contains(&channel_id));
 
-                for channel_id in &payload.delete_channels {
+                for channel_id in &delete_channels {
                     let channel_id = *channel_id;
                     if payload
                         .channels
                         .iter()
-                        .any(|channel| channel.id == channel_id)
+                        .any(|channel| channel.id == channel_id.0)
                     {
                         continue;
                     }
@@ -1064,7 +1130,7 @@ impl ChannelStore {
 
             let mut index = self.channel_index.bulk_insert();
             for channel in payload.channels {
-                let id = channel.id;
+                let id = ChannelId(channel.id);
                 let channel_changed = index.insert(channel);
 
                 if channel_changed {
@@ -1079,17 +1145,45 @@ impl ChannelStore {
             for latest_buffer_version in payload.latest_channel_buffer_versions {
                 let version = language::proto::deserialize_version(&latest_buffer_version.version);
                 self.channel_states
-                    .entry(latest_buffer_version.channel_id)
+                    .entry(ChannelId(latest_buffer_version.channel_id))
                     .or_default()
                     .update_latest_notes_version(latest_buffer_version.epoch, &version)
             }
 
             for latest_channel_message in payload.latest_channel_message_ids {
                 self.channel_states
-                    .entry(latest_channel_message.channel_id)
+                    .entry(ChannelId(latest_channel_message.channel_id))
                     .or_default()
                     .update_latest_message_id(latest_channel_message.message_id);
             }
+
+            for hosted_project in payload.hosted_projects {
+                let hosted_project: HostedProject = hosted_project.into();
+                if let Some(old_project) = self
+                    .hosted_projects
+                    .insert(hosted_project.id, hosted_project.clone())
+                {
+                    self.channel_states
+                        .entry(old_project.channel_id)
+                        .or_default()
+                        .remove_hosted_project(old_project.id);
+                }
+                self.channel_states
+                    .entry(hosted_project.channel_id)
+                    .or_default()
+                    .add_hosted_project(hosted_project.id);
+            }
+
+            for hosted_project_id in payload.deleted_hosted_projects {
+                let hosted_project_id = HostedProjectId(hosted_project_id);
+
+                if let Some(old_project) = self.hosted_projects.remove(&hosted_project_id) {
+                    self.channel_states
+                        .entry(old_project.channel_id)
+                        .or_default()
+                        .remove_hosted_project(old_project.id);
+                }
+            }
         }
 
         cx.notify();
@@ -1129,7 +1223,7 @@ impl ChannelStore {
                     participants.sort_by_key(|u| u.id);
 
                     this.channel_participants
-                        .insert(entry.channel_id, participants);
+                        .insert(ChannelId(entry.channel_id), participants);
                 }
 
                 cx.notify();
@@ -1207,4 +1301,12 @@ impl ChannelState {
             version: version.clone(),
         });
     }
+
+    fn add_hosted_project(&mut self, project_id: HostedProjectId) {
+        self.projects.insert(project_id);
+    }
+
+    fn remove_hosted_project(&mut self, project_id: HostedProjectId) {
+        self.projects.remove(&project_id);
+    }
 }

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

@@ -1,4 +1,5 @@
-use crate::{Channel, ChannelId};
+use crate::Channel;
+use client::ChannelId;
 use collections::BTreeMap;
 use rpc::proto;
 use std::sync::Arc;
@@ -50,27 +51,32 @@ pub struct ChannelPathsInsertGuard<'a> {
 impl<'a> ChannelPathsInsertGuard<'a> {
     pub fn insert(&mut self, channel_proto: proto::Channel) -> bool {
         let mut ret = false;
-        if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
+        let parent_path = channel_proto
+            .parent_path
+            .iter()
+            .map(|cid| ChannelId(*cid))
+            .collect();
+        if let Some(existing_channel) = self.channels_by_id.get_mut(&ChannelId(channel_proto.id)) {
             let existing_channel = Arc::make_mut(existing_channel);
 
             ret = existing_channel.visibility != channel_proto.visibility()
                 || existing_channel.name != channel_proto.name
-                || existing_channel.parent_path != channel_proto.parent_path;
+                || existing_channel.parent_path != parent_path;
 
             existing_channel.visibility = channel_proto.visibility();
             existing_channel.name = channel_proto.name.into();
-            existing_channel.parent_path = channel_proto.parent_path.into();
+            existing_channel.parent_path = parent_path;
         } else {
             self.channels_by_id.insert(
-                channel_proto.id,
+                ChannelId(channel_proto.id),
                 Arc::new(Channel {
-                    id: channel_proto.id,
+                    id: ChannelId(channel_proto.id),
                     visibility: channel_proto.visibility(),
                     name: channel_proto.name.into(),
-                    parent_path: channel_proto.parent_path,
+                    parent_path,
                 }),
             );
-            self.insert_root(channel_proto.id);
+            self.insert_root(ChannelId(channel_proto.id));
         }
         ret
     }
@@ -94,7 +100,7 @@ impl<'a> Drop for ChannelPathsInsertGuard<'a> {
 fn channel_path_sorting_key<'a>(
     id: ChannelId,
     channels_by_id: &'a BTreeMap<ChannelId, Arc<Channel>>,
-) -> impl Iterator<Item = (&str, u64)> {
+) -> impl Iterator<Item = (&str, ChannelId)> {
     let (parent_path, name) = channels_by_id
         .get(&id)
         .map_or((&[] as &[_], None), |channel| {

crates/client/src/telemetry.rs 🔗

@@ -1,6 +1,6 @@
 mod event_coalescer;
 
-use crate::TelemetrySettings;
+use crate::{ChannelId, TelemetrySettings};
 use chrono::{DateTime, Utc};
 use clock::SystemClock;
 use futures::Future;
@@ -278,12 +278,12 @@ impl Telemetry {
         self: &Arc<Self>,
         operation: &'static str,
         room_id: Option<u64>,
-        channel_id: Option<u64>,
+        channel_id: Option<ChannelId>,
     ) {
         let event = Event::Call(CallEvent {
             operation: operation.to_string(),
             room_id,
-            channel_id,
+            channel_id: channel_id.map(|cid| cid.0),
         });
 
         self.report_event(event)

crates/client/src/user.rs 🔗

@@ -15,6 +15,15 @@ use util::TryFutureExt as _;
 
 pub type UserId = u64;
 
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub struct ChannelId(pub u64);
+
+impl std::fmt::Display for ChannelId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub struct ParticipantIndex(pub u32);
 

crates/collab/migrations.sqlite/20221109000000_test_schema.sql 🔗

@@ -375,3 +375,13 @@ CREATE TABLE extension_versions (
 
 CREATE UNIQUE INDEX "index_extensions_external_id" ON "extensions" ("external_id");
 CREATE INDEX "index_extensions_total_download_count" ON "extensions" ("total_download_count");
+
+CREATE TABLE hosted_projects (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    channel_id INTEGER NOT NULL REFERENCES channels(id),
+    name TEXT NOT NULL,
+    visibility TEXT NOT NULL,
+    deleted_at TIMESTAMP NULL
+);
+CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
+CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);

crates/collab/migrations/20240226163408_hosted_projects.sql 🔗

@@ -0,0 +1,11 @@
+-- Add migration script here
+
+CREATE TABLE hosted_projects (
+    id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
+    channel_id INT NOT NULL REFERENCES channels(id),
+    name TEXT NOT NULL,
+    visibility TEXT NOT NULL,
+    deleted_at TIMESTAMP NULL
+);
+CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
+CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);

crates/collab/src/db.rs 🔗

@@ -587,6 +587,7 @@ pub struct ChannelsForUser {
     pub channels: Vec<Channel>,
     pub channel_memberships: Vec<channel_member::Model>,
     pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
+    pub hosted_projects: Vec<proto::HostedProject>,
 
     pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
     pub observed_channel_messages: Vec<proto::ChannelMessageId>,

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

@@ -88,6 +88,7 @@ id_type!(FlagId);
 id_type!(ExtensionId);
 id_type!(NotificationId);
 id_type!(NotificationKindId);
+id_type!(HostedProjectId);
 
 /// ChannelRole gives you permissions for both channels and calls.
 #[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]

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

@@ -6,6 +6,7 @@ pub mod channels;
 pub mod contacts;
 pub mod contributors;
 pub mod extensions;
+pub mod hosted_projects;
 pub mod messages;
 pub mod notifications;
 pub mod projects;

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

@@ -652,9 +652,14 @@ impl Database {
             .observed_channel_messages(&channel_ids, user_id, &*tx)
             .await?;
 
+        let hosted_projects = self
+            .get_hosted_projects(&channel_ids, &roles_by_channel_id, &*tx)
+            .await?;
+
         Ok(ChannelsForUser {
             channel_memberships,
             channels,
+            hosted_projects,
             channel_participants,
             latest_buffer_versions,
             latest_channel_messages,

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

@@ -0,0 +1,42 @@
+use rpc::proto;
+
+use super::*;
+
+impl Database {
+    pub async fn get_hosted_projects(
+        &self,
+        channel_ids: &Vec<ChannelId>,
+        roles: &HashMap<ChannelId, ChannelRole>,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<proto::HostedProject>> {
+        Ok(hosted_project::Entity::find()
+            .filter(hosted_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
+            .all(&*tx)
+            .await?
+            .into_iter()
+            .flat_map(|project| {
+                if project.deleted_at.is_some() {
+                    return None;
+                }
+                match project.visibility {
+                    ChannelVisibility::Public => {}
+                    ChannelVisibility::Members => {
+                        let is_visible = roles
+                            .get(&project.channel_id)
+                            .map(|role| role.can_see_all_descendants())
+                            .unwrap_or(false);
+                        if !is_visible {
+                            return None;
+                        }
+                    }
+                };
+                Some(proto::HostedProject {
+                    id: project.id.to_proto(),
+                    channel_id: project.channel_id.to_proto(),
+                    name: project.name.clone(),
+                    visibility: project.visibility.into(),
+                })
+            })
+            .collect())
+    }
+}

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

@@ -14,6 +14,7 @@ pub mod extension;
 pub mod extension_version;
 pub mod feature_flag;
 pub mod follower;
+pub mod hosted_project;
 pub mod language_server;
 pub mod notification;
 pub mod notification_kind;

crates/collab/src/db/tables/hosted_project.rs 🔗

@@ -0,0 +1,18 @@
+use crate::db::{ChannelId, ChannelVisibility, HostedProjectId};
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "hosted_projects")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: HostedProjectId,
+    pub channel_id: ChannelId,
+    pub name: String,
+    pub visibility: ChannelVisibility,
+    pub deleted_at: Option<DateTime>,
+}
+
+impl ActiveModelBehavior for ActiveModel {}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}

crates/collab/src/rpc.rs 🔗

@@ -3396,6 +3396,9 @@ fn build_channels_update(
     for channel in channel_invites {
         update.channel_invitations.push(channel.to_proto());
     }
+    for project in channels.hosted_projects {
+        update.hosted_projects.push(project);
+    }
 
     update
 }

crates/collab/src/tests.rs 🔗

@@ -1,4 +1,5 @@
 use call::Room;
+use client::ChannelId;
 use gpui::{Model, TestAppContext};
 
 mod channel_buffer_tests;
@@ -43,6 +44,6 @@ fn room_participants(room: &Model<Room>, cx: &mut TestAppContext) -> RoomPartici
     })
 }
 
-fn channel_id(room: &Model<Room>, cx: &mut TestAppContext) -> Option<u64> {
+fn channel_id(room: &Model<Room>, cx: &mut TestAppContext) -> Option<ChannelId> {
     cx.read(|cx| room.read(cx).channel_id())
 }

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

@@ -183,7 +183,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
     server
         .app_state
         .db
-        .set_channel_requires_zed_cla(ChannelId::from_proto(parent_channel_id), true)
+        .set_channel_requires_zed_cla(ChannelId::from_proto(parent_channel_id.0), true)
         .await
         .unwrap();
 

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

@@ -100,13 +100,13 @@ async fn test_basic_channel_messages(
             Notification::ChannelMessageMention {
                 message_id,
                 sender_id: client_a.id(),
-                channel_id,
+                channel_id: channel_id.0,
             }
         );
         assert_eq!(
             store.notification_at(1).unwrap().notification,
             Notification::ChannelInvitation {
-                channel_id,
+                channel_id: channel_id.0,
                 channel_name: "the-channel".to_string(),
                 inviter_id: client_a.id()
             }

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

@@ -4,8 +4,8 @@ use crate::{
     tests::{room_participants, RoomParticipants, TestServer},
 };
 use call::ActiveCall;
-use channel::{ChannelId, ChannelMembership, ChannelStore};
-use client::User;
+use channel::{ChannelMembership, ChannelStore};
+use client::{ChannelId, User};
 use futures::future::try_join_all;
 use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext};
 use rpc::{
@@ -281,7 +281,7 @@ async fn test_core_channels(
         .app_state
         .db
         .rename_channel(
-            db::ChannelId::from_proto(channel_a_id),
+            db::ChannelId::from_proto(channel_a_id.0),
             UserId::from_proto(client_a.id()),
             "channel-a-renamed",
         )
@@ -1444,7 +1444,7 @@ fn assert_channels(
 fn assert_channels_list_shape(
     channel_store: &Model<ChannelStore>,
     cx: &TestAppContext,
-    expected_channels: &[(u64, usize)],
+    expected_channels: &[(ChannelId, usize)],
 ) {
     let actual = cx.read(|cx| {
         channel_store.read_with(cx, |store, _| {

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

@@ -1,5 +1,6 @@
 use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
 use call::{ActiveCall, ParticipantLocation};
+use client::ChannelId;
 use collab_ui::{
     channel_view::ChannelView,
     notifications::project_shared_notification::ProjectSharedNotification,
@@ -2000,7 +2001,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
 }
 
 async fn join_channel(
-    channel_id: u64,
+    channel_id: ChannelId,
     client: &TestClient,
     cx: &mut TestAppContext,
 ) -> anyhow::Result<()> {

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

@@ -137,7 +137,7 @@ async fn test_notifications(
         assert_eq!(
             entry.notification,
             Notification::ChannelInvitation {
-                channel_id,
+                channel_id: channel_id.0,
                 channel_name: "the-channel".to_string(),
                 inviter_id: client_a.id()
             }

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

@@ -253,7 +253,7 @@ impl RandomizedTest for RandomChannelBufferTest {
                         .channel_buffers()
                         .deref()
                         .iter()
-                        .find(|b| b.read(cx).channel_id == channel_id.to_proto())
+                        .find(|b| b.read(cx).channel_id.0 == channel_id.to_proto())
                     {
                         let channel_buffer = channel_buffer.read(cx);
 

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

@@ -8,7 +8,8 @@ use anyhow::anyhow;
 use call::ActiveCall;
 use channel::{ChannelBuffer, ChannelStore};
 use client::{
-    self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
+    self, proto::PeerId, ChannelId, Client, Connection, Credentials, EstablishConnectionError,
+    UserStore,
 };
 use clock::FakeSystemClock;
 use collab_ui::channel_view::ChannelView;
@@ -120,7 +121,7 @@ impl TestServer {
     pub async fn start2(
         cx_a: &mut TestAppContext,
         cx_b: &mut TestAppContext,
-    ) -> (TestServer, TestClient, TestClient, u64) {
+    ) -> (TestServer, TestClient, TestClient, ChannelId) {
         let mut server = Self::start(cx_a.executor()).await;
         let client_a = server.create_client(cx_a, "user_a").await;
         let client_b = server.create_client(cx_b, "user_b").await;
@@ -353,10 +354,10 @@ impl TestServer {
     pub async fn make_channel(
         &self,
         channel: &str,
-        parent: Option<u64>,
+        parent: Option<ChannelId>,
         admin: (&TestClient, &mut TestAppContext),
         members: &mut [(&TestClient, &mut TestAppContext)],
-    ) -> u64 {
+    ) -> ChannelId {
         let (_, admin_cx) = admin;
         let channel_id = admin_cx
             .read(ChannelStore::global)
@@ -399,7 +400,7 @@ impl TestServer {
         channel: &str,
         client: &TestClient,
         cx: &mut TestAppContext,
-    ) -> u64 {
+    ) -> ChannelId {
         let channel_id = self
             .make_channel(channel, None, (client, cx), &mut [])
             .await;
@@ -423,7 +424,7 @@ impl TestServer {
         &self,
         channels: &[(&str, Option<&str>)],
         creator: (&TestClient, &mut TestAppContext),
-    ) -> Vec<u64> {
+    ) -> Vec<ChannelId> {
         let mut observed_channels = HashMap::default();
         let mut result = Vec::new();
         for (channel, parent) in channels {
@@ -677,7 +678,7 @@ impl TestClient {
     pub async fn host_workspace(
         &self,
         workspace: &View<Workspace>,
-        channel_id: u64,
+        channel_id: ChannelId,
         cx: &mut VisualTestContext,
     ) {
         cx.update(|cx| {
@@ -698,7 +699,7 @@ impl TestClient {
 
     pub async fn join_workspace<'a>(
         &'a self,
-        channel_id: u64,
+        channel_id: ChannelId,
         cx: &'a mut TestAppContext,
     ) -> (View<Workspace>, &'a mut VisualTestContext) {
         cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx))
@@ -777,7 +778,7 @@ impl TestClient {
 }
 
 pub fn open_channel_notes(
-    channel_id: u64,
+    channel_id: ChannelId,
     cx: &mut VisualTestContext,
 ) -> Task<anyhow::Result<View<ChannelView>>> {
     let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());

crates/collab_ui/src/channel_view.rs 🔗

@@ -1,9 +1,9 @@
 use anyhow::Result;
 use call::report_call_event_for_channel;
-use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
+use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelStore};
 use client::{
     proto::{self, PeerId},
-    Collaborator, ParticipantIndex,
+    ChannelId, Collaborator, ParticipantIndex,
 };
 use collections::HashMap;
 use editor::{
@@ -454,7 +454,7 @@ impl FollowableItem for ChannelView {
 
         Some(proto::view::Variant::ChannelView(
             proto::view::ChannelView {
-                channel_id: channel_buffer.channel_id,
+                channel_id: channel_buffer.channel_id.0,
                 editor: if let Some(proto::view::Variant::Editor(proto)) =
                     self.editor.read(cx).to_state_proto(cx)
                 {
@@ -480,7 +480,8 @@ impl FollowableItem for ChannelView {
             unreachable!()
         };
 
-        let open = ChannelView::open_in_pane(state.channel_id, None, pane, workspace, cx);
+        let open =
+            ChannelView::open_in_pane(ChannelId(state.channel_id), None, pane, workspace, cx);
 
         Some(cx.spawn(|mut cx| async move {
             let this = open.await?;

crates/collab_ui/src/chat_panel.rs 🔗

@@ -2,7 +2,7 @@ use crate::{collab_panel, ChatPanelSettings};
 use anyhow::Result;
 use call::{room, ActiveCall};
 use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, ChannelStore};
-use client::Client;
+use client::{ChannelId, Client};
 use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
@@ -169,7 +169,7 @@ impl ChatPanel {
         })
     }
 
-    pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
+    pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
         self.active_chat
             .as_ref()
             .map(|(chat, _)| chat.read(cx).channel_id)
@@ -710,7 +710,7 @@ impl ChatPanel {
 
     pub fn select_channel(
         &mut self,
-        selected_channel_id: u64,
+        selected_channel_id: ChannelId,
         scroll_to_message_id: Option<u64>,
         cx: &mut ViewContext<ChatPanel>,
     ) -> Task<Result<()>> {

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -1,6 +1,6 @@
 use anyhow::Result;
-use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
-use client::UserId;
+use channel::{ChannelMembership, ChannelStore, MessageParams};
+use client::{ChannelId, UserId};
 use collections::{HashMap, HashSet};
 use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
 use fuzzy::StringMatchCandidate;
@@ -131,7 +131,7 @@ impl MessageEditor {
 
     pub fn set_channel(
         &mut self,
-        channel_id: u64,
+        channel_id: ChannelId,
         channel_name: Option<SharedString>,
         cx: &mut ViewContext<Self>,
     ) {

crates/collab_ui/src/collab_panel.rs 🔗

@@ -7,8 +7,8 @@ use crate::{
     CollaborationPanelSettings,
 };
 use call::ActiveCall;
-use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
-use client::{Client, Contact, User, UserStore};
+use channel::{Channel, ChannelEvent, ChannelStore, HostedProjectId};
+use client::{ChannelId, Client, Contact, User, UserStore};
 use contact_finder::ContactFinder;
 use db::kvp::KEY_VALUE_STORE;
 use editor::{Editor, EditorElement, EditorStyle};
@@ -184,6 +184,10 @@ enum ListEntry {
     ChannelEditor {
         depth: usize,
     },
+    HostedProject {
+        id: HostedProjectId,
+        name: SharedString,
+    },
     Contact {
         contact: Arc<Contact>,
         calling: bool,
@@ -326,7 +330,10 @@ impl CollabPanel {
                     panel.width = serialized_panel.width;
                     panel.collapsed_channels = serialized_panel
                         .collapsed_channels
-                        .unwrap_or_else(|| Vec::new());
+                        .unwrap_or_else(|| Vec::new())
+                        .iter()
+                        .map(|cid| ChannelId(*cid))
+                        .collect();
                     cx.notify();
                 });
             }
@@ -344,7 +351,9 @@ impl CollabPanel {
                         COLLABORATION_PANEL_KEY.into(),
                         serde_json::to_string(&SerializedCollabPanel {
                             width,
-                            collapsed_channels: Some(collapsed_channels),
+                            collapsed_channels: Some(
+                                collapsed_channels.iter().map(|cid| cid.0).collect(),
+                            ),
                         })?,
                     )
                     .await?;
@@ -563,6 +572,7 @@ impl CollabPanel {
                     }
                 }
 
+                let hosted_projects = channel_store.projects_for_id(channel.id);
                 let has_children = channel_store
                     .channel_at_index(mat.candidate_id + 1)
                     .map_or(false, |next_channel| {
@@ -596,6 +606,10 @@ impl CollabPanel {
                         });
                     }
                 }
+
+                for (name, id) in hosted_projects {
+                    self.entries.push(ListEntry::HostedProject { id, name })
+                }
             }
         }
 
@@ -1023,6 +1037,33 @@ impl CollabPanel {
             .tooltip(move |cx| Tooltip::text("Open Chat", cx))
     }
 
+    fn render_channel_project(
+        &self,
+        id: HostedProjectId,
+        name: &SharedString,
+        is_selected: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        ListItem::new(ElementId::NamedInteger(
+            "channel-project".into(),
+            id.0 as usize,
+        ))
+        .indent_level(2)
+        .indent_step_size(px(20.))
+        .selected(is_selected)
+        .on_click(cx.listener(move |_this, _, _cx| {
+            // todo!()
+        }))
+        .start_slot(
+            h_flex()
+                .relative()
+                .gap_1()
+                .child(IconButton::new(0, IconName::FileTree)),
+        )
+        .child(Label::new(name.clone()))
+        .tooltip(move |cx| Tooltip::text("Open Project", cx))
+    }
+
     fn has_subchannels(&self, ix: usize) -> bool {
         self.entries.get(ix).map_or(false, |entry| {
             if let ListEntry::Channel { has_children, .. } = entry {
@@ -1486,6 +1527,12 @@ impl CollabPanel {
                     ListEntry::ChannelChat { channel_id } => {
                         self.join_channel_chat(*channel_id, cx)
                     }
+                    ListEntry::HostedProject {
+                        id: _id,
+                        name: _name,
+                    } => {
+                        // todo!()
+                    }
 
                     ListEntry::OutgoingRequest(_) => {}
                     ListEntry::ChannelEditor { .. } => {}
@@ -1923,7 +1970,7 @@ impl CollabPanel {
 
     fn respond_to_channel_invite(
         &mut self,
-        channel_id: u64,
+        channel_id: ChannelId,
         accept: bool,
         cx: &mut ViewContext<Self>,
     ) {
@@ -1942,7 +1989,7 @@ impl CollabPanel {
             .detach_and_prompt_err("Call failed", cx, |_, _| None);
     }
 
-    fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
+    fn join_channel(&self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
         let Some(workspace) = self.workspace.upgrade() else {
             return;
         };
@@ -2089,6 +2136,10 @@ impl CollabPanel {
             ListEntry::ChannelChat { channel_id } => self
                 .render_channel_chat(*channel_id, is_selected, cx)
                 .into_any_element(),
+
+            ListEntry::HostedProject { id, name } => self
+                .render_channel_project(*id, name, is_selected, cx)
+                .into_any_element(),
         }
     }
 
@@ -2405,7 +2456,7 @@ impl CollabPanel {
                 .tooltip(|cx| Tooltip::text("Accept invite", cx)),
         ];
 
-        ListItem::new(("channel-invite", channel.id as usize))
+        ListItem::new(("channel-invite", channel.id.0 as usize))
             .selected(is_selected)
             .child(
                 h_flex()
@@ -2497,7 +2548,7 @@ impl CollabPanel {
 
         div()
             .h_6()
-            .id(channel_id as usize)
+            .id(channel_id.0 as usize)
             .group("")
             .flex()
             .w_full()
@@ -2525,7 +2576,7 @@ impl CollabPanel {
                 this.move_channel(dragged_channel.id, channel_id, cx);
             }))
             .child(
-                ListItem::new(channel_id as usize)
+                ListItem::new(channel_id.0 as usize)
                     // Add one level of depth for the disclosure arrow.
                     .indent_level(depth + 1)
                     .indent_step_size(px(20.))
@@ -2572,7 +2623,7 @@ impl CollabPanel {
                     )
                     .child(
                         h_flex()
-                            .id(channel_id as usize)
+                            .id(channel_id.0 as usize)
                             .child(Label::new(channel.name.clone()))
                             .children(face_pile.map(|face_pile| face_pile.p_1())),
                     ),
@@ -2826,6 +2877,11 @@ impl PartialEq for ListEntry {
                     return channel_1.id == channel_2.id;
                 }
             }
+            ListEntry::HostedProject { id, .. } => {
+                if let ListEntry::HostedProject { id: other_id, .. } = other {
+                    return id == other_id;
+                }
+            }
             ListEntry::ChannelNotes { channel_id } => {
                 if let ListEntry::ChannelNotes {
                     channel_id: other_id,

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

@@ -1,7 +1,7 @@
-use channel::{ChannelId, ChannelMembership, ChannelStore};
+use channel::{ChannelMembership, ChannelStore};
 use client::{
     proto::{self, ChannelRole, ChannelVisibility},
-    User, UserId, UserStore,
+    ChannelId, User, UserId, UserStore,
 };
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{

crates/collab_ui/src/notification_panel.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{chat_panel::ChatPanel, NotificationPanelSettings};
 use anyhow::Result;
 use channel::ChannelStore;
-use client::{Client, Notification, User, UserStore};
+use client::{ChannelId, Client, Notification, User, UserStore};
 use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use futures::StreamExt;
@@ -357,7 +357,7 @@ impl NotificationPanel {
                         "{} invited you to join the #{channel_name} channel",
                         inviter.github_login
                     ),
-                    needs_response: channel_store.has_channel_invitation(channel_id),
+                    needs_response: channel_store.has_channel_invitation(ChannelId(channel_id)),
                     actor: Some(inviter),
                     can_navigate: false,
                 })
@@ -368,7 +368,7 @@ impl NotificationPanel {
                 message_id,
             } => {
                 let sender = user_store.get_cached_user(sender_id)?;
-                let channel = channel_store.channel_for_id(channel_id)?;
+                let channel = channel_store.channel_for_id(ChannelId(channel_id))?;
                 let message = self
                     .notification_store
                     .read(cx)
@@ -432,7 +432,7 @@ impl NotificationPanel {
                         if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
                             panel.update(cx, |panel, cx| {
                                 panel
-                                    .select_channel(channel_id, Some(message_id), cx)
+                                    .select_channel(ChannelId(channel_id), Some(message_id), cx)
                                     .detach_and_log_err(cx);
                             });
                         }
@@ -454,7 +454,7 @@ impl NotificationPanel {
                     panel.is_scrolled_to_bottom()
                         && panel
                             .active_chat()
-                            .map_or(false, |chat| chat.read(cx).channel_id == *channel_id)
+                            .map_or(false, |chat| chat.read(cx).channel_id.0 == *channel_id)
                 } else {
                     false
                 };

crates/notifications/src/notification_store.rs 🔗

@@ -1,6 +1,6 @@
 use anyhow::{Context, Result};
 use channel::{ChannelMessage, ChannelMessageId, ChannelStore};
-use client::{Client, UserStore};
+use client::{ChannelId, Client, UserStore};
 use collections::HashMap;
 use db::smol::stream::StreamExt;
 use gpui::{
@@ -413,7 +413,7 @@ impl NotificationStore {
             Notification::ChannelInvitation { channel_id, .. } => {
                 self.channel_store
                     .update(cx, |store, cx| {
-                        store.respond_to_channel_invite(channel_id, response, cx)
+                        store.respond_to_channel_invite(ChannelId(channel_id), response, cx)
                     })
                     .detach();
             }

crates/rpc/proto/zed.proto 🔗

@@ -12,7 +12,7 @@ message Envelope {
     uint32 id = 1;
     optional uint32 responding_to = 2;
     optional PeerId original_sender_id = 3;
-    
+
     /*
         When you are adding a new message type, instead of adding it in semantic order
         and bumping the message ID's of everything that follows, add it at the end of the
@@ -56,7 +56,7 @@ message Envelope {
         GetDefinitionResponse get_definition_response = 33;
         GetTypeDefinition get_type_definition = 34;
         GetTypeDefinitionResponse get_type_definition_response = 35;
-        
+
         GetReferences get_references = 36;
         GetReferencesResponse get_references_response = 37;
         GetDocumentHighlights get_document_highlights = 38;
@@ -192,7 +192,7 @@ message Envelope {
         LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155;
         SetRoomParticipantRole set_room_participant_role = 156;
 
-        UpdateUserChannels update_user_channels = 157; 
+        UpdateUserChannels update_user_channels = 157;
 
         GetImplementation get_implementation = 162;
         GetImplementationResponse get_implementation_response = 163;
@@ -1026,6 +1026,9 @@ message UpdateChannels {
     repeated ChannelParticipants channel_participants = 7;
     repeated ChannelMessageId latest_channel_message_ids = 8;
     repeated ChannelBufferVersion latest_channel_buffer_versions = 9;
+
+    repeated HostedProject hosted_projects = 10;
+    repeated uint64 deleted_hosted_projects = 11;
 }
 
 message UpdateUserChannels {
@@ -1054,6 +1057,13 @@ message ChannelParticipants {
     repeated uint64 participant_user_ids = 2;
 }
 
+message HostedProject {
+    uint64 id = 1;
+    uint64 channel_id = 2;
+    string name = 3;
+    ChannelVisibility visibility = 4;
+}
+
 message JoinChannel {
     uint64 channel_id = 1;
 }

crates/workspace/src/workspace.rs 🔗

@@ -15,7 +15,7 @@ use anyhow::{anyhow, Context as _, Result};
 use call::{call_settings::CallSettings, ActiveCall};
 use client::{
     proto::{self, ErrorCode, PeerId},
-    Client, ErrorExt, Status, TypedEnvelope, UserStore,
+    ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
 };
 use collections::{hash_map, HashMap, HashSet};
 use derive_more::{Deref, DerefMut};
@@ -4117,7 +4117,7 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
 actions!(collab, [OpenChannelNotes]);
 
 async fn join_channel_internal(
-    channel_id: u64,
+    channel_id: ChannelId,
     app_state: &Arc<AppState>,
     requesting_window: Option<WindowHandle<Workspace>>,
     active_call: &Model<ActiveCall>,
@@ -4257,7 +4257,7 @@ async fn join_channel_internal(
 }
 
 pub fn join_channel(
-    channel_id: u64,
+    channel_id: ChannelId,
     app_state: Arc<AppState>,
     requesting_window: Option<WindowHandle<Workspace>>,
     cx: &mut AppContext,

crates/zed/src/main.rs 🔗

@@ -323,8 +323,10 @@ fn main() {
                 cx.spawn(|cx| async move {
                     // ignore errors here, we'll show a generic "not signed in"
                     let _ = authenticate(client, &cx).await;
-                    cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))?
-                        .await?;
+                    cx.update(|cx| {
+                        workspace::join_channel(client::ChannelId(channel_id), app_state, None, cx)
+                    })?
+                    .await?;
                     anyhow::Ok(())
                 })
                 .detach_and_log_err(cx);
@@ -343,7 +345,7 @@ fn main() {
                         workspace::get_any_active_workspace(app_state, cx.clone()).await?;
                     let workspace = workspace_window.root_view(&cx)?;
                     cx.update_window(workspace_window.into(), |_, cx| {
-                        ChannelView::open(channel_id, heading, workspace, cx)
+                        ChannelView::open(client::ChannelId(channel_id), heading, workspace, cx)
                     })?
                     .await?;
                     anyhow::Ok(())
@@ -378,7 +380,12 @@ fn main() {
                         cx.update(|mut cx| {
                             cx.spawn(|cx| async move {
                                 cx.update(|cx| {
-                                    workspace::join_channel(channel_id, app_state, None, cx)
+                                    workspace::join_channel(
+                                        client::ChannelId(channel_id),
+                                        app_state,
+                                        None,
+                                        cx,
+                                    )
                                 })?
                                 .await?;
                                 anyhow::Ok(())
@@ -397,7 +404,12 @@ fn main() {
                                 workspace::get_any_active_workspace(app_state, cx.clone()).await?;
                             let workspace = workspace_window.root_view(&cx)?;
                             cx.update_window(workspace_window.into(), |_, cx| {
-                                ChannelView::open(channel_id, heading, workspace, cx)
+                                ChannelView::open(
+                                    client::ChannelId(channel_id),
+                                    heading,
+                                    workspace,
+                                    cx,
+                                )
                             })?
                             .await?;
                             anyhow::Ok(())