Remove 2 suffix for client, call, channel

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-01-03 12:02:14 -08:00
parent 9f99e58834
commit 53bdf6beb3
57 changed files with 962 additions and 9116 deletions

160
Cargo.lock generated
View File

@ -305,7 +305,7 @@ dependencies = [
"ai",
"anyhow",
"chrono",
"client2",
"client",
"collections",
"ctor",
"editor",
@ -687,7 +687,7 @@ name = "auto_update"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
"client",
"db2",
"gpui2",
"isahc",
@ -1116,37 +1116,11 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "call"
version = "0.1.0"
dependencies = [
"anyhow",
"async-broadcast",
"audio",
"client",
"collections",
"fs",
"futures 0.3.28",
"gpui",
"language",
"live_kit_client",
"log",
"media",
"postage",
"project",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings",
"util",
]
[[package]]
name = "call2"
version = "0.1.0"
dependencies = [
"anyhow",
"async-broadcast",
"audio2",
"client2",
"client",
"collections",
"fs2",
"futures 0.3.28",
@ -1236,43 +1210,6 @@ dependencies = [
"client",
"clock",
"collections",
"db",
"feature_flags",
"futures 0.3.28",
"gpui",
"image",
"language",
"lazy_static",
"log",
"parking_lot 0.11.2",
"postage",
"rand 0.8.5",
"rpc",
"schemars",
"serde",
"serde_derive",
"settings",
"smallvec",
"smol",
"sum_tree",
"tempfile",
"text",
"thiserror",
"time",
"tiny_http",
"url",
"util",
"uuid 1.4.1",
]
[[package]]
name = "channel2"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
"clock",
"collections",
"db2",
"feature_flags",
"futures 0.3.28",
@ -1441,43 +1378,6 @@ dependencies = [
[[package]]
name = "client"
version = "0.1.0"
dependencies = [
"anyhow",
"async-recursion 0.3.2",
"async-tungstenite",
"chrono",
"collections",
"db",
"feature_flags",
"futures 0.3.28",
"gpui",
"image",
"lazy_static",
"log",
"parking_lot 0.11.2",
"postage",
"rand 0.8.5",
"rpc",
"schemars",
"serde",
"serde_derive",
"settings",
"smol",
"sum_tree",
"sysinfo",
"tempfile",
"text",
"thiserror",
"time",
"tiny_http",
"url",
"util",
"uuid 1.4.1",
]
[[package]]
name = "client2"
version = "0.1.0"
dependencies = [
"anyhow",
"async-recursion 0.3.2",
@ -1641,10 +1541,10 @@ dependencies = [
"axum",
"axum-extra",
"base64 0.13.1",
"call2",
"channel2",
"call",
"channel",
"clap 3.2.25",
"client2",
"client",
"clock",
"collab_ui",
"collections",
@ -1709,9 +1609,9 @@ version = "0.1.0"
dependencies = [
"anyhow",
"auto_update",
"call2",
"channel2",
"client2",
"call",
"channel",
"client",
"clock",
"collections",
"db2",
@ -2433,7 +2333,7 @@ name = "diagnostics"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
"client",
"collections",
"editor",
"futures 0.3.28",
@ -2594,7 +2494,7 @@ version = "0.1.0"
dependencies = [
"aho-corasick",
"anyhow",
"client2",
"client",
"clock",
"collections",
"convert_case 0.6.0",
@ -2823,7 +2723,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bitflags 2.4.1",
"client2",
"client",
"db2",
"editor",
"futures 0.3.28",
@ -4187,7 +4087,7 @@ dependencies = [
"anyhow",
"async-broadcast",
"async-trait",
"client2",
"client",
"clock",
"collections",
"ctor",
@ -4257,7 +4157,7 @@ name = "language_tools"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
"client",
"collections",
"editor",
"env_logger",
@ -4856,7 +4756,7 @@ version = "0.1.0"
dependencies = [
"aho-corasick",
"anyhow",
"client2",
"client",
"clock",
"collections",
"convert_case 0.6.0",
@ -5071,8 +4971,8 @@ name = "notifications2"
version = "0.1.0"
dependencies = [
"anyhow",
"channel2",
"client2",
"channel",
"client",
"clock",
"collections",
"db2",
@ -5903,7 +5803,7 @@ name = "prettier2"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
"client",
"collections",
"fs2",
"futures 0.3.28",
@ -6010,7 +5910,7 @@ dependencies = [
"anyhow",
"async-trait",
"backtrace",
"client2",
"client",
"clock",
"collections",
"copilot",
@ -6062,7 +5962,7 @@ name = "project_panel"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
"client",
"collections",
"db2",
"editor",
@ -7265,7 +7165,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bitflags 1.3.2",
"client2",
"client",
"collections",
"editor",
"futures 0.3.28",
@ -7373,7 +7273,7 @@ dependencies = [
"ai",
"anyhow",
"async-trait",
"client2",
"client",
"collections",
"ctor",
"env_logger",
@ -8553,7 +8453,7 @@ name = "terminal_view"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
"client",
"db2",
"dirs 4.0.0",
"editor",
@ -8701,7 +8601,7 @@ dependencies = [
name = "theme_selector"
version = "0.1.0"
dependencies = [
"client2",
"client",
"editor",
"feature_flags",
"fs2",
@ -10215,7 +10115,7 @@ name = "welcome"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
"client",
"db2",
"editor",
"fs2",
@ -10482,8 +10382,8 @@ dependencies = [
"anyhow",
"async-recursion 1.0.5",
"bincode",
"call2",
"client2",
"call",
"client",
"collections",
"db2",
"env_logger",
@ -10607,11 +10507,11 @@ dependencies = [
"auto_update",
"backtrace",
"breadcrumbs",
"call2",
"channel2",
"call",
"channel",
"chrono",
"cli",
"client2",
"client",
"collab_ui",
"collections",
"command_palette",

View File

@ -8,12 +8,9 @@ members = [
"crates/auto_update",
"crates/breadcrumbs",
"crates/call",
"crates/call2",
"crates/channel",
"crates/channel2",
"crates/cli",
"crates/client",
"crates/client2",
"crates/clock",
"crates/collab",
"crates/collab2",

View File

@ -10,7 +10,7 @@ doctest = false
[dependencies]
ai = { path = "../ai" }
client = { package = "client2", path = "../client2" }
client = { path = "../client" }
collections = { path = "../collections"}
editor = { path = "../editor" }
fs = { package = "fs2", path = "../fs2" }

View File

@ -10,7 +10,7 @@ doctest = false
[dependencies]
db = { package = "db2", path = "../db2" }
client = { package = "client2", path = "../client2" }
client = { path = "../client" }
gpui = { package = "gpui2", path = "../gpui2" }
menu = { package = "menu2", path = "../menu2" }
project = { path = "../project" }

View File

@ -19,34 +19,36 @@ test-support = [
]
[dependencies]
audio = { path = "../audio" }
audio = { package = "audio2", path = "../audio2" }
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
gpui = { package = "gpui2", path = "../gpui2" }
log.workspace = true
live_kit_client = { path = "../live_kit_client" }
fs = { path = "../fs" }
live_kit_client = { package = "live_kit_client2", path = "../live_kit_client2" }
fs = { package = "fs2", path = "../fs2" }
language = { path = "../language" }
media = { path = "../media" }
project = { path = "../project" }
settings = { path = "../settings" }
settings = { package = "settings2", path = "../settings2" }
util = { path = "../util" }
anyhow.workspace = true
async-broadcast = "0.4"
futures.workspace = true
image = "0.23"
postage.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
fs = { package = "fs2", path = "../fs2", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
live_kit_client = { package = "live_kit_client2", path = "../live_kit_client2", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

View File

@ -9,31 +9,25 @@ use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, Z
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
WeakModelHandle,
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
WeakModel,
};
use postage::watch;
use project::Project;
use room::Event;
use settings::Settings;
use std::sync::Arc;
pub use participant::ParticipantLocation;
pub use room::Room;
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
settings::register::<CallSettings>(cx);
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
CallSettings::register(cx);
let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(active_call);
}
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub calling_user: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
pub struct OneAtATime {
cancel: Option<oneshot::Sender<()>>,
}
@ -65,43 +59,44 @@ impl OneAtATime {
}
}
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub calling_user: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
/// Singleton global maintaining the user's participation in a room across workspaces.
pub struct ActiveCall {
room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<ModelHandle<Room>, Arc<anyhow::Error>>>>>,
room: Option<(Model<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<Model<Room>, Arc<anyhow::Error>>>>>,
location: Option<WeakModel<Project>>,
_join_debouncer: OneAtATime,
location: Option<WeakModelHandle<Project>>,
pending_invites: HashSet<u64>,
incoming_call: (
watch::Sender<Option<IncomingCall>>,
watch::Receiver<Option<IncomingCall>>,
),
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
user_store: Model<UserStore>,
_subscriptions: Vec<client::Subscription>,
}
impl Entity for ActiveCall {
type Event = room::Event;
}
impl EventEmitter<Event> for ActiveCall {}
impl ActiveCall {
fn new(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut ModelContext<Self>,
) -> Self {
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
Self {
room: None,
pending_room_creation: None,
location: None,
pending_invites: Default::default(),
incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
client.add_message_handler(cx.handle(), Self::handle_call_canceled),
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
],
client,
user_store,
@ -113,35 +108,35 @@ impl ActiveCall {
}
async fn handle_incoming_call(
this: ModelHandle<Self>,
this: Model<Self>,
envelope: TypedEnvelope<proto::IncomingCall>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.calling_user_id, cx)
})
})?
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
});
})?;
Ok(proto::Ack {})
}
async fn handle_call_canceled(
this: ModelHandle<Self>,
this: Model<Self>,
envelope: TypedEnvelope<proto::CallCanceled>,
_: Arc<Client>,
mut cx: AsyncAppContext,
@ -154,18 +149,18 @@ impl ActiveCall {
{
incoming_call.take();
}
});
})?;
Ok(())
}
pub fn global(cx: &AppContext) -> ModelHandle<Self> {
cx.global::<ModelHandle<Self>>().clone()
pub fn global(cx: &AppContext) -> Model<Self> {
cx.global::<Model<Self>>().clone()
}
pub fn invite(
&mut self,
called_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
initial_project: Option<Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
@ -184,21 +179,21 @@ impl ActiveCall {
};
let invite = if let Some(room) = room {
cx.spawn_weak(|_, mut cx| async move {
cx.spawn(move |_, mut cx| async move {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
.await?,
)
} else {
None
};
room.update(&mut cx, |room, cx| {
room.update(&mut cx, move |room, cx| {
room.call(called_user_id, initial_project_id, cx)
})
})?
.await?;
anyhow::Ok(())
@ -207,7 +202,7 @@ impl ActiveCall {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = cx
.spawn(|this, mut cx| async move {
.spawn(move |this, mut cx| async move {
let create_room = async {
let room = cx
.update(|cx| {
@ -218,31 +213,31 @@ impl ActiveCall {
user_store,
cx,
)
})
})?
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
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);
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 {
cx.background_executor().spawn(async move {
room.await.map_err(|err| anyhow!("{:?}", err))?;
anyhow::Ok(())
})
};
cx.spawn(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx));
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
} else {
// TODO: Resport collaboration error
}
@ -250,7 +245,7 @@ impl ActiveCall {
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
cx.notify();
});
})?;
result
})
}
@ -267,7 +262,7 @@ impl ActiveCall {
};
let client = self.client.clone();
cx.foreground().spawn(async move {
cx.background_executor().spawn(async move {
client
.request(proto::CancelCall {
room_id,
@ -306,11 +301,11 @@ impl ActiveCall {
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("accept incoming", cx)
});
})?;
Ok(())
})
}
@ -333,7 +328,7 @@ impl ActiveCall {
&mut self,
channel_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<ModelHandle<Room>>>> {
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
@ -352,13 +347,13 @@ impl ActiveCall {
Room::join_channel(channel_id, client, user_store, cx).await
});
cx.spawn(move |this, mut cx| async move {
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("join channel", cx)
});
})?;
Ok(room)
})
}
@ -366,6 +361,7 @@ impl ActiveCall {
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("hang up", cx);
Audio::end_call(cx);
if let Some((room, _)) = self.room.take() {
room.update(cx, |room, cx| room.leave(cx))
@ -376,7 +372,7 @@ impl ActiveCall {
pub fn share_project(
&mut self,
project: ModelHandle<Project>,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
@ -389,7 +385,7 @@ impl ActiveCall {
pub fn unshare_project(
&mut self,
project: ModelHandle<Project>,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
@ -400,13 +396,13 @@ impl ActiveCall {
}
}
pub fn location(&self) -> Option<&WeakModelHandle<Project>> {
pub fn location(&self) -> Option<&WeakModel<Project>> {
self.location.as_ref()
}
pub fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
@ -420,7 +416,7 @@ impl ActiveCall {
fn set_room(
&mut self,
room: Option<ModelHandle<Room>>,
room: Option<Model<Room>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if room.as_ref() != self.room.as_ref().map(|room| &room.0) {
@ -441,7 +437,10 @@ impl ActiveCall {
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room.clone(), subscriptions));
let location = self.location.and_then(|location| location.upgrade(cx));
let location = self
.location
.as_ref()
.and_then(|location| location.upgrade());
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {
@ -453,7 +452,7 @@ impl ActiveCall {
}
}
pub fn room(&self) -> Option<&ModelHandle<Room>> {
pub fn room(&self) -> Option<&Model<Room>> {
self.room.as_ref().map(|(room, _)| room)
}
@ -465,7 +464,7 @@ impl ActiveCall {
&self.pending_invites
}
pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
if let Some(room) = self.room() {
let room = room.read(cx);
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx);
@ -478,10 +477,10 @@ pub fn report_call_event_for_room(
room_id: u64,
channel_id: Option<u64>,
client: &Arc<Client>,
cx: &AppContext,
cx: &mut AppContext,
) {
let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let telemetry_settings = *TelemetrySettings::get_global(cx);
telemetry.report_call_event(telemetry_settings, operation, Some(room_id), channel_id)
}
@ -495,7 +494,8 @@ pub fn report_call_event_for_channel(
let room = ActiveCall::global(cx).read(cx).room();
let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let telemetry_settings = *TelemetrySettings::get_global(cx);
telemetry.report_call_event(
telemetry_settings,

View File

@ -1,6 +1,8 @@
use anyhow::Result;
use gpui::AppContext;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Setting;
use settings::Settings;
#[derive(Deserialize, Debug)]
pub struct CallSettings {
@ -12,7 +14,7 @@ pub struct CallSettingsContent {
pub mute_on_join: Option<bool>,
}
impl Setting for CallSettings {
impl Settings for CallSettings {
const KEY: Option<&'static str> = Some("calls");
type FileContent = CallSettingsContent;
@ -20,8 +22,11 @@ impl Setting for CallSettings {
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &gpui::AppContext,
) -> anyhow::Result<Self> {
_cx: &mut AppContext,
) -> Result<Self>
where
Self: Sized,
{
Self::load_via_json_merge(default_value, user_values)
}
}

View File

@ -2,11 +2,11 @@ use anyhow::{anyhow, Result};
use client::ParticipantIndex;
use client::{proto, User};
use collections::HashMap;
use gpui::WeakModelHandle;
use gpui::WeakModel;
pub use live_kit_client::Frame;
use live_kit_client::RemoteAudioTrack;
pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
use project::Project;
use std::{fmt, sync::Arc};
use std::sync::Arc;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ParticipantLocation {
@ -35,7 +35,7 @@ impl ParticipantLocation {
#[derive(Clone, Default)]
pub struct LocalParticipant {
pub projects: Vec<proto::ParticipantProject>,
pub active_project: Option<WeakModelHandle<Project>>,
pub active_project: Option<WeakModel<Project>>,
}
#[derive(Clone, Debug)]
@ -50,20 +50,3 @@ pub struct RemoteParticipant {
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
}
#[derive(Clone)]
pub struct RemoteVideoTrack {
pub(crate) live_kit_track: Arc<live_kit_client::RemoteVideoTrack>,
}
impl fmt::Debug for RemoteVideoTrack {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RemoteVideoTrack").finish()
}
}
impl RemoteVideoTrack {
pub fn frames(&self) -> async_broadcast::Receiver<Frame> {
self.live_kit_track.frames()
}
}

View File

@ -1,6 +1,6 @@
use crate::{
call_settings::CallSettings,
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
};
use anyhow::{anyhow, Result};
use audio::{Audio, Sound};
@ -11,7 +11,9 @@ use client::{
use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
use futures::{FutureExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use language::LanguageRegistry;
use live_kit_client::{
LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate,
@ -19,7 +21,8 @@ use live_kit_client::{
};
use postage::{sink::Sink, stream::Stream, watch};
use project::Project;
use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
use settings::Settings as _;
use std::{future::Future, mem, sync::Arc, time::Duration};
use util::{post_inc, ResultExt, TryFutureExt};
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@ -54,11 +57,11 @@ pub enum Event {
pub struct Room {
id: u64,
pub channel_id: Option<u64>,
channel_id: Option<u64>,
live_kit: Option<LiveKitRoom>,
status: RoomStatus,
shared_projects: HashSet<WeakModelHandle<Project>>,
joined_projects: HashSet<WeakModelHandle<Project>>,
shared_projects: HashSet<WeakModel<Project>>,
joined_projects: HashSet<WeakModel<Project>>,
local_participant: LocalParticipant,
remote_participants: BTreeMap<u64, RemoteParticipant>,
pending_participants: Vec<Arc<User>>,
@ -66,39 +69,17 @@ pub struct Room {
pending_call_count: usize,
leave_when_empty: bool,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
user_store: Model<UserStore>,
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
subscriptions: Vec<client::Subscription>,
client_subscriptions: Vec<client::Subscription>,
_subscriptions: Vec<gpui::Subscription>,
room_update_completed_tx: watch::Sender<Option<()>>,
room_update_completed_rx: watch::Receiver<Option<()>>,
pending_room_update: Option<Task<()>>,
maintain_connection: Option<Task<Option<()>>>,
}
impl Entity for Room {
type Event = Event;
fn release(&mut self, cx: &mut AppContext) {
if self.status.is_online() {
self.leave_internal(cx).detach_and_log_err(cx);
}
}
fn app_will_quit(&mut self, cx: &mut AppContext) -> Option<Pin<Box<dyn Future<Output = ()>>>> {
if self.status.is_online() {
let leave = self.leave_internal(cx);
Some(
cx.background()
.spawn(async move {
leave.await.log_err();
})
.boxed(),
)
} else {
None
}
}
}
impl EventEmitter<Event> for Room {}
impl Room {
pub fn channel_id(&self) -> Option<u64> {
@ -121,16 +102,12 @@ impl Room {
}
}
pub fn can_publish(&self) -> bool {
self.live_kit.as_ref().is_some_and(|room| room.can_publish)
}
fn new(
id: u64,
channel_id: Option<u64>,
live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
user_store: Model<UserStore>,
cx: &mut ModelContext<Self>,
) -> Self {
let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
@ -138,69 +115,75 @@ impl Room {
let mut status = room.status();
// Consume the initial status of the room.
let _ = status.try_recv();
let _maintain_room = cx.spawn_weak(|this, mut cx| async move {
let _maintain_room = cx.spawn(|this, mut cx| async move {
while let Some(status) = status.next().await {
let this = if let Some(this) = this.upgrade(&cx) {
let this = if let Some(this) = this.upgrade() {
this
} else {
break;
};
if status == live_kit_client::ConnectionState::Disconnected {
this.update(&mut cx, |this, cx| this.leave(cx).log_err());
this.update(&mut cx, |this, cx| this.leave(cx).log_err())
.ok();
break;
}
}
});
let mut track_video_changes = room.remote_video_track_updates();
let _maintain_video_tracks = cx.spawn_weak(|this, mut cx| async move {
while let Some(track_change) = track_video_changes.next().await {
let this = if let Some(this) = this.upgrade(&cx) {
this
} else {
break;
};
let _maintain_video_tracks = cx.spawn({
let room = room.clone();
move |this, mut cx| async move {
let mut track_video_changes = room.remote_video_track_updates();
while let Some(track_change) = track_video_changes.next().await {
let this = if let Some(this) = this.upgrade() {
this
} else {
break;
};
this.update(&mut cx, |this, cx| {
this.remote_video_track_updated(track_change, cx).log_err()
});
this.update(&mut cx, |this, cx| {
this.remote_video_track_updated(track_change, cx).log_err()
})
.ok();
}
}
});
let mut track_audio_changes = room.remote_audio_track_updates();
let _maintain_audio_tracks = cx.spawn_weak(|this, mut cx| async move {
while let Some(track_change) = track_audio_changes.next().await {
let this = if let Some(this) = this.upgrade(&cx) {
this
} else {
break;
};
let _maintain_audio_tracks = cx.spawn({
let room = room.clone();
|this, mut cx| async move {
let mut track_audio_changes = room.remote_audio_track_updates();
while let Some(track_change) = track_audio_changes.next().await {
let this = if let Some(this) = this.upgrade() {
this
} else {
break;
};
this.update(&mut cx, |this, cx| {
this.remote_audio_track_updated(track_change, cx).log_err()
});
this.update(&mut cx, |this, cx| {
this.remote_audio_track_updated(track_change, cx).log_err()
})
.ok();
}
}
});
let connect = room.connect(&connection_info.server_url, &connection_info.token);
if connection_info.can_publish {
cx.spawn(|this, mut cx| async move {
connect.await?;
cx.spawn(|this, mut cx| async move {
connect.await?;
if !cx.read(Self::mute_on_join) {
this.update(&mut cx, |this, cx| this.share_microphone(cx))
.await?;
}
if !cx.update(|cx| Self::mute_on_join(cx))? {
this.update(&mut cx, |this, cx| this.share_microphone(cx))?
.await?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
Some(LiveKitRoom {
room,
can_publish: connection_info.can_publish,
screen_track: LocalTrack::None,
microphone_track: LocalTrack::None,
next_publish_id: 0,
@ -214,8 +197,10 @@ impl Room {
None
};
let maintain_connection =
cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx).log_err());
let maintain_connection = cx.spawn({
let client = client.clone();
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
});
Audio::play_sound(Sound::Joined, cx);
@ -233,7 +218,13 @@ impl Room {
remote_participants: Default::default(),
pending_participants: Default::default(),
pending_call_count: 0,
subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)],
client_subscriptions: vec![
client.add_message_handler(cx.weak_model(), Self::handle_room_updated)
],
_subscriptions: vec![
cx.on_release(Self::released),
cx.on_app_quit(Self::app_will_quit),
],
leave_when_empty: false,
pending_room_update: None,
client,
@ -247,15 +238,15 @@ impl Room {
pub(crate) fn create(
called_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
initial_project: Option<Model<Project>>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
user_store: Model<UserStore>,
cx: &mut AppContext,
) -> Task<Result<ModelHandle<Self>>> {
cx.spawn(|mut cx| async move {
) -> Task<Result<Model<Self>>> {
cx.spawn(move |mut cx| async move {
let response = client.request(proto::CreateRoom {}).await?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.add_model(|cx| {
let room = cx.new_model(|cx| {
Self::new(
room_proto.id,
None,
@ -264,13 +255,13 @@ impl Room {
user_store,
cx,
)
});
})?;
let initial_project_id = if let Some(initial_project) = initial_project {
let initial_project_id = room
.update(&mut cx, |room, cx| {
room.share_project(initial_project.clone(), cx)
})
})?
.await?;
Some(initial_project_id)
} else {
@ -281,7 +272,7 @@ impl Room {
.update(&mut cx, |room, cx| {
room.leave_when_empty = true;
room.call(called_user_id, initial_project_id, cx)
})
})?
.await
{
Ok(()) => Ok(room),
@ -293,9 +284,9 @@ impl Room {
pub(crate) async fn join_channel(
channel_id: u64,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
user_store: Model<UserStore>,
cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> {
) -> Result<Model<Self>> {
Self::from_join_response(
client.request(proto::JoinChannel { channel_id }).await?,
client,
@ -307,9 +298,9 @@ impl Room {
pub(crate) async fn join(
room_id: u64,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
user_store: Model<UserStore>,
cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> {
) -> Result<Model<Self>> {
Self::from_join_response(
client.request(proto::JoinRoom { id: room_id }).await?,
client,
@ -318,18 +309,41 @@ impl Room {
)
}
fn released(&mut self, cx: &mut AppContext) {
if self.status.is_online() {
self.leave_internal(cx).detach_and_log_err(cx);
}
}
fn app_will_quit(&mut self, cx: &mut ModelContext<Self>) -> impl Future<Output = ()> {
let task = if self.status.is_online() {
let leave = self.leave_internal(cx);
Some(cx.background_executor().spawn(async move {
leave.await.log_err();
}))
} else {
None
};
async move {
if let Some(task) = task {
task.await;
}
}
}
pub fn mute_on_join(cx: &AppContext) -> bool {
settings::get::<CallSettings>(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
}
fn from_join_response(
response: proto::JoinRoomResponse,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
user_store: Model<UserStore>,
mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> {
) -> Result<Model<Self>> {
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.add_model(|cx| {
let room = cx.new_model(|cx| {
Self::new(
room_proto.id,
response.channel_id,
@ -338,12 +352,12 @@ impl Room {
user_store,
cx,
)
});
})?;
room.update(&mut cx, |room, cx| {
room.leave_when_empty = room.channel_id.is_none();
room.apply_room_update(room_proto, cx)?;
anyhow::Ok(())
})?;
})??;
Ok(room)
}
@ -372,7 +386,7 @@ impl Room {
self.clear_state(cx);
let leave_room = self.client.request(proto::LeaveRoom {});
cx.background().spawn(async move {
cx.background_executor().spawn(async move {
leave_room.await?;
anyhow::Ok(())
})
@ -380,14 +394,14 @@ impl Room {
pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
for project in self.shared_projects.drain() {
if let Some(project) = project.upgrade(cx) {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| {
project.unshare(cx).log_err();
});
}
}
for project in self.joined_projects.drain() {
if let Some(project) = project.upgrade(cx) {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| {
project.disconnected_from_host(cx);
project.close(cx);
@ -399,14 +413,14 @@ impl Room {
self.remote_participants.clear();
self.pending_participants.clear();
self.participant_user_ids.clear();
self.subscriptions.clear();
self.client_subscriptions.clear();
self.live_kit.take();
self.pending_room_update.take();
self.maintain_connection.take();
}
async fn maintain_connection(
this: WeakModelHandle<Self>,
this: WeakModel<Self>,
client: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
@ -418,32 +432,33 @@ impl Room {
if !is_connected || client_status.next().await.is_some() {
log::info!("detected client disconnection");
this.upgrade(&cx)
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
this.status = RoomStatus::Rejoining;
cx.notify();
});
})?;
// Wait for client to re-establish a connection to the server.
{
let mut reconnection_timeout = cx.background().timer(RECONNECT_TIMEOUT).fuse();
let mut reconnection_timeout =
cx.background_executor().timer(RECONNECT_TIMEOUT).fuse();
let client_reconnection = async {
let mut remaining_attempts = 3;
while remaining_attempts > 0 {
if client_status.borrow().is_connected() {
log::info!("client reconnected, attempting to rejoin room");
let Some(this) = this.upgrade(&cx) else { break };
if this
.update(&mut cx, |this, cx| this.rejoin(cx))
.await
.log_err()
.is_some()
{
return true;
} else {
remaining_attempts -= 1;
let Some(this) = this.upgrade() else { break };
match this.update(&mut cx, |this, cx| this.rejoin(cx)) {
Ok(task) => {
if task.await.log_err().is_some() {
return true;
} else {
remaining_attempts -= 1;
}
}
Err(_app_dropped) => return false,
}
} else if client_status.borrow().is_signed_out() {
return false;
@ -482,9 +497,9 @@ impl Room {
// The client failed to re-establish a connection to the server
// or an error occurred while trying to re-join the room. Either way
// we leave the room and return an error.
if let Some(this) = this.upgrade(&cx) {
if let Some(this) = this.upgrade() {
log::info!("reconnection failed, leaving room");
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
let _ = this.update(&mut cx, |this, cx| this.leave(cx))?;
}
Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
@ -496,7 +511,7 @@ impl Room {
let mut reshared_projects = Vec::new();
let mut rejoined_projects = Vec::new();
self.shared_projects.retain(|project| {
if let Some(handle) = project.upgrade(cx) {
if let Some(handle) = project.upgrade() {
let project = handle.read(cx);
if let Some(project_id) = project.remote_id() {
projects.insert(project_id, handle.clone());
@ -510,14 +525,14 @@ impl Room {
false
});
self.joined_projects.retain(|project| {
if let Some(handle) = project.upgrade(cx) {
if let Some(handle) = project.upgrade() {
let project = handle.read(cx);
if let Some(project_id) = project.remote_id() {
projects.insert(project_id, handle.clone());
rejoined_projects.push(proto::RejoinProject {
id: project_id,
worktrees: project
.worktrees(cx)
.worktrees()
.map(|worktree| {
let worktree = worktree.read(cx);
proto::RejoinWorktree {
@ -565,7 +580,7 @@ impl Room {
}
anyhow::Ok(())
})
})?
})
}
@ -643,7 +658,7 @@ impl Room {
}
async fn handle_room_updated(
this: ModelHandle<Self>,
this: Model<Self>,
envelope: TypedEnvelope<proto::RoomUpdated>,
_: Arc<Client>,
mut cx: AsyncAppContext,
@ -652,7 +667,7 @@ impl Room {
.payload
.room
.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
}
fn apply_room_update(
@ -733,7 +748,7 @@ impl Room {
for unshared_project_id in old_projects.difference(&new_projects) {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade(cx) {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| {
if project.remote_id() == Some(*unshared_project_id) {
project.disconnected_from_host(cx);
@ -876,7 +891,8 @@ impl Room {
this.check_invariants();
this.room_update_completed_tx.try_send(Some(())).ok();
cx.notify();
});
})
.ok();
}));
cx.notify();
@ -907,12 +923,7 @@ impl Room {
.remote_participants
.get_mut(&user_id)
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
participant.video_tracks.insert(
track_id.clone(),
Arc::new(RemoteVideoTrack {
live_kit_track: track,
}),
);
participant.video_tracks.insert(track_id.clone(), track);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
});
@ -991,7 +1002,6 @@ impl Room {
.remote_participants
.get_mut(&user_id)
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
participant.audio_tracks.insert(track_id.clone(), track);
participant.muted = publication.is_muted();
@ -1053,7 +1063,7 @@ impl Room {
let client = self.client.clone();
let room_id = self.id;
self.pending_call_count += 1;
cx.spawn(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let result = client
.request(proto::Call {
room_id,
@ -1066,7 +1076,7 @@ impl Room {
if this.should_leave() {
this.leave(cx).detach_and_log_err(cx);
}
});
})?;
result?;
Ok(())
})
@ -1078,31 +1088,31 @@ impl Room {
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Project>>> {
) -> Task<Result<Model<Project>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
cx.emit(Event::RemoteProjectJoined { project_id: id });
cx.spawn(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let project =
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
this.update(&mut cx, |this, cx| {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade(cx) {
if let Some(project) = project.upgrade() {
!project.read(cx).is_read_only()
} else {
false
}
});
this.joined_projects.insert(project.downgrade());
});
})?;
Ok(project)
})
}
pub(crate) fn share_project(
&mut self,
project: ModelHandle<Project>,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some(project_id) = project.read(cx).remote_id() {
@ -1118,7 +1128,7 @@ impl Room {
project.update(&mut cx, |project, cx| {
project.shared(response.project_id, cx)
})?;
})??;
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
this.update(&mut cx, |this, cx| {
@ -1129,7 +1139,7 @@ impl Room {
} else {
Task::ready(Ok(()))
}
})
})?
.await?;
Ok(response.project_id)
@ -1138,7 +1148,7 @@ impl Room {
pub(crate) fn unshare_project(
&mut self,
project: ModelHandle<Project>,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
let project_id = match project.read(cx).remote_id() {
@ -1152,7 +1162,7 @@ impl Room {
pub(crate) fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if self.status.is_offline() {
@ -1178,7 +1188,7 @@ impl Room {
};
cx.notify();
cx.foreground().spawn(async move {
cx.background_executor().spawn(async move {
client
.request(proto::UpdateParticipantLocation {
room_id,
@ -1244,22 +1254,21 @@ impl Room {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
cx.spawn_weak(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let publish_track = async {
let track = LocalAudioTrack::create();
this.upgrade(&cx)
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.read_with(&cx, |this, _| {
.update(&mut cx, |this, _| {
this.live_kit
.as_ref()
.map(|live_kit| live_kit.room.publish_audio_track(track))
})
})?
.ok_or_else(|| anyhow!("live-kit was not initialized"))?
.await
};
let publication = publish_track.await;
this.upgrade(&cx)
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
let live_kit = this
@ -1283,7 +1292,9 @@ impl Room {
live_kit.room.unpublish_track(publication);
} else {
if muted {
cx.background().spawn(publication.set_mute(muted)).detach();
cx.background_executor()
.spawn(publication.set_mute(muted))
.detach();
}
live_kit.microphone_track = LocalTrack::Published {
track_publication: publication,
@ -1303,7 +1314,7 @@ impl Room {
}
}
}
})
})?
})
}
@ -1326,26 +1337,26 @@ impl Room {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
cx.spawn_weak(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let publish_track = async {
let displays = displays.await?;
let display = displays
.first()
.ok_or_else(|| anyhow!("no display found"))?;
let track = LocalVideoTrack::screen_share_for_display(&display);
this.upgrade(&cx)
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.read_with(&cx, |this, _| {
.update(&mut cx, |this, _| {
this.live_kit
.as_ref()
.map(|live_kit| live_kit.room.publish_video_track(track))
})
})?
.ok_or_else(|| anyhow!("live-kit was not initialized"))?
.await
};
let publication = publish_track.await;
this.upgrade(&cx)
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
let live_kit = this
@ -1369,7 +1380,9 @@ impl Room {
live_kit.room.unpublish_track(publication);
} else {
if muted {
cx.background().spawn(publication.set_mute(muted)).detach();
cx.background_executor()
.spawn(publication.set_mute(muted))
.detach();
}
live_kit.screen_track = LocalTrack::Published {
track_publication: publication,
@ -1392,7 +1405,7 @@ impl Room {
}
}
}
})
})?
})
}
@ -1435,11 +1448,12 @@ impl Room {
.room
.remote_audio_track_publications(&participant.user.id.to_string())
{
tasks.push(cx.foreground().spawn(track.set_enabled(!live_kit.deafened)));
let deafened = live_kit.deafened;
tasks.push(cx.foreground_executor().spawn(track.set_enabled(!deafened)));
}
}
Ok(cx.foreground().spawn(async move {
Ok(cx.foreground_executor().spawn(async move {
if let Some(mute_task) = mute_task {
mute_task.await?;
}
@ -1499,7 +1513,6 @@ struct LiveKitRoom {
deafened: bool,
speaking: bool,
next_publish_id: usize,
can_publish: bool,
_maintain_room: Task<()>,
_maintain_tracks: [Task<()>; 2],
}
@ -1531,7 +1544,8 @@ impl LiveKitRoom {
*muted = should_mute;
cx.notify();
Ok((
cx.background().spawn(track_publication.set_mute(*muted)),
cx.background_executor()
.spawn(track_publication.set_mute(*muted)),
old_muted,
))
}

View File

@ -1,54 +0,0 @@
[package]
name = "call2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/call2.rs"
doctest = false
[features]
test-support = [
"client/test-support",
"collections/test-support",
"gpui/test-support",
"live_kit_client/test-support",
"project/test-support",
"util/test-support"
]
[dependencies]
audio = { package = "audio2", path = "../audio2" }
client = { package = "client2", path = "../client2" }
collections = { path = "../collections" }
gpui = { package = "gpui2", path = "../gpui2" }
log.workspace = true
live_kit_client = { package = "live_kit_client2", path = "../live_kit_client2" }
fs = { package = "fs2", path = "../fs2" }
language = { path = "../language" }
media = { path = "../media" }
project = { path = "../project" }
settings = { package = "settings2", path = "../settings2" }
util = { path = "../util" }
anyhow.workspace = true
async-broadcast = "0.4"
futures.workspace = true
image = "0.23"
postage.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
[dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] }
fs = { package = "fs2", path = "../fs2", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
live_kit_client = { package = "live_kit_client2", path = "../live_kit_client2", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

View File

@ -1,543 +0,0 @@
pub mod call_settings;
pub mod participant;
pub mod room;
use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings;
use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
WeakModel,
};
use postage::watch;
use project::Project;
use room::Event;
use settings::Settings;
use std::sync::Arc;
pub use participant::ParticipantLocation;
pub use room::Room;
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
CallSettings::register(cx);
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(active_call);
}
pub struct OneAtATime {
cancel: Option<oneshot::Sender<()>>,
}
impl OneAtATime {
/// spawn a task in the given context.
/// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None)
/// otherwise you'll see the result of the task.
fn spawn<F, Fut, R>(&mut self, cx: &mut AppContext, f: F) -> Task<Result<Option<R>>>
where
F: 'static + FnOnce(AsyncAppContext) -> Fut,
Fut: Future<Output = Result<R>>,
R: 'static,
{
let (tx, rx) = oneshot::channel();
self.cancel.replace(tx);
cx.spawn(|cx| async move {
futures::select_biased! {
_ = rx.fuse() => Ok(None),
result = f(cx).fuse() => result.map(Some),
}
})
}
fn running(&self) -> bool {
self.cancel
.as_ref()
.is_some_and(|cancel| !cancel.is_canceled())
}
}
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub calling_user: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
/// Singleton global maintaining the user's participation in a room across workspaces.
pub struct ActiveCall {
room: Option<(Model<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<Model<Room>, Arc<anyhow::Error>>>>>,
location: Option<WeakModel<Project>>,
_join_debouncer: OneAtATime,
pending_invites: HashSet<u64>,
incoming_call: (
watch::Sender<Option<IncomingCall>>,
watch::Receiver<Option<IncomingCall>>,
),
client: Arc<Client>,
user_store: Model<UserStore>,
_subscriptions: Vec<client::Subscription>,
}
impl EventEmitter<Event> for ActiveCall {}
impl ActiveCall {
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
Self {
room: None,
pending_room_creation: None,
location: None,
pending_invites: Default::default(),
incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
],
client,
user_store,
}
}
pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
self.room()?.read(cx).channel_id()
}
async fn handle_incoming_call(
this: Model<Self>,
envelope: TypedEnvelope<proto::IncomingCall>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.calling_user_id, cx)
})?
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
})?;
Ok(proto::Ack {})
}
async fn handle_call_canceled(
this: Model<Self>,
envelope: TypedEnvelope<proto::CallCanceled>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
let mut incoming_call = this.incoming_call.0.borrow_mut();
if incoming_call
.as_ref()
.map_or(false, |call| call.room_id == envelope.payload.room_id)
{
incoming_call.take();
}
})?;
Ok(())
}
pub fn global(cx: &AppContext) -> Model<Self> {
cx.global::<Model<Self>>().clone()
}
pub fn invite(
&mut self,
called_user_id: u64,
initial_project: Option<Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
cx.notify();
if self._join_debouncer.running() {
return Task::ready(Ok(()));
}
let room = if let Some(room) = self.room().cloned() {
Some(Task::ready(Ok(room)).shared())
} else {
self.pending_room_creation.clone()
};
let invite = if let Some(room) = room {
cx.spawn(move |_, mut cx| async move {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
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, move |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(move |this, mut cx| async move {
let create_room = async {
let room = cx
.update(|cx| {
Room::create(
called_user_id,
initial_project,
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.background_executor().spawn(async move {
room.await.map_err(|err| anyhow!("{:?}", err))?;
anyhow::Ok(())
})
};
cx.spawn(move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
} else {
// TODO: Resport collaboration error
}
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
cx.notify();
})?;
result
})
}
pub fn cancel_invite(
&mut self,
called_user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
} else {
return Task::ready(Err(anyhow!("no active call")));
};
let client = self.client.clone();
cx.background_executor().spawn(async move {
client
.request(proto::CancelCall {
room_id,
called_user_id,
})
.await?;
anyhow::Ok(())
})
}
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
self.incoming_call.1.clone()
}
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
let call = if let Some(call) = self.incoming_call.1.borrow().clone() {
call
} else {
return Task::ready(Err(anyhow!("no incoming call")));
};
if self.pending_room_creation.is_some() {
return Task::ready(Ok(()));
}
let room_id = call.room_id.clone();
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self
._join_debouncer
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
let call = self
.incoming_call
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
Ok(())
}
pub fn join_channel(
&mut self,
channel_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
} else {
room.update(cx, |room, cx| room.clear_state(cx));
}
}
if self.pending_room_creation.is_some() {
return Task::ready(Ok(None));
}
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self._join_debouncer.spawn(cx, move |cx| async move {
Room::join_channel(channel_id, client, user_store, cx).await
});
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
}
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("hang up", cx);
Audio::end_call(cx);
if let Some((room, _)) = self.room.take() {
room.update(cx, |room, cx| room.leave(cx))
} else {
Task::ready(Ok(()))
}
}
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
pub fn unshare_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
}
pub fn location(&self) -> Option<&WeakModel<Project>> {
self.location.as_ref()
}
pub fn set_location(
&mut self,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
return room.update(cx, |room, cx| room.set_location(project, cx));
}
}
Task::ready(Ok(()))
}
fn set_room(
&mut self,
room: Option<Model<Room>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if room.as_ref() != self.room.as_ref().map(|room| &room.0) {
cx.notify();
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, cx).detach_and_log_err(cx);
}
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room.clone(), subscriptions));
let location = self
.location
.as_ref()
.and_then(|location| location.upgrade());
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {
self.room = None;
Task::ready(Ok(()))
}
} else {
Task::ready(Ok(()))
}
}
pub fn room(&self) -> Option<&Model<Room>> {
self.room.as_ref().map(|(room, _)| room)
}
pub fn client(&self) -> Arc<Client> {
self.client.clone()
}
pub fn pending_invites(&self) -> &HashSet<u64> {
&self.pending_invites
}
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
if let Some(room) = self.room() {
let room = room.read(cx);
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx);
}
}
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: u64,
channel_id: Option<u64>,
client: &Arc<Client>,
cx: &mut AppContext,
) {
let telemetry = client.telemetry();
let telemetry_settings = *TelemetrySettings::get_global(cx);
telemetry.report_call_event(telemetry_settings, operation, Some(room_id), channel_id)
}
pub fn report_call_event_for_channel(
operation: &'static str,
channel_id: u64,
client: &Arc<Client>,
cx: &AppContext,
) {
let room = ActiveCall::global(cx).read(cx).room();
let telemetry = client.telemetry();
let telemetry_settings = *TelemetrySettings::get_global(cx);
telemetry.report_call_event(
telemetry_settings,
operation,
room.map(|r| r.read(cx).id()),
Some(channel_id),
)
}
#[cfg(test)]
mod test {
use gpui::TestAppContext;
use crate::OneAtATime;
#[gpui::test]
async fn test_one_at_a_time(cx: &mut TestAppContext) {
let mut one_at_a_time = OneAtATime { cancel: None };
assert_eq!(
cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) }))
.await
.unwrap(),
Some(1)
);
let (a, b) = cx.update(|cx| {
(
one_at_a_time.spawn(cx, |_| async {
assert!(false);
Ok(2)
}),
one_at_a_time.spawn(cx, |_| async { Ok(3) }),
)
});
assert_eq!(a.await.unwrap(), None);
assert_eq!(b.await.unwrap(), Some(3));
let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
drop(one_at_a_time);
assert_eq!(promise.await.unwrap(), None);
}
}

View File

@ -1,32 +0,0 @@
use anyhow::Result;
use gpui::AppContext;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
#[derive(Deserialize, Debug)]
pub struct CallSettings {
pub mute_on_join: bool,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct CallSettingsContent {
pub mute_on_join: Option<bool>,
}
impl Settings for CallSettings {
const KEY: Option<&'static str> = Some("calls");
type FileContent = CallSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_cx: &mut AppContext,
) -> Result<Self>
where
Self: Sized,
{
Self::load_via_json_merge(default_value, user_values)
}
}

View File

@ -1,52 +0,0 @@
use anyhow::{anyhow, Result};
use client::ParticipantIndex;
use client::{proto, User};
use collections::HashMap;
use gpui::WeakModel;
pub use live_kit_client::Frame;
pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
use project::Project;
use std::sync::Arc;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ParticipantLocation {
SharedProject { project_id: u64 },
UnsharedProject,
External,
}
impl ParticipantLocation {
pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
match location.and_then(|l| l.variant) {
Some(proto::participant_location::Variant::SharedProject(project)) => {
Ok(Self::SharedProject {
project_id: project.id,
})
}
Some(proto::participant_location::Variant::UnsharedProject(_)) => {
Ok(Self::UnsharedProject)
}
Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
None => Err(anyhow!("participant location was not provided")),
}
}
}
#[derive(Clone, Default)]
pub struct LocalParticipant {
pub projects: Vec<proto::ParticipantProject>,
pub active_project: Option<WeakModel<Project>>,
}
#[derive(Clone, Debug)]
pub struct RemoteParticipant {
pub user: Arc<User>,
pub peer_id: proto::PeerId,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
pub participant_index: ParticipantIndex,
pub muted: bool,
pub speaking: bool,
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
}

File diff suppressed because it is too large Load Diff

View File

@ -14,13 +14,13 @@ test-support = ["collections/test-support", "gpui/test-support", "rpc/test-suppo
[dependencies]
client = { path = "../client" }
collections = { path = "../collections" }
db = { path = "../db" }
gpui = { path = "../gpui" }
db = { package = "db2", path = "../db2" }
gpui = { package = "gpui2", path = "../gpui2" }
util = { path = "../util" }
rpc = { path = "../rpc" }
text = { path = "../text" }
rpc = { package = "rpc2", path = "../rpc2" }
text = { package = "text2", path = "../text2" }
language = { path = "../language" }
settings = { path = "../settings" }
settings = { package = "settings2", path = "../settings2" }
feature_flags = { path = "../feature_flags" }
sum_tree = { path = "../sum_tree" }
clock = { path = "../clock" }
@ -47,8 +47,8 @@ tempfile = "3"
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

View File

@ -3,7 +3,7 @@ mod channel_chat;
mod channel_store;
use client::{Client, UserStore};
use gpui::{AppContext, ModelHandle};
use gpui::{AppContext, Model};
use std::sync::Arc;
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
@ -16,7 +16,7 @@ pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, Cha
#[cfg(test)]
mod channel_store_tests;
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
channel_store::init(client, user_store, cx);
channel_buffer::init(client);
channel_chat::init(client);

View File

@ -2,7 +2,7 @@ use crate::{Channel, ChannelId, ChannelStore};
use anyhow::Result;
use client::{Client, Collaborator, UserStore};
use collections::HashMap;
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
use language::proto::serialize_version;
use rpc::{
proto::{self, PeerId},
@ -22,9 +22,9 @@ pub struct ChannelBuffer {
pub channel_id: ChannelId,
connected: bool,
collaborators: HashMap<PeerId, Collaborator>,
user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
buffer: ModelHandle<language::Buffer>,
user_store: Model<UserStore>,
channel_store: Model<ChannelStore>,
buffer: Model<language::Buffer>,
buffer_epoch: u64,
client: Arc<Client>,
subscription: Option<client::Subscription>,
@ -38,31 +38,16 @@ pub enum ChannelBufferEvent {
ChannelChanged,
}
impl Entity for ChannelBuffer {
type Event = ChannelBufferEvent;
fn release(&mut self, _: &mut AppContext) {
if self.connected {
if let Some(task) = self.acknowledge_task.take() {
task.detach();
}
self.client
.send(proto::LeaveChannelBuffer {
channel_id: self.channel_id,
})
.log_err();
}
}
}
impl EventEmitter<ChannelBufferEvent> for ChannelBuffer {}
impl ChannelBuffer {
pub(crate) async fn new(
channel: Arc<Channel>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
user_store: Model<UserStore>,
channel_store: Model<ChannelStore>,
mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> {
) -> Result<Model<Self>> {
let response = client
.request(proto::JoinChannelBuffer {
channel_id: channel.id,
@ -76,16 +61,16 @@ impl ChannelBuffer {
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?;
let buffer = cx.add_model(|_| {
let buffer = cx.new_model(|_| {
language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
});
buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?;
})?;
buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))??;
let subscription = client.subscribe_to_entity(channel.id)?;
anyhow::Ok(cx.add_model(|cx| {
anyhow::Ok(cx.new_model(|cx| {
cx.subscribe(&buffer, Self::on_buffer_update).detach();
cx.on_release(Self::release).detach();
let mut this = Self {
buffer,
buffer_epoch: response.epoch,
@ -100,14 +85,27 @@ impl ChannelBuffer {
};
this.replace_collaborators(response.collaborators, cx);
this
}))
})?)
}
fn release(&mut self, _: &mut AppContext) {
if self.connected {
if let Some(task) = self.acknowledge_task.take() {
task.detach();
}
self.client
.send(proto::LeaveChannelBuffer {
channel_id: self.channel_id,
})
.log_err();
}
}
pub fn remote_id(&self, cx: &AppContext) -> u64 {
self.buffer.read(cx).remote_id()
}
pub fn user_store(&self) -> &ModelHandle<UserStore> {
pub fn user_store(&self) -> &Model<UserStore> {
&self.user_store
}
@ -136,7 +134,7 @@ impl ChannelBuffer {
}
async fn handle_update_channel_buffer(
this: ModelHandle<Self>,
this: Model<Self>,
update_channel_buffer: TypedEnvelope<proto::UpdateChannelBuffer>,
_: Arc<Client>,
mut cx: AsyncAppContext,
@ -152,13 +150,13 @@ impl ChannelBuffer {
cx.notify();
this.buffer
.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))
})?;
})??;
Ok(())
}
async fn handle_update_channel_buffer_collaborators(
this: ModelHandle<Self>,
this: Model<Self>,
message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
_: Arc<Client>,
mut cx: AsyncAppContext,
@ -167,14 +165,12 @@ impl ChannelBuffer {
this.replace_collaborators(message.payload.collaborators, cx);
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
});
Ok(())
})
}
fn on_buffer_update(
&mut self,
_: ModelHandle<language::Buffer>,
_: Model<language::Buffer>,
event: &language::Event,
cx: &mut ModelContext<Self>,
) {
@ -202,8 +198,10 @@ impl ChannelBuffer {
let client = self.client.clone();
let epoch = self.epoch();
self.acknowledge_task = Some(cx.spawn_weak(|_, cx| async move {
cx.background().timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL).await;
self.acknowledge_task = Some(cx.spawn(move |_, cx| async move {
cx.background_executor()
.timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL)
.await;
client
.send(proto::AckBufferOperation {
buffer_id,
@ -219,7 +217,7 @@ impl ChannelBuffer {
self.buffer_epoch
}
pub fn buffer(&self) -> ModelHandle<language::Buffer> {
pub fn buffer(&self) -> Model<language::Buffer> {
self.buffer.clone()
}

View File

@ -6,7 +6,7 @@ use client::{
Client, Subscription, TypedEnvelope, UserId,
};
use futures::lock::Mutex;
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
use rand::prelude::*;
use std::{
collections::HashSet,
@ -22,11 +22,11 @@ pub struct ChannelChat {
pub channel_id: ChannelId,
messages: SumTree<ChannelMessage>,
acknowledged_message_ids: HashSet<u64>,
channel_store: ModelHandle<ChannelStore>,
channel_store: Model<ChannelStore>,
loaded_all_messages: bool,
last_acknowledged_id: Option<u64>,
next_pending_message_id: usize,
user_store: ModelHandle<UserStore>,
user_store: Model<UserStore>,
rpc: Arc<Client>,
outgoing_messages_lock: Arc<Mutex<()>>,
rng: StdRng,
@ -76,31 +76,20 @@ pub enum ChannelChatEvent {
},
}
impl EventEmitter<ChannelChatEvent> for ChannelChat {}
pub fn init(client: &Arc<Client>) {
client.add_model_message_handler(ChannelChat::handle_message_sent);
client.add_model_message_handler(ChannelChat::handle_message_removed);
}
impl Entity for ChannelChat {
type Event = ChannelChatEvent;
fn release(&mut self, _: &mut AppContext) {
self.rpc
.send(proto::LeaveChannelChat {
channel_id: self.channel_id,
})
.log_err();
}
}
impl ChannelChat {
pub async fn new(
channel: Arc<Channel>,
channel_store: ModelHandle<ChannelStore>,
user_store: ModelHandle<UserStore>,
channel_store: Model<ChannelStore>,
user_store: Model<UserStore>,
client: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> {
) -> Result<Model<Self>> {
let channel_id = channel.id;
let subscription = client.subscribe_to_entity(channel_id).unwrap();
@ -110,7 +99,8 @@ impl ChannelChat {
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
let loaded_all_messages = response.done;
Ok(cx.add_model(|cx| {
Ok(cx.new_model(|cx| {
cx.on_release(Self::release).detach();
let mut this = Self {
channel_id: channel.id,
user_store,
@ -127,7 +117,15 @@ impl ChannelChat {
};
this.insert_messages(messages, cx);
this
}))
})?)
}
fn release(&mut self, _: &mut AppContext) {
self.rpc
.send(proto::LeaveChannelChat {
channel_id: self.channel_id,
})
.log_err();
}
pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
@ -176,7 +174,7 @@ impl ChannelChat {
let user_store = self.user_store.clone();
let rpc = self.rpc.clone();
let outgoing_messages_lock = self.outgoing_messages_lock.clone();
Ok(cx.spawn(|this, mut cx| async move {
Ok(cx.spawn(move |this, mut cx| async move {
let outgoing_message_guard = outgoing_messages_lock.lock().await;
let request = rpc.request(proto::SendChannelMessage {
channel_id,
@ -191,8 +189,8 @@ impl ChannelChat {
let message = ChannelMessage::from_proto(response, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
Ok(id)
})
})?;
Ok(id)
}))
}
@ -201,13 +199,12 @@ impl ChannelChat {
channel_id: self.channel_id,
message_id: id,
});
cx.spawn(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
response.await?;
this.update(&mut cx, |this, cx| {
this.message_removed(id, cx);
Ok(())
})
})?;
Ok(())
})
}
@ -220,7 +217,7 @@ impl ChannelChat {
let user_store = self.user_store.clone();
let channel_id = self.channel_id;
let before_message_id = self.first_loaded_message_id()?;
Some(cx.spawn(|this, mut cx| {
Some(cx.spawn(move |this, mut cx| {
async move {
let response = rpc
.request(proto::GetChannelMessages {
@ -233,7 +230,7 @@ impl ChannelChat {
this.update(&mut cx, |this, cx| {
this.loaded_all_messages = loaded_all_messages;
this.insert_messages(messages, cx);
});
})?;
anyhow::Ok(())
}
.log_err()
@ -251,31 +248,33 @@ impl ChannelChat {
///
/// For now, we always maintain a suffix of the channel's messages.
pub async fn load_history_since_message(
chat: ModelHandle<Self>,
chat: Model<Self>,
message_id: u64,
mut cx: AsyncAppContext,
) -> Option<usize> {
loop {
let step = chat.update(&mut cx, |chat, cx| {
if let Some(first_id) = chat.first_loaded_message_id() {
if first_id <= message_id {
let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>();
let message_id = ChannelMessageId::Saved(message_id);
cursor.seek(&message_id, Bias::Left, &());
return ControlFlow::Break(
if cursor
.item()
.map_or(false, |message| message.id == message_id)
{
Some(cursor.start().1 .0)
} else {
None
},
);
let step = chat
.update(&mut cx, |chat, cx| {
if let Some(first_id) = chat.first_loaded_message_id() {
if first_id <= message_id {
let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>();
let message_id = ChannelMessageId::Saved(message_id);
cursor.seek(&message_id, Bias::Left, &());
return ControlFlow::Break(
if cursor
.item()
.map_or(false, |message| message.id == message_id)
{
Some(cursor.start().1 .0)
} else {
None
},
);
}
}
}
ControlFlow::Continue(chat.load_more_messages(cx))
});
ControlFlow::Continue(chat.load_more_messages(cx))
})
.log_err()?;
match step {
ControlFlow::Break(ix) => return ix,
ControlFlow::Continue(task) => task?.await?,
@ -307,7 +306,7 @@ impl ChannelChat {
let user_store = self.user_store.clone();
let rpc = self.rpc.clone();
let channel_id = self.channel_id;
cx.spawn(|this, mut cx| {
cx.spawn(move |this, mut cx| {
async move {
let response = rpc.request(proto::JoinChannelChat { channel_id }).await?;
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
@ -333,7 +332,7 @@ impl ChannelChat {
}
this.pending_messages().cloned().collect::<Vec<_>>()
});
})?;
for pending_message in pending_messages {
let request = rpc.request(proto::SendChannelMessage {
@ -351,7 +350,7 @@ impl ChannelChat {
.await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
});
})?;
}
anyhow::Ok(())
@ -399,12 +398,12 @@ impl ChannelChat {
}
async fn handle_message_sent(
this: ModelHandle<Self>,
this: Model<Self>,
message: TypedEnvelope<proto::ChannelMessageSent>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let message = message
.payload
.message
@ -418,20 +417,20 @@ impl ChannelChat {
channel_id: this.channel_id,
message_id,
})
});
})?;
Ok(())
}
async fn handle_message_removed(
this: ModelHandle<Self>,
this: Model<Self>,
message: TypedEnvelope<proto::RemoveChannelMessage>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.message_removed(message.payload.message_id, cx)
});
})?;
Ok(())
}
@ -515,7 +514,7 @@ impl ChannelChat {
async fn messages_from_proto(
proto_messages: Vec<proto::ChannelMessage>,
user_store: &ModelHandle<UserStore>,
user_store: &Model<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<SumTree<ChannelMessage>> {
let messages = ChannelMessage::from_proto_vec(proto_messages, user_store, cx).await?;
@ -527,13 +526,13 @@ async fn messages_from_proto(
impl ChannelMessage {
pub async fn from_proto(
message: proto::ChannelMessage,
user_store: &ModelHandle<UserStore>,
user_store: &Model<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<Self> {
let sender = user_store
.update(cx, |user_store, cx| {
user_store.get_user(message.sender_id, cx)
})
})?
.await?;
Ok(ChannelMessage {
id: ChannelMessageId::Saved(message.id),
@ -561,7 +560,7 @@ impl ChannelMessage {
pub async fn from_proto_vec(
proto_messages: Vec<proto::ChannelMessage>,
user_store: &ModelHandle<UserStore>,
user_store: &Model<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<Vec<Self>> {
let unique_user_ids = proto_messages
@ -573,7 +572,7 @@ impl ChannelMessage {
user_store
.update(cx, |user_store, cx| {
user_store.get_users(unique_user_ids, cx)
})
})?
.await?;
let mut messages = Vec::with_capacity(proto_messages.len());

View File

@ -7,17 +7,20 @@ use client::{Client, Subscription, User, UserId, UserStore};
use collections::{hash_map, HashMap, HashSet};
use db::RELEASE_CHANNEL;
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
WeakModel,
};
use rpc::{
proto::{self, ChannelVisibility},
TypedEnvelope,
};
use std::{mem, sync::Arc, time::Duration};
use util::ResultExt;
use util::{async_maybe, ResultExt};
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
cx.new_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
cx.set_global(channel_store);
}
@ -34,7 +37,7 @@ pub struct ChannelStore {
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
user_store: Model<UserStore>,
_rpc_subscription: Subscription,
_watch_connection_status: Task<Option<()>>,
disconnect_channel_buffers_task: Option<Task<()>>,
@ -44,7 +47,7 @@ pub struct ChannelStore {
#[derive(Clone, Debug, PartialEq)]
pub struct Channel {
pub id: ChannelId,
pub name: String,
pub name: SharedString,
pub visibility: proto::ChannelVisibility,
pub role: proto::ChannelRole,
pub unseen_note_version: Option<(u64, clock::Global)>,
@ -112,44 +115,45 @@ pub enum ChannelEvent {
ChannelRenamed(ChannelId),
}
impl Entity for ChannelStore {
type Event = ChannelEvent;
}
impl EventEmitter<ChannelEvent> for ChannelStore {}
enum OpenedModelHandle<E: Entity> {
Open(WeakModelHandle<E>),
Loading(Shared<Task<Result<ModelHandle<E>, Arc<anyhow::Error>>>>),
enum OpenedModelHandle<E> {
Open(WeakModel<E>),
Loading(Shared<Task<Result<Model<E>, Arc<anyhow::Error>>>>),
}
impl ChannelStore {
pub fn global(cx: &AppContext) -> ModelHandle<Self> {
cx.global::<ModelHandle<Self>>().clone()
pub fn global(cx: &AppContext) -> Model<Self> {
cx.global::<Model<Self>>().clone()
}
pub fn new(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
user_store: Model<UserStore>,
cx: &mut ModelContext<Self>,
) -> Self {
let rpc_subscription =
client.add_message_handler(cx.handle(), Self::handle_update_channels);
client.add_message_handler(cx.weak_model(), Self::handle_update_channels);
let mut connection_status = client.status();
let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
let watch_connection_status = cx.spawn(|this, mut cx| async move {
while let Some(status) = connection_status.next().await {
let this = this.upgrade(&cx)?;
let this = this.upgrade()?;
match status {
client::Status::Connected { .. } => {
this.update(&mut cx, |this, cx| this.handle_connect(cx))
.ok()?
.await
.log_err()?;
}
client::Status::SignedOut | client::Status::UpgradeRequired => {
this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx));
this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx))
.ok();
}
_ => {
this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx));
this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx))
.ok();
}
}
}
@ -169,17 +173,22 @@ impl ChannelStore {
_rpc_subscription: rpc_subscription,
_watch_connection_status: watch_connection_status,
disconnect_channel_buffers_task: None,
_update_channels: cx.spawn_weak(|this, mut cx| async move {
while let Some(update_channels) = update_channels_rx.next().await {
if let Some(this) = this.upgrade(&cx) {
let update_task = this.update(&mut cx, |this, cx| {
this.update_channels(update_channels, cx)
});
if let Some(update_task) = update_task {
update_task.await.log_err();
_update_channels: cx.spawn(|this, mut cx| async move {
async_maybe!({
while let Some(update_channels) = update_channels_rx.next().await {
if let Some(this) = this.upgrade() {
let update_task = this.update(&mut cx, |this, cx| {
this.update_channels(update_channels, cx)
})?;
if let Some(update_task) = update_task {
update_task.await.log_err();
}
}
}
}
anyhow::Ok(())
})
.await
.log_err();
}),
}
}
@ -240,10 +249,10 @@ impl ChannelStore {
self.channel_index.by_id().get(&channel_id)
}
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, cx: &AppContext) -> bool {
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
if let OpenedModelHandle::Open(buffer) = buffer {
return buffer.upgrade(cx).is_some();
return buffer.upgrade().is_some();
}
}
false
@ -253,7 +262,7 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<ChannelBuffer>>> {
) -> Task<Result<Model<ChannelBuffer>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
let channel_store = cx.handle();
@ -278,13 +287,13 @@ impl ChannelStore {
.request(proto::GetChannelMessagesById { message_ids }),
)
};
cx.spawn_weak(|this, mut cx| async move {
cx.spawn(|this, mut cx| async move {
if let Some(request) = request {
let response = request.await?;
let this = this
.upgrade(&cx)
.upgrade()
.ok_or_else(|| anyhow!("channel store dropped"))?;
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
ChannelMessage::from_proto_vec(response.messages, &user_store, &mut cx).await
} else {
Ok(Vec::new())
@ -354,7 +363,7 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<ChannelChat>>> {
) -> Task<Result<Model<ChannelChat>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
let this = cx.handle();
@ -371,22 +380,23 @@ impl ChannelStore {
/// Make sure that the resource is only opened once, even if this method
/// is called multiple times with the same channel id while the first task
/// is still running.
fn open_channel_resource<T: Entity, F, Fut>(
fn open_channel_resource<T, F, Fut>(
&mut self,
channel_id: ChannelId,
get_map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenedModelHandle<T>>,
load: F,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<T>>>
) -> Task<Result<Model<T>>>
where
F: 'static + FnOnce(Arc<Channel>, AsyncAppContext) -> Fut,
Fut: Future<Output = Result<ModelHandle<T>>>,
Fut: Future<Output = Result<Model<T>>>,
T: 'static,
{
let task = loop {
match get_map(self).entry(channel_id) {
hash_map::Entry::Occupied(e) => match e.get() {
OpenedModelHandle::Open(model) => {
if let Some(model) = model.upgrade(cx) {
if let Some(model) = model.upgrade() {
break Task::ready(Ok(model)).shared();
} else {
get_map(self).remove(&channel_id);
@ -399,12 +409,12 @@ impl ChannelStore {
},
hash_map::Entry::Vacant(e) => {
let task = cx
.spawn(|this, cx| async move {
let channel = this.read_with(&cx, |this, _| {
.spawn(move |this, mut cx| async move {
let channel = this.update(&mut cx, |this, _| {
this.channel_for_id(channel_id).cloned().ok_or_else(|| {
Arc::new(anyhow!("no channel for id: {}", channel_id))
})
})?;
})??;
load(channel, cx).await.map_err(Arc::new)
})
@ -413,7 +423,7 @@ impl ChannelStore {
e.insert(OpenedModelHandle::Loading(task.clone()));
cx.spawn({
let task = task.clone();
|this, mut cx| async move {
move |this, mut cx| async move {
let result = task.await;
this.update(&mut cx, |this, _| match result {
Ok(model) => {
@ -425,7 +435,8 @@ impl ChannelStore {
Err(_) => {
get_map(this).remove(&channel_id);
}
});
})
.ok();
}
})
.detach();
@ -433,7 +444,7 @@ impl ChannelStore {
}
}
};
cx.foreground()
cx.background_executor()
.spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) })
}
@ -458,7 +469,7 @@ impl ChannelStore {
) -> Task<Result<ChannelId>> {
let client = self.client.clone();
let name = name.trim_start_matches("#").to_owned();
cx.spawn(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let response = client
.request(proto::CreateChannel { name, parent_id })
.await?;
@ -468,15 +479,6 @@ impl ChannelStore {
.ok_or_else(|| anyhow!("missing channel in response"))?;
let channel_id = channel.id;
// let parent_edge = if let Some(parent_id) = parent_id {
// vec![ChannelEdge {
// channel_id: channel.id,
// parent_id,
// }]
// } else {
// vec![]
// };
this.update(&mut cx, |this, cx| {
let task = this.update_channels(
proto::UpdateChannels {
@ -492,7 +494,7 @@ impl ChannelStore {
// will resolve before this flush_effects finishes. Synchronously emitting this event
// ensures that the collab panel will observe this creation before the frame completes
cx.emit(ChannelEvent::ChannelCreated(channel_id));
});
})?;
Ok(channel_id)
})
@ -505,7 +507,7 @@ impl ChannelStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(|_, _| async move {
cx.spawn(move |_, _| async move {
let _ = client
.request(proto::MoveChannel { channel_id, to })
.await?;
@ -521,7 +523,7 @@ impl ChannelStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(|_, _| async move {
cx.spawn(move |_, _| async move {
let _ = client
.request(proto::SetChannelVisibility {
channel_id,
@ -546,7 +548,7 @@ impl ChannelStore {
cx.notify();
let client = self.client.clone();
cx.spawn(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let result = client
.request(proto::InviteChannelMember {
channel_id,
@ -558,7 +560,7 @@ impl ChannelStore {
this.update(&mut cx, |this, cx| {
this.outgoing_invites.remove(&(channel_id, user_id));
cx.notify();
});
})?;
result?;
@ -578,7 +580,7 @@ impl ChannelStore {
cx.notify();
let client = self.client.clone();
cx.spawn(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let result = client
.request(proto::RemoveChannelMember {
channel_id,
@ -589,7 +591,7 @@ impl ChannelStore {
this.update(&mut cx, |this, cx| {
this.outgoing_invites.remove(&(channel_id, user_id));
cx.notify();
});
})?;
result?;
Ok(())
})
@ -608,7 +610,7 @@ impl ChannelStore {
cx.notify();
let client = self.client.clone();
cx.spawn(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let result = client
.request(proto::SetChannelMemberRole {
channel_id,
@ -620,7 +622,7 @@ impl ChannelStore {
this.update(&mut cx, |this, cx| {
this.outgoing_invites.remove(&(channel_id, user_id));
cx.notify();
});
})?;
result?;
Ok(())
@ -635,7 +637,7 @@ impl ChannelStore {
) -> Task<Result<()>> {
let client = self.client.clone();
let name = new_name.to_string();
cx.spawn(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let channel = client
.request(proto::RenameChannel { channel_id, name })
.await?
@ -656,7 +658,7 @@ impl ChannelStore {
// will resolve before this flush_effects finishes. Synchronously emitting this event
// ensures that the collab panel will observe this creation before the frame complete
cx.emit(ChannelEvent::ChannelRenamed(channel_id))
});
})?;
Ok(())
})
}
@ -668,7 +670,7 @@ impl ChannelStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.background().spawn(async move {
cx.background_executor().spawn(async move {
client
.request(proto::RespondToChannelInvite { channel_id, accept })
.await?;
@ -683,17 +685,17 @@ impl ChannelStore {
) -> Task<Result<Vec<ChannelMembership>>> {
let client = self.client.clone();
let user_store = self.user_store.downgrade();
cx.spawn(|_, mut cx| async move {
cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::GetChannelMembers { channel_id })
.await?;
let user_ids = response.members.iter().map(|m| m.user_id).collect();
let user_store = user_store
.upgrade(&cx)
.upgrade()
.ok_or_else(|| anyhow!("user store dropped"))?;
let users = user_store
.update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))
.update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))?
.await?;
Ok(users
@ -727,7 +729,7 @@ impl ChannelStore {
}
async fn handle_update_channels(
this: ModelHandle<Self>,
this: Model<Self>,
message: TypedEnvelope<proto::UpdateChannels>,
_: Arc<Client>,
mut cx: AsyncAppContext,
@ -736,7 +738,7 @@ impl ChannelStore {
this.update_channels_tx
.unbounded_send(message.payload)
.unwrap();
});
})?;
Ok(())
}
@ -750,7 +752,7 @@ impl ChannelStore {
for chat in self.opened_chats.values() {
if let OpenedModelHandle::Open(chat) = chat {
if let Some(chat) = chat.upgrade(cx) {
if let Some(chat) = chat.upgrade() {
chat.update(cx, |chat, cx| {
chat.rejoin(cx);
});
@ -761,7 +763,7 @@ impl ChannelStore {
let mut buffer_versions = Vec::new();
for buffer in self.opened_buffers.values() {
if let OpenedModelHandle::Open(buffer) = buffer {
if let Some(buffer) = buffer.upgrade(cx) {
if let Some(buffer) = buffer.upgrade() {
let channel_buffer = buffer.read(cx);
let buffer = channel_buffer.buffer().read(cx);
buffer_versions.push(proto::ChannelBufferVersion {
@ -787,7 +789,7 @@ impl ChannelStore {
this.update(&mut cx, |this, cx| {
this.opened_buffers.retain(|_, buffer| match buffer {
OpenedModelHandle::Open(channel_buffer) => {
let Some(channel_buffer) = channel_buffer.upgrade(cx) else {
let Some(channel_buffer) = channel_buffer.upgrade() else {
return false;
};
@ -824,7 +826,7 @@ impl ChannelStore {
if let Some(operations) = operations {
let client = this.client.clone();
cx.background()
cx.background_executor()
.spawn(async move {
let operations = operations.await;
for chunk in
@ -849,7 +851,8 @@ impl ChannelStore {
}
OpenedModelHandle::Loading(_) => true,
});
});
})
.ok();
anyhow::Ok(())
})
}
@ -858,21 +861,22 @@ impl ChannelStore {
cx.notify();
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
cx.spawn_weak(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
if wait_for_reconnect {
cx.background().timer(RECONNECT_TIMEOUT).await;
cx.background_executor().timer(RECONNECT_TIMEOUT).await;
}
if let Some(this) = this.upgrade(&cx) {
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
for (_, buffer) in this.opened_buffers.drain() {
if let OpenedModelHandle::Open(buffer) = buffer {
if let Some(buffer) = buffer.upgrade(cx) {
if let Some(buffer) = buffer.upgrade() {
buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
}
}
}
});
})
.ok();
}
})
});
@ -892,14 +896,16 @@ impl ChannelStore {
.channel_invitations
.binary_search_by_key(&channel.id, |c| c.id)
{
Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name,
Ok(ix) => {
Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into()
}
Err(ix) => self.channel_invitations.insert(
ix,
Arc::new(Channel {
id: channel.id,
visibility: channel.visibility(),
role: channel.role(),
name: channel.name,
name: channel.name.into(),
unseen_note_version: None,
unseen_message_id: None,
parent_path: channel.parent_path,
@ -931,7 +937,7 @@ impl ChannelStore {
if let Some(OpenedModelHandle::Open(buffer)) =
self.opened_buffers.remove(&channel_id)
{
if let Some(buffer) = buffer.upgrade(cx) {
if let Some(buffer) = buffer.upgrade() {
buffer.update(cx, ChannelBuffer::disconnect);
}
}
@ -945,7 +951,7 @@ impl ChannelStore {
if channel_changed {
if let Some(OpenedModelHandle::Open(buffer)) = self.opened_buffers.get(&id) {
if let Some(buffer) = buffer.upgrade(cx) {
if let Some(buffer) = buffer.upgrade() {
buffer.update(cx, ChannelBuffer::channel_changed);
}
}
@ -1010,8 +1016,7 @@ impl ChannelStore {
}
cx.notify();
});
anyhow::Ok(())
})
}))
}
}

View File

@ -104,7 +104,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
existing_channel.visibility = channel_proto.visibility();
existing_channel.role = channel_proto.role();
existing_channel.name = channel_proto.name;
existing_channel.name = channel_proto.name.into();
} else {
self.channels_by_id.insert(
channel_proto.id,
@ -112,7 +112,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
id: channel_proto.id,
visibility: channel_proto.visibility(),
role: channel_proto.role(),
name: channel_proto.name,
name: channel_proto.name.into(),
unseen_note_version: None,
unseen_message_id: None,
parent_path: channel_proto.parent_path,
@ -146,11 +146,11 @@ fn channel_path_sorting_key<'a>(
let (parent_path, name) = channels_by_id
.get(&id)
.map_or((&[] as &[_], None), |channel| {
(channel.parent_path.as_slice(), Some(channel.name.as_str()))
(channel.parent_path.as_slice(), Some(channel.name.as_ref()))
});
parent_path
.iter()
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_str()))
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
.chain(name)
}

View File

@ -2,7 +2,7 @@ use crate::channel_chat::ChannelChatEvent;
use super::*;
use client::{test::FakeServer, Client, UserStore};
use gpui::{AppContext, ModelHandle, TestAppContext};
use gpui::{AppContext, Context, Model, TestAppContext};
use rpc::proto::{self};
use settings::SettingsStore;
use util::http::FakeHttpClient;
@ -147,7 +147,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
let user_id = 5;
let channel_id = 5;
let channel_store = cx.update(init_test);
let client = channel_store.read_with(cx, |s, _| s.client());
let client = channel_store.update(cx, |s, _| s.client());
let server = FakeServer::for_client(user_id, &client, cx).await;
// Get the available channels.
@ -161,8 +161,8 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
}],
..Default::default()
});
cx.foreground().run_until_parked();
cx.read(|cx| {
cx.executor().run_until_parked();
cx.update(|cx| {
assert_channels(
&channel_store,
&[(0, "the-channel".to_string(), proto::ChannelRole::Member)],
@ -214,7 +214,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
},
);
cx.foreground().start_waiting();
cx.executor().start_waiting();
// Client requests all users for the received messages
let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
@ -232,7 +232,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
);
let channel = channel.await.unwrap();
channel.read_with(cx, |channel, _| {
channel.update(cx, |channel, _| {
assert_eq!(
channel
.messages_in_range(0..2)
@ -273,13 +273,13 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
);
assert_eq!(
channel.next_event(cx).await,
channel.next_event(cx),
ChannelChatEvent::MessagesUpdated {
old_range: 2..2,
new_count: 1,
}
);
channel.read_with(cx, |channel, _| {
channel.update(cx, |channel, _| {
assert_eq!(
channel
.messages_in_range(2..3)
@ -322,13 +322,13 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
);
assert_eq!(
channel.next_event(cx).await,
channel.next_event(cx),
ChannelChatEvent::MessagesUpdated {
old_range: 0..0,
new_count: 2,
}
);
channel.read_with(cx, |channel, _| {
channel.update(cx, |channel, _| {
assert_eq!(
channel
.messages_in_range(0..2)
@ -342,13 +342,13 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
});
}
fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
cx.foreground().forbid_parking();
cx.set_global(SettingsStore::test(cx));
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
client::init(&client, cx);
crate::init(&client, user_store, cx);
@ -356,7 +356,7 @@ fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
}
fn update_channels(
channel_store: &ModelHandle<ChannelStore>,
channel_store: &Model<ChannelStore>,
message: proto::UpdateChannels,
cx: &mut AppContext,
) {
@ -366,11 +366,11 @@ fn update_channels(
#[track_caller]
fn assert_channels(
channel_store: &ModelHandle<ChannelStore>,
channel_store: &Model<ChannelStore>,
expected_channels: &[(usize, String, proto::ChannelRole)],
cx: &AppContext,
cx: &mut AppContext,
) {
let actual = channel_store.read_with(cx, |store, _| {
let actual = channel_store.update(cx, |store, _| {
store
.ordered_channels()
.map(|(depth, channel)| (depth, channel.name.to_string(), channel.role))

View File

@ -1,54 +0,0 @@
[package]
name = "channel2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/channel2.rs"
doctest = false
[features]
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
[dependencies]
client = { package = "client2", path = "../client2" }
collections = { path = "../collections" }
db = { package = "db2", path = "../db2" }
gpui = { package = "gpui2", path = "../gpui2" }
util = { path = "../util" }
rpc = { package = "rpc2", path = "../rpc2" }
text = { package = "text2", path = "../text2" }
language = { path = "../language" }
settings = { package = "settings2", path = "../settings2" }
feature_flags = { path = "../feature_flags" }
sum_tree = { path = "../sum_tree" }
clock = { path = "../clock" }
anyhow.workspace = true
futures.workspace = true
image = "0.23"
lazy_static.workspace = true
smallvec.workspace = true
log.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
schemars.workspace = true
smol.workspace = true
thiserror.workspace = true
time.workspace = true
tiny_http = "0.8"
uuid.workspace = true
url = "2.2"
serde.workspace = true
serde_derive.workspace = true
tempfile = "3"
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

View File

@ -1,23 +0,0 @@
mod channel_buffer;
mod channel_chat;
mod channel_store;
use client::{Client, UserStore};
use gpui::{AppContext, Model};
use std::sync::Arc;
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
pub use channel_chat::{
mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
MessageParams,
};
pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore};
#[cfg(test)]
mod channel_store_tests;
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
channel_store::init(client, user_store, cx);
channel_buffer::init(client);
channel_chat::init(client);
}

View File

@ -1,257 +0,0 @@
use crate::{Channel, ChannelId, ChannelStore};
use anyhow::Result;
use client::{Client, Collaborator, UserStore};
use collections::HashMap;
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
use language::proto::serialize_version;
use rpc::{
proto::{self, PeerId},
TypedEnvelope,
};
use std::{sync::Arc, time::Duration};
use util::ResultExt;
pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250);
pub(crate) fn init(client: &Arc<Client>) {
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators);
}
pub struct ChannelBuffer {
pub channel_id: ChannelId,
connected: bool,
collaborators: HashMap<PeerId, Collaborator>,
user_store: Model<UserStore>,
channel_store: Model<ChannelStore>,
buffer: Model<language::Buffer>,
buffer_epoch: u64,
client: Arc<Client>,
subscription: Option<client::Subscription>,
acknowledge_task: Option<Task<Result<()>>>,
}
pub enum ChannelBufferEvent {
CollaboratorsChanged,
Disconnected,
BufferEdited,
ChannelChanged,
}
impl EventEmitter<ChannelBufferEvent> for ChannelBuffer {}
impl ChannelBuffer {
pub(crate) async fn new(
channel: Arc<Channel>,
client: Arc<Client>,
user_store: Model<UserStore>,
channel_store: Model<ChannelStore>,
mut cx: AsyncAppContext,
) -> Result<Model<Self>> {
let response = client
.request(proto::JoinChannelBuffer {
channel_id: channel.id,
})
.await?;
let base_text = response.base_text;
let operations = response
.operations
.into_iter()
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?;
let buffer = cx.new_model(|_| {
language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
})?;
buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))??;
let subscription = client.subscribe_to_entity(channel.id)?;
anyhow::Ok(cx.new_model(|cx| {
cx.subscribe(&buffer, Self::on_buffer_update).detach();
cx.on_release(Self::release).detach();
let mut this = Self {
buffer,
buffer_epoch: response.epoch,
client,
connected: true,
collaborators: Default::default(),
acknowledge_task: None,
channel_id: channel.id,
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
user_store,
channel_store,
};
this.replace_collaborators(response.collaborators, cx);
this
})?)
}
fn release(&mut self, _: &mut AppContext) {
if self.connected {
if let Some(task) = self.acknowledge_task.take() {
task.detach();
}
self.client
.send(proto::LeaveChannelBuffer {
channel_id: self.channel_id,
})
.log_err();
}
}
pub fn remote_id(&self, cx: &AppContext) -> u64 {
self.buffer.read(cx).remote_id()
}
pub fn user_store(&self) -> &Model<UserStore> {
&self.user_store
}
pub(crate) fn replace_collaborators(
&mut self,
collaborators: Vec<proto::Collaborator>,
cx: &mut ModelContext<Self>,
) {
let mut new_collaborators = HashMap::default();
for collaborator in collaborators {
if let Ok(collaborator) = Collaborator::from_proto(collaborator) {
new_collaborators.insert(collaborator.peer_id, collaborator);
}
}
for (_, old_collaborator) in &self.collaborators {
if !new_collaborators.contains_key(&old_collaborator.peer_id) {
self.buffer.update(cx, |buffer, cx| {
buffer.remove_peer(old_collaborator.replica_id as u16, cx)
});
}
}
self.collaborators = new_collaborators;
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
}
async fn handle_update_channel_buffer(
this: Model<Self>,
update_channel_buffer: TypedEnvelope<proto::UpdateChannelBuffer>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let ops = update_channel_buffer
.payload
.operations
.into_iter()
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?;
this.update(&mut cx, |this, cx| {
cx.notify();
this.buffer
.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))
})??;
Ok(())
}
async fn handle_update_channel_buffer_collaborators(
this: Model<Self>,
message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.replace_collaborators(message.payload.collaborators, cx);
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
})
}
fn on_buffer_update(
&mut self,
_: Model<language::Buffer>,
event: &language::Event,
cx: &mut ModelContext<Self>,
) {
match event {
language::Event::Operation(operation) => {
let operation = language::proto::serialize_operation(operation);
self.client
.send(proto::UpdateChannelBuffer {
channel_id: self.channel_id,
operations: vec![operation],
})
.log_err();
}
language::Event::Edited => {
cx.emit(ChannelBufferEvent::BufferEdited);
}
_ => {}
}
}
pub fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<'_, ChannelBuffer>) {
let buffer = self.buffer.read(cx);
let version = buffer.version();
let buffer_id = buffer.remote_id();
let client = self.client.clone();
let epoch = self.epoch();
self.acknowledge_task = Some(cx.spawn(move |_, cx| async move {
cx.background_executor()
.timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL)
.await;
client
.send(proto::AckBufferOperation {
buffer_id,
epoch,
version: serialize_version(&version),
})
.ok();
Ok(())
}));
}
pub fn epoch(&self) -> u64 {
self.buffer_epoch
}
pub fn buffer(&self) -> Model<language::Buffer> {
self.buffer.clone()
}
pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
&self.collaborators
}
pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
self.channel_store
.read(cx)
.channel_for_id(self.channel_id)
.cloned()
}
pub(crate) fn disconnect(&mut self, cx: &mut ModelContext<Self>) {
log::info!("channel buffer {} disconnected", self.channel_id);
if self.connected {
self.connected = false;
self.subscription.take();
cx.emit(ChannelBufferEvent::Disconnected);
cx.notify()
}
}
pub(crate) fn channel_changed(&mut self, cx: &mut ModelContext<Self>) {
cx.emit(ChannelBufferEvent::ChannelChanged);
cx.notify()
}
pub fn is_connected(&self) -> bool {
self.connected
}
pub fn replica_id(&self, cx: &AppContext) -> u16 {
self.buffer.read(cx).replica_id()
}
}

View File

@ -1,645 +0,0 @@
use crate::{Channel, ChannelId, ChannelStore};
use anyhow::{anyhow, Result};
use client::{
proto,
user::{User, UserStore},
Client, Subscription, TypedEnvelope, UserId,
};
use futures::lock::Mutex;
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
use rand::prelude::*;
use std::{
collections::HashSet,
mem,
ops::{ControlFlow, Range},
sync::Arc,
};
use sum_tree::{Bias, SumTree};
use time::OffsetDateTime;
use util::{post_inc, ResultExt as _, TryFutureExt};
pub struct ChannelChat {
pub channel_id: ChannelId,
messages: SumTree<ChannelMessage>,
acknowledged_message_ids: HashSet<u64>,
channel_store: Model<ChannelStore>,
loaded_all_messages: bool,
last_acknowledged_id: Option<u64>,
next_pending_message_id: usize,
user_store: Model<UserStore>,
rpc: Arc<Client>,
outgoing_messages_lock: Arc<Mutex<()>>,
rng: StdRng,
_subscription: Subscription,
}
#[derive(Debug, PartialEq, Eq)]
pub struct MessageParams {
pub text: String,
pub mentions: Vec<(Range<usize>, UserId)>,
}
#[derive(Clone, Debug)]
pub struct ChannelMessage {
pub id: ChannelMessageId,
pub body: String,
pub timestamp: OffsetDateTime,
pub sender: Arc<User>,
pub nonce: u128,
pub mentions: Vec<(Range<usize>, UserId)>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ChannelMessageId {
Saved(u64),
Pending(usize),
}
#[derive(Clone, Debug, Default)]
pub struct ChannelMessageSummary {
max_id: ChannelMessageId,
count: usize,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct Count(usize);
#[derive(Clone, Debug, PartialEq)]
pub enum ChannelChatEvent {
MessagesUpdated {
old_range: Range<usize>,
new_count: usize,
},
NewMessage {
channel_id: ChannelId,
message_id: u64,
},
}
impl EventEmitter<ChannelChatEvent> for ChannelChat {}
pub fn init(client: &Arc<Client>) {
client.add_model_message_handler(ChannelChat::handle_message_sent);
client.add_model_message_handler(ChannelChat::handle_message_removed);
}
impl ChannelChat {
pub async fn new(
channel: Arc<Channel>,
channel_store: Model<ChannelStore>,
user_store: Model<UserStore>,
client: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<Model<Self>> {
let channel_id = channel.id;
let subscription = client.subscribe_to_entity(channel_id).unwrap();
let response = client
.request(proto::JoinChannelChat { channel_id })
.await?;
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
let loaded_all_messages = response.done;
Ok(cx.new_model(|cx| {
cx.on_release(Self::release).detach();
let mut this = Self {
channel_id: channel.id,
user_store,
channel_store,
rpc: client,
outgoing_messages_lock: Default::default(),
messages: Default::default(),
acknowledged_message_ids: Default::default(),
loaded_all_messages,
next_pending_message_id: 0,
last_acknowledged_id: None,
rng: StdRng::from_entropy(),
_subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
};
this.insert_messages(messages, cx);
this
})?)
}
fn release(&mut self, _: &mut AppContext) {
self.rpc
.send(proto::LeaveChannelChat {
channel_id: self.channel_id,
})
.log_err();
}
pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
self.channel_store
.read(cx)
.channel_for_id(self.channel_id)
.cloned()
}
pub fn client(&self) -> &Arc<Client> {
&self.rpc
}
pub fn send_message(
&mut self,
message: MessageParams,
cx: &mut ModelContext<Self>,
) -> Result<Task<Result<u64>>> {
if message.text.is_empty() {
Err(anyhow!("message body can't be empty"))?;
}
let current_user = self
.user_store
.read(cx)
.current_user()
.ok_or_else(|| anyhow!("current_user is not present"))?;
let channel_id = self.channel_id;
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
let nonce = self.rng.gen();
self.insert_messages(
SumTree::from_item(
ChannelMessage {
id: pending_id,
body: message.text.clone(),
sender: current_user,
timestamp: OffsetDateTime::now_utc(),
mentions: message.mentions.clone(),
nonce,
},
&(),
),
cx,
);
let user_store = self.user_store.clone();
let rpc = self.rpc.clone();
let outgoing_messages_lock = self.outgoing_messages_lock.clone();
Ok(cx.spawn(move |this, mut cx| async move {
let outgoing_message_guard = outgoing_messages_lock.lock().await;
let request = rpc.request(proto::SendChannelMessage {
channel_id,
body: message.text,
nonce: Some(nonce.into()),
mentions: mentions_to_proto(&message.mentions),
});
let response = request.await?;
drop(outgoing_message_guard);
let response = response.message.ok_or_else(|| anyhow!("invalid message"))?;
let id = response.id;
let message = ChannelMessage::from_proto(response, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
})?;
Ok(id)
}))
}
pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let response = self.rpc.request(proto::RemoveChannelMessage {
channel_id: self.channel_id,
message_id: id,
});
cx.spawn(move |this, mut cx| async move {
response.await?;
this.update(&mut cx, |this, cx| {
this.message_removed(id, cx);
})?;
Ok(())
})
}
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<Option<()>>> {
if self.loaded_all_messages {
return None;
}
let rpc = self.rpc.clone();
let user_store = self.user_store.clone();
let channel_id = self.channel_id;
let before_message_id = self.first_loaded_message_id()?;
Some(cx.spawn(move |this, mut cx| {
async move {
let response = rpc
.request(proto::GetChannelMessages {
channel_id,
before_message_id,
})
.await?;
let loaded_all_messages = response.done;
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.loaded_all_messages = loaded_all_messages;
this.insert_messages(messages, cx);
})?;
anyhow::Ok(())
}
.log_err()
}))
}
pub fn first_loaded_message_id(&mut self) -> Option<u64> {
self.messages.first().and_then(|message| match message.id {
ChannelMessageId::Saved(id) => Some(id),
ChannelMessageId::Pending(_) => None,
})
}
/// Load all of the chat messages since a certain message id.
///
/// For now, we always maintain a suffix of the channel's messages.
pub async fn load_history_since_message(
chat: Model<Self>,
message_id: u64,
mut cx: AsyncAppContext,
) -> Option<usize> {
loop {
let step = chat
.update(&mut cx, |chat, cx| {
if let Some(first_id) = chat.first_loaded_message_id() {
if first_id <= message_id {
let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>();
let message_id = ChannelMessageId::Saved(message_id);
cursor.seek(&message_id, Bias::Left, &());
return ControlFlow::Break(
if cursor
.item()
.map_or(false, |message| message.id == message_id)
{
Some(cursor.start().1 .0)
} else {
None
},
);
}
}
ControlFlow::Continue(chat.load_more_messages(cx))
})
.log_err()?;
match step {
ControlFlow::Break(ix) => return ix,
ControlFlow::Continue(task) => task?.await?,
}
}
}
pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id {
if self
.last_acknowledged_id
.map_or(true, |acknowledged_id| acknowledged_id < latest_message_id)
{
self.rpc
.send(proto::AckChannelMessage {
channel_id: self.channel_id,
message_id: latest_message_id,
})
.ok();
self.last_acknowledged_id = Some(latest_message_id);
self.channel_store.update(cx, |store, cx| {
store.acknowledge_message_id(self.channel_id, latest_message_id, cx);
});
}
}
}
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
let user_store = self.user_store.clone();
let rpc = self.rpc.clone();
let channel_id = self.channel_id;
cx.spawn(move |this, mut cx| {
async move {
let response = rpc.request(proto::JoinChannelChat { channel_id }).await?;
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
let loaded_all_messages = response.done;
let pending_messages = this.update(&mut cx, |this, cx| {
if let Some((first_new_message, last_old_message)) =
messages.first().zip(this.messages.last())
{
if first_new_message.id > last_old_message.id {
let old_messages = mem::take(&mut this.messages);
cx.emit(ChannelChatEvent::MessagesUpdated {
old_range: 0..old_messages.summary().count,
new_count: 0,
});
this.loaded_all_messages = loaded_all_messages;
}
}
this.insert_messages(messages, cx);
if loaded_all_messages {
this.loaded_all_messages = loaded_all_messages;
}
this.pending_messages().cloned().collect::<Vec<_>>()
})?;
for pending_message in pending_messages {
let request = rpc.request(proto::SendChannelMessage {
channel_id,
body: pending_message.body,
mentions: mentions_to_proto(&pending_message.mentions),
nonce: Some(pending_message.nonce.into()),
});
let response = request.await?;
let message = ChannelMessage::from_proto(
response.message.ok_or_else(|| anyhow!("invalid message"))?,
&user_store,
&mut cx,
)
.await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
})?;
}
anyhow::Ok(())
}
.log_err()
})
.detach();
}
pub fn message_count(&self) -> usize {
self.messages.summary().count
}
pub fn messages(&self) -> &SumTree<ChannelMessage> {
&self.messages
}
pub fn message(&self, ix: usize) -> &ChannelMessage {
let mut cursor = self.messages.cursor::<Count>();
cursor.seek(&Count(ix), Bias::Right, &());
cursor.item().unwrap()
}
pub fn acknowledge_message(&mut self, id: u64) {
if self.acknowledged_message_ids.insert(id) {
self.rpc
.send(proto::AckChannelMessage {
channel_id: self.channel_id,
message_id: id,
})
.ok();
}
}
pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
let mut cursor = self.messages.cursor::<Count>();
cursor.seek(&Count(range.start), Bias::Right, &());
cursor.take(range.len())
}
pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
let mut cursor = self.messages.cursor::<ChannelMessageId>();
cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &());
cursor
}
async fn handle_message_sent(
this: Model<Self>,
message: TypedEnvelope<proto::ChannelMessageSent>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let message = message
.payload
.message
.ok_or_else(|| anyhow!("empty message"))?;
let message_id = message.id;
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
cx.emit(ChannelChatEvent::NewMessage {
channel_id: this.channel_id,
message_id,
})
})?;
Ok(())
}
async fn handle_message_removed(
this: Model<Self>,
message: TypedEnvelope<proto::RemoveChannelMessage>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.message_removed(message.payload.message_id, cx)
})?;
Ok(())
}
fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
let nonces = messages
.cursor::<()>()
.map(|m| m.nonce)
.collect::<HashSet<_>>();
let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>();
let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &());
let start_ix = old_cursor.start().1 .0;
let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &());
let removed_count = removed_messages.summary().count;
let new_count = messages.summary().count;
let end_ix = start_ix + removed_count;
new_messages.append(messages, &());
let mut ranges = Vec::<Range<usize>>::new();
if new_messages.last().unwrap().is_pending() {
new_messages.append(old_cursor.suffix(&()), &());
} else {
new_messages.append(
old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()),
&(),
);
while let Some(message) = old_cursor.item() {
let message_ix = old_cursor.start().1 .0;
if nonces.contains(&message.nonce) {
if ranges.last().map_or(false, |r| r.end == message_ix) {
ranges.last_mut().unwrap().end += 1;
} else {
ranges.push(message_ix..message_ix + 1);
}
} else {
new_messages.push(message.clone(), &());
}
old_cursor.next(&());
}
}
drop(old_cursor);
self.messages = new_messages;
for range in ranges.into_iter().rev() {
cx.emit(ChannelChatEvent::MessagesUpdated {
old_range: range,
new_count: 0,
});
}
cx.emit(ChannelChatEvent::MessagesUpdated {
old_range: start_ix..end_ix,
new_count,
});
cx.notify();
}
}
fn message_removed(&mut self, id: u64, cx: &mut ModelContext<Self>) {
let mut cursor = self.messages.cursor::<ChannelMessageId>();
let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &());
if let Some(item) = cursor.item() {
if item.id == ChannelMessageId::Saved(id) {
let ix = messages.summary().count;
cursor.next(&());
messages.append(cursor.suffix(&()), &());
drop(cursor);
self.messages = messages;
cx.emit(ChannelChatEvent::MessagesUpdated {
old_range: ix..ix + 1,
new_count: 0,
});
}
}
}
}
async fn messages_from_proto(
proto_messages: Vec<proto::ChannelMessage>,
user_store: &Model<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<SumTree<ChannelMessage>> {
let messages = ChannelMessage::from_proto_vec(proto_messages, user_store, cx).await?;
let mut result = SumTree::new();
result.extend(messages, &());
Ok(result)
}
impl ChannelMessage {
pub async fn from_proto(
message: proto::ChannelMessage,
user_store: &Model<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<Self> {
let sender = user_store
.update(cx, |user_store, cx| {
user_store.get_user(message.sender_id, cx)
})?
.await?;
Ok(ChannelMessage {
id: ChannelMessageId::Saved(message.id),
body: message.body,
mentions: message
.mentions
.into_iter()
.filter_map(|mention| {
let range = mention.range?;
Some((range.start as usize..range.end as usize, mention.user_id))
})
.collect(),
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
sender,
nonce: message
.nonce
.ok_or_else(|| anyhow!("nonce is required"))?
.into(),
})
}
pub fn is_pending(&self) -> bool {
matches!(self.id, ChannelMessageId::Pending(_))
}
pub async fn from_proto_vec(
proto_messages: Vec<proto::ChannelMessage>,
user_store: &Model<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<Vec<Self>> {
let unique_user_ids = proto_messages
.iter()
.map(|m| m.sender_id)
.collect::<HashSet<_>>()
.into_iter()
.collect();
user_store
.update(cx, |user_store, cx| {
user_store.get_users(unique_user_ids, cx)
})?
.await?;
let mut messages = Vec::with_capacity(proto_messages.len());
for message in proto_messages {
messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
}
Ok(messages)
}
}
pub fn mentions_to_proto(mentions: &[(Range<usize>, UserId)]) -> Vec<proto::ChatMention> {
mentions
.iter()
.map(|(range, user_id)| proto::ChatMention {
range: Some(proto::Range {
start: range.start as u64,
end: range.end as u64,
}),
user_id: *user_id as u64,
})
.collect()
}
impl sum_tree::Item for ChannelMessage {
type Summary = ChannelMessageSummary;
fn summary(&self) -> Self::Summary {
ChannelMessageSummary {
max_id: self.id,
count: 1,
}
}
}
impl Default for ChannelMessageId {
fn default() -> Self {
Self::Saved(0)
}
}
impl sum_tree::Summary for ChannelMessageSummary {
type Context = ();
fn add_summary(&mut self, summary: &Self, _: &()) {
self.max_id = summary.max_id;
self.count += summary.count;
}
}
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId {
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
debug_assert!(summary.max_id > *self);
*self = summary.max_id;
}
}
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
self.0 += summary.count;
}
}
impl<'a> From<&'a str> for MessageParams {
fn from(value: &'a str) -> Self {
Self {
text: value.into(),
mentions: Vec::new(),
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,184 +0,0 @@
use crate::{Channel, ChannelId};
use collections::BTreeMap;
use rpc::proto;
use std::sync::Arc;
#[derive(Default, Debug)]
pub struct ChannelIndex {
channels_ordered: Vec<ChannelId>,
channels_by_id: BTreeMap<ChannelId, Arc<Channel>>,
}
impl ChannelIndex {
pub fn by_id(&self) -> &BTreeMap<ChannelId, Arc<Channel>> {
&self.channels_by_id
}
pub fn ordered_channels(&self) -> &[ChannelId] {
&self.channels_ordered
}
pub fn clear(&mut self) {
self.channels_ordered.clear();
self.channels_by_id.clear();
}
/// Delete the given channels from this index.
pub fn delete_channels(&mut self, channels: &[ChannelId]) {
self.channels_by_id
.retain(|channel_id, _| !channels.contains(channel_id));
self.channels_ordered
.retain(|channel_id| !channels.contains(channel_id));
}
pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard {
ChannelPathsInsertGuard {
channels_ordered: &mut self.channels_ordered,
channels_by_id: &mut self.channels_by_id,
}
}
pub fn acknowledge_note_version(
&mut self,
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
) {
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
let channel = Arc::make_mut(channel);
if let Some((unseen_epoch, unseen_version)) = &channel.unseen_note_version {
if epoch > *unseen_epoch
|| epoch == *unseen_epoch && version.observed_all(unseen_version)
{
channel.unseen_note_version = None;
}
}
}
}
pub fn acknowledge_message_id(&mut self, channel_id: ChannelId, message_id: u64) {
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
let channel = Arc::make_mut(channel);
if let Some(unseen_message_id) = channel.unseen_message_id {
if message_id >= unseen_message_id {
channel.unseen_message_id = None;
}
}
}
}
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
insert_note_changed(&mut self.channels_by_id, channel_id, epoch, version);
}
pub fn new_message(&mut self, channel_id: ChannelId, message_id: u64) {
insert_new_message(&mut self.channels_by_id, channel_id, message_id)
}
}
/// A guard for ensuring that the paths index maintains its sort and uniqueness
/// invariants after a series of insertions
#[derive(Debug)]
pub struct ChannelPathsInsertGuard<'a> {
channels_ordered: &'a mut Vec<ChannelId>,
channels_by_id: &'a mut BTreeMap<ChannelId, Arc<Channel>>,
}
impl<'a> ChannelPathsInsertGuard<'a> {
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
insert_note_changed(&mut self.channels_by_id, channel_id, epoch, &version);
}
pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) {
insert_new_message(&mut self.channels_by_id, channel_id, message_id)
}
pub fn insert(&mut self, channel_proto: proto::Channel) -> bool {
let mut ret = false;
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
let existing_channel = Arc::make_mut(existing_channel);
ret = existing_channel.visibility != channel_proto.visibility()
|| existing_channel.role != channel_proto.role()
|| existing_channel.name != channel_proto.name;
existing_channel.visibility = channel_proto.visibility();
existing_channel.role = channel_proto.role();
existing_channel.name = channel_proto.name.into();
} else {
self.channels_by_id.insert(
channel_proto.id,
Arc::new(Channel {
id: channel_proto.id,
visibility: channel_proto.visibility(),
role: channel_proto.role(),
name: channel_proto.name.into(),
unseen_note_version: None,
unseen_message_id: None,
parent_path: channel_proto.parent_path,
}),
);
self.insert_root(channel_proto.id);
}
ret
}
fn insert_root(&mut self, channel_id: ChannelId) {
self.channels_ordered.push(channel_id);
}
}
impl<'a> Drop for ChannelPathsInsertGuard<'a> {
fn drop(&mut self) {
self.channels_ordered.sort_by(|a, b| {
let a = channel_path_sorting_key(*a, &self.channels_by_id);
let b = channel_path_sorting_key(*b, &self.channels_by_id);
a.cmp(b)
});
self.channels_ordered.dedup();
}
}
fn channel_path_sorting_key<'a>(
id: ChannelId,
channels_by_id: &'a BTreeMap<ChannelId, Arc<Channel>>,
) -> impl Iterator<Item = &str> {
let (parent_path, name) = channels_by_id
.get(&id)
.map_or((&[] as &[_], None), |channel| {
(channel.parent_path.as_slice(), Some(channel.name.as_ref()))
});
parent_path
.iter()
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
.chain(name)
}
fn insert_note_changed(
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
channel_id: u64,
epoch: u64,
version: &clock::Global,
) {
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
let unseen_version = Arc::make_mut(channel)
.unseen_note_version
.get_or_insert((0, clock::Global::new()));
if epoch > unseen_version.0 {
*unseen_version = (epoch, version.clone());
} else {
unseen_version.1.join(&version);
}
}
}
fn insert_new_message(
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
channel_id: u64,
message_id: u64,
) {
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0);
*unseen_message_id = message_id.max(*unseen_message_id);
}
}

View File

@ -1,380 +0,0 @@
use crate::channel_chat::ChannelChatEvent;
use super::*;
use client::{test::FakeServer, Client, UserStore};
use gpui::{AppContext, Context, Model, TestAppContext};
use rpc::proto::{self};
use settings::SettingsStore;
use util::http::FakeHttpClient;
#[gpui::test]
fn test_update_channels(cx: &mut AppContext) {
let channel_store = init_test(cx);
update_channels(
&channel_store,
proto::UpdateChannels {
channels: vec![
proto::Channel {
id: 1,
name: "b".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: Vec::new(),
},
proto::Channel {
id: 2,
name: "a".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Member.into(),
parent_path: Vec::new(),
},
],
..Default::default()
},
cx,
);
assert_channels(
&channel_store,
&[
//
(0, "a".to_string(), proto::ChannelRole::Member),
(0, "b".to_string(), proto::ChannelRole::Admin),
],
cx,
);
update_channels(
&channel_store,
proto::UpdateChannels {
channels: vec![
proto::Channel {
id: 3,
name: "x".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![1],
},
proto::Channel {
id: 4,
name: "y".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Member.into(),
parent_path: vec![2],
},
],
..Default::default()
},
cx,
);
assert_channels(
&channel_store,
&[
(0, "a".to_string(), proto::ChannelRole::Member),
(1, "y".to_string(), proto::ChannelRole::Member),
(0, "b".to_string(), proto::ChannelRole::Admin),
(1, "x".to_string(), proto::ChannelRole::Admin),
],
cx,
);
}
#[gpui::test]
fn test_dangling_channel_paths(cx: &mut AppContext) {
let channel_store = init_test(cx);
update_channels(
&channel_store,
proto::UpdateChannels {
channels: vec![
proto::Channel {
id: 0,
name: "a".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![],
},
proto::Channel {
id: 1,
name: "b".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![0],
},
proto::Channel {
id: 2,
name: "c".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![0, 1],
},
],
..Default::default()
},
cx,
);
// Sanity check
assert_channels(
&channel_store,
&[
//
(0, "a".to_string(), proto::ChannelRole::Admin),
(1, "b".to_string(), proto::ChannelRole::Admin),
(2, "c".to_string(), proto::ChannelRole::Admin),
],
cx,
);
update_channels(
&channel_store,
proto::UpdateChannels {
delete_channels: vec![1, 2],
..Default::default()
},
cx,
);
// Make sure that the 1/2/3 path is gone
assert_channels(
&channel_store,
&[(0, "a".to_string(), proto::ChannelRole::Admin)],
cx,
);
}
#[gpui::test]
async fn test_channel_messages(cx: &mut TestAppContext) {
let user_id = 5;
let channel_id = 5;
let channel_store = cx.update(init_test);
let client = channel_store.update(cx, |s, _| s.client());
let server = FakeServer::for_client(user_id, &client, cx).await;
// Get the available channels.
server.send(proto::UpdateChannels {
channels: vec![proto::Channel {
id: channel_id,
name: "the-channel".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Member.into(),
parent_path: vec![],
}],
..Default::default()
});
cx.executor().run_until_parked();
cx.update(|cx| {
assert_channels(
&channel_store,
&[(0, "the-channel".to_string(), proto::ChannelRole::Member)],
cx,
);
});
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
assert_eq!(get_users.payload.user_ids, vec![5]);
server.respond(
get_users.receipt(),
proto::UsersResponse {
users: vec![proto::User {
id: 5,
github_login: "nathansobo".into(),
avatar_url: "http://avatar.com/nathansobo".into(),
}],
},
);
// Join a channel and populate its existing messages.
let channel = channel_store.update(cx, |store, cx| {
let channel_id = store.ordered_channels().next().unwrap().1.id;
store.open_channel_chat(channel_id, cx)
});
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
server.respond(
join_channel.receipt(),
proto::JoinChannelChatResponse {
messages: vec![
proto::ChannelMessage {
id: 10,
body: "a".into(),
timestamp: 1000,
sender_id: 5,
mentions: vec![],
nonce: Some(1.into()),
},
proto::ChannelMessage {
id: 11,
body: "b".into(),
timestamp: 1001,
sender_id: 6,
mentions: vec![],
nonce: Some(2.into()),
},
],
done: false,
},
);
cx.executor().start_waiting();
// Client requests all users for the received messages
let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
get_users.payload.user_ids.sort();
assert_eq!(get_users.payload.user_ids, vec![6]);
server.respond(
get_users.receipt(),
proto::UsersResponse {
users: vec![proto::User {
id: 6,
github_login: "maxbrunsfeld".into(),
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
}],
},
);
let channel = channel.await.unwrap();
channel.update(cx, |channel, _| {
assert_eq!(
channel
.messages_in_range(0..2)
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
.collect::<Vec<_>>(),
&[
("nathansobo".into(), "a".into()),
("maxbrunsfeld".into(), "b".into())
]
);
});
// Receive a new message.
server.send(proto::ChannelMessageSent {
channel_id,
message: Some(proto::ChannelMessage {
id: 12,
body: "c".into(),
timestamp: 1002,
sender_id: 7,
mentions: vec![],
nonce: Some(3.into()),
}),
});
// Client requests user for message since they haven't seen them yet
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
assert_eq!(get_users.payload.user_ids, vec![7]);
server.respond(
get_users.receipt(),
proto::UsersResponse {
users: vec![proto::User {
id: 7,
github_login: "as-cii".into(),
avatar_url: "http://avatar.com/as-cii".into(),
}],
},
);
assert_eq!(
channel.next_event(cx),
ChannelChatEvent::MessagesUpdated {
old_range: 2..2,
new_count: 1,
}
);
channel.update(cx, |channel, _| {
assert_eq!(
channel
.messages_in_range(2..3)
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
.collect::<Vec<_>>(),
&[("as-cii".into(), "c".into())]
)
});
// Scroll up to view older messages.
channel.update(cx, |channel, cx| {
channel.load_more_messages(cx).unwrap().detach();
});
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
assert_eq!(get_messages.payload.channel_id, 5);
assert_eq!(get_messages.payload.before_message_id, 10);
server.respond(
get_messages.receipt(),
proto::GetChannelMessagesResponse {
done: true,
messages: vec![
proto::ChannelMessage {
id: 8,
body: "y".into(),
timestamp: 998,
sender_id: 5,
nonce: Some(4.into()),
mentions: vec![],
},
proto::ChannelMessage {
id: 9,
body: "z".into(),
timestamp: 999,
sender_id: 6,
nonce: Some(5.into()),
mentions: vec![],
},
],
},
);
assert_eq!(
channel.next_event(cx),
ChannelChatEvent::MessagesUpdated {
old_range: 0..0,
new_count: 2,
}
);
channel.update(cx, |channel, _| {
assert_eq!(
channel
.messages_in_range(0..2)
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
.collect::<Vec<_>>(),
&[
("nathansobo".into(), "y".into()),
("maxbrunsfeld".into(), "z".into())
]
);
});
}
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
client::init(&client, cx);
crate::init(&client, user_store, cx);
ChannelStore::global(cx)
}
fn update_channels(
channel_store: &Model<ChannelStore>,
message: proto::UpdateChannels,
cx: &mut AppContext,
) {
let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx));
assert!(task.is_none());
}
#[track_caller]
fn assert_channels(
channel_store: &Model<ChannelStore>,
expected_channels: &[(usize, String, proto::ChannelRole)],
cx: &mut AppContext,
) {
let actual = channel_store.update(cx, |store, _| {
store
.ordered_channels()
.map(|(depth, channel)| (depth, channel.name.to_string(), channel.role))
.collect::<Vec<_>>()
});
assert_eq!(actual, expected_channels);
}

View File

@ -14,12 +14,12 @@ test-support = ["collections/test-support", "gpui/test-support", "rpc/test-suppo
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
collections = { path = "../collections" }
db = { path = "../db" }
gpui = { path = "../gpui" }
db = { package = "db2", path = "../db2" }
gpui = { package = "gpui2", path = "../gpui2" }
util = { path = "../util" }
rpc = { path = "../rpc" }
text = { path = "../text" }
settings = { path = "../settings" }
rpc = { package = "rpc2", path = "../rpc2" }
text = { package = "text2", path = "../text2" }
settings = { package = "settings2", path = "../settings2" }
feature_flags = { path = "../feature_flags" }
sum_tree = { path = "../sum_tree" }
@ -47,7 +47,7 @@ url = "2.2"
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,11 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use chrono::{DateTime, Utc};
use gpui::{executor::Background, serde_json, AppContext, Task};
use futures::Future;
use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use settings::Settings;
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
@ -14,19 +16,16 @@ use util::{channel::ReleaseChannel, TryFutureExt};
pub struct Telemetry {
http_client: Arc<dyn HttpClient>,
executor: Arc<Background>,
executor: BackgroundExecutor,
state: Mutex<TelemetryState>,
}
#[derive(Default)]
struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
session_id: Option<Arc<str>>, // Per app launch
app_version: Option<Arc<str>>,
release_channel: Option<&'static str>,
os_name: &'static str,
os_version: Option<Arc<str>>,
app_metadata: AppMetadata,
architecture: &'static str,
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
flush_clickhouse_events_task: Option<Task<()>>,
@ -48,9 +47,9 @@ struct ClickhouseEventRequestBody {
installation_id: Option<Arc<str>>,
session_id: Option<Arc<str>>,
is_staff: Option<bool>,
app_version: Option<Arc<str>>,
app_version: Option<String>,
os_name: &'static str,
os_version: Option<Arc<str>>,
os_version: Option<String>,
architecture: &'static str,
release_channel: Option<&'static str>,
events: Vec<ClickhouseEventWrapper>,
@ -130,25 +129,23 @@ const MAX_QUEUE_LEN: usize = 50;
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(not(debug_assertions))]
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(120);
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(60 * 5);
impl Telemetry {
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
let platform = cx.platform();
pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
let release_channel = if cx.has_global::<ReleaseChannel>() {
Some(cx.global::<ReleaseChannel>().display_name())
} else {
None
};
// TODO: Replace all hardware stuff with nested SystemSpecs json
let this = Arc::new(Self {
http_client: client,
executor: cx.background().clone(),
executor: cx.background_executor().clone(),
state: Mutex::new(TelemetryState {
os_name: platform.os_name().into(),
os_version: platform.os_version().ok().map(|v| v.to_string().into()),
app_metadata: cx.app_metadata(),
architecture: env::consts::ARCH,
app_version: platform.app_version().ok().map(|v| v.to_string().into()),
release_channel,
installation_id: None,
metrics_id: None,
@ -161,9 +158,30 @@ impl Telemetry {
}),
});
// We should only ever have one instance of Telemetry, leak the subscription to keep it alive
// rather than store in TelemetryState, complicating spawn as subscriptions are not Send
std::mem::forget(cx.on_app_quit({
let this = this.clone();
move |cx| this.shutdown_telemetry(cx)
}));
this
}
#[cfg(any(test, feature = "test-support"))]
fn shutdown_telemetry(self: &Arc<Self>, _: &mut AppContext) -> impl Future<Output = ()> {
Task::ready(())
}
// Skip calling this function in tests.
// TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings
#[cfg(not(any(test, feature = "test-support")))]
fn shutdown_telemetry(self: &Arc<Self>, cx: &mut AppContext) -> impl Future<Output = ()> {
let telemetry_settings = TelemetrySettings::get_global(cx).clone();
self.report_app_event(telemetry_settings, "close", true);
Task::ready(())
}
pub fn log_file_path(&self) -> Option<PathBuf> {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
@ -180,7 +198,7 @@ impl Telemetry {
drop(state);
let this = self.clone();
cx.spawn(|mut cx| async move {
cx.spawn(|cx| async move {
// Avoiding calling `System::new_all()`, as there have been crashes related to it
let refresh_kind = RefreshKind::new()
.with_memory() // For memory usage
@ -209,7 +227,13 @@ impl Telemetry {
return;
};
let telemetry_settings = cx.update(|cx| *settings::get::<TelemetrySettings>(cx));
let telemetry_settings = if let Ok(telemetry_settings) =
cx.update(|cx| *TelemetrySettings::get_global(cx))
{
telemetry_settings
} else {
break;
};
this.report_memory_event(
telemetry_settings,
@ -232,7 +256,7 @@ impl Telemetry {
is_staff: bool,
cx: &AppContext,
) {
if !settings::get::<TelemetrySettings>(cx).metrics {
if !TelemetrySettings::get_global(cx).metrics {
return;
}
@ -461,9 +485,15 @@ impl Telemetry {
installation_id: state.installation_id.clone(),
session_id: state.session_id.clone(),
is_staff: state.is_staff.clone(),
app_version: state.app_version.clone(),
os_name: state.os_name,
os_version: state.os_version.clone(),
app_version: state
.app_metadata
.app_version
.map(|version| version.to_string()),
os_name: state.app_metadata.os_name,
os_version: state
.app_metadata
.os_version
.map(|version| version.to_string()),
architecture: state.architecture,
release_channel: state.release_channel,

View File

@ -1,20 +1,19 @@
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
use anyhow::{anyhow, Result};
use futures::{stream::BoxStream, StreamExt};
use gpui::{executor, ModelHandle, TestAppContext};
use gpui::{BackgroundExecutor, Context, Model, TestAppContext};
use parking_lot::Mutex;
use rpc::{
proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
ConnectionId, Peer, Receipt, TypedEnvelope,
};
use std::{rc::Rc, sync::Arc};
use util::http::FakeHttpClient;
use std::sync::Arc;
pub struct FakeServer {
peer: Arc<Peer>,
state: Arc<Mutex<FakeServerState>>,
user_id: u64,
executor: Rc<executor::Foreground>,
executor: BackgroundExecutor,
}
#[derive(Default)]
@ -36,7 +35,7 @@ impl FakeServer {
peer: Peer::new(0),
state: Default::default(),
user_id: client_user_id,
executor: cx.foreground(),
executor: cx.executor(),
};
client
@ -78,10 +77,11 @@ impl FakeServer {
Err(EstablishConnectionError::Unauthorized)?
}
let (client_conn, server_conn, _) = Connection::in_memory(cx.background());
let (client_conn, server_conn, _) =
Connection::in_memory(cx.background_executor().clone());
let (connection_id, io, incoming) =
peer.add_test_connection(server_conn, cx.background());
cx.background().spawn(io).detach();
peer.add_test_connection(server_conn, cx.background_executor().clone());
cx.background_executor().spawn(io).detach();
{
let mut state = state.lock();
state.connection_id = Some(connection_id);
@ -193,9 +193,8 @@ impl FakeServer {
&self,
client: Arc<Client>,
cx: &mut TestAppContext,
) -> ModelHandle<UserStore> {
let http_client = FakeHttpClient::with_404_response();
let user_store = cx.add_model(|cx| UserStore::new(client, http_client, cx));
) -> Model<UserStore> {
let user_store = cx.new_model(|cx| UserStore::new(client, cx));
assert_eq!(
self.receive::<proto::GetUsers>()
.await

View File

@ -2,13 +2,12 @@ use super::{proto, Client, Status, TypedEnvelope};
use anyhow::{anyhow, Context, Result};
use collections::{hash_map::Entry, HashMap, HashSet};
use feature_flags::FeatureFlagAppExt;
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use futures::{channel::mpsc, Future, StreamExt};
use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak};
use text::ReplicaId;
use util::http::HttpClient;
use util::TryFutureExt as _;
pub type UserId = u64;
@ -20,7 +19,7 @@ pub struct ParticipantIndex(pub u32);
pub struct User {
pub id: UserId,
pub github_login: String,
pub avatar: Option<Arc<ImageData>>,
pub avatar_uri: SharedString,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -76,9 +75,8 @@ pub struct UserStore {
pending_contact_requests: HashMap<u64, usize>,
invite_info: Option<InviteInfo>,
client: Weak<Client>,
http: Arc<dyn HttpClient>,
_maintain_contacts: Task<()>,
_maintain_current_user: Task<()>,
_maintain_current_user: Task<Result<()>>,
}
#[derive(Clone)]
@ -103,9 +101,7 @@ pub enum ContactEventKind {
Cancelled,
}
impl Entity for UserStore {
type Event = Event;
}
impl EventEmitter<Event> for UserStore {}
enum UpdateContacts {
Update(proto::UpdateContacts),
@ -114,17 +110,13 @@ enum UpdateContacts {
}
impl UserStore {
pub fn new(
client: Arc<Client>,
http: Arc<dyn HttpClient>,
cx: &mut ModelContext<Self>,
) -> Self {
pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
let (mut current_user_tx, current_user_rx) = watch::channel();
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
let rpc_subscriptions = vec![
client.add_message_handler(cx.handle(), Self::handle_update_contacts),
client.add_message_handler(cx.handle(), Self::handle_update_invite_info),
client.add_message_handler(cx.handle(), Self::handle_show_contacts),
client.add_message_handler(cx.weak_model(), Self::handle_update_contacts),
client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info),
client.add_message_handler(cx.weak_model(), Self::handle_show_contacts),
];
Self {
users: Default::default(),
@ -136,76 +128,71 @@ impl UserStore {
invite_info: None,
client: Arc::downgrade(&client),
update_contacts_tx,
http,
_maintain_contacts: cx.spawn_weak(|this, mut cx| async move {
_maintain_contacts: cx.spawn(|this, mut cx| async move {
let _subscriptions = rpc_subscriptions;
while let Some(message) = update_contacts_rx.next().await {
if let Some(this) = this.upgrade(&cx) {
if let Ok(task) =
this.update(&mut cx, |this, cx| this.update_contacts(message, cx))
.log_err()
.await;
{
task.log_err().await;
} else {
break;
}
}
}),
_maintain_current_user: cx.spawn_weak(|this, mut cx| async move {
_maintain_current_user: cx.spawn(|this, mut cx| async move {
let mut status = client.status();
while let Some(status) = status.next().await {
match status {
Status::Connected { .. } => {
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
let fetch_user = this
.update(&mut cx, |this, cx| this.get_user(user_id, cx))
.log_err();
if let Some(user_id) = client.user_id() {
let fetch_user = if let Ok(fetch_user) = this
.update(&mut cx, |this, cx| {
this.get_user(user_id, cx).log_err()
}) {
fetch_user
} else {
break;
};
let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
if let Some(info) = info {
cx.update(|cx| {
cx.update(|cx| {
if let Some(info) = info {
cx.update_flags(info.staff, info.flags);
client.telemetry.set_authenticated_user_info(
Some(info.metrics_id.clone()),
info.staff,
cx,
)
});
} else {
cx.read(|cx| {
client
.telemetry
.set_authenticated_user_info(None, false, cx)
});
}
}
})?;
current_user_tx.send(user).await.ok();
this.update(&mut cx, |_, cx| {
cx.notify();
});
this.update(&mut cx, |_, cx| cx.notify())?;
}
}
Status::SignedOut => {
current_user_tx.send(None).await.ok();
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
cx.notify();
this.clear_contacts()
})
.await;
}
this.update(&mut cx, |this, cx| {
cx.notify();
this.clear_contacts()
})?
.await;
}
Status::ConnectionLost => {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
cx.notify();
this.clear_contacts()
})
.await;
}
this.update(&mut cx, |this, cx| {
cx.notify();
this.clear_contacts()
})?
.await;
}
_ => {}
}
}
Ok(())
}),
pending_contact_requests: Default::default(),
}
@ -217,7 +204,7 @@ impl UserStore {
}
async fn handle_update_invite_info(
this: ModelHandle<Self>,
this: Model<Self>,
message: TypedEnvelope<proto::UpdateInviteInfo>,
_: Arc<Client>,
mut cx: AsyncAppContext,
@ -228,17 +215,17 @@ impl UserStore {
count: message.payload.count,
});
cx.notify();
});
})?;
Ok(())
}
async fn handle_show_contacts(
this: ModelHandle<Self>,
this: Model<Self>,
_: TypedEnvelope<proto::ShowContacts>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts));
this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts))?;
Ok(())
}
@ -247,7 +234,7 @@ impl UserStore {
}
async fn handle_update_contacts(
this: ModelHandle<Self>,
this: Model<Self>,
message: TypedEnvelope<proto::UpdateContacts>,
_: Arc<Client>,
mut cx: AsyncAppContext,
@ -256,7 +243,7 @@ impl UserStore {
this.update_contacts_tx
.unbounded_send(UpdateContacts::Update(message.payload))
.unwrap();
});
})?;
Ok(())
}
@ -292,6 +279,9 @@ impl UserStore {
// Users are fetched in parallel above and cached in call to get_users
// No need to paralellize here
let mut updated_contacts = Vec::new();
let this = this
.upgrade()
.ok_or_else(|| anyhow!("can't upgrade user store handle"))?;
for contact in message.contacts {
updated_contacts.push(Arc::new(
Contact::from_proto(contact, &this, &mut cx).await?,
@ -300,18 +290,18 @@ impl UserStore {
let mut incoming_requests = Vec::new();
for request in message.incoming_requests {
incoming_requests.push(
incoming_requests.push({
this.update(&mut cx, |this, cx| {
this.get_user(request.requester_id, cx)
})
.await?,
);
})?
.await?
});
}
let mut outgoing_requests = Vec::new();
for requested_user_id in message.outgoing_requests {
outgoing_requests.push(
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))?
.await?,
);
}
@ -378,7 +368,7 @@ impl UserStore {
}
cx.notify();
});
})?;
Ok(())
})
@ -400,12 +390,6 @@ impl UserStore {
&self.incoming_contact_requests
}
pub fn has_incoming_contact_request(&self, user_id: u64) -> bool {
self.incoming_contact_requests
.iter()
.any(|user| user.id == user_id)
}
pub fn outgoing_contact_requests(&self) -> &[Arc<User>] {
&self.outgoing_contact_requests
}
@ -454,6 +438,12 @@ impl UserStore {
self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx)
}
pub fn has_incoming_contact_request(&self, user_id: u64) -> bool {
self.incoming_contact_requests
.iter()
.any(|user| user.id == user_id)
}
pub fn respond_to_contact_request(
&mut self,
requester_id: u64,
@ -480,7 +470,7 @@ impl UserStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.upgrade();
cx.spawn_weak(|_, _| async move {
cx.spawn(move |_, _| async move {
client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.request(proto::RespondToContactRequest {
@ -502,7 +492,7 @@ impl UserStore {
*self.pending_contact_requests.entry(user_id).or_insert(0) += 1;
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
let response = client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.request(request)
@ -517,7 +507,7 @@ impl UserStore {
}
}
cx.notify();
});
})?;
response?;
Ok(())
})
@ -560,11 +550,11 @@ impl UserStore {
},
cx,
)
})
})?
.await?;
}
this.read_with(&cx, |this, _| {
this.update(&mut cx, |this, _| {
user_ids
.iter()
.map(|user_id| {
@ -574,7 +564,7 @@ impl UserStore {
.ok_or_else(|| anyhow!("user {} not found", user_id))
})
.collect()
})
})?
})
}
@ -596,18 +586,18 @@ impl UserStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<Arc<User>>> {
if let Some(user) = self.users.get(&user_id).cloned() {
return cx.foreground().spawn(async move { Ok(user) });
return Task::ready(Ok(user));
}
let load_users = self.get_users(vec![user_id], cx);
cx.spawn(|this, mut cx| async move {
cx.spawn(move |this, mut cx| async move {
load_users.await?;
this.update(&mut cx, |this, _| {
this.users
.get(&user_id)
.cloned()
.ok_or_else(|| anyhow!("server responded with no users"))
})
})?
})
}
@ -625,25 +615,22 @@ impl UserStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Arc<User>>>> {
let client = self.client.clone();
let http = self.http.clone();
cx.spawn_weak(|this, mut cx| async move {
cx.spawn(|this, mut cx| async move {
if let Some(rpc) = client.upgrade() {
let response = rpc.request(request).await.context("error loading users")?;
let users = future::join_all(
response
.users
.into_iter()
.map(|user| User::new(user, http.as_ref())),
)
.await;
let users = response
.users
.into_iter()
.map(|user| User::new(user))
.collect::<Vec<_>>();
this.update(&mut cx, |this, _| {
for user in &users {
this.users.insert(user.id, user.clone());
}
})
.ok();
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, _| {
for user in &users {
this.users.insert(user.id, user.clone());
}
});
}
Ok(users)
} else {
Ok(Vec::new())
@ -668,11 +655,11 @@ impl UserStore {
}
impl User {
async fn new(message: proto::User, http: &dyn HttpClient) -> Arc<Self> {
fn new(message: proto::User) -> Arc<Self> {
Arc::new(User {
id: message.id,
github_login: message.github_login,
avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await,
avatar_uri: message.avatar_url.into(),
})
}
}
@ -680,13 +667,13 @@ impl User {
impl Contact {
async fn from_proto(
contact: proto::Contact,
user_store: &ModelHandle<UserStore>,
user_store: &Model<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<Self> {
let user = user_store
.update(cx, |user_store, cx| {
user_store.get_user(contact.user_id, cx)
})
})?
.await?;
Ok(Self {
user,
@ -705,24 +692,3 @@ impl Collaborator {
})
}
}
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
let mut response = http
.get(url, Default::default(), true)
.await
.map_err(|e| anyhow!("failed to send user avatar request: {}", e))?;
if !response.status().is_success() {
return Err(anyhow!("avatar request failed {:?}", response.status()));
}
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?;
let format = image::guess_format(&body)?;
let image = image::load_from_memory_with_format(&body, format)?.into_bgra8();
Ok(ImageData::new(image))
}

View File

@ -1,53 +0,0 @@
[package]
name = "client2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/client2.rs"
doctest = false
[features]
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
collections = { path = "../collections" }
db = { package = "db2", path = "../db2" }
gpui = { package = "gpui2", path = "../gpui2" }
util = { path = "../util" }
rpc = { package = "rpc2", path = "../rpc2" }
text = { package = "text2", path = "../text2" }
settings = { package = "settings2", path = "../settings2" }
feature_flags = { path = "../feature_flags" }
sum_tree = { path = "../sum_tree" }
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { version = "0.16", features = ["async-tls"] }
futures.workspace = true
image = "0.23"
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
smol.workspace = true
sysinfo.workspace = true
tempfile = "3"
thiserror.workspace = true
time.workspace = true
tiny_http = "0.8"
uuid.workspace = true
url = "2.2"
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

File diff suppressed because it is too large Load Diff

View File

@ -1,515 +0,0 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use chrono::{DateTime, Utc};
use futures::Future;
use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use settings::Settings;
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
};
use tempfile::NamedTempFile;
use util::http::HttpClient;
use util::{channel::ReleaseChannel, TryFutureExt};
pub struct Telemetry {
http_client: Arc<dyn HttpClient>,
executor: BackgroundExecutor,
state: Mutex<TelemetryState>,
}
struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
session_id: Option<Arc<str>>, // Per app launch
release_channel: Option<&'static str>,
app_metadata: AppMetadata,
architecture: &'static str,
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
flush_clickhouse_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
first_event_datetime: Option<DateTime<Utc>>,
}
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
lazy_static! {
static ref CLICKHOUSE_EVENTS_URL: String =
format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
}
#[derive(Serialize, Debug)]
struct ClickhouseEventRequestBody {
token: &'static str,
installation_id: Option<Arc<str>>,
session_id: Option<Arc<str>>,
is_staff: Option<bool>,
app_version: Option<String>,
os_name: &'static str,
os_version: Option<String>,
architecture: &'static str,
release_channel: Option<&'static str>,
events: Vec<ClickhouseEventWrapper>,
}
#[derive(Serialize, Debug)]
struct ClickhouseEventWrapper {
signed_in: bool,
#[serde(flatten)]
event: ClickhouseEvent,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum AssistantKind {
Panel,
Inline,
}
#[derive(Serialize, Debug)]
#[serde(tag = "type")]
pub enum ClickhouseEvent {
Editor {
operation: &'static str,
file_extension: Option<String>,
vim_mode: bool,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
milliseconds_since_first_event: i64,
},
Copilot {
suggestion_id: Option<String>,
suggestion_accepted: bool,
file_extension: Option<String>,
milliseconds_since_first_event: i64,
},
Call {
operation: &'static str,
room_id: Option<u64>,
channel_id: Option<u64>,
milliseconds_since_first_event: i64,
},
Assistant {
conversation_id: Option<String>,
kind: AssistantKind,
model: &'static str,
milliseconds_since_first_event: i64,
},
Cpu {
usage_as_percentage: f32,
core_count: u32,
milliseconds_since_first_event: i64,
},
Memory {
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
milliseconds_since_first_event: i64,
},
App {
operation: &'static str,
milliseconds_since_first_event: i64,
},
Setting {
setting: &'static str,
value: String,
milliseconds_since_first_event: i64,
},
}
#[cfg(debug_assertions)]
const MAX_QUEUE_LEN: usize = 1;
#[cfg(not(debug_assertions))]
const MAX_QUEUE_LEN: usize = 50;
#[cfg(debug_assertions)]
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(not(debug_assertions))]
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(60 * 5);
impl Telemetry {
pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
let release_channel = if cx.has_global::<ReleaseChannel>() {
Some(cx.global::<ReleaseChannel>().display_name())
} else {
None
};
// TODO: Replace all hardware stuff with nested SystemSpecs json
let this = Arc::new(Self {
http_client: client,
executor: cx.background_executor().clone(),
state: Mutex::new(TelemetryState {
app_metadata: cx.app_metadata(),
architecture: env::consts::ARCH,
release_channel,
installation_id: None,
metrics_id: None,
session_id: None,
clickhouse_events_queue: Default::default(),
flush_clickhouse_events_task: Default::default(),
log_file: None,
is_staff: None,
first_event_datetime: None,
}),
});
// We should only ever have one instance of Telemetry, leak the subscription to keep it alive
// rather than store in TelemetryState, complicating spawn as subscriptions are not Send
std::mem::forget(cx.on_app_quit({
let this = this.clone();
move |cx| this.shutdown_telemetry(cx)
}));
this
}
#[cfg(any(test, feature = "test-support"))]
fn shutdown_telemetry(self: &Arc<Self>, _: &mut AppContext) -> impl Future<Output = ()> {
Task::ready(())
}
// Skip calling this function in tests.
// TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings
#[cfg(not(any(test, feature = "test-support")))]
fn shutdown_telemetry(self: &Arc<Self>, cx: &mut AppContext) -> impl Future<Output = ()> {
let telemetry_settings = TelemetrySettings::get_global(cx).clone();
self.report_app_event(telemetry_settings, "close", true);
Task::ready(())
}
pub fn log_file_path(&self) -> Option<PathBuf> {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
pub fn start(
self: &Arc<Self>,
installation_id: Option<String>,
session_id: String,
cx: &mut AppContext,
) {
let mut state = self.state.lock();
state.installation_id = installation_id.map(|id| id.into());
state.session_id = Some(session_id.into());
drop(state);
let this = self.clone();
cx.spawn(|cx| async move {
// Avoiding calling `System::new_all()`, as there have been crashes related to it
let refresh_kind = RefreshKind::new()
.with_memory() // For memory usage
.with_processes(ProcessRefreshKind::everything()) // For process usage
.with_cpu(CpuRefreshKind::everything()); // For core count
let mut system = System::new_with_specifics(refresh_kind);
// Avoiding calling `refresh_all()`, just update what we need
system.refresh_specifics(refresh_kind);
// Waiting some amount of time before the first query is important to get a reasonable value
// https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(4 * 60);
loop {
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
system.refresh_specifics(refresh_kind);
let current_process = Pid::from_u32(std::process::id());
let Some(process) = system.processes().get(&current_process) else {
let process = current_process;
log::error!("Failed to find own process {process:?} in system process table");
// TODO: Fire an error telemetry event
return;
};
let telemetry_settings = if let Ok(telemetry_settings) =
cx.update(|cx| *TelemetrySettings::get_global(cx))
{
telemetry_settings
} else {
break;
};
this.report_memory_event(
telemetry_settings,
process.memory(),
process.virtual_memory(),
);
this.report_cpu_event(
telemetry_settings,
process.cpu_usage(),
system.cpus().len() as u32,
);
}
})
.detach();
}
pub fn set_authenticated_user_info(
self: &Arc<Self>,
metrics_id: Option<String>,
is_staff: bool,
cx: &AppContext,
) {
if !TelemetrySettings::get_global(cx).metrics {
return;
}
let mut state = self.state.lock();
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
state.metrics_id = metrics_id.clone();
state.is_staff = Some(is_staff);
drop(state);
}
pub fn report_editor_event(
self: &Arc<Self>,
telemetry_settings: TelemetrySettings,
file_extension: Option<String>,
vim_mode: bool,
operation: &'static str,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
) {
let event = ClickhouseEvent::Editor {
file_extension,
vim_mode,
operation,
copilot_enabled,
copilot_enabled_for_language,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
self.report_clickhouse_event(event, telemetry_settings, false)
}
pub fn report_copilot_event(
self: &Arc<Self>,
telemetry_settings: TelemetrySettings,
suggestion_id: Option<String>,
suggestion_accepted: bool,
file_extension: Option<String>,
) {
let event = ClickhouseEvent::Copilot {
suggestion_id,
suggestion_accepted,
file_extension,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
self.report_clickhouse_event(event, telemetry_settings, false)
}
pub fn report_assistant_event(
self: &Arc<Self>,
telemetry_settings: TelemetrySettings,
conversation_id: Option<String>,
kind: AssistantKind,
model: &'static str,
) {
let event = ClickhouseEvent::Assistant {
conversation_id,
kind,
model,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
self.report_clickhouse_event(event, telemetry_settings, false)
}
pub fn report_call_event(
self: &Arc<Self>,
telemetry_settings: TelemetrySettings,
operation: &'static str,
room_id: Option<u64>,
channel_id: Option<u64>,
) {
let event = ClickhouseEvent::Call {
operation,
room_id,
channel_id,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
self.report_clickhouse_event(event, telemetry_settings, false)
}
pub fn report_cpu_event(
self: &Arc<Self>,
telemetry_settings: TelemetrySettings,
usage_as_percentage: f32,
core_count: u32,
) {
let event = ClickhouseEvent::Cpu {
usage_as_percentage,
core_count,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
self.report_clickhouse_event(event, telemetry_settings, false)
}
pub fn report_memory_event(
self: &Arc<Self>,
telemetry_settings: TelemetrySettings,
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
) {
let event = ClickhouseEvent::Memory {
memory_in_bytes,
virtual_memory_in_bytes,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
self.report_clickhouse_event(event, telemetry_settings, false)
}
pub fn report_app_event(
self: &Arc<Self>,
telemetry_settings: TelemetrySettings,
operation: &'static str,
immediate_flush: bool,
) {
let event = ClickhouseEvent::App {
operation,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
self.report_clickhouse_event(event, telemetry_settings, immediate_flush)
}
pub fn report_setting_event(
self: &Arc<Self>,
telemetry_settings: TelemetrySettings,
setting: &'static str,
value: String,
) {
let event = ClickhouseEvent::Setting {
setting,
value,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
self.report_clickhouse_event(event, telemetry_settings, false)
}
fn milliseconds_since_first_event(&self) -> i64 {
let mut state = self.state.lock();
match state.first_event_datetime {
Some(first_event_datetime) => {
let now: DateTime<Utc> = Utc::now();
now.timestamp_millis() - first_event_datetime.timestamp_millis()
}
None => {
state.first_event_datetime = Some(Utc::now());
0
}
}
}
fn report_clickhouse_event(
self: &Arc<Self>,
event: ClickhouseEvent,
telemetry_settings: TelemetrySettings,
immediate_flush: bool,
) {
if !telemetry_settings.metrics {
return;
}
let mut state = self.state.lock();
let signed_in = state.metrics_id.is_some();
state
.clickhouse_events_queue
.push(ClickhouseEventWrapper { signed_in, event });
if state.installation_id.is_some() {
if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush_clickhouse_events();
} else {
let this = self.clone();
let executor = self.executor.clone();
state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
executor.timer(DEBOUNCE_INTERVAL).await;
this.flush_clickhouse_events();
}));
}
}
}
pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
self.state.lock().metrics_id.clone()
}
pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
self.state.lock().installation_id.clone()
}
pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
self.state.lock().is_staff
}
fn flush_clickhouse_events(self: &Arc<Self>) {
let mut state = self.state.lock();
state.first_event_datetime = None;
let mut events = mem::take(&mut state.clickhouse_events_queue);
state.flush_clickhouse_events_task.take();
drop(state);
let this = self.clone();
self.executor
.spawn(
async move {
let mut json_bytes = Vec::new();
if let Some(file) = &mut this.state.lock().log_file {
let file = file.as_file_mut();
for event in &mut events {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, event)?;
file.write_all(&json_bytes)?;
file.write(b"\n")?;
}
}
{
let state = this.state.lock();
let request_body = ClickhouseEventRequestBody {
token: ZED_SECRET_CLIENT_TOKEN,
installation_id: state.installation_id.clone(),
session_id: state.session_id.clone(),
is_staff: state.is_staff.clone(),
app_version: state
.app_metadata
.app_version
.map(|version| version.to_string()),
os_name: state.app_metadata.os_name,
os_version: state
.app_metadata
.os_version
.map(|version| version.to_string()),
architecture: state.architecture,
release_channel: state.release_channel,
events,
};
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &request_body)?;
}
this.http_client
.post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
.await?;
anyhow::Ok(())
}
.log_err(),
)
.detach();
}
}

View File

@ -1,214 +0,0 @@
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
use anyhow::{anyhow, Result};
use futures::{stream::BoxStream, StreamExt};
use gpui::{BackgroundExecutor, Context, Model, TestAppContext};
use parking_lot::Mutex;
use rpc::{
proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
ConnectionId, Peer, Receipt, TypedEnvelope,
};
use std::sync::Arc;
pub struct FakeServer {
peer: Arc<Peer>,
state: Arc<Mutex<FakeServerState>>,
user_id: u64,
executor: BackgroundExecutor,
}
#[derive(Default)]
struct FakeServerState {
incoming: Option<BoxStream<'static, Box<dyn proto::AnyTypedEnvelope>>>,
connection_id: Option<ConnectionId>,
forbid_connections: bool,
auth_count: usize,
access_token: usize,
}
impl FakeServer {
pub async fn for_client(
client_user_id: u64,
client: &Arc<Client>,
cx: &TestAppContext,
) -> Self {
let server = Self {
peer: Peer::new(0),
state: Default::default(),
user_id: client_user_id,
executor: cx.executor(),
};
client
.override_authenticate({
let state = Arc::downgrade(&server.state);
move |cx| {
let state = state.clone();
cx.spawn(move |_| async move {
let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
let mut state = state.lock();
state.auth_count += 1;
let access_token = state.access_token.to_string();
Ok(Credentials {
user_id: client_user_id,
access_token,
})
})
}
})
.override_establish_connection({
let peer = Arc::downgrade(&server.peer);
let state = Arc::downgrade(&server.state);
move |credentials, cx| {
let peer = peer.clone();
let state = state.clone();
let credentials = credentials.clone();
cx.spawn(move |cx| async move {
let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
let peer = peer.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
if state.lock().forbid_connections {
Err(EstablishConnectionError::Other(anyhow!(
"server is forbidding connections"
)))?
}
assert_eq!(credentials.user_id, client_user_id);
if credentials.access_token != state.lock().access_token.to_string() {
Err(EstablishConnectionError::Unauthorized)?
}
let (client_conn, server_conn, _) =
Connection::in_memory(cx.background_executor().clone());
let (connection_id, io, incoming) =
peer.add_test_connection(server_conn, cx.background_executor().clone());
cx.background_executor().spawn(io).detach();
{
let mut state = state.lock();
state.connection_id = Some(connection_id);
state.incoming = Some(incoming);
}
peer.send(
connection_id,
proto::Hello {
peer_id: Some(connection_id.into()),
},
)
.unwrap();
Ok(client_conn)
})
}
});
client
.authenticate_and_connect(false, &cx.to_async())
.await
.unwrap();
server
}
pub fn disconnect(&self) {
if self.state.lock().connection_id.is_some() {
self.peer.disconnect(self.connection_id());
let mut state = self.state.lock();
state.connection_id.take();
state.incoming.take();
}
}
pub fn auth_count(&self) -> usize {
self.state.lock().auth_count
}
pub fn roll_access_token(&self) {
self.state.lock().access_token += 1;
}
pub fn forbid_connections(&self) {
self.state.lock().forbid_connections = true;
}
pub fn allow_connections(&self) {
self.state.lock().forbid_connections = false;
}
pub fn send<T: proto::EnvelopedMessage>(&self, message: T) {
self.peer.send(self.connection_id(), message).unwrap();
}
#[allow(clippy::await_holding_lock)]
pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
self.executor.start_waiting();
loop {
let message = self
.state
.lock()
.incoming
.as_mut()
.expect("not connected")
.next()
.await
.ok_or_else(|| anyhow!("other half hung up"))?;
self.executor.finish_waiting();
let type_name = message.payload_type_name();
let message = message.into_any();
if message.is::<TypedEnvelope<M>>() {
return Ok(*message.downcast().unwrap());
}
if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
self.respond(
message
.downcast::<TypedEnvelope<GetPrivateUserInfo>>()
.unwrap()
.receipt(),
GetPrivateUserInfoResponse {
metrics_id: "the-metrics-id".into(),
staff: false,
flags: Default::default(),
},
);
continue;
}
panic!(
"fake server received unexpected message type: {:?}",
type_name
);
}
}
pub fn respond<T: proto::RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) {
self.peer.respond(receipt, response).unwrap()
}
fn connection_id(&self) -> ConnectionId {
self.state.lock().connection_id.expect("not connected")
}
pub async fn build_user_store(
&self,
client: Arc<Client>,
cx: &mut TestAppContext,
) -> Model<UserStore> {
let user_store = cx.new_model(|cx| UserStore::new(client, cx));
assert_eq!(
self.receive::<proto::GetUsers>()
.await
.unwrap()
.payload
.user_ids,
&[self.user_id]
);
user_store
}
}
impl Drop for FakeServer {
fn drop(&mut self) {
self.disconnect();
}
}

View File

@ -1,694 +0,0 @@
use super::{proto, Client, Status, TypedEnvelope};
use anyhow::{anyhow, Context, Result};
use collections::{hash_map::Entry, HashMap, HashSet};
use feature_flags::FeatureFlagAppExt;
use futures::{channel::mpsc, Future, StreamExt};
use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak};
use text::ReplicaId;
use util::TryFutureExt as _;
pub type UserId = u64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParticipantIndex(pub u32);
#[derive(Default, Debug)]
pub struct User {
pub id: UserId,
pub github_login: String,
pub avatar_uri: SharedString,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Collaborator {
pub peer_id: proto::PeerId,
pub replica_id: ReplicaId,
pub user_id: UserId,
}
impl PartialOrd for User {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for User {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.github_login.cmp(&other.github_login)
}
}
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.github_login == other.github_login
}
}
impl Eq for User {}
#[derive(Debug, PartialEq)]
pub struct Contact {
pub user: Arc<User>,
pub online: bool,
pub busy: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContactRequestStatus {
None,
RequestSent,
RequestReceived,
RequestAccepted,
}
pub struct UserStore {
users: HashMap<u64, Arc<User>>,
participant_indices: HashMap<u64, ParticipantIndex>,
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
current_user: watch::Receiver<Option<Arc<User>>>,
contacts: Vec<Arc<Contact>>,
incoming_contact_requests: Vec<Arc<User>>,
outgoing_contact_requests: Vec<Arc<User>>,
pending_contact_requests: HashMap<u64, usize>,
invite_info: Option<InviteInfo>,
client: Weak<Client>,
_maintain_contacts: Task<()>,
_maintain_current_user: Task<Result<()>>,
}
#[derive(Clone)]
pub struct InviteInfo {
pub count: u32,
pub url: Arc<str>,
}
pub enum Event {
Contact {
user: Arc<User>,
kind: ContactEventKind,
},
ShowContacts,
ParticipantIndicesChanged,
}
#[derive(Clone, Copy)]
pub enum ContactEventKind {
Requested,
Accepted,
Cancelled,
}
impl EventEmitter<Event> for UserStore {}
enum UpdateContacts {
Update(proto::UpdateContacts),
Wait(postage::barrier::Sender),
Clear(postage::barrier::Sender),
}
impl UserStore {
pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
let (mut current_user_tx, current_user_rx) = watch::channel();
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
let rpc_subscriptions = vec![
client.add_message_handler(cx.weak_model(), Self::handle_update_contacts),
client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info),
client.add_message_handler(cx.weak_model(), Self::handle_show_contacts),
];
Self {
users: Default::default(),
current_user: current_user_rx,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
participant_indices: Default::default(),
outgoing_contact_requests: Default::default(),
invite_info: None,
client: Arc::downgrade(&client),
update_contacts_tx,
_maintain_contacts: cx.spawn(|this, mut cx| async move {
let _subscriptions = rpc_subscriptions;
while let Some(message) = update_contacts_rx.next().await {
if let Ok(task) =
this.update(&mut cx, |this, cx| this.update_contacts(message, cx))
{
task.log_err().await;
} else {
break;
}
}
}),
_maintain_current_user: cx.spawn(|this, mut cx| async move {
let mut status = client.status();
while let Some(status) = status.next().await {
match status {
Status::Connected { .. } => {
if let Some(user_id) = client.user_id() {
let fetch_user = if let Ok(fetch_user) = this
.update(&mut cx, |this, cx| {
this.get_user(user_id, cx).log_err()
}) {
fetch_user
} else {
break;
};
let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
cx.update(|cx| {
if let Some(info) = info {
cx.update_flags(info.staff, info.flags);
client.telemetry.set_authenticated_user_info(
Some(info.metrics_id.clone()),
info.staff,
cx,
)
}
})?;
current_user_tx.send(user).await.ok();
this.update(&mut cx, |_, cx| cx.notify())?;
}
}
Status::SignedOut => {
current_user_tx.send(None).await.ok();
this.update(&mut cx, |this, cx| {
cx.notify();
this.clear_contacts()
})?
.await;
}
Status::ConnectionLost => {
this.update(&mut cx, |this, cx| {
cx.notify();
this.clear_contacts()
})?
.await;
}
_ => {}
}
}
Ok(())
}),
pending_contact_requests: Default::default(),
}
}
#[cfg(feature = "test-support")]
pub fn clear_cache(&mut self) {
self.users.clear();
}
async fn handle_update_invite_info(
this: Model<Self>,
message: TypedEnvelope<proto::UpdateInviteInfo>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.invite_info = Some(InviteInfo {
url: Arc::from(message.payload.url),
count: message.payload.count,
});
cx.notify();
})?;
Ok(())
}
async fn handle_show_contacts(
this: Model<Self>,
_: TypedEnvelope<proto::ShowContacts>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts))?;
Ok(())
}
pub fn invite_info(&self) -> Option<&InviteInfo> {
self.invite_info.as_ref()
}
async fn handle_update_contacts(
this: Model<Self>,
message: TypedEnvelope<proto::UpdateContacts>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
this.update_contacts_tx
.unbounded_send(UpdateContacts::Update(message.payload))
.unwrap();
})?;
Ok(())
}
fn update_contacts(
&mut self,
message: UpdateContacts,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
match message {
UpdateContacts::Wait(barrier) => {
drop(barrier);
Task::ready(Ok(()))
}
UpdateContacts::Clear(barrier) => {
self.contacts.clear();
self.incoming_contact_requests.clear();
self.outgoing_contact_requests.clear();
drop(barrier);
Task::ready(Ok(()))
}
UpdateContacts::Update(message) => {
let mut user_ids = HashSet::default();
for contact in &message.contacts {
user_ids.insert(contact.user_id);
}
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
user_ids.extend(message.outgoing_requests.iter());
let load_users = self.get_users(user_ids.into_iter().collect(), cx);
cx.spawn(|this, mut cx| async move {
load_users.await?;
// Users are fetched in parallel above and cached in call to get_users
// No need to paralellize here
let mut updated_contacts = Vec::new();
let this = this
.upgrade()
.ok_or_else(|| anyhow!("can't upgrade user store handle"))?;
for contact in message.contacts {
updated_contacts.push(Arc::new(
Contact::from_proto(contact, &this, &mut cx).await?,
));
}
let mut incoming_requests = Vec::new();
for request in message.incoming_requests {
incoming_requests.push({
this.update(&mut cx, |this, cx| {
this.get_user(request.requester_id, cx)
})?
.await?
});
}
let mut outgoing_requests = Vec::new();
for requested_user_id in message.outgoing_requests {
outgoing_requests.push(
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))?
.await?,
);
}
let removed_contacts =
HashSet::<u64>::from_iter(message.remove_contacts.iter().copied());
let removed_incoming_requests =
HashSet::<u64>::from_iter(message.remove_incoming_requests.iter().copied());
let removed_outgoing_requests =
HashSet::<u64>::from_iter(message.remove_outgoing_requests.iter().copied());
this.update(&mut cx, |this, cx| {
// Remove contacts
this.contacts
.retain(|contact| !removed_contacts.contains(&contact.user.id));
// Update existing contacts and insert new ones
for updated_contact in updated_contacts {
match this.contacts.binary_search_by_key(
&&updated_contact.user.github_login,
|contact| &contact.user.github_login,
) {
Ok(ix) => this.contacts[ix] = updated_contact,
Err(ix) => this.contacts.insert(ix, updated_contact),
}
}
// Remove incoming contact requests
this.incoming_contact_requests.retain(|user| {
if removed_incoming_requests.contains(&user.id) {
cx.emit(Event::Contact {
user: user.clone(),
kind: ContactEventKind::Cancelled,
});
false
} else {
true
}
});
// Update existing incoming requests and insert new ones
for user in incoming_requests {
match this
.incoming_contact_requests
.binary_search_by_key(&&user.github_login, |contact| {
&contact.github_login
}) {
Ok(ix) => this.incoming_contact_requests[ix] = user,
Err(ix) => this.incoming_contact_requests.insert(ix, user),
}
}
// Remove outgoing contact requests
this.outgoing_contact_requests
.retain(|user| !removed_outgoing_requests.contains(&user.id));
// Update existing incoming requests and insert new ones
for request in outgoing_requests {
match this
.outgoing_contact_requests
.binary_search_by_key(&&request.github_login, |contact| {
&contact.github_login
}) {
Ok(ix) => this.outgoing_contact_requests[ix] = request,
Err(ix) => this.outgoing_contact_requests.insert(ix, request),
}
}
cx.notify();
})?;
Ok(())
})
}
}
}
pub fn contacts(&self) -> &[Arc<Contact>] {
&self.contacts
}
pub fn has_contact(&self, user: &Arc<User>) -> bool {
self.contacts
.binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login)
.is_ok()
}
pub fn incoming_contact_requests(&self) -> &[Arc<User>] {
&self.incoming_contact_requests
}
pub fn outgoing_contact_requests(&self) -> &[Arc<User>] {
&self.outgoing_contact_requests
}
pub fn is_contact_request_pending(&self, user: &User) -> bool {
self.pending_contact_requests.contains_key(&user.id)
}
pub fn contact_request_status(&self, user: &User) -> ContactRequestStatus {
if self
.contacts
.binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login)
.is_ok()
{
ContactRequestStatus::RequestAccepted
} else if self
.outgoing_contact_requests
.binary_search_by_key(&&user.github_login, |user| &user.github_login)
.is_ok()
{
ContactRequestStatus::RequestSent
} else if self
.incoming_contact_requests
.binary_search_by_key(&&user.github_login, |user| &user.github_login)
.is_ok()
{
ContactRequestStatus::RequestReceived
} else {
ContactRequestStatus::None
}
}
pub fn request_contact(
&mut self,
responder_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
self.perform_contact_request(responder_id, proto::RequestContact { responder_id }, cx)
}
pub fn remove_contact(
&mut self,
user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx)
}
pub fn has_incoming_contact_request(&self, user_id: u64) -> bool {
self.incoming_contact_requests
.iter()
.any(|user| user.id == user_id)
}
pub fn respond_to_contact_request(
&mut self,
requester_id: u64,
accept: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
self.perform_contact_request(
requester_id,
proto::RespondToContactRequest {
requester_id,
response: if accept {
proto::ContactRequestResponse::Accept
} else {
proto::ContactRequestResponse::Decline
} as i32,
},
cx,
)
}
pub fn dismiss_contact_request(
&mut self,
requester_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.upgrade();
cx.spawn(move |_, _| async move {
client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.request(proto::RespondToContactRequest {
requester_id,
response: proto::ContactRequestResponse::Dismiss as i32,
})
.await?;
Ok(())
})
}
fn perform_contact_request<T: RequestMessage>(
&mut self,
user_id: u64,
request: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.upgrade();
*self.pending_contact_requests.entry(user_id).or_insert(0) += 1;
cx.notify();
cx.spawn(move |this, mut cx| async move {
let response = client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.request(request)
.await;
this.update(&mut cx, |this, cx| {
if let Entry::Occupied(mut request_count) =
this.pending_contact_requests.entry(user_id)
{
*request_count.get_mut() -= 1;
if *request_count.get() == 0 {
request_count.remove();
}
}
cx.notify();
})?;
response?;
Ok(())
})
}
pub fn clear_contacts(&mut self) -> impl Future<Output = ()> {
let (tx, mut rx) = postage::barrier::channel();
self.update_contacts_tx
.unbounded_send(UpdateContacts::Clear(tx))
.unwrap();
async move {
rx.next().await;
}
}
pub fn contact_updates_done(&mut self) -> impl Future<Output = ()> {
let (tx, mut rx) = postage::barrier::channel();
self.update_contacts_tx
.unbounded_send(UpdateContacts::Wait(tx))
.unwrap();
async move {
rx.next().await;
}
}
pub fn get_users(
&mut self,
user_ids: Vec<u64>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Arc<User>>>> {
let mut user_ids_to_fetch = user_ids.clone();
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
cx.spawn(|this, mut cx| async move {
if !user_ids_to_fetch.is_empty() {
this.update(&mut cx, |this, cx| {
this.load_users(
proto::GetUsers {
user_ids: user_ids_to_fetch,
},
cx,
)
})?
.await?;
}
this.update(&mut cx, |this, _| {
user_ids
.iter()
.map(|user_id| {
this.users
.get(user_id)
.cloned()
.ok_or_else(|| anyhow!("user {} not found", user_id))
})
.collect()
})?
})
}
pub fn fuzzy_search_users(
&mut self,
query: String,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Arc<User>>>> {
self.load_users(proto::FuzzySearchUsers { query }, cx)
}
pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
self.users.get(&user_id).cloned()
}
pub fn get_user(
&mut self,
user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<Arc<User>>> {
if let Some(user) = self.users.get(&user_id).cloned() {
return Task::ready(Ok(user));
}
let load_users = self.get_users(vec![user_id], cx);
cx.spawn(move |this, mut cx| async move {
load_users.await?;
this.update(&mut cx, |this, _| {
this.users
.get(&user_id)
.cloned()
.ok_or_else(|| anyhow!("server responded with no users"))
})?
})
}
pub fn current_user(&self) -> Option<Arc<User>> {
self.current_user.borrow().clone()
}
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
self.current_user.clone()
}
fn load_users(
&mut self,
request: impl RequestMessage<Response = UsersResponse>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Arc<User>>>> {
let client = self.client.clone();
cx.spawn(|this, mut cx| async move {
if let Some(rpc) = client.upgrade() {
let response = rpc.request(request).await.context("error loading users")?;
let users = response
.users
.into_iter()
.map(|user| User::new(user))
.collect::<Vec<_>>();
this.update(&mut cx, |this, _| {
for user in &users {
this.users.insert(user.id, user.clone());
}
})
.ok();
Ok(users)
} else {
Ok(Vec::new())
}
})
}
pub fn set_participant_indices(
&mut self,
participant_indices: HashMap<u64, ParticipantIndex>,
cx: &mut ModelContext<Self>,
) {
if participant_indices != self.participant_indices {
self.participant_indices = participant_indices;
cx.emit(Event::ParticipantIndicesChanged);
}
}
pub fn participant_indices(&self) -> &HashMap<u64, ParticipantIndex> {
&self.participant_indices
}
}
impl User {
fn new(message: proto::User) -> Arc<Self> {
Arc::new(User {
id: message.id,
github_login: message.github_login,
avatar_uri: message.avatar_url.into(),
})
}
}
impl Contact {
async fn from_proto(
contact: proto::Contact,
user_store: &Model<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<Self> {
let user = user_store
.update(cx, |user_store, cx| {
user_store.get_user(contact.user_id, cx)
})?
.await?;
Ok(Self {
user,
online: contact.online,
busy: contact.busy,
})
}
}
impl Collaborator {
pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
Ok(Self {
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
replica_id: message.replica_id as ReplicaId,
user_id: message.user_id as UserId,
})
}
}

View File

@ -63,9 +63,9 @@ uuid.workspace = true
audio = { path = "../audio" }
collections = { path = "../collections", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
call = { package = "call2", path = "../call2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"] }
channel = { package = "channel2", path = "../channel2" }
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
channel = { path = "../channel" }
editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
fs = { package = "fs2", path = "../fs2", features = ["test-support"] }

View File

@ -24,9 +24,9 @@ test-support = [
[dependencies]
auto_update = { path = "../auto_update" }
db = { package = "db2", path = "../db2" }
call = { package = "call2", path = "../call2" }
client = { package = "client2", path = "../client2" }
channel = { package = "channel2", path = "../channel2" }
call = { path = "../call" }
client = { path = "../client" }
channel = { path = "../channel" }
clock = { path = "../clock" }
collections = { path = "../collections" }
# context_menu = { path = "../context_menu" }
@ -65,8 +65,8 @@ time.workspace = true
smallvec.workspace = true
[dev-dependencies]
call = { package = "call2", path = "../call2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"] }
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }

View File

@ -31,7 +31,7 @@ smallvec.workspace = true
postage.workspace = true
[dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }

View File

@ -23,7 +23,7 @@ test-support = [
]
[dependencies]
client = { package = "client2", path = "../client2" }
client = { path = "../client" }
clock = { path = "../clock" }
copilot = { path = "../copilot" }
db = { package="db2", path = "../db2" }

View File

@ -11,7 +11,7 @@ path = "src/feedback.rs"
test-support = []
[dependencies]
client = { package = "client2", path = "../client2" }
client = { path = "../client" }
db = { package = "db2", path = "../db2" }
editor = { path = "../editor" }
gpui = { package = "gpui2", path = "../gpui2" }

View File

@ -61,7 +61,7 @@ tree-sitter-typescript = { workspace = true, optional = true }
pulldown-cmark = { version = "0.9.2", default-features = false }
[dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }

View File

@ -26,7 +26,7 @@ anyhow.workspace = true
tree-sitter.workspace = true
[dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

View File

@ -20,7 +20,7 @@ test-support = [
]
[dependencies]
client = { package = "client2", path = "../client2" }
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
git = { package = "git3", path = "../git3" }

View File

@ -17,8 +17,8 @@ test-support = [
]
[dependencies]
channel = { package = "channel2", path = "../channel2" }
client = { package = "client2", path = "../client2" }
channel = { path = "../channel" }
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
db = { package = "db2", path = "../db2" }
@ -34,7 +34,7 @@ anyhow.workspace = true
time.workspace = true
[dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }

View File

@ -12,7 +12,7 @@ doctest = false
test-support = []
[dependencies]
client = { package = "client2", path = "../client2" }
client = { path = "../client" }
collections = { path = "../collections"}
language = { path = "../language" }
gpui = { package = "gpui2", path = "../gpui2" }

View File

@ -22,7 +22,7 @@ test-support = [
[dependencies]
text = { package = "text2", path = "../text2" }
copilot = { path = "../copilot" }
client = { package = "client2", path = "../client2" }
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
db = { package = "db2", path = "../db2" }
@ -69,7 +69,7 @@ itertools = "0.10"
ctor.workspace = true
env_logger.workspace = true
pretty_assertions.workspace = true
client = { package = "client2", path = "../client2", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
db = { package = "db2", path = "../db2", features = ["test-support"] }
fs = { package = "fs2", path = "../fs2", features = ["test-support"] }

View File

@ -33,7 +33,7 @@ pretty_assertions.workspace = true
unicase = "2.6"
[dev-dependencies]
client = { path = "../client2", package = "client2", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }

View File

@ -32,7 +32,7 @@ smallvec.workspace = true
smol.workspace = true
serde_json.workspace = true
[dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }

View File

@ -48,7 +48,7 @@ rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"]}
rust-embed = { version = "8.0", features = ["include-exclude"] }
client = { package = "client2", path = "../client2" }
client = { path = "../client" }
node_runtime = { path = "../node_runtime"}
pretty_assertions.workspace = true

View File

@ -40,7 +40,7 @@ serde_derive.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"]}
client = { path = "../client", features = ["test-support"]}
project = { path = "../project", features = ["test-support"]}
workspace = { path = "../workspace", features = ["test-support"] }
rand.workspace = true

View File

@ -9,7 +9,7 @@ path = "src/theme_selector.rs"
doctest = false
[dependencies]
client = { package = "client2", path = "../client2" }
client = { path = "../client" }
editor = { path = "../editor" }
feature_flags = { path = "../feature_flags" }
fs = { package = "fs2", path = "../fs2" }

View File

@ -11,7 +11,7 @@ path = "src/welcome.rs"
test-support = []
[dependencies]
client = { package = "client2", path = "../client2" }
client = { path = "../client" }
editor = { path = "../editor" }
fs = { package = "fs2", path = "../fs2" }
fuzzy = { path = "../fuzzy" }

View File

@ -20,8 +20,8 @@ test-support = [
[dependencies]
db = { path = "../db2", package = "db2" }
call = { path = "../call2", package = "call2" }
client = { path = "../client2", package = "client2" }
call = { path = "../call" }
client = { path = "../client" }
collections = { path = "../collections" }
# context_menu = { path = "../context_menu" }
fs = { path = "../fs2", package = "fs2" }
@ -54,8 +54,8 @@ smallvec.workspace = true
uuid.workspace = true
[dev-dependencies]
call = { path = "../call2", package = "call2", features = ["test-support"] }
client = { path = "../client2", package = "client2", features = ["test-support"] }
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings2", package = "settings2", features = ["test-support"] }

View File

@ -20,14 +20,14 @@ audio = { package = "audio2", path = "../audio2" }
activity_indicator = { path = "../activity_indicator"}
auto_update = { path = "../auto_update" }
breadcrumbs = { path = "../breadcrumbs" }
call = { package = "call2", path = "../call2" }
channel = { package = "channel2", path = "../channel2" }
call = { path = "../call" }
channel = { path = "../channel" }
cli = { path = "../cli" }
collab_ui = { path = "../collab_ui" }
collections = { path = "../collections" }
command_palette = { path = "../command_palette" }
# component_test = { path = "../component_test" }
client = { package = "client2", path = "../client2" }
client = { path = "../client" }
# clock = { path = "../clock" }
copilot = { path = "../copilot" }
copilot_button = { path = "../copilot_button" }
@ -144,7 +144,7 @@ urlencoding = "2.1.2"
uuid.workspace = true
[dev-dependencies]
call = { package = "call2", path = "../call2", features = ["test-support"] }
call = { path = "../call", features = ["test-support"] }
# client = { path = "../client", features = ["test-support"] }
# editor = { path = "../editor", features = ["test-support"] }
# gpui = { path = "../gpui", features = ["test-support"] }