mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-24 17:28:40 +00:00
Start writing role to database (#3120)
Scaffolding for guest members in channels Release notes: - You can now set channels to "public" which will allow anyone to join and become a member. In a future release guests joining public channels will have reduced permissions.
This commit is contained in:
commit
cc390ba862
41 changed files with 2186 additions and 865 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1625,6 +1625,7 @@ dependencies = [
|
|||
"theme",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed-actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -10174,6 +10175,7 @@ name = "zed-actions"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
2
Procfile
2
Procfile
|
@ -1,4 +1,4 @@
|
|||
web: cd ../zed.dev && PORT=3000 npm run dev
|
||||
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
|
||||
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve
|
||||
livekit: livekit-server --dev
|
||||
postgrest: postgrest crates/collab/admin_api.conf
|
||||
|
|
3
assets/icons/link.svg
Normal file
3
assets/icons/link.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.51192 3.00541C9.18827 2.54594 10.0434 2.53694 10.6788 2.95419C10.823 3.04893 10.9771 3.1993 11.389 3.61119C11.8009 4.02307 11.9513 4.17714 12.046 4.32141C12.4632 4.95675 12.4542 5.81192 11.9948 6.48827C11.8899 6.64264 11.7276 6.80811 11.3006 7.23511L10.6819 7.85383C10.4866 8.04909 10.4866 8.36567 10.6819 8.56093C10.8772 8.7562 11.1937 8.7562 11.389 8.56093L12.0077 7.94221L12.0507 7.89929C12.4203 7.52976 12.6568 7.2933 12.822 7.0502C13.4972 6.05623 13.5321 4.76252 12.8819 3.77248C12.7233 3.53102 12.4922 3.30001 12.1408 2.94871L12.0961 2.90408L12.0515 2.85942C11.7002 2.508 11.4692 2.27689 11.2277 2.11832C10.2377 1.46813 8.94396 1.50299 7.94999 2.17822C7.70689 2.34336 7.47042 2.57991 7.10088 2.94955L7.05797 2.99247L6.43926 3.61119C6.24399 3.80645 6.24399 4.12303 6.43926 4.31829C6.63452 4.51355 6.9511 4.51355 7.14636 4.31829L7.76508 3.69957C8.19208 3.27257 8.35755 3.11027 8.51192 3.00541ZM4.31794 7.14672C4.5132 6.95146 4.5132 6.63487 4.31794 6.43961C4.12267 6.24435 3.80609 6.24435 3.61083 6.43961L2.99211 7.05833L2.9492 7.10124C2.57955 7.47077 2.34301 7.70724 2.17786 7.95035C1.50263 8.94432 1.46778 10.238 2.11797 11.2281C2.27654 11.4695 2.50764 11.7005 2.85908 12.0518L2.90372 12.0965L2.94835 12.1411C3.29965 12.4925 3.53066 12.7237 3.77212 12.8822C4.76217 13.5324 6.05587 13.4976 7.04984 12.8223C7.29294 12.6572 7.52941 12.4206 7.89894 12.051L7.89895 12.051L7.94186 12.0081L8.56058 11.3894C8.75584 11.1941 8.75584 10.8775 8.56058 10.6823C8.36531 10.487 8.04873 10.487 7.85347 10.6823L7.23475 11.301C6.80775 11.728 6.64228 11.8903 6.48792 11.9951C5.81156 12.4546 4.9564 12.4636 4.32105 12.0464C4.17679 11.9516 4.02272 11.8012 3.61083 11.3894C3.19894 10.9775 3.04858 10.8234 2.95383 10.6791C2.53659 10.0438 2.54558 9.18863 3.00505 8.51227C3.10991 8.35791 3.27222 8.19244 3.69922 7.76544L4.31794 7.14672ZM9.6217 6.08558C9.81696 5.89032 9.81696 5.57373 9.6217 5.37847C9.42644 5.18321 9.10986 5.18321 8.91459 5.37847L5.37906 8.91401C5.1838 9.10927 5.1838 9.42585 5.37906 9.62111C5.57432 9.81637 5.8909 9.81637 6.08617 9.62111L9.6217 6.08558Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
3
assets/icons/public.svg
Normal file
3
assets/icons/public.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.74393 2.00204C3.41963 1.97524 3.13502 2.37572 3.10823 2.70001C3.08143 3.0243 3.32558 3.47321 3.64986 3.50001C7.99878 3.85934 11.1406 7.00122 11.5 11.3501C11.5267 11.6744 11.9756 12.0269 12.3 12C12.6243 11.9733 13.0247 11.5804 12.998 11.2561C12.5912 6.33295 8.66704 2.40882 3.74393 2.00204ZM2.9 6.00001C2.96411 5.68099 3.33084 5.29361 3.64986 5.35772C6.66377 5.96341 9.03654 8.33618 9.64223 11.3501C9.70634 11.6691 9.319 12.0359 8.99999 12.1C8.68097 12.1641 8.06411 11.819 7.99999 11.5C7.48788 8.95167 6.0483 7.51213 3.49999 7.00001C3.18097 6.9359 2.8359 6.31902 2.9 6.00001ZM2 9.20001C2.0641 8.88099 2.38635 8.65788 2.70537 8.722C4.50255 9.08317 5.91684 10.4975 6.27801 12.2946C6.34212 12.6137 6.13547 12.9242 5.81646 12.9883C5.49744 13.0525 4.86411 12.819 4.8 12.5C4.53239 11.1683 3.83158 10.4676 2.5 10.2C2.18098 10.1359 1.93588 9.51902 2 9.20001Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1,021 B |
|
@ -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},
|
||||
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>,
|
||||
}
|
||||
|
@ -79,7 +80,32 @@ pub struct ChannelPath(Arc<[ChannelId]>);
|
|||
pub struct ChannelMembership {
|
||||
pub user: Arc<User>,
|
||||
pub kind: proto::channel_member::Kind,
|
||||
pub admin: bool,
|
||||
pub role: proto::ChannelRole,
|
||||
}
|
||||
impl ChannelMembership {
|
||||
pub fn sort_key(&self) -> MembershipSortKey {
|
||||
MembershipSortKey {
|
||||
role_order: match self.role {
|
||||
proto::ChannelRole::Admin => 0,
|
||||
proto::ChannelRole::Member => 1,
|
||||
proto::ChannelRole::Banned => 2,
|
||||
proto::ChannelRole::Guest => 3,
|
||||
},
|
||||
kind_order: match self.kind {
|
||||
proto::channel_member::Kind::Member => 0,
|
||||
proto::channel_member::Kind::AncestorMember => 1,
|
||||
proto::channel_member::Kind::Invitee => 2,
|
||||
},
|
||||
username_order: self.user.github_login.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, Ord, PartialEq, Eq)]
|
||||
pub struct MembershipSortKey<'a> {
|
||||
role_order: u8,
|
||||
kind_order: u8,
|
||||
username_order: &'a str,
|
||||
}
|
||||
|
||||
pub enum ChannelEvent {
|
||||
|
@ -445,7 +471,7 @@ impl ChannelStore {
|
|||
insert_edge: parent_edge,
|
||||
channel_permissions: vec![ChannelPermission {
|
||||
channel_id,
|
||||
is_admin: true,
|
||||
role: ChannelRole::Admin.into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -517,11 +543,30 @@ 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,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
role: proto::ChannelRole,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
|
@ -535,7 +580,7 @@ impl ChannelStore {
|
|||
.request(proto::InviteChannelMember {
|
||||
channel_id,
|
||||
user_id,
|
||||
admin,
|
||||
role: role.into(),
|
||||
})
|
||||
.await;
|
||||
|
||||
|
@ -579,11 +624,11 @@ impl ChannelStore {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn set_member_admin(
|
||||
pub fn set_member_role(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
role: proto::ChannelRole,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
|
@ -594,10 +639,10 @@ impl ChannelStore {
|
|||
let client = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::SetChannelMemberAdmin {
|
||||
.request(proto::SetChannelMemberRole {
|
||||
channel_id,
|
||||
user_id,
|
||||
admin,
|
||||
role: role.into(),
|
||||
})
|
||||
.await;
|
||||
|
||||
|
@ -685,8 +730,8 @@ impl ChannelStore {
|
|||
.filter_map(|(user, member)| {
|
||||
Some(ChannelMembership {
|
||||
user,
|
||||
admin: member.admin,
|
||||
kind: proto::channel_member::Kind::from_i32(member.kind)?,
|
||||
role: member.role(),
|
||||
kind: member.kind(),
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
|
@ -881,6 +926,7 @@ impl ChannelStore {
|
|||
ix,
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
visibility: channel.visibility(),
|
||||
name: channel.name,
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
|
@ -947,7 +993,7 @@ impl ChannelStore {
|
|||
}
|
||||
|
||||
for permission in payload.channel_permissions {
|
||||
if permission.is_admin {
|
||||
if permission.role() == proto::ChannelRole::Admin {
|
||||
self.channels_with_admin_privileges
|
||||
.insert(permission.channel_id);
|
||||
} else {
|
||||
|
|
|
@ -123,12 +123,15 @@ 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).name = channel_proto.name;
|
||||
let existing_channel = Arc::make_mut(existing_channel);
|
||||
existing_channel.visibility = channel_proto.visibility();
|
||||
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,
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent;
|
|||
use super::*;
|
||||
use client::{test::FakeServer, Client, UserStore};
|
||||
use gpui::{AppContext, ModelHandle, TestAppContext};
|
||||
use rpc::proto;
|
||||
use rpc::proto::{self};
|
||||
use settings::SettingsStore;
|
||||
use util::http::FakeHttpClient;
|
||||
|
||||
|
@ -18,15 +18,17 @@ fn test_update_channels(cx: &mut AppContext) {
|
|||
proto::Channel {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "a".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
],
|
||||
channel_permissions: vec![proto::ChannelPermission {
|
||||
channel_id: 1,
|
||||
is_admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -49,10 +51,12 @@ fn test_update_channels(cx: &mut AppContext) {
|
|||
proto::Channel {
|
||||
id: 3,
|
||||
name: "x".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 4,
|
||||
name: "y".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
],
|
||||
insert_edge: vec![
|
||||
|
@ -92,14 +96,17 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
|||
proto::Channel {
|
||||
id: 0,
|
||||
name: "a".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "c".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
],
|
||||
insert_edge: vec![
|
||||
|
@ -114,7 +121,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
|||
],
|
||||
channel_permissions: vec![proto::ChannelPermission {
|
||||
channel_id: 0,
|
||||
is_admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -158,6 +165,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
|||
channels: vec![proto::Channel {
|
||||
id: channel_id,
|
||||
name: "the-channel".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
|
|
|
@ -44,7 +44,7 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
|
|||
|
||||
CREATE TABLE "projects" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"room_id" INTEGER REFERENCES rooms (id) NOT NULL,
|
||||
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
|
||||
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"host_connection_id" INTEGER,
|
||||
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
|
||||
|
@ -192,7 +192,8 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
|
|||
CREATE TABLE "channels" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"name" VARCHAR NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now,
|
||||
"visibility" VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
|
||||
|
@ -226,6 +227,7 @@ CREATE TABLE "channel_members" (
|
|||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
"admin" BOOLEAN NOT NULL DEFAULT false,
|
||||
"role" VARCHAR,
|
||||
"accepted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now
|
||||
);
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
ALTER TABLE channel_members ADD COLUMN role TEXT;
|
||||
UPDATE channel_members SET role = CASE WHEN admin THEN 'admin' ELSE 'member' END;
|
||||
|
||||
ALTER TABLE channels ADD COLUMN visibility TEXT NOT NULL DEFAULT 'members';
|
|
@ -0,0 +1,8 @@
|
|||
-- Add migration script here
|
||||
|
||||
ALTER TABLE projects
|
||||
DROP CONSTRAINT projects_room_id_fkey,
|
||||
ADD CONSTRAINT projects_room_id_fkey
|
||||
FOREIGN KEY (room_id)
|
||||
REFERENCES rooms (id)
|
||||
ON DELETE CASCADE;
|
|
@ -432,6 +432,7 @@ pub struct NewUserResult {
|
|||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub visibility: ChannelVisibility,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::Result;
|
||||
use rpc::proto;
|
||||
use sea_orm::{entity::prelude::*, DbErr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -80,3 +81,101 @@ id_type!(SignupId);
|
|||
id_type!(UserId);
|
||||
id_type!(ChannelBufferCollaboratorId);
|
||||
id_type!(FlagId);
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(None)")]
|
||||
pub enum ChannelRole {
|
||||
#[sea_orm(string_value = "admin")]
|
||||
Admin,
|
||||
#[sea_orm(string_value = "member")]
|
||||
#[default]
|
||||
Member,
|
||||
#[sea_orm(string_value = "guest")]
|
||||
Guest,
|
||||
#[sea_orm(string_value = "banned")]
|
||||
Banned,
|
||||
}
|
||||
|
||||
impl ChannelRole {
|
||||
pub fn should_override(&self, other: Self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin => matches!(other, Member | Banned | Guest),
|
||||
Member => matches!(other, Banned | Guest),
|
||||
Banned => matches!(other, Guest),
|
||||
Guest => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max(&self, other: Self) -> Self {
|
||||
if self.should_override(other) {
|
||||
*self
|
||||
} else {
|
||||
other
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::ChannelRole> for ChannelRole {
|
||||
fn from(value: proto::ChannelRole) -> Self {
|
||||
match value {
|
||||
proto::ChannelRole::Admin => ChannelRole::Admin,
|
||||
proto::ChannelRole::Member => ChannelRole::Member,
|
||||
proto::ChannelRole::Guest => ChannelRole::Guest,
|
||||
proto::ChannelRole::Banned => ChannelRole::Banned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<proto::ChannelRole> for ChannelRole {
|
||||
fn into(self) -> proto::ChannelRole {
|
||||
match self {
|
||||
ChannelRole::Admin => proto::ChannelRole::Admin,
|
||||
ChannelRole::Member => proto::ChannelRole::Member,
|
||||
ChannelRole::Guest => proto::ChannelRole::Guest,
|
||||
ChannelRole::Banned => proto::ChannelRole::Banned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<i32> for ChannelRole {
|
||||
fn into(self) -> i32 {
|
||||
let proto: proto::ChannelRole = self.into();
|
||||
proto.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[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")]
|
||||
Public,
|
||||
#[sea_orm(string_value = "members")]
|
||||
#[default]
|
||||
Members,
|
||||
}
|
||||
|
||||
impl From<proto::ChannelVisibility> for ChannelVisibility {
|
||||
fn from(value: proto::ChannelVisibility) -> Self {
|
||||
match value {
|
||||
proto::ChannelVisibility::Public => ChannelVisibility::Public,
|
||||
proto::ChannelVisibility::Members => ChannelVisibility::Members,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<proto::ChannelVisibility> for ChannelVisibility {
|
||||
fn into(self) -> proto::ChannelVisibility {
|
||||
match self {
|
||||
ChannelVisibility::Public => proto::ChannelVisibility::Public,
|
||||
ChannelVisibility::Members => proto::ChannelVisibility::Members,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<i32> for ChannelVisibility {
|
||||
fn into(self) -> i32 {
|
||||
let proto: proto::ChannelVisibility = self.into();
|
||||
proto.into()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -482,7 +482,9 @@ impl Database {
|
|||
)
|
||||
.await?;
|
||||
|
||||
channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
|
||||
channel_members = self
|
||||
.get_channel_participants_internal(channel_id, &*tx)
|
||||
.await?;
|
||||
let collaborators = self
|
||||
.get_channel_buffer_collaborators_internal(channel_id, &*tx)
|
||||
.await?;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -180,7 +180,9 @@ impl Database {
|
|||
)
|
||||
.await?;
|
||||
|
||||
let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
|
||||
let mut channel_members = self
|
||||
.get_channel_participants_internal(channel_id, &*tx)
|
||||
.await?;
|
||||
channel_members.retain(|member| !participant_user_ids.contains(member));
|
||||
|
||||
Ok((
|
||||
|
|
|
@ -53,7 +53,9 @@ impl Database {
|
|||
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
||||
let channel_members;
|
||||
if let Some(channel_id) = channel_id {
|
||||
channel_members = self.get_channel_members_internal(channel_id, &tx).await?;
|
||||
channel_members = self
|
||||
.get_channel_participants_internal(channel_id, &tx)
|
||||
.await?;
|
||||
} else {
|
||||
channel_members = Vec::new();
|
||||
|
||||
|
@ -298,98 +300,139 @@ impl Database {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryParticipantIndices {
|
||||
ParticipantIndex,
|
||||
if channel_id.is_some() {
|
||||
Err(anyhow!("tried to join channel call directly"))?
|
||||
}
|
||||
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
|
||||
.filter(
|
||||
room_participant::Column::RoomId
|
||||
.eq(room_id)
|
||||
.and(room_participant::Column::ParticipantIndex.is_not_null()),
|
||||
)
|
||||
.select_only()
|
||||
.column(room_participant::Column::ParticipantIndex)
|
||||
.into_values::<_, QueryParticipantIndices>()
|
||||
.all(&*tx)
|
||||
|
||||
let participant_index = self
|
||||
.get_next_participant_index_internal(room_id, &*tx)
|
||||
.await?;
|
||||
|
||||
let mut participant_index = 0;
|
||||
while existing_participant_indices.contains(&participant_index) {
|
||||
participant_index += 1;
|
||||
}
|
||||
|
||||
if let Some(channel_id) = channel_id {
|
||||
self.check_user_is_channel_member(channel_id, user_id, &*tx)
|
||||
.await?;
|
||||
|
||||
room_participant::Entity::insert_many([room_participant::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
user_id: ActiveValue::set(user_id),
|
||||
let result = room_participant::Entity::update_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(room_participant::Column::RoomId.eq(room_id))
|
||||
.add(room_participant::Column::UserId.eq(user_id))
|
||||
.add(room_participant::Column::AnsweringConnectionId.is_null()),
|
||||
)
|
||||
.set(room_participant::ActiveModel {
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
answering_connection_lost: ActiveValue::set(false),
|
||||
calling_user_id: ActiveValue::set(user_id),
|
||||
calling_connection_id: ActiveValue::set(connection.id as i32),
|
||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
..Default::default()
|
||||
}])
|
||||
.on_conflict(
|
||||
OnConflict::columns([room_participant::Column::UserId])
|
||||
.update_columns([
|
||||
room_participant::Column::AnsweringConnectionId,
|
||||
room_participant::Column::AnsweringConnectionServerId,
|
||||
room_participant::Column::AnsweringConnectionLost,
|
||||
room_participant::Column::ParticipantIndex,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
} else {
|
||||
let result = room_participant::Entity::update_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(room_participant::Column::RoomId.eq(room_id))
|
||||
.add(room_participant::Column::UserId.eq(user_id))
|
||||
.add(room_participant::Column::AnsweringConnectionId.is_null()),
|
||||
)
|
||||
.set(room_participant::ActiveModel {
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
answering_connection_lost: ActiveValue::set(false),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
if result.rows_affected == 0 {
|
||||
Err(anyhow!("room does not exist or was already joined"))?;
|
||||
}
|
||||
if result.rows_affected == 0 {
|
||||
Err(anyhow!("room does not exist or was already joined"))?;
|
||||
}
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
let channel_members = if let Some(channel_id) = channel_id {
|
||||
self.get_channel_members_internal(channel_id, &tx).await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Ok(JoinRoom {
|
||||
room,
|
||||
channel_id,
|
||||
channel_members,
|
||||
channel_id: None,
|
||||
channel_members: vec![],
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_next_participant_index_internal(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<i32> {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryParticipantIndices {
|
||||
ParticipantIndex,
|
||||
}
|
||||
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
|
||||
.filter(
|
||||
room_participant::Column::RoomId
|
||||
.eq(room_id)
|
||||
.and(room_participant::Column::ParticipantIndex.is_not_null()),
|
||||
)
|
||||
.select_only()
|
||||
.column(room_participant::Column::ParticipantIndex)
|
||||
.into_values::<_, QueryParticipantIndices>()
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut participant_index = 0;
|
||||
while existing_participant_indices.contains(&participant_index) {
|
||||
participant_index += 1;
|
||||
}
|
||||
|
||||
Ok(participant_index)
|
||||
}
|
||||
|
||||
pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result<Option<ChannelId>> {
|
||||
self.transaction(|tx| async move {
|
||||
let room: Option<room::Model> = room::Entity::find()
|
||||
.filter(room::Column::Id.eq(room_id))
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(room.and_then(|room| room.channel_id))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn join_channel_room_internal(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<JoinRoom> {
|
||||
let participant_index = self
|
||||
.get_next_participant_index_internal(room_id, &*tx)
|
||||
.await?;
|
||||
|
||||
room_participant::Entity::insert_many([room_participant::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
user_id: ActiveValue::set(user_id),
|
||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
answering_connection_lost: ActiveValue::set(false),
|
||||
calling_user_id: ActiveValue::set(user_id),
|
||||
calling_connection_id: ActiveValue::set(connection.id as i32),
|
||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
..Default::default()
|
||||
}])
|
||||
.on_conflict(
|
||||
OnConflict::columns([room_participant::Column::UserId])
|
||||
.update_columns([
|
||||
room_participant::Column::AnsweringConnectionId,
|
||||
room_participant::Column::AnsweringConnectionServerId,
|
||||
room_participant::Column::AnsweringConnectionLost,
|
||||
room_participant::Column::ParticipantIndex,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
let channel_members = self
|
||||
.get_channel_participants_internal(channel_id, &tx)
|
||||
.await?;
|
||||
Ok(JoinRoom {
|
||||
room,
|
||||
channel_id: Some(channel_id),
|
||||
channel_members,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn rejoin_room(
|
||||
&self,
|
||||
rejoin_room: proto::RejoinRoom,
|
||||
|
@ -681,7 +724,8 @@ impl Database {
|
|||
|
||||
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
||||
let channel_members = if let Some(channel_id) = channel_id {
|
||||
self.get_channel_members_internal(channel_id, &tx).await?
|
||||
self.get_channel_participants_internal(channel_id, &tx)
|
||||
.await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
@ -839,7 +883,8 @@ impl Database {
|
|||
};
|
||||
|
||||
let channel_members = if let Some(channel_id) = channel_id {
|
||||
self.get_channel_members_internal(channel_id, &tx).await?
|
||||
self.get_channel_participants_internal(channel_id, &tx)
|
||||
.await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::db::ChannelId;
|
||||
use crate::db::{ChannelId, ChannelVisibility};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
||||
|
@ -7,6 +7,7 @@ pub struct Model {
|
|||
#[sea_orm(primary_key)]
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub visibility: ChannelVisibility,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::db::{channel_member, ChannelId, ChannelMemberId, UserId};
|
||||
use crate::db::{channel_member, ChannelId, ChannelMemberId, ChannelRole, UserId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "channel_members")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
|
@ -9,7 +9,7 @@ pub struct Model {
|
|||
pub channel_id: ChannelId,
|
||||
pub user_id: UserId,
|
||||
pub accepted: bool,
|
||||
pub admin: bool,
|
||||
pub role: ChannelRole,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
|
|||
|
||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
db.invite_channel_member(zed_id, b_id, a_id, false)
|
||||
db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -211,7 +211,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(channel, observer_id, user_id, false)
|
||||
db.invite_channel_member(channel, observer_id, user_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.respond_to_channel_invite(channel, observer_id, true)
|
||||
|
|
|
@ -8,11 +8,14 @@ use crate::{
|
|||
db::{
|
||||
queries::channels::ChannelGraph,
|
||||
tests::{graph, TEST_RELEASE_CHANNEL},
|
||||
ChannelId, Database, NewUserParams,
|
||||
ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
|
||||
},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{
|
||||
atomic::{AtomicI32, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
|
||||
|
||||
|
@ -48,9 +51,9 @@ async fn test_channels(db: &Arc<Database>) {
|
|||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
// Make sure that people cannot read channels they haven't been invited to
|
||||
assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(zed_id, b_id).await.is_err());
|
||||
|
||||
db.invite_channel_member(zed_id, b_id, a_id, false)
|
||||
db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -125,9 +128,13 @@ async fn test_channels(db: &Arc<Database>) {
|
|||
);
|
||||
|
||||
// Update member permissions
|
||||
let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
|
||||
let set_subchannel_admin = db
|
||||
.set_channel_member_role(crdb_id, a_id, b_id, ChannelRole::Admin)
|
||||
.await;
|
||||
assert!(set_subchannel_admin.is_err());
|
||||
let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
|
||||
let set_channel_admin = db
|
||||
.set_channel_member_role(zed_id, a_id, b_id, ChannelRole::Admin)
|
||||
.await;
|
||||
assert!(set_channel_admin.is_ok());
|
||||
|
||||
let result = db.get_channels_for_user(b_id).await.unwrap();
|
||||
|
@ -150,7 +157,7 @@ async fn test_channels(db: &Arc<Database>) {
|
|||
|
||||
// Remove a single channel
|
||||
db.delete_channel(crdb_id, a_id).await.unwrap();
|
||||
assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(crdb_id, a_id).await.is_err());
|
||||
|
||||
// Remove a channel tree
|
||||
let (mut channel_ids, user_ids) = db.delete_channel(rust_id, a_id).await.unwrap();
|
||||
|
@ -158,9 +165,9 @@ async fn test_channels(db: &Arc<Database>) {
|
|||
assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
|
||||
assert_eq!(user_ids, &[a_id]);
|
||||
|
||||
assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(rust_id, a_id).await.is_err());
|
||||
assert!(db.get_channel(cargo_id, a_id).await.is_err());
|
||||
assert!(db.get_channel(cargo_ra_id, a_id).await.is_err());
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
|
@ -200,15 +207,11 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
|||
.user_id;
|
||||
|
||||
let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
||||
let room_1 = db
|
||||
.get_or_create_channel_room(channel_1, "1", TEST_RELEASE_CHANNEL)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// can join a room with membership to its channel
|
||||
let joined_room = db
|
||||
.join_room(
|
||||
room_1,
|
||||
let (joined_room, _) = db
|
||||
.join_channel(
|
||||
channel_1,
|
||||
user_1,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
TEST_RELEASE_CHANNEL,
|
||||
|
@ -217,11 +220,12 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
|||
.unwrap();
|
||||
assert_eq!(joined_room.room.participants.len(), 1);
|
||||
|
||||
let room_id = RoomId::from_proto(joined_room.room.id);
|
||||
drop(joined_room);
|
||||
// cannot join a room without membership to its channel
|
||||
assert!(db
|
||||
.join_room(
|
||||
room_1,
|
||||
room_id,
|
||||
user_2,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
TEST_RELEASE_CHANNEL
|
||||
|
@ -239,58 +243,21 @@ test_both_dbs!(
|
|||
async fn test_channel_invites(db: &Arc<Database>) {
|
||||
db.create_server("test").await.unwrap();
|
||||
|
||||
let user_1 = db
|
||||
.create_user(
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user1".into(),
|
||||
github_user_id: 5,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let user_2 = db
|
||||
.create_user(
|
||||
"user2@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user2".into(),
|
||||
github_user_id: 6,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let user_3 = db
|
||||
.create_user(
|
||||
"user3@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user3".into(),
|
||||
github_user_id: 7,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let user_1 = new_test_user(db, "user1@example.com").await;
|
||||
let user_2 = new_test_user(db, "user2@example.com").await;
|
||||
let user_3 = new_test_user(db, "user3@example.com").await;
|
||||
|
||||
let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
||||
|
||||
let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_1_1, user_2, user_1, false)
|
||||
db.invite_channel_member(channel_1_1, user_2, user_1, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(channel_1_2, user_2, user_1, false)
|
||||
db.invite_channel_member(channel_1_2, user_2, user_1, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(channel_1_1, user_3, user_1, true)
|
||||
db.invite_channel_member(channel_1_1, user_3, user_1, ChannelRole::Admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -314,27 +281,29 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
|||
|
||||
assert_eq!(user_3_invites, &[channel_1_1]);
|
||||
|
||||
let members = db
|
||||
.get_channel_member_details(channel_1_1, user_1)
|
||||
let mut members = db
|
||||
.get_channel_participant_details(channel_1_1, user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: user_1.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_2.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
admin: false,
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_3.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
@ -349,7 +318,7 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
|||
.unwrap();
|
||||
|
||||
let members = db
|
||||
.get_channel_member_details(channel_1_3, user_1)
|
||||
.get_channel_participant_details(channel_1_3, user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -358,12 +327,12 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
|||
proto::ChannelMember {
|
||||
user_id: user_1.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_2.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
admin: false,
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
@ -414,11 +383,7 @@ async fn test_channel_renames(db: &Arc<Database>) {
|
|||
|
||||
let zed_archive_id = zed_id;
|
||||
|
||||
let (channel, _) = db
|
||||
.get_channel(zed_archive_id, user_1)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let channel = db.get_channel(zed_archive_id, user_1).await.unwrap();
|
||||
assert_eq!(channel.name, "zed-archive");
|
||||
|
||||
let non_permissioned_rename = db
|
||||
|
@ -846,6 +811,284 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
|||
);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_user_is_channel_participant,
|
||||
test_user_is_channel_participant_postgres,
|
||||
test_user_is_channel_participant_sqlite
|
||||
);
|
||||
|
||||
async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
let admin = new_test_user(db, "admin@example.com").await;
|
||||
let member = new_test_user(db, "member@example.com").await;
|
||||
let guest = new_test_user(db, "guest@example.com").await;
|
||||
|
||||
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
|
||||
let active_channel = db
|
||||
.create_channel("active", Some(zed_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
let vim_channel = db
|
||||
.create_channel("vim", Some(active_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(active_channel, member, admin, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(vim_channel, guest, admin, ChannelRole::Guest)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.respond_to_channel_invite(active_channel, member, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, admin, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, member, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
role: proto::ChannelRole::Guest.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.respond_to_channel_invite(vim_channel, guest, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
|
||||
assert_dag(channels, &[(vim_channel, None)]);
|
||||
let channels = db.get_channels_for_user(member).await.unwrap().channels;
|
||||
assert_dag(
|
||||
channels,
|
||||
&[(active_channel, None), (vim_channel, Some(active_channel))],
|
||||
);
|
||||
|
||||
db.set_channel_member_role(vim_channel, admin, guest, ChannelRole::Banned)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(db
|
||||
.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Banned.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.remove_channel_member(vim_channel, guest, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// currently people invited to parent channels are not shown here
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.respond_to_channel_invite(zed_channel, guest, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(zed_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(db
|
||||
.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(active_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.is_err(),);
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Guest.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
|
||||
assert_dag(
|
||||
channels,
|
||||
&[(zed_channel, None), (vim_channel, Some(zed_channel))],
|
||||
)
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_user_joins_correct_channel,
|
||||
test_user_joins_correct_channel_postgres,
|
||||
test_user_joins_correct_channel_sqlite
|
||||
);
|
||||
|
||||
async fn test_user_joins_correct_channel(db: &Arc<Database>) {
|
||||
let admin = new_test_user(db, "admin@example.com").await;
|
||||
|
||||
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
|
||||
|
||||
let active_channel = db
|
||||
.create_channel("active", Some(zed_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let vim_channel = db
|
||||
.create_channel("vim", Some(active_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let vim2_channel = db
|
||||
.create_channel("vim2", Some(vim_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim2_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let most_public = db
|
||||
.transaction(
|
||||
|tx| async move { db.most_public_ancestor_for_channel(vim_channel, &*tx).await },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(most_public, Some(zed_channel))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
|
||||
let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
|
||||
|
@ -870,3 +1113,20 @@ fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)])
|
|||
|
||||
pretty_assertions::assert_eq!(actual_map, expected_map)
|
||||
}
|
||||
|
||||
static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
|
||||
|
||||
async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
|
||||
db.create_user(
|
||||
email,
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: email[0..email.find("@").unwrap()].to_string(),
|
||||
github_user_id: GITHUB_USER_ID.fetch_add(1, Ordering::SeqCst),
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
db::{Database, MessageId, NewUserParams},
|
||||
db::{ChannelRole, Database, MessageId, NewUserParams},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
@ -155,7 +155,7 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
|
|||
|
||||
let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_1, observer, user, false)
|
||||
db.invite_channel_member(channel_1, observer, user, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -163,7 +163,7 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_2, observer, user, false)
|
||||
db.invite_channel_member(channel_2, observer, user, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
@ -255,7 +255,8 @@ impl Server {
|
|||
.add_request_handler(delete_channel)
|
||||
.add_request_handler(invite_channel_member)
|
||||
.add_request_handler(remove_channel_member)
|
||||
.add_request_handler(set_channel_member_admin)
|
||||
.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)
|
||||
|
@ -977,6 +978,13 @@ async fn join_room(
|
|||
session: Session,
|
||||
) -> Result<()> {
|
||||
let room_id = RoomId::from_proto(request.id);
|
||||
|
||||
let channel_id = session.db().await.channel_id_for_room(room_id).await?;
|
||||
|
||||
if let Some(channel_id) = channel_id {
|
||||
return join_channel_internal(channel_id, Box::new(response), session).await;
|
||||
}
|
||||
|
||||
let joined_room = {
|
||||
let room = session
|
||||
.db()
|
||||
|
@ -992,16 +1000,6 @@ async fn join_room(
|
|||
room.into_inner()
|
||||
};
|
||||
|
||||
if let Some(channel_id) = joined_room.channel_id {
|
||||
channel_updated(
|
||||
channel_id,
|
||||
&joined_room.room,
|
||||
&joined_room.channel_members,
|
||||
&session.peer,
|
||||
&*session.connection_pool().await,
|
||||
)
|
||||
}
|
||||
|
||||
for connection_id in session
|
||||
.connection_pool()
|
||||
.await
|
||||
|
@ -1039,7 +1037,7 @@ async fn join_room(
|
|||
|
||||
response.send(proto::JoinRoomResponse {
|
||||
room: Some(joined_room.room),
|
||||
channel_id: joined_room.channel_id.map(|id| id.to_proto()),
|
||||
channel_id: None,
|
||||
live_kit_connection_info,
|
||||
})?;
|
||||
|
||||
|
@ -2211,6 +2209,7 @@ async fn create_channel(
|
|||
let channel = proto::Channel {
|
||||
id: id.to_proto(),
|
||||
name: request.name,
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
};
|
||||
|
||||
response.send(proto::CreateChannelResponse {
|
||||
|
@ -2283,17 +2282,20 @@ async fn invite_channel_member(
|
|||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let invitee_id = UserId::from_proto(request.user_id);
|
||||
db.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin)
|
||||
.await?;
|
||||
db.invite_channel_member(
|
||||
channel_id,
|
||||
invitee_id,
|
||||
session.user_id,
|
||||
request.role().into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (channel, _) = db
|
||||
.get_channel(channel_id, session.user_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("channel not found"))?;
|
||||
let channel = db.get_channel(channel_id, session.user_id).await?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update.channel_invitations.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
visibility: channel.visibility.into(),
|
||||
name: channel.name,
|
||||
});
|
||||
for connection_id in session
|
||||
|
@ -2335,27 +2337,63 @@ async fn remove_channel_member(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_channel_member_admin(
|
||||
request: proto::SetChannelMemberAdmin,
|
||||
response: Response<proto::SetChannelMemberAdmin>,
|
||||
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>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let member_id = UserId::from_proto(request.user_id);
|
||||
db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin)
|
||||
let channel_member = db
|
||||
.set_channel_member_role(
|
||||
channel_id,
|
||||
session.user_id,
|
||||
member_id,
|
||||
request.role().into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (channel, has_accepted) = db
|
||||
.get_channel(channel_id, member_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("channel not found"))?;
|
||||
let channel = db.get_channel(channel_id, session.user_id).await?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
if has_accepted {
|
||||
if channel_member.accepted {
|
||||
update.channel_permissions.push(proto::ChannelPermission {
|
||||
channel_id: channel.id.to_proto(),
|
||||
is_admin: request.admin,
|
||||
role: request.role,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2378,13 +2416,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,
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
visibility: channel.visibility.into(),
|
||||
};
|
||||
response.send(proto::RenameChannelResponse {
|
||||
channel: Some(channel.clone()),
|
||||
|
@ -2422,6 +2461,7 @@ async fn link_channel(
|
|||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
visibility: channel.visibility.into(),
|
||||
name: channel.name,
|
||||
})
|
||||
.collect(),
|
||||
|
@ -2513,6 +2553,7 @@ async fn move_channel(
|
|||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
visibility: channel.visibility.into(),
|
||||
name: channel.name,
|
||||
})
|
||||
.collect(),
|
||||
|
@ -2538,7 +2579,7 @@ async fn get_channel_members(
|
|||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let members = db
|
||||
.get_channel_member_details(channel_id, session.user_id)
|
||||
.get_channel_participant_details(channel_id, session.user_id)
|
||||
.await?;
|
||||
response.send(proto::GetChannelMembersResponse { members })?;
|
||||
Ok(())
|
||||
|
@ -2554,53 +2595,68 @@ async fn respond_to_channel_invite(
|
|||
db.respond_to_channel_invite(channel_id, session.user_id, request.accept)
|
||||
.await?;
|
||||
|
||||
if request.accept {
|
||||
channel_membership_updated(db, channel_id, &session).await?;
|
||||
} else {
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update
|
||||
.remove_channel_invitations
|
||||
.push(channel_id.to_proto());
|
||||
session.peer.send(session.connection_id, update)?;
|
||||
}
|
||||
response.send(proto::Ack {})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn channel_membership_updated(
|
||||
db: tokio::sync::MutexGuard<'_, DbHandle>,
|
||||
channel_id: ChannelId,
|
||||
session: &Session,
|
||||
) -> Result<(), crate::Error> {
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update
|
||||
.remove_channel_invitations
|
||||
.push(channel_id.to_proto());
|
||||
if request.accept {
|
||||
let result = db.get_channel_for_user(channel_id, session.user_id).await?;
|
||||
update
|
||||
.channels
|
||||
.extend(
|
||||
result
|
||||
.channels
|
||||
.channels
|
||||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
}),
|
||||
);
|
||||
update.unseen_channel_messages = result.channel_messages;
|
||||
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
|
||||
update.insert_edge = result.channels.edges;
|
||||
update
|
||||
.channel_participants
|
||||
.extend(
|
||||
result
|
||||
.channel_participants
|
||||
.into_iter()
|
||||
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
|
||||
channel_id: channel_id.to_proto(),
|
||||
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
|
||||
}),
|
||||
);
|
||||
update
|
||||
.channel_permissions
|
||||
.extend(
|
||||
result
|
||||
.channels_with_admin_privileges
|
||||
.into_iter()
|
||||
.map(|channel_id| proto::ChannelPermission {
|
||||
channel_id: channel_id.to_proto(),
|
||||
is_admin: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
session.peer.send(session.connection_id, update)?;
|
||||
response.send(proto::Ack {})?;
|
||||
|
||||
let result = db.get_channel_for_user(channel_id, session.user_id).await?;
|
||||
update.channels.extend(
|
||||
result
|
||||
.channels
|
||||
.channels
|
||||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
visibility: channel.visibility.into(),
|
||||
name: channel.name,
|
||||
}),
|
||||
);
|
||||
update.unseen_channel_messages = result.channel_messages;
|
||||
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
|
||||
update.insert_edge = result.channels.edges;
|
||||
update
|
||||
.channel_participants
|
||||
.extend(
|
||||
result
|
||||
.channel_participants
|
||||
.into_iter()
|
||||
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
|
||||
channel_id: channel_id.to_proto(),
|
||||
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
|
||||
}),
|
||||
);
|
||||
update
|
||||
.channel_permissions
|
||||
.extend(
|
||||
result
|
||||
.channels_with_admin_privileges
|
||||
.into_iter()
|
||||
.map(|channel_id| proto::ChannelPermission {
|
||||
channel_id: channel_id.to_proto(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
}),
|
||||
);
|
||||
session.peer.send(session.connection_id, update)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -2610,19 +2666,35 @@ async fn join_channel(
|
|||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||
join_channel_internal(channel_id, Box::new(response), session).await
|
||||
}
|
||||
|
||||
trait JoinChannelInternalResponse {
|
||||
fn send(self, result: proto::JoinRoomResponse) -> Result<()>;
|
||||
}
|
||||
impl JoinChannelInternalResponse for Response<proto::JoinChannel> {
|
||||
fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
|
||||
Response::<proto::JoinChannel>::send(self, result)
|
||||
}
|
||||
}
|
||||
impl JoinChannelInternalResponse for Response<proto::JoinRoom> {
|
||||
fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
|
||||
Response::<proto::JoinRoom>::send(self, result)
|
||||
}
|
||||
}
|
||||
|
||||
async fn join_channel_internal(
|
||||
channel_id: ChannelId,
|
||||
response: Box<impl JoinChannelInternalResponse>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let joined_room = {
|
||||
leave_room_for_session(&session).await?;
|
||||
let db = session.db().await;
|
||||
|
||||
let room_id = db
|
||||
.get_or_create_channel_room(channel_id, &live_kit_room, &*RELEASE_CHANNEL_NAME)
|
||||
.await?;
|
||||
|
||||
let joined_room = db
|
||||
.join_room(
|
||||
room_id,
|
||||
let (joined_room, joined_channel) = db
|
||||
.join_channel(
|
||||
channel_id,
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
RELEASE_CHANNEL_NAME.as_str(),
|
||||
|
@ -2649,9 +2721,13 @@ async fn join_channel(
|
|||
live_kit_connection_info,
|
||||
})?;
|
||||
|
||||
if let Some(joined_channel) = joined_channel {
|
||||
channel_membership_updated(db, joined_channel, &session).await?
|
||||
}
|
||||
|
||||
room_updated(&joined_room.room, &session.peer);
|
||||
|
||||
joined_room.into_inner()
|
||||
joined_room
|
||||
};
|
||||
|
||||
channel_updated(
|
||||
|
@ -2663,7 +2739,6 @@ async fn join_channel(
|
|||
);
|
||||
|
||||
update_user_contacts(session.user_id, &session).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -3073,6 +3148,7 @@ fn build_initial_channels_update(
|
|||
update.channels.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
visibility: channel.visibility.into(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -3097,7 +3173,7 @@ fn build_initial_channels_update(
|
|||
.into_iter()
|
||||
.map(|id| proto::ChannelPermission {
|
||||
channel_id: id.to_proto(),
|
||||
is_admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -3105,6 +3181,8 @@ fn build_initial_channels_update(
|
|||
update.channel_invitations.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
// TODO: Visibility
|
||||
visibility: ChannelVisibility::Public.into(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -6,7 +6,10 @@ use call::ActiveCall;
|
|||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||
use client::User;
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
||||
use rpc::{proto, RECEIVE_TIMEOUT};
|
||||
use rpc::{
|
||||
proto::{self, ChannelRole},
|
||||
RECEIVE_TIMEOUT,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -68,7 +71,12 @@ async fn test_core_channels(
|
|||
.update(cx_a, |store, cx| {
|
||||
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||
|
||||
let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx);
|
||||
let invite = store.invite_member(
|
||||
channel_a_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Make sure we're synchronously storing the pending invite
|
||||
assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||
|
@ -103,12 +111,12 @@ async fn test_core_channels(
|
|||
&[
|
||||
(
|
||||
client_a.user_id().unwrap(),
|
||||
true,
|
||||
proto::ChannelRole::Admin,
|
||||
proto::channel_member::Kind::Member,
|
||||
),
|
||||
(
|
||||
client_b.user_id().unwrap(),
|
||||
false,
|
||||
proto::ChannelRole::Member,
|
||||
proto::channel_member::Kind::Invitee,
|
||||
),
|
||||
],
|
||||
|
@ -183,7 +191,12 @@ async fn test_core_channels(
|
|||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
store.set_member_admin(channel_a_id, client_b.user_id().unwrap(), true, cx)
|
||||
store.set_member_role(
|
||||
channel_a_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Admin,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -305,12 +318,12 @@ fn assert_participants_eq(participants: &[Arc<User>], expected_partitipants: &[u
|
|||
#[track_caller]
|
||||
fn assert_members_eq(
|
||||
members: &[ChannelMembership],
|
||||
expected_members: &[(u64, bool, proto::channel_member::Kind)],
|
||||
expected_members: &[(u64, proto::ChannelRole, proto::channel_member::Kind)],
|
||||
) {
|
||||
assert_eq!(
|
||||
members
|
||||
.iter()
|
||||
.map(|member| (member.user.id, member.admin, member.kind))
|
||||
.map(|member| (member.user.id, member.role, member.kind))
|
||||
.collect::<Vec<_>>(),
|
||||
expected_members
|
||||
);
|
||||
|
@ -611,7 +624,12 @@ async fn test_permissions_update_while_invited(
|
|||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.invite_member(rust_id, client_b.user_id().unwrap(), false, cx)
|
||||
channel_store.invite_member(
|
||||
rust_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -634,7 +652,12 @@ async fn test_permissions_update_while_invited(
|
|||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_member_admin(rust_id, client_b.user_id().unwrap(), true, cx)
|
||||
channel_store.set_member_role(
|
||||
rust_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Admin,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -803,7 +826,12 @@ async fn test_lost_channel_creation(
|
|||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.invite_member(channel_id, client_b.user_id().unwrap(), false, cx)
|
||||
channel_store.invite_member(
|
||||
channel_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -884,6 +912,119 @@ async fn test_lost_channel_creation(
|
|||
],
|
||||
);
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_guest_access(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let channels = server
|
||||
.make_channel_tree(&[("channel-a", None)], (&client_a, cx_a))
|
||||
.await;
|
||||
let channel_a_id = channels[0];
|
||||
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// should not be allowed to join
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a_id, cx))
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(channel_a_id, proto::ChannelVisibility::Public, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
assert!(client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |channel_store, _| channel_store
|
||||
.channel_for_id(channel_a_id)
|
||||
.is_some()));
|
||||
|
||||
client_a.channel_store().update(cx_a, |channel_store, _| {
|
||||
let participants = channel_store.channel_participants(channel_a_id);
|
||||
assert_eq!(participants.len(), 1);
|
||||
assert_eq!(participants[0].id, client_b.user_id().unwrap());
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_invite_access(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let channels = server
|
||||
.make_channel_tree(
|
||||
&[("channel-a", None), ("channel-b", Some("channel-a"))],
|
||||
(&client_a, cx_a),
|
||||
)
|
||||
.await;
|
||||
let channel_a_id = channels[0];
|
||||
let channel_b_id = channels[0];
|
||||
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// should not be allowed to join
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.invite_member(
|
||||
channel_a_id,
|
||||
client_b.user_id().unwrap(),
|
||||
ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
client_b.channel_store().update(cx_b, |channel_store, _| {
|
||||
assert!(channel_store.channel_for_id(channel_b_id).is_some());
|
||||
assert!(channel_store.channel_for_id(channel_a_id).is_some());
|
||||
});
|
||||
|
||||
client_a.channel_store().update(cx_a, |channel_store, _| {
|
||||
let participants = channel_store.channel_participants(channel_b_id);
|
||||
assert_eq!(participants.len(), 1);
|
||||
assert_eq!(participants[0].id, client_b.user_id().unwrap());
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_moving(
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use crate::db::ChannelRole;
|
||||
|
||||
use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
@ -50,7 +52,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
|||
.await
|
||||
.unwrap();
|
||||
for user in &users[1..] {
|
||||
db.invite_channel_member(id, user.user_id, users[0].user_id, false)
|
||||
db.invite_channel_member(id, user.user_id, users[0].user_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.respond_to_channel_invite(id, user.user_id, true)
|
||||
|
|
|
@ -18,7 +18,7 @@ use language::LanguageRegistry;
|
|||
use node_runtime::FakeNodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use rpc::RECEIVE_TIMEOUT;
|
||||
use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT};
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
|
@ -327,7 +327,7 @@ impl TestServer {
|
|||
channel_store.invite_member(
|
||||
channel_id,
|
||||
member_client.user_id().unwrap(),
|
||||
false,
|
||||
ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
@ -616,7 +616,12 @@ impl TestClient {
|
|||
cx_self
|
||||
.read(ChannelStore::global)
|
||||
.update(cx_self, |channel_store, cx| {
|
||||
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
|
||||
channel_store.invite_member(
|
||||
channel,
|
||||
other_client.user_id().unwrap(),
|
||||
ChannelRole::Admin,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -11,7 +11,10 @@ use anyhow::Result;
|
|||
use call::ActiveCall;
|
||||
use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
|
||||
use channel_modal::ChannelModal;
|
||||
use client::{proto::PeerId, Client, Contact, User, UserStore};
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Client, Contact, User, UserStore,
|
||||
};
|
||||
use contact_finder::ContactFinder;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
|
@ -428,7 +431,7 @@ enum ListEntry {
|
|||
is_last: bool,
|
||||
},
|
||||
ParticipantScreen {
|
||||
peer_id: PeerId,
|
||||
peer_id: Option<PeerId>,
|
||||
is_last: bool,
|
||||
},
|
||||
IncomingRequest(Arc<User>),
|
||||
|
@ -442,6 +445,9 @@ enum ListEntry {
|
|||
ChannelNotes {
|
||||
channel_id: ChannelId,
|
||||
},
|
||||
ChannelChat {
|
||||
channel_id: ChannelId,
|
||||
},
|
||||
ChannelEditor {
|
||||
depth: usize,
|
||||
},
|
||||
|
@ -602,6 +608,13 @@ impl CollabPanel {
|
|||
ix,
|
||||
cx,
|
||||
),
|
||||
ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
|
||||
*channel_id,
|
||||
&theme.collab_panel,
|
||||
is_selected,
|
||||
ix,
|
||||
cx,
|
||||
),
|
||||
ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
|
||||
channel.clone(),
|
||||
this.channel_store.clone(),
|
||||
|
@ -804,7 +817,8 @@ impl CollabPanel {
|
|||
let room = room.read(cx);
|
||||
|
||||
if let Some(channel_id) = room.channel_id() {
|
||||
self.entries.push(ListEntry::ChannelNotes { channel_id })
|
||||
self.entries.push(ListEntry::ChannelNotes { channel_id });
|
||||
self.entries.push(ListEntry::ChannelChat { channel_id })
|
||||
}
|
||||
|
||||
// Populate the active user.
|
||||
|
@ -836,7 +850,13 @@ impl CollabPanel {
|
|||
project_id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
host_user_id: user_id,
|
||||
is_last: projects.peek().is_none(),
|
||||
is_last: projects.peek().is_none() && !room.is_screen_sharing(),
|
||||
});
|
||||
}
|
||||
if room.is_screen_sharing() {
|
||||
self.entries.push(ListEntry::ParticipantScreen {
|
||||
peer_id: None,
|
||||
is_last: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -880,7 +900,7 @@ impl CollabPanel {
|
|||
}
|
||||
if !participant.video_tracks.is_empty() {
|
||||
self.entries.push(ListEntry::ParticipantScreen {
|
||||
peer_id: participant.peer_id,
|
||||
peer_id: Some(participant.peer_id),
|
||||
is_last: true,
|
||||
});
|
||||
}
|
||||
|
@ -1225,14 +1245,18 @@ impl CollabPanel {
|
|||
) -> AnyElement<Self> {
|
||||
enum CallParticipant {}
|
||||
enum CallParticipantTooltip {}
|
||||
enum LeaveCallButton {}
|
||||
enum LeaveCallTooltip {}
|
||||
|
||||
let collab_theme = &theme.collab_panel;
|
||||
|
||||
let is_current_user =
|
||||
user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
|
||||
|
||||
let content =
|
||||
MouseEventHandler::new::<CallParticipant, _>(user.id as usize, cx, |mouse_state, _| {
|
||||
let content = MouseEventHandler::new::<CallParticipant, _>(
|
||||
user.id as usize,
|
||||
cx,
|
||||
|mouse_state, cx| {
|
||||
let style = if is_current_user {
|
||||
*collab_theme
|
||||
.contact_row
|
||||
|
@ -1268,14 +1292,32 @@ impl CollabPanel {
|
|||
Label::new("Calling", collab_theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(collab_theme.calling_indicator.container)
|
||||
.aligned(),
|
||||
.aligned()
|
||||
.into_any(),
|
||||
)
|
||||
} else if is_current_user {
|
||||
Some(
|
||||
Label::new("You", collab_theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(collab_theme.calling_indicator.container)
|
||||
.aligned(),
|
||||
MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
|
||||
render_icon_button(
|
||||
theme
|
||||
.collab_panel
|
||||
.leave_call_button
|
||||
.style_for(is_selected, state),
|
||||
"icons/exit.svg",
|
||||
)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
Self::leave_call(cx);
|
||||
})
|
||||
.with_tooltip::<LeaveCallTooltip>(
|
||||
0,
|
||||
"Leave call",
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.into_any(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
|
@ -1284,7 +1326,8 @@ impl CollabPanel {
|
|||
.with_height(collab_theme.row_height)
|
||||
.contained()
|
||||
.with_style(style)
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if is_current_user || is_pending || peer_id.is_none() {
|
||||
return content.into_any();
|
||||
|
@ -1406,7 +1449,7 @@ impl CollabPanel {
|
|||
}
|
||||
|
||||
fn render_participant_screen(
|
||||
peer_id: PeerId,
|
||||
peer_id: Option<PeerId>,
|
||||
is_last: bool,
|
||||
is_selected: bool,
|
||||
theme: &theme::CollabPanel,
|
||||
|
@ -1421,8 +1464,8 @@ impl CollabPanel {
|
|||
.unwrap_or(0.);
|
||||
let tree_branch = theme.tree_branch;
|
||||
|
||||
MouseEventHandler::new::<OpenSharedScreen, _>(
|
||||
peer_id.as_u64() as usize,
|
||||
let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
|
||||
peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
|
||||
cx,
|
||||
|mouse_state, cx| {
|
||||
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
||||
|
@ -1460,16 +1503,20 @@ impl CollabPanel {
|
|||
.contained()
|
||||
.with_style(row.container)
|
||||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_shared_screen(peer_id, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
);
|
||||
if peer_id.is_none() {
|
||||
return handler.into_any();
|
||||
}
|
||||
handler
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_shared_screen(peer_id.unwrap(), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
|
@ -1496,23 +1543,32 @@ impl CollabPanel {
|
|||
enum AddChannel {}
|
||||
|
||||
let tooltip_style = &theme.tooltip;
|
||||
let mut channel_link = None;
|
||||
let mut channel_tooltip_text = None;
|
||||
let mut channel_icon = None;
|
||||
|
||||
let text = match section {
|
||||
Section::ActiveCall => {
|
||||
let channel_name = iife!({
|
||||
let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
|
||||
|
||||
let name = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_for_id(channel_id)?
|
||||
.name
|
||||
.as_str();
|
||||
let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
|
||||
|
||||
Some(name)
|
||||
channel_link = Some(channel.link());
|
||||
(channel_icon, channel_tooltip_text) = match channel.visibility {
|
||||
proto::ChannelVisibility::Public => {
|
||||
(Some("icons/public.svg"), Some("Copy public channel link."))
|
||||
}
|
||||
proto::ChannelVisibility::Members => {
|
||||
(Some("icons/hash.svg"), Some("Copy private channel link."))
|
||||
}
|
||||
};
|
||||
|
||||
Some(channel.name.as_str())
|
||||
});
|
||||
|
||||
if let Some(name) = channel_name {
|
||||
Cow::Owned(format!("#{}", name))
|
||||
Cow::Owned(format!("{}", name))
|
||||
} else {
|
||||
Cow::Borrowed("Current Call")
|
||||
}
|
||||
|
@ -1527,28 +1583,30 @@ impl CollabPanel {
|
|||
|
||||
enum AddContact {}
|
||||
let button = match section {
|
||||
Section::ActiveCall => Some(
|
||||
Section::ActiveCall => channel_link.map(|channel_link| {
|
||||
let channel_link_copy = channel_link.clone();
|
||||
MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
|
||||
render_icon_button(
|
||||
theme
|
||||
.collab_panel
|
||||
.leave_call_button
|
||||
.style_for(is_selected, state),
|
||||
"icons/exit.svg",
|
||||
"icons/link.svg",
|
||||
)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
Self::leave_call(cx);
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
let item = ClipboardItem::new(channel_link_copy.clone());
|
||||
cx.write_to_clipboard(item)
|
||||
})
|
||||
.with_tooltip::<AddContact>(
|
||||
0,
|
||||
"Leave call",
|
||||
channel_tooltip_text.unwrap(),
|
||||
None,
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
Section::Contacts => Some(
|
||||
MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
|
||||
render_icon_button(
|
||||
|
@ -1633,6 +1691,21 @@ impl CollabPanel {
|
|||
theme.collab_panel.contact_username.container.margin.left,
|
||||
),
|
||||
)
|
||||
} else if let Some(channel_icon) = channel_icon {
|
||||
Some(
|
||||
Svg::new(channel_icon)
|
||||
.with_color(header_style.text.color)
|
||||
.constrained()
|
||||
.with_max_width(icon_size)
|
||||
.with_max_height(icon_size)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(icon_size)
|
||||
.contained()
|
||||
.with_margin_right(
|
||||
theme.collab_panel.contact_username.container.margin.left,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
|
@ -1908,6 +1981,12 @@ impl CollabPanel {
|
|||
let channel_id = channel.id;
|
||||
let collab_theme = &theme.collab_panel;
|
||||
let has_children = self.channel_store.read(cx).has_children(channel_id);
|
||||
let is_public = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_for_id(channel_id)
|
||||
.map(|channel| channel.visibility)
|
||||
== Some(proto::ChannelVisibility::Public);
|
||||
let other_selected =
|
||||
self.selected_channel().map(|channel| channel.0.id) == Some(channel.id);
|
||||
let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok());
|
||||
|
@ -1965,12 +2044,16 @@ impl CollabPanel {
|
|||
|
||||
Flex::<Self>::row()
|
||||
.with_child(
|
||||
Svg::new("icons/hash.svg")
|
||||
.with_color(collab_theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.aligned()
|
||||
.left(),
|
||||
Svg::new(if is_public {
|
||||
"icons/public.svg"
|
||||
} else {
|
||||
"icons/hash.svg"
|
||||
})
|
||||
.with_color(collab_theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_child({
|
||||
let style = collab_theme.channel_name.inactive_state();
|
||||
|
@ -2275,7 +2358,7 @@ impl CollabPanel {
|
|||
.with_child(render_tree_branch(
|
||||
tree_branch,
|
||||
&row.name.text,
|
||||
true,
|
||||
false,
|
||||
vec2f(host_avatar_width, theme.row_height),
|
||||
cx.font_cache(),
|
||||
))
|
||||
|
@ -2308,6 +2391,62 @@ impl CollabPanel {
|
|||
.into_any()
|
||||
}
|
||||
|
||||
fn render_channel_chat(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
theme: &theme::CollabPanel,
|
||||
is_selected: bool,
|
||||
ix: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
enum ChannelChat {}
|
||||
let host_avatar_width = theme
|
||||
.contact_avatar
|
||||
.width
|
||||
.or(theme.contact_avatar.height)
|
||||
.unwrap_or(0.);
|
||||
|
||||
MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
|
||||
let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
|
||||
let row = theme.project_row.in_state(is_selected).style_for(state);
|
||||
|
||||
Flex::<Self>::row()
|
||||
.with_child(render_tree_branch(
|
||||
tree_branch,
|
||||
&row.name.text,
|
||||
true,
|
||||
vec2f(host_avatar_width, theme.row_height),
|
||||
cx.font_cache(),
|
||||
))
|
||||
.with_child(
|
||||
Svg::new("icons/conversations.svg")
|
||||
.with_color(theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(theme.channel_hash.width)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new("chat", theme.channel_name.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.channel_name.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1., true),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(*theme.channel_row.style_for(is_selected, state))
|
||||
.with_padding_left(theme.channel_row.default_style().padding.left)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_channel_invite(
|
||||
channel: Arc<Channel>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
|
@ -2771,6 +2910,9 @@ impl CollabPanel {
|
|||
}
|
||||
}
|
||||
ListEntry::ParticipantScreen { peer_id, .. } => {
|
||||
let Some(peer_id) = peer_id else {
|
||||
return;
|
||||
};
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_shared_screen(*peer_id, cx)
|
||||
|
@ -3498,6 +3640,14 @@ impl PartialEq for ListEntry {
|
|||
return channel_id == other_id;
|
||||
}
|
||||
}
|
||||
ListEntry::ChannelChat { channel_id } => {
|
||||
if let ListEntry::ChannelChat {
|
||||
channel_id: other_id,
|
||||
} = other
|
||||
{
|
||||
return channel_id == other_id;
|
||||
}
|
||||
}
|
||||
ListEntry::ChannelInvite(channel_1) => {
|
||||
if let ListEntry::ChannelInvite(channel_2) = other {
|
||||
return channel_1.id == channel_2.id;
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||
use client::{proto, User, UserId, UserStore};
|
||||
use client::{
|
||||
proto::{self, ChannelRole, ChannelVisibility},
|
||||
User, UserId, UserStore,
|
||||
};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
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;
|
||||
|
@ -96,11 +100,14 @@ impl ChannelModal {
|
|||
let channel_id = self.channel_id;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if mode == Mode::ManageMembers {
|
||||
let members = channel_store
|
||||
let mut members = channel_store
|
||||
.update(&mut cx, |channel_store, cx| {
|
||||
channel_store.get_channel_member_details(channel_id, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, _| picker.delegate_mut().members = members);
|
||||
|
@ -182,6 +189,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()
|
||||
|
@ -190,6 +272,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,
|
||||
|
@ -343,9 +426,11 @@ impl PickerDelegate for ChannelModalDelegate {
|
|||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
|
||||
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx),
|
||||
Mode::ManageMembers => {
|
||||
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
|
||||
}
|
||||
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
self.remove_selected_member(cx);
|
||||
|
@ -373,7 +458,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
|||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.channel_modal;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
let (user, admin) = self.user_at_index(ix).unwrap();
|
||||
let (user, role) = self.user_at_index(ix).unwrap();
|
||||
let request_status = self.member_status(user.id, cx);
|
||||
|
||||
let style = tabbed_modal
|
||||
|
@ -409,15 +494,25 @@ impl PickerDelegate for ChannelModalDelegate {
|
|||
},
|
||||
)
|
||||
})
|
||||
.with_children(admin.and_then(|admin| {
|
||||
(in_manage && admin).then(|| {
|
||||
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||
Some(
|
||||
Label::new("Admin", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left()
|
||||
})
|
||||
}))
|
||||
.left(),
|
||||
)
|
||||
} else if in_manage && role == Some(ChannelRole::Guest) {
|
||||
Some(
|
||||
Label::new("Guest", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.with_children({
|
||||
let svg = match self.mode {
|
||||
Mode::ManageMembers => Some(
|
||||
|
@ -502,13 +597,13 @@ impl ChannelModalDelegate {
|
|||
})
|
||||
}
|
||||
|
||||
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<bool>)> {
|
||||
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
|
||||
let channel_membership = self.members.get(*ix)?;
|
||||
Some((
|
||||
channel_membership.user.clone(),
|
||||
Some(channel_membership.admin),
|
||||
Some(channel_membership.role),
|
||||
))
|
||||
}),
|
||||
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
|
||||
|
@ -516,17 +611,21 @@ impl ChannelModalDelegate {
|
|||
}
|
||||
|
||||
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, admin) = self.user_at_index(self.selected_index)?;
|
||||
let admin = !admin.unwrap_or(false);
|
||||
let (user, role) = self.user_at_index(self.selected_index)?;
|
||||
let new_role = if role == Some(ChannelRole::Admin) {
|
||||
ChannelRole::Member
|
||||
} else {
|
||||
ChannelRole::Admin
|
||||
};
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.set_member_admin(self.channel_id, user.id, admin, cx)
|
||||
store.set_member_role(self.channel_id, user.id, new_role, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||
member.admin = admin;
|
||||
member.role = new_role;
|
||||
}
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
|
@ -572,25 +671,30 @@ impl ChannelModalDelegate {
|
|||
|
||||
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let invite_member = self.channel_store.update(cx, |store, cx| {
|
||||
store.invite_member(self.channel_id, user.id, false, cx)
|
||||
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
invite_member.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate_mut().members.push(ChannelMembership {
|
||||
let new_member = ChannelMembership {
|
||||
user,
|
||||
kind: proto::channel_member::Kind::Invitee,
|
||||
admin: false,
|
||||
});
|
||||
role: ChannelRole::Member,
|
||||
};
|
||||
let members = &mut this.delegate_mut().members;
|
||||
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
|
||||
Ok(ix) | Err(ix) => members.insert(ix, new_member),
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_menu.update(cx, |context_menu, cx| {
|
||||
context_menu.show(
|
||||
Default::default(),
|
||||
|
@ -598,7 +702,7 @@ impl ChannelModalDelegate {
|
|||
vec![
|
||||
ContextMenuItem::action("Remove", RemoveMember),
|
||||
ContextMenuItem::action(
|
||||
if user_is_admin {
|
||||
if role == ChannelRole::Admin {
|
||||
"Make non-admin"
|
||||
} else {
|
||||
"Make admin"
|
||||
|
|
|
@ -19,6 +19,7 @@ settings = { path = "../settings" }
|
|||
util = { path = "../util" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
zed-actions = { path = "../zed-actions" }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
|
|
|
@ -6,8 +6,12 @@ use gpui::{
|
|||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::cmp::{self, Reverse};
|
||||
use util::ResultExt;
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use zed_actions::OpenZedURL;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(toggle_command_palette);
|
||||
|
@ -167,13 +171,22 @@ impl PickerDelegate for CommandPaletteDelegate {
|
|||
)
|
||||
.await
|
||||
};
|
||||
let intercept_result = cx.read(|cx| {
|
||||
let mut intercept_result = cx.read(|cx| {
|
||||
if cx.has_global::<CommandPaletteInterceptor>() {
|
||||
cx.global::<CommandPaletteInterceptor>()(&query, cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if *RELEASE_CHANNEL == ReleaseChannel::Dev {
|
||||
if parse_zed_link(&query).is_some() {
|
||||
intercept_result = Some(CommandInterceptResult {
|
||||
action: OpenZedURL { url: query.clone() }.boxed_clone(),
|
||||
string: query.clone(),
|
||||
positions: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
if let Some(CommandInterceptResult {
|
||||
action,
|
||||
string,
|
||||
|
|
|
@ -146,7 +146,7 @@ message Envelope {
|
|||
DeleteChannel delete_channel = 120;
|
||||
GetChannelMembers get_channel_members = 121;
|
||||
GetChannelMembersResponse get_channel_members_response = 122;
|
||||
SetChannelMemberAdmin set_channel_member_admin = 123;
|
||||
SetChannelMemberRole set_channel_member_role = 123;
|
||||
RenameChannel rename_channel = 124;
|
||||
RenameChannelResponse rename_channel_response = 125;
|
||||
|
||||
|
@ -172,7 +172,8 @@ message Envelope {
|
|||
|
||||
LinkChannel link_channel = 142;
|
||||
UnlinkChannel unlink_channel = 143;
|
||||
MoveChannel move_channel = 144; // current max: 146
|
||||
MoveChannel move_channel = 144;
|
||||
SetChannelVisibility set_channel_visibility = 147; // current max: 147
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -992,7 +993,7 @@ message ChannelEdge {
|
|||
|
||||
message ChannelPermission {
|
||||
uint64 channel_id = 1;
|
||||
bool is_admin = 2;
|
||||
ChannelRole role = 3;
|
||||
}
|
||||
|
||||
message ChannelParticipants {
|
||||
|
@ -1018,8 +1019,8 @@ message GetChannelMembersResponse {
|
|||
|
||||
message ChannelMember {
|
||||
uint64 user_id = 1;
|
||||
bool admin = 2;
|
||||
Kind kind = 3;
|
||||
ChannelRole role = 4;
|
||||
|
||||
enum Kind {
|
||||
Member = 0;
|
||||
|
@ -1041,7 +1042,7 @@ message CreateChannelResponse {
|
|||
message InviteChannelMember {
|
||||
uint64 channel_id = 1;
|
||||
uint64 user_id = 2;
|
||||
bool admin = 3;
|
||||
ChannelRole role = 4;
|
||||
}
|
||||
|
||||
message RemoveChannelMember {
|
||||
|
@ -1049,10 +1050,22 @@ message RemoveChannelMember {
|
|||
uint64 user_id = 2;
|
||||
}
|
||||
|
||||
message SetChannelMemberAdmin {
|
||||
enum ChannelRole {
|
||||
Admin = 0;
|
||||
Member = 1;
|
||||
Guest = 2;
|
||||
Banned = 3;
|
||||
}
|
||||
|
||||
message SetChannelMemberRole {
|
||||
uint64 channel_id = 1;
|
||||
uint64 user_id = 2;
|
||||
bool admin = 3;
|
||||
ChannelRole role = 3;
|
||||
}
|
||||
|
||||
message SetChannelVisibility {
|
||||
uint64 channel_id = 1;
|
||||
ChannelVisibility visibility = 2;
|
||||
}
|
||||
|
||||
message RenameChannel {
|
||||
|
@ -1546,9 +1559,15 @@ message Nonce {
|
|||
uint64 lower_half = 2;
|
||||
}
|
||||
|
||||
enum ChannelVisibility {
|
||||
Public = 0;
|
||||
Members = 1;
|
||||
}
|
||||
|
||||
message Channel {
|
||||
uint64 id = 1;
|
||||
string name = 2;
|
||||
ChannelVisibility visibility = 3;
|
||||
}
|
||||
|
||||
message Contact {
|
||||
|
|
|
@ -232,7 +232,8 @@ messages!(
|
|||
(SaveBuffer, Foreground),
|
||||
(RenameChannel, Foreground),
|
||||
(RenameChannelResponse, Foreground),
|
||||
(SetChannelMemberAdmin, Foreground),
|
||||
(SetChannelMemberRole, Foreground),
|
||||
(SetChannelVisibility, Foreground),
|
||||
(SearchProject, Background),
|
||||
(SearchProjectResponse, Background),
|
||||
(ShareProject, Foreground),
|
||||
|
@ -332,7 +333,8 @@ request_messages!(
|
|||
(RemoveContact, Ack),
|
||||
(RespondToContactRequest, Ack),
|
||||
(RespondToChannelInvite, Ack),
|
||||
(SetChannelMemberAdmin, Ack),
|
||||
(SetChannelMemberRole, Ack),
|
||||
(SetChannelVisibility, Ack),
|
||||
(SendChannelMessage, SendChannelMessageResponse),
|
||||
(GetChannelMessages, GetChannelMessagesResponse),
|
||||
(GetChannelMembers, GetChannelMembersResponse),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -289,6 +289,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
cx.add_global_action(restart);
|
||||
cx.add_async_action(Workspace::save_all);
|
||||
cx.add_action(Workspace::add_folder_to_project);
|
||||
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
|
||||
let pane = workspace.active_pane().clone();
|
||||
|
|
|
@ -8,3 +8,4 @@ publish = false
|
|||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
serde.workspace = true
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use gpui::actions;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{actions, impl_actions};
|
||||
use serde::Deserialize;
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
|
@ -26,3 +29,13 @@ actions!(
|
|||
ResetDatabase,
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Deserialize, Clone, PartialEq)]
|
||||
pub struct OpenBrowser {
|
||||
pub url: Arc<str>,
|
||||
}
|
||||
#[derive(Deserialize, Clone, PartialEq)]
|
||||
pub struct OpenZedURL {
|
||||
pub url: String,
|
||||
}
|
||||
impl_actions!(zed, [OpenBrowser, OpenZedURL]);
|
||||
|
|
|
@ -3,22 +3,16 @@
|
|||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use backtrace::Backtrace;
|
||||
use cli::{
|
||||
ipc::{self, IpcSender},
|
||||
CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
|
||||
};
|
||||
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
||||
use client::{
|
||||
self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
|
||||
};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
FutureExt, SinkExt, StreamExt,
|
||||
};
|
||||
use editor::Editor;
|
||||
use futures::StreamExt;
|
||||
use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task};
|
||||
use isahc::{config::Configurable, Request};
|
||||
use language::{LanguageRegistry, Point};
|
||||
use language::LanguageRegistry;
|
||||
use log::LevelFilter;
|
||||
use node_runtime::RealNodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
|
@ -28,7 +22,6 @@ use settings::{default_settings, handle_settings_file_changes, watch_config_file
|
|||
use simplelog::ConfigBuilder;
|
||||
use smol::process::Command;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs::OpenOptions,
|
||||
|
@ -42,11 +35,9 @@ use std::{
|
|||
thread,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel},
|
||||
http::{self, HttpClient},
|
||||
paths::PathLikeWithPosition,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use welcome::{show_welcome_experience, FIRST_OPEN};
|
||||
|
@ -58,12 +49,9 @@ use zed::{
|
|||
assets::Assets,
|
||||
build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
|
||||
only_instance::{ensure_only_instance, IsOnlyInstance},
|
||||
open_listener::{handle_cli_connection, OpenListener, OpenRequest},
|
||||
};
|
||||
|
||||
use crate::open_listener::{OpenListener, OpenRequest};
|
||||
|
||||
mod open_listener;
|
||||
|
||||
fn main() {
|
||||
let http = http::client();
|
||||
init_paths();
|
||||
|
@ -113,6 +101,7 @@ fn main() {
|
|||
|
||||
app.run(move |cx| {
|
||||
cx.set_global(*RELEASE_CHANNEL);
|
||||
cx.set_global(listener.clone());
|
||||
|
||||
let mut store = SettingsStore::default();
|
||||
store
|
||||
|
@ -735,189 +724,6 @@ async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()>
|
|||
#[cfg(not(debug_assertions))]
|
||||
fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
|
||||
|
||||
fn connect_to_cli(
|
||||
server_name: &str,
|
||||
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
|
||||
let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
|
||||
.context("error connecting to cli")?;
|
||||
let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
|
||||
let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
|
||||
|
||||
handshake_tx
|
||||
.send(IpcHandshake {
|
||||
requests: request_tx,
|
||||
responses: response_rx,
|
||||
})
|
||||
.context("error sending ipc handshake")?;
|
||||
|
||||
let (mut async_request_tx, async_request_rx) =
|
||||
futures::channel::mpsc::channel::<CliRequest>(16);
|
||||
thread::spawn(move || {
|
||||
while let Ok(cli_request) = request_rx.recv() {
|
||||
if smol::block_on(async_request_tx.send(cli_request)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
});
|
||||
|
||||
Ok((async_request_rx, response_tx))
|
||||
}
|
||||
|
||||
async fn handle_cli_connection(
|
||||
(mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
|
||||
app_state: Arc<AppState>,
|
||||
mut cx: AsyncAppContext,
|
||||
) {
|
||||
if let Some(request) = requests.next().await {
|
||||
match request {
|
||||
CliRequest::Open { paths, wait } => {
|
||||
let mut caret_positions = HashMap::new();
|
||||
|
||||
let paths = if paths.is_empty() {
|
||||
workspace::last_opened_workspace_paths()
|
||||
.await
|
||||
.map(|location| location.paths().to_vec())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
paths
|
||||
.into_iter()
|
||||
.filter_map(|path_with_position_string| {
|
||||
let path_with_position = PathLikeWithPosition::parse_str(
|
||||
&path_with_position_string,
|
||||
|path_str| {
|
||||
Ok::<_, std::convert::Infallible>(
|
||||
Path::new(path_str).to_path_buf(),
|
||||
)
|
||||
},
|
||||
)
|
||||
.expect("Infallible");
|
||||
let path = path_with_position.path_like;
|
||||
if let Some(row) = path_with_position.row {
|
||||
if path.is_file() {
|
||||
let row = row.saturating_sub(1);
|
||||
let col =
|
||||
path_with_position.column.unwrap_or(0).saturating_sub(1);
|
||||
caret_positions.insert(path.clone(), Point::new(row, col));
|
||||
}
|
||||
}
|
||||
Some(path)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let mut errored = false;
|
||||
match cx
|
||||
.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||
.await
|
||||
{
|
||||
Ok((workspace, items)) => {
|
||||
let mut item_release_futures = Vec::new();
|
||||
|
||||
for (item, path) in items.into_iter().zip(&paths) {
|
||||
match item {
|
||||
Some(Ok(item)) => {
|
||||
if let Some(point) = caret_positions.remove(path) {
|
||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update(&mut cx, |editor, cx| {
|
||||
let snapshot =
|
||||
editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot
|
||||
.buffer_snapshot
|
||||
.clip_point(point, Bias::Left);
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::center()),
|
||||
cx,
|
||||
|s| s.select_ranges([point..point]),
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
let released = oneshot::channel();
|
||||
cx.update(|cx| {
|
||||
item.on_release(
|
||||
cx,
|
||||
Box::new(move |_| {
|
||||
let _ = released.0.send(());
|
||||
}),
|
||||
)
|
||||
.detach();
|
||||
});
|
||||
item_release_futures.push(released.1);
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
responses
|
||||
.send(CliResponse::Stderr {
|
||||
message: format!("error opening {:?}: {}", path, err),
|
||||
})
|
||||
.log_err();
|
||||
errored = true;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
if wait {
|
||||
let background = cx.background();
|
||||
let wait = async move {
|
||||
if paths.is_empty() {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
if let Some(workspace) = workspace.upgrade(&cx) {
|
||||
let _subscription = cx.update(|cx| {
|
||||
cx.observe_release(&workspace, move |_, _| {
|
||||
let _ = done_tx.send(());
|
||||
})
|
||||
});
|
||||
drop(workspace);
|
||||
let _ = done_rx.await;
|
||||
}
|
||||
} else {
|
||||
let _ =
|
||||
futures::future::try_join_all(item_release_futures).await;
|
||||
};
|
||||
}
|
||||
.fuse();
|
||||
futures::pin_mut!(wait);
|
||||
|
||||
loop {
|
||||
// Repeatedly check if CLI is still open to avoid wasting resources
|
||||
// waiting for files or workspaces to close.
|
||||
let mut timer = background.timer(Duration::from_secs(1)).fuse();
|
||||
futures::select_biased! {
|
||||
_ = wait => break,
|
||||
_ = timer => {
|
||||
if responses.send(CliResponse::Ping).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
errored = true;
|
||||
responses
|
||||
.send(CliResponse::Stderr {
|
||||
message: format!("error opening {:?}: {}", paths, error),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
responses
|
||||
.send(CliResponse::Exit {
|
||||
status: i32::from(errored),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
|
||||
&[
|
||||
("Go to file", &file_finder::Toggle),
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
use anyhow::anyhow;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use cli::{ipc, IpcHandshake};
|
||||
use cli::{ipc::IpcSender, CliRequest, CliResponse};
|
||||
use futures::channel::mpsc;
|
||||
use editor::scroll::autoscroll::Autoscroll;
|
||||
use editor::Editor;
|
||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use gpui::AsyncAppContext;
|
||||
use language::{Bias, Point};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::prelude::OsStrExt;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::{path::PathBuf, sync::atomic::AtomicBool};
|
||||
use util::channel::parse_zed_link;
|
||||
use util::paths::PathLikeWithPosition;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::connect_to_cli;
|
||||
use workspace::AppState;
|
||||
|
||||
pub enum OpenRequest {
|
||||
Paths {
|
||||
|
@ -96,3 +107,186 @@ impl OpenListener {
|
|||
Some(OpenRequest::Paths { paths })
|
||||
}
|
||||
}
|
||||
|
||||
fn connect_to_cli(
|
||||
server_name: &str,
|
||||
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
|
||||
let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
|
||||
.context("error connecting to cli")?;
|
||||
let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
|
||||
let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
|
||||
|
||||
handshake_tx
|
||||
.send(IpcHandshake {
|
||||
requests: request_tx,
|
||||
responses: response_rx,
|
||||
})
|
||||
.context("error sending ipc handshake")?;
|
||||
|
||||
let (mut async_request_tx, async_request_rx) =
|
||||
futures::channel::mpsc::channel::<CliRequest>(16);
|
||||
thread::spawn(move || {
|
||||
while let Ok(cli_request) = request_rx.recv() {
|
||||
if smol::block_on(async_request_tx.send(cli_request)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
});
|
||||
|
||||
Ok((async_request_rx, response_tx))
|
||||
}
|
||||
|
||||
pub async fn handle_cli_connection(
|
||||
(mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
|
||||
app_state: Arc<AppState>,
|
||||
mut cx: AsyncAppContext,
|
||||
) {
|
||||
if let Some(request) = requests.next().await {
|
||||
match request {
|
||||
CliRequest::Open { paths, wait } => {
|
||||
let mut caret_positions = HashMap::new();
|
||||
|
||||
let paths = if paths.is_empty() {
|
||||
workspace::last_opened_workspace_paths()
|
||||
.await
|
||||
.map(|location| location.paths().to_vec())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
paths
|
||||
.into_iter()
|
||||
.filter_map(|path_with_position_string| {
|
||||
let path_with_position = PathLikeWithPosition::parse_str(
|
||||
&path_with_position_string,
|
||||
|path_str| {
|
||||
Ok::<_, std::convert::Infallible>(
|
||||
Path::new(path_str).to_path_buf(),
|
||||
)
|
||||
},
|
||||
)
|
||||
.expect("Infallible");
|
||||
let path = path_with_position.path_like;
|
||||
if let Some(row) = path_with_position.row {
|
||||
if path.is_file() {
|
||||
let row = row.saturating_sub(1);
|
||||
let col =
|
||||
path_with_position.column.unwrap_or(0).saturating_sub(1);
|
||||
caret_positions.insert(path.clone(), Point::new(row, col));
|
||||
}
|
||||
}
|
||||
Some(path)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let mut errored = false;
|
||||
match cx
|
||||
.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||
.await
|
||||
{
|
||||
Ok((workspace, items)) => {
|
||||
let mut item_release_futures = Vec::new();
|
||||
|
||||
for (item, path) in items.into_iter().zip(&paths) {
|
||||
match item {
|
||||
Some(Ok(item)) => {
|
||||
if let Some(point) = caret_positions.remove(path) {
|
||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update(&mut cx, |editor, cx| {
|
||||
let snapshot =
|
||||
editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot
|
||||
.buffer_snapshot
|
||||
.clip_point(point, Bias::Left);
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::center()),
|
||||
cx,
|
||||
|s| s.select_ranges([point..point]),
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
let released = oneshot::channel();
|
||||
cx.update(|cx| {
|
||||
item.on_release(
|
||||
cx,
|
||||
Box::new(move |_| {
|
||||
let _ = released.0.send(());
|
||||
}),
|
||||
)
|
||||
.detach();
|
||||
});
|
||||
item_release_futures.push(released.1);
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
responses
|
||||
.send(CliResponse::Stderr {
|
||||
message: format!("error opening {:?}: {}", path, err),
|
||||
})
|
||||
.log_err();
|
||||
errored = true;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
if wait {
|
||||
let background = cx.background();
|
||||
let wait = async move {
|
||||
if paths.is_empty() {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
if let Some(workspace) = workspace.upgrade(&cx) {
|
||||
let _subscription = cx.update(|cx| {
|
||||
cx.observe_release(&workspace, move |_, _| {
|
||||
let _ = done_tx.send(());
|
||||
})
|
||||
});
|
||||
drop(workspace);
|
||||
let _ = done_rx.await;
|
||||
}
|
||||
} else {
|
||||
let _ =
|
||||
futures::future::try_join_all(item_release_futures).await;
|
||||
};
|
||||
}
|
||||
.fuse();
|
||||
futures::pin_mut!(wait);
|
||||
|
||||
loop {
|
||||
// Repeatedly check if CLI is still open to avoid wasting resources
|
||||
// waiting for files or workspaces to close.
|
||||
let mut timer = background.timer(Duration::from_secs(1)).fuse();
|
||||
futures::select_biased! {
|
||||
_ = wait => break,
|
||||
_ = timer => {
|
||||
if responses.send(CliResponse::Ping).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
errored = true;
|
||||
responses
|
||||
.send(CliResponse::Stderr {
|
||||
message: format!("error opening {:?}: {}", paths, error),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
responses
|
||||
.send(CliResponse::Exit {
|
||||
status: i32::from(errored),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ pub mod assets;
|
|||
pub mod languages;
|
||||
pub mod menus;
|
||||
pub mod only_instance;
|
||||
pub mod open_listener;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
|
@ -28,6 +29,7 @@ use gpui::{
|
|||
AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle,
|
||||
};
|
||||
pub use lsp;
|
||||
use open_listener::OpenListener;
|
||||
pub use project;
|
||||
use project_panel::ProjectPanel;
|
||||
use quick_action_bar::QuickActionBar;
|
||||
|
@ -87,6 +89,10 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
|||
},
|
||||
);
|
||||
cx.add_global_action(quit);
|
||||
cx.add_global_action(move |action: &OpenZedURL, cx| {
|
||||
cx.global::<Arc<OpenListener>>()
|
||||
.open_urls(vec![action.url.clone()])
|
||||
});
|
||||
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
|
||||
cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
|
||||
theme::adjust_font_size(cx, |size| *size += 1.0)
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { useTheme } from "../theme"
|
||||
import { StyleSets, 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: {
|
||||
|
|
Loading…
Reference in a new issue