mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 10:29:35 +03:00
Merge pull request #1700 from zed-industries/room
Introduce call-based collaboration
This commit is contained in:
commit
a656047c15
94
Cargo.lock
generated
94
Cargo.lock
generated
@ -684,6 +684,20 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
|
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "call"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"client",
|
||||||
|
"collections",
|
||||||
|
"futures",
|
||||||
|
"gpui",
|
||||||
|
"postage",
|
||||||
|
"project",
|
||||||
|
"util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cap-fs-ext"
|
name = "cap-fs-ext"
|
||||||
version = "0.24.4"
|
version = "0.24.4"
|
||||||
@ -1023,6 +1037,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"base64",
|
"base64",
|
||||||
|
"call",
|
||||||
"clap 3.2.8",
|
"clap 3.2.8",
|
||||||
"client",
|
"client",
|
||||||
"collections",
|
"collections",
|
||||||
@ -1067,6 +1082,31 @@ dependencies = [
|
|||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "collab_ui"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"call",
|
||||||
|
"client",
|
||||||
|
"clock",
|
||||||
|
"collections",
|
||||||
|
"editor",
|
||||||
|
"futures",
|
||||||
|
"fuzzy",
|
||||||
|
"gpui",
|
||||||
|
"log",
|
||||||
|
"menu",
|
||||||
|
"picker",
|
||||||
|
"postage",
|
||||||
|
"project",
|
||||||
|
"serde",
|
||||||
|
"settings",
|
||||||
|
"theme",
|
||||||
|
"util",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -1108,54 +1148,6 @@ dependencies = [
|
|||||||
"cache-padded",
|
"cache-padded",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "contacts_panel"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"client",
|
|
||||||
"collections",
|
|
||||||
"editor",
|
|
||||||
"futures",
|
|
||||||
"fuzzy",
|
|
||||||
"gpui",
|
|
||||||
"language",
|
|
||||||
"log",
|
|
||||||
"menu",
|
|
||||||
"picker",
|
|
||||||
"postage",
|
|
||||||
"project",
|
|
||||||
"serde",
|
|
||||||
"settings",
|
|
||||||
"theme",
|
|
||||||
"util",
|
|
||||||
"workspace",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "contacts_status_item"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"client",
|
|
||||||
"collections",
|
|
||||||
"editor",
|
|
||||||
"futures",
|
|
||||||
"fuzzy",
|
|
||||||
"gpui",
|
|
||||||
"language",
|
|
||||||
"log",
|
|
||||||
"menu",
|
|
||||||
"picker",
|
|
||||||
"postage",
|
|
||||||
"project",
|
|
||||||
"serde",
|
|
||||||
"settings",
|
|
||||||
"theme",
|
|
||||||
"util",
|
|
||||||
"workspace",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "context_menu"
|
name = "context_menu"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -7176,8 +7168,8 @@ name = "workspace"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"call",
|
||||||
"client",
|
"client",
|
||||||
"clock",
|
|
||||||
"collections",
|
"collections",
|
||||||
"context_menu",
|
"context_menu",
|
||||||
"drag_and_drop",
|
"drag_and_drop",
|
||||||
@ -7247,15 +7239,15 @@ dependencies = [
|
|||||||
"auto_update",
|
"auto_update",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"breadcrumbs",
|
"breadcrumbs",
|
||||||
|
"call",
|
||||||
"chat_panel",
|
"chat_panel",
|
||||||
"chrono",
|
"chrono",
|
||||||
"cli",
|
"cli",
|
||||||
"client",
|
"client",
|
||||||
"clock",
|
"clock",
|
||||||
|
"collab_ui",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette",
|
"command_palette",
|
||||||
"contacts_panel",
|
|
||||||
"contacts_status_item",
|
|
||||||
"context_menu",
|
"context_menu",
|
||||||
"ctor",
|
"ctor",
|
||||||
"diagnostics",
|
"diagnostics",
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 11C5 14.3137 7.68629 17 11 17C14.3137 17 17 14.3137 17 11C17 7.68629 14.3137 5 11 5C7.68629 5 5 7.68629 5 11ZM11 3C6.58172 3 3 6.58172 3 11C3 15.4183 6.58172 19 11 19C15.4183 19 19 15.4183 19 11C19 6.58172 15.4183 3 11 3Z" fill="white"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.09092 8.09088H14.6364L10.5511 12.4545H12.4546L13.9091 13.9091H7.36365L11.7273 9.54543H9.54547L8.09092 8.09088Z" fill="white"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 571 B |
@ -395,7 +395,6 @@
|
|||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"shift-escape": "dock::FocusDock",
|
"shift-escape": "dock::FocusDock",
|
||||||
"cmd-shift-c": "contacts_panel::ToggleFocus",
|
|
||||||
"cmd-shift-b": "workspace::ToggleRightSidebar"
|
"cmd-shift-b": "workspace::ToggleRightSidebar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
35
crates/call/Cargo.toml
Normal file
35
crates/call/Cargo.toml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
[package]
|
||||||
|
name = "call"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/call.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = [
|
||||||
|
"client/test-support",
|
||||||
|
"collections/test-support",
|
||||||
|
"gpui/test-support",
|
||||||
|
"project/test-support",
|
||||||
|
"util/test-support"
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
client = { path = "../client" }
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
project = { path = "../project" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
|
||||||
|
anyhow = "1.0.38"
|
||||||
|
futures = "0.3"
|
||||||
|
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
client = { path = "../client", features = ["test-support"] }
|
||||||
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
util = { path = "../util", features = ["test-support"] }
|
261
crates/call/src/call.rs
Normal file
261
crates/call/src/call.rs
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
mod participant;
|
||||||
|
pub mod room;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use client::{proto, Client, TypedEnvelope, User, UserStore};
|
||||||
|
use gpui::{
|
||||||
|
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||||
|
Subscription, Task,
|
||||||
|
};
|
||||||
|
pub use participant::ParticipantLocation;
|
||||||
|
use postage::watch;
|
||||||
|
use project::Project;
|
||||||
|
pub use room::Room;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) {
|
||||||
|
let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
|
||||||
|
cx.set_global(active_call);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct IncomingCall {
|
||||||
|
pub room_id: u64,
|
||||||
|
pub caller: Arc<User>,
|
||||||
|
pub participants: Vec<Arc<User>>,
|
||||||
|
pub initial_project: Option<proto::ParticipantProject>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ActiveCall {
|
||||||
|
room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
|
||||||
|
incoming_call: (
|
||||||
|
watch::Sender<Option<IncomingCall>>,
|
||||||
|
watch::Receiver<Option<IncomingCall>>,
|
||||||
|
),
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
_subscriptions: Vec<client::Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ActiveCall {
|
||||||
|
type Event = room::Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveCall {
|
||||||
|
fn new(
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
room: None,
|
||||||
|
incoming_call: watch::channel(),
|
||||||
|
_subscriptions: vec![
|
||||||
|
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
|
||||||
|
client.add_message_handler(cx.handle(), Self::handle_call_canceled),
|
||||||
|
],
|
||||||
|
client,
|
||||||
|
user_store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_incoming_call(
|
||||||
|
this: ModelHandle<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 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?,
|
||||||
|
caller: user_store
|
||||||
|
.update(&mut cx, |user_store, cx| {
|
||||||
|
user_store.get_user(envelope.payload.caller_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>,
|
||||||
|
_: TypedEnvelope<proto::CallCanceled>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
*this.incoming_call.0.borrow_mut() = None;
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn global(cx: &AppContext) -> ModelHandle<Self> {
|
||||||
|
cx.global::<ModelHandle<Self>>().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invite(
|
||||||
|
&mut self,
|
||||||
|
recipient_user_id: u64,
|
||||||
|
initial_project: Option<ModelHandle<Project>>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
let user_store = self.user_store.clone();
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) {
|
||||||
|
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||||
|
Some(
|
||||||
|
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
room.update(&mut cx, |room, cx| {
|
||||||
|
room.call(recipient_user_id, initial_project_id, cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
let room = cx
|
||||||
|
.update(|cx| {
|
||||||
|
Room::create(recipient_user_id, initial_project, client, user_store, cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
this.update(&mut cx, |this, cx| this.set_room(Some(room), cx));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel_invite(
|
||||||
|
&mut self,
|
||||||
|
recipient_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.foreground().spawn(async move {
|
||||||
|
client
|
||||||
|
.request(proto::CancelCall {
|
||||||
|
room_id,
|
||||||
|
recipient_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")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let room = join.await?;
|
||||||
|
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx));
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decline_incoming(&mut self) -> Result<()> {
|
||||||
|
let call = self
|
||||||
|
.incoming_call
|
||||||
|
.0
|
||||||
|
.borrow_mut()
|
||||||
|
.take()
|
||||||
|
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||||
|
self.client.send(proto::DeclineCall {
|
||||||
|
room_id: call.room_id,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||||
|
if let Some((room, _)) = self.room.take() {
|
||||||
|
room.update(cx, |room, cx| room.leave(cx))?;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share_project(
|
||||||
|
&mut self,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<u64>> {
|
||||||
|
if let Some((room, _)) = self.room.as_ref() {
|
||||||
|
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||||
|
} else {
|
||||||
|
Task::ready(Err(anyhow!("no active call")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_location(
|
||||||
|
&mut self,
|
||||||
|
project: Option<&ModelHandle<Project>>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
if let Some((room, _)) = self.room.as_ref() {
|
||||||
|
room.update(cx, |room, cx| room.set_location(project, cx))
|
||||||
|
} else {
|
||||||
|
Task::ready(Err(anyhow!("no active call")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_room(&mut self, room: Option<ModelHandle<Room>>, cx: &mut ModelContext<Self>) {
|
||||||
|
if room.as_ref() != self.room.as_ref().map(|room| &room.0) {
|
||||||
|
if let Some(room) = room {
|
||||||
|
if room.read(cx).status().is_offline() {
|
||||||
|
self.room = None;
|
||||||
|
} else {
|
||||||
|
let subscriptions = vec![
|
||||||
|
cx.observe(&room, |this, room, cx| {
|
||||||
|
if room.read(cx).status().is_offline() {
|
||||||
|
this.set_room(None, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
|
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
|
||||||
|
];
|
||||||
|
self.room = Some((room, subscriptions));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.room = None;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn room(&self) -> Option<&ModelHandle<Room>> {
|
||||||
|
self.room.as_ref().map(|(room, _)| room)
|
||||||
|
}
|
||||||
|
}
|
42
crates/call/src/participant.rs
Normal file
42
crates/call/src/participant.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use client::{proto, User};
|
||||||
|
use gpui::WeakModelHandle;
|
||||||
|
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<WeakModelHandle<Project>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct RemoteParticipant {
|
||||||
|
pub user: Arc<User>,
|
||||||
|
pub projects: Vec<proto::ParticipantProject>,
|
||||||
|
pub location: ParticipantLocation,
|
||||||
|
}
|
474
crates/call/src/room.rs
Normal file
474
crates/call/src/room.rs
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
use crate::{
|
||||||
|
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
|
||||||
|
IncomingCall,
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
||||||
|
use collections::{BTreeMap, HashSet};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
|
||||||
|
use project::Project;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Event {
|
||||||
|
RemoteProjectShared {
|
||||||
|
owner: Arc<User>,
|
||||||
|
project_id: u64,
|
||||||
|
worktree_root_names: Vec<String>,
|
||||||
|
},
|
||||||
|
RemoteProjectUnshared {
|
||||||
|
project_id: u64,
|
||||||
|
},
|
||||||
|
Left,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Room {
|
||||||
|
id: u64,
|
||||||
|
status: RoomStatus,
|
||||||
|
local_participant: LocalParticipant,
|
||||||
|
remote_participants: BTreeMap<PeerId, RemoteParticipant>,
|
||||||
|
pending_participants: Vec<Arc<User>>,
|
||||||
|
participant_user_ids: HashSet<u64>,
|
||||||
|
pending_call_count: usize,
|
||||||
|
leave_when_empty: bool,
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
subscriptions: Vec<client::Subscription>,
|
||||||
|
pending_room_update: Option<Task<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for Room {
|
||||||
|
type Event = Event;
|
||||||
|
|
||||||
|
fn release(&mut self, _: &mut MutableAppContext) {
|
||||||
|
self.client.send(proto::LeaveRoom { id: self.id }).log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Room {
|
||||||
|
fn new(
|
||||||
|
id: u64,
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let mut client_status = client.status();
|
||||||
|
cx.spawn_weak(|this, mut cx| async move {
|
||||||
|
let is_connected = client_status
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.map_or(false, |s| s.is_connected());
|
||||||
|
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
|
||||||
|
if !is_connected || client_status.next().await.is_some() {
|
||||||
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
|
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
status: RoomStatus::Online,
|
||||||
|
participant_user_ids: Default::default(),
|
||||||
|
local_participant: Default::default(),
|
||||||
|
remote_participants: Default::default(),
|
||||||
|
pending_participants: Default::default(),
|
||||||
|
pending_call_count: 0,
|
||||||
|
subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)],
|
||||||
|
leave_when_empty: false,
|
||||||
|
pending_room_update: None,
|
||||||
|
client,
|
||||||
|
user_store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create(
|
||||||
|
recipient_user_id: u64,
|
||||||
|
initial_project: Option<ModelHandle<Project>>,
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) -> Task<Result<ModelHandle<Self>>> {
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let response = client.request(proto::CreateRoom {}).await?;
|
||||||
|
let room = cx.add_model(|cx| Self::new(response.id, client, 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 {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
match room
|
||||||
|
.update(&mut cx, |room, cx| {
|
||||||
|
room.leave_when_empty = true;
|
||||||
|
room.call(recipient_user_id, initial_project_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => Ok(room),
|
||||||
|
Err(error) => Err(anyhow!("room creation failed: {:?}", error)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn join(
|
||||||
|
call: &IncomingCall,
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) -> Task<Result<ModelHandle<Self>>> {
|
||||||
|
let room_id = call.room_id;
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let response = client.request(proto::JoinRoom { id: room_id }).await?;
|
||||||
|
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||||
|
let room = cx.add_model(|cx| Self::new(room_id, client, user_store, cx));
|
||||||
|
room.update(&mut cx, |room, cx| {
|
||||||
|
room.leave_when_empty = true;
|
||||||
|
room.apply_room_update(room_proto, cx)?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
})?;
|
||||||
|
Ok(room)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_leave(&self) -> bool {
|
||||||
|
self.leave_when_empty
|
||||||
|
&& self.pending_room_update.is_none()
|
||||||
|
&& self.pending_participants.is_empty()
|
||||||
|
&& self.remote_participants.is_empty()
|
||||||
|
&& self.pending_call_count == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||||
|
if self.status.is_offline() {
|
||||||
|
return Err(anyhow!("room is offline"));
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
cx.emit(Event::Left);
|
||||||
|
self.status = RoomStatus::Offline;
|
||||||
|
self.remote_participants.clear();
|
||||||
|
self.pending_participants.clear();
|
||||||
|
self.participant_user_ids.clear();
|
||||||
|
self.subscriptions.clear();
|
||||||
|
self.client.send(proto::LeaveRoom { id: self.id })?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> u64 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status(&self) -> RoomStatus {
|
||||||
|
self.status
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local_participant(&self) -> &LocalParticipant {
|
||||||
|
&self.local_participant
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remote_participants(&self) -> &BTreeMap<PeerId, RemoteParticipant> {
|
||||||
|
&self.remote_participants
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pending_participants(&self) -> &[Arc<User>] {
|
||||||
|
&self.pending_participants
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains_participant(&self, user_id: u64) -> bool {
|
||||||
|
self.participant_user_ids.contains(&user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_room_updated(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::RoomUpdated>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let room = envelope
|
||||||
|
.payload
|
||||||
|
.room
|
||||||
|
.ok_or_else(|| anyhow!("invalid room"))?;
|
||||||
|
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_room_update(
|
||||||
|
&mut self,
|
||||||
|
mut room: proto::Room,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Filter ourselves out from the room's participants.
|
||||||
|
let local_participant_ix = room
|
||||||
|
.participants
|
||||||
|
.iter()
|
||||||
|
.position(|participant| Some(participant.user_id) == self.client.user_id());
|
||||||
|
let local_participant = local_participant_ix.map(|ix| room.participants.swap_remove(ix));
|
||||||
|
|
||||||
|
let remote_participant_user_ids = room
|
||||||
|
.participants
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.user_id)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let (remote_participants, pending_participants) =
|
||||||
|
self.user_store.update(cx, move |user_store, cx| {
|
||||||
|
(
|
||||||
|
user_store.get_users(remote_participant_user_ids, cx),
|
||||||
|
user_store.get_users(room.pending_participant_user_ids, cx),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
|
||||||
|
let (remote_participants, pending_participants) =
|
||||||
|
futures::join!(remote_participants, pending_participants);
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.participant_user_ids.clear();
|
||||||
|
|
||||||
|
if let Some(participant) = local_participant {
|
||||||
|
this.local_participant.projects = participant.projects;
|
||||||
|
} else {
|
||||||
|
this.local_participant.projects.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(participants) = remote_participants.log_err() {
|
||||||
|
for (participant, user) in room.participants.into_iter().zip(participants) {
|
||||||
|
let peer_id = PeerId(participant.peer_id);
|
||||||
|
this.participant_user_ids.insert(participant.user_id);
|
||||||
|
|
||||||
|
let old_projects = this
|
||||||
|
.remote_participants
|
||||||
|
.get(&peer_id)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|existing| &existing.projects)
|
||||||
|
.map(|project| project.id)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let new_projects = participant
|
||||||
|
.projects
|
||||||
|
.iter()
|
||||||
|
.map(|project| project.id)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
for project in &participant.projects {
|
||||||
|
if !old_projects.contains(&project.id) {
|
||||||
|
cx.emit(Event::RemoteProjectShared {
|
||||||
|
owner: user.clone(),
|
||||||
|
project_id: project.id,
|
||||||
|
worktree_root_names: project.worktree_root_names.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for unshared_project_id in old_projects.difference(&new_projects) {
|
||||||
|
cx.emit(Event::RemoteProjectUnshared {
|
||||||
|
project_id: *unshared_project_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.remote_participants.insert(
|
||||||
|
peer_id,
|
||||||
|
RemoteParticipant {
|
||||||
|
user: user.clone(),
|
||||||
|
projects: participant.projects,
|
||||||
|
location: ParticipantLocation::from_proto(participant.location)
|
||||||
|
.unwrap_or(ParticipantLocation::External),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.remote_participants.retain(|_, participant| {
|
||||||
|
if this.participant_user_ids.contains(&participant.user.id) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
for project in &participant.projects {
|
||||||
|
cx.emit(Event::RemoteProjectUnshared {
|
||||||
|
project_id: project.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pending_participants) = pending_participants.log_err() {
|
||||||
|
this.pending_participants = pending_participants;
|
||||||
|
for participant in &this.pending_participants {
|
||||||
|
this.participant_user_ids.insert(participant.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pending_room_update.take();
|
||||||
|
if this.should_leave() {
|
||||||
|
let _ = this.leave(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.check_invariants();
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_invariants(&self) {
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
{
|
||||||
|
for participant in self.remote_participants.values() {
|
||||||
|
assert!(self.participant_user_ids.contains(&participant.user.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
for participant in &self.pending_participants {
|
||||||
|
assert!(self.participant_user_ids.contains(&participant.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
self.participant_user_ids.len(),
|
||||||
|
self.remote_participants.len() + self.pending_participants.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn call(
|
||||||
|
&mut self,
|
||||||
|
recipient_user_id: u64,
|
||||||
|
initial_project_id: Option<u64>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
if self.status.is_offline() {
|
||||||
|
return Task::ready(Err(anyhow!("room is offline")));
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
let client = self.client.clone();
|
||||||
|
let room_id = self.id;
|
||||||
|
self.pending_call_count += 1;
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let result = client
|
||||||
|
.request(proto::Call {
|
||||||
|
room_id,
|
||||||
|
recipient_user_id,
|
||||||
|
initial_project_id,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.pending_call_count -= 1;
|
||||||
|
if this.should_leave() {
|
||||||
|
this.leave(cx)?;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn share_project(
|
||||||
|
&mut self,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<u64>> {
|
||||||
|
if project.read(cx).is_remote() {
|
||||||
|
return Task::ready(Err(anyhow!("can't share remote project")));
|
||||||
|
} else if let Some(project_id) = project.read(cx).remote_id() {
|
||||||
|
return Task::ready(Ok(project_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = self.client.request(proto::ShareProject {
|
||||||
|
room_id: self.id(),
|
||||||
|
worktrees: project
|
||||||
|
.read(cx)
|
||||||
|
.worktrees(cx)
|
||||||
|
.map(|worktree| {
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
proto::WorktreeMetadata {
|
||||||
|
id: worktree.id().to_proto(),
|
||||||
|
root_name: worktree.root_name().into(),
|
||||||
|
visible: worktree.is_visible(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let response = request.await?;
|
||||||
|
|
||||||
|
project
|
||||||
|
.update(&mut cx, |project, cx| {
|
||||||
|
project.shared(response.project_id, cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let active_project = this.local_participant.active_project.as_ref();
|
||||||
|
if active_project.map_or(false, |location| *location == project) {
|
||||||
|
this.set_location(Some(&project), cx)
|
||||||
|
} else {
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(response.project_id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_location(
|
||||||
|
&mut self,
|
||||||
|
project: Option<&ModelHandle<Project>>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
if self.status.is_offline() {
|
||||||
|
return Task::ready(Err(anyhow!("room is offline")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = self.client.clone();
|
||||||
|
let room_id = self.id;
|
||||||
|
let location = if let Some(project) = project {
|
||||||
|
self.local_participant.active_project = Some(project.downgrade());
|
||||||
|
if let Some(project_id) = project.read(cx).remote_id() {
|
||||||
|
proto::participant_location::Variant::SharedProject(
|
||||||
|
proto::participant_location::SharedProject { id: project_id },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
proto::participant_location::Variant::UnsharedProject(
|
||||||
|
proto::participant_location::UnsharedProject {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.local_participant.active_project = None;
|
||||||
|
proto::participant_location::Variant::External(proto::participant_location::External {})
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
cx.foreground().spawn(async move {
|
||||||
|
client
|
||||||
|
.request(proto::UpdateParticipantLocation {
|
||||||
|
room_id,
|
||||||
|
location: Some(proto::ParticipantLocation {
|
||||||
|
variant: Some(location),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum RoomStatus {
|
||||||
|
Online,
|
||||||
|
Offline,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoomStatus {
|
||||||
|
pub fn is_offline(&self) -> bool {
|
||||||
|
matches!(self, RoomStatus::Offline)
|
||||||
|
}
|
||||||
|
}
|
@ -530,7 +530,7 @@ impl ChannelMessage {
|
|||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let sender = user_store
|
let sender = user_store
|
||||||
.update(cx, |user_store, cx| {
|
.update(cx, |user_store, cx| {
|
||||||
user_store.fetch_user(message.sender_id, cx)
|
user_store.get_user(message.sender_id, cx)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
Ok(ChannelMessage {
|
Ok(ChannelMessage {
|
||||||
|
@ -434,6 +434,29 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_request_handler<M, E, H, F>(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
model: ModelHandle<E>,
|
||||||
|
handler: H,
|
||||||
|
) -> Subscription
|
||||||
|
where
|
||||||
|
M: RequestMessage,
|
||||||
|
E: Entity,
|
||||||
|
H: 'static
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||||
|
F: 'static + Future<Output = Result<M::Response>>,
|
||||||
|
{
|
||||||
|
self.add_message_handler(model, move |handle, envelope, this, cx| {
|
||||||
|
Self::respond_to_request(
|
||||||
|
envelope.receipt(),
|
||||||
|
handler(handle, envelope, this.clone(), cx),
|
||||||
|
this,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||||
where
|
where
|
||||||
M: EntityMessage,
|
M: EntityMessage,
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
|
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet};
|
use collections::{hash_map::Entry, HashMap, HashSet};
|
||||||
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
|
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
|
||||||
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
|
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
|
||||||
use postage::{prelude::Stream, sink::Sink, watch};
|
use postage::{sink::Sink, watch};
|
||||||
use rpc::proto::{RequestMessage, UsersResponse};
|
use rpc::proto::{RequestMessage, UsersResponse};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use util::TryFutureExt as _;
|
use util::TryFutureExt as _;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub github_login: String,
|
pub github_login: String,
|
||||||
@ -39,14 +39,7 @@ impl Eq for User {}
|
|||||||
pub struct Contact {
|
pub struct Contact {
|
||||||
pub user: Arc<User>,
|
pub user: Arc<User>,
|
||||||
pub online: bool,
|
pub online: bool,
|
||||||
pub projects: Vec<ProjectMetadata>,
|
pub busy: bool,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct ProjectMetadata {
|
|
||||||
pub id: u64,
|
|
||||||
pub visible_worktree_root_names: Vec<String>,
|
|
||||||
pub guests: BTreeSet<Arc<User>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@ -138,12 +131,12 @@ impl UserStore {
|
|||||||
}),
|
}),
|
||||||
_maintain_current_user: cx.spawn_weak(|this, mut cx| async move {
|
_maintain_current_user: cx.spawn_weak(|this, mut cx| async move {
|
||||||
let mut status = client.status();
|
let mut status = client.status();
|
||||||
while let Some(status) = status.recv().await {
|
while let Some(status) = status.next().await {
|
||||||
match status {
|
match status {
|
||||||
Status::Connected { .. } => {
|
Status::Connected { .. } => {
|
||||||
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
|
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
|
||||||
let fetch_user = this
|
let fetch_user = this
|
||||||
.update(&mut cx, |this, cx| this.fetch_user(user_id, cx))
|
.update(&mut cx, |this, cx| this.get_user(user_id, cx))
|
||||||
.log_err();
|
.log_err();
|
||||||
let fetch_metrics_id =
|
let fetch_metrics_id =
|
||||||
client.request(proto::GetPrivateUserInfo {}).log_err();
|
client.request(proto::GetPrivateUserInfo {}).log_err();
|
||||||
@ -244,7 +237,6 @@ impl UserStore {
|
|||||||
let mut user_ids = HashSet::default();
|
let mut user_ids = HashSet::default();
|
||||||
for contact in &message.contacts {
|
for contact in &message.contacts {
|
||||||
user_ids.insert(contact.user_id);
|
user_ids.insert(contact.user_id);
|
||||||
user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied());
|
|
||||||
}
|
}
|
||||||
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
|
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
|
||||||
user_ids.extend(message.outgoing_requests.iter());
|
user_ids.extend(message.outgoing_requests.iter());
|
||||||
@ -268,9 +260,7 @@ impl UserStore {
|
|||||||
for request in message.incoming_requests {
|
for request in message.incoming_requests {
|
||||||
incoming_requests.push({
|
incoming_requests.push({
|
||||||
let user = this
|
let user = this
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| this.get_user(request.requester_id, cx))
|
||||||
this.fetch_user(request.requester_id, cx)
|
|
||||||
})
|
|
||||||
.await?;
|
.await?;
|
||||||
(user, request.should_notify)
|
(user, request.should_notify)
|
||||||
});
|
});
|
||||||
@ -279,7 +269,7 @@ impl UserStore {
|
|||||||
let mut outgoing_requests = Vec::new();
|
let mut outgoing_requests = Vec::new();
|
||||||
for requested_user_id in message.outgoing_requests {
|
for requested_user_id in message.outgoing_requests {
|
||||||
outgoing_requests.push(
|
outgoing_requests.push(
|
||||||
this.update(&mut cx, |this, cx| this.fetch_user(requested_user_id, cx))
|
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))
|
||||||
.await?,
|
.await?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -504,7 +494,7 @@ impl UserStore {
|
|||||||
.unbounded_send(UpdateContacts::Clear(tx))
|
.unbounded_send(UpdateContacts::Clear(tx))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
async move {
|
async move {
|
||||||
rx.recv().await;
|
rx.next().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -514,25 +504,43 @@ impl UserStore {
|
|||||||
.unbounded_send(UpdateContacts::Wait(tx))
|
.unbounded_send(UpdateContacts::Wait(tx))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
async move {
|
async move {
|
||||||
rx.recv().await;
|
rx.next().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_users(
|
pub fn get_users(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut user_ids: Vec<u64>,
|
user_ids: Vec<u64>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<Vec<Arc<User>>>> {
|
||||||
user_ids.retain(|id| !self.users.contains_key(id));
|
let mut user_ids_to_fetch = user_ids.clone();
|
||||||
if user_ids.is_empty() {
|
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
|
||||||
Task::ready(Ok(()))
|
|
||||||
} else {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let load = self.load_users(proto::GetUsers { user_ids }, cx);
|
if !user_ids_to_fetch.is_empty() {
|
||||||
cx.foreground().spawn(async move {
|
this.update(&mut cx, |this, cx| {
|
||||||
load.await?;
|
this.load_users(
|
||||||
Ok(())
|
proto::GetUsers {
|
||||||
|
user_ids: user_ids_to_fetch,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.read_with(&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(
|
pub fn fuzzy_search_users(
|
||||||
@ -543,7 +551,7 @@ impl UserStore {
|
|||||||
self.load_users(proto::FuzzySearchUsers { query }, cx)
|
self.load_users(proto::FuzzySearchUsers { query }, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_user(
|
pub fn get_user(
|
||||||
&mut self,
|
&mut self,
|
||||||
user_id: u64,
|
user_id: u64,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
@ -623,39 +631,15 @@ impl Contact {
|
|||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let user = user_store
|
let user = user_store
|
||||||
.update(cx, |user_store, cx| {
|
.update(cx, |user_store, cx| {
|
||||||
user_store.fetch_user(contact.user_id, cx)
|
user_store.get_user(contact.user_id, cx)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let mut projects = Vec::new();
|
|
||||||
for project in contact.projects {
|
|
||||||
let mut guests = BTreeSet::new();
|
|
||||||
for participant_id in project.guests {
|
|
||||||
guests.insert(
|
|
||||||
user_store
|
|
||||||
.update(cx, |user_store, cx| {
|
|
||||||
user_store.fetch_user(participant_id, cx)
|
|
||||||
})
|
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
projects.push(ProjectMetadata {
|
|
||||||
id: project.id,
|
|
||||||
visible_worktree_root_names: project.visible_worktree_root_names.clone(),
|
|
||||||
guests,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
user,
|
user,
|
||||||
online: contact.online,
|
online: contact.online,
|
||||||
projects,
|
busy: contact.busy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn non_empty_projects(&self) -> impl Iterator<Item = &ProjectMetadata> {
|
|
||||||
self.projects
|
|
||||||
.iter()
|
|
||||||
.filter(|project| !project.visible_worktree_root_names.is_empty())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
||||||
|
@ -56,13 +56,14 @@ features = ["runtime-tokio-rustls", "postgres", "time", "uuid"]
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
call = { path = "../call", features = ["test-support"] }
|
||||||
client = { path = "../client", features = ["test-support"] }
|
client = { path = "../client", features = ["test-support"] }
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
|
@ -84,7 +84,23 @@ async fn main() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("failed to insert user"),
|
.expect("failed to insert user")
|
||||||
|
.user_id,
|
||||||
|
);
|
||||||
|
} else if admin {
|
||||||
|
zed_user_ids.push(
|
||||||
|
db.create_user(
|
||||||
|
&format!("{}@zed.dev", github_user.login),
|
||||||
|
admin,
|
||||||
|
db::NewUserParams {
|
||||||
|
github_login: github_user.login,
|
||||||
|
github_user_id: github_user.id,
|
||||||
|
invite_count: 5,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("failed to insert user")
|
||||||
|
.user_id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1098,10 +1098,7 @@ impl Db for PostgresDb {
|
|||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch(&self.pool);
|
.fetch(&self.pool);
|
||||||
|
|
||||||
let mut contacts = vec![Contact::Accepted {
|
let mut contacts = Vec::new();
|
||||||
user_id,
|
|
||||||
should_notify: false,
|
|
||||||
}];
|
|
||||||
while let Some(row) = rows.next().await {
|
while let Some(row) = rows.next().await {
|
||||||
let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?;
|
let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?;
|
||||||
|
|
||||||
@ -2080,10 +2077,7 @@ mod test {
|
|||||||
|
|
||||||
async fn get_contacts(&self, id: UserId) -> Result<Vec<Contact>> {
|
async fn get_contacts(&self, id: UserId) -> Result<Vec<Contact>> {
|
||||||
self.background.simulate_random_delay().await;
|
self.background.simulate_random_delay().await;
|
||||||
let mut contacts = vec![Contact::Accepted {
|
let mut contacts = Vec::new();
|
||||||
user_id: id,
|
|
||||||
should_notify: false,
|
|
||||||
}];
|
|
||||||
|
|
||||||
for contact in self.contacts.lock().iter() {
|
for contact in self.contacts.lock().iter() {
|
||||||
if contact.requester_id == id {
|
if contact.requester_id == id {
|
||||||
|
@ -666,13 +666,7 @@ async fn test_add_contacts() {
|
|||||||
let user_3 = user_ids[2];
|
let user_3 = user_ids[2];
|
||||||
|
|
||||||
// User starts with no contacts
|
// User starts with no contacts
|
||||||
assert_eq!(
|
assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
|
||||||
db.get_contacts(user_1).await.unwrap(),
|
|
||||||
vec![Contact::Accepted {
|
|
||||||
user_id: user_1,
|
|
||||||
should_notify: false
|
|
||||||
}],
|
|
||||||
);
|
|
||||||
|
|
||||||
// User requests a contact. Both users see the pending request.
|
// User requests a contact. Both users see the pending request.
|
||||||
db.send_contact_request(user_1, user_2).await.unwrap();
|
db.send_contact_request(user_1, user_2).await.unwrap();
|
||||||
@ -680,26 +674,14 @@ async fn test_add_contacts() {
|
|||||||
assert!(!db.has_contact(user_2, user_1).await.unwrap());
|
assert!(!db.has_contact(user_2, user_1).await.unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user_1).await.unwrap(),
|
db.get_contacts(user_1).await.unwrap(),
|
||||||
&[
|
&[Contact::Outgoing { user_id: user_2 }],
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user_1,
|
|
||||||
should_notify: false
|
|
||||||
},
|
|
||||||
Contact::Outgoing { user_id: user_2 }
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user_2).await.unwrap(),
|
db.get_contacts(user_2).await.unwrap(),
|
||||||
&[
|
&[Contact::Incoming {
|
||||||
Contact::Incoming {
|
user_id: user_1,
|
||||||
user_id: user_1,
|
should_notify: true
|
||||||
should_notify: true
|
}]
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user_2,
|
|
||||||
should_notify: false
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// User 2 dismisses the contact request notification without accepting or rejecting.
|
// User 2 dismisses the contact request notification without accepting or rejecting.
|
||||||
@ -712,16 +694,10 @@ async fn test_add_contacts() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user_2).await.unwrap(),
|
db.get_contacts(user_2).await.unwrap(),
|
||||||
&[
|
&[Contact::Incoming {
|
||||||
Contact::Incoming {
|
user_id: user_1,
|
||||||
user_id: user_1,
|
should_notify: false
|
||||||
should_notify: false
|
}]
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user_2,
|
|
||||||
should_notify: false
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// User can't accept their own contact request
|
// User can't accept their own contact request
|
||||||
@ -735,31 +711,19 @@ async fn test_add_contacts() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user_1).await.unwrap(),
|
db.get_contacts(user_1).await.unwrap(),
|
||||||
&[
|
&[Contact::Accepted {
|
||||||
Contact::Accepted {
|
user_id: user_2,
|
||||||
user_id: user_1,
|
should_notify: true
|
||||||
should_notify: false
|
}],
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user_2,
|
|
||||||
should_notify: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
assert!(db.has_contact(user_1, user_2).await.unwrap());
|
assert!(db.has_contact(user_1, user_2).await.unwrap());
|
||||||
assert!(db.has_contact(user_2, user_1).await.unwrap());
|
assert!(db.has_contact(user_2, user_1).await.unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user_2).await.unwrap(),
|
db.get_contacts(user_2).await.unwrap(),
|
||||||
&[
|
&[Contact::Accepted {
|
||||||
Contact::Accepted {
|
user_id: user_1,
|
||||||
user_id: user_1,
|
should_notify: false,
|
||||||
should_notify: false,
|
}]
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user_2,
|
|
||||||
should_notify: false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Users cannot re-request existing contacts.
|
// Users cannot re-request existing contacts.
|
||||||
@ -772,16 +736,10 @@ async fn test_add_contacts() {
|
|||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user_1).await.unwrap(),
|
db.get_contacts(user_1).await.unwrap(),
|
||||||
&[
|
&[Contact::Accepted {
|
||||||
Contact::Accepted {
|
user_id: user_2,
|
||||||
user_id: user_1,
|
should_notify: true,
|
||||||
should_notify: false
|
}]
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user_2,
|
|
||||||
should_notify: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Users can dismiss notifications of other users accepting their requests.
|
// Users can dismiss notifications of other users accepting their requests.
|
||||||
@ -790,16 +748,10 @@ async fn test_add_contacts() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user_1).await.unwrap(),
|
db.get_contacts(user_1).await.unwrap(),
|
||||||
&[
|
&[Contact::Accepted {
|
||||||
Contact::Accepted {
|
user_id: user_2,
|
||||||
user_id: user_1,
|
should_notify: false,
|
||||||
should_notify: false
|
}]
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user_2,
|
|
||||||
should_notify: false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Users send each other concurrent contact requests and
|
// Users send each other concurrent contact requests and
|
||||||
@ -809,10 +761,6 @@ async fn test_add_contacts() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user_1).await.unwrap(),
|
db.get_contacts(user_1).await.unwrap(),
|
||||||
&[
|
&[
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user_1,
|
|
||||||
should_notify: false
|
|
||||||
},
|
|
||||||
Contact::Accepted {
|
Contact::Accepted {
|
||||||
user_id: user_2,
|
user_id: user_2,
|
||||||
should_notify: false,
|
should_notify: false,
|
||||||
@ -820,21 +768,15 @@ async fn test_add_contacts() {
|
|||||||
Contact::Accepted {
|
Contact::Accepted {
|
||||||
user_id: user_3,
|
user_id: user_3,
|
||||||
should_notify: false
|
should_notify: false
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user_3).await.unwrap(),
|
db.get_contacts(user_3).await.unwrap(),
|
||||||
&[
|
&[Contact::Accepted {
|
||||||
Contact::Accepted {
|
user_id: user_1,
|
||||||
user_id: user_1,
|
should_notify: false
|
||||||
should_notify: false
|
}],
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user_3,
|
|
||||||
should_notify: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// User declines a contact request. Both users see that it is gone.
|
// User declines a contact request. Both users see that it is gone.
|
||||||
@ -846,29 +788,17 @@ async fn test_add_contacts() {
|
|||||||
assert!(!db.has_contact(user_3, user_2).await.unwrap());
|
assert!(!db.has_contact(user_3, user_2).await.unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user_2).await.unwrap(),
|
db.get_contacts(user_2).await.unwrap(),
|
||||||
&[
|
&[Contact::Accepted {
|
||||||
Contact::Accepted {
|
user_id: user_1,
|
||||||
user_id: user_1,
|
should_notify: false
|
||||||
should_notify: false
|
}]
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user_2,
|
|
||||||
should_notify: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user_3).await.unwrap(),
|
db.get_contacts(user_3).await.unwrap(),
|
||||||
&[
|
&[Contact::Accepted {
|
||||||
Contact::Accepted {
|
user_id: user_1,
|
||||||
user_id: user_1,
|
should_notify: false
|
||||||
should_notify: false
|
}],
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user_3,
|
|
||||||
should_notify: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -930,29 +860,17 @@ async fn test_invite_codes() {
|
|||||||
assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
|
assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user1).await.unwrap(),
|
db.get_contacts(user1).await.unwrap(),
|
||||||
[
|
[Contact::Accepted {
|
||||||
Contact::Accepted {
|
user_id: user2,
|
||||||
user_id: user1,
|
should_notify: true
|
||||||
should_notify: false
|
}]
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user2,
|
|
||||||
should_notify: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user2).await.unwrap(),
|
db.get_contacts(user2).await.unwrap(),
|
||||||
[
|
[Contact::Accepted {
|
||||||
Contact::Accepted {
|
user_id: user1,
|
||||||
user_id: user1,
|
should_notify: false
|
||||||
should_notify: false
|
}]
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user2,
|
|
||||||
should_notify: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
|
db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
|
||||||
@ -987,10 +905,6 @@ async fn test_invite_codes() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user1).await.unwrap(),
|
db.get_contacts(user1).await.unwrap(),
|
||||||
[
|
[
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user1,
|
|
||||||
should_notify: false
|
|
||||||
},
|
|
||||||
Contact::Accepted {
|
Contact::Accepted {
|
||||||
user_id: user2,
|
user_id: user2,
|
||||||
should_notify: true
|
should_notify: true
|
||||||
@ -1003,16 +917,10 @@ async fn test_invite_codes() {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user3).await.unwrap(),
|
db.get_contacts(user3).await.unwrap(),
|
||||||
[
|
[Contact::Accepted {
|
||||||
Contact::Accepted {
|
user_id: user1,
|
||||||
user_id: user1,
|
should_notify: false
|
||||||
should_notify: false
|
}]
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user3,
|
|
||||||
should_notify: false
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
|
db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
|
||||||
@ -1053,10 +961,6 @@ async fn test_invite_codes() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user1).await.unwrap(),
|
db.get_contacts(user1).await.unwrap(),
|
||||||
[
|
[
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user1,
|
|
||||||
should_notify: false
|
|
||||||
},
|
|
||||||
Contact::Accepted {
|
Contact::Accepted {
|
||||||
user_id: user2,
|
user_id: user2,
|
||||||
should_notify: true
|
should_notify: true
|
||||||
@ -1073,16 +977,10 @@ async fn test_invite_codes() {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_contacts(user4).await.unwrap(),
|
db.get_contacts(user4).await.unwrap(),
|
||||||
[
|
[Contact::Accepted {
|
||||||
Contact::Accepted {
|
user_id: user1,
|
||||||
user_id: user1,
|
should_notify: false
|
||||||
should_notify: false
|
}]
|
||||||
},
|
|
||||||
Contact::Accepted {
|
|
||||||
user_id: user4,
|
|
||||||
should_notify: false
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
|
db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,7 @@ use axum::{
|
|||||||
routing::get,
|
routing::get,
|
||||||
Extension, Router, TypedHeader,
|
Extension, Router, TypedHeader,
|
||||||
};
|
};
|
||||||
use collections::HashMap;
|
use collections::{HashMap, HashSet};
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::mpsc,
|
channel::mpsc,
|
||||||
future::{self, BoxFuture},
|
future::{self, BoxFuture},
|
||||||
@ -88,11 +88,6 @@ impl<R: RequestMessage> Response<R> {
|
|||||||
self.server.peer.respond(self.receipt, payload)?;
|
self.server.peer.respond(self.receipt, payload)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_receipt(self) -> Receipt<R> {
|
|
||||||
self.responded.store(true, SeqCst);
|
|
||||||
self.receipt
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
@ -151,11 +146,17 @@ impl Server {
|
|||||||
|
|
||||||
server
|
server
|
||||||
.add_request_handler(Server::ping)
|
.add_request_handler(Server::ping)
|
||||||
.add_request_handler(Server::register_project)
|
.add_request_handler(Server::create_room)
|
||||||
.add_request_handler(Server::unregister_project)
|
.add_request_handler(Server::join_room)
|
||||||
|
.add_message_handler(Server::leave_room)
|
||||||
|
.add_request_handler(Server::call)
|
||||||
|
.add_request_handler(Server::cancel_call)
|
||||||
|
.add_message_handler(Server::decline_call)
|
||||||
|
.add_request_handler(Server::update_participant_location)
|
||||||
|
.add_request_handler(Server::share_project)
|
||||||
|
.add_message_handler(Server::unshare_project)
|
||||||
.add_request_handler(Server::join_project)
|
.add_request_handler(Server::join_project)
|
||||||
.add_message_handler(Server::leave_project)
|
.add_message_handler(Server::leave_project)
|
||||||
.add_message_handler(Server::respond_to_join_project_request)
|
|
||||||
.add_message_handler(Server::update_project)
|
.add_message_handler(Server::update_project)
|
||||||
.add_message_handler(Server::register_project_activity)
|
.add_message_handler(Server::register_project_activity)
|
||||||
.add_request_handler(Server::update_worktree)
|
.add_request_handler(Server::update_worktree)
|
||||||
@ -385,7 +386,11 @@ impl Server {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let mut store = this.store().await;
|
let mut store = this.store().await;
|
||||||
store.add_connection(connection_id, user_id, user.admin);
|
let incoming_call = store.add_connection(connection_id, user_id, user.admin);
|
||||||
|
if let Some(incoming_call) = incoming_call {
|
||||||
|
this.peer.send(connection_id, incoming_call)?;
|
||||||
|
}
|
||||||
|
|
||||||
this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?;
|
this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?;
|
||||||
|
|
||||||
if let Some((code, count)) = invite_code {
|
if let Some((code, count)) = invite_code {
|
||||||
@ -468,69 +473,58 @@ impl Server {
|
|||||||
async fn sign_out(self: &mut Arc<Self>, connection_id: ConnectionId) -> Result<()> {
|
async fn sign_out(self: &mut Arc<Self>, connection_id: ConnectionId) -> Result<()> {
|
||||||
self.peer.disconnect(connection_id);
|
self.peer.disconnect(connection_id);
|
||||||
|
|
||||||
let mut projects_to_unregister = Vec::new();
|
let mut projects_to_unshare = Vec::new();
|
||||||
let removed_user_id;
|
let mut contacts_to_update = HashSet::default();
|
||||||
{
|
{
|
||||||
let mut store = self.store().await;
|
let mut store = self.store().await;
|
||||||
let removed_connection = store.remove_connection(connection_id)?;
|
let removed_connection = store.remove_connection(connection_id)?;
|
||||||
|
|
||||||
for (project_id, project) in removed_connection.hosted_projects {
|
for project in removed_connection.hosted_projects {
|
||||||
projects_to_unregister.push(project_id);
|
projects_to_unshare.push(project.id);
|
||||||
broadcast(connection_id, project.guests.keys().copied(), |conn_id| {
|
broadcast(connection_id, project.guests.keys().copied(), |conn_id| {
|
||||||
self.peer.send(
|
self.peer.send(
|
||||||
conn_id,
|
conn_id,
|
||||||
proto::UnregisterProject {
|
proto::UnshareProject {
|
||||||
project_id: project_id.to_proto(),
|
project_id: project.id.to_proto(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
for (_, receipts) in project.join_requests {
|
|
||||||
for receipt in receipts {
|
|
||||||
self.peer.respond(
|
|
||||||
receipt,
|
|
||||||
proto::JoinProjectResponse {
|
|
||||||
variant: Some(proto::join_project_response::Variant::Decline(
|
|
||||||
proto::join_project_response::Decline {
|
|
||||||
reason: proto::join_project_response::decline::Reason::WentOffline as i32
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for project_id in removed_connection.guest_project_ids {
|
for project in removed_connection.guest_projects {
|
||||||
if let Some(project) = store.project(project_id).trace_err() {
|
broadcast(connection_id, project.connection_ids, |conn_id| {
|
||||||
broadcast(connection_id, project.connection_ids(), |conn_id| {
|
self.peer.send(
|
||||||
self.peer.send(
|
conn_id,
|
||||||
conn_id,
|
proto::RemoveProjectCollaborator {
|
||||||
proto::RemoveProjectCollaborator {
|
project_id: project.id.to_proto(),
|
||||||
project_id: project_id.to_proto(),
|
peer_id: connection_id.0,
|
||||||
peer_id: connection_id.0,
|
},
|
||||||
},
|
)
|
||||||
)
|
});
|
||||||
});
|
|
||||||
if project.guests.is_empty() {
|
|
||||||
self.peer
|
|
||||||
.send(
|
|
||||||
project.host_connection_id,
|
|
||||||
proto::ProjectUnshared {
|
|
||||||
project_id: project_id.to_proto(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.trace_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removed_user_id = removed_connection.user_id;
|
for connection_id in removed_connection.canceled_call_connection_ids {
|
||||||
|
self.peer
|
||||||
|
.send(connection_id, proto::CallCanceled {})
|
||||||
|
.trace_err();
|
||||||
|
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(room) = removed_connection
|
||||||
|
.room_id
|
||||||
|
.and_then(|room_id| store.room(room_id))
|
||||||
|
{
|
||||||
|
self.room_updated(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
contacts_to_update.insert(removed_connection.user_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
self.update_user_contacts(removed_user_id).await.trace_err();
|
for user_id in contacts_to_update {
|
||||||
|
self.update_user_contacts(user_id).await.trace_err();
|
||||||
|
}
|
||||||
|
|
||||||
for project_id in projects_to_unregister {
|
for project_id in projects_to_unshare {
|
||||||
self.app_state
|
self.app_state
|
||||||
.db
|
.db
|
||||||
.unregister_project(project_id)
|
.unregister_project(project_id)
|
||||||
@ -598,76 +592,286 @@ impl Server {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn register_project(
|
async fn create_room(
|
||||||
self: Arc<Server>,
|
self: Arc<Server>,
|
||||||
request: TypedEnvelope<proto::RegisterProject>,
|
request: TypedEnvelope<proto::CreateRoom>,
|
||||||
response: Response<proto::RegisterProject>,
|
response: Response<proto::CreateRoom>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let user_id;
|
||||||
|
let room_id;
|
||||||
|
{
|
||||||
|
let mut store = self.store().await;
|
||||||
|
user_id = store.user_id_for_connection(request.sender_id)?;
|
||||||
|
room_id = store.create_room(request.sender_id)?;
|
||||||
|
}
|
||||||
|
response.send(proto::CreateRoomResponse { id: room_id })?;
|
||||||
|
self.update_user_contacts(user_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn join_room(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::JoinRoom>,
|
||||||
|
response: Response<proto::JoinRoom>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let user_id;
|
||||||
|
{
|
||||||
|
let mut store = self.store().await;
|
||||||
|
user_id = store.user_id_for_connection(request.sender_id)?;
|
||||||
|
let (room, recipient_connection_ids) =
|
||||||
|
store.join_room(request.payload.id, request.sender_id)?;
|
||||||
|
for recipient_id in recipient_connection_ids {
|
||||||
|
self.peer
|
||||||
|
.send(recipient_id, proto::CallCanceled {})
|
||||||
|
.trace_err();
|
||||||
|
}
|
||||||
|
response.send(proto::JoinRoomResponse {
|
||||||
|
room: Some(room.clone()),
|
||||||
|
})?;
|
||||||
|
self.room_updated(room);
|
||||||
|
}
|
||||||
|
self.update_user_contacts(user_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn leave_room(self: Arc<Server>, message: TypedEnvelope<proto::LeaveRoom>) -> Result<()> {
|
||||||
|
let mut contacts_to_update = HashSet::default();
|
||||||
|
{
|
||||||
|
let mut store = self.store().await;
|
||||||
|
let user_id = store.user_id_for_connection(message.sender_id)?;
|
||||||
|
let left_room = store.leave_room(message.payload.id, message.sender_id)?;
|
||||||
|
contacts_to_update.insert(user_id);
|
||||||
|
|
||||||
|
for project in left_room.unshared_projects {
|
||||||
|
for connection_id in project.connection_ids() {
|
||||||
|
self.peer.send(
|
||||||
|
connection_id,
|
||||||
|
proto::UnshareProject {
|
||||||
|
project_id: project.id.to_proto(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for project in left_room.left_projects {
|
||||||
|
if project.remove_collaborator {
|
||||||
|
for connection_id in project.connection_ids {
|
||||||
|
self.peer.send(
|
||||||
|
connection_id,
|
||||||
|
proto::RemoveProjectCollaborator {
|
||||||
|
project_id: project.id.to_proto(),
|
||||||
|
peer_id: message.sender_id.0,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.peer.send(
|
||||||
|
message.sender_id,
|
||||||
|
proto::UnshareProject {
|
||||||
|
project_id: project.id.to_proto(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(room) = left_room.room {
|
||||||
|
self.room_updated(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
for connection_id in left_room.canceled_call_connection_ids {
|
||||||
|
self.peer
|
||||||
|
.send(connection_id, proto::CallCanceled {})
|
||||||
|
.trace_err();
|
||||||
|
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for user_id in contacts_to_update {
|
||||||
|
self.update_user_contacts(user_id).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn call(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::Call>,
|
||||||
|
response: Response<proto::Call>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let caller_user_id = self
|
||||||
|
.store()
|
||||||
|
.await
|
||||||
|
.user_id_for_connection(request.sender_id)?;
|
||||||
|
let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id);
|
||||||
|
let initial_project_id = request
|
||||||
|
.payload
|
||||||
|
.initial_project_id
|
||||||
|
.map(ProjectId::from_proto);
|
||||||
|
if !self
|
||||||
|
.app_state
|
||||||
|
.db
|
||||||
|
.has_contact(caller_user_id, recipient_user_id)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Err(anyhow!("cannot call a user who isn't a contact"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let room_id = request.payload.room_id;
|
||||||
|
let mut calls = {
|
||||||
|
let mut store = self.store().await;
|
||||||
|
let (room, recipient_connection_ids, incoming_call) = store.call(
|
||||||
|
room_id,
|
||||||
|
recipient_user_id,
|
||||||
|
initial_project_id,
|
||||||
|
request.sender_id,
|
||||||
|
)?;
|
||||||
|
self.room_updated(room);
|
||||||
|
recipient_connection_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|recipient_connection_id| {
|
||||||
|
self.peer
|
||||||
|
.request(recipient_connection_id, incoming_call.clone())
|
||||||
|
})
|
||||||
|
.collect::<FuturesUnordered<_>>()
|
||||||
|
};
|
||||||
|
self.update_user_contacts(recipient_user_id).await?;
|
||||||
|
|
||||||
|
while let Some(call_response) = calls.next().await {
|
||||||
|
match call_response.as_ref() {
|
||||||
|
Ok(_) => {
|
||||||
|
response.send(proto::Ack {})?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
call_response.trace_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut store = self.store().await;
|
||||||
|
let room = store.call_failed(room_id, recipient_user_id)?;
|
||||||
|
self.room_updated(&room);
|
||||||
|
}
|
||||||
|
self.update_user_contacts(recipient_user_id).await?;
|
||||||
|
|
||||||
|
Err(anyhow!("failed to ring call recipient"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cancel_call(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::CancelCall>,
|
||||||
|
response: Response<proto::CancelCall>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id);
|
||||||
|
{
|
||||||
|
let mut store = self.store().await;
|
||||||
|
let (room, recipient_connection_ids) = store.cancel_call(
|
||||||
|
request.payload.room_id,
|
||||||
|
recipient_user_id,
|
||||||
|
request.sender_id,
|
||||||
|
)?;
|
||||||
|
for recipient_id in recipient_connection_ids {
|
||||||
|
self.peer
|
||||||
|
.send(recipient_id, proto::CallCanceled {})
|
||||||
|
.trace_err();
|
||||||
|
}
|
||||||
|
self.room_updated(room);
|
||||||
|
response.send(proto::Ack {})?;
|
||||||
|
}
|
||||||
|
self.update_user_contacts(recipient_user_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn decline_call(
|
||||||
|
self: Arc<Server>,
|
||||||
|
message: TypedEnvelope<proto::DeclineCall>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let recipient_user_id;
|
||||||
|
{
|
||||||
|
let mut store = self.store().await;
|
||||||
|
recipient_user_id = store.user_id_for_connection(message.sender_id)?;
|
||||||
|
let (room, recipient_connection_ids) =
|
||||||
|
store.decline_call(message.payload.room_id, message.sender_id)?;
|
||||||
|
for recipient_id in recipient_connection_ids {
|
||||||
|
self.peer
|
||||||
|
.send(recipient_id, proto::CallCanceled {})
|
||||||
|
.trace_err();
|
||||||
|
}
|
||||||
|
self.room_updated(room);
|
||||||
|
}
|
||||||
|
self.update_user_contacts(recipient_user_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_participant_location(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::UpdateParticipantLocation>,
|
||||||
|
response: Response<proto::UpdateParticipantLocation>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let room_id = request.payload.room_id;
|
||||||
|
let location = request
|
||||||
|
.payload
|
||||||
|
.location
|
||||||
|
.ok_or_else(|| anyhow!("invalid location"))?;
|
||||||
|
let mut store = self.store().await;
|
||||||
|
let room = store.update_participant_location(room_id, location, request.sender_id)?;
|
||||||
|
self.room_updated(room);
|
||||||
|
response.send(proto::Ack {})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn room_updated(&self, room: &proto::Room) {
|
||||||
|
for participant in &room.participants {
|
||||||
|
self.peer
|
||||||
|
.send(
|
||||||
|
ConnectionId(participant.peer_id),
|
||||||
|
proto::RoomUpdated {
|
||||||
|
room: Some(room.clone()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.trace_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn share_project(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::ShareProject>,
|
||||||
|
response: Response<proto::ShareProject>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let user_id = self
|
let user_id = self
|
||||||
.store()
|
.store()
|
||||||
.await
|
.await
|
||||||
.user_id_for_connection(request.sender_id)?;
|
.user_id_for_connection(request.sender_id)?;
|
||||||
let project_id = self.app_state.db.register_project(user_id).await?;
|
let project_id = self.app_state.db.register_project(user_id).await?;
|
||||||
self.store().await.register_project(
|
let mut store = self.store().await;
|
||||||
request.sender_id,
|
let room = store.share_project(
|
||||||
|
request.payload.room_id,
|
||||||
project_id,
|
project_id,
|
||||||
request.payload.online,
|
request.payload.worktrees,
|
||||||
|
request.sender_id,
|
||||||
)?;
|
)?;
|
||||||
|
response.send(proto::ShareProjectResponse {
|
||||||
response.send(proto::RegisterProjectResponse {
|
|
||||||
project_id: project_id.to_proto(),
|
project_id: project_id.to_proto(),
|
||||||
})?;
|
})?;
|
||||||
|
self.room_updated(room);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unregister_project(
|
async fn unshare_project(
|
||||||
self: Arc<Server>,
|
self: Arc<Server>,
|
||||||
request: TypedEnvelope<proto::UnregisterProject>,
|
message: TypedEnvelope<proto::UnshareProject>,
|
||||||
response: Response<proto::UnregisterProject>,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(message.payload.project_id);
|
||||||
let (user_id, project) = {
|
let mut store = self.store().await;
|
||||||
let mut state = self.store().await;
|
let (room, project) = store.unshare_project(project_id, message.sender_id)?;
|
||||||
let project = state.unregister_project(project_id, request.sender_id)?;
|
|
||||||
(state.user_id_for_connection(request.sender_id)?, project)
|
|
||||||
};
|
|
||||||
self.app_state.db.unregister_project(project_id).await?;
|
|
||||||
|
|
||||||
broadcast(
|
broadcast(
|
||||||
request.sender_id,
|
message.sender_id,
|
||||||
project.guests.keys().copied(),
|
project.guest_connection_ids(),
|
||||||
|conn_id| {
|
|conn_id| self.peer.send(conn_id, message.payload.clone()),
|
||||||
self.peer.send(
|
|
||||||
conn_id,
|
|
||||||
proto::UnregisterProject {
|
|
||||||
project_id: project_id.to_proto(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
for (_, receipts) in project.join_requests {
|
self.room_updated(room);
|
||||||
for receipt in receipts {
|
|
||||||
self.peer.respond(
|
|
||||||
receipt,
|
|
||||||
proto::JoinProjectResponse {
|
|
||||||
variant: Some(proto::join_project_response::Variant::Decline(
|
|
||||||
proto::join_project_response::Decline {
|
|
||||||
reason: proto::join_project_response::decline::Reason::Closed
|
|
||||||
as i32,
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send out the `UpdateContacts` message before responding to the unregister
|
|
||||||
// request. This way, when the project's host can keep track of the project's
|
|
||||||
// remote id until after they've received the `UpdateContacts` message for
|
|
||||||
// themself.
|
|
||||||
self.update_user_contacts(user_id).await?;
|
|
||||||
response.send(proto::Ack {})?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -721,176 +925,94 @@ impl Server {
|
|||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!(%project_id, %host_user_id, %host_connection_id, "join project");
|
tracing::info!(%project_id, %host_user_id, %host_connection_id, "join project");
|
||||||
let has_contact = self
|
|
||||||
.app_state
|
let mut store = self.store().await;
|
||||||
.db
|
let (project, replica_id) = store.join_project(request.sender_id, project_id)?;
|
||||||
.has_contact(guest_user_id, host_user_id)
|
let peer_count = project.guests.len();
|
||||||
.await?;
|
let mut collaborators = Vec::with_capacity(peer_count);
|
||||||
if !has_contact {
|
collaborators.push(proto::Collaborator {
|
||||||
return Err(anyhow!("no such project"))?;
|
peer_id: project.host_connection_id.0,
|
||||||
|
replica_id: 0,
|
||||||
|
user_id: project.host.user_id.to_proto(),
|
||||||
|
});
|
||||||
|
let worktrees = project
|
||||||
|
.worktrees
|
||||||
|
.iter()
|
||||||
|
.map(|(id, worktree)| proto::WorktreeMetadata {
|
||||||
|
id: *id,
|
||||||
|
root_name: worktree.root_name.clone(),
|
||||||
|
visible: worktree.visible,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Add all guests other than the requesting user's own connections as collaborators
|
||||||
|
for (guest_conn_id, guest) in &project.guests {
|
||||||
|
if request.sender_id != *guest_conn_id {
|
||||||
|
collaborators.push(proto::Collaborator {
|
||||||
|
peer_id: guest_conn_id.0,
|
||||||
|
replica_id: guest.replica_id as u32,
|
||||||
|
user_id: guest.user_id.to_proto(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.store().await.request_join_project(
|
for conn_id in project.connection_ids() {
|
||||||
guest_user_id,
|
if conn_id != request.sender_id {
|
||||||
project_id,
|
self.peer.send(
|
||||||
response.into_receipt(),
|
conn_id,
|
||||||
)?;
|
proto::AddProjectCollaborator {
|
||||||
self.peer.send(
|
project_id: project_id.to_proto(),
|
||||||
host_connection_id,
|
collaborator: Some(proto::Collaborator {
|
||||||
proto::RequestJoinProject {
|
peer_id: request.sender_id.0,
|
||||||
project_id: project_id.to_proto(),
|
replica_id: replica_id as u32,
|
||||||
requester_id: guest_user_id.to_proto(),
|
user_id: guest_user_id.to_proto(),
|
||||||
},
|
}),
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn respond_to_join_project_request(
|
|
||||||
self: Arc<Server>,
|
|
||||||
request: TypedEnvelope<proto::RespondToJoinProjectRequest>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let host_user_id;
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut state = self.store().await;
|
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
|
||||||
let project = state.project(project_id)?;
|
|
||||||
if project.host_connection_id != request.sender_id {
|
|
||||||
Err(anyhow!("no such connection"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
host_user_id = project.host.user_id;
|
|
||||||
let guest_user_id = UserId::from_proto(request.payload.requester_id);
|
|
||||||
|
|
||||||
if !request.payload.allow {
|
|
||||||
let receipts = state
|
|
||||||
.deny_join_project_request(request.sender_id, guest_user_id, project_id)
|
|
||||||
.ok_or_else(|| anyhow!("no such request"))?;
|
|
||||||
for receipt in receipts {
|
|
||||||
self.peer.respond(
|
|
||||||
receipt,
|
|
||||||
proto::JoinProjectResponse {
|
|
||||||
variant: Some(proto::join_project_response::Variant::Decline(
|
|
||||||
proto::join_project_response::Decline {
|
|
||||||
reason: proto::join_project_response::decline::Reason::Declined
|
|
||||||
as i32,
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let (receipts_with_replica_ids, project) = state
|
|
||||||
.accept_join_project_request(request.sender_id, guest_user_id, project_id)
|
|
||||||
.ok_or_else(|| anyhow!("no such request"))?;
|
|
||||||
|
|
||||||
let peer_count = project.guests.len();
|
|
||||||
let mut collaborators = Vec::with_capacity(peer_count);
|
|
||||||
collaborators.push(proto::Collaborator {
|
|
||||||
peer_id: project.host_connection_id.0,
|
|
||||||
replica_id: 0,
|
|
||||||
user_id: project.host.user_id.to_proto(),
|
|
||||||
});
|
|
||||||
let worktrees = project
|
|
||||||
.worktrees
|
|
||||||
.iter()
|
|
||||||
.map(|(id, worktree)| proto::WorktreeMetadata {
|
|
||||||
id: *id,
|
|
||||||
root_name: worktree.root_name.clone(),
|
|
||||||
visible: worktree.visible,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// Add all guests other than the requesting user's own connections as collaborators
|
|
||||||
for (guest_conn_id, guest) in &project.guests {
|
|
||||||
if receipts_with_replica_ids
|
|
||||||
.iter()
|
|
||||||
.all(|(receipt, _)| receipt.sender_id != *guest_conn_id)
|
|
||||||
{
|
|
||||||
collaborators.push(proto::Collaborator {
|
|
||||||
peer_id: guest_conn_id.0,
|
|
||||||
replica_id: guest.replica_id as u32,
|
|
||||||
user_id: guest.user_id.to_proto(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for conn_id in project.connection_ids() {
|
|
||||||
for (receipt, replica_id) in &receipts_with_replica_ids {
|
|
||||||
if conn_id != receipt.sender_id {
|
|
||||||
self.peer.send(
|
|
||||||
conn_id,
|
|
||||||
proto::AddProjectCollaborator {
|
|
||||||
project_id: project_id.to_proto(),
|
|
||||||
collaborator: Some(proto::Collaborator {
|
|
||||||
peer_id: receipt.sender_id.0,
|
|
||||||
replica_id: *replica_id as u32,
|
|
||||||
user_id: guest_user_id.to_proto(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, we send the metadata associated with each worktree.
|
|
||||||
for (receipt, replica_id) in &receipts_with_replica_ids {
|
|
||||||
self.peer.respond(
|
|
||||||
*receipt,
|
|
||||||
proto::JoinProjectResponse {
|
|
||||||
variant: Some(proto::join_project_response::Variant::Accept(
|
|
||||||
proto::join_project_response::Accept {
|
|
||||||
worktrees: worktrees.clone(),
|
|
||||||
replica_id: *replica_id as u32,
|
|
||||||
collaborators: collaborators.clone(),
|
|
||||||
language_servers: project.language_servers.clone(),
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (worktree_id, worktree) in &project.worktrees {
|
// First, we send the metadata associated with each worktree.
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
response.send(proto::JoinProjectResponse {
|
||||||
const MAX_CHUNK_SIZE: usize = 2;
|
worktrees: worktrees.clone(),
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
replica_id: replica_id as u32,
|
||||||
const MAX_CHUNK_SIZE: usize = 256;
|
collaborators: collaborators.clone(),
|
||||||
|
language_servers: project.language_servers.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
// Stream this worktree's entries.
|
for (worktree_id, worktree) in &project.worktrees {
|
||||||
let message = proto::UpdateWorktree {
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
project_id: project_id.to_proto(),
|
const MAX_CHUNK_SIZE: usize = 2;
|
||||||
worktree_id: *worktree_id,
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
root_name: worktree.root_name.clone(),
|
const MAX_CHUNK_SIZE: usize = 256;
|
||||||
updated_entries: worktree.entries.values().cloned().collect(),
|
|
||||||
removed_entries: Default::default(),
|
|
||||||
scan_id: worktree.scan_id,
|
|
||||||
is_last_update: worktree.is_complete,
|
|
||||||
};
|
|
||||||
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
|
|
||||||
for (receipt, _) in &receipts_with_replica_ids {
|
|
||||||
self.peer.send(receipt.sender_id, update.clone())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream this worktree's diagnostics.
|
// Stream this worktree's entries.
|
||||||
for summary in worktree.diagnostic_summaries.values() {
|
let message = proto::UpdateWorktree {
|
||||||
for (receipt, _) in &receipts_with_replica_ids {
|
project_id: project_id.to_proto(),
|
||||||
self.peer.send(
|
worktree_id: *worktree_id,
|
||||||
receipt.sender_id,
|
root_name: worktree.root_name.clone(),
|
||||||
proto::UpdateDiagnosticSummary {
|
updated_entries: worktree.entries.values().cloned().collect(),
|
||||||
project_id: project_id.to_proto(),
|
removed_entries: Default::default(),
|
||||||
worktree_id: *worktree_id,
|
scan_id: worktree.scan_id,
|
||||||
summary: Some(summary.clone()),
|
is_last_update: worktree.is_complete,
|
||||||
},
|
};
|
||||||
)?;
|
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
|
||||||
}
|
self.peer.send(request.sender_id, update.clone())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stream this worktree's diagnostics.
|
||||||
|
for summary in worktree.diagnostic_summaries.values() {
|
||||||
|
self.peer.send(
|
||||||
|
request.sender_id,
|
||||||
|
proto::UpdateDiagnosticSummary {
|
||||||
|
project_id: project_id.to_proto(),
|
||||||
|
worktree_id: *worktree_id,
|
||||||
|
summary: Some(summary.clone()),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_user_contacts(host_user_id).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -903,7 +1025,7 @@ impl Server {
|
|||||||
let project;
|
let project;
|
||||||
{
|
{
|
||||||
let mut store = self.store().await;
|
let mut store = self.store().await;
|
||||||
project = store.leave_project(sender_id, project_id)?;
|
project = store.leave_project(project_id, sender_id)?;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
%project_id,
|
%project_id,
|
||||||
host_user_id = %project.host_user_id,
|
host_user_id = %project.host_user_id,
|
||||||
@ -922,27 +1044,8 @@ impl Server {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(requester_id) = project.cancel_request {
|
|
||||||
self.peer.send(
|
|
||||||
project.host_connection_id,
|
|
||||||
proto::JoinProjectRequestCancelled {
|
|
||||||
project_id: project_id.to_proto(),
|
|
||||||
requester_id: requester_id.to_proto(),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if project.unshare {
|
|
||||||
self.peer.send(
|
|
||||||
project.host_connection_id,
|
|
||||||
proto::ProjectUnshared {
|
|
||||||
project_id: project_id.to_proto(),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.update_user_contacts(project.host_user_id).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -951,61 +1054,20 @@ impl Server {
|
|||||||
request: TypedEnvelope<proto::UpdateProject>,
|
request: TypedEnvelope<proto::UpdateProject>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||||
let user_id;
|
|
||||||
{
|
{
|
||||||
let mut state = self.store().await;
|
let mut state = self.store().await;
|
||||||
user_id = state.user_id_for_connection(request.sender_id)?;
|
|
||||||
let guest_connection_ids = state
|
let guest_connection_ids = state
|
||||||
.read_project(project_id, request.sender_id)?
|
.read_project(project_id, request.sender_id)?
|
||||||
.guest_connection_ids();
|
.guest_connection_ids();
|
||||||
let unshared_project = state.update_project(
|
let room =
|
||||||
project_id,
|
state.update_project(project_id, &request.payload.worktrees, request.sender_id)?;
|
||||||
&request.payload.worktrees,
|
broadcast(request.sender_id, guest_connection_ids, |connection_id| {
|
||||||
request.payload.online,
|
self.peer
|
||||||
request.sender_id,
|
.forward_send(request.sender_id, connection_id, request.payload.clone())
|
||||||
)?;
|
});
|
||||||
|
self.room_updated(room);
|
||||||
if let Some(unshared_project) = unshared_project {
|
|
||||||
broadcast(
|
|
||||||
request.sender_id,
|
|
||||||
unshared_project.guests.keys().copied(),
|
|
||||||
|conn_id| {
|
|
||||||
self.peer.send(
|
|
||||||
conn_id,
|
|
||||||
proto::UnregisterProject {
|
|
||||||
project_id: project_id.to_proto(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
for (_, receipts) in unshared_project.pending_join_requests {
|
|
||||||
for receipt in receipts {
|
|
||||||
self.peer.respond(
|
|
||||||
receipt,
|
|
||||||
proto::JoinProjectResponse {
|
|
||||||
variant: Some(proto::join_project_response::Variant::Decline(
|
|
||||||
proto::join_project_response::Decline {
|
|
||||||
reason:
|
|
||||||
proto::join_project_response::decline::Reason::Closed
|
|
||||||
as i32,
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
broadcast(request.sender_id, guest_connection_ids, |connection_id| {
|
|
||||||
self.peer.forward_send(
|
|
||||||
request.sender_id,
|
|
||||||
connection_id,
|
|
||||||
request.payload.clone(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.update_user_contacts(user_id).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1027,32 +1089,21 @@ impl Server {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||||
let worktree_id = request.payload.worktree_id;
|
let worktree_id = request.payload.worktree_id;
|
||||||
let (connection_ids, metadata_changed) = {
|
let connection_ids = self.store().await.update_worktree(
|
||||||
let mut store = self.store().await;
|
request.sender_id,
|
||||||
let (connection_ids, metadata_changed) = store.update_worktree(
|
project_id,
|
||||||
request.sender_id,
|
worktree_id,
|
||||||
project_id,
|
&request.payload.root_name,
|
||||||
worktree_id,
|
&request.payload.removed_entries,
|
||||||
&request.payload.root_name,
|
&request.payload.updated_entries,
|
||||||
&request.payload.removed_entries,
|
request.payload.scan_id,
|
||||||
&request.payload.updated_entries,
|
request.payload.is_last_update,
|
||||||
request.payload.scan_id,
|
)?;
|
||||||
request.payload.is_last_update,
|
|
||||||
)?;
|
|
||||||
(connection_ids, metadata_changed)
|
|
||||||
};
|
|
||||||
|
|
||||||
broadcast(request.sender_id, connection_ids, |connection_id| {
|
broadcast(request.sender_id, connection_ids, |connection_id| {
|
||||||
self.peer
|
self.peer
|
||||||
.forward_send(request.sender_id, connection_id, request.payload.clone())
|
.forward_send(request.sender_id, connection_id, request.payload.clone())
|
||||||
});
|
});
|
||||||
if metadata_changed {
|
|
||||||
let user_id = self
|
|
||||||
.store()
|
|
||||||
.await
|
|
||||||
.user_id_for_connection(request.sender_id)?;
|
|
||||||
self.update_user_contacts(user_id).await?;
|
|
||||||
}
|
|
||||||
response.send(proto::Ack {})?;
|
response.send(proto::Ack {})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
53
crates/collab_ui/Cargo.toml
Normal file
53
crates/collab_ui/Cargo.toml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
[package]
|
||||||
|
name = "collab_ui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/collab_ui.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = [
|
||||||
|
"call/test-support",
|
||||||
|
"client/test-support",
|
||||||
|
"collections/test-support",
|
||||||
|
"editor/test-support",
|
||||||
|
"gpui/test-support",
|
||||||
|
"project/test-support",
|
||||||
|
"settings/test-support",
|
||||||
|
"util/test-support",
|
||||||
|
"workspace/test-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
call = { path = "../call" }
|
||||||
|
client = { path = "../client" }
|
||||||
|
clock = { path = "../clock" }
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
editor = { path = "../editor" }
|
||||||
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
menu = { path = "../menu" }
|
||||||
|
picker = { path = "../picker" }
|
||||||
|
project = { path = "../project" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
workspace = { path = "../workspace" }
|
||||||
|
anyhow = "1.0"
|
||||||
|
futures = "0.3"
|
||||||
|
log = "0.4"
|
||||||
|
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||||
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
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 = { path = "../gpui", features = ["test-support"] }
|
||||||
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
|
util = { path = "../util", features = ["test-support"] }
|
||||||
|
workspace = { path = "../workspace", features = ["test-support"] }
|
566
crates/collab_ui/src/collab_titlebar_item.rs
Normal file
566
crates/collab_ui/src/collab_titlebar_item.rs
Normal file
@ -0,0 +1,566 @@
|
|||||||
|
use crate::{contact_notification::ContactNotification, contacts_popover};
|
||||||
|
use call::{ActiveCall, ParticipantLocation};
|
||||||
|
use client::{Authenticate, ContactEventKind, PeerId, User, UserStore};
|
||||||
|
use clock::ReplicaId;
|
||||||
|
use contacts_popover::ContactsPopover;
|
||||||
|
use gpui::{
|
||||||
|
actions,
|
||||||
|
color::Color,
|
||||||
|
elements::*,
|
||||||
|
geometry::{rect::RectF, vector::vec2f, PathBuilder},
|
||||||
|
json::{self, ToJson},
|
||||||
|
Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
|
||||||
|
Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
|
};
|
||||||
|
use settings::Settings;
|
||||||
|
use std::ops::Range;
|
||||||
|
use theme::Theme;
|
||||||
|
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
contacts_titlebar_item,
|
||||||
|
[ToggleContactsPopover, ShareProject]
|
||||||
|
);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
|
||||||
|
cx.add_action(CollabTitlebarItem::share_project);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CollabTitlebarItem {
|
||||||
|
workspace: WeakViewHandle<Workspace>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
contacts_popover: Option<ViewHandle<ContactsPopover>>,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for CollabTitlebarItem {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for CollabTitlebarItem {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"CollabTitlebarItem"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
|
workspace
|
||||||
|
} else {
|
||||||
|
return Empty::new().boxed();
|
||||||
|
};
|
||||||
|
|
||||||
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
|
let project = workspace.read(cx).project().read(cx);
|
||||||
|
|
||||||
|
let mut container = Flex::row();
|
||||||
|
if workspace.read(cx).client().status().borrow().is_connected() {
|
||||||
|
if project.is_shared()
|
||||||
|
|| project.is_remote()
|
||||||
|
|| ActiveCall::global(cx).read(cx).room().is_none()
|
||||||
|
{
|
||||||
|
container.add_child(self.render_toggle_contacts_button(&theme, cx));
|
||||||
|
} else {
|
||||||
|
container.add_child(self.render_share_button(&theme, cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.add_children(self.render_collaborators(&workspace, &theme, cx));
|
||||||
|
container.add_children(self.render_current_user(&workspace, &theme, cx));
|
||||||
|
container.add_children(self.render_connection_status(&workspace, cx));
|
||||||
|
container.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CollabTitlebarItem {
|
||||||
|
pub fn new(
|
||||||
|
workspace: &ViewHandle<Workspace>,
|
||||||
|
user_store: &ModelHandle<UserStore>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let active_call = ActiveCall::global(cx);
|
||||||
|
let mut subscriptions = Vec::new();
|
||||||
|
subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
|
||||||
|
subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify()));
|
||||||
|
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
|
||||||
|
this.window_activation_changed(active, cx)
|
||||||
|
}));
|
||||||
|
subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify()));
|
||||||
|
subscriptions.push(
|
||||||
|
cx.subscribe(user_store, move |this, user_store, event, cx| {
|
||||||
|
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
if let client::Event::Contact { user, kind } = event {
|
||||||
|
if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
|
||||||
|
workspace.show_notification(user.id as usize, cx, |cx| {
|
||||||
|
cx.add_view(|cx| {
|
||||||
|
ContactNotification::new(
|
||||||
|
user.clone(),
|
||||||
|
*kind,
|
||||||
|
user_store,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
workspace: workspace.downgrade(),
|
||||||
|
user_store: user_store.clone(),
|
||||||
|
contacts_popover: None,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
let workspace = self.workspace.upgrade(cx);
|
||||||
|
let room = ActiveCall::global(cx).read(cx).room().cloned();
|
||||||
|
if let Some((workspace, room)) = workspace.zip(room) {
|
||||||
|
let workspace = workspace.read(cx);
|
||||||
|
let project = if active {
|
||||||
|
Some(workspace.project().clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
room.update(cx, |room, cx| {
|
||||||
|
room.set_location(project.as_ref(), cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
|
let active_call = ActiveCall::global(cx);
|
||||||
|
let project = workspace.read(cx).project().clone();
|
||||||
|
active_call
|
||||||
|
.update(cx, |call, cx| call.share_project(project, cx))
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext<Self>) {
|
||||||
|
match self.contacts_popover.take() {
|
||||||
|
Some(_) => {}
|
||||||
|
None => {
|
||||||
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
|
let project = workspace.read(cx).project().clone();
|
||||||
|
let user_store = workspace.read(cx).user_store().clone();
|
||||||
|
let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
|
||||||
|
cx.focus(&view);
|
||||||
|
cx.subscribe(&view, |this, _, event, cx| {
|
||||||
|
match event {
|
||||||
|
contacts_popover::Event::Dismissed => {
|
||||||
|
this.contacts_popover = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
self.contacts_popover = Some(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_toggle_contacts_button(
|
||||||
|
&self,
|
||||||
|
theme: &Theme,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> ElementBox {
|
||||||
|
let titlebar = &theme.workspace.titlebar;
|
||||||
|
let badge = if self
|
||||||
|
.user_store
|
||||||
|
.read(cx)
|
||||||
|
.incoming_contact_requests()
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
Empty::new()
|
||||||
|
.collapsed()
|
||||||
|
.contained()
|
||||||
|
.with_style(titlebar.toggle_contacts_badge)
|
||||||
|
.contained()
|
||||||
|
.with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
|
||||||
|
.with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Stack::new()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::<ToggleContactsPopover>::new(0, cx, |state, _| {
|
||||||
|
let style = titlebar
|
||||||
|
.toggle_contacts_button
|
||||||
|
.style_for(state, self.contacts_popover.is_some());
|
||||||
|
Svg::new("icons/plus_8.svg")
|
||||||
|
.with_color(style.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.button_width)
|
||||||
|
.with_height(style.button_width)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, cx| {
|
||||||
|
cx.dispatch_action(ToggleContactsPopover);
|
||||||
|
})
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_children(badge)
|
||||||
|
.with_children(self.contacts_popover.as_ref().map(|popover| {
|
||||||
|
Overlay::new(
|
||||||
|
ChildView::new(popover)
|
||||||
|
.contained()
|
||||||
|
.with_margin_top(titlebar.height)
|
||||||
|
.with_margin_left(titlebar.toggle_contacts_button.default.button_width)
|
||||||
|
.with_margin_right(-titlebar.toggle_contacts_button.default.button_width)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||||
|
.with_anchor_corner(AnchorCorner::BottomLeft)
|
||||||
|
.boxed()
|
||||||
|
}))
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
enum Share {}
|
||||||
|
|
||||||
|
let titlebar = &theme.workspace.titlebar;
|
||||||
|
MouseEventHandler::<Share>::new(0, cx, |state, _| {
|
||||||
|
let style = titlebar.share_button.style_for(state, false);
|
||||||
|
Label::new("Share".into(), style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(ShareProject))
|
||||||
|
.with_tooltip::<Share, _>(
|
||||||
|
0,
|
||||||
|
"Share project with call participants".into(),
|
||||||
|
None,
|
||||||
|
theme.tooltip.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.aligned()
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_collaborators(
|
||||||
|
&self,
|
||||||
|
workspace: &ViewHandle<Workspace>,
|
||||||
|
theme: &Theme,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> Vec<ElementBox> {
|
||||||
|
let active_call = ActiveCall::global(cx);
|
||||||
|
if let Some(room) = active_call.read(cx).room().cloned() {
|
||||||
|
let project = workspace.read(cx).project().read(cx);
|
||||||
|
let mut participants = room
|
||||||
|
.read(cx)
|
||||||
|
.remote_participants()
|
||||||
|
.iter()
|
||||||
|
.map(|(peer_id, collaborator)| (*peer_id, collaborator.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
participants
|
||||||
|
.sort_by_key(|(peer_id, _)| Some(project.collaborators().get(peer_id)?.replica_id));
|
||||||
|
participants
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(peer_id, participant)| {
|
||||||
|
let project = workspace.read(cx).project().read(cx);
|
||||||
|
let replica_id = project
|
||||||
|
.collaborators()
|
||||||
|
.get(&peer_id)
|
||||||
|
.map(|collaborator| collaborator.replica_id);
|
||||||
|
let user = participant.user.clone();
|
||||||
|
Some(self.render_avatar(
|
||||||
|
&user,
|
||||||
|
replica_id,
|
||||||
|
Some((peer_id, &user.github_login, participant.location)),
|
||||||
|
workspace,
|
||||||
|
theme,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_current_user(
|
||||||
|
&self,
|
||||||
|
workspace: &ViewHandle<Workspace>,
|
||||||
|
theme: &Theme,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> Option<ElementBox> {
|
||||||
|
let user = workspace.read(cx).user_store().read(cx).current_user();
|
||||||
|
let replica_id = workspace.read(cx).project().read(cx).replica_id();
|
||||||
|
let status = *workspace.read(cx).client().status().borrow();
|
||||||
|
if let Some(user) = user {
|
||||||
|
Some(self.render_avatar(&user, Some(replica_id), None, workspace, theme, cx))
|
||||||
|
} else if matches!(status, client::Status::UpgradeRequired) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
|
||||||
|
let style = theme
|
||||||
|
.workspace
|
||||||
|
.titlebar
|
||||||
|
.sign_in_prompt
|
||||||
|
.style_for(state, false);
|
||||||
|
Label::new("Sign in".to_string(), style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_avatar(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
replica_id: Option<ReplicaId>,
|
||||||
|
peer: Option<(PeerId, &str, ParticipantLocation)>,
|
||||||
|
workspace: &ViewHandle<Workspace>,
|
||||||
|
theme: &Theme,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> ElementBox {
|
||||||
|
let is_followed = peer.map_or(false, |(peer_id, _, _)| {
|
||||||
|
workspace.read(cx).is_following(peer_id)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut avatar_style;
|
||||||
|
if let Some((_, _, location)) = peer.as_ref() {
|
||||||
|
if let ParticipantLocation::SharedProject { project_id } = *location {
|
||||||
|
if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() {
|
||||||
|
avatar_style = theme.workspace.titlebar.avatar;
|
||||||
|
} else {
|
||||||
|
avatar_style = theme.workspace.titlebar.inactive_avatar;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
avatar_style = theme.workspace.titlebar.inactive_avatar;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
avatar_style = theme.workspace.titlebar.avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut replica_color = None;
|
||||||
|
if let Some(replica_id) = replica_id {
|
||||||
|
let color = theme.editor.replica_selection_style(replica_id).cursor;
|
||||||
|
replica_color = Some(color);
|
||||||
|
if is_followed {
|
||||||
|
avatar_style.border = Border::all(1.0, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = Stack::new()
|
||||||
|
.with_children(user.avatar.as_ref().map(|avatar| {
|
||||||
|
Image::new(avatar.clone())
|
||||||
|
.with_style(avatar_style)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.workspace.titlebar.avatar_width)
|
||||||
|
.aligned()
|
||||||
|
.boxed()
|
||||||
|
}))
|
||||||
|
.with_children(replica_color.map(|replica_color| {
|
||||||
|
AvatarRibbon::new(replica_color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
|
||||||
|
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
|
||||||
|
.aligned()
|
||||||
|
.bottom()
|
||||||
|
.boxed()
|
||||||
|
}))
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.workspace.titlebar.avatar_width)
|
||||||
|
.contained()
|
||||||
|
.with_margin_left(theme.workspace.titlebar.avatar_margin)
|
||||||
|
.boxed();
|
||||||
|
|
||||||
|
if let Some((peer_id, peer_github_login, location)) = peer {
|
||||||
|
if let Some(replica_id) = replica_id {
|
||||||
|
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.dispatch_action(ToggleFollow(peer_id))
|
||||||
|
})
|
||||||
|
.with_tooltip::<ToggleFollow, _>(
|
||||||
|
peer_id.0 as usize,
|
||||||
|
if is_followed {
|
||||||
|
format!("Unfollow {}", peer_github_login)
|
||||||
|
} else {
|
||||||
|
format!("Follow {}", peer_github_login)
|
||||||
|
},
|
||||||
|
Some(Box::new(FollowNextCollaborator)),
|
||||||
|
theme.tooltip.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
} else if let ParticipantLocation::SharedProject { project_id } = location {
|
||||||
|
let user_id = user.id;
|
||||||
|
MouseEventHandler::<JoinProject>::new(peer_id.0 as usize, cx, move |_, _| content)
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.dispatch_action(JoinProject {
|
||||||
|
project_id,
|
||||||
|
follow_user_id: user_id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.with_tooltip::<JoinProject, _>(
|
||||||
|
peer_id.0 as usize,
|
||||||
|
format!("Follow {} into external project", peer_github_login),
|
||||||
|
Some(Box::new(FollowNextCollaborator)),
|
||||||
|
theme.tooltip.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_connection_status(
|
||||||
|
&self,
|
||||||
|
workspace: &ViewHandle<Workspace>,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> Option<ElementBox> {
|
||||||
|
let theme = &cx.global::<Settings>().theme;
|
||||||
|
match &*workspace.read(cx).client().status().borrow() {
|
||||||
|
client::Status::ConnectionError
|
||||||
|
| client::Status::ConnectionLost
|
||||||
|
| client::Status::Reauthenticating { .. }
|
||||||
|
| client::Status::Reconnecting { .. }
|
||||||
|
| client::Status::ReconnectionError { .. } => Some(
|
||||||
|
Container::new(
|
||||||
|
Align::new(
|
||||||
|
ConstrainedBox::new(
|
||||||
|
Svg::new("icons/cloud_slash_12.svg")
|
||||||
|
.with_color(theme.workspace.titlebar.offline_icon.color)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_width(theme.workspace.titlebar.offline_icon.width)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_style(theme.workspace.titlebar.offline_icon.container)
|
||||||
|
.boxed(),
|
||||||
|
),
|
||||||
|
client::Status::UpgradeRequired => Some(
|
||||||
|
Label::new(
|
||||||
|
"Please update Zed to collaborate".to_string(),
|
||||||
|
theme.workspace.titlebar.outdated_warning.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.workspace.titlebar.outdated_warning.container)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AvatarRibbon {
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AvatarRibbon {
|
||||||
|
pub fn new(color: Color) -> AvatarRibbon {
|
||||||
|
AvatarRibbon { color }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for AvatarRibbon {
|
||||||
|
type LayoutState = ();
|
||||||
|
|
||||||
|
type PaintState = ();
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
constraint: gpui::SizeConstraint,
|
||||||
|
_: &mut gpui::LayoutContext,
|
||||||
|
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
|
||||||
|
(constraint.max, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
bounds: gpui::geometry::rect::RectF,
|
||||||
|
_: gpui::geometry::rect::RectF,
|
||||||
|
_: &mut Self::LayoutState,
|
||||||
|
cx: &mut gpui::PaintContext,
|
||||||
|
) -> Self::PaintState {
|
||||||
|
let mut path = PathBuilder::new();
|
||||||
|
path.reset(bounds.lower_left());
|
||||||
|
path.curve_to(
|
||||||
|
bounds.origin() + vec2f(bounds.height(), 0.),
|
||||||
|
bounds.origin(),
|
||||||
|
);
|
||||||
|
path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
|
||||||
|
path.curve_to(bounds.lower_right(), bounds.upper_right());
|
||||||
|
path.line_to(bounds.lower_left());
|
||||||
|
cx.scene.push_path(path.build(self.color, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_event(
|
||||||
|
&mut self,
|
||||||
|
_: &gpui::Event,
|
||||||
|
_: RectF,
|
||||||
|
_: RectF,
|
||||||
|
_: &mut Self::LayoutState,
|
||||||
|
_: &mut Self::PaintState,
|
||||||
|
_: &mut gpui::EventContext,
|
||||||
|
) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rect_for_text_range(
|
||||||
|
&self,
|
||||||
|
_: Range<usize>,
|
||||||
|
_: RectF,
|
||||||
|
_: RectF,
|
||||||
|
_: &Self::LayoutState,
|
||||||
|
_: &Self::PaintState,
|
||||||
|
_: &gpui::MeasurementContext,
|
||||||
|
) -> Option<RectF> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug(
|
||||||
|
&self,
|
||||||
|
bounds: gpui::geometry::rect::RectF,
|
||||||
|
_: &Self::LayoutState,
|
||||||
|
_: &Self::PaintState,
|
||||||
|
_: &gpui::DebugContext,
|
||||||
|
) -> gpui::json::Value {
|
||||||
|
json::json!({
|
||||||
|
"type": "AvatarRibbon",
|
||||||
|
"bounds": bounds.to_json(),
|
||||||
|
"color": self.color.to_json(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
97
crates/collab_ui/src/collab_ui.rs
Normal file
97
crates/collab_ui/src/collab_ui.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
mod collab_titlebar_item;
|
||||||
|
mod contact_finder;
|
||||||
|
mod contact_list;
|
||||||
|
mod contact_notification;
|
||||||
|
mod contacts_popover;
|
||||||
|
mod incoming_call_notification;
|
||||||
|
mod notifications;
|
||||||
|
mod project_shared_notification;
|
||||||
|
|
||||||
|
use call::ActiveCall;
|
||||||
|
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||||
|
use gpui::MutableAppContext;
|
||||||
|
use project::Project;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
|
||||||
|
|
||||||
|
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
|
collab_titlebar_item::init(cx);
|
||||||
|
contact_notification::init(cx);
|
||||||
|
contact_list::init(cx);
|
||||||
|
contact_finder::init(cx);
|
||||||
|
contacts_popover::init(cx);
|
||||||
|
incoming_call_notification::init(cx);
|
||||||
|
project_shared_notification::init(cx);
|
||||||
|
|
||||||
|
cx.add_global_action(move |action: &JoinProject, cx| {
|
||||||
|
let project_id = action.project_id;
|
||||||
|
let follow_user_id = action.follow_user_id;
|
||||||
|
let app_state = app_state.clone();
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let existing_workspace = cx.update(|cx| {
|
||||||
|
cx.window_ids()
|
||||||
|
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
|
||||||
|
.find(|workspace| {
|
||||||
|
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let workspace = if let Some(existing_workspace) = existing_workspace {
|
||||||
|
existing_workspace
|
||||||
|
} else {
|
||||||
|
let project = Project::remote(
|
||||||
|
project_id,
|
||||||
|
app_state.client.clone(),
|
||||||
|
app_state.user_store.clone(),
|
||||||
|
app_state.project_store.clone(),
|
||||||
|
app_state.languages.clone(),
|
||||||
|
app_state.fs.clone(),
|
||||||
|
cx.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
|
||||||
|
let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
|
||||||
|
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||||
|
workspace
|
||||||
|
});
|
||||||
|
workspace
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.activate_window(workspace.window_id());
|
||||||
|
cx.platform().activate(true);
|
||||||
|
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||||
|
let follow_peer_id = room
|
||||||
|
.read(cx)
|
||||||
|
.remote_participants()
|
||||||
|
.iter()
|
||||||
|
.find(|(_, participant)| participant.user.id == follow_user_id)
|
||||||
|
.map(|(peer_id, _)| *peer_id)
|
||||||
|
.or_else(|| {
|
||||||
|
// If we couldn't follow the given user, follow the host instead.
|
||||||
|
let collaborator = workspace
|
||||||
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.collaborators()
|
||||||
|
.values()
|
||||||
|
.find(|collaborator| collaborator.replica_id == 0)?;
|
||||||
|
Some(collaborator.peer_id)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(follow_peer_id) = follow_peer_id {
|
||||||
|
if !workspace.is_following(follow_peer_id) {
|
||||||
|
workspace
|
||||||
|
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
|
||||||
|
.map(|follow| follow.detach_and_log_err(cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
});
|
||||||
|
}
|
@ -1,21 +1,15 @@
|
|||||||
use client::{ContactRequestStatus, User, UserStore};
|
use client::{ContactRequestStatus, User, UserStore};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext,
|
elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext,
|
||||||
RenderContext, Task, View, ViewContext, ViewHandle,
|
Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
use crate::render_icon_button;
|
|
||||||
|
|
||||||
actions!(contact_finder, [Toggle]);
|
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
Picker::<ContactFinder>::init(cx);
|
Picker::<ContactFinder>::init(cx);
|
||||||
cx.add_action(ContactFinder::toggle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ContactFinder {
|
pub struct ContactFinder {
|
||||||
@ -117,18 +111,21 @@ impl PickerDelegate for ContactFinder {
|
|||||||
|
|
||||||
let icon_path = match request_status {
|
let icon_path = match request_status {
|
||||||
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
|
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
|
||||||
"icons/check_8.svg"
|
Some("icons/check_8.svg")
|
||||||
}
|
|
||||||
ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => {
|
|
||||||
"icons/x_mark_8.svg"
|
|
||||||
}
|
}
|
||||||
|
ContactRequestStatus::RequestSent => Some("icons/x_mark_8.svg"),
|
||||||
|
ContactRequestStatus::RequestAccepted => None,
|
||||||
};
|
};
|
||||||
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
|
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
|
||||||
&theme.contact_finder.disabled_contact_button
|
&theme.contact_finder.disabled_contact_button
|
||||||
} else {
|
} else {
|
||||||
&theme.contact_finder.contact_button
|
&theme.contact_finder.contact_button
|
||||||
};
|
};
|
||||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
let style = theme
|
||||||
|
.contact_finder
|
||||||
|
.picker
|
||||||
|
.item
|
||||||
|
.style_for(mouse_state, selected);
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children(user.avatar.clone().map(|avatar| {
|
.with_children(user.avatar.clone().map(|avatar| {
|
||||||
Image::new(avatar)
|
Image::new(avatar)
|
||||||
@ -145,12 +142,21 @@ impl PickerDelegate for ContactFinder {
|
|||||||
.left()
|
.left()
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_children(icon_path.map(|icon_path| {
|
||||||
render_icon_button(button_style, icon_path)
|
Svg::new(icon_path)
|
||||||
|
.with_color(button_style.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(button_style.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(button_style.container)
|
||||||
|
.constrained()
|
||||||
|
.with_width(button_style.button_width)
|
||||||
|
.with_height(button_style.button_width)
|
||||||
.aligned()
|
.aligned()
|
||||||
.flex_float()
|
.flex_float()
|
||||||
.boxed(),
|
.boxed()
|
||||||
)
|
}))
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(style.container)
|
.with_style(style.container)
|
||||||
.constrained()
|
.constrained()
|
||||||
@ -160,34 +166,16 @@ impl PickerDelegate for ContactFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ContactFinder {
|
impl ContactFinder {
|
||||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
|
||||||
workspace.toggle_modal(cx, |workspace, cx| {
|
|
||||||
let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx));
|
|
||||||
cx.subscribe(&finder, Self::on_event).detach();
|
|
||||||
finder
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
let this = cx.weak_handle();
|
let this = cx.weak_handle();
|
||||||
Self {
|
Self {
|
||||||
picker: cx.add_view(|cx| Picker::new(this, cx)),
|
picker: cx.add_view(|cx| {
|
||||||
|
Picker::new(this, cx)
|
||||||
|
.with_theme(|cx| &cx.global::<Settings>().theme.contact_finder.picker)
|
||||||
|
}),
|
||||||
potential_contacts: Arc::from([]),
|
potential_contacts: Arc::from([]),
|
||||||
user_store,
|
user_store,
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_event(
|
|
||||||
workspace: &mut Workspace,
|
|
||||||
_: ViewHandle<Self>,
|
|
||||||
event: &Event,
|
|
||||||
cx: &mut ViewContext<Workspace>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
Event::Dismissed => {
|
|
||||||
workspace.dismiss_modal(cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
1140
crates/collab_ui/src/contact_list.rs
Normal file
1140
crates/collab_ui/src/contact_list.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -49,10 +49,7 @@ impl View for ContactNotification {
|
|||||||
self.user.clone(),
|
self.user.clone(),
|
||||||
"wants to add you as a contact",
|
"wants to add you as a contact",
|
||||||
Some("They won't know if you decline."),
|
Some("They won't know if you decline."),
|
||||||
RespondToContactRequest {
|
Dismiss(self.user.id),
|
||||||
user_id: self.user.id,
|
|
||||||
accept: false,
|
|
||||||
},
|
|
||||||
vec![
|
vec![
|
||||||
(
|
(
|
||||||
"Decline",
|
"Decline",
|
162
crates/collab_ui/src/contacts_popover.rs
Normal file
162
crates/collab_ui/src/contacts_popover.rs
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
use crate::{contact_finder::ContactFinder, contact_list::ContactList};
|
||||||
|
use client::UserStore;
|
||||||
|
use gpui::{
|
||||||
|
actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
|
||||||
|
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
||||||
|
};
|
||||||
|
use project::Project;
|
||||||
|
use settings::Settings;
|
||||||
|
|
||||||
|
actions!(contacts_popover, [ToggleContactFinder]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(ContactsPopover::toggle_contact_finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
Dismissed,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Child {
|
||||||
|
ContactList(ViewHandle<ContactList>),
|
||||||
|
ContactFinder(ViewHandle<ContactFinder>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContactsPopover {
|
||||||
|
child: Child,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
_subscription: Option<gpui::Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContactsPopover {
|
||||||
|
pub fn new(
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let mut this = Self {
|
||||||
|
child: Child::ContactList(
|
||||||
|
cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)),
|
||||||
|
),
|
||||||
|
project,
|
||||||
|
user_store,
|
||||||
|
_subscription: None,
|
||||||
|
};
|
||||||
|
this.show_contact_list(cx);
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
|
||||||
|
match &self.child {
|
||||||
|
Child::ContactList(_) => self.show_contact_finder(cx),
|
||||||
|
Child::ContactFinder(_) => self.show_contact_list(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_contact_finder(&mut self, cx: &mut ViewContext<ContactsPopover>) {
|
||||||
|
let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx));
|
||||||
|
cx.focus(&child);
|
||||||
|
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||||
|
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||||
|
}));
|
||||||
|
self.child = Child::ContactFinder(child);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_contact_list(&mut self, cx: &mut ViewContext<ContactsPopover>) {
|
||||||
|
let child =
|
||||||
|
cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx));
|
||||||
|
cx.focus(&child);
|
||||||
|
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||||
|
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||||
|
}));
|
||||||
|
self.child = Child::ContactList(child);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ContactsPopover {
|
||||||
|
type Event = Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ContactsPopover {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"ContactsPopover"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
|
let child = match &self.child {
|
||||||
|
Child::ContactList(child) => ChildView::new(child),
|
||||||
|
Child::ContactFinder(child) => ChildView::new(child),
|
||||||
|
};
|
||||||
|
|
||||||
|
Flex::column()
|
||||||
|
.with_child(child.flex(1., true).boxed())
|
||||||
|
.with_children(
|
||||||
|
self.user_store
|
||||||
|
.read(cx)
|
||||||
|
.invite_info()
|
||||||
|
.cloned()
|
||||||
|
.and_then(|info| {
|
||||||
|
enum InviteLink {}
|
||||||
|
|
||||||
|
if info.count > 0 {
|
||||||
|
Some(
|
||||||
|
MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
|
||||||
|
let style = theme
|
||||||
|
.contacts_popover
|
||||||
|
.invite_row
|
||||||
|
.style_for(state, false)
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let copied = cx.read_from_clipboard().map_or(false, |item| {
|
||||||
|
item.text().as_str() == info.url.as_ref()
|
||||||
|
});
|
||||||
|
|
||||||
|
Label::new(
|
||||||
|
format!(
|
||||||
|
"{} invite link ({} left)",
|
||||||
|
if copied { "Copied" } else { "Copy" },
|
||||||
|
info.count
|
||||||
|
),
|
||||||
|
style.label.clone(),
|
||||||
|
)
|
||||||
|
.aligned()
|
||||||
|
.left()
|
||||||
|
.constrained()
|
||||||
|
.with_height(theme.contacts_popover.invite_row_height)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new(info.url.to_string()));
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.contacts_popover.container)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.contacts_popover.width)
|
||||||
|
.with_height(theme.contacts_popover.height)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
if cx.is_self_focused() {
|
||||||
|
match &self.child {
|
||||||
|
Child::ContactList(child) => cx.focus(child),
|
||||||
|
Child::ContactFinder(child) => cx.focus(child),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
232
crates/collab_ui/src/incoming_call_notification.rs
Normal file
232
crates/collab_ui/src/incoming_call_notification.rs
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
use call::{ActiveCall, IncomingCall};
|
||||||
|
use client::proto;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use gpui::{
|
||||||
|
elements::*,
|
||||||
|
geometry::{rect::RectF, vector::vec2f},
|
||||||
|
impl_internal_actions, CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext,
|
||||||
|
View, ViewContext, WindowBounds, WindowKind, WindowOptions,
|
||||||
|
};
|
||||||
|
use settings::Settings;
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::JoinProject;
|
||||||
|
|
||||||
|
impl_internal_actions!(incoming_call_notification, [RespondToCall]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(IncomingCallNotification::respond_to_call);
|
||||||
|
|
||||||
|
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let mut notification_window = None;
|
||||||
|
while let Some(incoming_call) = incoming_call.next().await {
|
||||||
|
if let Some(window_id) = notification_window.take() {
|
||||||
|
cx.remove_window(window_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(incoming_call) = incoming_call {
|
||||||
|
const PADDING: f32 = 16.;
|
||||||
|
let screen_size = cx.platform().screen_size();
|
||||||
|
|
||||||
|
let window_size = cx.read(|cx| {
|
||||||
|
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||||
|
vec2f(theme.window_width, theme.window_height)
|
||||||
|
});
|
||||||
|
let (window_id, _) = cx.add_window(
|
||||||
|
WindowOptions {
|
||||||
|
bounds: WindowBounds::Fixed(RectF::new(
|
||||||
|
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
|
||||||
|
window_size,
|
||||||
|
)),
|
||||||
|
titlebar: None,
|
||||||
|
center: false,
|
||||||
|
kind: WindowKind::PopUp,
|
||||||
|
is_movable: false,
|
||||||
|
},
|
||||||
|
|_| IncomingCallNotification::new(incoming_call),
|
||||||
|
);
|
||||||
|
notification_window = Some(window_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
struct RespondToCall {
|
||||||
|
accept: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IncomingCallNotification {
|
||||||
|
call: IncomingCall,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IncomingCallNotification {
|
||||||
|
pub fn new(call: IncomingCall) -> Self {
|
||||||
|
Self { call }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
|
||||||
|
let active_call = ActiveCall::global(cx);
|
||||||
|
if action.accept {
|
||||||
|
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
|
||||||
|
let caller_user_id = self.call.caller.id;
|
||||||
|
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
|
||||||
|
cx.spawn_weak(|_, mut cx| async move {
|
||||||
|
join.await?;
|
||||||
|
if let Some(project_id) = initial_project_id {
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.dispatch_global_action(JoinProject {
|
||||||
|
project_id,
|
||||||
|
follow_user_id: caller_user_id,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
} else {
|
||||||
|
active_call.update(cx, |active_call, _| {
|
||||||
|
active_call.decline_incoming().log_err();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_caller(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||||
|
let default_project = proto::ParticipantProject::default();
|
||||||
|
let initial_project = self
|
||||||
|
.call
|
||||||
|
.initial_project
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&default_project);
|
||||||
|
Flex::row()
|
||||||
|
.with_children(self.call.caller.avatar.clone().map(|avatar| {
|
||||||
|
Image::new(avatar)
|
||||||
|
.with_style(theme.caller_avatar)
|
||||||
|
.aligned()
|
||||||
|
.boxed()
|
||||||
|
}))
|
||||||
|
.with_child(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
self.call.caller.github_login.clone(),
|
||||||
|
theme.caller_username.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.caller_username.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
format!(
|
||||||
|
"is sharing a project in Zed{}",
|
||||||
|
if initial_project.worktree_root_names.is_empty() {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
":"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
theme.caller_message.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.caller_message.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_children(if initial_project.worktree_root_names.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
Label::new(
|
||||||
|
initial_project.worktree_root_names.join(", "),
|
||||||
|
theme.worktree_roots.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.worktree_roots.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.caller_metadata)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.caller_container)
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
enum Accept {}
|
||||||
|
enum Decline {}
|
||||||
|
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
|
||||||
|
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||||
|
Label::new("Accept".to_string(), theme.accept_button.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.accept_button.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, cx| {
|
||||||
|
cx.dispatch_action(RespondToCall { accept: true });
|
||||||
|
})
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
|
||||||
|
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||||
|
Label::new("Decline".to_string(), theme.decline_button.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.decline_button.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, cx| {
|
||||||
|
cx.dispatch_action(RespondToCall { accept: false });
|
||||||
|
})
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.constrained()
|
||||||
|
.with_width(
|
||||||
|
cx.global::<Settings>()
|
||||||
|
.theme
|
||||||
|
.incoming_call_notification
|
||||||
|
.button_width,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for IncomingCallNotification {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for IncomingCallNotification {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"IncomingCallNotification"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
||||||
|
let background = cx
|
||||||
|
.global::<Settings>()
|
||||||
|
.theme
|
||||||
|
.incoming_call_notification
|
||||||
|
.background;
|
||||||
|
Flex::row()
|
||||||
|
.with_child(self.render_caller(cx))
|
||||||
|
.with_child(self.render_buttons(cx))
|
||||||
|
.contained()
|
||||||
|
.with_background_color(background)
|
||||||
|
.expanded()
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,7 @@
|
|||||||
use crate::render_icon_button;
|
|
||||||
use client::User;
|
use client::User;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text},
|
elements::*, platform::CursorStyle, Action, Element, ElementBox, MouseButton, RenderContext,
|
||||||
platform::CursorStyle,
|
View,
|
||||||
Action, Element, ElementBox, MouseButton, RenderContext, View,
|
|
||||||
};
|
};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -53,11 +51,18 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
|||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
|
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
|
||||||
render_icon_button(
|
let style = theme.dismiss_button.style_for(state, false);
|
||||||
theme.dismiss_button.style_for(state, false),
|
Svg::new("icons/x_mark_thin_8.svg")
|
||||||
"icons/x_mark_thin_8.svg",
|
.with_color(style.color)
|
||||||
)
|
.constrained()
|
||||||
.boxed()
|
.with_width(style.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.button_width)
|
||||||
|
.with_height(style.button_width)
|
||||||
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.with_padding(Padding::uniform(5.))
|
.with_padding(Padding::uniform(5.))
|
232
crates/collab_ui/src/project_shared_notification.rs
Normal file
232
crates/collab_ui/src/project_shared_notification.rs
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
use call::{room, ActiveCall};
|
||||||
|
use client::User;
|
||||||
|
use collections::HashMap;
|
||||||
|
use gpui::{
|
||||||
|
actions,
|
||||||
|
elements::*,
|
||||||
|
geometry::{rect::RectF, vector::vec2f},
|
||||||
|
CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
|
||||||
|
WindowBounds, WindowKind, WindowOptions,
|
||||||
|
};
|
||||||
|
use settings::Settings;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use workspace::JoinProject;
|
||||||
|
|
||||||
|
actions!(project_shared_notification, [DismissProject]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(ProjectSharedNotification::join);
|
||||||
|
cx.add_action(ProjectSharedNotification::dismiss);
|
||||||
|
|
||||||
|
let active_call = ActiveCall::global(cx);
|
||||||
|
let mut notification_windows = HashMap::default();
|
||||||
|
cx.subscribe(&active_call, move |_, event, cx| match event {
|
||||||
|
room::Event::RemoteProjectShared {
|
||||||
|
owner,
|
||||||
|
project_id,
|
||||||
|
worktree_root_names,
|
||||||
|
} => {
|
||||||
|
const PADDING: f32 = 16.;
|
||||||
|
let screen_size = cx.platform().screen_size();
|
||||||
|
|
||||||
|
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||||
|
let window_size = vec2f(theme.window_width, theme.window_height);
|
||||||
|
let (window_id, _) = cx.add_window(
|
||||||
|
WindowOptions {
|
||||||
|
bounds: WindowBounds::Fixed(RectF::new(
|
||||||
|
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
|
||||||
|
window_size,
|
||||||
|
)),
|
||||||
|
titlebar: None,
|
||||||
|
center: false,
|
||||||
|
kind: WindowKind::PopUp,
|
||||||
|
is_movable: false,
|
||||||
|
},
|
||||||
|
|_| {
|
||||||
|
ProjectSharedNotification::new(
|
||||||
|
owner.clone(),
|
||||||
|
*project_id,
|
||||||
|
worktree_root_names.clone(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
notification_windows.insert(*project_id, window_id);
|
||||||
|
}
|
||||||
|
room::Event::RemoteProjectUnshared { project_id } => {
|
||||||
|
if let Some(window_id) = notification_windows.remove(&project_id) {
|
||||||
|
cx.remove_window(window_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
room::Event::Left => {
|
||||||
|
for (_, window_id) in notification_windows.drain() {
|
||||||
|
cx.remove_window(window_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProjectSharedNotification {
|
||||||
|
project_id: u64,
|
||||||
|
worktree_root_names: Vec<String>,
|
||||||
|
owner: Arc<User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectSharedNotification {
|
||||||
|
fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
project_id,
|
||||||
|
worktree_root_names,
|
||||||
|
owner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
|
||||||
|
let window_id = cx.window_id();
|
||||||
|
cx.remove_window(window_id);
|
||||||
|
cx.propagate_action();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
|
||||||
|
let window_id = cx.window_id();
|
||||||
|
cx.remove_window(window_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_owner(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||||
|
Flex::row()
|
||||||
|
.with_children(self.owner.avatar.clone().map(|avatar| {
|
||||||
|
Image::new(avatar)
|
||||||
|
.with_style(theme.owner_avatar)
|
||||||
|
.aligned()
|
||||||
|
.boxed()
|
||||||
|
}))
|
||||||
|
.with_child(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
self.owner.github_login.clone(),
|
||||||
|
theme.owner_username.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.owner_username.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
format!(
|
||||||
|
"is sharing a project in Zed{}",
|
||||||
|
if self.worktree_root_names.is_empty() {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
":"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
theme.message.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.message.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_children(if self.worktree_root_names.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
Label::new(
|
||||||
|
self.worktree_root_names.join(", "),
|
||||||
|
theme.worktree_roots.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.worktree_roots.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.owner_metadata)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.owner_container)
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
enum Open {}
|
||||||
|
enum Dismiss {}
|
||||||
|
|
||||||
|
let project_id = self.project_id;
|
||||||
|
let owner_user_id = self.owner.id;
|
||||||
|
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::<Open>::new(0, cx, |_, cx| {
|
||||||
|
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||||
|
Label::new("Open".to_string(), theme.open_button.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.open_button.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.dispatch_action(JoinProject {
|
||||||
|
project_id,
|
||||||
|
follow_user_id: owner_user_id,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
|
||||||
|
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||||
|
Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.dismiss_button.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, cx| {
|
||||||
|
cx.dispatch_action(DismissProject);
|
||||||
|
})
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.constrained()
|
||||||
|
.with_width(
|
||||||
|
cx.global::<Settings>()
|
||||||
|
.theme
|
||||||
|
.project_shared_notification
|
||||||
|
.button_width,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ProjectSharedNotification {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ProjectSharedNotification {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"ProjectSharedNotification"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
||||||
|
let background = cx
|
||||||
|
.global::<Settings>()
|
||||||
|
.theme
|
||||||
|
.project_shared_notification
|
||||||
|
.background;
|
||||||
|
Flex::row()
|
||||||
|
.with_child(self.render_owner(cx))
|
||||||
|
.with_child(self.render_buttons(cx))
|
||||||
|
.contained()
|
||||||
|
.with_background_color(background)
|
||||||
|
.expanded()
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
@ -1,32 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "contacts_panel"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
path = "src/contacts_panel.rs"
|
|
||||||
doctest = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
client = { path = "../client" }
|
|
||||||
collections = { path = "../collections" }
|
|
||||||
editor = { path = "../editor" }
|
|
||||||
fuzzy = { path = "../fuzzy" }
|
|
||||||
gpui = { path = "../gpui" }
|
|
||||||
menu = { path = "../menu" }
|
|
||||||
picker = { path = "../picker" }
|
|
||||||
project = { path = "../project" }
|
|
||||||
settings = { path = "../settings" }
|
|
||||||
theme = { path = "../theme" }
|
|
||||||
util = { path = "../util" }
|
|
||||||
workspace = { path = "../workspace" }
|
|
||||||
anyhow = "1.0"
|
|
||||||
futures = "0.3"
|
|
||||||
log = "0.4"
|
|
||||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
language = { path = "../language", features = ["test-support"] }
|
|
||||||
project = { path = "../project", features = ["test-support"] }
|
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
|
File diff suppressed because it is too large
Load Diff
@ -1,80 +0,0 @@
|
|||||||
use client::User;
|
|
||||||
use gpui::{
|
|
||||||
actions, ElementBox, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext,
|
|
||||||
};
|
|
||||||
use project::Project;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use workspace::Notification;
|
|
||||||
|
|
||||||
use crate::notifications::render_user_notification;
|
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
|
||||||
cx.add_action(JoinProjectNotification::decline);
|
|
||||||
cx.add_action(JoinProjectNotification::accept);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Event {
|
|
||||||
Dismiss,
|
|
||||||
}
|
|
||||||
|
|
||||||
actions!(contacts_panel, [Accept, Decline]);
|
|
||||||
|
|
||||||
pub struct JoinProjectNotification {
|
|
||||||
project: ModelHandle<Project>,
|
|
||||||
user: Arc<User>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JoinProjectNotification {
|
|
||||||
pub fn new(project: ModelHandle<Project>, user: Arc<User>, cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
cx.subscribe(&project, |this, _, event, cx| {
|
|
||||||
if let project::Event::ContactCancelledJoinRequest(user) = event {
|
|
||||||
if *user == this.user {
|
|
||||||
cx.emit(Event::Dismiss);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
Self { project, user }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decline(&mut self, _: &Decline, cx: &mut ViewContext<Self>) {
|
|
||||||
self.project.update(cx, |project, cx| {
|
|
||||||
project.respond_to_join_request(self.user.id, false, cx)
|
|
||||||
});
|
|
||||||
cx.emit(Event::Dismiss)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accept(&mut self, _: &Accept, cx: &mut ViewContext<Self>) {
|
|
||||||
self.project.update(cx, |project, cx| {
|
|
||||||
project.respond_to_join_request(self.user.id, true, cx)
|
|
||||||
});
|
|
||||||
cx.emit(Event::Dismiss)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for JoinProjectNotification {
|
|
||||||
type Event = Event;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for JoinProjectNotification {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"JoinProjectNotification"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
|
||||||
render_user_notification(
|
|
||||||
self.user.clone(),
|
|
||||||
"wants to join your project",
|
|
||||||
None,
|
|
||||||
Decline,
|
|
||||||
vec![("Decline", Box::new(Decline)), ("Accept", Box::new(Accept))],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Notification for JoinProjectNotification {
|
|
||||||
fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
|
|
||||||
matches!(event, Event::Dismiss)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "contacts_status_item"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
path = "src/contacts_status_item.rs"
|
|
||||||
doctest = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
client = { path = "../client" }
|
|
||||||
collections = { path = "../collections" }
|
|
||||||
editor = { path = "../editor" }
|
|
||||||
fuzzy = { path = "../fuzzy" }
|
|
||||||
gpui = { path = "../gpui" }
|
|
||||||
menu = { path = "../menu" }
|
|
||||||
picker = { path = "../picker" }
|
|
||||||
project = { path = "../project" }
|
|
||||||
settings = { path = "../settings" }
|
|
||||||
theme = { path = "../theme" }
|
|
||||||
util = { path = "../util" }
|
|
||||||
workspace = { path = "../workspace" }
|
|
||||||
anyhow = "1.0"
|
|
||||||
futures = "0.3"
|
|
||||||
log = "0.4"
|
|
||||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
language = { path = "../language", features = ["test-support"] }
|
|
||||||
project = { path = "../project", features = ["test-support"] }
|
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
|
@ -1,94 +0,0 @@
|
|||||||
use editor::Editor;
|
|
||||||
use gpui::{elements::*, Entity, RenderContext, View, ViewContext, ViewHandle};
|
|
||||||
use settings::Settings;
|
|
||||||
|
|
||||||
pub enum Event {
|
|
||||||
Deactivated,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ContactsPopover {
|
|
||||||
filter_editor: ViewHandle<Editor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for ContactsPopover {
|
|
||||||
type Event = Event;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for ContactsPopover {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"ContactsPopover"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
|
||||||
let theme = &cx.global::<Settings>().theme.contacts_popover;
|
|
||||||
|
|
||||||
Flex::row()
|
|
||||||
.with_child(
|
|
||||||
ChildView::new(self.filter_editor.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(
|
|
||||||
cx.global::<Settings>()
|
|
||||||
.theme
|
|
||||||
.contacts_panel
|
|
||||||
.user_query_editor
|
|
||||||
.container,
|
|
||||||
)
|
|
||||||
.flex(1., true)
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
// .with_child(
|
|
||||||
// MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
|
|
||||||
// Svg::new("icons/user_plus_16.svg")
|
|
||||||
// .with_color(theme.add_contact_button.color)
|
|
||||||
// .constrained()
|
|
||||||
// .with_height(16.)
|
|
||||||
// .contained()
|
|
||||||
// .with_style(theme.add_contact_button.container)
|
|
||||||
// .aligned()
|
|
||||||
// .boxed()
|
|
||||||
// })
|
|
||||||
// .with_cursor_style(CursorStyle::PointingHand)
|
|
||||||
// .on_click(MouseButton::Left, |_, cx| {
|
|
||||||
// cx.dispatch_action(contact_finder::Toggle)
|
|
||||||
// })
|
|
||||||
// .boxed(),
|
|
||||||
// )
|
|
||||||
.constrained()
|
|
||||||
.with_height(
|
|
||||||
cx.global::<Settings>()
|
|
||||||
.theme
|
|
||||||
.contacts_panel
|
|
||||||
.user_query_editor_height,
|
|
||||||
)
|
|
||||||
.aligned()
|
|
||||||
.top()
|
|
||||||
.contained()
|
|
||||||
.with_background_color(theme.background)
|
|
||||||
.with_uniform_padding(4.)
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContactsPopover {
|
|
||||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
cx.observe_window_activation(Self::window_activation_changed)
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
let filter_editor = cx.add_view(|cx| {
|
|
||||||
let mut editor = Editor::single_line(
|
|
||||||
Some(|theme| theme.contacts_panel.user_query_editor.clone()),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
editor.set_placeholder_text("Filter contacts", cx);
|
|
||||||
editor
|
|
||||||
});
|
|
||||||
|
|
||||||
Self { filter_editor }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn window_activation_changed(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
|
|
||||||
if !is_active {
|
|
||||||
cx.emit(Event::Deactivated);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
mod contacts_popover;
|
|
||||||
|
|
||||||
use contacts_popover::ContactsPopover;
|
|
||||||
use gpui::{
|
|
||||||
actions,
|
|
||||||
color::Color,
|
|
||||||
elements::*,
|
|
||||||
geometry::{rect::RectF, vector::vec2f},
|
|
||||||
Appearance, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
|
|
||||||
ViewHandle, WindowKind,
|
|
||||||
};
|
|
||||||
|
|
||||||
actions!(contacts_status_item, [ToggleContactsPopover]);
|
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
|
||||||
cx.add_action(ContactsStatusItem::toggle_contacts_popover);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ContactsStatusItem {
|
|
||||||
popover: Option<ViewHandle<ContactsPopover>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for ContactsStatusItem {
|
|
||||||
type Event = ();
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for ContactsStatusItem {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"ContactsStatusItem"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
|
||||||
let color = match cx.appearance {
|
|
||||||
Appearance::Light | Appearance::VibrantLight => Color::black(),
|
|
||||||
Appearance::Dark | Appearance::VibrantDark => Color::white(),
|
|
||||||
};
|
|
||||||
MouseEventHandler::<Self>::new(0, cx, |_, _| {
|
|
||||||
Svg::new("icons/zed_22.svg")
|
|
||||||
.with_color(color)
|
|
||||||
.aligned()
|
|
||||||
.boxed()
|
|
||||||
})
|
|
||||||
.on_click(MouseButton::Left, |_, cx| {
|
|
||||||
cx.dispatch_action(ToggleContactsPopover);
|
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContactsStatusItem {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self { popover: None }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext<Self>) {
|
|
||||||
match self.popover.take() {
|
|
||||||
Some(popover) => {
|
|
||||||
cx.remove_window(popover.window_id());
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let window_bounds = cx.window_bounds();
|
|
||||||
let size = vec2f(360., 460.);
|
|
||||||
let origin = window_bounds.lower_left()
|
|
||||||
+ vec2f(window_bounds.width() / 2. - size.x() / 2., 0.);
|
|
||||||
let (_, popover) = cx.add_window(
|
|
||||||
gpui::WindowOptions {
|
|
||||||
bounds: gpui::WindowBounds::Fixed(RectF::new(origin, size)),
|
|
||||||
titlebar: None,
|
|
||||||
center: false,
|
|
||||||
kind: WindowKind::PopUp,
|
|
||||||
is_movable: false,
|
|
||||||
},
|
|
||||||
|cx| ContactsPopover::new(cx),
|
|
||||||
);
|
|
||||||
cx.subscribe(&popover, Self::on_popover_event).detach();
|
|
||||||
self.popover = Some(popover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_popover_event(
|
|
||||||
&mut self,
|
|
||||||
popover: ViewHandle<ContactsPopover>,
|
|
||||||
event: &contacts_popover::Event,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
contacts_popover::Event::Deactivated => {
|
|
||||||
self.popover.take();
|
|
||||||
cx.remove_window(popover.window_id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1731,7 +1731,8 @@ impl Element for EditorElement {
|
|||||||
layout: &mut Self::LayoutState,
|
layout: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
cx.scene.push_layer(Some(bounds));
|
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||||
|
cx.scene.push_layer(Some(visible_bounds));
|
||||||
|
|
||||||
let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size);
|
let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size);
|
||||||
let text_bounds = RectF::new(
|
let text_bounds = RectF::new(
|
||||||
|
@ -786,6 +786,24 @@ impl AsyncAppContext {
|
|||||||
self.update(|cx| cx.add_window(window_options, build_root_view))
|
self.update(|cx| cx.add_window(window_options, build_root_view))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_window(&mut self, window_id: usize) {
|
||||||
|
self.update(|cx| cx.remove_window(window_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_window(&mut self, window_id: usize) {
|
||||||
|
self.update(|cx| cx.activate_window(window_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prompt(
|
||||||
|
&mut self,
|
||||||
|
window_id: usize,
|
||||||
|
level: PromptLevel,
|
||||||
|
msg: &str,
|
||||||
|
answers: &[&str],
|
||||||
|
) -> oneshot::Receiver<usize> {
|
||||||
|
self.update(|cx| cx.prompt(window_id, level, msg, answers))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn platform(&self) -> Arc<dyn Platform> {
|
pub fn platform(&self) -> Arc<dyn Platform> {
|
||||||
self.0.borrow().platform()
|
self.0.borrow().platform()
|
||||||
}
|
}
|
||||||
@ -1519,6 +1537,17 @@ impl MutableAppContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn observe_default_global<G, F>(&mut self, observe: F) -> Subscription
|
||||||
|
where
|
||||||
|
G: Any + Default,
|
||||||
|
F: 'static + FnMut(&mut MutableAppContext),
|
||||||
|
{
|
||||||
|
if !self.has_global::<G>() {
|
||||||
|
self.set_global(G::default());
|
||||||
|
}
|
||||||
|
self.observe_global::<G, F>(observe)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription
|
pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
E: Entity,
|
E: Entity,
|
||||||
@ -1887,6 +1916,10 @@ impl MutableAppContext {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_globals(&mut self) {
|
||||||
|
self.cx.globals.clear();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
||||||
where
|
where
|
||||||
T: Entity,
|
T: Entity,
|
||||||
@ -1967,6 +2000,10 @@ impl MutableAppContext {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_status_bar_item(&mut self, id: usize) {
|
||||||
|
self.remove_window(id);
|
||||||
|
}
|
||||||
|
|
||||||
fn register_platform_window(
|
fn register_platform_window(
|
||||||
&mut self,
|
&mut self,
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
@ -4650,6 +4687,12 @@ impl<T> PartialEq for WeakModelHandle<T> {
|
|||||||
|
|
||||||
impl<T> Eq for WeakModelHandle<T> {}
|
impl<T> Eq for WeakModelHandle<T> {}
|
||||||
|
|
||||||
|
impl<T: Entity> PartialEq<ModelHandle<T>> for WeakModelHandle<T> {
|
||||||
|
fn eq(&self, other: &ModelHandle<T>) -> bool {
|
||||||
|
self.model_id == other.model_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Clone for WeakModelHandle<T> {
|
impl<T> Clone for WeakModelHandle<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -271,9 +271,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
|||||||
mut layout,
|
mut layout,
|
||||||
} => {
|
} => {
|
||||||
let bounds = RectF::new(origin, size);
|
let bounds = RectF::new(origin, size);
|
||||||
let visible_bounds = visible_bounds
|
|
||||||
.intersection(bounds)
|
|
||||||
.unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
|
|
||||||
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
|
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
|
||||||
Lifecycle::PostPaint {
|
Lifecycle::PostPaint {
|
||||||
element,
|
element,
|
||||||
@ -292,9 +289,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
|||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let bounds = RectF::new(origin, bounds.size());
|
let bounds = RectF::new(origin, bounds.size());
|
||||||
let visible_bounds = visible_bounds
|
|
||||||
.intersection(bounds)
|
|
||||||
.unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
|
|
||||||
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
|
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
|
||||||
Lifecycle::PostPaint {
|
Lifecycle::PostPaint {
|
||||||
element,
|
element,
|
||||||
|
@ -241,11 +241,12 @@ impl Element for Flex {
|
|||||||
remaining_space: &mut Self::LayoutState,
|
remaining_space: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
let mut remaining_space = *remaining_space;
|
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||||
|
|
||||||
|
let mut remaining_space = *remaining_space;
|
||||||
let overflowing = remaining_space < 0.;
|
let overflowing = remaining_space < 0.;
|
||||||
if overflowing {
|
if overflowing {
|
||||||
cx.scene.push_layer(Some(bounds));
|
cx.scene.push_layer(Some(visible_bounds));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(scroll_state) = &self.scroll_state {
|
if let Some(scroll_state) = &self.scroll_state {
|
||||||
|
@ -27,6 +27,8 @@ pub struct ImageStyle {
|
|||||||
pub height: Option<f32>,
|
pub height: Option<f32>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub width: Option<f32>,
|
pub width: Option<f32>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub grayscale: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
@ -74,6 +76,7 @@ impl Element for Image {
|
|||||||
bounds,
|
bounds,
|
||||||
border: self.style.border,
|
border: self.style.border,
|
||||||
corner_radius: self.style.corner_radius,
|
corner_radius: self.style.corner_radius,
|
||||||
|
grayscale: self.style.grayscale,
|
||||||
data: self.data.clone(),
|
data: self.data.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -261,7 +261,8 @@ impl Element for List {
|
|||||||
scroll_top: &mut ListOffset,
|
scroll_top: &mut ListOffset,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) {
|
) {
|
||||||
cx.scene.push_layer(Some(bounds));
|
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
||||||
|
cx.scene.push_layer(Some(visible_bounds));
|
||||||
|
|
||||||
cx.scene
|
cx.scene
|
||||||
.push_mouse_region(MouseRegion::new::<Self>(10, 0, bounds).on_scroll({
|
.push_mouse_region(MouseRegion::new::<Self>(10, 0, bounds).on_scroll({
|
||||||
|
@ -169,6 +169,7 @@ impl<Tag> Element for MouseEventHandler<Tag> {
|
|||||||
_: &mut Self::LayoutState,
|
_: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
|
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
||||||
let hit_bounds = self.hit_bounds(visible_bounds);
|
let hit_bounds = self.hit_bounds(visible_bounds);
|
||||||
if let Some(style) = self.cursor_style {
|
if let Some(style) = self.cursor_style {
|
||||||
cx.scene.push_cursor_region(CursorRegion {
|
cx.scene.push_cursor_region(CursorRegion {
|
||||||
|
@ -217,7 +217,11 @@ impl Element for Overlay {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.child.paint(bounds.origin(), bounds, cx);
|
self.child.paint(
|
||||||
|
bounds.origin(),
|
||||||
|
RectF::new(Vector2F::zero(), cx.window_size),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
cx.scene.pop_stacking_context();
|
cx.scene.pop_stacking_context();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,7 +284,9 @@ impl Element for UniformList {
|
|||||||
layout: &mut Self::LayoutState,
|
layout: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
cx.scene.push_layer(Some(bounds));
|
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
||||||
|
|
||||||
|
cx.scene.push_layer(Some(visible_bounds));
|
||||||
|
|
||||||
cx.scene.push_mouse_region(
|
cx.scene.push_mouse_region(
|
||||||
MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({
|
MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({
|
||||||
|
@ -44,6 +44,8 @@ pub trait Platform: Send + Sync {
|
|||||||
fn unhide_other_apps(&self);
|
fn unhide_other_apps(&self);
|
||||||
fn quit(&self);
|
fn quit(&self);
|
||||||
|
|
||||||
|
fn screen_size(&self) -> Vector2F;
|
||||||
|
|
||||||
fn open_window(
|
fn open_window(
|
||||||
&self,
|
&self,
|
||||||
id: usize,
|
id: usize,
|
||||||
|
@ -2,7 +2,9 @@ use super::{
|
|||||||
event::key_to_native, status_item::StatusItem, BoolExt as _, Dispatcher, FontSystem, Window,
|
event::key_to_native, status_item::StatusItem, BoolExt as _, Dispatcher, FontSystem, Window,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
executor, keymap,
|
executor,
|
||||||
|
geometry::vector::{vec2f, Vector2F},
|
||||||
|
keymap,
|
||||||
platform::{self, CursorStyle},
|
platform::{self, CursorStyle},
|
||||||
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
|
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
|
||||||
};
|
};
|
||||||
@ -12,7 +14,7 @@ use cocoa::{
|
|||||||
appkit::{
|
appkit::{
|
||||||
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
||||||
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
|
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
|
||||||
NSPasteboardTypeString, NSSavePanel, NSWindow,
|
NSPasteboardTypeString, NSSavePanel, NSScreen, NSWindow,
|
||||||
},
|
},
|
||||||
base::{id, nil, selector, YES},
|
base::{id, nil, selector, YES},
|
||||||
foundation::{
|
foundation::{
|
||||||
@ -486,6 +488,14 @@ impl platform::Platform for MacPlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn screen_size(&self) -> Vector2F {
|
||||||
|
unsafe {
|
||||||
|
let screen = NSScreen::mainScreen(nil);
|
||||||
|
let frame = NSScreen::frame(screen);
|
||||||
|
vec2f(frame.size.width as f32, frame.size.height as f32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn open_window(
|
fn open_window(
|
||||||
&self,
|
&self,
|
||||||
id: usize,
|
id: usize,
|
||||||
|
@ -747,6 +747,7 @@ impl Renderer {
|
|||||||
border_left: border_width * (image.border.left as usize as f32),
|
border_left: border_width * (image.border.left as usize as f32),
|
||||||
border_color: image.border.color.to_uchar4(),
|
border_color: image.border.color.to_uchar4(),
|
||||||
corner_radius,
|
corner_radius,
|
||||||
|
grayscale: image.grayscale as u8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -769,6 +770,7 @@ impl Renderer {
|
|||||||
border_left: 0.,
|
border_left: 0.,
|
||||||
border_color: Default::default(),
|
border_color: Default::default(),
|
||||||
corner_radius: 0.,
|
corner_radius: 0.,
|
||||||
|
grayscale: false as u8,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log::warn!("could not render glyph with id {}", image_glyph.id);
|
log::warn!("could not render glyph with id {}", image_glyph.id);
|
||||||
|
@ -90,6 +90,7 @@ typedef struct {
|
|||||||
float border_left;
|
float border_left;
|
||||||
vector_uchar4 border_color;
|
vector_uchar4 border_color;
|
||||||
float corner_radius;
|
float corner_radius;
|
||||||
|
uint8_t grayscale;
|
||||||
} GPUIImage;
|
} GPUIImage;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
@ -44,6 +44,7 @@ struct QuadFragmentInput {
|
|||||||
float border_left;
|
float border_left;
|
||||||
float4 border_color;
|
float4 border_color;
|
||||||
float corner_radius;
|
float corner_radius;
|
||||||
|
uchar grayscale; // only used in image shader
|
||||||
};
|
};
|
||||||
|
|
||||||
float4 quad_sdf(QuadFragmentInput input) {
|
float4 quad_sdf(QuadFragmentInput input) {
|
||||||
@ -110,6 +111,7 @@ vertex QuadFragmentInput quad_vertex(
|
|||||||
quad.border_left,
|
quad.border_left,
|
||||||
coloru_to_colorf(quad.border_color),
|
coloru_to_colorf(quad.border_color),
|
||||||
quad.corner_radius,
|
quad.corner_radius,
|
||||||
|
0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,6 +253,7 @@ vertex QuadFragmentInput image_vertex(
|
|||||||
image.border_left,
|
image.border_left,
|
||||||
coloru_to_colorf(image.border_color),
|
coloru_to_colorf(image.border_color),
|
||||||
image.corner_radius,
|
image.corner_radius,
|
||||||
|
image.grayscale,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,6 +263,13 @@ fragment float4 image_fragment(
|
|||||||
) {
|
) {
|
||||||
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
|
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
|
||||||
input.background_color = atlas.sample(atlas_sampler, input.atlas_position);
|
input.background_color = atlas.sample(atlas_sampler, input.atlas_position);
|
||||||
|
if (input.grayscale) {
|
||||||
|
float grayscale =
|
||||||
|
0.2126 * input.background_color.r +
|
||||||
|
0.7152 * input.background_color.g +
|
||||||
|
0.0722 * input.background_color.b;
|
||||||
|
input.background_color = float4(grayscale, grayscale, grayscale, input.background_color.a);
|
||||||
|
}
|
||||||
return quad_sdf(input);
|
return quad_sdf(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,6 +299,7 @@ vertex QuadFragmentInput surface_vertex(
|
|||||||
0.,
|
0.,
|
||||||
float4(0.),
|
float4(0.),
|
||||||
0.,
|
0.,
|
||||||
|
0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +131,10 @@ impl super::Platform for Platform {
|
|||||||
|
|
||||||
fn quit(&self) {}
|
fn quit(&self) {}
|
||||||
|
|
||||||
|
fn screen_size(&self) -> Vector2F {
|
||||||
|
vec2f(1024., 768.)
|
||||||
|
}
|
||||||
|
|
||||||
fn open_window(
|
fn open_window(
|
||||||
&self,
|
&self,
|
||||||
_: usize,
|
_: usize,
|
||||||
|
@ -172,6 +172,7 @@ pub struct Image {
|
|||||||
pub bounds: RectF,
|
pub bounds: RectF,
|
||||||
pub border: Border,
|
pub border: Border,
|
||||||
pub corner_radius: f32,
|
pub corner_radius: f32,
|
||||||
|
pub grayscale: bool,
|
||||||
pub data: Arc<ImageData>,
|
pub data: Arc<ImageData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ pub fn run_test(
|
|||||||
|
|
||||||
cx.update(|cx| cx.remove_all_windows());
|
cx.update(|cx| cx.remove_all_windows());
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
cx.update(|_| {}); // flush effects
|
cx.update(|cx| cx.clear_globals());
|
||||||
|
|
||||||
leak_detector.lock().detect();
|
leak_detector.lock().detect();
|
||||||
if is_last_iteration {
|
if is_last_iteration {
|
||||||
|
@ -122,7 +122,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
|||||||
cx_teardowns.extend(quote!(
|
cx_teardowns.extend(quote!(
|
||||||
#cx_varname.update(|cx| cx.remove_all_windows());
|
#cx_varname.update(|cx| cx.remove_all_windows());
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
#cx_varname.update(|_| {}); // flush effects
|
#cx_varname.update(|cx| cx.clear_globals());
|
||||||
));
|
));
|
||||||
inner_fn_args.extend(quote!(&mut #cx_varname,));
|
inner_fn_args.extend(quote!(&mut #cx_varname,));
|
||||||
continue;
|
continue;
|
||||||
|
@ -19,6 +19,7 @@ pub struct Picker<D: PickerDelegate> {
|
|||||||
query_editor: ViewHandle<Editor>,
|
query_editor: ViewHandle<Editor>,
|
||||||
list_state: UniformListState,
|
list_state: UniformListState,
|
||||||
max_size: Vector2F,
|
max_size: Vector2F,
|
||||||
|
theme: Box<dyn FnMut(&AppContext) -> &theme::Picker>,
|
||||||
confirmed: bool,
|
confirmed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,8 +52,8 @@ impl<D: PickerDelegate> View for Picker<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
||||||
let settings = cx.global::<Settings>();
|
let theme = (self.theme)(cx);
|
||||||
let container_style = settings.theme.picker.container;
|
let container_style = theme.container;
|
||||||
let delegate = self.delegate.clone();
|
let delegate = self.delegate.clone();
|
||||||
let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
|
let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
|
||||||
delegate.read(cx).match_count()
|
delegate.read(cx).match_count()
|
||||||
@ -64,17 +65,14 @@ impl<D: PickerDelegate> View for Picker<D> {
|
|||||||
.with_child(
|
.with_child(
|
||||||
ChildView::new(&self.query_editor)
|
ChildView::new(&self.query_editor)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(settings.theme.picker.input_editor.container)
|
.with_style(theme.input_editor.container)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
if match_count == 0 {
|
if match_count == 0 {
|
||||||
Label::new(
|
Label::new("No matches".into(), theme.empty.label.clone())
|
||||||
"No matches".into(),
|
.contained()
|
||||||
settings.theme.picker.empty.label.clone(),
|
.with_style(theme.empty.container)
|
||||||
)
|
|
||||||
.contained()
|
|
||||||
.with_style(settings.theme.picker.empty.container)
|
|
||||||
} else {
|
} else {
|
||||||
UniformList::new(
|
UniformList::new(
|
||||||
self.list_state.clone(),
|
self.list_state.clone(),
|
||||||
@ -147,6 +145,7 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
list_state: Default::default(),
|
list_state: Default::default(),
|
||||||
delegate,
|
delegate,
|
||||||
max_size: vec2f(540., 420.),
|
max_size: vec2f(540., 420.),
|
||||||
|
theme: Box::new(|cx| &cx.global::<Settings>().theme.picker),
|
||||||
confirmed: false,
|
confirmed: false,
|
||||||
};
|
};
|
||||||
cx.defer(|this, cx| {
|
cx.defer(|this, cx| {
|
||||||
@ -163,6 +162,14 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_theme<F>(mut self, theme: F) -> Self
|
||||||
|
where
|
||||||
|
F: 'static + FnMut(&AppContext) -> &theme::Picker,
|
||||||
|
{
|
||||||
|
self.theme = Box::new(theme);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn query(&self, cx: &AppContext) -> String {
|
pub fn query(&self, cx: &AppContext) -> String {
|
||||||
self.query_editor.read(cx).text(cx)
|
self.query_editor.read(cx).text(cx)
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ pub mod worktree;
|
|||||||
mod project_tests;
|
mod project_tests;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||||
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
|
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
|
||||||
@ -35,7 +35,6 @@ use lsp::{
|
|||||||
};
|
};
|
||||||
use lsp_command::*;
|
use lsp_command::*;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::stream::Stream;
|
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use search::SearchQuery;
|
use search::SearchQuery;
|
||||||
@ -74,7 +73,6 @@ pub trait Item: Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct ProjectStore {
|
pub struct ProjectStore {
|
||||||
db: Arc<Db>,
|
|
||||||
projects: Vec<WeakModelHandle<Project>>,
|
projects: Vec<WeakModelHandle<Project>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +125,6 @@ pub struct Project {
|
|||||||
buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>,
|
buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>,
|
||||||
buffers_being_formatted: HashSet<usize>,
|
buffers_being_formatted: HashSet<usize>,
|
||||||
nonce: u128,
|
nonce: u128,
|
||||||
initialized_persistent_state: bool,
|
|
||||||
_maintain_buffer_languages: Task<()>,
|
_maintain_buffer_languages: Task<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,13 +153,8 @@ enum WorktreeHandle {
|
|||||||
|
|
||||||
enum ProjectClientState {
|
enum ProjectClientState {
|
||||||
Local {
|
Local {
|
||||||
is_shared: bool,
|
remote_id: Option<u64>,
|
||||||
remote_id_tx: watch::Sender<Option<u64>>,
|
_detect_unshare: Task<Option<()>>,
|
||||||
remote_id_rx: watch::Receiver<Option<u64>>,
|
|
||||||
online_tx: watch::Sender<bool>,
|
|
||||||
online_rx: watch::Receiver<bool>,
|
|
||||||
_maintain_remote_id: Task<Option<()>>,
|
|
||||||
_maintain_online_status: Task<Option<()>>,
|
|
||||||
},
|
},
|
||||||
Remote {
|
Remote {
|
||||||
sharing_has_stopped: bool,
|
sharing_has_stopped: bool,
|
||||||
@ -174,7 +166,6 @@ enum ProjectClientState {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Collaborator {
|
pub struct Collaborator {
|
||||||
pub user: Arc<User>,
|
|
||||||
pub peer_id: PeerId,
|
pub peer_id: PeerId,
|
||||||
pub replica_id: ReplicaId,
|
pub replica_id: ReplicaId,
|
||||||
}
|
}
|
||||||
@ -197,8 +188,6 @@ pub enum Event {
|
|||||||
RemoteIdChanged(Option<u64>),
|
RemoteIdChanged(Option<u64>),
|
||||||
DisconnectedFromHost,
|
DisconnectedFromHost,
|
||||||
CollaboratorLeft(PeerId),
|
CollaboratorLeft(PeerId),
|
||||||
ContactRequestedJoin(Arc<User>),
|
|
||||||
ContactCancelledJoinRequest(Arc<User>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum LanguageServerState {
|
pub enum LanguageServerState {
|
||||||
@ -383,17 +372,14 @@ impl FormatTrigger {
|
|||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
pub fn init(client: &Arc<Client>) {
|
pub fn init(client: &Arc<Client>) {
|
||||||
client.add_model_message_handler(Self::handle_request_join_project);
|
|
||||||
client.add_model_message_handler(Self::handle_add_collaborator);
|
client.add_model_message_handler(Self::handle_add_collaborator);
|
||||||
client.add_model_message_handler(Self::handle_buffer_reloaded);
|
client.add_model_message_handler(Self::handle_buffer_reloaded);
|
||||||
client.add_model_message_handler(Self::handle_buffer_saved);
|
client.add_model_message_handler(Self::handle_buffer_saved);
|
||||||
client.add_model_message_handler(Self::handle_start_language_server);
|
client.add_model_message_handler(Self::handle_start_language_server);
|
||||||
client.add_model_message_handler(Self::handle_update_language_server);
|
client.add_model_message_handler(Self::handle_update_language_server);
|
||||||
client.add_model_message_handler(Self::handle_remove_collaborator);
|
client.add_model_message_handler(Self::handle_remove_collaborator);
|
||||||
client.add_model_message_handler(Self::handle_join_project_request_cancelled);
|
|
||||||
client.add_model_message_handler(Self::handle_update_project);
|
client.add_model_message_handler(Self::handle_update_project);
|
||||||
client.add_model_message_handler(Self::handle_unregister_project);
|
client.add_model_message_handler(Self::handle_unshare_project);
|
||||||
client.add_model_message_handler(Self::handle_project_unshared);
|
|
||||||
client.add_model_message_handler(Self::handle_create_buffer_for_peer);
|
client.add_model_message_handler(Self::handle_create_buffer_for_peer);
|
||||||
client.add_model_message_handler(Self::handle_update_buffer_file);
|
client.add_model_message_handler(Self::handle_update_buffer_file);
|
||||||
client.add_model_message_handler(Self::handle_update_buffer);
|
client.add_model_message_handler(Self::handle_update_buffer);
|
||||||
@ -426,7 +412,6 @@ impl Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn local(
|
pub fn local(
|
||||||
online: bool,
|
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
user_store: ModelHandle<UserStore>,
|
user_store: ModelHandle<UserStore>,
|
||||||
project_store: ModelHandle<ProjectStore>,
|
project_store: ModelHandle<ProjectStore>,
|
||||||
@ -435,41 +420,19 @@ impl Project {
|
|||||||
cx: &mut MutableAppContext,
|
cx: &mut MutableAppContext,
|
||||||
) -> ModelHandle<Self> {
|
) -> ModelHandle<Self> {
|
||||||
cx.add_model(|cx: &mut ModelContext<Self>| {
|
cx.add_model(|cx: &mut ModelContext<Self>| {
|
||||||
let (remote_id_tx, remote_id_rx) = watch::channel();
|
let mut status = client.status();
|
||||||
let _maintain_remote_id = cx.spawn_weak({
|
let _detect_unshare = cx.spawn_weak(move |this, mut cx| {
|
||||||
let mut status_rx = client.clone().status();
|
async move {
|
||||||
move |this, mut cx| async move {
|
let is_connected = status.next().await.map_or(false, |s| s.is_connected());
|
||||||
while let Some(status) = status_rx.recv().await {
|
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
|
||||||
let this = this.upgrade(&cx)?;
|
if !is_connected || status.next().await.is_some() {
|
||||||
if status.is_connected() {
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
this.update(&mut cx, |this, cx| this.register(cx))
|
let _ = this.update(&mut cx, |this, cx| this.unshare(cx));
|
||||||
.await
|
|
||||||
.log_err()?;
|
|
||||||
} else {
|
|
||||||
this.update(&mut cx, |this, cx| this.unregister(cx))
|
|
||||||
.await
|
|
||||||
.log_err();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
Ok(())
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let (online_tx, online_rx) = watch::channel_with(online);
|
|
||||||
let _maintain_online_status = cx.spawn_weak({
|
|
||||||
let mut online_rx = online_rx.clone();
|
|
||||||
move |this, mut cx| async move {
|
|
||||||
while let Some(online) = online_rx.recv().await {
|
|
||||||
let this = this.upgrade(&cx)?;
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
if !online {
|
|
||||||
this.unshared(cx);
|
|
||||||
}
|
|
||||||
this.metadata_changed(false, cx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
.log_err()
|
||||||
});
|
});
|
||||||
|
|
||||||
let handle = cx.weak_handle();
|
let handle = cx.weak_handle();
|
||||||
@ -485,13 +448,8 @@ impl Project {
|
|||||||
loading_local_worktrees: Default::default(),
|
loading_local_worktrees: Default::default(),
|
||||||
buffer_snapshots: Default::default(),
|
buffer_snapshots: Default::default(),
|
||||||
client_state: ProjectClientState::Local {
|
client_state: ProjectClientState::Local {
|
||||||
is_shared: false,
|
remote_id: None,
|
||||||
remote_id_tx,
|
_detect_unshare,
|
||||||
remote_id_rx,
|
|
||||||
online_tx,
|
|
||||||
online_rx,
|
|
||||||
_maintain_remote_id,
|
|
||||||
_maintain_online_status,
|
|
||||||
},
|
},
|
||||||
opened_buffer: watch::channel(),
|
opened_buffer: watch::channel(),
|
||||||
client_subscriptions: Vec::new(),
|
client_subscriptions: Vec::new(),
|
||||||
@ -513,7 +471,6 @@ impl Project {
|
|||||||
buffers_being_formatted: Default::default(),
|
buffers_being_formatted: Default::default(),
|
||||||
next_language_server_id: 0,
|
next_language_server_id: 0,
|
||||||
nonce: StdRng::from_entropy().gen(),
|
nonce: StdRng::from_entropy().gen(),
|
||||||
initialized_persistent_state: false,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -535,24 +492,6 @@ impl Project {
|
|||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let response = match response.variant.ok_or_else(|| anyhow!("missing variant"))? {
|
|
||||||
proto::join_project_response::Variant::Accept(response) => response,
|
|
||||||
proto::join_project_response::Variant::Decline(decline) => {
|
|
||||||
match proto::join_project_response::decline::Reason::from_i32(decline.reason) {
|
|
||||||
Some(proto::join_project_response::decline::Reason::Declined) => {
|
|
||||||
Err(JoinProjectError::HostDeclined)?
|
|
||||||
}
|
|
||||||
Some(proto::join_project_response::decline::Reason::Closed) => {
|
|
||||||
Err(JoinProjectError::HostClosedProject)?
|
|
||||||
}
|
|
||||||
Some(proto::join_project_response::decline::Reason::WentOffline) => {
|
|
||||||
Err(JoinProjectError::HostWentOffline)?
|
|
||||||
}
|
|
||||||
None => Err(anyhow!("missing decline reason"))?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let replica_id = response.replica_id as ReplicaId;
|
let replica_id = response.replica_id as ReplicaId;
|
||||||
|
|
||||||
let mut worktrees = Vec::new();
|
let mut worktrees = Vec::new();
|
||||||
@ -629,7 +568,6 @@ impl Project {
|
|||||||
buffers_being_formatted: Default::default(),
|
buffers_being_formatted: Default::default(),
|
||||||
buffer_snapshots: Default::default(),
|
buffer_snapshots: Default::default(),
|
||||||
nonce: StdRng::from_entropy().gen(),
|
nonce: StdRng::from_entropy().gen(),
|
||||||
initialized_persistent_state: false,
|
|
||||||
};
|
};
|
||||||
for worktree in worktrees {
|
for worktree in worktrees {
|
||||||
this.add_worktree(&worktree, cx);
|
this.add_worktree(&worktree, cx);
|
||||||
@ -647,7 +585,7 @@ impl Project {
|
|||||||
.await?;
|
.await?;
|
||||||
let mut collaborators = HashMap::default();
|
let mut collaborators = HashMap::default();
|
||||||
for message in response.collaborators {
|
for message in response.collaborators {
|
||||||
let collaborator = Collaborator::from_proto(message, &user_store, &mut cx).await?;
|
let collaborator = Collaborator::from_proto(message);
|
||||||
collaborators.insert(collaborator.peer_id, collaborator);
|
collaborators.insert(collaborator.peer_id, collaborator);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -672,10 +610,9 @@ impl Project {
|
|||||||
let http_client = client::test::FakeHttpClient::with_404_response();
|
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||||
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
|
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||||
let project_store = cx.add_model(|_| ProjectStore::new(Db::open_fake()));
|
let project_store = cx.add_model(|_| ProjectStore::new());
|
||||||
let project = cx.update(|cx| {
|
let project =
|
||||||
Project::local(true, client, user_store, project_store, languages, fs, cx)
|
cx.update(|cx| Project::local(client, user_store, project_store, languages, fs, cx));
|
||||||
});
|
|
||||||
for path in root_paths {
|
for path in root_paths {
|
||||||
let (tree, _) = project
|
let (tree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
@ -689,53 +626,6 @@ impl Project {
|
|||||||
project
|
project
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restore_state(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
|
||||||
if self.is_remote() {
|
|
||||||
return Task::ready(Ok(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let db = self.project_store.read(cx).db.clone();
|
|
||||||
let keys = self.db_keys_for_online_state(cx);
|
|
||||||
let online_by_default = cx.global::<Settings>().projects_online_by_default;
|
|
||||||
let read_online = cx.background().spawn(async move {
|
|
||||||
let values = db.read(keys)?;
|
|
||||||
anyhow::Ok(
|
|
||||||
values
|
|
||||||
.into_iter()
|
|
||||||
.all(|e| e.map_or(online_by_default, |e| e == [true as u8])),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
|
||||||
let online = read_online.await.log_err().unwrap_or(false);
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.initialized_persistent_state = true;
|
|
||||||
if let ProjectClientState::Local { online_tx, .. } = &mut this.client_state {
|
|
||||||
let mut online_tx = online_tx.borrow_mut();
|
|
||||||
if *online_tx != online {
|
|
||||||
*online_tx = online;
|
|
||||||
drop(online_tx);
|
|
||||||
this.metadata_changed(false, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn persist_state(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
|
||||||
if self.is_remote() || !self.initialized_persistent_state {
|
|
||||||
return Task::ready(Ok(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let db = self.project_store.read(cx).db.clone();
|
|
||||||
let keys = self.db_keys_for_online_state(cx);
|
|
||||||
let is_online = self.is_online();
|
|
||||||
cx.background().spawn(async move {
|
|
||||||
let value = &[is_online as u8];
|
|
||||||
db.write(keys.into_iter().map(|key| (key, value)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
|
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
|
|
||||||
@ -864,136 +754,9 @@ impl Project {
|
|||||||
&self.fs
|
&self.fs
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_online(&mut self, online: bool, _: &mut ModelContext<Self>) {
|
|
||||||
if let ProjectClientState::Local { online_tx, .. } = &mut self.client_state {
|
|
||||||
let mut online_tx = online_tx.borrow_mut();
|
|
||||||
if *online_tx != online {
|
|
||||||
*online_tx = online;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_online(&self) -> bool {
|
|
||||||
match &self.client_state {
|
|
||||||
ProjectClientState::Local { online_rx, .. } => *online_rx.borrow(),
|
|
||||||
ProjectClientState::Remote { .. } => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unregister(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
|
||||||
self.unshared(cx);
|
|
||||||
if let ProjectClientState::Local { remote_id_rx, .. } = &mut self.client_state {
|
|
||||||
if let Some(remote_id) = *remote_id_rx.borrow() {
|
|
||||||
let request = self.client.request(proto::UnregisterProject {
|
|
||||||
project_id: remote_id,
|
|
||||||
});
|
|
||||||
return cx.spawn(|this, mut cx| async move {
|
|
||||||
let response = request.await;
|
|
||||||
|
|
||||||
// Unregistering the project causes the server to send out a
|
|
||||||
// contact update removing this project from the host's list
|
|
||||||
// of online projects. Wait until this contact update has been
|
|
||||||
// processed before clearing out this project's remote id, so
|
|
||||||
// that there is no moment where this project appears in the
|
|
||||||
// contact metadata and *also* has no remote id.
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.user_store()
|
|
||||||
.update(cx, |store, _| store.contact_updates_done())
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
if let ProjectClientState::Local { remote_id_tx, .. } =
|
|
||||||
&mut this.client_state
|
|
||||||
{
|
|
||||||
*remote_id_tx.borrow_mut() = None;
|
|
||||||
}
|
|
||||||
this.client_subscriptions.clear();
|
|
||||||
this.metadata_changed(false, cx);
|
|
||||||
});
|
|
||||||
response.map(drop)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Task::ready(Ok(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
|
||||||
if let ProjectClientState::Local {
|
|
||||||
remote_id_rx,
|
|
||||||
online_rx,
|
|
||||||
..
|
|
||||||
} = &self.client_state
|
|
||||||
{
|
|
||||||
if remote_id_rx.borrow().is_some() {
|
|
||||||
return Task::ready(Ok(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = self.client.request(proto::RegisterProject {
|
|
||||||
online: *online_rx.borrow(),
|
|
||||||
});
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
|
||||||
let remote_id = response.await?.project_id;
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state {
|
|
||||||
*remote_id_tx.borrow_mut() = Some(remote_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.metadata_changed(false, cx);
|
|
||||||
cx.emit(Event::RemoteIdChanged(Some(remote_id)));
|
|
||||||
this.client_subscriptions
|
|
||||||
.push(this.client.add_model_for_remote_entity(remote_id, cx));
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Task::ready(Err(anyhow!("can't register a remote project")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remote_id(&self) -> Option<u64> {
|
pub fn remote_id(&self) -> Option<u64> {
|
||||||
match &self.client_state {
|
match &self.client_state {
|
||||||
ProjectClientState::Local { remote_id_rx, .. } => *remote_id_rx.borrow(),
|
ProjectClientState::Local { remote_id, .. } => *remote_id,
|
||||||
ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_remote_id(&self) -> impl Future<Output = u64> {
|
|
||||||
let mut id = None;
|
|
||||||
let mut watch = None;
|
|
||||||
match &self.client_state {
|
|
||||||
ProjectClientState::Local { remote_id_rx, .. } => watch = Some(remote_id_rx.clone()),
|
|
||||||
ProjectClientState::Remote { remote_id, .. } => id = Some(*remote_id),
|
|
||||||
}
|
|
||||||
|
|
||||||
async move {
|
|
||||||
if let Some(id) = id {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
let mut watch = watch.unwrap();
|
|
||||||
loop {
|
|
||||||
let id = *watch.borrow();
|
|
||||||
if let Some(id) = id {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
watch.next().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shared_remote_id(&self) -> Option<u64> {
|
|
||||||
match &self.client_state {
|
|
||||||
ProjectClientState::Local {
|
|
||||||
remote_id_rx,
|
|
||||||
is_shared,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if *is_shared {
|
|
||||||
*remote_id_rx.borrow()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
|
ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1005,65 +768,50 @@ impl Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metadata_changed(&mut self, persist: bool, cx: &mut ModelContext<Self>) {
|
fn metadata_changed(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
if let ProjectClientState::Local {
|
if let ProjectClientState::Local { remote_id, .. } = &self.client_state {
|
||||||
remote_id_rx,
|
|
||||||
online_rx,
|
|
||||||
..
|
|
||||||
} = &self.client_state
|
|
||||||
{
|
|
||||||
// Broadcast worktrees only if the project is online.
|
// Broadcast worktrees only if the project is online.
|
||||||
let worktrees = if *online_rx.borrow() {
|
let worktrees = self
|
||||||
self.worktrees
|
.worktrees
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|worktree| {
|
.filter_map(|worktree| {
|
||||||
worktree
|
worktree
|
||||||
.upgrade(cx)
|
.upgrade(cx)
|
||||||
.map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto())
|
.map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto())
|
||||||
})
|
})
|
||||||
.collect()
|
.collect();
|
||||||
} else {
|
if let Some(project_id) = *remote_id {
|
||||||
Default::default()
|
|
||||||
};
|
|
||||||
if let Some(project_id) = *remote_id_rx.borrow() {
|
|
||||||
let online = *online_rx.borrow();
|
|
||||||
self.client
|
self.client
|
||||||
.send(proto::UpdateProject {
|
.send(proto::UpdateProject {
|
||||||
project_id,
|
project_id,
|
||||||
worktrees,
|
worktrees,
|
||||||
online,
|
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
|
|
||||||
if online {
|
let worktrees = self.visible_worktrees(cx).collect::<Vec<_>>();
|
||||||
let worktrees = self.visible_worktrees(cx).collect::<Vec<_>>();
|
let scans_complete =
|
||||||
let scans_complete =
|
futures::future::join_all(worktrees.iter().filter_map(|worktree| {
|
||||||
futures::future::join_all(worktrees.iter().filter_map(|worktree| {
|
Some(worktree.read(cx).as_local()?.scan_complete())
|
||||||
Some(worktree.read(cx).as_local()?.scan_complete())
|
}));
|
||||||
}));
|
|
||||||
|
|
||||||
let worktrees = worktrees.into_iter().map(|handle| handle.downgrade());
|
let worktrees = worktrees.into_iter().map(|handle| handle.downgrade());
|
||||||
cx.spawn_weak(move |_, cx| async move {
|
cx.spawn_weak(move |_, cx| async move {
|
||||||
scans_complete.await;
|
scans_complete.await;
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
for worktree in worktrees {
|
for worktree in worktrees {
|
||||||
if let Some(worktree) = worktree
|
if let Some(worktree) = worktree
|
||||||
.upgrade(cx)
|
.upgrade(cx)
|
||||||
.and_then(|worktree| worktree.read(cx).as_local())
|
.and_then(|worktree| worktree.read(cx).as_local())
|
||||||
{
|
{
|
||||||
worktree.send_extension_counts(project_id);
|
worktree.send_extension_counts(project_id);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
})
|
||||||
}
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.project_store.update(cx, |_, cx| cx.notify());
|
self.project_store.update(cx, |_, cx| cx.notify());
|
||||||
if persist {
|
|
||||||
self.persist_state(cx).detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1101,23 +849,6 @@ impl Project {
|
|||||||
.map(|tree| tree.read(cx).root_name())
|
.map(|tree| tree.read(cx).root_name())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn db_keys_for_online_state(&self, cx: &AppContext) -> Vec<String> {
|
|
||||||
self.worktrees
|
|
||||||
.iter()
|
|
||||||
.filter_map(|worktree| {
|
|
||||||
let worktree = worktree.upgrade(cx)?.read(cx);
|
|
||||||
if worktree.is_visible() {
|
|
||||||
Some(format!(
|
|
||||||
"project-path-online:{}",
|
|
||||||
worktree.as_local().unwrap().abs_path().to_string_lossy()
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn worktree_for_id(
|
pub fn worktree_for_id(
|
||||||
&self,
|
&self,
|
||||||
id: WorktreeId,
|
id: WorktreeId,
|
||||||
@ -1321,142 +1052,106 @@ impl Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn share(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
if !self.is_online() {
|
if let ProjectClientState::Local { remote_id, .. } = &mut self.client_state {
|
||||||
return Task::ready(Err(anyhow!("can't share an offline project")));
|
if remote_id.is_some() {
|
||||||
}
|
return Task::ready(Err(anyhow!("project was already shared")));
|
||||||
|
|
||||||
let project_id;
|
|
||||||
if let ProjectClientState::Local {
|
|
||||||
remote_id_rx,
|
|
||||||
is_shared,
|
|
||||||
..
|
|
||||||
} = &mut self.client_state
|
|
||||||
{
|
|
||||||
if *is_shared {
|
|
||||||
return Task::ready(Ok(()));
|
|
||||||
}
|
|
||||||
*is_shared = true;
|
|
||||||
if let Some(id) = *remote_id_rx.borrow() {
|
|
||||||
project_id = id;
|
|
||||||
} else {
|
|
||||||
return Task::ready(Err(anyhow!("project hasn't been registered")));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Task::ready(Err(anyhow!("can't share a remote project")));
|
|
||||||
};
|
|
||||||
|
|
||||||
for open_buffer in self.opened_buffers.values_mut() {
|
|
||||||
match open_buffer {
|
|
||||||
OpenBuffer::Strong(_) => {}
|
|
||||||
OpenBuffer::Weak(buffer) => {
|
|
||||||
if let Some(buffer) = buffer.upgrade(cx) {
|
|
||||||
*open_buffer = OpenBuffer::Strong(buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OpenBuffer::Operations(_) => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for worktree_handle in self.worktrees.iter_mut() {
|
|
||||||
match worktree_handle {
|
|
||||||
WorktreeHandle::Strong(_) => {}
|
|
||||||
WorktreeHandle::Weak(worktree) => {
|
|
||||||
if let Some(worktree) = worktree.upgrade(cx) {
|
|
||||||
*worktree_handle = WorktreeHandle::Strong(worktree);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut tasks = Vec::new();
|
|
||||||
for worktree in self.worktrees(cx).collect::<Vec<_>>() {
|
|
||||||
worktree.update(cx, |worktree, cx| {
|
|
||||||
let worktree = worktree.as_local_mut().unwrap();
|
|
||||||
tasks.push(worktree.share(project_id, cx));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (server_id, status) in &self.language_server_statuses {
|
|
||||||
self.client
|
|
||||||
.send(proto::StartLanguageServer {
|
|
||||||
project_id,
|
|
||||||
server: Some(proto::LanguageServer {
|
|
||||||
id: *server_id as u64,
|
|
||||||
name: status.name.clone(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
|
||||||
for task in tasks {
|
|
||||||
task.await?;
|
|
||||||
}
|
|
||||||
this.update(&mut cx, |_, cx| cx.notify());
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unshared(&mut self, cx: &mut ModelContext<Self>) {
|
|
||||||
if let ProjectClientState::Local { is_shared, .. } = &mut self.client_state {
|
|
||||||
if !*is_shared {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*is_shared = false;
|
*remote_id = Some(project_id);
|
||||||
self.collaborators.clear();
|
|
||||||
self.shared_buffers.clear();
|
let mut worktree_share_tasks = Vec::new();
|
||||||
for worktree_handle in self.worktrees.iter_mut() {
|
|
||||||
if let WorktreeHandle::Strong(worktree) = worktree_handle {
|
|
||||||
let is_visible = worktree.update(cx, |worktree, _| {
|
|
||||||
worktree.as_local_mut().unwrap().unshare();
|
|
||||||
worktree.is_visible()
|
|
||||||
});
|
|
||||||
if !is_visible {
|
|
||||||
*worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for open_buffer in self.opened_buffers.values_mut() {
|
for open_buffer in self.opened_buffers.values_mut() {
|
||||||
if let OpenBuffer::Strong(buffer) = open_buffer {
|
match open_buffer {
|
||||||
*open_buffer = OpenBuffer::Weak(buffer.downgrade());
|
OpenBuffer::Strong(_) => {}
|
||||||
|
OpenBuffer::Weak(buffer) => {
|
||||||
|
if let Some(buffer) = buffer.upgrade(cx) {
|
||||||
|
*open_buffer = OpenBuffer::Strong(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenBuffer::Operations(_) => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for worktree_handle in self.worktrees.iter_mut() {
|
||||||
|
match worktree_handle {
|
||||||
|
WorktreeHandle::Strong(_) => {}
|
||||||
|
WorktreeHandle::Weak(worktree) => {
|
||||||
|
if let Some(worktree) = worktree.upgrade(cx) {
|
||||||
|
*worktree_handle = WorktreeHandle::Strong(worktree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for worktree in self.worktrees(cx).collect::<Vec<_>>() {
|
||||||
|
worktree.update(cx, |worktree, cx| {
|
||||||
|
let worktree = worktree.as_local_mut().unwrap();
|
||||||
|
worktree_share_tasks.push(worktree.share(project_id, cx));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (server_id, status) in &self.language_server_statuses {
|
||||||
|
self.client
|
||||||
|
.send(proto::StartLanguageServer {
|
||||||
|
project_id,
|
||||||
|
server: Some(proto::LanguageServer {
|
||||||
|
id: *server_id as u64,
|
||||||
|
name: status.name.clone(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.client_subscriptions
|
||||||
|
.push(self.client.add_model_for_remote_entity(project_id, cx));
|
||||||
|
self.metadata_changed(cx);
|
||||||
|
cx.emit(Event::RemoteIdChanged(Some(project_id)));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
|
cx.foreground().spawn(async move {
|
||||||
|
futures::future::try_join_all(worktree_share_tasks).await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
log::error!("attempted to unshare a remote project");
|
Task::ready(Err(anyhow!("can't share a remote project")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn respond_to_join_request(
|
pub fn unshare(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||||
&mut self,
|
if let ProjectClientState::Local { remote_id, .. } = &mut self.client_state {
|
||||||
requester_id: u64,
|
if let Some(project_id) = remote_id.take() {
|
||||||
allow: bool,
|
self.collaborators.clear();
|
||||||
cx: &mut ModelContext<Self>,
|
self.shared_buffers.clear();
|
||||||
) {
|
self.client_subscriptions.clear();
|
||||||
if let Some(project_id) = self.remote_id() {
|
|
||||||
let share = if self.is_online() && allow {
|
for worktree_handle in self.worktrees.iter_mut() {
|
||||||
Some(self.share(cx))
|
if let WorktreeHandle::Strong(worktree) = worktree_handle {
|
||||||
} else {
|
let is_visible = worktree.update(cx, |worktree, _| {
|
||||||
None
|
worktree.as_local_mut().unwrap().unshare();
|
||||||
};
|
worktree.is_visible()
|
||||||
let client = self.client.clone();
|
});
|
||||||
cx.foreground()
|
if !is_visible {
|
||||||
.spawn(async move {
|
*worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
|
||||||
client.send(proto::RespondToJoinProjectRequest {
|
}
|
||||||
requester_id,
|
|
||||||
project_id,
|
|
||||||
allow,
|
|
||||||
})?;
|
|
||||||
if let Some(share) = share {
|
|
||||||
share.await?;
|
|
||||||
}
|
}
|
||||||
anyhow::Ok(())
|
}
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
for open_buffer in self.opened_buffers.values_mut() {
|
||||||
|
if let OpenBuffer::Strong(buffer) = open_buffer {
|
||||||
|
*open_buffer = OpenBuffer::Weak(buffer.downgrade());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.metadata_changed(cx);
|
||||||
|
cx.notify();
|
||||||
|
self.client.send(proto::UnshareProject { project_id })?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("attempted to unshare a remote project"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1930,7 +1625,7 @@ impl Project {
|
|||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
match event {
|
match event {
|
||||||
BufferEvent::Operation(operation) => {
|
BufferEvent::Operation(operation) => {
|
||||||
if let Some(project_id) = self.shared_remote_id() {
|
if let Some(project_id) = self.remote_id() {
|
||||||
let request = self.client.request(proto::UpdateBuffer {
|
let request = self.client.request(proto::UpdateBuffer {
|
||||||
project_id,
|
project_id,
|
||||||
buffer_id: buffer.read(cx).remote_id(),
|
buffer_id: buffer.read(cx).remote_id(),
|
||||||
@ -2335,7 +2030,7 @@ impl Project {
|
|||||||
)
|
)
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
if let Some(project_id) = this.shared_remote_id() {
|
if let Some(project_id) = this.remote_id() {
|
||||||
this.client
|
this.client
|
||||||
.send(proto::StartLanguageServer {
|
.send(proto::StartLanguageServer {
|
||||||
project_id,
|
project_id,
|
||||||
@ -2742,7 +2437,7 @@ impl Project {
|
|||||||
language_server_id: usize,
|
language_server_id: usize,
|
||||||
event: proto::update_language_server::Variant,
|
event: proto::update_language_server::Variant,
|
||||||
) {
|
) {
|
||||||
if let Some(project_id) = self.shared_remote_id() {
|
if let Some(project_id) = self.remote_id() {
|
||||||
self.client
|
self.client
|
||||||
.send(proto::UpdateLanguageServer {
|
.send(proto::UpdateLanguageServer {
|
||||||
project_id,
|
project_id,
|
||||||
@ -4472,7 +4167,7 @@ impl Project {
|
|||||||
|
|
||||||
pub fn is_shared(&self) -> bool {
|
pub fn is_shared(&self) -> bool {
|
||||||
match &self.client_state {
|
match &self.client_state {
|
||||||
ProjectClientState::Local { is_shared, .. } => *is_shared,
|
ProjectClientState::Local { remote_id, .. } => remote_id.is_some(),
|
||||||
ProjectClientState::Remote { .. } => false,
|
ProjectClientState::Remote { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4509,7 +4204,7 @@ impl Project {
|
|||||||
|
|
||||||
let project_id = project.update(&mut cx, |project, cx| {
|
let project_id = project.update(&mut cx, |project, cx| {
|
||||||
project.add_worktree(&worktree, cx);
|
project.add_worktree(&worktree, cx);
|
||||||
project.shared_remote_id()
|
project.remote_id()
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(project_id) = project_id {
|
if let Some(project_id) = project_id {
|
||||||
@ -4550,7 +4245,7 @@ impl Project {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
self.metadata_changed(true, cx);
|
self.metadata_changed(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4578,7 +4273,7 @@ impl Project {
|
|||||||
.push(WorktreeHandle::Weak(worktree.downgrade()));
|
.push(WorktreeHandle::Weak(worktree.downgrade()));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.metadata_changed(true, cx);
|
self.metadata_changed(cx);
|
||||||
cx.observe_release(worktree, |this, worktree, cx| {
|
cx.observe_release(worktree, |this, worktree, cx| {
|
||||||
this.remove_worktree(worktree.id(), cx);
|
this.remove_worktree(worktree.id(), cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@ -4641,7 +4336,7 @@ impl Project {
|
|||||||
renamed_buffers.push((cx.handle(), old_path));
|
renamed_buffers.push((cx.handle(), old_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(project_id) = self.shared_remote_id() {
|
if let Some(project_id) = self.remote_id() {
|
||||||
self.client
|
self.client
|
||||||
.send(proto::UpdateBufferFile {
|
.send(proto::UpdateBufferFile {
|
||||||
project_id,
|
project_id,
|
||||||
@ -4697,7 +4392,7 @@ impl Project {
|
|||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let shared_remote_id = self.shared_remote_id();
|
let remote_id = self.remote_id();
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
|
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
@ -4711,7 +4406,7 @@ impl Project {
|
|||||||
buffer.remote_id()
|
buffer.remote_id()
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(project_id) = shared_remote_id {
|
if let Some(project_id) = remote_id {
|
||||||
client
|
client
|
||||||
.send(proto::UpdateDiffBase {
|
.send(proto::UpdateDiffBase {
|
||||||
project_id,
|
project_id,
|
||||||
@ -4811,47 +4506,20 @@ impl Project {
|
|||||||
|
|
||||||
// RPC message handlers
|
// RPC message handlers
|
||||||
|
|
||||||
async fn handle_request_join_project(
|
async fn handle_unshare_project(
|
||||||
this: ModelHandle<Self>,
|
this: ModelHandle<Self>,
|
||||||
message: TypedEnvelope<proto::RequestJoinProject>,
|
_: TypedEnvelope<proto::UnshareProject>,
|
||||||
_: Arc<Client>,
|
_: Arc<Client>,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let user_id = message.payload.requester_id;
|
this.update(&mut cx, |this, cx| {
|
||||||
if this.read_with(&cx, |project, _| {
|
if this.is_local() {
|
||||||
project.collaborators.values().any(|c| c.user.id == user_id)
|
this.unshare(cx)?;
|
||||||
}) {
|
} else {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.disconnected_from_host(cx);
|
||||||
this.respond_to_join_request(user_id, true, cx)
|
}
|
||||||
});
|
Ok(())
|
||||||
} else {
|
})
|
||||||
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
|
|
||||||
let user = user_store
|
|
||||||
.update(&mut cx, |store, cx| store.fetch_user(user_id, cx))
|
|
||||||
.await?;
|
|
||||||
this.update(&mut cx, |_, cx| cx.emit(Event::ContactRequestedJoin(user)));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_unregister_project(
|
|
||||||
this: ModelHandle<Self>,
|
|
||||||
_: TypedEnvelope<proto::UnregisterProject>,
|
|
||||||
_: Arc<Client>,
|
|
||||||
mut cx: AsyncAppContext,
|
|
||||||
) -> Result<()> {
|
|
||||||
this.update(&mut cx, |this, cx| this.disconnected_from_host(cx));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_project_unshared(
|
|
||||||
this: ModelHandle<Self>,
|
|
||||||
_: TypedEnvelope<proto::ProjectUnshared>,
|
|
||||||
_: Arc<Client>,
|
|
||||||
mut cx: AsyncAppContext,
|
|
||||||
) -> Result<()> {
|
|
||||||
this.update(&mut cx, |this, cx| this.unshared(cx));
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_add_collaborator(
|
async fn handle_add_collaborator(
|
||||||
@ -4860,14 +4528,13 @@ impl Project {
|
|||||||
_: Arc<Client>,
|
_: Arc<Client>,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
|
|
||||||
let collaborator = envelope
|
let collaborator = envelope
|
||||||
.payload
|
.payload
|
||||||
.collaborator
|
.collaborator
|
||||||
.take()
|
.take()
|
||||||
.ok_or_else(|| anyhow!("empty collaborator"))?;
|
.ok_or_else(|| anyhow!("empty collaborator"))?;
|
||||||
|
|
||||||
let collaborator = Collaborator::from_proto(collaborator, &user_store, &mut cx).await?;
|
let collaborator = Collaborator::from_proto(collaborator);
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.collaborators
|
this.collaborators
|
||||||
.insert(collaborator.peer_id, collaborator);
|
.insert(collaborator.peer_id, collaborator);
|
||||||
@ -4902,27 +4569,6 @@ impl Project {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_join_project_request_cancelled(
|
|
||||||
this: ModelHandle<Self>,
|
|
||||||
envelope: TypedEnvelope<proto::JoinProjectRequestCancelled>,
|
|
||||||
_: Arc<Client>,
|
|
||||||
mut cx: AsyncAppContext,
|
|
||||||
) -> Result<()> {
|
|
||||||
let user = this
|
|
||||||
.update(&mut cx, |this, cx| {
|
|
||||||
this.user_store.update(cx, |user_store, cx| {
|
|
||||||
user_store.fetch_user(envelope.payload.requester_id, cx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
this.update(&mut cx, |_, cx| {
|
|
||||||
cx.emit(Event::ContactCancelledJoinRequest(user));
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_update_project(
|
async fn handle_update_project(
|
||||||
this: ModelHandle<Self>,
|
this: ModelHandle<Self>,
|
||||||
envelope: TypedEnvelope<proto::UpdateProject>,
|
envelope: TypedEnvelope<proto::UpdateProject>,
|
||||||
@ -4954,7 +4600,7 @@ impl Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.metadata_changed(true, cx);
|
this.metadata_changed(cx);
|
||||||
for (id, _) in old_worktrees_by_id {
|
for (id, _) in old_worktrees_by_id {
|
||||||
cx.emit(Event::WorktreeRemoved(id));
|
cx.emit(Event::WorktreeRemoved(id));
|
||||||
}
|
}
|
||||||
@ -6182,9 +5828,8 @@ impl Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectStore {
|
impl ProjectStore {
|
||||||
pub fn new(db: Arc<Db>) -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
db,
|
|
||||||
projects: Default::default(),
|
projects: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6313,10 +5958,10 @@ impl Entity for Project {
|
|||||||
self.project_store.update(cx, ProjectStore::prune_projects);
|
self.project_store.update(cx, ProjectStore::prune_projects);
|
||||||
|
|
||||||
match &self.client_state {
|
match &self.client_state {
|
||||||
ProjectClientState::Local { remote_id_rx, .. } => {
|
ProjectClientState::Local { remote_id, .. } => {
|
||||||
if let Some(project_id) = *remote_id_rx.borrow() {
|
if let Some(project_id) = *remote_id {
|
||||||
self.client
|
self.client
|
||||||
.send(proto::UnregisterProject { project_id })
|
.send(proto::UnshareProject { project_id })
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6357,21 +6002,10 @@ impl Entity for Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Collaborator {
|
impl Collaborator {
|
||||||
fn from_proto(
|
fn from_proto(message: proto::Collaborator) -> Self {
|
||||||
message: proto::Collaborator,
|
Self {
|
||||||
user_store: &ModelHandle<UserStore>,
|
peer_id: PeerId(message.peer_id),
|
||||||
cx: &mut AsyncAppContext,
|
replica_id: message.replica_id as ReplicaId,
|
||||||
) -> impl Future<Output = Result<Self>> {
|
|
||||||
let user = user_store.update(cx, |user_store, cx| {
|
|
||||||
user_store.fetch_user(message.user_id, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
async move {
|
|
||||||
Ok(Self {
|
|
||||||
peer_id: PeerId(message.peer_id),
|
|
||||||
user: user.await?,
|
|
||||||
replica_id: message.replica_id as ReplicaId,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,107 +10,116 @@ message Envelope {
|
|||||||
Error error = 5;
|
Error error = 5;
|
||||||
Ping ping = 6;
|
Ping ping = 6;
|
||||||
Test test = 7;
|
Test test = 7;
|
||||||
|
|
||||||
|
CreateRoom create_room = 8;
|
||||||
|
CreateRoomResponse create_room_response = 9;
|
||||||
|
JoinRoom join_room = 10;
|
||||||
|
JoinRoomResponse join_room_response = 11;
|
||||||
|
LeaveRoom leave_room = 12;
|
||||||
|
Call call = 13;
|
||||||
|
IncomingCall incoming_call = 14;
|
||||||
|
CallCanceled call_canceled = 15;
|
||||||
|
CancelCall cancel_call = 16;
|
||||||
|
DeclineCall decline_call = 17;
|
||||||
|
UpdateParticipantLocation update_participant_location = 18;
|
||||||
|
RoomUpdated room_updated = 19;
|
||||||
|
|
||||||
RegisterProject register_project = 8;
|
ShareProject share_project = 20;
|
||||||
RegisterProjectResponse register_project_response = 9;
|
ShareProjectResponse share_project_response = 21;
|
||||||
UnregisterProject unregister_project = 10;
|
UnshareProject unshare_project = 22;
|
||||||
RequestJoinProject request_join_project = 11;
|
JoinProject join_project = 23;
|
||||||
RespondToJoinProjectRequest respond_to_join_project_request = 12;
|
JoinProjectResponse join_project_response = 24;
|
||||||
JoinProjectRequestCancelled join_project_request_cancelled = 13;
|
LeaveProject leave_project = 25;
|
||||||
JoinProject join_project = 14;
|
AddProjectCollaborator add_project_collaborator = 26;
|
||||||
JoinProjectResponse join_project_response = 15;
|
RemoveProjectCollaborator remove_project_collaborator = 27;
|
||||||
LeaveProject leave_project = 16;
|
|
||||||
AddProjectCollaborator add_project_collaborator = 17;
|
|
||||||
RemoveProjectCollaborator remove_project_collaborator = 18;
|
|
||||||
ProjectUnshared project_unshared = 19;
|
|
||||||
|
|
||||||
GetDefinition get_definition = 20;
|
GetDefinition get_definition = 28;
|
||||||
GetDefinitionResponse get_definition_response = 21;
|
GetDefinitionResponse get_definition_response = 29;
|
||||||
GetTypeDefinition get_type_definition = 22;
|
GetTypeDefinition get_type_definition = 30;
|
||||||
GetTypeDefinitionResponse get_type_definition_response = 23;
|
GetTypeDefinitionResponse get_type_definition_response = 31;
|
||||||
GetReferences get_references = 24;
|
GetReferences get_references = 32;
|
||||||
GetReferencesResponse get_references_response = 25;
|
GetReferencesResponse get_references_response = 33;
|
||||||
GetDocumentHighlights get_document_highlights = 26;
|
GetDocumentHighlights get_document_highlights = 34;
|
||||||
GetDocumentHighlightsResponse get_document_highlights_response = 27;
|
GetDocumentHighlightsResponse get_document_highlights_response = 35;
|
||||||
GetProjectSymbols get_project_symbols = 28;
|
GetProjectSymbols get_project_symbols = 36;
|
||||||
GetProjectSymbolsResponse get_project_symbols_response = 29;
|
GetProjectSymbolsResponse get_project_symbols_response = 37;
|
||||||
OpenBufferForSymbol open_buffer_for_symbol = 30;
|
OpenBufferForSymbol open_buffer_for_symbol = 38;
|
||||||
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 31;
|
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 39;
|
||||||
|
|
||||||
UpdateProject update_project = 32;
|
UpdateProject update_project = 40;
|
||||||
RegisterProjectActivity register_project_activity = 33;
|
RegisterProjectActivity register_project_activity = 41;
|
||||||
UpdateWorktree update_worktree = 34;
|
UpdateWorktree update_worktree = 42;
|
||||||
UpdateWorktreeExtensions update_worktree_extensions = 35;
|
UpdateWorktreeExtensions update_worktree_extensions = 43;
|
||||||
|
|
||||||
CreateProjectEntry create_project_entry = 36;
|
CreateProjectEntry create_project_entry = 44;
|
||||||
RenameProjectEntry rename_project_entry = 37;
|
RenameProjectEntry rename_project_entry = 45;
|
||||||
CopyProjectEntry copy_project_entry = 38;
|
CopyProjectEntry copy_project_entry = 46;
|
||||||
DeleteProjectEntry delete_project_entry = 39;
|
DeleteProjectEntry delete_project_entry = 47;
|
||||||
ProjectEntryResponse project_entry_response = 40;
|
ProjectEntryResponse project_entry_response = 48;
|
||||||
|
|
||||||
UpdateDiagnosticSummary update_diagnostic_summary = 41;
|
UpdateDiagnosticSummary update_diagnostic_summary = 49;
|
||||||
StartLanguageServer start_language_server = 42;
|
StartLanguageServer start_language_server = 50;
|
||||||
UpdateLanguageServer update_language_server = 43;
|
UpdateLanguageServer update_language_server = 51;
|
||||||
|
|
||||||
OpenBufferById open_buffer_by_id = 44;
|
OpenBufferById open_buffer_by_id = 52;
|
||||||
OpenBufferByPath open_buffer_by_path = 45;
|
OpenBufferByPath open_buffer_by_path = 53;
|
||||||
OpenBufferResponse open_buffer_response = 46;
|
OpenBufferResponse open_buffer_response = 54;
|
||||||
CreateBufferForPeer create_buffer_for_peer = 47;
|
CreateBufferForPeer create_buffer_for_peer = 55;
|
||||||
UpdateBuffer update_buffer = 48;
|
UpdateBuffer update_buffer = 56;
|
||||||
UpdateBufferFile update_buffer_file = 49;
|
UpdateBufferFile update_buffer_file = 57;
|
||||||
SaveBuffer save_buffer = 50;
|
SaveBuffer save_buffer = 58;
|
||||||
BufferSaved buffer_saved = 51;
|
BufferSaved buffer_saved = 59;
|
||||||
BufferReloaded buffer_reloaded = 52;
|
BufferReloaded buffer_reloaded = 60;
|
||||||
ReloadBuffers reload_buffers = 53;
|
ReloadBuffers reload_buffers = 61;
|
||||||
ReloadBuffersResponse reload_buffers_response = 54;
|
ReloadBuffersResponse reload_buffers_response = 62;
|
||||||
FormatBuffers format_buffers = 55;
|
FormatBuffers format_buffers = 63;
|
||||||
FormatBuffersResponse format_buffers_response = 56;
|
FormatBuffersResponse format_buffers_response = 64;
|
||||||
GetCompletions get_completions = 57;
|
GetCompletions get_completions = 65;
|
||||||
GetCompletionsResponse get_completions_response = 58;
|
GetCompletionsResponse get_completions_response = 66;
|
||||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 59;
|
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 67;
|
||||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 60;
|
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 68;
|
||||||
GetCodeActions get_code_actions = 61;
|
GetCodeActions get_code_actions = 69;
|
||||||
GetCodeActionsResponse get_code_actions_response = 62;
|
GetCodeActionsResponse get_code_actions_response = 70;
|
||||||
GetHover get_hover = 63;
|
GetHover get_hover = 71;
|
||||||
GetHoverResponse get_hover_response = 64;
|
GetHoverResponse get_hover_response = 72;
|
||||||
ApplyCodeAction apply_code_action = 65;
|
ApplyCodeAction apply_code_action = 73;
|
||||||
ApplyCodeActionResponse apply_code_action_response = 66;
|
ApplyCodeActionResponse apply_code_action_response = 74;
|
||||||
PrepareRename prepare_rename = 67;
|
PrepareRename prepare_rename = 75;
|
||||||
PrepareRenameResponse prepare_rename_response = 68;
|
PrepareRenameResponse prepare_rename_response = 76;
|
||||||
PerformRename perform_rename = 69;
|
PerformRename perform_rename = 77;
|
||||||
PerformRenameResponse perform_rename_response = 70;
|
PerformRenameResponse perform_rename_response = 78;
|
||||||
SearchProject search_project = 71;
|
SearchProject search_project = 79;
|
||||||
SearchProjectResponse search_project_response = 72;
|
SearchProjectResponse search_project_response = 80;
|
||||||
|
|
||||||
GetChannels get_channels = 73;
|
GetChannels get_channels = 81;
|
||||||
GetChannelsResponse get_channels_response = 74;
|
GetChannelsResponse get_channels_response = 82;
|
||||||
JoinChannel join_channel = 75;
|
JoinChannel join_channel = 83;
|
||||||
JoinChannelResponse join_channel_response = 76;
|
JoinChannelResponse join_channel_response = 84;
|
||||||
LeaveChannel leave_channel = 77;
|
LeaveChannel leave_channel = 85;
|
||||||
SendChannelMessage send_channel_message = 78;
|
SendChannelMessage send_channel_message = 86;
|
||||||
SendChannelMessageResponse send_channel_message_response = 79;
|
SendChannelMessageResponse send_channel_message_response = 87;
|
||||||
ChannelMessageSent channel_message_sent = 80;
|
ChannelMessageSent channel_message_sent = 88;
|
||||||
GetChannelMessages get_channel_messages = 81;
|
GetChannelMessages get_channel_messages = 89;
|
||||||
GetChannelMessagesResponse get_channel_messages_response = 82;
|
GetChannelMessagesResponse get_channel_messages_response = 90;
|
||||||
|
|
||||||
UpdateContacts update_contacts = 83;
|
UpdateContacts update_contacts = 91;
|
||||||
UpdateInviteInfo update_invite_info = 84;
|
UpdateInviteInfo update_invite_info = 92;
|
||||||
ShowContacts show_contacts = 85;
|
ShowContacts show_contacts = 93;
|
||||||
|
|
||||||
GetUsers get_users = 86;
|
GetUsers get_users = 94;
|
||||||
FuzzySearchUsers fuzzy_search_users = 87;
|
FuzzySearchUsers fuzzy_search_users = 95;
|
||||||
UsersResponse users_response = 88;
|
UsersResponse users_response = 96;
|
||||||
RequestContact request_contact = 89;
|
RequestContact request_contact = 97;
|
||||||
RespondToContactRequest respond_to_contact_request = 90;
|
RespondToContactRequest respond_to_contact_request = 98;
|
||||||
RemoveContact remove_contact = 91;
|
RemoveContact remove_contact = 99;
|
||||||
|
|
||||||
Follow follow = 92;
|
Follow follow = 100;
|
||||||
FollowResponse follow_response = 93;
|
FollowResponse follow_response = 101;
|
||||||
UpdateFollowers update_followers = 94;
|
UpdateFollowers update_followers = 102;
|
||||||
Unfollow unfollow = 95;
|
Unfollow unfollow = 103;
|
||||||
GetPrivateUserInfo get_private_user_info = 96;
|
GetPrivateUserInfo get_private_user_info = 104;
|
||||||
GetPrivateUserInfoResponse get_private_user_info_response = 97;
|
GetPrivateUserInfoResponse get_private_user_info_response = 105;
|
||||||
UpdateDiffBase update_diff_base = 98;
|
UpdateDiffBase update_diff_base = 106;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,70 +137,121 @@ message Test {
|
|||||||
uint64 id = 1;
|
uint64 id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RegisterProject {
|
message CreateRoom {}
|
||||||
bool online = 1;
|
|
||||||
|
message CreateRoomResponse {
|
||||||
|
uint64 id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RegisterProjectResponse {
|
message JoinRoom {
|
||||||
|
uint64 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message JoinRoomResponse {
|
||||||
|
Room room = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LeaveRoom {
|
||||||
|
uint64 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Room {
|
||||||
|
repeated Participant participants = 1;
|
||||||
|
repeated uint64 pending_participant_user_ids = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Participant {
|
||||||
|
uint64 user_id = 1;
|
||||||
|
uint32 peer_id = 2;
|
||||||
|
repeated ParticipantProject projects = 3;
|
||||||
|
ParticipantLocation location = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ParticipantProject {
|
||||||
|
uint64 id = 1;
|
||||||
|
repeated string worktree_root_names = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ParticipantLocation {
|
||||||
|
oneof variant {
|
||||||
|
SharedProject shared_project = 1;
|
||||||
|
UnsharedProject unshared_project = 2;
|
||||||
|
External external = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SharedProject {
|
||||||
|
uint64 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UnsharedProject {}
|
||||||
|
|
||||||
|
message External {}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Call {
|
||||||
|
uint64 room_id = 1;
|
||||||
|
uint64 recipient_user_id = 2;
|
||||||
|
optional uint64 initial_project_id = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IncomingCall {
|
||||||
|
uint64 room_id = 1;
|
||||||
|
uint64 caller_user_id = 2;
|
||||||
|
repeated uint64 participant_user_ids = 3;
|
||||||
|
optional ParticipantProject initial_project = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CallCanceled {}
|
||||||
|
|
||||||
|
message CancelCall {
|
||||||
|
uint64 room_id = 1;
|
||||||
|
uint64 recipient_user_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeclineCall {
|
||||||
|
uint64 room_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateParticipantLocation {
|
||||||
|
uint64 room_id = 1;
|
||||||
|
ParticipantLocation location = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RoomUpdated {
|
||||||
|
Room room = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ShareProject {
|
||||||
|
uint64 room_id = 1;
|
||||||
|
repeated WorktreeMetadata worktrees = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ShareProjectResponse {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UnregisterProject {
|
message UnshareProject {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UpdateProject {
|
message UpdateProject {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
repeated WorktreeMetadata worktrees = 2;
|
repeated WorktreeMetadata worktrees = 2;
|
||||||
bool online = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message RegisterProjectActivity {
|
message RegisterProjectActivity {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RequestJoinProject {
|
|
||||||
uint64 requester_id = 1;
|
|
||||||
uint64 project_id = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RespondToJoinProjectRequest {
|
|
||||||
uint64 requester_id = 1;
|
|
||||||
uint64 project_id = 2;
|
|
||||||
bool allow = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message JoinProjectRequestCancelled {
|
|
||||||
uint64 requester_id = 1;
|
|
||||||
uint64 project_id = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message JoinProject {
|
message JoinProject {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message JoinProjectResponse {
|
message JoinProjectResponse {
|
||||||
oneof variant {
|
uint32 replica_id = 1;
|
||||||
Accept accept = 1;
|
repeated WorktreeMetadata worktrees = 2;
|
||||||
Decline decline = 2;
|
repeated Collaborator collaborators = 3;
|
||||||
}
|
repeated LanguageServer language_servers = 4;
|
||||||
|
|
||||||
message Accept {
|
|
||||||
uint32 replica_id = 1;
|
|
||||||
repeated WorktreeMetadata worktrees = 2;
|
|
||||||
repeated Collaborator collaborators = 3;
|
|
||||||
repeated LanguageServer language_servers = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Decline {
|
|
||||||
Reason reason = 1;
|
|
||||||
|
|
||||||
enum Reason {
|
|
||||||
Declined = 0;
|
|
||||||
Closed = 1;
|
|
||||||
WentOffline = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message LeaveProject {
|
message LeaveProject {
|
||||||
@ -254,10 +314,6 @@ message RemoveProjectCollaborator {
|
|||||||
uint32 peer_id = 2;
|
uint32 peer_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ProjectUnshared {
|
|
||||||
uint64 project_id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GetDefinition {
|
message GetDefinition {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
uint64 buffer_id = 2;
|
uint64 buffer_id = 2;
|
||||||
@ -986,17 +1042,11 @@ message ChannelMessage {
|
|||||||
|
|
||||||
message Contact {
|
message Contact {
|
||||||
uint64 user_id = 1;
|
uint64 user_id = 1;
|
||||||
repeated ProjectMetadata projects = 2;
|
bool online = 2;
|
||||||
bool online = 3;
|
bool busy = 3;
|
||||||
bool should_notify = 4;
|
bool should_notify = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ProjectMetadata {
|
|
||||||
uint64 id = 1;
|
|
||||||
repeated string visible_worktree_root_names = 3;
|
|
||||||
repeated uint64 guests = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message WorktreeMetadata {
|
message WorktreeMetadata {
|
||||||
uint64 id = 1;
|
uint64 id = 1;
|
||||||
string root_name = 2;
|
string root_name = 2;
|
||||||
|
@ -33,7 +33,7 @@ impl fmt::Display for ConnectionId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||||
pub struct PeerId(pub u32);
|
pub struct PeerId(pub u32);
|
||||||
|
|
||||||
impl fmt::Display for PeerId {
|
impl fmt::Display for PeerId {
|
||||||
@ -394,7 +394,11 @@ impl Peer {
|
|||||||
send?;
|
send?;
|
||||||
let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?;
|
let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?;
|
||||||
if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
|
if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
|
||||||
Err(anyhow!("RPC request failed - {}", error.message))
|
Err(anyhow!(
|
||||||
|
"RPC request {} failed - {}",
|
||||||
|
T::NAME,
|
||||||
|
error.message
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
T::Response::from_envelope(response)
|
T::Response::from_envelope(response)
|
||||||
.ok_or_else(|| anyhow!("received response of the wrong type"))
|
.ok_or_else(|| anyhow!("received response of the wrong type"))
|
||||||
|
@ -83,11 +83,16 @@ messages!(
|
|||||||
(ApplyCompletionAdditionalEditsResponse, Background),
|
(ApplyCompletionAdditionalEditsResponse, Background),
|
||||||
(BufferReloaded, Foreground),
|
(BufferReloaded, Foreground),
|
||||||
(BufferSaved, Foreground),
|
(BufferSaved, Foreground),
|
||||||
(RemoveContact, Foreground),
|
(Call, Foreground),
|
||||||
|
(CallCanceled, Foreground),
|
||||||
|
(CancelCall, Foreground),
|
||||||
(ChannelMessageSent, Foreground),
|
(ChannelMessageSent, Foreground),
|
||||||
(CopyProjectEntry, Foreground),
|
(CopyProjectEntry, Foreground),
|
||||||
(CreateBufferForPeer, Foreground),
|
(CreateBufferForPeer, Foreground),
|
||||||
(CreateProjectEntry, Foreground),
|
(CreateProjectEntry, Foreground),
|
||||||
|
(CreateRoom, Foreground),
|
||||||
|
(CreateRoomResponse, Foreground),
|
||||||
|
(DeclineCall, Foreground),
|
||||||
(DeleteProjectEntry, Foreground),
|
(DeleteProjectEntry, Foreground),
|
||||||
(Error, Foreground),
|
(Error, Foreground),
|
||||||
(Follow, Foreground),
|
(Follow, Foreground),
|
||||||
@ -116,14 +121,17 @@ messages!(
|
|||||||
(GetProjectSymbols, Background),
|
(GetProjectSymbols, Background),
|
||||||
(GetProjectSymbolsResponse, Background),
|
(GetProjectSymbolsResponse, Background),
|
||||||
(GetUsers, Foreground),
|
(GetUsers, Foreground),
|
||||||
|
(IncomingCall, Foreground),
|
||||||
(UsersResponse, Foreground),
|
(UsersResponse, Foreground),
|
||||||
(JoinChannel, Foreground),
|
(JoinChannel, Foreground),
|
||||||
(JoinChannelResponse, Foreground),
|
(JoinChannelResponse, Foreground),
|
||||||
(JoinProject, Foreground),
|
(JoinProject, Foreground),
|
||||||
(JoinProjectResponse, Foreground),
|
(JoinProjectResponse, Foreground),
|
||||||
(JoinProjectRequestCancelled, Foreground),
|
(JoinRoom, Foreground),
|
||||||
|
(JoinRoomResponse, Foreground),
|
||||||
(LeaveChannel, Foreground),
|
(LeaveChannel, Foreground),
|
||||||
(LeaveProject, Foreground),
|
(LeaveProject, Foreground),
|
||||||
|
(LeaveRoom, Foreground),
|
||||||
(OpenBufferById, Background),
|
(OpenBufferById, Background),
|
||||||
(OpenBufferByPath, Background),
|
(OpenBufferByPath, Background),
|
||||||
(OpenBufferForSymbol, Background),
|
(OpenBufferForSymbol, Background),
|
||||||
@ -134,29 +142,28 @@ messages!(
|
|||||||
(PrepareRename, Background),
|
(PrepareRename, Background),
|
||||||
(PrepareRenameResponse, Background),
|
(PrepareRenameResponse, Background),
|
||||||
(ProjectEntryResponse, Foreground),
|
(ProjectEntryResponse, Foreground),
|
||||||
(ProjectUnshared, Foreground),
|
(RemoveContact, Foreground),
|
||||||
(RegisterProjectResponse, Foreground),
|
|
||||||
(Ping, Foreground),
|
(Ping, Foreground),
|
||||||
(RegisterProject, Foreground),
|
|
||||||
(RegisterProjectActivity, Foreground),
|
(RegisterProjectActivity, Foreground),
|
||||||
(ReloadBuffers, Foreground),
|
(ReloadBuffers, Foreground),
|
||||||
(ReloadBuffersResponse, Foreground),
|
(ReloadBuffersResponse, Foreground),
|
||||||
(RemoveProjectCollaborator, Foreground),
|
(RemoveProjectCollaborator, Foreground),
|
||||||
(RenameProjectEntry, Foreground),
|
(RenameProjectEntry, Foreground),
|
||||||
(RequestContact, Foreground),
|
(RequestContact, Foreground),
|
||||||
(RequestJoinProject, Foreground),
|
|
||||||
(RespondToContactRequest, Foreground),
|
(RespondToContactRequest, Foreground),
|
||||||
(RespondToJoinProjectRequest, Foreground),
|
(RoomUpdated, Foreground),
|
||||||
(SaveBuffer, Foreground),
|
(SaveBuffer, Foreground),
|
||||||
(SearchProject, Background),
|
(SearchProject, Background),
|
||||||
(SearchProjectResponse, Background),
|
(SearchProjectResponse, Background),
|
||||||
(SendChannelMessage, Foreground),
|
(SendChannelMessage, Foreground),
|
||||||
(SendChannelMessageResponse, Foreground),
|
(SendChannelMessageResponse, Foreground),
|
||||||
|
(ShareProject, Foreground),
|
||||||
|
(ShareProjectResponse, Foreground),
|
||||||
(ShowContacts, Foreground),
|
(ShowContacts, Foreground),
|
||||||
(StartLanguageServer, Foreground),
|
(StartLanguageServer, Foreground),
|
||||||
(Test, Foreground),
|
(Test, Foreground),
|
||||||
(Unfollow, Foreground),
|
(Unfollow, Foreground),
|
||||||
(UnregisterProject, Foreground),
|
(UnshareProject, Foreground),
|
||||||
(UpdateBuffer, Foreground),
|
(UpdateBuffer, Foreground),
|
||||||
(UpdateBufferFile, Foreground),
|
(UpdateBufferFile, Foreground),
|
||||||
(UpdateContacts, Foreground),
|
(UpdateContacts, Foreground),
|
||||||
@ -164,6 +171,7 @@ messages!(
|
|||||||
(UpdateFollowers, Foreground),
|
(UpdateFollowers, Foreground),
|
||||||
(UpdateInviteInfo, Foreground),
|
(UpdateInviteInfo, Foreground),
|
||||||
(UpdateLanguageServer, Foreground),
|
(UpdateLanguageServer, Foreground),
|
||||||
|
(UpdateParticipantLocation, Foreground),
|
||||||
(UpdateProject, Foreground),
|
(UpdateProject, Foreground),
|
||||||
(UpdateWorktree, Foreground),
|
(UpdateWorktree, Foreground),
|
||||||
(UpdateWorktreeExtensions, Background),
|
(UpdateWorktreeExtensions, Background),
|
||||||
@ -178,8 +186,12 @@ request_messages!(
|
|||||||
ApplyCompletionAdditionalEdits,
|
ApplyCompletionAdditionalEdits,
|
||||||
ApplyCompletionAdditionalEditsResponse
|
ApplyCompletionAdditionalEditsResponse
|
||||||
),
|
),
|
||||||
|
(Call, Ack),
|
||||||
|
(CancelCall, Ack),
|
||||||
(CopyProjectEntry, ProjectEntryResponse),
|
(CopyProjectEntry, ProjectEntryResponse),
|
||||||
(CreateProjectEntry, ProjectEntryResponse),
|
(CreateProjectEntry, ProjectEntryResponse),
|
||||||
|
(CreateRoom, CreateRoomResponse),
|
||||||
|
(DeclineCall, Ack),
|
||||||
(DeleteProjectEntry, ProjectEntryResponse),
|
(DeleteProjectEntry, ProjectEntryResponse),
|
||||||
(Follow, FollowResponse),
|
(Follow, FollowResponse),
|
||||||
(FormatBuffers, FormatBuffersResponse),
|
(FormatBuffers, FormatBuffersResponse),
|
||||||
@ -198,13 +210,14 @@ request_messages!(
|
|||||||
(GetUsers, UsersResponse),
|
(GetUsers, UsersResponse),
|
||||||
(JoinChannel, JoinChannelResponse),
|
(JoinChannel, JoinChannelResponse),
|
||||||
(JoinProject, JoinProjectResponse),
|
(JoinProject, JoinProjectResponse),
|
||||||
|
(JoinRoom, JoinRoomResponse),
|
||||||
|
(IncomingCall, Ack),
|
||||||
(OpenBufferById, OpenBufferResponse),
|
(OpenBufferById, OpenBufferResponse),
|
||||||
(OpenBufferByPath, OpenBufferResponse),
|
(OpenBufferByPath, OpenBufferResponse),
|
||||||
(OpenBufferForSymbol, OpenBufferForSymbolResponse),
|
(OpenBufferForSymbol, OpenBufferForSymbolResponse),
|
||||||
(Ping, Ack),
|
(Ping, Ack),
|
||||||
(PerformRename, PerformRenameResponse),
|
(PerformRename, PerformRenameResponse),
|
||||||
(PrepareRename, PrepareRenameResponse),
|
(PrepareRename, PrepareRenameResponse),
|
||||||
(RegisterProject, RegisterProjectResponse),
|
|
||||||
(ReloadBuffers, ReloadBuffersResponse),
|
(ReloadBuffers, ReloadBuffersResponse),
|
||||||
(RequestContact, Ack),
|
(RequestContact, Ack),
|
||||||
(RemoveContact, Ack),
|
(RemoveContact, Ack),
|
||||||
@ -213,9 +226,10 @@ request_messages!(
|
|||||||
(SaveBuffer, BufferSaved),
|
(SaveBuffer, BufferSaved),
|
||||||
(SearchProject, SearchProjectResponse),
|
(SearchProject, SearchProjectResponse),
|
||||||
(SendChannelMessage, SendChannelMessageResponse),
|
(SendChannelMessage, SendChannelMessageResponse),
|
||||||
|
(ShareProject, ShareProjectResponse),
|
||||||
(Test, Test),
|
(Test, Test),
|
||||||
(UnregisterProject, Ack),
|
|
||||||
(UpdateBuffer, Ack),
|
(UpdateBuffer, Ack),
|
||||||
|
(UpdateParticipantLocation, Ack),
|
||||||
(UpdateWorktree, Ack),
|
(UpdateWorktree, Ack),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -241,24 +255,21 @@ entity_messages!(
|
|||||||
GetReferences,
|
GetReferences,
|
||||||
GetProjectSymbols,
|
GetProjectSymbols,
|
||||||
JoinProject,
|
JoinProject,
|
||||||
JoinProjectRequestCancelled,
|
|
||||||
LeaveProject,
|
LeaveProject,
|
||||||
OpenBufferById,
|
OpenBufferById,
|
||||||
OpenBufferByPath,
|
OpenBufferByPath,
|
||||||
OpenBufferForSymbol,
|
OpenBufferForSymbol,
|
||||||
PerformRename,
|
PerformRename,
|
||||||
PrepareRename,
|
PrepareRename,
|
||||||
ProjectUnshared,
|
|
||||||
RegisterProjectActivity,
|
RegisterProjectActivity,
|
||||||
ReloadBuffers,
|
ReloadBuffers,
|
||||||
RemoveProjectCollaborator,
|
RemoveProjectCollaborator,
|
||||||
RenameProjectEntry,
|
RenameProjectEntry,
|
||||||
RequestJoinProject,
|
|
||||||
SaveBuffer,
|
SaveBuffer,
|
||||||
SearchProject,
|
SearchProject,
|
||||||
StartLanguageServer,
|
StartLanguageServer,
|
||||||
Unfollow,
|
Unfollow,
|
||||||
UnregisterProject,
|
UnshareProject,
|
||||||
UpdateBuffer,
|
UpdateBuffer,
|
||||||
UpdateBufferFile,
|
UpdateBufferFile,
|
||||||
UpdateDiagnosticSummary,
|
UpdateDiagnosticSummary,
|
||||||
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
|||||||
pub use peer::*;
|
pub use peer::*;
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
pub const PROTOCOL_VERSION: u32 = 34;
|
pub const PROTOCOL_VERSION: u32 = 35;
|
||||||
|
@ -726,6 +726,8 @@ impl Element for TerminalElement {
|
|||||||
layout: &mut Self::LayoutState,
|
layout: &mut Self::LayoutState,
|
||||||
cx: &mut gpui::PaintContext,
|
cx: &mut gpui::PaintContext,
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
|
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||||
|
|
||||||
//Setup element stuff
|
//Setup element stuff
|
||||||
let clip_bounds = Some(visible_bounds);
|
let clip_bounds = Some(visible_bounds);
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ pub struct Theme {
|
|||||||
pub context_menu: ContextMenu,
|
pub context_menu: ContextMenu,
|
||||||
pub chat_panel: ChatPanel,
|
pub chat_panel: ChatPanel,
|
||||||
pub contacts_popover: ContactsPopover,
|
pub contacts_popover: ContactsPopover,
|
||||||
pub contacts_panel: ContactsPanel,
|
pub contact_list: ContactList,
|
||||||
pub contact_finder: ContactFinder,
|
pub contact_finder: ContactFinder,
|
||||||
pub project_panel: ProjectPanel,
|
pub project_panel: ProjectPanel,
|
||||||
pub command_palette: CommandPalette,
|
pub command_palette: CommandPalette,
|
||||||
@ -31,6 +31,8 @@ pub struct Theme {
|
|||||||
pub breadcrumbs: ContainedText,
|
pub breadcrumbs: ContainedText,
|
||||||
pub contact_notification: ContactNotification,
|
pub contact_notification: ContactNotification,
|
||||||
pub update_notification: UpdateNotification,
|
pub update_notification: UpdateNotification,
|
||||||
|
pub project_shared_notification: ProjectSharedNotification,
|
||||||
|
pub incoming_call_notification: IncomingCallNotification,
|
||||||
pub tooltip: TooltipStyle,
|
pub tooltip: TooltipStyle,
|
||||||
pub terminal: TerminalStyle,
|
pub terminal: TerminalStyle,
|
||||||
}
|
}
|
||||||
@ -58,6 +60,7 @@ pub struct Workspace {
|
|||||||
pub notifications: Notifications,
|
pub notifications: Notifications,
|
||||||
pub joining_project_avatar: ImageStyle,
|
pub joining_project_avatar: ImageStyle,
|
||||||
pub joining_project_message: ContainedText,
|
pub joining_project_message: ContainedText,
|
||||||
|
pub external_location_message: ContainedText,
|
||||||
pub dock: Dock,
|
pub dock: Dock,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,8 +75,67 @@ pub struct Titlebar {
|
|||||||
pub avatar_ribbon: AvatarRibbon,
|
pub avatar_ribbon: AvatarRibbon,
|
||||||
pub offline_icon: OfflineIcon,
|
pub offline_icon: OfflineIcon,
|
||||||
pub avatar: ImageStyle,
|
pub avatar: ImageStyle,
|
||||||
|
pub inactive_avatar: ImageStyle,
|
||||||
pub sign_in_prompt: Interactive<ContainedText>,
|
pub sign_in_prompt: Interactive<ContainedText>,
|
||||||
pub outdated_warning: ContainedText,
|
pub outdated_warning: ContainedText,
|
||||||
|
pub share_button: Interactive<ContainedText>,
|
||||||
|
pub toggle_contacts_button: Interactive<IconButton>,
|
||||||
|
pub toggle_contacts_badge: ContainerStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct ContactsPopover {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub container: ContainerStyle,
|
||||||
|
pub height: f32,
|
||||||
|
pub width: f32,
|
||||||
|
pub invite_row_height: f32,
|
||||||
|
pub invite_row: Interactive<ContainedLabel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct ContactList {
|
||||||
|
pub user_query_editor: FieldEditor,
|
||||||
|
pub user_query_editor_height: f32,
|
||||||
|
pub add_contact_button: IconButton,
|
||||||
|
pub header_row: Interactive<ContainedText>,
|
||||||
|
pub leave_call: Interactive<ContainedText>,
|
||||||
|
pub contact_row: Interactive<ContainerStyle>,
|
||||||
|
pub row_height: f32,
|
||||||
|
pub project_row: Interactive<ProjectRow>,
|
||||||
|
pub tree_branch: Interactive<TreeBranch>,
|
||||||
|
pub contact_avatar: ImageStyle,
|
||||||
|
pub contact_status_free: ContainerStyle,
|
||||||
|
pub contact_status_busy: ContainerStyle,
|
||||||
|
pub contact_username: ContainedText,
|
||||||
|
pub contact_button: Interactive<IconButton>,
|
||||||
|
pub contact_button_spacing: f32,
|
||||||
|
pub disabled_button: IconButton,
|
||||||
|
pub section_icon_size: f32,
|
||||||
|
pub calling_indicator: ContainedText,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct ProjectRow {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub container: ContainerStyle,
|
||||||
|
pub name: ContainedText,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default, Clone, Copy)]
|
||||||
|
pub struct TreeBranch {
|
||||||
|
pub width: f32,
|
||||||
|
pub color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct ContactFinder {
|
||||||
|
pub picker: Picker,
|
||||||
|
pub row_height: f32,
|
||||||
|
pub contact_avatar: ImageStyle,
|
||||||
|
pub contact_username: ContainerStyle,
|
||||||
|
pub contact_button: IconButton,
|
||||||
|
pub disabled_contact_button: IconButton,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
@ -315,33 +377,6 @@ pub struct CommandPalette {
|
|||||||
pub keystroke_spacing: f32,
|
pub keystroke_spacing: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
|
||||||
pub struct ContactsPopover {
|
|
||||||
pub background: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
|
||||||
pub struct ContactsPanel {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub container: ContainerStyle,
|
|
||||||
pub user_query_editor: FieldEditor,
|
|
||||||
pub user_query_editor_height: f32,
|
|
||||||
pub add_contact_button: IconButton,
|
|
||||||
pub header_row: Interactive<ContainedText>,
|
|
||||||
pub contact_row: Interactive<ContainerStyle>,
|
|
||||||
pub project_row: Interactive<ProjectRow>,
|
|
||||||
pub row_height: f32,
|
|
||||||
pub contact_avatar: ImageStyle,
|
|
||||||
pub contact_username: ContainedText,
|
|
||||||
pub contact_button: Interactive<IconButton>,
|
|
||||||
pub contact_button_spacing: f32,
|
|
||||||
pub disabled_button: IconButton,
|
|
||||||
pub tree_branch: Interactive<TreeBranch>,
|
|
||||||
pub private_button: Interactive<IconButton>,
|
|
||||||
pub section_icon_size: f32,
|
|
||||||
pub invite_row: Interactive<ContainedLabel>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
#[derive(Deserialize, Default)]
|
||||||
pub struct InviteLink {
|
pub struct InviteLink {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@ -351,21 +386,6 @@ pub struct InviteLink {
|
|||||||
pub icon: Icon,
|
pub icon: Icon,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default, Clone, Copy)]
|
|
||||||
pub struct TreeBranch {
|
|
||||||
pub width: f32,
|
|
||||||
pub color: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
|
||||||
pub struct ContactFinder {
|
|
||||||
pub row_height: f32,
|
|
||||||
pub contact_avatar: ImageStyle,
|
|
||||||
pub contact_username: ContainerStyle,
|
|
||||||
pub contact_button: IconButton,
|
|
||||||
pub disabled_contact_button: IconButton,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
#[derive(Deserialize, Default)]
|
||||||
pub struct Icon {
|
pub struct Icon {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@ -384,16 +404,6 @@ pub struct IconButton {
|
|||||||
pub button_width: f32,
|
pub button_width: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
|
||||||
pub struct ProjectRow {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub container: ContainerStyle,
|
|
||||||
pub name: ContainedText,
|
|
||||||
pub guests: ContainerStyle,
|
|
||||||
pub guest_avatar: ImageStyle,
|
|
||||||
pub guest_avatar_spacing: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
#[derive(Deserialize, Default)]
|
||||||
pub struct ChatMessage {
|
pub struct ChatMessage {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@ -475,6 +485,40 @@ pub struct UpdateNotification {
|
|||||||
pub dismiss_button: Interactive<IconButton>,
|
pub dismiss_button: Interactive<IconButton>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct ProjectSharedNotification {
|
||||||
|
pub window_height: f32,
|
||||||
|
pub window_width: f32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background: Color,
|
||||||
|
pub owner_container: ContainerStyle,
|
||||||
|
pub owner_avatar: ImageStyle,
|
||||||
|
pub owner_metadata: ContainerStyle,
|
||||||
|
pub owner_username: ContainedText,
|
||||||
|
pub message: ContainedText,
|
||||||
|
pub worktree_roots: ContainedText,
|
||||||
|
pub button_width: f32,
|
||||||
|
pub open_button: ContainedText,
|
||||||
|
pub dismiss_button: ContainedText,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct IncomingCallNotification {
|
||||||
|
pub window_height: f32,
|
||||||
|
pub window_width: f32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background: Color,
|
||||||
|
pub caller_container: ContainerStyle,
|
||||||
|
pub caller_avatar: ImageStyle,
|
||||||
|
pub caller_metadata: ContainerStyle,
|
||||||
|
pub caller_username: ContainedText,
|
||||||
|
pub caller_message: ContainedText,
|
||||||
|
pub worktree_roots: ContainedText,
|
||||||
|
pub button_width: f32,
|
||||||
|
pub accept_button: ContainedText,
|
||||||
|
pub decline_button: ContainedText,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
pub struct Editor {
|
pub struct Editor {
|
||||||
pub text_color: Color,
|
pub text_color: Color,
|
||||||
|
@ -8,11 +8,16 @@ path = "src/workspace.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
test-support = ["client/test-support", "project/test-support", "settings/test-support"]
|
test-support = [
|
||||||
|
"call/test-support",
|
||||||
|
"client/test-support",
|
||||||
|
"project/test-support",
|
||||||
|
"settings/test-support"
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
call = { path = "../call" }
|
||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
clock = { path = "../clock" }
|
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
context_menu = { path = "../context_menu" }
|
context_menu = { path = "../context_menu" }
|
||||||
drag_and_drop = { path = "../drag_and_drop" }
|
drag_and_drop = { path = "../drag_and_drop" }
|
||||||
@ -33,6 +38,7 @@ serde_json = { version = "1.0", features = ["preserve_order"] }
|
|||||||
smallvec = { version = "1.6", features = ["union"] }
|
smallvec = { version = "1.6", features = ["union"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
call = { path = "../call", features = ["test-support"] }
|
||||||
client = { path = "../client", features = ["test-support"] }
|
client = { path = "../client", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
use crate::{FollowerStatesByLeader, Pane};
|
use crate::{FollowerStatesByLeader, JoinProject, Pane, Workspace};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use client::PeerId;
|
use call::ActiveCall;
|
||||||
use collections::HashMap;
|
use gpui::{
|
||||||
use gpui::{elements::*, Axis, Border, ViewHandle};
|
elements::*, Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle,
|
||||||
use project::Collaborator;
|
};
|
||||||
|
use project::Project;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use theme::Theme;
|
use theme::Theme;
|
||||||
|
|
||||||
@ -56,11 +57,14 @@ impl PaneGroup {
|
|||||||
|
|
||||||
pub(crate) fn render(
|
pub(crate) fn render(
|
||||||
&self,
|
&self,
|
||||||
|
project: &ModelHandle<Project>,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
follower_states: &FollowerStatesByLeader,
|
follower_states: &FollowerStatesByLeader,
|
||||||
collaborators: &HashMap<PeerId, Collaborator>,
|
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||||
|
cx: &mut RenderContext<Workspace>,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
self.root.render(theme, follower_states, collaborators)
|
self.root
|
||||||
|
.render(project, theme, follower_states, active_call, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
|
pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
|
||||||
@ -100,13 +104,16 @@ impl Member {
|
|||||||
|
|
||||||
pub fn render(
|
pub fn render(
|
||||||
&self,
|
&self,
|
||||||
|
project: &ModelHandle<Project>,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
follower_states: &FollowerStatesByLeader,
|
follower_states: &FollowerStatesByLeader,
|
||||||
collaborators: &HashMap<PeerId, Collaborator>,
|
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||||
|
cx: &mut RenderContext<Workspace>,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
|
enum FollowIntoExternalProject {}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Member::Pane(pane) => {
|
Member::Pane(pane) => {
|
||||||
let mut border = Border::default();
|
|
||||||
let leader = follower_states
|
let leader = follower_states
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(leader_id, follower_states)| {
|
.find_map(|(leader_id, follower_states)| {
|
||||||
@ -116,21 +123,110 @@ impl Member {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.and_then(|leader_id| collaborators.get(leader_id));
|
.and_then(|leader_id| {
|
||||||
if let Some(leader) = leader {
|
let room = active_call?.read(cx).room()?.read(cx);
|
||||||
let leader_color = theme
|
let collaborator = project.read(cx).collaborators().get(leader_id)?;
|
||||||
.editor
|
let participant = room.remote_participants().get(&leader_id)?;
|
||||||
.replica_selection_style(leader.replica_id)
|
Some((collaborator.replica_id, participant))
|
||||||
.cursor;
|
});
|
||||||
|
|
||||||
|
let mut border = Border::default();
|
||||||
|
|
||||||
|
let prompt = if let Some((replica_id, leader)) = leader {
|
||||||
|
let leader_color = theme.editor.replica_selection_style(replica_id).cursor;
|
||||||
border = Border::all(theme.workspace.leader_border_width, leader_color);
|
border = Border::all(theme.workspace.leader_border_width, leader_color);
|
||||||
border
|
border
|
||||||
.color
|
.color
|
||||||
.fade_out(1. - theme.workspace.leader_border_opacity);
|
.fade_out(1. - theme.workspace.leader_border_opacity);
|
||||||
border.overlay = true;
|
border.overlay = true;
|
||||||
}
|
|
||||||
ChildView::new(pane).contained().with_border(border).boxed()
|
match leader.location {
|
||||||
|
call::ParticipantLocation::SharedProject {
|
||||||
|
project_id: leader_project_id,
|
||||||
|
} => {
|
||||||
|
if Some(leader_project_id) == project.read(cx).remote_id() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let leader_user = leader.user.clone();
|
||||||
|
let leader_user_id = leader.user.id;
|
||||||
|
Some(
|
||||||
|
MouseEventHandler::<FollowIntoExternalProject>::new(
|
||||||
|
pane.id(),
|
||||||
|
cx,
|
||||||
|
|_, _| {
|
||||||
|
Label::new(
|
||||||
|
format!(
|
||||||
|
"Follow {} on their active project",
|
||||||
|
leader_user.github_login,
|
||||||
|
),
|
||||||
|
theme
|
||||||
|
.workspace
|
||||||
|
.external_location_message
|
||||||
|
.text
|
||||||
|
.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(
|
||||||
|
theme.workspace.external_location_message.container,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.dispatch_action(JoinProject {
|
||||||
|
project_id: leader_project_id,
|
||||||
|
follow_user_id: leader_user_id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.aligned()
|
||||||
|
.bottom()
|
||||||
|
.right()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
call::ParticipantLocation::UnsharedProject => Some(
|
||||||
|
Label::new(
|
||||||
|
format!(
|
||||||
|
"{} is viewing an unshared Zed project",
|
||||||
|
leader.user.github_login
|
||||||
|
),
|
||||||
|
theme.workspace.external_location_message.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.workspace.external_location_message.container)
|
||||||
|
.aligned()
|
||||||
|
.bottom()
|
||||||
|
.right()
|
||||||
|
.boxed(),
|
||||||
|
),
|
||||||
|
call::ParticipantLocation::External => Some(
|
||||||
|
Label::new(
|
||||||
|
format!(
|
||||||
|
"{} is viewing a window outside of Zed",
|
||||||
|
leader.user.github_login
|
||||||
|
),
|
||||||
|
theme.workspace.external_location_message.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.workspace.external_location_message.container)
|
||||||
|
.aligned()
|
||||||
|
.bottom()
|
||||||
|
.right()
|
||||||
|
.boxed(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Stack::new()
|
||||||
|
.with_child(ChildView::new(pane).contained().with_border(border).boxed())
|
||||||
|
.with_children(prompt)
|
||||||
|
.boxed()
|
||||||
}
|
}
|
||||||
Member::Axis(axis) => axis.render(theme, follower_states, collaborators),
|
Member::Axis(axis) => axis.render(project, theme, follower_states, active_call, cx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,14 +328,16 @@ impl PaneAxis {
|
|||||||
|
|
||||||
fn render(
|
fn render(
|
||||||
&self,
|
&self,
|
||||||
|
project: &ModelHandle<Project>,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
follower_state: &FollowerStatesByLeader,
|
follower_state: &FollowerStatesByLeader,
|
||||||
collaborators: &HashMap<PeerId, Collaborator>,
|
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||||
|
cx: &mut RenderContext<Workspace>,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
let last_member_ix = self.members.len() - 1;
|
let last_member_ix = self.members.len() - 1;
|
||||||
Flex::new(self.axis)
|
Flex::new(self.axis)
|
||||||
.with_children(self.members.iter().enumerate().map(|(ix, member)| {
|
.with_children(self.members.iter().enumerate().map(|(ix, member)| {
|
||||||
let mut member = member.render(theme, follower_state, collaborators);
|
let mut member = member.render(project, theme, follower_state, active_call, cx);
|
||||||
if ix < last_member_ix {
|
if ix < last_member_ix {
|
||||||
let mut border = theme.workspace.pane_divider;
|
let mut border = theme.workspace.pane_divider;
|
||||||
border.left = false;
|
border.left = false;
|
||||||
|
@ -1,185 +0,0 @@
|
|||||||
use crate::{sidebar::SidebarSide, AppState, ToggleFollow, Workspace};
|
|
||||||
use anyhow::Result;
|
|
||||||
use client::{proto, Client, Contact};
|
|
||||||
use gpui::{
|
|
||||||
elements::*, ElementBox, Entity, ImageData, MutableAppContext, RenderContext, Task, View,
|
|
||||||
ViewContext,
|
|
||||||
};
|
|
||||||
use project::Project;
|
|
||||||
use settings::Settings;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use util::ResultExt;
|
|
||||||
|
|
||||||
pub struct WaitingRoom {
|
|
||||||
project_id: u64,
|
|
||||||
avatar: Option<Arc<ImageData>>,
|
|
||||||
message: String,
|
|
||||||
waiting: bool,
|
|
||||||
client: Arc<Client>,
|
|
||||||
_join_task: Task<Result<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for WaitingRoom {
|
|
||||||
type Event = ();
|
|
||||||
|
|
||||||
fn release(&mut self, _: &mut MutableAppContext) {
|
|
||||||
if self.waiting {
|
|
||||||
self.client
|
|
||||||
.send(proto::LeaveProject {
|
|
||||||
project_id: self.project_id,
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for WaitingRoom {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"WaitingRoom"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
|
||||||
let theme = &cx.global::<Settings>().theme.workspace;
|
|
||||||
|
|
||||||
Flex::column()
|
|
||||||
.with_children(self.avatar.clone().map(|avatar| {
|
|
||||||
Image::new(avatar)
|
|
||||||
.with_style(theme.joining_project_avatar)
|
|
||||||
.aligned()
|
|
||||||
.boxed()
|
|
||||||
}))
|
|
||||||
.with_child(
|
|
||||||
Text::new(
|
|
||||||
self.message.clone(),
|
|
||||||
theme.joining_project_message.text.clone(),
|
|
||||||
)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.joining_project_message.container)
|
|
||||||
.aligned()
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
.aligned()
|
|
||||||
.contained()
|
|
||||||
.with_background_color(theme.background)
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WaitingRoom {
|
|
||||||
pub fn new(
|
|
||||||
contact: Arc<Contact>,
|
|
||||||
project_index: usize,
|
|
||||||
app_state: Arc<AppState>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let project_id = contact.projects[project_index].id;
|
|
||||||
let client = app_state.client.clone();
|
|
||||||
let _join_task = cx.spawn_weak({
|
|
||||||
let contact = contact.clone();
|
|
||||||
|this, mut cx| async move {
|
|
||||||
let project = Project::remote(
|
|
||||||
project_id,
|
|
||||||
app_state.client.clone(),
|
|
||||||
app_state.user_store.clone(),
|
|
||||||
app_state.project_store.clone(),
|
|
||||||
app_state.languages.clone(),
|
|
||||||
app_state.fs.clone(),
|
|
||||||
cx.clone(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.waiting = false;
|
|
||||||
match project {
|
|
||||||
Ok(project) => {
|
|
||||||
cx.replace_root_view(|cx| {
|
|
||||||
let mut workspace =
|
|
||||||
Workspace::new(project, app_state.default_item_factory, cx);
|
|
||||||
(app_state.initialize_workspace)(
|
|
||||||
&mut workspace,
|
|
||||||
&app_state,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
workspace.toggle_sidebar(SidebarSide::Left, cx);
|
|
||||||
if let Some((host_peer_id, _)) = workspace
|
|
||||||
.project
|
|
||||||
.read(cx)
|
|
||||||
.collaborators()
|
|
||||||
.iter()
|
|
||||||
.find(|(_, collaborator)| collaborator.replica_id == 0)
|
|
||||||
{
|
|
||||||
if let Some(follow) = workspace
|
|
||||||
.toggle_follow(&ToggleFollow(*host_peer_id), cx)
|
|
||||||
{
|
|
||||||
follow.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
workspace
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
let login = &contact.user.github_login;
|
|
||||||
let message = match error {
|
|
||||||
project::JoinProjectError::HostDeclined => {
|
|
||||||
format!("@{} declined your request.", login)
|
|
||||||
}
|
|
||||||
project::JoinProjectError::HostClosedProject => {
|
|
||||||
format!(
|
|
||||||
"@{} closed their copy of {}.",
|
|
||||||
login,
|
|
||||||
humanize_list(
|
|
||||||
&contact.projects[project_index]
|
|
||||||
.visible_worktree_root_names
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
project::JoinProjectError::HostWentOffline => {
|
|
||||||
format!("@{} went offline.", login)
|
|
||||||
}
|
|
||||||
project::JoinProjectError::Other(error) => {
|
|
||||||
log::error!("error joining project: {}", error);
|
|
||||||
"An error occurred.".to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.message = message;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
project_id,
|
|
||||||
avatar: contact.user.avatar.clone(),
|
|
||||||
message: format!(
|
|
||||||
"Asking to join @{}'s copy of {}...",
|
|
||||||
contact.user.github_login,
|
|
||||||
humanize_list(&contact.projects[project_index].visible_worktree_root_names)
|
|
||||||
),
|
|
||||||
waiting: true,
|
|
||||||
client,
|
|
||||||
_join_task,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn humanize_list<'a>(items: impl IntoIterator<Item = &'a String>) -> String {
|
|
||||||
let mut list = String::new();
|
|
||||||
let mut items = items.into_iter().enumerate().peekable();
|
|
||||||
while let Some((ix, item)) = items.next() {
|
|
||||||
if ix > 0 {
|
|
||||||
list.push_str(", ");
|
|
||||||
if items.peek().is_none() {
|
|
||||||
list.push_str("and ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list.push_str(item);
|
|
||||||
}
|
|
||||||
list
|
|
||||||
}
|
|
@ -10,28 +10,22 @@ pub mod searchable;
|
|||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
mod status_bar;
|
mod status_bar;
|
||||||
mod toolbar;
|
mod toolbar;
|
||||||
mod waiting_room;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use client::{
|
use call::ActiveCall;
|
||||||
proto, Authenticate, Client, Contact, PeerId, Subscription, TypedEnvelope, User, UserStore,
|
use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
|
||||||
};
|
|
||||||
use clock::ReplicaId;
|
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
use dock::{DefaultItemFactory, Dock, ToggleDockButton};
|
use dock::{DefaultItemFactory, Dock, ToggleDockButton};
|
||||||
use drag_and_drop::DragAndDrop;
|
use drag_and_drop::DragAndDrop;
|
||||||
use futures::{channel::oneshot, FutureExt};
|
use futures::{channel::oneshot, FutureExt, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
color::Color,
|
|
||||||
elements::*,
|
elements::*,
|
||||||
geometry::{rect::RectF, vector::vec2f, PathBuilder},
|
|
||||||
impl_actions, impl_internal_actions,
|
impl_actions, impl_internal_actions,
|
||||||
json::{self, ToJson},
|
|
||||||
platform::{CursorStyle, WindowOptions},
|
platform::{CursorStyle, WindowOptions},
|
||||||
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData,
|
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||||
ModelContext, ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, PromptLevel,
|
MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
|
||||||
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
@ -52,7 +46,6 @@ use std::{
|
|||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
fmt,
|
fmt,
|
||||||
future::Future,
|
future::Future,
|
||||||
ops::Range,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
@ -64,7 +57,6 @@ use std::{
|
|||||||
use theme::{Theme, ThemeRegistry};
|
use theme::{Theme, ThemeRegistry};
|
||||||
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use waiting_room::WaitingRoom;
|
|
||||||
|
|
||||||
type ProjectItemBuilders = HashMap<
|
type ProjectItemBuilders = HashMap<
|
||||||
TypeId,
|
TypeId,
|
||||||
@ -115,12 +107,6 @@ pub struct OpenPaths {
|
|||||||
pub paths: Vec<PathBuf>,
|
pub paths: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
|
||||||
pub struct ToggleProjectOnline {
|
|
||||||
#[serde(skip_deserializing)]
|
|
||||||
pub project: Option<ModelHandle<Project>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
pub struct ActivatePane(pub usize);
|
pub struct ActivatePane(pub usize);
|
||||||
|
|
||||||
@ -129,8 +115,8 @@ pub struct ToggleFollow(pub PeerId);
|
|||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct JoinProject {
|
pub struct JoinProject {
|
||||||
pub contact: Arc<Contact>,
|
pub project_id: u64,
|
||||||
pub project_index: usize,
|
pub follow_user_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_internal_actions!(
|
impl_internal_actions!(
|
||||||
@ -142,7 +128,7 @@ impl_internal_actions!(
|
|||||||
RemoveWorktreeFromProject
|
RemoveWorktreeFromProject
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
|
impl_actions!(workspace, [ActivatePane]);
|
||||||
|
|
||||||
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
pane::init(cx);
|
pane::init(cx);
|
||||||
@ -173,14 +159,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cx.add_global_action({
|
|
||||||
let app_state = Arc::downgrade(&app_state);
|
|
||||||
move |action: &JoinProject, cx: &mut MutableAppContext| {
|
|
||||||
if let Some(app_state) = app_state.upgrade() {
|
|
||||||
join_project(action.contact.clone(), action.project_index, &app_state, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.add_async_action(Workspace::toggle_follow);
|
cx.add_async_action(Workspace::toggle_follow);
|
||||||
cx.add_async_action(Workspace::follow_next_collaborator);
|
cx.add_async_action(Workspace::follow_next_collaborator);
|
||||||
@ -188,7 +166,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
|||||||
cx.add_async_action(Workspace::save_all);
|
cx.add_async_action(Workspace::save_all);
|
||||||
cx.add_action(Workspace::add_folder_to_project);
|
cx.add_action(Workspace::add_folder_to_project);
|
||||||
cx.add_action(Workspace::remove_folder_from_project);
|
cx.add_action(Workspace::remove_folder_from_project);
|
||||||
cx.add_action(Workspace::toggle_project_online);
|
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
|
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
|
||||||
let pane = workspace.active_pane().clone();
|
let pane = workspace.active_pane().clone();
|
||||||
@ -957,7 +934,7 @@ impl AppState {
|
|||||||
let languages = Arc::new(LanguageRegistry::test());
|
let languages = Arc::new(LanguageRegistry::test());
|
||||||
let http_client = client::test::FakeHttpClient::with_404_response();
|
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||||
let client = Client::new(http_client.clone(), cx);
|
let client = Client::new(http_client.clone(), cx);
|
||||||
let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
|
let project_store = cx.add_model(|_| ProjectStore::new());
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||||
let themes = ThemeRegistry::new((), cx.font_cache().clone());
|
let themes = ThemeRegistry::new((), cx.font_cache().clone());
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
@ -984,7 +961,7 @@ pub struct Workspace {
|
|||||||
weak_self: WeakViewHandle<Self>,
|
weak_self: WeakViewHandle<Self>,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
user_store: ModelHandle<client::UserStore>,
|
user_store: ModelHandle<client::UserStore>,
|
||||||
remote_entity_subscription: Option<Subscription>,
|
remote_entity_subscription: Option<client::Subscription>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
modal: Option<AnyViewHandle>,
|
modal: Option<AnyViewHandle>,
|
||||||
center: PaneGroup,
|
center: PaneGroup,
|
||||||
@ -995,6 +972,7 @@ pub struct Workspace {
|
|||||||
active_pane: ViewHandle<Pane>,
|
active_pane: ViewHandle<Pane>,
|
||||||
last_active_center_pane: Option<ViewHandle<Pane>>,
|
last_active_center_pane: Option<ViewHandle<Pane>>,
|
||||||
status_bar: ViewHandle<StatusBar>,
|
status_bar: ViewHandle<StatusBar>,
|
||||||
|
titlebar_item: Option<AnyViewHandle>,
|
||||||
dock: Dock,
|
dock: Dock,
|
||||||
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
@ -1002,7 +980,9 @@ pub struct Workspace {
|
|||||||
follower_states_by_leader: FollowerStatesByLeader,
|
follower_states_by_leader: FollowerStatesByLeader,
|
||||||
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
|
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
|
||||||
window_edited: bool,
|
window_edited: bool,
|
||||||
|
active_call: Option<ModelHandle<ActiveCall>>,
|
||||||
_observe_current_user: Task<()>,
|
_observe_current_user: Task<()>,
|
||||||
|
_active_call_observation: Option<gpui::Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -1111,6 +1091,14 @@ impl Workspace {
|
|||||||
drag_and_drop.register_container(weak_handle.clone());
|
drag_and_drop.register_container(weak_handle.clone());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut active_call = None;
|
||||||
|
let mut active_call_observation = None;
|
||||||
|
if cx.has_global::<ModelHandle<ActiveCall>>() {
|
||||||
|
let call = cx.global::<ModelHandle<ActiveCall>>().clone();
|
||||||
|
active_call_observation = Some(cx.observe(&call, |_, _, cx| cx.notify()));
|
||||||
|
active_call = Some(call);
|
||||||
|
}
|
||||||
|
|
||||||
let mut this = Workspace {
|
let mut this = Workspace {
|
||||||
modal: None,
|
modal: None,
|
||||||
weak_self: weak_handle,
|
weak_self: weak_handle,
|
||||||
@ -1124,6 +1112,7 @@ impl Workspace {
|
|||||||
active_pane: center_pane.clone(),
|
active_pane: center_pane.clone(),
|
||||||
last_active_center_pane: Some(center_pane.clone()),
|
last_active_center_pane: Some(center_pane.clone()),
|
||||||
status_bar,
|
status_bar,
|
||||||
|
titlebar_item: None,
|
||||||
notifications: Default::default(),
|
notifications: Default::default(),
|
||||||
client,
|
client,
|
||||||
remote_entity_subscription: None,
|
remote_entity_subscription: None,
|
||||||
@ -1136,7 +1125,9 @@ impl Workspace {
|
|||||||
follower_states_by_leader: Default::default(),
|
follower_states_by_leader: Default::default(),
|
||||||
last_leaders_by_pane: Default::default(),
|
last_leaders_by_pane: Default::default(),
|
||||||
window_edited: false,
|
window_edited: false,
|
||||||
|
active_call,
|
||||||
_observe_current_user,
|
_observe_current_user,
|
||||||
|
_active_call_observation: active_call_observation,
|
||||||
};
|
};
|
||||||
this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
|
this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
|
||||||
cx.defer(|this, cx| this.update_window_title(cx));
|
cx.defer(|this, cx| this.update_window_title(cx));
|
||||||
@ -1168,6 +1159,19 @@ impl Workspace {
|
|||||||
&self.project
|
&self.project
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn client(&self) -> &Arc<Client> {
|
||||||
|
&self.client
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_titlebar_item(
|
||||||
|
&mut self,
|
||||||
|
item: impl Into<AnyViewHandle>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.titlebar_item = Some(item.into());
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
/// Call the given callback with a workspace whose project is local.
|
/// Call the given callback with a workspace whose project is local.
|
||||||
///
|
///
|
||||||
/// If the given workspace has a local project, then it will be passed
|
/// If the given workspace has a local project, then it will be passed
|
||||||
@ -1188,7 +1192,6 @@ impl Workspace {
|
|||||||
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
|
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
|
||||||
let mut workspace = Workspace::new(
|
let mut workspace = Workspace::new(
|
||||||
Project::local(
|
Project::local(
|
||||||
false,
|
|
||||||
app_state.client.clone(),
|
app_state.client.clone(),
|
||||||
app_state.user_store.clone(),
|
app_state.user_store.clone(),
|
||||||
app_state.project_store.clone(),
|
app_state.project_store.clone(),
|
||||||
@ -1238,7 +1241,7 @@ impl Workspace {
|
|||||||
_: &CloseWindow,
|
_: &CloseWindow,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Option<Task<Result<()>>> {
|
) -> Option<Task<Result<()>>> {
|
||||||
let prepare = self.prepare_to_close(cx);
|
let prepare = self.prepare_to_close(false, cx);
|
||||||
Some(cx.spawn(|this, mut cx| async move {
|
Some(cx.spawn(|this, mut cx| async move {
|
||||||
if prepare.await? {
|
if prepare.await? {
|
||||||
this.update(&mut cx, |_, cx| {
|
this.update(&mut cx, |_, cx| {
|
||||||
@ -1250,8 +1253,44 @@ impl Workspace {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepare_to_close(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
|
pub fn prepare_to_close(
|
||||||
self.save_all_internal(true, cx)
|
&mut self,
|
||||||
|
quitting: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Task<Result<bool>> {
|
||||||
|
let active_call = self.active_call.clone();
|
||||||
|
let window_id = cx.window_id();
|
||||||
|
let workspace_count = cx
|
||||||
|
.window_ids()
|
||||||
|
.flat_map(|window_id| cx.root_view::<Workspace>(window_id))
|
||||||
|
.count();
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
if let Some(active_call) = active_call {
|
||||||
|
if !quitting
|
||||||
|
&& workspace_count == 1
|
||||||
|
&& active_call.read_with(&cx, |call, _| call.room().is_some())
|
||||||
|
{
|
||||||
|
let answer = cx
|
||||||
|
.prompt(
|
||||||
|
window_id,
|
||||||
|
PromptLevel::Warning,
|
||||||
|
"Do you want to leave the current call?",
|
||||||
|
&["Close window and hang up", "Cancel"],
|
||||||
|
)
|
||||||
|
.next()
|
||||||
|
.await;
|
||||||
|
if answer == Some(1) {
|
||||||
|
return anyhow::Ok(false);
|
||||||
|
} else {
|
||||||
|
active_call.update(&mut cx, |call, cx| call.hang_up(cx))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(this
|
||||||
|
.update(&mut cx, |this, cx| this.save_all_internal(true, cx))
|
||||||
|
.await?)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
|
fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
|
||||||
@ -1393,17 +1432,6 @@ impl Workspace {
|
|||||||
.update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
|
.update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_project_online(&mut self, action: &ToggleProjectOnline, cx: &mut ViewContext<Self>) {
|
|
||||||
let project = action
|
|
||||||
.project
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| self.project.clone());
|
|
||||||
project.update(cx, |project, cx| {
|
|
||||||
let public = !project.is_online();
|
|
||||||
project.set_online(public, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn project_path_for_path(
|
fn project_path_for_path(
|
||||||
&self,
|
&self,
|
||||||
abs_path: &Path,
|
abs_path: &Path,
|
||||||
@ -2068,46 +2096,12 @@ impl Workspace {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
|
pub fn is_following(&self, peer_id: PeerId) -> bool {
|
||||||
let theme = &cx.global::<Settings>().theme;
|
self.follower_states_by_leader.contains_key(&peer_id)
|
||||||
match &*self.client.status().borrow() {
|
|
||||||
client::Status::ConnectionError
|
|
||||||
| client::Status::ConnectionLost
|
|
||||||
| client::Status::Reauthenticating { .. }
|
|
||||||
| client::Status::Reconnecting { .. }
|
|
||||||
| client::Status::ReconnectionError { .. } => Some(
|
|
||||||
Container::new(
|
|
||||||
Align::new(
|
|
||||||
ConstrainedBox::new(
|
|
||||||
Svg::new("icons/cloud_slash_12.svg")
|
|
||||||
.with_color(theme.workspace.titlebar.offline_icon.color)
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
.with_width(theme.workspace.titlebar.offline_icon.width)
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
.with_style(theme.workspace.titlebar.offline_icon.container)
|
|
||||||
.boxed(),
|
|
||||||
),
|
|
||||||
client::Status::UpgradeRequired => Some(
|
|
||||||
Label::new(
|
|
||||||
"Please update Zed to collaborate".to_string(),
|
|
||||||
theme.workspace.titlebar.outdated_warning.text.clone(),
|
|
||||||
)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.workspace.titlebar.outdated_warning.container)
|
|
||||||
.aligned()
|
|
||||||
.boxed(),
|
|
||||||
),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
let project = &self.project.read(cx);
|
let project = &self.project.read(cx);
|
||||||
let replica_id = project.replica_id();
|
|
||||||
let mut worktree_root_names = String::new();
|
let mut worktree_root_names = String::new();
|
||||||
for (i, name) in project.worktree_root_names(cx).enumerate() {
|
for (i, name) in project.worktree_root_names(cx).enumerate() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@ -2129,7 +2123,7 @@ impl Workspace {
|
|||||||
|
|
||||||
enum TitleBar {}
|
enum TitleBar {}
|
||||||
ConstrainedBox::new(
|
ConstrainedBox::new(
|
||||||
MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
|
MouseEventHandler::<TitleBar>::new(0, cx, |_, _| {
|
||||||
Container::new(
|
Container::new(
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(
|
.with_child(
|
||||||
@ -2138,21 +2132,10 @@ impl Workspace {
|
|||||||
.left()
|
.left()
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_children(
|
||||||
Align::new(
|
self.titlebar_item
|
||||||
Flex::row()
|
.as_ref()
|
||||||
.with_children(self.render_collaborators(theme, cx))
|
.map(|item| ChildView::new(item).aligned().right().boxed()),
|
||||||
.with_children(self.render_current_user(
|
|
||||||
self.user_store.read(cx).current_user().as_ref(),
|
|
||||||
replica_id,
|
|
||||||
theme,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
.with_children(self.render_connection_status(cx))
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
.right()
|
|
||||||
.boxed(),
|
|
||||||
)
|
)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
@ -2221,125 +2204,6 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
|
|
||||||
let mut collaborators = self
|
|
||||||
.project
|
|
||||||
.read(cx)
|
|
||||||
.collaborators()
|
|
||||||
.values()
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id);
|
|
||||||
collaborators
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|collaborator| {
|
|
||||||
Some(self.render_avatar(
|
|
||||||
collaborator.user.avatar.clone()?,
|
|
||||||
collaborator.replica_id,
|
|
||||||
Some((collaborator.peer_id, &collaborator.user.github_login)),
|
|
||||||
theme,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_current_user(
|
|
||||||
&self,
|
|
||||||
user: Option<&Arc<User>>,
|
|
||||||
replica_id: ReplicaId,
|
|
||||||
theme: &Theme,
|
|
||||||
cx: &mut RenderContext<Self>,
|
|
||||||
) -> Option<ElementBox> {
|
|
||||||
let status = *self.client.status().borrow();
|
|
||||||
if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
|
|
||||||
Some(self.render_avatar(avatar, replica_id, None, theme, cx))
|
|
||||||
} else if matches!(status, client::Status::UpgradeRequired) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(
|
|
||||||
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
|
|
||||||
let style = theme
|
|
||||||
.workspace
|
|
||||||
.titlebar
|
|
||||||
.sign_in_prompt
|
|
||||||
.style_for(state, false);
|
|
||||||
Label::new("Sign in".to_string(), style.text.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(style.container)
|
|
||||||
.boxed()
|
|
||||||
})
|
|
||||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
|
||||||
.aligned()
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_avatar(
|
|
||||||
&self,
|
|
||||||
avatar: Arc<ImageData>,
|
|
||||||
replica_id: ReplicaId,
|
|
||||||
peer: Option<(PeerId, &str)>,
|
|
||||||
theme: &Theme,
|
|
||||||
cx: &mut RenderContext<Self>,
|
|
||||||
) -> ElementBox {
|
|
||||||
let replica_color = theme.editor.replica_selection_style(replica_id).cursor;
|
|
||||||
let is_followed = peer.map_or(false, |(peer_id, _)| {
|
|
||||||
self.follower_states_by_leader.contains_key(&peer_id)
|
|
||||||
});
|
|
||||||
let mut avatar_style = theme.workspace.titlebar.avatar;
|
|
||||||
if is_followed {
|
|
||||||
avatar_style.border = Border::all(1.0, replica_color);
|
|
||||||
}
|
|
||||||
let content = Stack::new()
|
|
||||||
.with_child(
|
|
||||||
Image::new(avatar)
|
|
||||||
.with_style(avatar_style)
|
|
||||||
.constrained()
|
|
||||||
.with_width(theme.workspace.titlebar.avatar_width)
|
|
||||||
.aligned()
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
AvatarRibbon::new(replica_color)
|
|
||||||
.constrained()
|
|
||||||
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
|
|
||||||
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
|
|
||||||
.aligned()
|
|
||||||
.bottom()
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
.constrained()
|
|
||||||
.with_width(theme.workspace.titlebar.avatar_width)
|
|
||||||
.contained()
|
|
||||||
.with_margin_left(theme.workspace.titlebar.avatar_margin)
|
|
||||||
.boxed();
|
|
||||||
|
|
||||||
if let Some((peer_id, peer_github_login)) = peer {
|
|
||||||
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
|
||||||
.on_click(MouseButton::Left, move |_, cx| {
|
|
||||||
cx.dispatch_action(ToggleFollow(peer_id))
|
|
||||||
})
|
|
||||||
.with_tooltip::<ToggleFollow, _>(
|
|
||||||
peer_id.0 as usize,
|
|
||||||
if is_followed {
|
|
||||||
format!("Unfollow {}", peer_github_login)
|
|
||||||
} else {
|
|
||||||
format!("Follow {}", peer_github_login)
|
|
||||||
},
|
|
||||||
Some(Box::new(FollowNextCollaborator)),
|
|
||||||
theme.tooltip.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
|
fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
|
||||||
if self.project.read(cx).is_read_only() {
|
if self.project.read(cx).is_read_only() {
|
||||||
enum DisconnectedOverlay {}
|
enum DisconnectedOverlay {}
|
||||||
@ -2698,6 +2562,7 @@ impl View for Workspace {
|
|||||||
.with_child(
|
.with_child(
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child({
|
.with_child({
|
||||||
|
let project = self.project.clone();
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children(
|
.with_children(
|
||||||
if self.left_sidebar.read(cx).active_item().is_some() {
|
if self.left_sidebar.read(cx).active_item().is_some() {
|
||||||
@ -2715,9 +2580,11 @@ impl View for Workspace {
|
|||||||
Flex::column()
|
Flex::column()
|
||||||
.with_child(
|
.with_child(
|
||||||
FlexItem::new(self.center.render(
|
FlexItem::new(self.center.render(
|
||||||
|
&project,
|
||||||
&theme,
|
&theme,
|
||||||
&self.follower_states_by_leader,
|
&self.follower_states_by_leader,
|
||||||
self.project.read(cx).collaborators(),
|
self.active_call.as_ref(),
|
||||||
|
cx,
|
||||||
))
|
))
|
||||||
.flex(1., true)
|
.flex(1., true)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
@ -2814,87 +2681,6 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AvatarRibbon {
|
|
||||||
color: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AvatarRibbon {
|
|
||||||
pub fn new(color: Color) -> AvatarRibbon {
|
|
||||||
AvatarRibbon { color }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for AvatarRibbon {
|
|
||||||
type LayoutState = ();
|
|
||||||
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: gpui::SizeConstraint,
|
|
||||||
_: &mut gpui::LayoutContext,
|
|
||||||
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
|
|
||||||
(constraint.max, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: gpui::geometry::rect::RectF,
|
|
||||||
_: gpui::geometry::rect::RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
cx: &mut gpui::PaintContext,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
let mut path = PathBuilder::new();
|
|
||||||
path.reset(bounds.lower_left());
|
|
||||||
path.curve_to(
|
|
||||||
bounds.origin() + vec2f(bounds.height(), 0.),
|
|
||||||
bounds.origin(),
|
|
||||||
);
|
|
||||||
path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
|
|
||||||
path.curve_to(bounds.lower_right(), bounds.upper_right());
|
|
||||||
path.line_to(bounds.lower_left());
|
|
||||||
cx.scene.push_path(path.build(self.color, None));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
_: &gpui::Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
_: &mut gpui::EventContext,
|
|
||||||
) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
_: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &gpui::MeasurementContext,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
bounds: gpui::geometry::rect::RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &gpui::DebugContext,
|
|
||||||
) -> gpui::json::Value {
|
|
||||||
json::json!({
|
|
||||||
"type": "AvatarRibbon",
|
|
||||||
"bounds": bounds.to_json(),
|
|
||||||
"color": self.color.to_json(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for OpenPaths {
|
impl std::fmt::Debug for OpenPaths {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("OpenPaths")
|
f.debug_struct("OpenPaths")
|
||||||
@ -2964,7 +2750,6 @@ pub fn open_paths(
|
|||||||
|
|
||||||
cx.add_window((app_state.build_window_options)(), |cx| {
|
cx.add_window((app_state.build_window_options)(), |cx| {
|
||||||
let project = Project::local(
|
let project = Project::local(
|
||||||
false,
|
|
||||||
app_state.client.clone(),
|
app_state.client.clone(),
|
||||||
app_state.user_store.clone(),
|
app_state.user_store.clone(),
|
||||||
app_state.project_store.clone(),
|
app_state.project_store.clone(),
|
||||||
@ -2989,44 +2774,14 @@ pub fn open_paths(
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Some(project) = new_project {
|
|
||||||
project
|
|
||||||
.update(&mut cx, |project, cx| project.restore_state(cx))
|
|
||||||
.await
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
(workspace, items)
|
(workspace, items)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn join_project(
|
|
||||||
contact: Arc<Contact>,
|
|
||||||
project_index: usize,
|
|
||||||
app_state: &Arc<AppState>,
|
|
||||||
cx: &mut MutableAppContext,
|
|
||||||
) {
|
|
||||||
let project_id = contact.projects[project_index].id;
|
|
||||||
|
|
||||||
for window_id in cx.window_ids().collect::<Vec<_>>() {
|
|
||||||
if let Some(workspace) = cx.root_view::<Workspace>(window_id) {
|
|
||||||
if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) {
|
|
||||||
cx.activate_window(window_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.add_window((app_state.build_window_options)(), |cx| {
|
|
||||||
WaitingRoom::new(contact, project_index, app_state.clone(), cx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
|
let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
|
||||||
let mut workspace = Workspace::new(
|
let mut workspace = Workspace::new(
|
||||||
Project::local(
|
Project::local(
|
||||||
false,
|
|
||||||
app_state.client.clone(),
|
app_state.client.clone(),
|
||||||
app_state.user_store.clone(),
|
app_state.user_store.clone(),
|
||||||
app_state.project_store.clone(),
|
app_state.project_store.clone(),
|
||||||
@ -3236,7 +2991,7 @@ mod tests {
|
|||||||
// When there are no dirty items, there's nothing to do.
|
// When there are no dirty items, there's nothing to do.
|
||||||
let item1 = cx.add_view(&workspace, |_| TestItem::new());
|
let item1 = cx.add_view(&workspace, |_| TestItem::new());
|
||||||
workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
|
workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
|
||||||
let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
|
let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
|
||||||
assert!(task.await.unwrap());
|
assert!(task.await.unwrap());
|
||||||
|
|
||||||
// When there are dirty untitled items, prompt to save each one. If the user
|
// When there are dirty untitled items, prompt to save each one. If the user
|
||||||
@ -3256,7 +3011,7 @@ mod tests {
|
|||||||
w.add_item(Box::new(item2.clone()), cx);
|
w.add_item(Box::new(item2.clone()), cx);
|
||||||
w.add_item(Box::new(item3.clone()), cx);
|
w.add_item(Box::new(item3.clone()), cx);
|
||||||
});
|
});
|
||||||
let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
|
let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
|
||||||
cx.foreground().run_until_parked();
|
cx.foreground().run_until_parked();
|
||||||
cx.simulate_prompt_answer(window_id, 2 /* cancel */);
|
cx.simulate_prompt_answer(window_id, 2 /* cancel */);
|
||||||
cx.foreground().run_until_parked();
|
cx.foreground().run_until_parked();
|
||||||
|
@ -19,15 +19,15 @@ activity_indicator = { path = "../activity_indicator" }
|
|||||||
assets = { path = "../assets" }
|
assets = { path = "../assets" }
|
||||||
auto_update = { path = "../auto_update" }
|
auto_update = { path = "../auto_update" }
|
||||||
breadcrumbs = { path = "../breadcrumbs" }
|
breadcrumbs = { path = "../breadcrumbs" }
|
||||||
|
call = { path = "../call" }
|
||||||
chat_panel = { path = "../chat_panel" }
|
chat_panel = { path = "../chat_panel" }
|
||||||
cli = { path = "../cli" }
|
cli = { path = "../cli" }
|
||||||
|
collab_ui = { path = "../collab_ui" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
command_palette = { path = "../command_palette" }
|
command_palette = { path = "../command_palette" }
|
||||||
context_menu = { path = "../context_menu" }
|
context_menu = { path = "../context_menu" }
|
||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
contacts_panel = { path = "../contacts_panel" }
|
|
||||||
contacts_status_item = { path = "../contacts_status_item" }
|
|
||||||
diagnostics = { path = "../diagnostics" }
|
diagnostics = { path = "../diagnostics" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
file_finder = { path = "../file_finder" }
|
file_finder = { path = "../file_finder" }
|
||||||
@ -105,17 +105,19 @@ tree-sitter-html = "0.19.0"
|
|||||||
url = "2.2"
|
url = "2.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
text = { path = "../text", features = ["test-support"] }
|
call = { path = "../call", features = ["test-support"] }
|
||||||
|
client = { path = "../client", features = ["test-support"] }
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
client = { path = "../client", features = ["test-support"] }
|
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
|
text = { path = "../text", features = ["test-support"] }
|
||||||
util = { path = "../util", features = ["test-support"] }
|
util = { path = "../util", features = ["test-support"] }
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
|
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||||
unindent = "0.1.7"
|
unindent = "0.1.7"
|
||||||
|
@ -112,7 +112,6 @@ fn main() {
|
|||||||
go_to_line::init(cx);
|
go_to_line::init(cx);
|
||||||
file_finder::init(cx);
|
file_finder::init(cx);
|
||||||
chat_panel::init(cx);
|
chat_panel::init(cx);
|
||||||
contacts_panel::init(cx);
|
|
||||||
outline::init(cx);
|
outline::init(cx);
|
||||||
project_symbols::init(cx);
|
project_symbols::init(cx);
|
||||||
project_panel::init(cx);
|
project_panel::init(cx);
|
||||||
@ -138,11 +137,11 @@ fn main() {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
let project_store = cx.add_model(|_| ProjectStore::new());
|
||||||
let db = cx.background().block(db);
|
let db = cx.background().block(db);
|
||||||
client.start_telemetry(db.clone());
|
client.start_telemetry(db.clone());
|
||||||
client.report_event("start app", Default::default());
|
client.report_event("start app", Default::default());
|
||||||
|
|
||||||
let project_store = cx.add_model(|_| ProjectStore::new(db.clone()));
|
|
||||||
let app_state = Arc::new(AppState {
|
let app_state = Arc::new(AppState {
|
||||||
languages,
|
languages,
|
||||||
themes,
|
themes,
|
||||||
@ -159,6 +158,7 @@ fn main() {
|
|||||||
journal::init(app_state.clone(), cx);
|
journal::init(app_state.clone(), cx);
|
||||||
theme_selector::init(app_state.clone(), cx);
|
theme_selector::init(app_state.clone(), cx);
|
||||||
zed::init(&app_state, cx);
|
zed::init(&app_state, cx);
|
||||||
|
collab_ui::init(app_state.clone(), cx);
|
||||||
|
|
||||||
cx.set_menus(menus::menus());
|
cx.set_menus(menus::menus());
|
||||||
|
|
||||||
|
@ -244,10 +244,6 @@ pub fn menus() -> Vec<Menu<'static>> {
|
|||||||
name: "Project Panel",
|
name: "Project Panel",
|
||||||
action: Box::new(project_panel::ToggleFocus),
|
action: Box::new(project_panel::ToggleFocus),
|
||||||
},
|
},
|
||||||
MenuItem::Action {
|
|
||||||
name: "Contacts Panel",
|
|
||||||
action: Box::new(contacts_panel::ToggleFocus),
|
|
||||||
},
|
|
||||||
MenuItem::Action {
|
MenuItem::Action {
|
||||||
name: "Command Palette",
|
name: "Command Palette",
|
||||||
action: Box::new(command_palette::Toggle),
|
action: Box::new(command_palette::Toggle),
|
||||||
|
@ -10,9 +10,8 @@ use anyhow::{anyhow, Context, Result};
|
|||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use breadcrumbs::Breadcrumbs;
|
use breadcrumbs::Breadcrumbs;
|
||||||
pub use client;
|
pub use client;
|
||||||
|
use collab_ui::CollabTitlebarItem;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
pub use contacts_panel;
|
|
||||||
use contacts_panel::ContactsPanel;
|
|
||||||
pub use editor;
|
pub use editor;
|
||||||
use editor::{Editor, MultiBuffer};
|
use editor::{Editor, MultiBuffer};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@ -214,15 +213,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
|||||||
workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
|
workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
cx.add_action(
|
|
||||||
|workspace: &mut Workspace,
|
|
||||||
_: &contacts_panel::ToggleFocus,
|
|
||||||
cx: &mut ViewContext<Workspace>| {
|
|
||||||
workspace.toggle_sidebar_item_focus(SidebarSide::Right, 0, cx);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
activity_indicator::init(cx);
|
activity_indicator::init(cx);
|
||||||
|
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||||
settings::KeymapFileContent::load_defaults(cx);
|
settings::KeymapFileContent::load_defaults(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,7 +224,8 @@ pub fn initialize_workspace(
|
|||||||
app_state: &Arc<AppState>,
|
app_state: &Arc<AppState>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
cx.subscribe(&cx.handle(), {
|
let workspace_handle = cx.handle();
|
||||||
|
cx.subscribe(&workspace_handle, {
|
||||||
move |_, _, event, cx| {
|
move |_, _, event, cx| {
|
||||||
if let workspace::Event::PaneAdded(pane) = event {
|
if let workspace::Event::PaneAdded(pane) = event {
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
@ -285,16 +279,11 @@ pub fn initialize_workspace(
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
|
let collab_titlebar_item =
|
||||||
let contact_panel = cx.add_view(|cx| {
|
cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx));
|
||||||
ContactsPanel::new(
|
workspace.set_titlebar_item(collab_titlebar_item, cx);
|
||||||
app_state.user_store.clone(),
|
|
||||||
app_state.project_store.clone(),
|
|
||||||
workspace.weak_handle(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
|
let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
|
||||||
workspace.left_sidebar().update(cx, |sidebar, cx| {
|
workspace.left_sidebar().update(cx, |sidebar, cx| {
|
||||||
sidebar.add_item(
|
sidebar.add_item(
|
||||||
"icons/folder_tree_16.svg",
|
"icons/folder_tree_16.svg",
|
||||||
@ -303,14 +292,6 @@ pub fn initialize_workspace(
|
|||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
workspace.right_sidebar().update(cx, |sidebar, cx| {
|
|
||||||
sidebar.add_item(
|
|
||||||
"icons/user_group_16.svg",
|
|
||||||
"Contacts Panel".to_string(),
|
|
||||||
contact_panel,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let diagnostic_summary =
|
let diagnostic_summary =
|
||||||
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
|
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
|
||||||
@ -363,7 +344,9 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
|
|||||||
// If the user cancels any save prompt, then keep the app open.
|
// If the user cancels any save prompt, then keep the app open.
|
||||||
for workspace in workspaces {
|
for workspace in workspaces {
|
||||||
if !workspace
|
if !workspace
|
||||||
.update(&mut cx, |workspace, cx| workspace.prepare_to_close(cx))
|
.update(&mut cx, |workspace, cx| {
|
||||||
|
workspace.prepare_to_close(true, cx)
|
||||||
|
})
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@ -1772,6 +1755,7 @@ mod tests {
|
|||||||
let state = Arc::get_mut(&mut app_state).unwrap();
|
let state = Arc::get_mut(&mut app_state).unwrap();
|
||||||
state.initialize_workspace = initialize_workspace;
|
state.initialize_workspace = initialize_workspace;
|
||||||
state.build_window_options = build_window_options;
|
state.build_window_options = build_window_options;
|
||||||
|
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||||
workspace::init(app_state.clone(), cx);
|
workspace::init(app_state.clone(), cx);
|
||||||
editor::init(cx);
|
editor::init(cx);
|
||||||
pane::init(cx);
|
pane::init(cx);
|
||||||
|
@ -2,7 +2,6 @@ import Theme from "../themes/common/theme";
|
|||||||
import chatPanel from "./chatPanel";
|
import chatPanel from "./chatPanel";
|
||||||
import { text } from "./components";
|
import { text } from "./components";
|
||||||
import contactFinder from "./contactFinder";
|
import contactFinder from "./contactFinder";
|
||||||
import contactsPanel from "./contactsPanel";
|
|
||||||
import contactsPopover from "./contactsPopover";
|
import contactsPopover from "./contactsPopover";
|
||||||
import commandPalette from "./commandPalette";
|
import commandPalette from "./commandPalette";
|
||||||
import editor from "./editor";
|
import editor from "./editor";
|
||||||
@ -14,8 +13,11 @@ import contextMenu from "./contextMenu";
|
|||||||
import projectDiagnostics from "./projectDiagnostics";
|
import projectDiagnostics from "./projectDiagnostics";
|
||||||
import contactNotification from "./contactNotification";
|
import contactNotification from "./contactNotification";
|
||||||
import updateNotification from "./updateNotification";
|
import updateNotification from "./updateNotification";
|
||||||
|
import projectSharedNotification from "./projectSharedNotification";
|
||||||
import tooltip from "./tooltip";
|
import tooltip from "./tooltip";
|
||||||
import terminal from "./terminal";
|
import terminal from "./terminal";
|
||||||
|
import contactList from "./contactList";
|
||||||
|
import incomingCallNotification from "./incomingCallNotification";
|
||||||
|
|
||||||
export const panel = {
|
export const panel = {
|
||||||
padding: { top: 12, bottom: 12 },
|
padding: { top: 12, bottom: 12 },
|
||||||
@ -36,7 +38,7 @@ export default function app(theme: Theme): Object {
|
|||||||
projectPanel: projectPanel(theme),
|
projectPanel: projectPanel(theme),
|
||||||
chatPanel: chatPanel(theme),
|
chatPanel: chatPanel(theme),
|
||||||
contactsPopover: contactsPopover(theme),
|
contactsPopover: contactsPopover(theme),
|
||||||
contactsPanel: contactsPanel(theme),
|
contactList: contactList(theme),
|
||||||
contactFinder: contactFinder(theme),
|
contactFinder: contactFinder(theme),
|
||||||
search: search(theme),
|
search: search(theme),
|
||||||
breadcrumbs: {
|
breadcrumbs: {
|
||||||
@ -47,6 +49,8 @@ export default function app(theme: Theme): Object {
|
|||||||
},
|
},
|
||||||
contactNotification: contactNotification(theme),
|
contactNotification: contactNotification(theme),
|
||||||
updateNotification: updateNotification(theme),
|
updateNotification: updateNotification(theme),
|
||||||
|
projectSharedNotification: projectSharedNotification(theme),
|
||||||
|
incomingCallNotification: incomingCallNotification(theme),
|
||||||
tooltip: tooltip(theme),
|
tooltip: tooltip(theme),
|
||||||
terminal: terminal(theme),
|
terminal: terminal(theme),
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import Theme from "../themes/common/theme";
|
import Theme from "../themes/common/theme";
|
||||||
import picker from "./picker";
|
import picker from "./picker";
|
||||||
import { backgroundColor, iconColor } from "./components";
|
import { backgroundColor, border, iconColor, player, text } from "./components";
|
||||||
|
|
||||||
export default function contactFinder(theme: Theme) {
|
export default function contactFinder(theme: Theme) {
|
||||||
|
const sideMargin = 6;
|
||||||
const contactButton = {
|
const contactButton = {
|
||||||
background: backgroundColor(theme, 100),
|
background: backgroundColor(theme, 100),
|
||||||
color: iconColor(theme, "primary"),
|
color: iconColor(theme, "primary"),
|
||||||
@ -12,7 +13,31 @@ export default function contactFinder(theme: Theme) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...picker(theme),
|
picker: {
|
||||||
|
item: {
|
||||||
|
...picker(theme).item,
|
||||||
|
margin: { left: sideMargin, right: sideMargin }
|
||||||
|
},
|
||||||
|
empty: picker(theme).empty,
|
||||||
|
inputEditor: {
|
||||||
|
background: backgroundColor(theme, 500),
|
||||||
|
cornerRadius: 6,
|
||||||
|
text: text(theme, "mono", "primary"),
|
||||||
|
placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
|
||||||
|
selection: player(theme, 1).selection,
|
||||||
|
border: border(theme, "secondary"),
|
||||||
|
padding: {
|
||||||
|
bottom: 4,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
top: 4,
|
||||||
|
},
|
||||||
|
margin: {
|
||||||
|
left: sideMargin,
|
||||||
|
right: sideMargin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
rowHeight: 28,
|
rowHeight: 28,
|
||||||
contactAvatar: {
|
contactAvatar: {
|
||||||
cornerRadius: 10,
|
cornerRadius: 10,
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import Theme from "../themes/common/theme";
|
import Theme from "../themes/common/theme";
|
||||||
import { panel } from "./app";
|
import { backgroundColor, border, borderColor, iconColor, player, text } from "./components";
|
||||||
import {
|
|
||||||
backgroundColor,
|
|
||||||
border,
|
|
||||||
borderColor,
|
|
||||||
iconColor,
|
|
||||||
player,
|
|
||||||
text,
|
|
||||||
} from "./components";
|
|
||||||
|
|
||||||
export default function contactsPanel(theme: Theme) {
|
export default function contactList(theme: Theme) {
|
||||||
const nameMargin = 8;
|
const nameMargin = 8;
|
||||||
const sidePadding = 12;
|
const sidePadding = 12;
|
||||||
|
|
||||||
|
const contactButton = {
|
||||||
|
background: backgroundColor(theme, 100),
|
||||||
|
color: iconColor(theme, "primary"),
|
||||||
|
iconWidth: 8,
|
||||||
|
buttonWidth: 16,
|
||||||
|
cornerRadius: 8,
|
||||||
|
};
|
||||||
const projectRow = {
|
const projectRow = {
|
||||||
guestAvatarSpacing: 4,
|
guestAvatarSpacing: 4,
|
||||||
height: 24,
|
height: 24,
|
||||||
@ -39,17 +38,7 @@ export default function contactsPanel(theme: Theme) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const contactButton = {
|
|
||||||
background: backgroundColor(theme, 100),
|
|
||||||
color: iconColor(theme, "primary"),
|
|
||||||
iconWidth: 8,
|
|
||||||
buttonWidth: 16,
|
|
||||||
cornerRadius: 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...panel,
|
|
||||||
padding: { top: panel.padding.top, bottom: 0 },
|
|
||||||
userQueryEditor: {
|
userQueryEditor: {
|
||||||
background: backgroundColor(theme, 500),
|
background: backgroundColor(theme, 500),
|
||||||
cornerRadius: 6,
|
cornerRadius: 6,
|
||||||
@ -64,28 +53,20 @@ export default function contactsPanel(theme: Theme) {
|
|||||||
top: 4,
|
top: 4,
|
||||||
},
|
},
|
||||||
margin: {
|
margin: {
|
||||||
left: sidePadding,
|
left: 6
|
||||||
right: sidePadding,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
userQueryEditorHeight: 32,
|
userQueryEditorHeight: 33,
|
||||||
addContactButton: {
|
addContactButton: {
|
||||||
margin: { left: 6, right: 12 },
|
|
||||||
color: iconColor(theme, "primary"),
|
color: iconColor(theme, "primary"),
|
||||||
buttonWidth: 16,
|
buttonWidth: 28,
|
||||||
iconWidth: 16,
|
iconWidth: 16,
|
||||||
},
|
},
|
||||||
privateButton: {
|
|
||||||
iconWidth: 12,
|
|
||||||
color: iconColor(theme, "primary"),
|
|
||||||
cornerRadius: 5,
|
|
||||||
buttonWidth: 12,
|
|
||||||
},
|
|
||||||
rowHeight: 28,
|
rowHeight: 28,
|
||||||
sectionIconSize: 8,
|
sectionIconSize: 8,
|
||||||
headerRow: {
|
headerRow: {
|
||||||
...text(theme, "mono", "secondary", { size: "sm" }),
|
...text(theme, "mono", "secondary", { size: "sm" }),
|
||||||
margin: { top: 14 },
|
margin: { top: 6 },
|
||||||
padding: {
|
padding: {
|
||||||
left: sidePadding,
|
left: sidePadding,
|
||||||
right: sidePadding,
|
right: sidePadding,
|
||||||
@ -95,6 +76,26 @@ export default function contactsPanel(theme: Theme) {
|
|||||||
background: backgroundColor(theme, 100, "active"),
|
background: backgroundColor(theme, 100, "active"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
leaveCall: {
|
||||||
|
background: backgroundColor(theme, 100),
|
||||||
|
border: border(theme, "secondary"),
|
||||||
|
cornerRadius: 6,
|
||||||
|
margin: {
|
||||||
|
top: 1,
|
||||||
|
},
|
||||||
|
padding: {
|
||||||
|
top: 1,
|
||||||
|
bottom: 1,
|
||||||
|
left: 7,
|
||||||
|
right: 7,
|
||||||
|
},
|
||||||
|
...text(theme, "sans", "secondary", { size: "xs" }),
|
||||||
|
hover: {
|
||||||
|
...text(theme, "sans", "active", { size: "xs" }),
|
||||||
|
background: backgroundColor(theme, "on300", "hovered"),
|
||||||
|
border: border(theme, "primary"),
|
||||||
|
},
|
||||||
|
},
|
||||||
contactRow: {
|
contactRow: {
|
||||||
padding: {
|
padding: {
|
||||||
left: sidePadding,
|
left: sidePadding,
|
||||||
@ -104,20 +105,22 @@ export default function contactsPanel(theme: Theme) {
|
|||||||
background: backgroundColor(theme, 100, "active"),
|
background: backgroundColor(theme, 100, "active"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
treeBranch: {
|
|
||||||
color: borderColor(theme, "active"),
|
|
||||||
width: 1,
|
|
||||||
hover: {
|
|
||||||
color: borderColor(theme, "active"),
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
color: borderColor(theme, "active"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
contactAvatar: {
|
contactAvatar: {
|
||||||
cornerRadius: 10,
|
cornerRadius: 10,
|
||||||
width: 18,
|
width: 18,
|
||||||
},
|
},
|
||||||
|
contactStatusFree: {
|
||||||
|
cornerRadius: 4,
|
||||||
|
padding: 4,
|
||||||
|
margin: { top: 12, left: 12 },
|
||||||
|
background: iconColor(theme, "ok"),
|
||||||
|
},
|
||||||
|
contactStatusBusy: {
|
||||||
|
cornerRadius: 4,
|
||||||
|
padding: 4,
|
||||||
|
margin: { top: 12, left: 12 },
|
||||||
|
background: iconColor(theme, "error"),
|
||||||
|
},
|
||||||
contactUsername: {
|
contactUsername: {
|
||||||
...text(theme, "mono", "primary", { size: "sm" }),
|
...text(theme, "mono", "primary", { size: "sm" }),
|
||||||
margin: {
|
margin: {
|
||||||
@ -136,6 +139,19 @@ export default function contactsPanel(theme: Theme) {
|
|||||||
background: backgroundColor(theme, 100),
|
background: backgroundColor(theme, 100),
|
||||||
color: iconColor(theme, "muted"),
|
color: iconColor(theme, "muted"),
|
||||||
},
|
},
|
||||||
|
callingIndicator: {
|
||||||
|
...text(theme, "mono", "muted", { size: "xs" })
|
||||||
|
},
|
||||||
|
treeBranch: {
|
||||||
|
color: borderColor(theme, "active"),
|
||||||
|
width: 1,
|
||||||
|
hover: {
|
||||||
|
color: borderColor(theme, "active"),
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
color: borderColor(theme, "active"),
|
||||||
|
},
|
||||||
|
},
|
||||||
projectRow: {
|
projectRow: {
|
||||||
...projectRow,
|
...projectRow,
|
||||||
background: backgroundColor(theme, 300),
|
background: backgroundColor(theme, 300),
|
||||||
@ -150,16 +166,5 @@ export default function contactsPanel(theme: Theme) {
|
|||||||
background: backgroundColor(theme, 300, "active"),
|
background: backgroundColor(theme, 300, "active"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
inviteRow: {
|
}
|
||||||
padding: {
|
|
||||||
left: sidePadding,
|
|
||||||
right: sidePadding,
|
|
||||||
},
|
|
||||||
border: { top: true, width: 1, color: borderColor(theme, "primary") },
|
|
||||||
text: text(theme, "sans", "secondary", { size: "sm" }),
|
|
||||||
hover: {
|
|
||||||
text: text(theme, "sans", "active", { size: "sm" }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
@ -1,8 +1,28 @@
|
|||||||
import Theme from "../themes/common/theme";
|
import Theme from "../themes/common/theme";
|
||||||
import { backgroundColor } from "./components";
|
import { backgroundColor, border, borderColor, popoverShadow, text } from "./components";
|
||||||
|
|
||||||
export default function workspace(theme: Theme) {
|
export default function contactsPopover(theme: Theme) {
|
||||||
|
const sidePadding = 12;
|
||||||
return {
|
return {
|
||||||
background: backgroundColor(theme, 300),
|
background: backgroundColor(theme, 300, "base"),
|
||||||
|
cornerRadius: 6,
|
||||||
|
padding: { top: 6 },
|
||||||
|
margin: { top: -6 },
|
||||||
|
shadow: popoverShadow(theme),
|
||||||
|
border: border(theme, "primary"),
|
||||||
|
width: 300,
|
||||||
|
height: 400,
|
||||||
|
inviteRowHeight: 28,
|
||||||
|
inviteRow: {
|
||||||
|
padding: {
|
||||||
|
left: sidePadding,
|
||||||
|
right: sidePadding,
|
||||||
|
},
|
||||||
|
border: { top: true, width: 1, color: borderColor(theme, "primary") },
|
||||||
|
text: text(theme, "sans", "secondary", { size: "sm" }),
|
||||||
|
hover: {
|
||||||
|
text: text(theme, "sans", "active", { size: "sm" }),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
44
styles/src/styleTree/incomingCallNotification.ts
Normal file
44
styles/src/styleTree/incomingCallNotification.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import Theme from "../themes/common/theme";
|
||||||
|
import { backgroundColor, borderColor, text } from "./components";
|
||||||
|
|
||||||
|
export default function incomingCallNotification(theme: Theme): Object {
|
||||||
|
const avatarSize = 48;
|
||||||
|
return {
|
||||||
|
windowHeight: 74,
|
||||||
|
windowWidth: 380,
|
||||||
|
background: backgroundColor(theme, 300),
|
||||||
|
callerContainer: {
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
callerAvatar: {
|
||||||
|
height: avatarSize,
|
||||||
|
width: avatarSize,
|
||||||
|
cornerRadius: avatarSize / 2,
|
||||||
|
},
|
||||||
|
callerMetadata: {
|
||||||
|
margin: { left: 10 },
|
||||||
|
},
|
||||||
|
callerUsername: {
|
||||||
|
...text(theme, "sans", "active", { size: "sm", weight: "bold" }),
|
||||||
|
margin: { top: -3 },
|
||||||
|
},
|
||||||
|
callerMessage: {
|
||||||
|
...text(theme, "sans", "secondary", { size: "xs" }),
|
||||||
|
margin: { top: -3 },
|
||||||
|
},
|
||||||
|
worktreeRoots: {
|
||||||
|
...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }),
|
||||||
|
margin: { top: -3 },
|
||||||
|
},
|
||||||
|
buttonWidth: 96,
|
||||||
|
acceptButton: {
|
||||||
|
background: backgroundColor(theme, "ok", "active"),
|
||||||
|
border: { left: true, bottom: true, width: 1, color: borderColor(theme, "primary") },
|
||||||
|
...text(theme, "sans", "ok", { size: "xs", weight: "extra_bold" })
|
||||||
|
},
|
||||||
|
declineButton: {
|
||||||
|
border: { left: true, width: 1, color: borderColor(theme, "primary") },
|
||||||
|
...text(theme, "sans", "error", { size: "xs", weight: "extra_bold" })
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
44
styles/src/styleTree/projectSharedNotification.ts
Normal file
44
styles/src/styleTree/projectSharedNotification.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import Theme from "../themes/common/theme";
|
||||||
|
import { backgroundColor, borderColor, text } from "./components";
|
||||||
|
|
||||||
|
export default function projectSharedNotification(theme: Theme): Object {
|
||||||
|
const avatarSize = 48;
|
||||||
|
return {
|
||||||
|
windowHeight: 74,
|
||||||
|
windowWidth: 380,
|
||||||
|
background: backgroundColor(theme, 300),
|
||||||
|
ownerContainer: {
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
ownerAvatar: {
|
||||||
|
height: avatarSize,
|
||||||
|
width: avatarSize,
|
||||||
|
cornerRadius: avatarSize / 2,
|
||||||
|
},
|
||||||
|
ownerMetadata: {
|
||||||
|
margin: { left: 10 },
|
||||||
|
},
|
||||||
|
ownerUsername: {
|
||||||
|
...text(theme, "sans", "active", { size: "sm", weight: "bold" }),
|
||||||
|
margin: { top: -3 },
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
...text(theme, "sans", "secondary", { size: "xs" }),
|
||||||
|
margin: { top: -3 },
|
||||||
|
},
|
||||||
|
worktreeRoots: {
|
||||||
|
...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }),
|
||||||
|
margin: { top: -3 },
|
||||||
|
},
|
||||||
|
buttonWidth: 96,
|
||||||
|
openButton: {
|
||||||
|
background: backgroundColor(theme, "info", "active"),
|
||||||
|
border: { left: true, bottom: true, width: 1, color: borderColor(theme, "primary") },
|
||||||
|
...text(theme, "sans", "info", { size: "xs", weight: "extra_bold" })
|
||||||
|
},
|
||||||
|
dismissButton: {
|
||||||
|
border: { left: true, width: 1, color: borderColor(theme, "primary") },
|
||||||
|
...text(theme, "sans", "secondary", { size: "xs", weight: "extra_bold" })
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -16,6 +16,27 @@ export function workspaceBackground(theme: Theme) {
|
|||||||
|
|
||||||
export default function workspace(theme: Theme) {
|
export default function workspace(theme: Theme) {
|
||||||
const titlebarPadding = 6;
|
const titlebarPadding = 6;
|
||||||
|
const titlebarButton = {
|
||||||
|
background: backgroundColor(theme, 100),
|
||||||
|
border: border(theme, "secondary"),
|
||||||
|
cornerRadius: 6,
|
||||||
|
margin: {
|
||||||
|
top: 1,
|
||||||
|
},
|
||||||
|
padding: {
|
||||||
|
top: 1,
|
||||||
|
bottom: 1,
|
||||||
|
left: 7,
|
||||||
|
right: 7,
|
||||||
|
},
|
||||||
|
...text(theme, "sans", "secondary", { size: "xs" }),
|
||||||
|
hover: {
|
||||||
|
...text(theme, "sans", "active", { size: "xs" }),
|
||||||
|
background: backgroundColor(theme, "on300", "hovered"),
|
||||||
|
border: border(theme, "primary"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const avatarWidth = 18;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
background: backgroundColor(theme, 300),
|
background: backgroundColor(theme, 300),
|
||||||
@ -27,6 +48,14 @@ export default function workspace(theme: Theme) {
|
|||||||
padding: 12,
|
padding: 12,
|
||||||
...text(theme, "sans", "primary", { size: "lg" }),
|
...text(theme, "sans", "primary", { size: "lg" }),
|
||||||
},
|
},
|
||||||
|
externalLocationMessage: {
|
||||||
|
background: backgroundColor(theme, "info"),
|
||||||
|
border: border(theme, "secondary"),
|
||||||
|
cornerRadius: 6,
|
||||||
|
padding: 12,
|
||||||
|
margin: { bottom: 8, right: 8 },
|
||||||
|
...text(theme, "sans", "secondary", { size: "xs" }),
|
||||||
|
},
|
||||||
leaderBorderOpacity: 0.7,
|
leaderBorderOpacity: 0.7,
|
||||||
leaderBorderWidth: 2.0,
|
leaderBorderWidth: 2.0,
|
||||||
tabBar: tabBar(theme),
|
tabBar: tabBar(theme),
|
||||||
@ -52,7 +81,7 @@ export default function workspace(theme: Theme) {
|
|||||||
},
|
},
|
||||||
statusBar: statusBar(theme),
|
statusBar: statusBar(theme),
|
||||||
titlebar: {
|
titlebar: {
|
||||||
avatarWidth: 18,
|
avatarWidth,
|
||||||
avatarMargin: 8,
|
avatarMargin: 8,
|
||||||
height: 33,
|
height: 33,
|
||||||
background: backgroundColor(theme, 100),
|
background: backgroundColor(theme, 100),
|
||||||
@ -62,12 +91,20 @@ export default function workspace(theme: Theme) {
|
|||||||
},
|
},
|
||||||
title: text(theme, "sans", "primary"),
|
title: text(theme, "sans", "primary"),
|
||||||
avatar: {
|
avatar: {
|
||||||
cornerRadius: 10,
|
cornerRadius: avatarWidth / 2,
|
||||||
border: {
|
border: {
|
||||||
color: "#00000088",
|
color: "#00000088",
|
||||||
width: 1,
|
width: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
inactiveAvatar: {
|
||||||
|
cornerRadius: avatarWidth / 2,
|
||||||
|
border: {
|
||||||
|
color: "#00000088",
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
grayscale: true,
|
||||||
|
},
|
||||||
avatarRibbon: {
|
avatarRibbon: {
|
||||||
height: 3,
|
height: 3,
|
||||||
width: 12,
|
width: 12,
|
||||||
@ -76,24 +113,7 @@ export default function workspace(theme: Theme) {
|
|||||||
},
|
},
|
||||||
border: border(theme, "primary", { bottom: true, overlay: true }),
|
border: border(theme, "primary", { bottom: true, overlay: true }),
|
||||||
signInPrompt: {
|
signInPrompt: {
|
||||||
background: backgroundColor(theme, 100),
|
...titlebarButton
|
||||||
border: border(theme, "secondary"),
|
|
||||||
cornerRadius: 6,
|
|
||||||
margin: {
|
|
||||||
top: 1,
|
|
||||||
},
|
|
||||||
padding: {
|
|
||||||
top: 1,
|
|
||||||
bottom: 1,
|
|
||||||
left: 7,
|
|
||||||
right: 7,
|
|
||||||
},
|
|
||||||
...text(theme, "sans", "secondary", { size: "xs" }),
|
|
||||||
hover: {
|
|
||||||
...text(theme, "sans", "active", { size: "xs" }),
|
|
||||||
background: backgroundColor(theme, "on300", "hovered"),
|
|
||||||
border: border(theme, "primary"),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
offlineIcon: {
|
offlineIcon: {
|
||||||
color: iconColor(theme, "secondary"),
|
color: iconColor(theme, "secondary"),
|
||||||
@ -118,6 +138,30 @@ export default function workspace(theme: Theme) {
|
|||||||
},
|
},
|
||||||
cornerRadius: 6,
|
cornerRadius: 6,
|
||||||
},
|
},
|
||||||
|
toggleContactsButton: {
|
||||||
|
cornerRadius: 6,
|
||||||
|
color: iconColor(theme, "secondary"),
|
||||||
|
iconWidth: 8,
|
||||||
|
buttonWidth: 20,
|
||||||
|
active: {
|
||||||
|
background: backgroundColor(theme, "on300", "active"),
|
||||||
|
color: iconColor(theme, "active"),
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
background: backgroundColor(theme, "on300", "hovered"),
|
||||||
|
color: iconColor(theme, "active"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toggleContactsBadge: {
|
||||||
|
cornerRadius: 3,
|
||||||
|
padding: 2,
|
||||||
|
margin: { top: 3, left: 3 },
|
||||||
|
border: { width: 1, color: workspaceBackground(theme) },
|
||||||
|
background: iconColor(theme, "feature"),
|
||||||
|
},
|
||||||
|
shareButton: {
|
||||||
|
...titlebarButton
|
||||||
|
}
|
||||||
},
|
},
|
||||||
toolbar: {
|
toolbar: {
|
||||||
height: 34,
|
height: 34,
|
||||||
|
Loading…
Reference in New Issue
Block a user