Add ability to join a room from a channel ID

co-authored-by: max <max@zed.dev>
This commit is contained in:
Mikayla Maki 2023-07-31 15:27:10 -07:00
parent 4b94bfa045
commit 92fa879b0c
No known key found for this signature in database
16 changed files with 485 additions and 105 deletions

View File

@ -209,6 +209,80 @@ impl ActiveCall {
})
}
pub fn join_channel(
&mut self,
channel_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room = if let Some(room) = self.room().cloned() {
Some(Task::ready(Ok(room)).shared())
} else {
self.pending_room_creation.clone()
};
todo!()
// let invite = if let Some(room) = room {
// cx.spawn_weak(|_, mut cx| async move {
// let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
// // TODO join_channel:
// // let initial_project_id = if let Some(initial_project) = initial_project {
// // Some(
// // room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))
// // .await?,
// // )
// // } else {
// // None
// // };
// // room.update(&mut cx, |room, cx| {
// // room.call(called_user_id, initial_project_id, cx)
// // })
// // .await?;
// anyhow::Ok(())
// })
// } else {
// let client = self.client.clone();
// let user_store = self.user_store.clone();
// let room = cx
// .spawn(|this, mut cx| async move {
// let create_room = async {
// let room = cx
// .update(|cx| {
// Room::create_from_channel(channel_id, client, user_store, cx)
// })
// .await?;
// this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
// .await?;
// anyhow::Ok(room)
// };
// let room = create_room.await;
// this.update(&mut cx, |this, _| this.pending_room_creation = None);
// room.map_err(Arc::new)
// })
// .shared();
// self.pending_room_creation = Some(room.clone());
// cx.foreground().spawn(async move {
// room.await.map_err(|err| anyhow!("{:?}", err))?;
// anyhow::Ok(())
// })
// };
// cx.spawn(|this, mut cx| async move {
// let result = invite.await;
// this.update(&mut cx, |this, cx| {
// this.pending_invites.remove(&called_user_id);
// this.report_call_event("invite", cx);
// cx.notify();
// });
// result
// })
}
pub fn cancel_invite(
&mut self,
called_user_id: u64,

View File

@ -204,6 +204,15 @@ impl Room {
}
}
pub(crate) fn create_from_channel(
channel_id: u64,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut AppContext,
) -> Task<Result<ModelHandle<Self>>> {
todo!()
}
pub(crate) fn create(
called_user_id: u64,
initial_project: Option<ModelHandle<Project>>,

View File

@ -10,7 +10,7 @@ pub struct ChannelStore {
channel_invitations: Vec<Channel>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
rpc_subscription: Subscription,
_rpc_subscription: Subscription,
}
#[derive(Debug, PartialEq)]
@ -37,7 +37,7 @@ impl ChannelStore {
channel_invitations: vec![],
client,
user_store,
rpc_subscription,
_rpc_subscription: rpc_subscription,
}
}

View File

@ -36,7 +36,8 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
CREATE TABLE "rooms" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"live_kit_room" VARCHAR NOT NULL
"live_kit_room" VARCHAR NOT NULL,
"channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
);
CREATE TABLE "projects" (
@ -188,7 +189,6 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
CREATE TABLE "channels" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"name" VARCHAR NOT NULL,
"room_id" INTEGER REFERENCES rooms (id) ON DELETE SET NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT now
);

View File

@ -7,7 +7,6 @@ DROP TABLE "channels";
CREATE TABLE "channels" (
"id" SERIAL PRIMARY KEY,
"name" VARCHAR NOT NULL,
"room_id" INTEGER REFERENCES rooms (id) ON DELETE SET NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT now()
);
@ -27,3 +26,5 @@ CREATE TABLE "channel_members" (
);
CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id");
ALTER TABLE rooms ADD COLUMN "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE;

View File

