channel projects (#8456)

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
This commit is contained in:
Conrad Irwin 2024-02-26 22:15:11 -07:00 committed by GitHub
parent 8cf36ae603
commit c31626717f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 446 additions and 144 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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>,

View file

@ -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)]

View file

@ -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;

View file

@ -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,

View file

@ -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())
}
}

View file

@ -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;

View file

@ -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 {}

View file

@ -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
}

View file

@ -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())
}

View file

@ -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();

View file

@ -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()
}

View file

@ -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, _| {

View file

@ -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<()> {

View file

@ -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()
}

View file

@ -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);

View file

@ -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());