mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-26 18:41:10 +00:00
Merge pull request #1700 from zed-industries/room
Introduce call-based collaboration
This commit is contained in:
commit
a656047c15
74 changed files with 7164 additions and 5222 deletions
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"
|
||||
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
|
||||
|
||||
[[package]]
|
||||
name = "call"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"futures",
|
||||
"gpui",
|
||||
"postage",
|
||||
"project",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cap-fs-ext"
|
||||
version = "0.24.4"
|
||||
|
@ -1023,6 +1037,7 @@ dependencies = [
|
|||
"axum",
|
||||
"axum-extra",
|
||||
"base64",
|
||||
"call",
|
||||
"clap 3.2.8",
|
||||
"client",
|
||||
"collections",
|
||||
|
@ -1067,6 +1082,31 @@ dependencies = [
|
|||
"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]]
|
||||
name = "collections"
|
||||
version = "0.1.0"
|
||||
|
@ -1108,54 +1148,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "context_menu"
|
||||
version = "0.1.0"
|
||||
|
@ -7176,8 +7168,8 @@ name = "workspace"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"call",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"context_menu",
|
||||
"drag_and_drop",
|
||||
|
@ -7247,15 +7239,15 @@ dependencies = [
|
|||
"auto_update",
|
||||
"backtrace",
|
||||
"breadcrumbs",
|
||||
"call",
|
||||
"chat_panel",
|
||||
"chrono",
|
||||
"cli",
|
||||
"client",
|
||||
"clock",
|
||||
"collab_ui",
|
||||
"collections",
|
||||
"command_palette",
|
||||
"contacts_panel",
|
||||
"contacts_status_item",
|
||||
"context_menu",
|
||||
"ctor",
|
||||
"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",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::FocusDock",
|
||||
"cmd-shift-c": "contacts_panel::ToggleFocus",
|
||||
"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> {
|
||||
let sender = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.fetch_user(message.sender_id, cx)
|
||||
user_store.get_user(message.sender_id, cx)
|
||||
})
|
||||
.await?;
|
||||
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)
|
||||
where
|
||||
M: EntityMessage,
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
|
||||
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 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 std::sync::{Arc, Weak};
|
||||
use util::TryFutureExt as _;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct User {
|
||||
pub id: u64,
|
||||
pub github_login: String,
|
||||
|
@ -39,14 +39,7 @@ impl Eq for User {}
|
|||
pub struct Contact {
|
||||
pub user: Arc<User>,
|
||||
pub online: bool,
|
||||
pub projects: Vec<ProjectMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ProjectMetadata {
|
||||
pub id: u64,
|
||||
pub visible_worktree_root_names: Vec<String>,
|
||||
pub guests: BTreeSet<Arc<User>>,
|
||||
pub busy: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
@ -138,12 +131,12 @@ impl UserStore {
|
|||
}),
|
||||
_maintain_current_user: cx.spawn_weak(|this, mut cx| async move {
|
||||
let mut status = client.status();
|
||||
while let Some(status) = status.recv().await {
|
||||
while let Some(status) = status.next().await {
|
||||
match status {
|
||||
Status::Connected { .. } => {
|
||||
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
|
||||
let fetch_user = this
|
||||
.update(&mut cx, |this, cx| this.fetch_user(user_id, cx))
|
||||
.update(&mut cx, |this, cx| this.get_user(user_id, cx))
|
||||
.log_err();
|
||||
let fetch_metrics_id =
|
||||
client.request(proto::GetPrivateUserInfo {}).log_err();
|
||||
|
@ -244,7 +237,6 @@ impl UserStore {
|
|||
let mut user_ids = HashSet::default();
|
||||
for contact in &message.contacts {
|
||||
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.outgoing_requests.iter());
|
||||
|
@ -268,9 +260,7 @@ impl UserStore {
|
|||
for request in message.incoming_requests {
|
||||
incoming_requests.push({
|
||||
let user = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.fetch_user(request.requester_id, cx)
|
||||
})
|
||||
.update(&mut cx, |this, cx| this.get_user(request.requester_id, cx))
|
||||
.await?;
|
||||
(user, request.should_notify)
|
||||
});
|
||||
|
@ -279,7 +269,7 @@ impl UserStore {
|
|||
let mut outgoing_requests = Vec::new();
|
||||
for requested_user_id in message.outgoing_requests {
|
||||
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?,
|
||||
);
|
||||
}
|
||||
|
@ -504,7 +494,7 @@ impl UserStore {
|
|||
.unbounded_send(UpdateContacts::Clear(tx))
|
||||
.unwrap();
|
||||
async move {
|
||||
rx.recv().await;
|
||||
rx.next().await;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -514,25 +504,43 @@ impl UserStore {
|
|||
.unbounded_send(UpdateContacts::Wait(tx))
|
||||
.unwrap();
|
||||
async move {
|
||||
rx.recv().await;
|
||||
rx.next().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_users(
|
||||
&mut self,
|
||||
mut user_ids: Vec<u64>,
|
||||
user_ids: Vec<u64>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
user_ids.retain(|id| !self.users.contains_key(id));
|
||||
if user_ids.is_empty() {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
let load = self.load_users(proto::GetUsers { user_ids }, cx);
|
||||
cx.foreground().spawn(async move {
|
||||
load.await?;
|
||||
Ok(())
|
||||
) -> Task<Result<Vec<Arc<User>>>> {
|
||||
let mut user_ids_to_fetch = user_ids.clone();
|
||||
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if !user_ids_to_fetch.is_empty() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.load_users(
|
||||
proto::GetUsers {
|
||||
user_ids: user_ids_to_fetch,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
this.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(
|
||||
|
@ -543,7 +551,7 @@ impl UserStore {
|
|||
self.load_users(proto::FuzzySearchUsers { query }, cx)
|
||||
}
|
||||
|
||||
pub fn fetch_user(
|
||||
pub fn get_user(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
|
@ -623,39 +631,15 @@ impl Contact {
|
|||
) -> Result<Self> {
|
||||
let user = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.fetch_user(contact.user_id, cx)
|
||||
user_store.get_user(contact.user_id, cx)
|
||||
})
|
||||
.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 {
|
||||
user,
|
||||
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>> {
|
||||
|
|
|
@ -56,13 +56,14 @@ features = ["runtime-tokio-rustls", "postgres", "time", "uuid"]
|
|||
[dev-dependencies]
|
||||
collections = { path = "../collections", 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"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
|
|
@ -84,7 +84,23 @@ async fn main() {
|
|||
},
|
||||
)
|
||||
.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)
|
||||
.fetch(&self.pool);
|
||||
|
||||
let mut contacts = vec![Contact::Accepted {
|
||||
user_id,
|
||||
should_notify: false,
|
||||
}];
|
||||
let mut contacts = Vec::new();
|
||||
while let Some(row) = rows.next().await {
|
||||
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>> {
|
||||
self.background.simulate_random_delay().await;
|
||||
let mut contacts = vec![Contact::Accepted {
|
||||
user_id: id,
|
||||
should_notify: false,
|
||||
}];
|
||||
let mut contacts = Vec::new();
|
||||
|
||||
for contact in self.contacts.lock().iter() {
|
||||
if contact.requester_id == id {
|
||||
|
|
|
@ -666,13 +666,7 @@ async fn test_add_contacts() {
|
|||
let user_3 = user_ids[2];
|
||||
|
||||
// User starts with no contacts
|
||||
assert_eq!(
|
||||
db.get_contacts(user_1).await.unwrap(),
|
||||
vec![Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
}],
|
||||
);
|
||||
assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
|
||||
|
||||
// User requests a contact. Both users see the pending request.
|
||||
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_eq!(
|
||||
db.get_contacts(user_1).await.unwrap(),
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Outgoing { user_id: user_2 }
|
||||
],
|
||||
&[Contact::Outgoing { user_id: user_2 }],
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user_2).await.unwrap(),
|
||||
&[
|
||||
Contact::Incoming {
|
||||
user_id: user_1,
|
||||
should_notify: true
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false
|
||||
},
|
||||
]
|
||||
&[Contact::Incoming {
|
||||
user_id: user_1,
|
||||
should_notify: true
|
||||
}]
|
||||
);
|
||||
|
||||
// User 2 dismisses the contact request notification without accepting or rejecting.
|
||||
|
@ -712,16 +694,10 @@ async fn test_add_contacts() {
|
|||
.unwrap();
|
||||
assert_eq!(
|
||||
db.get_contacts(user_2).await.unwrap(),
|
||||
&[
|
||||
Contact::Incoming {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false
|
||||
},
|
||||
]
|
||||
&[Contact::Incoming {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
}]
|
||||
);
|
||||
|
||||
// User can't accept their own contact request
|
||||
|
@ -735,31 +711,19 @@ async fn test_add_contacts() {
|
|||
.unwrap();
|
||||
assert_eq!(
|
||||
db.get_contacts(user_1).await.unwrap(),
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: true
|
||||
}
|
||||
],
|
||||
&[Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: true
|
||||
}],
|
||||
);
|
||||
assert!(db.has_contact(user_1, user_2).await.unwrap());
|
||||
assert!(db.has_contact(user_2, user_1).await.unwrap());
|
||||
assert_eq!(
|
||||
db.get_contacts(user_2).await.unwrap(),
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false,
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false,
|
||||
},
|
||||
]
|
||||
&[Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false,
|
||||
}]
|
||||
);
|
||||
|
||||
// Users cannot re-request existing contacts.
|
||||
|
@ -772,16 +736,10 @@ async fn test_add_contacts() {
|
|||
.unwrap_err();
|
||||
assert_eq!(
|
||||
db.get_contacts(user_1).await.unwrap(),
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: true,
|
||||
},
|
||||
]
|
||||
&[Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: true,
|
||||
}]
|
||||
);
|
||||
|
||||
// Users can dismiss notifications of other users accepting their requests.
|
||||
|
@ -790,16 +748,10 @@ async fn test_add_contacts() {
|
|||
.unwrap();
|
||||
assert_eq!(
|
||||
db.get_contacts(user_1).await.unwrap(),
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false,
|
||||
},
|
||||
]
|
||||
&[Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false,
|
||||
}]
|
||||
);
|
||||
|
||||
// Users send each other concurrent contact requests and
|
||||
|
@ -809,10 +761,6 @@ async fn test_add_contacts() {
|
|||
assert_eq!(
|
||||
db.get_contacts(user_1).await.unwrap(),
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false,
|
||||
|
@ -820,21 +768,15 @@ async fn test_add_contacts() {
|
|||
Contact::Accepted {
|
||||
user_id: user_3,
|
||||
should_notify: false
|
||||
},
|
||||
}
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user_3).await.unwrap(),
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_3,
|
||||
should_notify: false
|
||||
}
|
||||
],
|
||||
&[Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
}],
|
||||
);
|
||||
|
||||
// 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_eq!(
|
||||
db.get_contacts(user_2).await.unwrap(),
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false
|
||||
}
|
||||
]
|
||||
&[Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user_3).await.unwrap(),
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_3,
|
||||
should_notify: false
|
||||
}
|
||||
],
|
||||
&[Contact::Accepted {
|
||||
user_id: user_1,
|
||||
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_contacts(user1).await.unwrap(),
|
||||
[
|
||||
Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user2,
|
||||
should_notify: true
|
||||
}
|
||||
]
|
||||
[Contact::Accepted {
|
||||
user_id: user2,
|
||||
should_notify: true
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user2).await.unwrap(),
|
||||
[
|
||||
Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user2,
|
||||
should_notify: false
|
||||
}
|
||||
]
|
||||
[Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: false
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
|
||||
|
@ -987,10 +905,6 @@ async fn test_invite_codes() {
|
|||
assert_eq!(
|
||||
db.get_contacts(user1).await.unwrap(),
|
||||
[
|
||||
Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user2,
|
||||
should_notify: true
|
||||
|
@ -1003,16 +917,10 @@ async fn test_invite_codes() {
|
|||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user3).await.unwrap(),
|
||||
[
|
||||
Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user3,
|
||||
should_notify: false
|
||||
},
|
||||
]
|
||||
[Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: false
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
|
||||
|
@ -1053,10 +961,6 @@ async fn test_invite_codes() {
|
|||
assert_eq!(
|
||||
db.get_contacts(user1).await.unwrap(),
|
||||
[
|
||||
Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user2,
|
||||
should_notify: true
|
||||
|
@ -1073,16 +977,10 @@ async fn test_invite_codes() {
|
|||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user4).await.unwrap(),
|
||||
[
|
||||
Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user4,
|
||||
should_notify: false
|
||||
},
|
||||
]
|
||||
[Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: false
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
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,
|
||||
Extension, Router, TypedHeader,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::{
|
||||
channel::mpsc,
|
||||
future::{self, BoxFuture},
|
||||
|
@ -88,11 +88,6 @@ impl<R: RequestMessage> Response<R> {
|
|||
self.server.peer.respond(self.receipt, payload)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_receipt(self) -> Receipt<R> {
|
||||
self.responded.store(true, SeqCst);
|
||||
self.receipt
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
|
@ -151,11 +146,17 @@ impl Server {
|
|||
|
||||
server
|
||||
.add_request_handler(Server::ping)
|
||||
.add_request_handler(Server::register_project)
|
||||
.add_request_handler(Server::unregister_project)
|
||||
.add_request_handler(Server::create_room)
|
||||
.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_message_handler(Server::leave_project)
|
||||
.add_message_handler(Server::respond_to_join_project_request)
|
||||
.add_message_handler(Server::update_project)
|
||||
.add_message_handler(Server::register_project_activity)
|
||||
.add_request_handler(Server::update_worktree)
|
||||
|
@ -385,7 +386,11 @@ impl Server {
|
|||
|
||||
{
|
||||
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))?;
|
||||
|
||||
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<()> {
|
||||
self.peer.disconnect(connection_id);
|
||||
|
||||
let mut projects_to_unregister = Vec::new();
|
||||
let removed_user_id;
|
||||
let mut projects_to_unshare = Vec::new();
|
||||
let mut contacts_to_update = HashSet::default();
|
||||
{
|
||||
let mut store = self.store().await;
|
||||
let removed_connection = store.remove_connection(connection_id)?;
|
||||
|
||||
for (project_id, project) in removed_connection.hosted_projects {
|
||||
projects_to_unregister.push(project_id);
|
||||
for project in removed_connection.hosted_projects {
|
||||
projects_to_unshare.push(project.id);
|
||||
broadcast(connection_id, project.guests.keys().copied(), |conn_id| {
|
||||
self.peer.send(
|
||||
conn_id,
|
||||
proto::UnregisterProject {
|
||||
project_id: project_id.to_proto(),
|
||||
proto::UnshareProject {
|
||||
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 {
|
||||
if let Some(project) = store.project(project_id).trace_err() {
|
||||
broadcast(connection_id, project.connection_ids(), |conn_id| {
|
||||
self.peer.send(
|
||||
conn_id,
|
||||
proto::RemoveProjectCollaborator {
|
||||
project_id: project_id.to_proto(),
|
||||
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();
|
||||
}
|
||||
}
|
||||
for project in removed_connection.guest_projects {
|
||||
broadcast(connection_id, project.connection_ids, |conn_id| {
|
||||
self.peer.send(
|
||||
conn_id,
|
||||
proto::RemoveProjectCollaborator {
|
||||
project_id: project.id.to_proto(),
|
||||
peer_id: connection_id.0,
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
.db
|
||||
.unregister_project(project_id)
|
||||
|
@ -598,76 +592,286 @@ impl Server {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn register_project(
|
||||
async fn create_room(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::RegisterProject>,
|
||||
response: Response<proto::RegisterProject>,
|
||||
request: TypedEnvelope<proto::CreateRoom>,
|
||||
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<()> {
|
||||
let user_id = self
|
||||
.store()
|
||||
.await
|
||||
.user_id_for_connection(request.sender_id)?;
|
||||
let project_id = self.app_state.db.register_project(user_id).await?;
|
||||
self.store().await.register_project(
|
||||
request.sender_id,
|
||||
let mut store = self.store().await;
|
||||
let room = store.share_project(
|
||||
request.payload.room_id,
|
||||
project_id,
|
||||
request.payload.online,
|
||||
request.payload.worktrees,
|
||||
request.sender_id,
|
||||
)?;
|
||||
|
||||
response.send(proto::RegisterProjectResponse {
|
||||
response.send(proto::ShareProjectResponse {
|
||||
project_id: project_id.to_proto(),
|
||||
})?;
|
||||
self.room_updated(room);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn unregister_project(
|
||||
async fn unshare_project(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::UnregisterProject>,
|
||||
response: Response<proto::UnregisterProject>,
|
||||
message: TypedEnvelope<proto::UnshareProject>,
|
||||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||
let (user_id, project) = {
|
||||
let mut state = self.store().await;
|
||||
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?;
|
||||
|
||||
let project_id = ProjectId::from_proto(message.payload.project_id);
|
||||
let mut store = self.store().await;
|
||||
let (room, project) = store.unshare_project(project_id, message.sender_id)?;
|
||||
broadcast(
|
||||
request.sender_id,
|
||||
project.guests.keys().copied(),
|
||||
|conn_id| {
|
||||
self.peer.send(
|
||||
conn_id,
|
||||
proto::UnregisterProject {
|
||||
project_id: project_id.to_proto(),
|
||||
},
|
||||
)
|
||||
},
|
||||
message.sender_id,
|
||||
project.guest_connection_ids(),
|
||||
|conn_id| self.peer.send(conn_id, message.payload.clone()),
|
||||
);
|
||||
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::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 {})?;
|
||||
self.room_updated(room);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -721,176 +925,94 @@ impl Server {
|
|||
};
|
||||
|
||||
tracing::info!(%project_id, %host_user_id, %host_connection_id, "join project");
|
||||
let has_contact = self
|
||||
.app_state
|
||||
.db
|
||||
.has_contact(guest_user_id, host_user_id)
|
||||
.await?;
|
||||
if !has_contact {
|
||||
return Err(anyhow!("no such project"))?;
|
||||
|
||||
let mut store = self.store().await;
|
||||
let (project, replica_id) = store.join_project(request.sender_id, project_id)?;
|
||||
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 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(
|
||||
guest_user_id,
|
||||
project_id,
|
||||
response.into_receipt(),
|
||||
)?;
|
||||
self.peer.send(
|
||||
host_connection_id,
|
||||
proto::RequestJoinProject {
|
||||
project_id: project_id.to_proto(),
|
||||
requester_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 conn_id in project.connection_ids() {
|
||||
if conn_id != request.sender_id {
|
||||
self.peer.send(
|
||||
conn_id,
|
||||
proto::AddProjectCollaborator {
|
||||
project_id: project_id.to_proto(),
|
||||
collaborator: Some(proto::Collaborator {
|
||||
peer_id: request.sender_id.0,
|
||||
replica_id: replica_id as u32,
|
||||
user_id: guest_user_id.to_proto(),
|
||||
}),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for (worktree_id, worktree) in &project.worktrees {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
const MAX_CHUNK_SIZE: usize = 2;
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
const MAX_CHUNK_SIZE: usize = 256;
|
||||
// First, we send the metadata associated with each worktree.
|
||||
response.send(proto::JoinProjectResponse {
|
||||
worktrees: worktrees.clone(),
|
||||
replica_id: replica_id as u32,
|
||||
collaborators: collaborators.clone(),
|
||||
language_servers: project.language_servers.clone(),
|
||||
})?;
|
||||
|
||||
// Stream this worktree's entries.
|
||||
let message = proto::UpdateWorktree {
|
||||
project_id: project_id.to_proto(),
|
||||
worktree_id: *worktree_id,
|
||||
root_name: worktree.root_name.clone(),
|
||||
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())?;
|
||||
}
|
||||
}
|
||||
for (worktree_id, worktree) in &project.worktrees {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
const MAX_CHUNK_SIZE: usize = 2;
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
const MAX_CHUNK_SIZE: usize = 256;
|
||||
|
||||
// Stream this worktree's diagnostics.
|
||||
for summary in worktree.diagnostic_summaries.values() {
|
||||
for (receipt, _) in &receipts_with_replica_ids {
|
||||
self.peer.send(
|
||||
receipt.sender_id,
|
||||
proto::UpdateDiagnosticSummary {
|
||||
project_id: project_id.to_proto(),
|
||||
worktree_id: *worktree_id,
|
||||
summary: Some(summary.clone()),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
// Stream this worktree's entries.
|
||||
let message = proto::UpdateWorktree {
|
||||
project_id: project_id.to_proto(),
|
||||
worktree_id: *worktree_id,
|
||||
root_name: worktree.root_name.clone(),
|
||||
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) {
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -903,7 +1025,7 @@ impl Server {
|
|||
let project;
|
||||
{
|
||||
let mut store = self.store().await;
|
||||
project = store.leave_project(sender_id, project_id)?;
|
||||
project = store.leave_project(project_id, sender_id)?;
|
||||
tracing::info!(
|
||||
%project_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(())
|
||||
}
|
||||
|
||||
|
@ -951,61 +1054,20 @@ impl Server {
|
|||
request: TypedEnvelope<proto::UpdateProject>,
|
||||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||
let user_id;
|
||||
{
|
||||
let mut state = self.store().await;
|
||||
user_id = state.user_id_for_connection(request.sender_id)?;
|
||||
let guest_connection_ids = state
|
||||
.read_project(project_id, request.sender_id)?
|
||||
.guest_connection_ids();
|
||||
let unshared_project = state.update_project(
|
||||
project_id,
|
||||
&request.payload.worktrees,
|
||||
request.payload.online,
|
||||
request.sender_id,
|
||||
)?;
|
||||
|
||||
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(),
|
||||
)
|
||||
});
|
||||
}
|
||||
let room =
|
||||
state.update_project(project_id, &request.payload.worktrees, request.sender_id)?;
|
||||
broadcast(request.sender_id, guest_connection_ids, |connection_id| {
|
||||
self.peer
|
||||
.forward_send(request.sender_id, connection_id, request.payload.clone())
|
||||
});
|
||||
self.room_updated(room);
|
||||
};
|
||||
|
||||
self.update_user_contacts(user_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1027,32 +1089,21 @@ impl Server {
|
|||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||
let worktree_id = request.payload.worktree_id;
|
||||
let (connection_ids, metadata_changed) = {
|
||||
let mut store = self.store().await;
|
||||
let (connection_ids, metadata_changed) = store.update_worktree(
|
||||
request.sender_id,
|
||||
project_id,
|
||||
worktree_id,
|
||||
&request.payload.root_name,
|
||||
&request.payload.removed_entries,
|
||||
&request.payload.updated_entries,
|
||||
request.payload.scan_id,
|
||||
request.payload.is_last_update,
|
||||
)?;
|
||||
(connection_ids, metadata_changed)
|
||||
};
|
||||
let connection_ids = self.store().await.update_worktree(
|
||||
request.sender_id,
|
||||
project_id,
|
||||
worktree_id,
|
||||
&request.payload.root_name,
|
||||
&request.payload.removed_entries,
|
||||
&request.payload.updated_entries,
|
||||
request.payload.scan_id,
|
||||
request.payload.is_last_update,
|
||||
)?;
|
||||
|
||||
broadcast(request.sender_id, connection_ids, |connection_id| {
|
||||
self.peer
|
||||
.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 {})?;
|
||||
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 gpui::{
|
||||
actions, elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext,
|
||||
RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext,
|
||||
Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::render_icon_button;
|
||||
|
||||
actions!(contact_finder, [Toggle]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
Picker::<ContactFinder>::init(cx);
|
||||
cx.add_action(ContactFinder::toggle);
|
||||
}
|
||||
|
||||
pub struct ContactFinder {
|
||||
|
@ -117,18 +111,21 @@ impl PickerDelegate for ContactFinder {
|
|||
|
||||
let icon_path = match request_status {
|
||||
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
|
||||
"icons/check_8.svg"
|
||||
}
|
||||
ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => {
|
||||
"icons/x_mark_8.svg"
|
||||
Some("icons/check_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) {
|
||||
&theme.contact_finder.disabled_contact_button
|
||||
} else {
|
||||
&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()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
|
@ -145,12 +142,21 @@ impl PickerDelegate for ContactFinder {
|
|||
.left()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
render_icon_button(button_style, icon_path)
|
||||
.with_children(icon_path.map(|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()
|
||||
.flex_float()
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
|
@ -160,34 +166,16 @@ impl PickerDelegate for 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 {
|
||||
let this = cx.weak_handle();
|
||||
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([]),
|
||||
user_store,
|
||||
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(),
|
||||
"wants to add you as a contact",
|
||||
Some("They won't know if you decline."),
|
||||
RespondToContactRequest {
|
||||
user_id: self.user.id,
|
||||
accept: false,
|
||||
},
|
||||
Dismiss(self.user.id),
|
||||
vec![
|
||||
(
|
||||
"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 gpui::{
|
||||
elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text},
|
||||
platform::CursorStyle,
|
||||
Action, Element, ElementBox, MouseButton, RenderContext, View,
|
||||
elements::*, platform::CursorStyle, Action, Element, ElementBox, MouseButton, RenderContext,
|
||||
View,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
|
@ -53,11 +51,18 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
|||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
|
||||
render_icon_button(
|
||||
theme.dismiss_button.style_for(state, false),
|
||||
"icons/x_mark_thin_8.svg",
|
||||
)
|
||||
.boxed()
|
||||
let style = theme.dismiss_button.style_for(state, false);
|
||||
Svg::new("icons/x_mark_thin_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.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_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,
|
||||
cx: &mut PaintContext,
|
||||
) -> 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 text_bounds = RectF::new(
|
||||
|
|
|
@ -786,6 +786,24 @@ impl AsyncAppContext {
|
|||
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> {
|
||||
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
|
||||
where
|
||||
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>
|
||||
where
|
||||
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(
|
||||
&mut self,
|
||||
window_id: usize,
|
||||
|
@ -4650,6 +4687,12 @@ impl<T> PartialEq 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> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
|
|
|
@ -271,9 +271,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
|||
mut layout,
|
||||
} => {
|
||||
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);
|
||||
Lifecycle::PostPaint {
|
||||
element,
|
||||
|
@ -292,9 +289,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
|||
..
|
||||
} => {
|
||||
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);
|
||||
Lifecycle::PostPaint {
|
||||
element,
|
||||
|
|
|
@ -241,11 +241,12 @@ impl Element for Flex {
|
|||
remaining_space: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> 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.;
|
||||
if overflowing {
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
cx.scene.push_layer(Some(visible_bounds));
|
||||
}
|
||||
|
||||
if let Some(scroll_state) = &self.scroll_state {
|
||||
|
|
|
@ -27,6 +27,8 @@ pub struct ImageStyle {
|
|||
pub height: Option<f32>,
|
||||
#[serde(default)]
|
||||
pub width: Option<f32>,
|
||||
#[serde(default)]
|
||||
pub grayscale: bool,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
|
@ -74,6 +76,7 @@ impl Element for Image {
|
|||
bounds,
|
||||
border: self.style.border,
|
||||
corner_radius: self.style.corner_radius,
|
||||
grayscale: self.style.grayscale,
|
||||
data: self.data.clone(),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -261,7 +261,8 @@ impl Element for List {
|
|||
scroll_top: &mut ListOffset,
|
||||
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
|
||||
.push_mouse_region(MouseRegion::new::<Self>(10, 0, bounds).on_scroll({
|
||||
|
|
|
@ -169,6 +169,7 @@ impl<Tag> Element for MouseEventHandler<Tag> {
|
|||
_: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
||||
let hit_bounds = self.hit_bounds(visible_bounds);
|
||||
if let Some(style) = self.cursor_style {
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -284,7 +284,9 @@ impl Element for UniformList {
|
|||
layout: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> 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(
|
||||
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 quit(&self);
|
||||
|
||||
fn screen_size(&self) -> Vector2F;
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
id: usize,
|
||||
|
|
|
@ -2,7 +2,9 @@ use super::{
|
|||
event::key_to_native, status_item::StatusItem, BoolExt as _, Dispatcher, FontSystem, Window,
|
||||
};
|
||||
use crate::{
|
||||
executor, keymap,
|
||||
executor,
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
keymap,
|
||||
platform::{self, CursorStyle},
|
||||
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
|
||||
};
|
||||
|
@ -12,7 +14,7 @@ use cocoa::{
|
|||
appkit::{
|
||||
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
||||
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
|
||||
NSPasteboardTypeString, NSSavePanel, NSWindow,
|
||||
NSPasteboardTypeString, NSSavePanel, NSScreen, NSWindow,
|
||||
},
|
||||
base::{id, nil, selector, YES},
|
||||
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(
|
||||
&self,
|
||||
id: usize,
|
||||
|
|
|
@ -747,6 +747,7 @@ impl Renderer {
|
|||
border_left: border_width * (image.border.left as usize as f32),
|
||||
border_color: image.border.color.to_uchar4(),
|
||||
corner_radius,
|
||||
grayscale: image.grayscale as u8,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -769,6 +770,7 @@ impl Renderer {
|
|||
border_left: 0.,
|
||||
border_color: Default::default(),
|
||||
corner_radius: 0.,
|
||||
grayscale: false as u8,
|
||||
});
|
||||
} else {
|
||||
log::warn!("could not render glyph with id {}", image_glyph.id);
|
||||
|
|
|
@ -90,6 +90,7 @@ typedef struct {
|
|||
float border_left;
|
||||
vector_uchar4 border_color;
|
||||
float corner_radius;
|
||||
uint8_t grayscale;
|
||||
} GPUIImage;
|
||||
|
||||
typedef enum {
|
||||
|
|
|
@ -44,6 +44,7 @@ struct QuadFragmentInput {
|
|||
float border_left;
|
||||
float4 border_color;
|
||||
float corner_radius;
|
||||
uchar grayscale; // only used in image shader
|
||||
};
|
||||
|
||||
float4 quad_sdf(QuadFragmentInput input) {
|
||||
|
@ -110,6 +111,7 @@ vertex QuadFragmentInput quad_vertex(
|
|||
quad.border_left,
|
||||
coloru_to_colorf(quad.border_color),
|
||||
quad.corner_radius,
|
||||
0,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -251,6 +253,7 @@ vertex QuadFragmentInput image_vertex(
|
|||
image.border_left,
|
||||
coloru_to_colorf(image.border_color),
|
||||
image.corner_radius,
|
||||
image.grayscale,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -260,6 +263,13 @@ fragment float4 image_fragment(
|
|||
) {
|
||||
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -289,6 +299,7 @@ vertex QuadFragmentInput surface_vertex(
|
|||
0.,
|
||||
float4(0.),
|
||||
0.,
|
||||
0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -131,6 +131,10 @@ impl super::Platform for Platform {
|
|||
|
||||
fn quit(&self) {}
|
||||
|
||||
fn screen_size(&self) -> Vector2F {
|
||||
vec2f(1024., 768.)
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
_: usize,
|
||||
|
|
|
@ -172,6 +172,7 @@ pub struct Image {
|
|||
pub bounds: RectF,
|
||||
pub border: Border,
|
||||
pub corner_radius: f32,
|
||||
pub grayscale: bool,
|
||||
pub data: Arc<ImageData>,
|
||||
}
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ pub fn run_test(
|
|||
|
||||
cx.update(|cx| cx.remove_all_windows());
|
||||
deterministic.run_until_parked();
|
||||
cx.update(|_| {}); // flush effects
|
||||
cx.update(|cx| cx.clear_globals());
|
||||
|
||||
leak_detector.lock().detect();
|
||||
if is_last_iteration {
|
||||
|
|
|
@ -122,7 +122,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
|||
cx_teardowns.extend(quote!(
|
||||
#cx_varname.update(|cx| cx.remove_all_windows());
|
||||
deterministic.run_until_parked();
|
||||
#cx_varname.update(|_| {}); // flush effects
|
||||
#cx_varname.update(|cx| cx.clear_globals());
|
||||
));
|
||||
inner_fn_args.extend(quote!(&mut #cx_varname,));
|
||||
continue;
|
||||
|
|
|
@ -19,6 +19,7 @@ pub struct Picker<D: PickerDelegate> {
|
|||
query_editor: ViewHandle<Editor>,
|
||||
list_state: UniformListState,
|
||||
max_size: Vector2F,
|
||||
theme: Box<dyn FnMut(&AppContext) -> &theme::Picker>,
|
||||
confirmed: bool,
|
||||
}
|
||||
|
||||
|
@ -51,8 +52,8 @@ impl<D: PickerDelegate> View for Picker<D> {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
||||
let settings = cx.global::<Settings>();
|
||||
let container_style = settings.theme.picker.container;
|
||||
let theme = (self.theme)(cx);
|
||||
let container_style = theme.container;
|
||||
let delegate = self.delegate.clone();
|
||||
let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
|
||||
delegate.read(cx).match_count()
|
||||
|
@ -64,17 +65,14 @@ impl<D: PickerDelegate> View for Picker<D> {
|
|||
.with_child(
|
||||
ChildView::new(&self.query_editor)
|
||||
.contained()
|
||||
.with_style(settings.theme.picker.input_editor.container)
|
||||
.with_style(theme.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
if match_count == 0 {
|
||||
Label::new(
|
||||
"No matches".into(),
|
||||
settings.theme.picker.empty.label.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(settings.theme.picker.empty.container)
|
||||
Label::new("No matches".into(), theme.empty.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.empty.container)
|
||||
} else {
|
||||
UniformList::new(
|
||||
self.list_state.clone(),
|
||||
|
@ -147,6 +145,7 @@ impl<D: PickerDelegate> Picker<D> {
|
|||
list_state: Default::default(),
|
||||
delegate,
|
||||
max_size: vec2f(540., 420.),
|
||||
theme: Box::new(|cx| &cx.global::<Settings>().theme.picker),
|
||||
confirmed: false,
|
||||
};
|
||||
cx.defer(|this, cx| {
|
||||
|
@ -163,6 +162,14 @@ impl<D: PickerDelegate> Picker<D> {
|
|||
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 {
|
||||
self.query_editor.read(cx).text(cx)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ pub mod worktree;
|
|||
mod project_tests;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
||||
use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
|
||||
use clock::ReplicaId;
|
||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
|
||||
|
@ -35,7 +35,6 @@ use lsp::{
|
|||
};
|
||||
use lsp_command::*;
|
||||
use parking_lot::Mutex;
|
||||
use postage::stream::Stream;
|
||||
use postage::watch;
|
||||
use rand::prelude::*;
|
||||
use search::SearchQuery;
|
||||
|
@ -74,7 +73,6 @@ pub trait Item: Entity {
|
|||
}
|
||||
|
||||
pub struct ProjectStore {
|
||||
db: Arc<Db>,
|
||||
projects: Vec<WeakModelHandle<Project>>,
|
||||
}
|
||||
|
||||
|
@ -127,7 +125,6 @@ pub struct Project {
|
|||
buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>,
|
||||
buffers_being_formatted: HashSet<usize>,
|
||||
nonce: u128,
|
||||
initialized_persistent_state: bool,
|
||||
_maintain_buffer_languages: Task<()>,
|
||||
}
|
||||
|
||||
|
@ -156,13 +153,8 @@ enum WorktreeHandle {
|
|||
|
||||
enum ProjectClientState {
|
||||
Local {
|
||||
is_shared: bool,
|
||||
remote_id_tx: watch::Sender<Option<u64>>,
|
||||
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_id: Option<u64>,
|
||||
_detect_unshare: Task<Option<()>>,
|
||||
},
|
||||
Remote {
|
||||
sharing_has_stopped: bool,
|
||||
|
@ -174,7 +166,6 @@ enum ProjectClientState {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Collaborator {
|
||||
pub user: Arc<User>,
|
||||
pub peer_id: PeerId,
|
||||
pub replica_id: ReplicaId,
|
||||
}
|
||||
|
@ -197,8 +188,6 @@ pub enum Event {
|
|||
RemoteIdChanged(Option<u64>),
|
||||
DisconnectedFromHost,
|
||||
CollaboratorLeft(PeerId),
|
||||
ContactRequestedJoin(Arc<User>),
|
||||
ContactCancelledJoinRequest(Arc<User>),
|
||||
}
|
||||
|
||||
pub enum LanguageServerState {
|
||||
|
@ -383,17 +372,14 @@ impl FormatTrigger {
|
|||
|
||||
impl Project {
|
||||
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_buffer_reloaded);
|
||||
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_update_language_server);
|
||||
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_unregister_project);
|
||||
client.add_model_message_handler(Self::handle_project_unshared);
|
||||
client.add_model_message_handler(Self::handle_unshare_project);
|
||||
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);
|
||||
|
@ -426,7 +412,6 @@ impl Project {
|
|||
}
|
||||
|
||||
pub fn local(
|
||||
online: bool,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
project_store: ModelHandle<ProjectStore>,
|
||||
|
@ -435,41 +420,19 @@ impl Project {
|
|||
cx: &mut MutableAppContext,
|
||||
) -> ModelHandle<Self> {
|
||||
cx.add_model(|cx: &mut ModelContext<Self>| {
|
||||
let (remote_id_tx, remote_id_rx) = watch::channel();
|
||||
let _maintain_remote_id = cx.spawn_weak({
|
||||
let mut status_rx = client.clone().status();
|
||||
move |this, mut cx| async move {
|
||||
while let Some(status) = status_rx.recv().await {
|
||||
let this = this.upgrade(&cx)?;
|
||||
if status.is_connected() {
|
||||
this.update(&mut cx, |this, cx| this.register(cx))
|
||||
.await
|
||||
.log_err()?;
|
||||
} else {
|
||||
this.update(&mut cx, |this, cx| this.unregister(cx))
|
||||
.await
|
||||
.log_err();
|
||||
let mut status = client.status();
|
||||
let _detect_unshare = cx.spawn_weak(move |this, mut cx| {
|
||||
async move {
|
||||
let is_connected = 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 || status.next().await.is_some() {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
let _ = this.update(&mut cx, |this, cx| this.unshare(cx));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
let handle = cx.weak_handle();
|
||||
|
@ -485,13 +448,8 @@ impl Project {
|
|||
loading_local_worktrees: Default::default(),
|
||||
buffer_snapshots: Default::default(),
|
||||
client_state: ProjectClientState::Local {
|
||||
is_shared: false,
|
||||
remote_id_tx,
|
||||
remote_id_rx,
|
||||
online_tx,
|
||||
online_rx,
|
||||
_maintain_remote_id,
|
||||
_maintain_online_status,
|
||||
remote_id: None,
|
||||
_detect_unshare,
|
||||
},
|
||||
opened_buffer: watch::channel(),
|
||||
client_subscriptions: Vec::new(),
|
||||
|
@ -513,7 +471,6 @@ impl Project {
|
|||
buffers_being_formatted: Default::default(),
|
||||
next_language_server_id: 0,
|
||||
nonce: StdRng::from_entropy().gen(),
|
||||
initialized_persistent_state: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -535,24 +492,6 @@ impl Project {
|
|||
})
|
||||
.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 mut worktrees = Vec::new();
|
||||
|
@ -629,7 +568,6 @@ impl Project {
|
|||
buffers_being_formatted: Default::default(),
|
||||
buffer_snapshots: Default::default(),
|
||||
nonce: StdRng::from_entropy().gen(),
|
||||
initialized_persistent_state: false,
|
||||
};
|
||||
for worktree in worktrees {
|
||||
this.add_worktree(&worktree, cx);
|
||||
|
@ -647,7 +585,7 @@ impl Project {
|
|||
.await?;
|
||||
let mut collaborators = HashMap::default();
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -672,10 +610,9 @@ impl Project {
|
|||
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||
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 project_store = cx.add_model(|_| ProjectStore::new(Db::open_fake()));
|
||||
let project = cx.update(|cx| {
|
||||
Project::local(true, client, user_store, project_store, languages, fs, cx)
|
||||
});
|
||||
let project_store = cx.add_model(|_| ProjectStore::new());
|
||||
let project =
|
||||
cx.update(|cx| Project::local(client, user_store, project_store, languages, fs, cx));
|
||||
for path in root_paths {
|
||||
let (tree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
|
@ -689,53 +626,6 @@ impl 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>) {
|
||||
let settings = cx.global::<Settings>();
|
||||
|
||||
|
@ -864,136 +754,9 @@ impl Project {
|
|||
&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> {
|
||||
match &self.client_state {
|
||||
ProjectClientState::Local { remote_id_rx, .. } => *remote_id_rx.borrow(),
|
||||
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::Local { remote_id, .. } => *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>) {
|
||||
if let ProjectClientState::Local {
|
||||
remote_id_rx,
|
||||
online_rx,
|
||||
..
|
||||
} = &self.client_state
|
||||
{
|
||||
fn metadata_changed(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let ProjectClientState::Local { remote_id, .. } = &self.client_state {
|
||||
// Broadcast worktrees only if the project is online.
|
||||
let worktrees = if *online_rx.borrow() {
|
||||
self.worktrees
|
||||
.iter()
|
||||
.filter_map(|worktree| {
|
||||
worktree
|
||||
.upgrade(cx)
|
||||
.map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto())
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
if let Some(project_id) = *remote_id_rx.borrow() {
|
||||
let online = *online_rx.borrow();
|
||||
let worktrees = self
|
||||
.worktrees
|
||||
.iter()
|
||||
.filter_map(|worktree| {
|
||||
worktree
|
||||
.upgrade(cx)
|
||||
.map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto())
|
||||
})
|
||||
.collect();
|
||||
if let Some(project_id) = *remote_id {
|
||||
self.client
|
||||
.send(proto::UpdateProject {
|
||||
project_id,
|
||||
worktrees,
|
||||
online,
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if online {
|
||||
let worktrees = self.visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let scans_complete =
|
||||
futures::future::join_all(worktrees.iter().filter_map(|worktree| {
|
||||
Some(worktree.read(cx).as_local()?.scan_complete())
|
||||
}));
|
||||
let worktrees = self.visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let scans_complete =
|
||||
futures::future::join_all(worktrees.iter().filter_map(|worktree| {
|
||||
Some(worktree.read(cx).as_local()?.scan_complete())
|
||||
}));
|
||||
|
||||
let worktrees = worktrees.into_iter().map(|handle| handle.downgrade());
|
||||
cx.spawn_weak(move |_, cx| async move {
|
||||
scans_complete.await;
|
||||
cx.read(|cx| {
|
||||
for worktree in worktrees {
|
||||
if let Some(worktree) = worktree
|
||||
.upgrade(cx)
|
||||
.and_then(|worktree| worktree.read(cx).as_local())
|
||||
{
|
||||
worktree.send_extension_counts(project_id);
|
||||
}
|
||||
let worktrees = worktrees.into_iter().map(|handle| handle.downgrade());
|
||||
cx.spawn_weak(move |_, cx| async move {
|
||||
scans_complete.await;
|
||||
cx.read(|cx| {
|
||||
for worktree in worktrees {
|
||||
if let Some(worktree) = worktree
|
||||
.upgrade(cx)
|
||||
.and_then(|worktree| worktree.read(cx).as_local())
|
||||
{
|
||||
worktree.send_extension_counts(project_id);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
self.project_store.update(cx, |_, cx| cx.notify());
|
||||
if persist {
|
||||
self.persist_state(cx).detach_and_log_err(cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
@ -1101,23 +849,6 @@ impl Project {
|
|||
.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(
|
||||
&self,
|
||||
id: WorktreeId,
|
||||
|
@ -1321,142 +1052,106 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
fn share(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
if !self.is_online() {
|
||||
return Task::ready(Err(anyhow!("can't share an offline project")));
|
||||
}
|
||||
|
||||
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;
|
||||
pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
if let ProjectClientState::Local { remote_id, .. } = &mut self.client_state {
|
||||
if remote_id.is_some() {
|
||||
return Task::ready(Err(anyhow!("project was already shared")));
|
||||
}
|
||||
|
||||
*is_shared = false;
|
||||
self.collaborators.clear();
|
||||
self.shared_buffers.clear();
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
*remote_id = Some(project_id);
|
||||
|
||||
let mut worktree_share_tasks = Vec::new();
|
||||
|
||||
for open_buffer in self.opened_buffers.values_mut() {
|
||||
if let OpenBuffer::Strong(buffer) = open_buffer {
|
||||
*open_buffer = OpenBuffer::Weak(buffer.downgrade());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.foreground().spawn(async move {
|
||||
futures::future::try_join_all(worktree_share_tasks).await?;
|
||||
Ok(())
|
||||
})
|
||||
} 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(
|
||||
&mut self,
|
||||
requester_id: u64,
|
||||
allow: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(project_id) = self.remote_id() {
|
||||
let share = if self.is_online() && allow {
|
||||
Some(self.share(cx))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let client = self.client.clone();
|
||||
cx.foreground()
|
||||
.spawn(async move {
|
||||
client.send(proto::RespondToJoinProjectRequest {
|
||||
requester_id,
|
||||
project_id,
|
||||
allow,
|
||||
})?;
|
||||
if let Some(share) = share {
|
||||
share.await?;
|
||||
pub fn unshare(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
if let ProjectClientState::Local { remote_id, .. } = &mut self.client_state {
|
||||
if let Some(project_id) = remote_id.take() {
|
||||
self.collaborators.clear();
|
||||
self.shared_buffers.clear();
|
||||
self.client_subscriptions.clear();
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
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<()> {
|
||||
match event {
|
||||
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 {
|
||||
project_id,
|
||||
buffer_id: buffer.read(cx).remote_id(),
|
||||
|
@ -2335,7 +2030,7 @@ impl Project {
|
|||
)
|
||||
.ok();
|
||||
|
||||
if let Some(project_id) = this.shared_remote_id() {
|
||||
if let Some(project_id) = this.remote_id() {
|
||||
this.client
|
||||
.send(proto::StartLanguageServer {
|
||||
project_id,
|
||||
|
@ -2742,7 +2437,7 @@ impl Project {
|
|||
language_server_id: usize,
|
||||
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
|
||||
.send(proto::UpdateLanguageServer {
|
||||
project_id,
|
||||
|
@ -4472,7 +4167,7 @@ impl Project {
|
|||
|
||||
pub fn is_shared(&self) -> bool {
|
||||
match &self.client_state {
|
||||
ProjectClientState::Local { is_shared, .. } => *is_shared,
|
||||
ProjectClientState::Local { remote_id, .. } => remote_id.is_some(),
|
||||
ProjectClientState::Remote { .. } => false,
|
||||
}
|
||||
}
|
||||
|
@ -4509,7 +4204,7 @@ impl Project {
|
|||
|
||||
let project_id = project.update(&mut cx, |project, cx| {
|
||||
project.add_worktree(&worktree, cx);
|
||||
project.shared_remote_id()
|
||||
project.remote_id()
|
||||
});
|
||||
|
||||
if let Some(project_id) = project_id {
|
||||
|
@ -4550,7 +4245,7 @@ impl Project {
|
|||
false
|
||||
}
|
||||
});
|
||||
self.metadata_changed(true, cx);
|
||||
self.metadata_changed(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -4578,7 +4273,7 @@ impl Project {
|
|||
.push(WorktreeHandle::Weak(worktree.downgrade()));
|
||||
}
|
||||
|
||||
self.metadata_changed(true, cx);
|
||||
self.metadata_changed(cx);
|
||||
cx.observe_release(worktree, |this, worktree, cx| {
|
||||
this.remove_worktree(worktree.id(), cx);
|
||||
cx.notify();
|
||||
|
@ -4641,7 +4336,7 @@ impl Project {
|
|||
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
|
||||
.send(proto::UpdateBufferFile {
|
||||
project_id,
|
||||
|
@ -4697,7 +4392,7 @@ impl Project {
|
|||
Err(_) => return,
|
||||
};
|
||||
|
||||
let shared_remote_id = self.shared_remote_id();
|
||||
let remote_id = self.remote_id();
|
||||
let client = self.client.clone();
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
|
@ -4711,7 +4406,7 @@ impl Project {
|
|||
buffer.remote_id()
|
||||
});
|
||||
|
||||
if let Some(project_id) = shared_remote_id {
|
||||
if let Some(project_id) = remote_id {
|
||||
client
|
||||
.send(proto::UpdateDiffBase {
|
||||
project_id,
|
||||
|
@ -4811,47 +4506,20 @@ impl Project {
|
|||
|
||||
// RPC message handlers
|
||||
|
||||
async fn handle_request_join_project(
|
||||
async fn handle_unshare_project(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::RequestJoinProject>,
|
||||
_: TypedEnvelope<proto::UnshareProject>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_id = message.payload.requester_id;
|
||||
if this.read_with(&cx, |project, _| {
|
||||
project.collaborators.values().any(|c| c.user.id == user_id)
|
||||
}) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.respond_to_join_request(user_id, true, cx)
|
||||
});
|
||||
} 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(())
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.is_local() {
|
||||
this.unshare(cx)?;
|
||||
} else {
|
||||
this.disconnected_from_host(cx);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_add_collaborator(
|
||||
|
@ -4860,14 +4528,13 @@ impl Project {
|
|||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
|
||||
let collaborator = envelope
|
||||
.payload
|
||||
.collaborator
|
||||
.take()
|
||||
.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.collaborators
|
||||
.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(
|
||||
this: ModelHandle<Self>,
|
||||
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 {
|
||||
cx.emit(Event::WorktreeRemoved(id));
|
||||
}
|
||||
|
@ -6182,9 +5828,8 @@ impl Project {
|
|||
}
|
||||
|
||||
impl ProjectStore {
|
||||
pub fn new(db: Arc<Db>) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
db,
|
||||
projects: Default::default(),
|
||||
}
|
||||
}
|
||||
|
@ -6313,10 +5958,10 @@ impl Entity for Project {
|
|||
self.project_store.update(cx, ProjectStore::prune_projects);
|
||||
|
||||
match &self.client_state {
|
||||
ProjectClientState::Local { remote_id_rx, .. } => {
|
||||
if let Some(project_id) = *remote_id_rx.borrow() {
|
||||
ProjectClientState::Local { remote_id, .. } => {
|
||||
if let Some(project_id) = *remote_id {
|
||||
self.client
|
||||
.send(proto::UnregisterProject { project_id })
|
||||
.send(proto::UnshareProject { project_id })
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
@ -6357,21 +6002,10 @@ impl Entity for Project {
|
|||
}
|
||||
|
||||
impl Collaborator {
|
||||
fn from_proto(
|
||||
message: proto::Collaborator,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> 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,
|
||||
})
|
||||
fn from_proto(message: proto::Collaborator) -> Self {
|
||||
Self {
|
||||
peer_id: PeerId(message.peer_id),
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,107 +10,116 @@ message Envelope {
|
|||
Error error = 5;
|
||||
Ping ping = 6;
|
||||
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;
|
||||
RegisterProjectResponse register_project_response = 9;
|
||||
UnregisterProject unregister_project = 10;
|
||||
RequestJoinProject request_join_project = 11;
|
||||
RespondToJoinProjectRequest respond_to_join_project_request = 12;
|
||||
JoinProjectRequestCancelled join_project_request_cancelled = 13;
|
||||
JoinProject join_project = 14;
|
||||
JoinProjectResponse join_project_response = 15;
|
||||
LeaveProject leave_project = 16;
|
||||
AddProjectCollaborator add_project_collaborator = 17;
|
||||
RemoveProjectCollaborator remove_project_collaborator = 18;
|
||||
ProjectUnshared project_unshared = 19;
|
||||
ShareProject share_project = 20;
|
||||
ShareProjectResponse share_project_response = 21;
|
||||
UnshareProject unshare_project = 22;
|
||||
JoinProject join_project = 23;
|
||||
JoinProjectResponse join_project_response = 24;
|
||||
LeaveProject leave_project = 25;
|
||||
AddProjectCollaborator add_project_collaborator = 26;
|
||||
RemoveProjectCollaborator remove_project_collaborator = 27;
|
||||
|
||||
GetDefinition get_definition = 20;
|
||||
GetDefinitionResponse get_definition_response = 21;
|
||||
GetTypeDefinition get_type_definition = 22;
|
||||
GetTypeDefinitionResponse get_type_definition_response = 23;
|
||||
GetReferences get_references = 24;
|
||||
GetReferencesResponse get_references_response = 25;
|
||||
GetDocumentHighlights get_document_highlights = 26;
|
||||
GetDocumentHighlightsResponse get_document_highlights_response = 27;
|
||||
GetProjectSymbols get_project_symbols = 28;
|
||||
GetProjectSymbolsResponse get_project_symbols_response = 29;
|
||||
OpenBufferForSymbol open_buffer_for_symbol = 30;
|
||||
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 31;
|
||||
GetDefinition get_definition = 28;
|
||||
GetDefinitionResponse get_definition_response = 29;
|
||||
GetTypeDefinition get_type_definition = 30;
|
||||
GetTypeDefinitionResponse get_type_definition_response = 31;
|
||||
GetReferences get_references = 32;
|
||||
GetReferencesResponse get_references_response = 33;
|
||||
GetDocumentHighlights get_document_highlights = 34;
|
||||
GetDocumentHighlightsResponse get_document_highlights_response = 35;
|
||||
GetProjectSymbols get_project_symbols = 36;
|
||||
GetProjectSymbolsResponse get_project_symbols_response = 37;
|
||||
OpenBufferForSymbol open_buffer_for_symbol = 38;
|
||||
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 39;
|
||||
|
||||
UpdateProject update_project = 32;
|
||||
RegisterProjectActivity register_project_activity = 33;
|
||||
UpdateWorktree update_worktree = 34;
|
||||
UpdateWorktreeExtensions update_worktree_extensions = 35;
|
||||
UpdateProject update_project = 40;
|
||||
RegisterProjectActivity register_project_activity = 41;
|
||||
UpdateWorktree update_worktree = 42;
|
||||
UpdateWorktreeExtensions update_worktree_extensions = 43;
|
||||
|
||||
CreateProjectEntry create_project_entry = 36;
|
||||
RenameProjectEntry rename_project_entry = 37;
|
||||
CopyProjectEntry copy_project_entry = 38;
|
||||
DeleteProjectEntry delete_project_entry = 39;
|
||||
ProjectEntryResponse project_entry_response = 40;
|
||||
CreateProjectEntry create_project_entry = 44;
|
||||
RenameProjectEntry rename_project_entry = 45;
|
||||
CopyProjectEntry copy_project_entry = 46;
|
||||
DeleteProjectEntry delete_project_entry = 47;
|
||||
ProjectEntryResponse project_entry_response = 48;
|
||||
|
||||
UpdateDiagnosticSummary update_diagnostic_summary = 41;
|
||||
StartLanguageServer start_language_server = 42;
|
||||
UpdateLanguageServer update_language_server = 43;
|
||||
UpdateDiagnosticSummary update_diagnostic_summary = 49;
|
||||
StartLanguageServer start_language_server = 50;
|
||||
UpdateLanguageServer update_language_server = 51;
|
||||
|
||||
OpenBufferById open_buffer_by_id = 44;
|
||||
OpenBufferByPath open_buffer_by_path = 45;
|
||||
OpenBufferResponse open_buffer_response = 46;
|
||||
CreateBufferForPeer create_buffer_for_peer = 47;
|
||||
UpdateBuffer update_buffer = 48;
|
||||
UpdateBufferFile update_buffer_file = 49;
|
||||
SaveBuffer save_buffer = 50;
|
||||
BufferSaved buffer_saved = 51;
|
||||
BufferReloaded buffer_reloaded = 52;
|
||||
ReloadBuffers reload_buffers = 53;
|
||||
ReloadBuffersResponse reload_buffers_response = 54;
|
||||
FormatBuffers format_buffers = 55;
|
||||
FormatBuffersResponse format_buffers_response = 56;
|
||||
GetCompletions get_completions = 57;
|
||||
GetCompletionsResponse get_completions_response = 58;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 59;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 60;
|
||||
GetCodeActions get_code_actions = 61;
|
||||
GetCodeActionsResponse get_code_actions_response = 62;
|
||||
GetHover get_hover = 63;
|
||||
GetHoverResponse get_hover_response = 64;
|
||||
ApplyCodeAction apply_code_action = 65;
|
||||
ApplyCodeActionResponse apply_code_action_response = 66;
|
||||
PrepareRename prepare_rename = 67;
|
||||
PrepareRenameResponse prepare_rename_response = 68;
|
||||
PerformRename perform_rename = 69;
|
||||
PerformRenameResponse perform_rename_response = 70;
|
||||
SearchProject search_project = 71;
|
||||
SearchProjectResponse search_project_response = 72;
|
||||
OpenBufferById open_buffer_by_id = 52;
|
||||
OpenBufferByPath open_buffer_by_path = 53;
|
||||
OpenBufferResponse open_buffer_response = 54;
|
||||
CreateBufferForPeer create_buffer_for_peer = 55;
|
||||
UpdateBuffer update_buffer = 56;
|
||||
UpdateBufferFile update_buffer_file = 57;
|
||||
SaveBuffer save_buffer = 58;
|
||||
BufferSaved buffer_saved = 59;
|
||||
BufferReloaded buffer_reloaded = 60;
|
||||
ReloadBuffers reload_buffers = 61;
|
||||
ReloadBuffersResponse reload_buffers_response = 62;
|
||||
FormatBuffers format_buffers = 63;
|
||||
FormatBuffersResponse format_buffers_response = 64;
|
||||
GetCompletions get_completions = 65;
|
||||
GetCompletionsResponse get_completions_response = 66;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 67;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 68;
|
||||
GetCodeActions get_code_actions = 69;
|
||||
GetCodeActionsResponse get_code_actions_response = 70;
|
||||
GetHover get_hover = 71;
|
||||
GetHoverResponse get_hover_response = 72;
|
||||
ApplyCodeAction apply_code_action = 73;
|
||||
ApplyCodeActionResponse apply_code_action_response = 74;
|
||||
PrepareRename prepare_rename = 75;
|
||||
PrepareRenameResponse prepare_rename_response = 76;
|
||||
PerformRename perform_rename = 77;
|
||||
PerformRenameResponse perform_rename_response = 78;
|
||||
SearchProject search_project = 79;
|
||||
SearchProjectResponse search_project_response = 80;
|
||||
|
||||
GetChannels get_channels = 73;
|
||||
GetChannelsResponse get_channels_response = 74;
|
||||
JoinChannel join_channel = 75;
|
||||
JoinChannelResponse join_channel_response = 76;
|
||||
LeaveChannel leave_channel = 77;
|
||||
SendChannelMessage send_channel_message = 78;
|
||||
SendChannelMessageResponse send_channel_message_response = 79;
|
||||
ChannelMessageSent channel_message_sent = 80;
|
||||
GetChannelMessages get_channel_messages = 81;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 82;
|
||||
GetChannels get_channels = 81;
|
||||
GetChannelsResponse get_channels_response = 82;
|
||||
JoinChannel join_channel = 83;
|
||||
JoinChannelResponse join_channel_response = 84;
|
||||
LeaveChannel leave_channel = 85;
|
||||
SendChannelMessage send_channel_message = 86;
|
||||
SendChannelMessageResponse send_channel_message_response = 87;
|
||||
ChannelMessageSent channel_message_sent = 88;
|
||||
GetChannelMessages get_channel_messages = 89;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 90;
|
||||
|
||||
UpdateContacts update_contacts = 83;
|
||||
UpdateInviteInfo update_invite_info = 84;
|
||||
ShowContacts show_contacts = 85;
|
||||
UpdateContacts update_contacts = 91;
|
||||
UpdateInviteInfo update_invite_info = 92;
|
||||
ShowContacts show_contacts = 93;
|
||||
|
||||
GetUsers get_users = 86;
|
||||
FuzzySearchUsers fuzzy_search_users = 87;
|
||||
UsersResponse users_response = 88;
|
||||
RequestContact request_contact = 89;
|
||||
RespondToContactRequest respond_to_contact_request = 90;
|
||||
RemoveContact remove_contact = 91;
|
||||
GetUsers get_users = 94;
|
||||
FuzzySearchUsers fuzzy_search_users = 95;
|
||||
UsersResponse users_response = 96;
|
||||
RequestContact request_contact = 97;
|
||||
RespondToContactRequest respond_to_contact_request = 98;
|
||||
RemoveContact remove_contact = 99;
|
||||
|
||||
Follow follow = 92;
|
||||
FollowResponse follow_response = 93;
|
||||
UpdateFollowers update_followers = 94;
|
||||
Unfollow unfollow = 95;
|
||||
GetPrivateUserInfo get_private_user_info = 96;
|
||||
GetPrivateUserInfoResponse get_private_user_info_response = 97;
|
||||
UpdateDiffBase update_diff_base = 98;
|
||||
Follow follow = 100;
|
||||
FollowResponse follow_response = 101;
|
||||
UpdateFollowers update_followers = 102;
|
||||
Unfollow unfollow = 103;
|
||||
GetPrivateUserInfo get_private_user_info = 104;
|
||||
GetPrivateUserInfoResponse get_private_user_info_response = 105;
|
||||
UpdateDiffBase update_diff_base = 106;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,70 +137,121 @@ message Test {
|
|||
uint64 id = 1;
|
||||
}
|
||||
|
||||
message RegisterProject {
|
||||
bool online = 1;
|
||||
message CreateRoom {}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
message UnregisterProject {
|
||||
message UnshareProject {
|
||||
uint64 project_id = 1;
|
||||
}
|
||||
|
||||
message UpdateProject {
|
||||
uint64 project_id = 1;
|
||||
repeated WorktreeMetadata worktrees = 2;
|
||||
bool online = 3;
|
||||
}
|
||||
|
||||
message RegisterProjectActivity {
|
||||
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 {
|
||||
uint64 project_id = 1;
|
||||
}
|
||||
|
||||
message JoinProjectResponse {
|
||||
oneof variant {
|
||||
Accept accept = 1;
|
||||
Decline decline = 2;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
uint32 replica_id = 1;
|
||||
repeated WorktreeMetadata worktrees = 2;
|
||||
repeated Collaborator collaborators = 3;
|
||||
repeated LanguageServer language_servers = 4;
|
||||
}
|
||||
|
||||
message LeaveProject {
|
||||
|
@ -254,10 +314,6 @@ message RemoveProjectCollaborator {
|
|||
uint32 peer_id = 2;
|
||||
}
|
||||
|
||||
message ProjectUnshared {
|
||||
uint64 project_id = 1;
|
||||
}
|
||||
|
||||
message GetDefinition {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
|
@ -986,17 +1042,11 @@ message ChannelMessage {
|
|||
|
||||
message Contact {
|
||||
uint64 user_id = 1;
|
||||
repeated ProjectMetadata projects = 2;
|
||||
bool online = 3;
|
||||
bool online = 2;
|
||||
bool busy = 3;
|
||||
bool should_notify = 4;
|
||||
}
|
||||
|
||||
message ProjectMetadata {
|
||||
uint64 id = 1;
|
||||
repeated string visible_worktree_root_names = 3;
|
||||
repeated uint64 guests = 4;
|
||||
}
|
||||
|
||||
message WorktreeMetadata {
|
||||
uint64 id = 1;
|
||||
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);
|
||||
|
||||
impl fmt::Display for PeerId {
|
||||
|
@ -394,7 +394,11 @@ impl Peer {
|
|||
send?;
|
||||
let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?;
|
||||
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 {
|
||||
T::Response::from_envelope(response)
|
||||
.ok_or_else(|| anyhow!("received response of the wrong type"))
|
||||
|
|
|
@ -83,11 +83,16 @@ messages!(
|
|||
(ApplyCompletionAdditionalEditsResponse, Background),
|
||||
(BufferReloaded, Foreground),
|
||||
(BufferSaved, Foreground),
|
||||
(RemoveContact, Foreground),
|
||||
(Call, Foreground),
|
||||
(CallCanceled, Foreground),
|
||||
(CancelCall, Foreground),
|
||||
(ChannelMessageSent, Foreground),
|
||||
(CopyProjectEntry, Foreground),
|
||||
(CreateBufferForPeer, Foreground),
|
||||
(CreateProjectEntry, Foreground),
|
||||
(CreateRoom, Foreground),
|
||||
(CreateRoomResponse, Foreground),
|
||||
(DeclineCall, Foreground),
|
||||
(DeleteProjectEntry, Foreground),
|
||||
(Error, Foreground),
|
||||
(Follow, Foreground),
|
||||
|
@ -116,14 +121,17 @@ messages!(
|
|||
(GetProjectSymbols, Background),
|
||||
(GetProjectSymbolsResponse, Background),
|
||||
(GetUsers, Foreground),
|
||||
(IncomingCall, Foreground),
|
||||
(UsersResponse, Foreground),
|
||||
(JoinChannel, Foreground),
|
||||
(JoinChannelResponse, Foreground),
|
||||
(JoinProject, Foreground),
|
||||
(JoinProjectResponse, Foreground),
|
||||
(JoinProjectRequestCancelled, Foreground),
|
||||
(JoinRoom, Foreground),
|
||||
(JoinRoomResponse, Foreground),
|
||||
(LeaveChannel, Foreground),
|
||||
(LeaveProject, Foreground),
|
||||
(LeaveRoom, Foreground),
|
||||
(OpenBufferById, Background),
|
||||
(OpenBufferByPath, Background),
|
||||
(OpenBufferForSymbol, Background),
|
||||
|
@ -134,29 +142,28 @@ messages!(
|
|||
(PrepareRename, Background),
|
||||
(PrepareRenameResponse, Background),
|
||||
(ProjectEntryResponse, Foreground),
|
||||
(ProjectUnshared, Foreground),
|
||||
(RegisterProjectResponse, Foreground),
|
||||
(RemoveContact, Foreground),
|
||||
(Ping, Foreground),
|
||||
(RegisterProject, Foreground),
|
||||
(RegisterProjectActivity, Foreground),
|
||||
(ReloadBuffers, Foreground),
|
||||
(ReloadBuffersResponse, Foreground),
|
||||
(RemoveProjectCollaborator, Foreground),
|
||||
(RenameProjectEntry, Foreground),
|
||||
(RequestContact, Foreground),
|
||||
(RequestJoinProject, Foreground),
|
||||
(RespondToContactRequest, Foreground),
|
||||
(RespondToJoinProjectRequest, Foreground),
|
||||
(RoomUpdated, Foreground),
|
||||
(SaveBuffer, Foreground),
|
||||
(SearchProject, Background),
|
||||
(SearchProjectResponse, Background),
|
||||
(SendChannelMessage, Foreground),
|
||||
(SendChannelMessageResponse, Foreground),
|
||||
(ShareProject, Foreground),
|
||||
(ShareProjectResponse, Foreground),
|
||||
(ShowContacts, Foreground),
|
||||
(StartLanguageServer, Foreground),
|
||||
(Test, Foreground),
|
||||
(Unfollow, Foreground),
|
||||
(UnregisterProject, Foreground),
|
||||
(UnshareProject, Foreground),
|
||||
(UpdateBuffer, Foreground),
|
||||
(UpdateBufferFile, Foreground),
|
||||
(UpdateContacts, Foreground),
|
||||
|
@ -164,6 +171,7 @@ messages!(
|
|||
(UpdateFollowers, Foreground),
|
||||
(UpdateInviteInfo, Foreground),
|
||||
(UpdateLanguageServer, Foreground),
|
||||
(UpdateParticipantLocation, Foreground),
|
||||
(UpdateProject, Foreground),
|
||||
(UpdateWorktree, Foreground),
|
||||
(UpdateWorktreeExtensions, Background),
|
||||
|
@ -178,8 +186,12 @@ request_messages!(
|
|||
ApplyCompletionAdditionalEdits,
|
||||
ApplyCompletionAdditionalEditsResponse
|
||||
),
|
||||
(Call, Ack),
|
||||
(CancelCall, Ack),
|
||||
(CopyProjectEntry, ProjectEntryResponse),
|
||||
(CreateProjectEntry, ProjectEntryResponse),
|
||||
(CreateRoom, CreateRoomResponse),
|
||||
(DeclineCall, Ack),
|
||||
(DeleteProjectEntry, ProjectEntryResponse),
|
||||
(Follow, FollowResponse),
|
||||
(FormatBuffers, FormatBuffersResponse),
|
||||
|
@ -198,13 +210,14 @@ request_messages!(
|
|||
(GetUsers, UsersResponse),
|
||||
(JoinChannel, JoinChannelResponse),
|
||||
(JoinProject, JoinProjectResponse),
|
||||
(JoinRoom, JoinRoomResponse),
|
||||
(IncomingCall, Ack),
|
||||
(OpenBufferById, OpenBufferResponse),
|
||||
(OpenBufferByPath, OpenBufferResponse),
|
||||
(OpenBufferForSymbol, OpenBufferForSymbolResponse),
|
||||
(Ping, Ack),
|
||||
(PerformRename, PerformRenameResponse),
|
||||
(PrepareRename, PrepareRenameResponse),
|
||||
(RegisterProject, RegisterProjectResponse),
|
||||
(ReloadBuffers, ReloadBuffersResponse),
|
||||
(RequestContact, Ack),
|
||||
(RemoveContact, Ack),
|
||||
|
@ -213,9 +226,10 @@ request_messages!(
|
|||
(SaveBuffer, BufferSaved),
|
||||
(SearchProject, SearchProjectResponse),
|
||||
(SendChannelMessage, SendChannelMessageResponse),
|
||||
(ShareProject, ShareProjectResponse),
|
||||
(Test, Test),
|
||||
(UnregisterProject, Ack),
|
||||
(UpdateBuffer, Ack),
|
||||
(UpdateParticipantLocation, Ack),
|
||||
(UpdateWorktree, Ack),
|
||||
);
|
||||
|
||||
|
@ -241,24 +255,21 @@ entity_messages!(
|
|||
GetReferences,
|
||||
GetProjectSymbols,
|
||||
JoinProject,
|
||||
JoinProjectRequestCancelled,
|
||||
LeaveProject,
|
||||
OpenBufferById,
|
||||
OpenBufferByPath,
|
||||
OpenBufferForSymbol,
|
||||
PerformRename,
|
||||
PrepareRename,
|
||||
ProjectUnshared,
|
||||
RegisterProjectActivity,
|
||||
ReloadBuffers,
|
||||
RemoveProjectCollaborator,
|
||||
RenameProjectEntry,
|
||||
RequestJoinProject,
|
||||
SaveBuffer,
|
||||
SearchProject,
|
||||
StartLanguageServer,
|
||||
Unfollow,
|
||||
UnregisterProject,
|
||||
UnshareProject,
|
||||
UpdateBuffer,
|
||||
UpdateBufferFile,
|
||||
UpdateDiagnosticSummary,
|
||||
|
|
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
|||
pub use peer::*;
|
||||
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,
|
||||
cx: &mut gpui::PaintContext,
|
||||
) -> Self::PaintState {
|
||||
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
//Setup element stuff
|
||||
let clip_bounds = Some(visible_bounds);
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ pub struct Theme {
|
|||
pub context_menu: ContextMenu,
|
||||
pub chat_panel: ChatPanel,
|
||||
pub contacts_popover: ContactsPopover,
|
||||
pub contacts_panel: ContactsPanel,
|
||||
pub contact_list: ContactList,
|
||||
pub contact_finder: ContactFinder,
|
||||
pub project_panel: ProjectPanel,
|
||||
pub command_palette: CommandPalette,
|
||||
|
@ -31,6 +31,8 @@ pub struct Theme {
|
|||
pub breadcrumbs: ContainedText,
|
||||
pub contact_notification: ContactNotification,
|
||||
pub update_notification: UpdateNotification,
|
||||
pub project_shared_notification: ProjectSharedNotification,
|
||||
pub incoming_call_notification: IncomingCallNotification,
|
||||
pub tooltip: TooltipStyle,
|
||||
pub terminal: TerminalStyle,
|
||||
}
|
||||
|
@ -58,6 +60,7 @@ pub struct Workspace {
|
|||
pub notifications: Notifications,
|
||||
pub joining_project_avatar: ImageStyle,
|
||||
pub joining_project_message: ContainedText,
|
||||
pub external_location_message: ContainedText,
|
||||
pub dock: Dock,
|
||||
}
|
||||
|
||||
|
@ -72,8 +75,67 @@ pub struct Titlebar {
|
|||
pub avatar_ribbon: AvatarRibbon,
|
||||
pub offline_icon: OfflineIcon,
|
||||
pub avatar: ImageStyle,
|
||||
pub inactive_avatar: ImageStyle,
|
||||
pub sign_in_prompt: Interactive<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)]
|
||||
|
@ -315,33 +377,6 @@ pub struct CommandPalette {
|
|||
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)]
|
||||
pub struct InviteLink {
|
||||
#[serde(flatten)]
|
||||
|
@ -351,21 +386,6 @@ pub struct InviteLink {
|
|||
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)]
|
||||
pub struct Icon {
|
||||
#[serde(flatten)]
|
||||
|
@ -384,16 +404,6 @@ pub struct IconButton {
|
|||
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)]
|
||||
pub struct ChatMessage {
|
||||
#[serde(flatten)]
|
||||
|
@ -475,6 +485,40 @@ pub struct UpdateNotification {
|
|||
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)]
|
||||
pub struct Editor {
|
||||
pub text_color: Color,
|
||||
|
|
|
@ -8,11 +8,16 @@ path = "src/workspace.rs"
|
|||
doctest = false
|
||||
|
||||
[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]
|
||||
call = { path = "../call" }
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
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"] }
|
||||
|
||||
[dev-dependencies]
|
||||
call = { path = "../call", features = ["test-support"] }
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", 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 client::PeerId;
|
||||
use collections::HashMap;
|
||||
use gpui::{elements::*, Axis, Border, ViewHandle};
|
||||
use project::Collaborator;
|
||||
use call::ActiveCall;
|
||||
use gpui::{
|
||||
elements::*, Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle,
|
||||
};
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
use theme::Theme;
|
||||
|
||||
|
@ -56,11 +57,14 @@ impl PaneGroup {
|
|||
|
||||
pub(crate) fn render(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
theme: &Theme,
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
collaborators: &HashMap<PeerId, Collaborator>,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
cx: &mut RenderContext<Workspace>,
|
||||
) -> 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>> {
|
||||
|
@ -100,13 +104,16 @@ impl Member {
|
|||
|
||||
pub fn render(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
theme: &Theme,
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
collaborators: &HashMap<PeerId, Collaborator>,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
cx: &mut RenderContext<Workspace>,
|
||||
) -> ElementBox {
|
||||
enum FollowIntoExternalProject {}
|
||||
|
||||
match self {
|
||||
Member::Pane(pane) => {
|
||||
let mut border = Border::default();
|
||||
let leader = follower_states
|
||||
.iter()
|
||||
.find_map(|(leader_id, follower_states)| {
|
||||
|
@ -116,21 +123,110 @@ impl Member {
|
|||
None
|
||||
}
|
||||
})
|
||||
.and_then(|leader_id| collaborators.get(leader_id));
|
||||
if let Some(leader) = leader {
|
||||
let leader_color = theme
|
||||
.editor
|
||||
.replica_selection_style(leader.replica_id)
|
||||
.cursor;
|
||||
.and_then(|leader_id| {
|
||||
let room = active_call?.read(cx).room()?.read(cx);
|
||||
let collaborator = project.read(cx).collaborators().get(leader_id)?;
|
||||
let participant = room.remote_participants().get(&leader_id)?;
|
||||
Some((collaborator.replica_id, participant))
|
||||
});
|
||||
|
||||
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
|
||||
.color
|
||||
.fade_out(1. - theme.workspace.leader_border_opacity);
|
||||
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(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
theme: &Theme,
|
||||
follower_state: &FollowerStatesByLeader,
|
||||
collaborators: &HashMap<PeerId, Collaborator>,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
cx: &mut RenderContext<Workspace>,
|
||||
) -> ElementBox {
|
||||
let last_member_ix = self.members.len() - 1;
|
||||
Flex::new(self.axis)
|
||||
.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 {
|
||||
let mut border = theme.workspace.pane_divider;
|
||||
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;
|
||||
mod status_bar;
|
||||
mod toolbar;
|
||||
mod waiting_room;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{
|
||||
proto, Authenticate, Client, Contact, PeerId, Subscription, TypedEnvelope, User, UserStore,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
use call::ActiveCall;
|
||||
use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use dock::{DefaultItemFactory, Dock, ToggleDockButton};
|
||||
use drag_and_drop::DragAndDrop;
|
||||
use futures::{channel::oneshot, FutureExt};
|
||||
use futures::{channel::oneshot, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
actions,
|
||||
color::Color,
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f, PathBuilder},
|
||||
impl_actions, impl_internal_actions,
|
||||
json::{self, ToJson},
|
||||
platform::{CursorStyle, WindowOptions},
|
||||
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData,
|
||||
ModelContext, ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, PromptLevel,
|
||||
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use log::{error, warn};
|
||||
|
@ -52,7 +46,6 @@ use std::{
|
|||
cell::RefCell,
|
||||
fmt,
|
||||
future::Future,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{
|
||||
|
@ -64,7 +57,6 @@ use std::{
|
|||
use theme::{Theme, ThemeRegistry};
|
||||
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
||||
use util::ResultExt;
|
||||
use waiting_room::WaitingRoom;
|
||||
|
||||
type ProjectItemBuilders = HashMap<
|
||||
TypeId,
|
||||
|
@ -115,12 +107,6 @@ pub struct OpenPaths {
|
|||
pub paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct ToggleProjectOnline {
|
||||
#[serde(skip_deserializing)]
|
||||
pub project: Option<ModelHandle<Project>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct ActivatePane(pub usize);
|
||||
|
||||
|
@ -129,8 +115,8 @@ pub struct ToggleFollow(pub PeerId);
|
|||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct JoinProject {
|
||||
pub contact: Arc<Contact>,
|
||||
pub project_index: usize,
|
||||
pub project_id: u64,
|
||||
pub follow_user_id: u64,
|
||||
}
|
||||
|
||||
impl_internal_actions!(
|
||||
|
@ -142,7 +128,7 @@ impl_internal_actions!(
|
|||
RemoveWorktreeFromProject
|
||||
]
|
||||
);
|
||||
impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
|
||||
impl_actions!(workspace, [ActivatePane]);
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
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::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_action(Workspace::add_folder_to_project);
|
||||
cx.add_action(Workspace::remove_folder_from_project);
|
||||
cx.add_action(Workspace::toggle_project_online);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
|
||||
let pane = workspace.active_pane().clone();
|
||||
|
@ -957,7 +934,7 @@ impl AppState {
|
|||
let languages = Arc::new(LanguageRegistry::test());
|
||||
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||
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 themes = ThemeRegistry::new((), cx.font_cache().clone());
|
||||
Arc::new(Self {
|
||||
|
@ -984,7 +961,7 @@ pub struct Workspace {
|
|||
weak_self: WeakViewHandle<Self>,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<client::UserStore>,
|
||||
remote_entity_subscription: Option<Subscription>,
|
||||
remote_entity_subscription: Option<client::Subscription>,
|
||||
fs: Arc<dyn Fs>,
|
||||
modal: Option<AnyViewHandle>,
|
||||
center: PaneGroup,
|
||||
|
@ -995,6 +972,7 @@ pub struct Workspace {
|
|||
active_pane: ViewHandle<Pane>,
|
||||
last_active_center_pane: Option<ViewHandle<Pane>>,
|
||||
status_bar: ViewHandle<StatusBar>,
|
||||
titlebar_item: Option<AnyViewHandle>,
|
||||
dock: Dock,
|
||||
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
||||
project: ModelHandle<Project>,
|
||||
|
@ -1002,7 +980,9 @@ pub struct Workspace {
|
|||
follower_states_by_leader: FollowerStatesByLeader,
|
||||
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
|
||||
window_edited: bool,
|
||||
active_call: Option<ModelHandle<ActiveCall>>,
|
||||
_observe_current_user: Task<()>,
|
||||
_active_call_observation: Option<gpui::Subscription>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -1111,6 +1091,14 @@ impl Workspace {
|
|||
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 {
|
||||
modal: None,
|
||||
weak_self: weak_handle,
|
||||
|
@ -1124,6 +1112,7 @@ impl Workspace {
|
|||
active_pane: center_pane.clone(),
|
||||
last_active_center_pane: Some(center_pane.clone()),
|
||||
status_bar,
|
||||
titlebar_item: None,
|
||||
notifications: Default::default(),
|
||||
client,
|
||||
remote_entity_subscription: None,
|
||||
|
@ -1136,7 +1125,9 @@ impl Workspace {
|
|||
follower_states_by_leader: Default::default(),
|
||||
last_leaders_by_pane: Default::default(),
|
||||
window_edited: false,
|
||||
active_call,
|
||||
_observe_current_user,
|
||||
_active_call_observation: active_call_observation,
|
||||
};
|
||||
this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
|
||||
cx.defer(|this, cx| this.update_window_title(cx));
|
||||
|
@ -1168,6 +1159,19 @@ impl Workspace {
|
|||
&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.
|
||||
///
|
||||
/// 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 mut workspace = Workspace::new(
|
||||
Project::local(
|
||||
false,
|
||||
app_state.client.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.project_store.clone(),
|
||||
|
@ -1238,7 +1241,7 @@ impl Workspace {
|
|||
_: &CloseWindow,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> 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 {
|
||||
if prepare.await? {
|
||||
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>> {
|
||||
self.save_all_internal(true, cx)
|
||||
pub fn prepare_to_close(
|
||||
&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<()>>> {
|
||||
|
@ -1393,17 +1432,6 @@ impl Workspace {
|
|||
.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(
|
||||
&self,
|
||||
abs_path: &Path,
|
||||
|
@ -2068,46 +2096,12 @@ impl Workspace {
|
|||
None
|
||||
}
|
||||
|
||||
fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
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,
|
||||
}
|
||||
pub fn is_following(&self, peer_id: PeerId) -> bool {
|
||||
self.follower_states_by_leader.contains_key(&peer_id)
|
||||
}
|
||||
|
||||
fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let project = &self.project.read(cx);
|
||||
let replica_id = project.replica_id();
|
||||
let mut worktree_root_names = String::new();
|
||||
for (i, name) in project.worktree_root_names(cx).enumerate() {
|
||||
if i > 0 {
|
||||
|
@ -2129,7 +2123,7 @@ impl Workspace {
|
|||
|
||||
enum TitleBar {}
|
||||
ConstrainedBox::new(
|
||||
MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
|
||||
MouseEventHandler::<TitleBar>::new(0, cx, |_, _| {
|
||||
Container::new(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
|
@ -2138,21 +2132,10 @@ impl Workspace {
|
|||
.left()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Align::new(
|
||||
Flex::row()
|
||||
.with_children(self.render_collaborators(theme, cx))
|
||||
.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(),
|
||||
.with_children(
|
||||
self.titlebar_item
|
||||
.as_ref()
|
||||
.map(|item| ChildView::new(item).aligned().right().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> {
|
||||
if self.project.read(cx).is_read_only() {
|
||||
enum DisconnectedOverlay {}
|
||||
|
@ -2698,6 +2562,7 @@ impl View for Workspace {
|
|||
.with_child(
|
||||
Stack::new()
|
||||
.with_child({
|
||||
let project = self.project.clone();
|
||||
Flex::row()
|
||||
.with_children(
|
||||
if self.left_sidebar.read(cx).active_item().is_some() {
|
||||
|
@ -2715,9 +2580,11 @@ impl View for Workspace {
|
|||
Flex::column()
|
||||
.with_child(
|
||||
FlexItem::new(self.center.render(
|
||||
&project,
|
||||
&theme,
|
||||
&self.follower_states_by_leader,
|
||||
self.project.read(cx).collaborators(),
|
||||
self.active_call.as_ref(),
|
||||
cx,
|
||||
))
|
||||
.flex(1., true)
|
||||
.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 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("OpenPaths")
|
||||
|
@ -2964,7 +2750,6 @@ pub fn open_paths(
|
|||
|
||||
cx.add_window((app_state.build_window_options)(), |cx| {
|
||||
let project = Project::local(
|
||||
false,
|
||||
app_state.client.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.project_store.clone(),
|
||||
|
@ -2989,44 +2774,14 @@ pub fn open_paths(
|
|||
})
|
||||
.await;
|
||||
|
||||
if let Some(project) = new_project {
|
||||
project
|
||||
.update(&mut cx, |project, cx| project.restore_state(cx))
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
(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) {
|
||||
let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
|
||||
let mut workspace = Workspace::new(
|
||||
Project::local(
|
||||
false,
|
||||
app_state.client.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.project_store.clone(),
|
||||
|
@ -3236,7 +2991,7 @@ mod tests {
|
|||
// When there are no dirty items, there's nothing to do.
|
||||
let item1 = cx.add_view(&workspace, |_| TestItem::new());
|
||||
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());
|
||||
|
||||
// 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(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.simulate_prompt_answer(window_id, 2 /* cancel */);
|
||||
cx.foreground().run_until_parked();
|
||||
|
|
|
@ -19,15 +19,15 @@ activity_indicator = { path = "../activity_indicator" }
|
|||
assets = { path = "../assets" }
|
||||
auto_update = { path = "../auto_update" }
|
||||
breadcrumbs = { path = "../breadcrumbs" }
|
||||
call = { path = "../call" }
|
||||
chat_panel = { path = "../chat_panel" }
|
||||
cli = { path = "../cli" }
|
||||
collab_ui = { path = "../collab_ui" }
|
||||
collections = { path = "../collections" }
|
||||
command_palette = { path = "../command_palette" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
contacts_panel = { path = "../contacts_panel" }
|
||||
contacts_status_item = { path = "../contacts_status_item" }
|
||||
diagnostics = { path = "../diagnostics" }
|
||||
editor = { path = "../editor" }
|
||||
file_finder = { path = "../file_finder" }
|
||||
|
@ -105,17 +105,19 @@ tree-sitter-html = "0.19.0"
|
|||
url = "2.2"
|
||||
|
||||
[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"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
text = { path = "../text", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
||||
env_logger = "0.9"
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
unindent = "0.1.7"
|
||||
|
|
|
@ -112,7 +112,6 @@ fn main() {
|
|||
go_to_line::init(cx);
|
||||
file_finder::init(cx);
|
||||
chat_panel::init(cx);
|
||||
contacts_panel::init(cx);
|
||||
outline::init(cx);
|
||||
project_symbols::init(cx);
|
||||
project_panel::init(cx);
|
||||
|
@ -138,11 +137,11 @@ fn main() {
|
|||
})
|
||||
.detach();
|
||||
|
||||
let project_store = cx.add_model(|_| ProjectStore::new());
|
||||
let db = cx.background().block(db);
|
||||
client.start_telemetry(db.clone());
|
||||
client.report_event("start app", Default::default());
|
||||
|
||||
let project_store = cx.add_model(|_| ProjectStore::new(db.clone()));
|
||||
let app_state = Arc::new(AppState {
|
||||
languages,
|
||||
themes,
|
||||
|
@ -159,6 +158,7 @@ fn main() {
|
|||
journal::init(app_state.clone(), cx);
|
||||
theme_selector::init(app_state.clone(), cx);
|
||||
zed::init(&app_state, cx);
|
||||
collab_ui::init(app_state.clone(), cx);
|
||||
|
||||
cx.set_menus(menus::menus());
|
||||
|
||||
|
|
|
@ -244,10 +244,6 @@ pub fn menus() -> Vec<Menu<'static>> {
|
|||
name: "Project Panel",
|
||||
action: Box::new(project_panel::ToggleFocus),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Contacts Panel",
|
||||
action: Box::new(contacts_panel::ToggleFocus),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Command Palette",
|
||||
action: Box::new(command_palette::Toggle),
|
||||
|
|
|
@ -10,9 +10,8 @@ use anyhow::{anyhow, Context, Result};
|
|||
use assets::Assets;
|
||||
use breadcrumbs::Breadcrumbs;
|
||||
pub use client;
|
||||
use collab_ui::CollabTitlebarItem;
|
||||
use collections::VecDeque;
|
||||
pub use contacts_panel;
|
||||
use contacts_panel::ContactsPanel;
|
||||
pub use editor;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
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);
|
||||
},
|
||||
);
|
||||
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);
|
||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
settings::KeymapFileContent::load_defaults(cx);
|
||||
}
|
||||
|
||||
|
@ -231,7 +224,8 @@ pub fn initialize_workspace(
|
|||
app_state: &Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
cx.subscribe(&cx.handle(), {
|
||||
let workspace_handle = cx.handle();
|
||||
cx.subscribe(&workspace_handle, {
|
||||
move |_, _, event, cx| {
|
||||
if let workspace::Event::PaneAdded(pane) = event {
|
||||
pane.update(cx, |pane, cx| {
|
||||
|
@ -285,16 +279,11 @@ pub fn initialize_workspace(
|
|||
}));
|
||||
});
|
||||
|
||||
let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
|
||||
let contact_panel = cx.add_view(|cx| {
|
||||
ContactsPanel::new(
|
||||
app_state.user_store.clone(),
|
||||
app_state.project_store.clone(),
|
||||
workspace.weak_handle(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let collab_titlebar_item =
|
||||
cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx));
|
||||
workspace.set_titlebar_item(collab_titlebar_item, cx);
|
||||
|
||||
let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
|
||||
workspace.left_sidebar().update(cx, |sidebar, cx| {
|
||||
sidebar.add_item(
|
||||
"icons/folder_tree_16.svg",
|
||||
|
@ -303,14 +292,6 @@ pub fn initialize_workspace(
|
|||
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 =
|
||||
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.
|
||||
for workspace in workspaces {
|
||||
if !workspace
|
||||
.update(&mut cx, |workspace, cx| workspace.prepare_to_close(cx))
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.prepare_to_close(true, cx)
|
||||
})
|
||||
.await?
|
||||
{
|
||||
return Ok(());
|
||||
|
@ -1772,6 +1755,7 @@ mod tests {
|
|||
let state = Arc::get_mut(&mut app_state).unwrap();
|
||||
state.initialize_workspace = initialize_workspace;
|
||||
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);
|
||||
editor::init(cx);
|
||||
pane::init(cx);
|
||||
|
|
|
@ -2,7 +2,6 @@ import Theme from "../themes/common/theme";
|
|||
import chatPanel from "./chatPanel";
|
||||
import { text } from "./components";
|
||||
import contactFinder from "./contactFinder";
|
||||
import contactsPanel from "./contactsPanel";
|
||||
import contactsPopover from "./contactsPopover";
|
||||
import commandPalette from "./commandPalette";
|
||||
import editor from "./editor";
|
||||
|
@ -14,8 +13,11 @@ import contextMenu from "./contextMenu";
|
|||
import projectDiagnostics from "./projectDiagnostics";
|
||||
import contactNotification from "./contactNotification";
|
||||
import updateNotification from "./updateNotification";
|
||||
import projectSharedNotification from "./projectSharedNotification";
|
||||
import tooltip from "./tooltip";
|
||||
import terminal from "./terminal";
|
||||
import contactList from "./contactList";
|
||||
import incomingCallNotification from "./incomingCallNotification";
|
||||
|
||||
export const panel = {
|
||||
padding: { top: 12, bottom: 12 },
|
||||
|
@ -36,7 +38,7 @@ export default function app(theme: Theme): Object {
|
|||
projectPanel: projectPanel(theme),
|
||||
chatPanel: chatPanel(theme),
|
||||
contactsPopover: contactsPopover(theme),
|
||||
contactsPanel: contactsPanel(theme),
|
||||
contactList: contactList(theme),
|
||||
contactFinder: contactFinder(theme),
|
||||
search: search(theme),
|
||||
breadcrumbs: {
|
||||
|
@ -47,6 +49,8 @@ export default function app(theme: Theme): Object {
|
|||
},
|
||||
contactNotification: contactNotification(theme),
|
||||
updateNotification: updateNotification(theme),
|
||||
projectSharedNotification: projectSharedNotification(theme),
|
||||
incomingCallNotification: incomingCallNotification(theme),
|
||||
tooltip: tooltip(theme),
|
||||
terminal: terminal(theme),
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import Theme from "../themes/common/theme";
|
||||
import picker from "./picker";
|
||||
import { backgroundColor, iconColor } from "./components";
|
||||
import { backgroundColor, border, iconColor, player, text } from "./components";
|
||||
|
||||
export default function contactFinder(theme: Theme) {
|
||||
const sideMargin = 6;
|
||||
const contactButton = {
|
||||
background: backgroundColor(theme, 100),
|
||||
color: iconColor(theme, "primary"),
|
||||
|
@ -12,7 +13,31 @@ export default function contactFinder(theme: Theme) {
|
|||
};
|
||||
|
||||
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,
|
||||
contactAvatar: {
|
||||
cornerRadius: 10,
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
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 sidePadding = 12;
|
||||
|
||||
const contactButton = {
|
||||
background: backgroundColor(theme, 100),
|
||||
color: iconColor(theme, "primary"),
|
||||
iconWidth: 8,
|
||||
buttonWidth: 16,
|
||||
cornerRadius: 8,
|
||||
};
|
||||
const projectRow = {
|
||||
guestAvatarSpacing: 4,
|
||||
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 {
|
||||
...panel,
|
||||
padding: { top: panel.padding.top, bottom: 0 },
|
||||
userQueryEditor: {
|
||||
background: backgroundColor(theme, 500),
|
||||
cornerRadius: 6,
|
||||
|
@ -64,28 +53,20 @@ export default function contactsPanel(theme: Theme) {
|
|||
top: 4,
|
||||
},
|
||||
margin: {
|
||||
left: sidePadding,
|
||||
right: sidePadding,
|
||||
left: 6
|
||||
},
|
||||
},
|
||||
userQueryEditorHeight: 32,
|
||||
userQueryEditorHeight: 33,
|
||||
addContactButton: {
|
||||
margin: { left: 6, right: 12 },
|
||||
color: iconColor(theme, "primary"),
|
||||
buttonWidth: 16,
|
||||
buttonWidth: 28,
|
||||
iconWidth: 16,
|
||||
},
|
||||
privateButton: {
|
||||
iconWidth: 12,
|
||||
color: iconColor(theme, "primary"),
|
||||
cornerRadius: 5,
|
||||
buttonWidth: 12,
|
||||
},
|
||||
rowHeight: 28,
|
||||
sectionIconSize: 8,
|
||||
headerRow: {
|
||||
...text(theme, "mono", "secondary", { size: "sm" }),
|
||||
margin: { top: 14 },
|
||||
margin: { top: 6 },
|
||||
padding: {
|
||||
left: sidePadding,
|
||||
right: sidePadding,
|
||||
|
@ -95,6 +76,26 @@ export default function contactsPanel(theme: Theme) {
|
|||
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: {
|
||||
padding: {
|
||||
left: sidePadding,
|
||||
|
@ -104,20 +105,22 @@ export default function contactsPanel(theme: Theme) {
|
|||
background: backgroundColor(theme, 100, "active"),
|
||||
},
|
||||
},
|
||||
treeBranch: {
|
||||
color: borderColor(theme, "active"),
|
||||
width: 1,
|
||||
hover: {
|
||||
color: borderColor(theme, "active"),
|
||||
},
|
||||
active: {
|
||||
color: borderColor(theme, "active"),
|
||||
},
|
||||
},
|
||||
contactAvatar: {
|
||||
cornerRadius: 10,
|
||||
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: {
|
||||
...text(theme, "mono", "primary", { size: "sm" }),
|
||||
margin: {
|
||||
|
@ -136,6 +139,19 @@ export default function contactsPanel(theme: Theme) {
|
|||
background: backgroundColor(theme, 100),
|
||||
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,
|
||||
background: backgroundColor(theme, 300),
|
||||
|
@ -150,16 +166,5 @@ export default function contactsPanel(theme: Theme) {
|
|||
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 { 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 {
|
||||
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) {
|
||||
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 {
|
||||
background: backgroundColor(theme, 300),
|
||||
|
@ -27,6 +48,14 @@ export default function workspace(theme: Theme) {
|
|||
padding: 12,
|
||||
...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,
|
||||
leaderBorderWidth: 2.0,
|
||||
tabBar: tabBar(theme),
|
||||
|
@ -52,7 +81,7 @@ export default function workspace(theme: Theme) {
|
|||
},
|
||||
statusBar: statusBar(theme),
|
||||
titlebar: {
|
||||
avatarWidth: 18,
|
||||
avatarWidth,
|
||||
avatarMargin: 8,
|
||||
height: 33,
|
||||
background: backgroundColor(theme, 100),
|
||||
|
@ -62,12 +91,20 @@ export default function workspace(theme: Theme) {
|
|||
},
|
||||
title: text(theme, "sans", "primary"),
|
||||
avatar: {
|
||||
cornerRadius: 10,
|
||||
cornerRadius: avatarWidth / 2,
|
||||
border: {
|
||||
color: "#00000088",
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
inactiveAvatar: {
|
||||
cornerRadius: avatarWidth / 2,
|
||||
border: {
|
||||
color: "#00000088",
|
||||
width: 1,
|
||||
},
|
||||
grayscale: true,
|
||||
},
|
||||
avatarRibbon: {
|
||||
height: 3,
|
||||
width: 12,
|
||||
|
@ -76,24 +113,7 @@ export default function workspace(theme: Theme) {
|
|||
},
|
||||
border: border(theme, "primary", { bottom: true, overlay: true }),
|
||||
signInPrompt: {
|
||||
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"),
|
||||
},
|
||||
...titlebarButton
|
||||
},
|
||||
offlineIcon: {
|
||||
color: iconColor(theme, "secondary"),
|
||||
|
@ -118,6 +138,30 @@ export default function workspace(theme: Theme) {
|
|||
},
|
||||
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: {
|
||||
height: 34,
|
||||
|
|
Loading…
Reference in a new issue