@ -1337,32 +1337,65 @@ impl Database {
&self,
room_id: RoomId,
user_id: UserId,
channel_id: Option<ChannelId>,
connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(room_id, |tx| async move {
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 {
if let Some(channel_id) = channel_id {
channel_member::Entity::find()
.filter(
channel_member::Column::ChannelId
.eq(channel_id)
.and(channel_member::Column::UserId.eq(user_id))
.and(channel_member::Column::Accepted.eq(true)),
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such channel membership"))?;
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),
// Redundant for the channel join use case, used for channel and call invitations
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,
))),
..Default::default()
})
.exec(&*tx)
}
.insert(&*tx)
.await?;
if result.rows_affected == 0 {
Err(anyhow!("room does not exist or was already joined"))?
} else {
let room = self.get_room(room_id, &tx).await?;
Ok(room)
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 {
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"))?;
}
}
let room = self.get_room(room_id, &tx).await?;
Ok(room)
})
.await
}
@ -3071,6 +3104,14 @@ impl Database {
.insert(&*tx)
.await?;
room::ActiveModel {
channel_id: ActiveValue::Set(Some(channel.id)),
live_kit_room: ActiveValue::Set(format!("channel-{}", channel.id)),
..Default::default()
}
.insert(&*tx)
.await?;
Ok(channel.id)
})
.await
@ -3163,6 +3204,7 @@ impl Database {
self.transaction(|tx| async move {
let tx = tx;
// Breadth first list of all edges in this user's channels
let sql = r#"
WITH RECURSIVE channel_tree(child_id, parent_id, depth) AS (
SELECT channel_id as child_id, CAST(NULL as INTEGER) as parent_id, 0
@ -3173,23 +3215,52 @@ impl Database {
FROM channel_parents, channel_tree
WHERE channel_parents.parent_id = channel_tree.child_id
)
SELECT channel_tree.child_id as id, channels.name, channel_tree.parent_id
SELECT channel_tree.child_id, channel_tree.parent_id
FROM channel_tree
JOIN channels ON channels.id = channel_tree.child_id
ORDER BY channel_tree.depth;
ORDER BY child_id, parent_id IS NOT NULL
"#;
#[derive(FromQueryResult, Debug, PartialEq)]
pub struct ChannelParent {
pub child_id: ChannelId,
pub parent_id: Option<ChannelId>,
}
let stmt = Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
vec![user_id.into()],
);
Ok(channel_parent::Entity::find()
let mut parents_by_child_id = HashMap::default();
let mut parents = channel_parent::Entity::find()
.from_raw_sql(stmt)
.into_model::<Channel>()
.all(&*tx)
.await?)
.into_model::<ChannelParent>()
.stream(&*tx).await?;
while let Some(parent) = parents.next().await {
let parent = parent?;
parents_by_child_id.insert(parent.child_id, parent.parent_id);
}
drop(parents);
let mut channels = Vec::with_capacity(parents_by_child_id.len());
let mut rows = channel::Entity::find()
.filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied()))
.stream(&*tx).await?;
while let Some(row) = rows.next().await {
let row = row?;
channels.push(Channel {
id: row.id,
name: row.name,
parent_id: parents_by_child_id.get(&row.id).copied().flatten(),
});
}
drop(rows);
Ok(channels)
})
.await
}
@ -3210,6 +3281,22 @@ impl Database {
.await
}
pub async fn get_channel_room(&self, channel_id: ChannelId) -> Result<RoomId> {
self.transaction(|tx| async move {
let tx = tx;
let room = channel::Model {
id: channel_id,
..Default::default()
}
.find_related(room::Entity)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("invalid channel"))?;
Ok(room.id)
})
.await
}
async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
where
F: Send + Fn(TransactionHandle) -> Fut,

View File

@ -1,4 +1,4 @@
use super::{ChannelId, RoomId};
use super::ChannelId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
@ -7,7 +7,6 @@ pub struct Model {
#[sea_orm(primary_key)]
pub id: ChannelId,
pub name: String,
pub room_id: Option<RoomId>,
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,4 +1,4 @@
use super::RoomId;
use super::{ChannelId, RoomId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@ -7,6 +7,7 @@ pub struct Model {
#[sea_orm(primary_key)]
pub id: RoomId,
pub live_kit_room: String,
pub channel_id: Option<ChannelId>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@ -17,6 +18,12 @@ pub enum Relation {
Project,
#[sea_orm(has_many = "super::follower::Entity")]
Follower,
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id"
)]
Channel,
}
impl Related<super::room_participant::Entity> for Entity {
@ -39,7 +46,7 @@ impl Related<super::follower::Entity> for Entity {
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Follower.def()
Relation::Channel.def()
}
}

View File

@ -494,9 +494,14 @@ test_both_dbs!(
)
.await
.unwrap();
db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
.await
.unwrap();
db.join_room(
room_id,
user2.user_id,
None,
ConnectionId { owner_id, id: 1 },
)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
@ -920,11 +925,6 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
name: "zed".to_string(),
parent_id: None,
},
Channel {
id: rust_id,
name: "rust".to_string(),
parent_id: None,
},
Channel {
id: crdb_id,
name: "crdb".to_string(),
@ -940,6 +940,11 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
name: "replace".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: rust_id,
name: "rust".to_string(),
parent_id: None,
},
Channel {
id: cargo_id,
name: "cargo".to_string(),
@ -949,6 +954,69 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
);
});
test_both_dbs!(
test_joining_channels_postgres,
test_joining_channels_sqlite,
db,
{
let owner_id = db.create_server("test").await.unwrap().0 as u32;
let user_1 = db
.create_user(
"user1@example.com",
false,
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let user_2 = db
.create_user(
"user2@example.com",
false,
NewUserParams {
github_login: "user2".into(),
github_user_id: 6,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
let room_1 = db.get_channel_room(channel_1).await.unwrap();
// can join a room with membership to its channel
let room = db
.join_room(
room_1,
user_1,
Some(channel_1),
ConnectionId { owner_id, id: 1 },
)
.await
.unwrap();
assert_eq!(room.participants.len(), 1);
drop(room);
// cannot join a room without membership to its channel
assert!(db
.join_room(
room_1,
user_2,
Some(channel_1),
ConnectionId { owner_id, id: 1 }
)
.await
.is_err());
}
);
#[gpui::test]
async fn test_multiple_signup_overwrite() {
let test_db = TestDb::postgres(build_background_executor());

View File

@ -34,7 +34,10 @@ use futures::{
use lazy_static::lazy_static;
use prometheus::{register_int_gauge, IntGauge};
use rpc::{
proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage},
proto::{
self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
RequestMessage,
},
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
};
use serde::{Serialize, Serializer};
@ -183,7 +186,7 @@ impl Server {
server
.add_request_handler(ping)
.add_request_handler(create_room)
.add_request_handler(create_room_request)
.add_request_handler(join_room)
.add_request_handler(rejoin_room)
.add_request_handler(leave_room)
@ -243,6 +246,7 @@ impl Server {
.add_request_handler(invite_channel_member)
.add_request_handler(remove_channel_member)
.add_request_handler(respond_to_channel_invite)
.add_request_handler(join_channel)
.add_request_handler(follow)
.add_message_handler(unfollow)
.add_message_handler(update_followers)
@ -855,48 +859,17 @@ async fn ping(_: proto::Ping, response: Response<proto::Ping>, _session: Session
Ok(())
}
async fn create_room(
async fn create_room_request(
_request: proto::CreateRoom,
response: Response<proto::CreateRoom>,
session: Session,
) -> Result<()> {
let live_kit_room = nanoid::nanoid!(30);
let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() {
if let Some(_) = live_kit
.create_room(live_kit_room.clone())
.await
.trace_err()
{
if let Some(token) = live_kit
.room_token(&live_kit_room, &session.user_id.to_string())
.trace_err()
{
Some(proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(),
token,
})
} else {
None
}
} else {
None
}
} else {
None
};
let (room, live_kit_connection_info) = create_room(&session).await?;
{
let room = session
.db()
.await
.create_room(session.user_id, session.connection_id, &live_kit_room)
.await?;
response.send(proto::CreateRoomResponse {
room: Some(room.clone()),
live_kit_connection_info,
})?;
}
response.send(proto::CreateRoomResponse {
room: Some(room.clone()),
live_kit_connection_info,
})?;
update_user_contacts(session.user_id, &session).await?;
Ok(())
@ -912,7 +885,7 @@ async fn join_room(
let room = session
.db()
.await
.join_room(room_id, session.user_id, session.connection_id)
.join_room(room_id, session.user_id, None, session.connection_id)
.await?;
room_updated(&room, &session.peer);
room.clone()
@ -2182,6 +2155,32 @@ async fn respond_to_channel_invite(
Ok(())
}
async fn join_channel(
request: proto::JoinChannel,
response: Response<proto::JoinChannel>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
todo!();
// db.check_channel_membership(session.user_id, channel_id)
// .await?;
let (room, live_kit_connection_info) = create_room(&session).await?;
// db.set_channel_room(channel_id, room.id).await?;
response.send(proto::CreateRoomResponse {
room: Some(room.clone()),
live_kit_connection_info,
})?;
update_user_contacts(session.user_id, &session).await?;
Ok(())
}
async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id);
let project_connection_ids = session
@ -2436,6 +2435,42 @@ fn project_left(project: &db::LeftProject, session: &Session) {
}
}
async fn create_room(session: &Session) -> Result<(proto::Room, Option<LiveKitConnectionInfo>)> {
let live_kit_room = nanoid::nanoid!(30);
let live_kit_connection_info = {
let live_kit_room = live_kit_room.clone();
let live_kit = session.live_kit_client.as_ref();
util::async_iife!({
let live_kit = live_kit?;
live_kit
.create_room(live_kit_room.clone())
.await
.trace_err()?;
let token = live_kit
.room_token(&live_kit_room, &session.user_id.to_string())
.trace_err()?;
Some(proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(),
token,
})
})
}
.await;
let room = session
.db()
.await
.create_room(session.user_id, session.connection_id, &live_kit_room)
.await?;
Ok((room, live_kit_connection_info))
}
pub trait ResultExt {
type Ok;

View File

@ -5,7 +5,7 @@ use crate::{
AppState,
};
use anyhow::anyhow;
use call::ActiveCall;
use call::{ActiveCall, Room};
use client::{
self, proto::PeerId, ChannelStore, Client, Connection, Credentials, EstablishConnectionError,
UserStore,
@ -269,6 +269,44 @@ impl TestServer {
}
}
async fn make_channel(
&self,
channel: &str,
admin: (&TestClient, &mut TestAppContext),
members: &mut [(&TestClient, &mut TestAppContext)],
) -> u64 {
let (admin_client, admin_cx) = admin;
let channel_id = admin_client
.channel_store
.update(admin_cx, |channel_store, _| {
channel_store.create_channel(channel, None)
})
.await
.unwrap();
for (member_client, member_cx) in members {
admin_client
.channel_store
.update(admin_cx, |channel_store, _| {
channel_store.invite_member(channel_id, member_client.user_id().unwrap(), false)
})
.await
.unwrap();
admin_cx.foreground().run_until_parked();
member_client
.channel_store
.update(*member_cx, |channels, _| {
channels.respond_to_channel_invite(channel_id, true)
})
.await
.unwrap();
}
channel_id
}
async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
self.make_contacts(clients).await;
@ -516,3 +554,27 @@ impl Drop for TestClient {
self.client.teardown();
}
}
#[derive(Debug, Eq, PartialEq)]
struct RoomParticipants {
remote: Vec<String>,
pending: Vec<String>,
}
fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
room.read_with(cx, |room, _| {
let mut remote = room
.remote_participants()
.iter()
.map(|(_, participant)| participant.user.github_login.clone())
.collect::<Vec<_>>();
let mut pending = room
.pending_participants()
.iter()
.map(|user| user.github_login.clone())
.collect::<Vec<_>>();
remote.sort();
pending.sort();
RoomParticipants { remote, pending }
})
}

View File

@ -1,7 +1,10 @@
use call::ActiveCall;
use client::Channel;
use gpui::{executor::Deterministic, TestAppContext};
use std::sync::Arc;
use crate::tests::{room_participants, RoomParticipants};
use super::TestServer;
#[gpui::test]
@ -82,6 +85,58 @@ async fn test_basic_channels(
});
}
#[gpui::test]
async fn test_channel_room(
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 zed_id = server
.make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
active_call_a
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
deterministic.run_until_parked();
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec!["user_a".to_string()],
pending: vec![]
}
);
active_call_b
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
deterministic.run_until_parked();
let active_call_b = cx_b.read(ActiveCall::global);
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec!["user_a".to_string(), "user_b".to_string()],
pending: vec![]
}
);
}
// TODO:
// Invariants to test:
// 1. Dag structure is maintained for all operations (can't make a cycle)

View File

@ -1,6 +1,6 @@
use crate::{
rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
tests::{TestClient, TestServer},
tests::{room_participants, RoomParticipants, TestClient, TestServer},
};
use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{User, RECEIVE_TIMEOUT};
@ -8319,30 +8319,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
});
}
#[derive(Debug, Eq, PartialEq)]
struct RoomParticipants {
remote: Vec<String>,
pending: Vec<String>,
}
fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
room.read_with(cx, |room, _| {
let mut remote = room
.remote_participants()
.iter()
.map(|(_, participant)| participant.user.github_login.clone())
.collect::<Vec<_>>();
let mut pending = room
.pending_participants()
.iter()
.map(|user| user.github_login.clone())
.collect::<Vec<_>>();
remote.sort();
pending.sort();
RoomParticipants { remote, pending }
})
}
fn extract_hint_labels(editor: &Editor) -> Vec<String> {
let mut labels = Vec::new();
for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {

View File

@ -136,6 +136,7 @@ message Envelope {
RemoveChannelMember remove_channel_member = 122;
RespondToChannelInvite respond_to_channel_invite = 123;
UpdateChannels update_channels = 124;
JoinChannel join_channel = 125;
}
}
@ -870,6 +871,10 @@ message UpdateChannels {
repeated uint64 remove_channel_invitations = 4;
}
message JoinChannel {
uint64 channel_id = 1;
}
message CreateChannel {
string name = 1;
optional uint64 parent_id = 2;

View File

@ -214,6 +214,7 @@ messages!(
(RequestContact, Foreground),
(RespondToContactRequest, Foreground),
(RespondToChannelInvite, Foreground),
(JoinChannel, Foreground),
(RoomUpdated, Foreground),
(SaveBuffer, Foreground),
(SearchProject, Background),
@ -294,6 +295,7 @@ request_messages!(
(RemoveContact, Ack),
(RespondToContactRequest, Ack),
(RespondToChannelInvite, Ack),
(JoinChannel, CreateRoomResponse),
(RenameProjectEntry, ProjectEntryResponse),
(SaveBuffer, BufferSaved),
(SearchProject, SearchProjectResponse),

View File

@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*;
mod macros;
pub const PROTOCOL_VERSION: u32 = 59;
pub const PROTOCOL_VERSION: u32 = 60;