Merge pull request #1700 from zed-industries/room

Introduce call-based collaboration
This commit is contained in:
Antonio Scandurra 2022-10-11 17:40:44 +01:00 committed by GitHub
commit a656047c15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 7164 additions and 5222 deletions

94
Cargo.lock generated
View File

@ -684,6 +684,20 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
[[package]]
name = "call"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"futures",
"gpui",
"postage",
"project",
"util",
]
[[package]] [[package]]
name = "cap-fs-ext" name = "cap-fs-ext"
version = "0.24.4" version = "0.24.4"
@ -1023,6 +1037,7 @@ dependencies = [
"axum", "axum",
"axum-extra", "axum-extra",
"base64", "base64",
"call",
"clap 3.2.8", "clap 3.2.8",
"client", "client",
"collections", "collections",
@ -1067,6 +1082,31 @@ dependencies = [
"workspace", "workspace",
] ]
[[package]]
name = "collab_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"call",
"client",
"clock",
"collections",
"editor",
"futures",
"fuzzy",
"gpui",
"log",
"menu",
"picker",
"postage",
"project",
"serde",
"settings",
"theme",
"util",
"workspace",
]
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
@ -1108,54 +1148,6 @@ dependencies = [
"cache-padded", "cache-padded",
] ]
[[package]]
name = "contacts_panel"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"editor",
"futures",
"fuzzy",
"gpui",
"language",
"log",
"menu",
"picker",
"postage",
"project",
"serde",
"settings",
"theme",
"util",
"workspace",
]
[[package]]
name = "contacts_status_item"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"editor",
"futures",
"fuzzy",
"gpui",
"language",
"log",
"menu",
"picker",
"postage",
"project",
"serde",
"settings",
"theme",
"util",
"workspace",
]
[[package]] [[package]]
name = "context_menu" name = "context_menu"
version = "0.1.0" version = "0.1.0"
@ -7176,8 +7168,8 @@ name = "workspace"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"call",
"client", "client",
"clock",
"collections", "collections",
"context_menu", "context_menu",
"drag_and_drop", "drag_and_drop",
@ -7247,15 +7239,15 @@ dependencies = [
"auto_update", "auto_update",
"backtrace", "backtrace",
"breadcrumbs", "breadcrumbs",
"call",
"chat_panel", "chat_panel",
"chrono", "chrono",
"cli", "cli",
"client", "client",
"clock", "clock",
"collab_ui",
"collections", "collections",
"command_palette", "command_palette",
"contacts_panel",
"contacts_status_item",
"context_menu", "context_menu",
"ctor", "ctor",
"diagnostics", "diagnostics",

View File

@ -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

View File

@ -395,7 +395,6 @@
"context": "Workspace", "context": "Workspace",
"bindings": { "bindings": {
"shift-escape": "dock::FocusDock", "shift-escape": "dock::FocusDock",
"cmd-shift-c": "contacts_panel::ToggleFocus",
"cmd-shift-b": "workspace::ToggleRightSidebar" "cmd-shift-b": "workspace::ToggleRightSidebar"
} }
}, },

35
crates/call/Cargo.toml Normal file
View 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
View 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)
}
}

View 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
View 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)
}
}

View File

@ -530,7 +530,7 @@ impl ChannelMessage {
) -> Result<Self> { ) -> Result<Self> {
let sender = user_store let sender = user_store
.update(cx, |user_store, cx| { .update(cx, |user_store, cx| {
user_store.fetch_user(message.sender_id, cx) user_store.get_user(message.sender_id, cx)
}) })
.await?; .await?;
Ok(ChannelMessage { Ok(ChannelMessage {

View File

@ -434,6 +434,29 @@ impl Client {
} }
} }
pub fn add_request_handler<M, E, H, F>(
self: &Arc<Self>,
model: ModelHandle<E>,
handler: H,
) -> Subscription
where
M: RequestMessage,
E: Entity,
H: 'static
+ Send
+ Sync
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<M::Response>>,
{
self.add_message_handler(model, move |handle, envelope, this, cx| {
Self::respond_to_request(
envelope.receipt(),
handler(handle, envelope, this.clone(), cx),
this,
)
})
}
pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H) pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where where
M: EntityMessage, M: EntityMessage,

View File

@ -1,14 +1,14 @@
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet}; use collections::{hash_map::Entry, HashMap, HashSet};
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{prelude::Stream, sink::Sink, watch}; use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse}; use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use util::TryFutureExt as _; use util::TryFutureExt as _;
#[derive(Debug)] #[derive(Default, Debug)]
pub struct User { pub struct User {
pub id: u64, pub id: u64,
pub github_login: String, pub github_login: String,
@ -39,14 +39,7 @@ impl Eq for User {}
pub struct Contact { pub struct Contact {
pub user: Arc<User>, pub user: Arc<User>,
pub online: bool, pub online: bool,
pub projects: Vec<ProjectMetadata>, pub busy: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProjectMetadata {
pub id: u64,
pub visible_worktree_root_names: Vec<String>,
pub guests: BTreeSet<Arc<User>>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -138,12 +131,12 @@ impl UserStore {
}), }),
_maintain_current_user: cx.spawn_weak(|this, mut cx| async move { _maintain_current_user: cx.spawn_weak(|this, mut cx| async move {
let mut status = client.status(); let mut status = client.status();
while let Some(status) = status.recv().await { while let Some(status) = status.next().await {
match status { match status {
Status::Connected { .. } => { Status::Connected { .. } => {
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) { if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
let fetch_user = this let fetch_user = this
.update(&mut cx, |this, cx| this.fetch_user(user_id, cx)) .update(&mut cx, |this, cx| this.get_user(user_id, cx))
.log_err(); .log_err();
let fetch_metrics_id = let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err(); client.request(proto::GetPrivateUserInfo {}).log_err();
@ -244,7 +237,6 @@ impl UserStore {
let mut user_ids = HashSet::default(); let mut user_ids = HashSet::default();
for contact in &message.contacts { for contact in &message.contacts {
user_ids.insert(contact.user_id); user_ids.insert(contact.user_id);
user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied());
} }
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id)); user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
user_ids.extend(message.outgoing_requests.iter()); user_ids.extend(message.outgoing_requests.iter());
@ -268,9 +260,7 @@ impl UserStore {
for request in message.incoming_requests { for request in message.incoming_requests {
incoming_requests.push({ incoming_requests.push({
let user = this let user = this
.update(&mut cx, |this, cx| { .update(&mut cx, |this, cx| this.get_user(request.requester_id, cx))
this.fetch_user(request.requester_id, cx)
})
.await?; .await?;
(user, request.should_notify) (user, request.should_notify)
}); });
@ -279,7 +269,7 @@ impl UserStore {
let mut outgoing_requests = Vec::new(); let mut outgoing_requests = Vec::new();
for requested_user_id in message.outgoing_requests { for requested_user_id in message.outgoing_requests {
outgoing_requests.push( outgoing_requests.push(
this.update(&mut cx, |this, cx| this.fetch_user(requested_user_id, cx)) this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))
.await?, .await?,
); );
} }
@ -504,7 +494,7 @@ impl UserStore {
.unbounded_send(UpdateContacts::Clear(tx)) .unbounded_send(UpdateContacts::Clear(tx))
.unwrap(); .unwrap();
async move { async move {
rx.recv().await; rx.next().await;
} }
} }
@ -514,25 +504,43 @@ impl UserStore {
.unbounded_send(UpdateContacts::Wait(tx)) .unbounded_send(UpdateContacts::Wait(tx))
.unwrap(); .unwrap();
async move { async move {
rx.recv().await; rx.next().await;
} }
} }
pub fn get_users( pub fn get_users(
&mut self, &mut self,
mut user_ids: Vec<u64>, user_ids: Vec<u64>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<Vec<Arc<User>>>> {
user_ids.retain(|id| !self.users.contains_key(id)); let mut user_ids_to_fetch = user_ids.clone();
if user_ids.is_empty() { user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
Task::ready(Ok(()))
} else { cx.spawn(|this, mut cx| async move {
let load = self.load_users(proto::GetUsers { user_ids }, cx); if !user_ids_to_fetch.is_empty() {
cx.foreground().spawn(async move { this.update(&mut cx, |this, cx| {
load.await?; this.load_users(
Ok(()) proto::GetUsers {
user_ids: user_ids_to_fetch,
},
cx,
)
})
.await?;
}
this.read_with(&cx, |this, _| {
user_ids
.iter()
.map(|user_id| {
this.users
.get(user_id)
.cloned()
.ok_or_else(|| anyhow!("user {} not found", user_id))
})
.collect()
}) })
} })
} }
pub fn fuzzy_search_users( pub fn fuzzy_search_users(
@ -543,7 +551,7 @@ impl UserStore {
self.load_users(proto::FuzzySearchUsers { query }, cx) self.load_users(proto::FuzzySearchUsers { query }, cx)
} }
pub fn fetch_user( pub fn get_user(
&mut self, &mut self,
user_id: u64, user_id: u64,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
@ -623,39 +631,15 @@ impl Contact {
) -> Result<Self> { ) -> Result<Self> {
let user = user_store let user = user_store
.update(cx, |user_store, cx| { .update(cx, |user_store, cx| {
user_store.fetch_user(contact.user_id, cx) user_store.get_user(contact.user_id, cx)
}) })
.await?; .await?;
let mut projects = Vec::new();
for project in contact.projects {
let mut guests = BTreeSet::new();
for participant_id in project.guests {
guests.insert(
user_store
.update(cx, |user_store, cx| {
user_store.fetch_user(participant_id, cx)
})
.await?,
);
}
projects.push(ProjectMetadata {
id: project.id,
visible_worktree_root_names: project.visible_worktree_root_names.clone(),
guests,
});
}
Ok(Self { Ok(Self {
user, user,
online: contact.online, online: contact.online,
projects, busy: contact.busy,
}) })
} }
pub fn non_empty_projects(&self) -> impl Iterator<Item = &ProjectMetadata> {
self.projects
.iter()
.filter(|project| !project.visible_worktree_root_names.is_empty())
}
} }
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> { async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {

View File

@ -56,13 +56,14 @@ features = ["runtime-tokio-rustls", "postgres", "time", "uuid"]
[dev-dependencies] [dev-dependencies]
collections = { path = "../collections", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] } call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] } client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] } language = { path = "../language", features = ["test-support"] }
log = { version = "0.4.16", features = ["kv_unstable_serde"] } log = { version = "0.4.16", features = ["kv_unstable_serde"] }
lsp = { path = "../lsp", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] } project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme" } theme = { path = "../theme" }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }

View File

@ -84,7 +84,23 @@ async fn main() {
}, },
) )
.await .await
.expect("failed to insert user"), .expect("failed to insert user")
.user_id,
);
} else if admin {
zed_user_ids.push(
db.create_user(
&format!("{}@zed.dev", github_user.login),
admin,
db::NewUserParams {
github_login: github_user.login,
github_user_id: github_user.id,
invite_count: 5,
},
)
.await
.expect("failed to insert user")
.user_id,
); );
} }
} }

View File

@ -1098,10 +1098,7 @@ impl Db for PostgresDb {
.bind(user_id) .bind(user_id)
.fetch(&self.pool); .fetch(&self.pool);
let mut contacts = vec![Contact::Accepted { let mut contacts = Vec::new();
user_id,
should_notify: false,
}];
while let Some(row) = rows.next().await { while let Some(row) = rows.next().await {
let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?; let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?;
@ -2080,10 +2077,7 @@ mod test {
async fn get_contacts(&self, id: UserId) -> Result<Vec<Contact>> { async fn get_contacts(&self, id: UserId) -> Result<Vec<Contact>> {
self.background.simulate_random_delay().await; self.background.simulate_random_delay().await;
let mut contacts = vec![Contact::Accepted { let mut contacts = Vec::new();
user_id: id,
should_notify: false,
}];
for contact in self.contacts.lock().iter() { for contact in self.contacts.lock().iter() {
if contact.requester_id == id { if contact.requester_id == id {

View File

@ -666,13 +666,7 @@ async fn test_add_contacts() {
let user_3 = user_ids[2]; let user_3 = user_ids[2];
// User starts with no contacts // User starts with no contacts
assert_eq!( assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
db.get_contacts(user_1).await.unwrap(),
vec![Contact::Accepted {
user_id: user_1,
should_notify: false
}],
);
// User requests a contact. Both users see the pending request. // User requests a contact. Both users see the pending request.
db.send_contact_request(user_1, user_2).await.unwrap(); db.send_contact_request(user_1, user_2).await.unwrap();
@ -680,26 +674,14 @@ async fn test_add_contacts() {
assert!(!db.has_contact(user_2, user_1).await.unwrap()); assert!(!db.has_contact(user_2, user_1).await.unwrap());
assert_eq!( assert_eq!(
db.get_contacts(user_1).await.unwrap(), db.get_contacts(user_1).await.unwrap(),
&[ &[Contact::Outgoing { user_id: user_2 }],
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Outgoing { user_id: user_2 }
],
); );
assert_eq!( assert_eq!(
db.get_contacts(user_2).await.unwrap(), db.get_contacts(user_2).await.unwrap(),
&[ &[Contact::Incoming {
Contact::Incoming { user_id: user_1,
user_id: user_1, should_notify: true
should_notify: true }]
},
Contact::Accepted {
user_id: user_2,
should_notify: false
},
]
); );
// User 2 dismisses the contact request notification without accepting or rejecting. // User 2 dismisses the contact request notification without accepting or rejecting.
@ -712,16 +694,10 @@ async fn test_add_contacts() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
db.get_contacts(user_2).await.unwrap(), db.get_contacts(user_2).await.unwrap(),
&[ &[Contact::Incoming {
Contact::Incoming { user_id: user_1,
user_id: user_1, should_notify: false
should_notify: false }]
},
Contact::Accepted {
user_id: user_2,
should_notify: false
},
]
); );
// User can't accept their own contact request // User can't accept their own contact request
@ -735,31 +711,19 @@ async fn test_add_contacts() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
db.get_contacts(user_1).await.unwrap(), db.get_contacts(user_1).await.unwrap(),
&[ &[Contact::Accepted {
Contact::Accepted { user_id: user_2,
user_id: user_1, should_notify: true
should_notify: false }],
},
Contact::Accepted {
user_id: user_2,
should_notify: true
}
],
); );
assert!(db.has_contact(user_1, user_2).await.unwrap()); assert!(db.has_contact(user_1, user_2).await.unwrap());
assert!(db.has_contact(user_2, user_1).await.unwrap()); assert!(db.has_contact(user_2, user_1).await.unwrap());
assert_eq!( assert_eq!(
db.get_contacts(user_2).await.unwrap(), db.get_contacts(user_2).await.unwrap(),
&[ &[Contact::Accepted {
Contact::Accepted { user_id: user_1,
user_id: user_1, should_notify: false,
should_notify: false, }]
},
Contact::Accepted {
user_id: user_2,
should_notify: false,
},
]
); );
// Users cannot re-request existing contacts. // Users cannot re-request existing contacts.
@ -772,16 +736,10 @@ async fn test_add_contacts() {
.unwrap_err(); .unwrap_err();
assert_eq!( assert_eq!(
db.get_contacts(user_1).await.unwrap(), db.get_contacts(user_1).await.unwrap(),
&[ &[Contact::Accepted {
Contact::Accepted { user_id: user_2,
user_id: user_1, should_notify: true,
should_notify: false }]
},
Contact::Accepted {
user_id: user_2,
should_notify: true,
},
]
); );
// Users can dismiss notifications of other users accepting their requests. // Users can dismiss notifications of other users accepting their requests.
@ -790,16 +748,10 @@ async fn test_add_contacts() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
db.get_contacts(user_1).await.unwrap(), db.get_contacts(user_1).await.unwrap(),
&[ &[Contact::Accepted {
Contact::Accepted { user_id: user_2,
user_id: user_1, should_notify: false,
should_notify: false }]
},
Contact::Accepted {
user_id: user_2,
should_notify: false,
},
]
); );
// Users send each other concurrent contact requests and // Users send each other concurrent contact requests and
@ -809,10 +761,6 @@ async fn test_add_contacts() {
assert_eq!( assert_eq!(
db.get_contacts(user_1).await.unwrap(), db.get_contacts(user_1).await.unwrap(),
&[ &[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted { Contact::Accepted {
user_id: user_2, user_id: user_2,
should_notify: false, should_notify: false,
@ -820,21 +768,15 @@ async fn test_add_contacts() {
Contact::Accepted { Contact::Accepted {
user_id: user_3, user_id: user_3,
should_notify: false should_notify: false
}, }
] ]
); );
assert_eq!( assert_eq!(
db.get_contacts(user_3).await.unwrap(), db.get_contacts(user_3).await.unwrap(),
&[ &[Contact::Accepted {
Contact::Accepted { user_id: user_1,
user_id: user_1, should_notify: false
should_notify: false }],
},
Contact::Accepted {
user_id: user_3,
should_notify: false
}
],
); );
// User declines a contact request. Both users see that it is gone. // User declines a contact request. Both users see that it is gone.
@ -846,29 +788,17 @@ async fn test_add_contacts() {
assert!(!db.has_contact(user_3, user_2).await.unwrap()); assert!(!db.has_contact(user_3, user_2).await.unwrap());
assert_eq!( assert_eq!(
db.get_contacts(user_2).await.unwrap(), db.get_contacts(user_2).await.unwrap(),
&[ &[Contact::Accepted {
Contact::Accepted { user_id: user_1,
user_id: user_1, should_notify: false
should_notify: false }]
},
Contact::Accepted {
user_id: user_2,
should_notify: false
}
]
); );
assert_eq!( assert_eq!(
db.get_contacts(user_3).await.unwrap(), db.get_contacts(user_3).await.unwrap(),
&[ &[Contact::Accepted {
Contact::Accepted { user_id: user_1,
user_id: user_1, should_notify: false
should_notify: false }],
},
Contact::Accepted {
user_id: user_3,
should_notify: false
}
],
); );
} }
} }
@ -930,29 +860,17 @@ async fn test_invite_codes() {
assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id); assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
assert_eq!( assert_eq!(
db.get_contacts(user1).await.unwrap(), db.get_contacts(user1).await.unwrap(),
[ [Contact::Accepted {
Contact::Accepted { user_id: user2,
user_id: user1, should_notify: true
should_notify: false }]
},
Contact::Accepted {
user_id: user2,
should_notify: true
}
]
); );
assert_eq!( assert_eq!(
db.get_contacts(user2).await.unwrap(), db.get_contacts(user2).await.unwrap(),
[ [Contact::Accepted {
Contact::Accepted { user_id: user1,
user_id: user1, should_notify: false
should_notify: false }]
},
Contact::Accepted {
user_id: user2,
should_notify: false
}
]
); );
assert_eq!( assert_eq!(
db.get_invite_code_for_user(user2).await.unwrap().unwrap().1, db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
@ -987,10 +905,6 @@ async fn test_invite_codes() {
assert_eq!( assert_eq!(
db.get_contacts(user1).await.unwrap(), db.get_contacts(user1).await.unwrap(),
[ [
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted { Contact::Accepted {
user_id: user2, user_id: user2,
should_notify: true should_notify: true
@ -1003,16 +917,10 @@ async fn test_invite_codes() {
); );
assert_eq!( assert_eq!(
db.get_contacts(user3).await.unwrap(), db.get_contacts(user3).await.unwrap(),
[ [Contact::Accepted {
Contact::Accepted { user_id: user1,
user_id: user1, should_notify: false
should_notify: false }]
},
Contact::Accepted {
user_id: user3,
should_notify: false
},
]
); );
assert_eq!( assert_eq!(
db.get_invite_code_for_user(user3).await.unwrap().unwrap().1, db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
@ -1053,10 +961,6 @@ async fn test_invite_codes() {
assert_eq!( assert_eq!(
db.get_contacts(user1).await.unwrap(), db.get_contacts(user1).await.unwrap(),
[ [
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted { Contact::Accepted {
user_id: user2, user_id: user2,
should_notify: true should_notify: true
@ -1073,16 +977,10 @@ async fn test_invite_codes() {
); );
assert_eq!( assert_eq!(
db.get_contacts(user4).await.unwrap(), db.get_contacts(user4).await.unwrap(),
[ [Contact::Accepted {
Contact::Accepted { user_id: user1,
user_id: user1, should_notify: false
should_notify: false }]
},
Contact::Accepted {
user_id: user4,
should_notify: false
},
]
); );
assert_eq!( assert_eq!(
db.get_invite_code_for_user(user4).await.unwrap().unwrap().1, db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@ use axum::{
routing::get, routing::get,
Extension, Router, TypedHeader, Extension, Router, TypedHeader,
}; };
use collections::HashMap; use collections::{HashMap, HashSet};
use futures::{ use futures::{
channel::mpsc, channel::mpsc,
future::{self, BoxFuture}, future::{self, BoxFuture},
@ -88,11 +88,6 @@ impl<R: RequestMessage> Response<R> {
self.server.peer.respond(self.receipt, payload)?; self.server.peer.respond(self.receipt, payload)?;
Ok(()) Ok(())
} }
fn into_receipt(self) -> Receipt<R> {
self.responded.store(true, SeqCst);
self.receipt
}
} }
pub struct Server { pub struct Server {
@ -151,11 +146,17 @@ impl Server {
server server
.add_request_handler(Server::ping) .add_request_handler(Server::ping)
.add_request_handler(Server::register_project) .add_request_handler(Server::create_room)
.add_request_handler(Server::unregister_project) .add_request_handler(Server::join_room)
.add_message_handler(Server::leave_room)
.add_request_handler(Server::call)
.add_request_handler(Server::cancel_call)
.add_message_handler(Server::decline_call)
.add_request_handler(Server::update_participant_location)
.add_request_handler(Server::share_project)
.add_message_handler(Server::unshare_project)
.add_request_handler(Server::join_project) .add_request_handler(Server::join_project)
.add_message_handler(Server::leave_project) .add_message_handler(Server::leave_project)
.add_message_handler(Server::respond_to_join_project_request)
.add_message_handler(Server::update_project) .add_message_handler(Server::update_project)
.add_message_handler(Server::register_project_activity) .add_message_handler(Server::register_project_activity)
.add_request_handler(Server::update_worktree) .add_request_handler(Server::update_worktree)
@ -385,7 +386,11 @@ impl Server {
{ {
let mut store = this.store().await; let mut store = this.store().await;
store.add_connection(connection_id, user_id, user.admin); let incoming_call = store.add_connection(connection_id, user_id, user.admin);
if let Some(incoming_call) = incoming_call {
this.peer.send(connection_id, incoming_call)?;
}
this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?; this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?;
if let Some((code, count)) = invite_code { if let Some((code, count)) = invite_code {
@ -468,69 +473,58 @@ impl Server {
async fn sign_out(self: &mut Arc<Self>, connection_id: ConnectionId) -> Result<()> { async fn sign_out(self: &mut Arc<Self>, connection_id: ConnectionId) -> Result<()> {
self.peer.disconnect(connection_id); self.peer.disconnect(connection_id);
let mut projects_to_unregister = Vec::new(); let mut projects_to_unshare = Vec::new();
let removed_user_id; let mut contacts_to_update = HashSet::default();
{ {
let mut store = self.store().await; let mut store = self.store().await;
let removed_connection = store.remove_connection(connection_id)?; let removed_connection = store.remove_connection(connection_id)?;
for (project_id, project) in removed_connection.hosted_projects { for project in removed_connection.hosted_projects {
projects_to_unregister.push(project_id); projects_to_unshare.push(project.id);
broadcast(connection_id, project.guests.keys().copied(), |conn_id| { broadcast(connection_id, project.guests.keys().copied(), |conn_id| {
self.peer.send( self.peer.send(
conn_id, conn_id,
proto::UnregisterProject { proto::UnshareProject {
project_id: project_id.to_proto(), project_id: project.id.to_proto(),
}, },
) )
}); });
for (_, receipts) in project.join_requests {
for receipt in receipts {
self.peer.respond(
receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {
reason: proto::join_project_response::decline::Reason::WentOffline as i32
},
)),
},
)?;
}
}
} }
for project_id in removed_connection.guest_project_ids { for project in removed_connection.guest_projects {
if let Some(project) = store.project(project_id).trace_err() { broadcast(connection_id, project.connection_ids, |conn_id| {
broadcast(connection_id, project.connection_ids(), |conn_id| { self.peer.send(
self.peer.send( conn_id,
conn_id, proto::RemoveProjectCollaborator {
proto::RemoveProjectCollaborator { project_id: project.id.to_proto(),
project_id: project_id.to_proto(), peer_id: connection_id.0,
peer_id: connection_id.0, },
}, )
) });
});
if project.guests.is_empty() {
self.peer
.send(
project.host_connection_id,
proto::ProjectUnshared {
project_id: project_id.to_proto(),
},
)
.trace_err();
}
}
} }
removed_user_id = removed_connection.user_id; for connection_id in removed_connection.canceled_call_connection_ids {
self.peer
.send(connection_id, proto::CallCanceled {})
.trace_err();
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
}
if let Some(room) = removed_connection
.room_id
.and_then(|room_id| store.room(room_id))
{
self.room_updated(room);
}
contacts_to_update.insert(removed_connection.user_id);
}; };
self.update_user_contacts(removed_user_id).await.trace_err(); for user_id in contacts_to_update {
self.update_user_contacts(user_id).await.trace_err();
}
for project_id in projects_to_unregister { for project_id in projects_to_unshare {
self.app_state self.app_state
.db .db
.unregister_project(project_id) .unregister_project(project_id)
@ -598,76 +592,286 @@ impl Server {
Ok(()) Ok(())
} }
async fn register_project( async fn create_room(
self: Arc<Server>, self: Arc<Server>,
request: TypedEnvelope<proto::RegisterProject>, request: TypedEnvelope<proto::CreateRoom>,
response: Response<proto::RegisterProject>, response: Response<proto::CreateRoom>,
) -> Result<()> {
let user_id;
let room_id;
{
let mut store = self.store().await;
user_id = store.user_id_for_connection(request.sender_id)?;
room_id = store.create_room(request.sender_id)?;
}
response.send(proto::CreateRoomResponse { id: room_id })?;
self.update_user_contacts(user_id).await?;
Ok(())
}
async fn join_room(
self: Arc<Server>,
request: TypedEnvelope<proto::JoinRoom>,
response: Response<proto::JoinRoom>,
) -> Result<()> {
let user_id;
{
let mut store = self.store().await;
user_id = store.user_id_for_connection(request.sender_id)?;
let (room, recipient_connection_ids) =
store.join_room(request.payload.id, request.sender_id)?;
for recipient_id in recipient_connection_ids {
self.peer
.send(recipient_id, proto::CallCanceled {})
.trace_err();
}
response.send(proto::JoinRoomResponse {
room: Some(room.clone()),
})?;
self.room_updated(room);
}
self.update_user_contacts(user_id).await?;
Ok(())
}
async fn leave_room(self: Arc<Server>, message: TypedEnvelope<proto::LeaveRoom>) -> Result<()> {
let mut contacts_to_update = HashSet::default();
{
let mut store = self.store().await;
let user_id = store.user_id_for_connection(message.sender_id)?;
let left_room = store.leave_room(message.payload.id, message.sender_id)?;
contacts_to_update.insert(user_id);
for project in left_room.unshared_projects {
for connection_id in project.connection_ids() {
self.peer.send(
connection_id,
proto::UnshareProject {
project_id: project.id.to_proto(),
},
)?;
}
}
for project in left_room.left_projects {
if project.remove_collaborator {
for connection_id in project.connection_ids {
self.peer.send(
connection_id,
proto::RemoveProjectCollaborator {
project_id: project.id.to_proto(),
peer_id: message.sender_id.0,
},
)?;
}
self.peer.send(
message.sender_id,
proto::UnshareProject {
project_id: project.id.to_proto(),
},
)?;
}
}
if let Some(room) = left_room.room {
self.room_updated(room);
}
for connection_id in left_room.canceled_call_connection_ids {
self.peer
.send(connection_id, proto::CallCanceled {})
.trace_err();
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
}
}
for user_id in contacts_to_update {
self.update_user_contacts(user_id).await?;
}
Ok(())
}
async fn call(
self: Arc<Server>,
request: TypedEnvelope<proto::Call>,
response: Response<proto::Call>,
) -> Result<()> {
let caller_user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id);
let initial_project_id = request
.payload
.initial_project_id
.map(ProjectId::from_proto);
if !self
.app_state
.db
.has_contact(caller_user_id, recipient_user_id)
.await?
{
return Err(anyhow!("cannot call a user who isn't a contact"))?;
}
let room_id = request.payload.room_id;
let mut calls = {
let mut store = self.store().await;
let (room, recipient_connection_ids, incoming_call) = store.call(
room_id,
recipient_user_id,
initial_project_id,
request.sender_id,
)?;
self.room_updated(room);
recipient_connection_ids
.into_iter()
.map(|recipient_connection_id| {
self.peer
.request(recipient_connection_id, incoming_call.clone())
})
.collect::<FuturesUnordered<_>>()
};
self.update_user_contacts(recipient_user_id).await?;
while let Some(call_response) = calls.next().await {
match call_response.as_ref() {
Ok(_) => {
response.send(proto::Ack {})?;
return Ok(());
}
Err(_) => {
call_response.trace_err();
}
}
}
{
let mut store = self.store().await;
let room = store.call_failed(room_id, recipient_user_id)?;
self.room_updated(&room);
}
self.update_user_contacts(recipient_user_id).await?;
Err(anyhow!("failed to ring call recipient"))?
}
async fn cancel_call(
self: Arc<Server>,
request: TypedEnvelope<proto::CancelCall>,
response: Response<proto::CancelCall>,
) -> Result<()> {
let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id);
{
let mut store = self.store().await;
let (room, recipient_connection_ids) = store.cancel_call(
request.payload.room_id,
recipient_user_id,
request.sender_id,
)?;
for recipient_id in recipient_connection_ids {
self.peer
.send(recipient_id, proto::CallCanceled {})
.trace_err();
}
self.room_updated(room);
response.send(proto::Ack {})?;
}
self.update_user_contacts(recipient_user_id).await?;
Ok(())
}
async fn decline_call(
self: Arc<Server>,
message: TypedEnvelope<proto::DeclineCall>,
) -> Result<()> {
let recipient_user_id;
{
let mut store = self.store().await;
recipient_user_id = store.user_id_for_connection(message.sender_id)?;
let (room, recipient_connection_ids) =
store.decline_call(message.payload.room_id, message.sender_id)?;
for recipient_id in recipient_connection_ids {
self.peer
.send(recipient_id, proto::CallCanceled {})
.trace_err();
}
self.room_updated(room);
}
self.update_user_contacts(recipient_user_id).await?;
Ok(())
}
async fn update_participant_location(
self: Arc<Server>,
request: TypedEnvelope<proto::UpdateParticipantLocation>,
response: Response<proto::UpdateParticipantLocation>,
) -> Result<()> {
let room_id = request.payload.room_id;
let location = request
.payload
.location
.ok_or_else(|| anyhow!("invalid location"))?;
let mut store = self.store().await;
let room = store.update_participant_location(room_id, location, request.sender_id)?;
self.room_updated(room);
response.send(proto::Ack {})?;
Ok(())
}
fn room_updated(&self, room: &proto::Room) {
for participant in &room.participants {
self.peer
.send(
ConnectionId(participant.peer_id),
proto::RoomUpdated {
room: Some(room.clone()),
},
)
.trace_err();
}
}
async fn share_project(
self: Arc<Server>,
request: TypedEnvelope<proto::ShareProject>,
response: Response<proto::ShareProject>,
) -> Result<()> { ) -> Result<()> {
let user_id = self let user_id = self
.store() .store()
.await .await
.user_id_for_connection(request.sender_id)?; .user_id_for_connection(request.sender_id)?;
let project_id = self.app_state.db.register_project(user_id).await?; let project_id = self.app_state.db.register_project(user_id).await?;
self.store().await.register_project( let mut store = self.store().await;
request.sender_id, let room = store.share_project(
request.payload.room_id,
project_id, project_id,
request.payload.online, request.payload.worktrees,
request.sender_id,
)?; )?;
response.send(proto::ShareProjectResponse {
response.send(proto::RegisterProjectResponse {
project_id: project_id.to_proto(), project_id: project_id.to_proto(),
})?; })?;
self.room_updated(room);
Ok(()) Ok(())
} }
async fn unregister_project( async fn unshare_project(
self: Arc<Server>, self: Arc<Server>,
request: TypedEnvelope<proto::UnregisterProject>, message: TypedEnvelope<proto::UnshareProject>,
response: Response<proto::UnregisterProject>,
) -> Result<()> { ) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id); let project_id = ProjectId::from_proto(message.payload.project_id);
let (user_id, project) = { let mut store = self.store().await;
let mut state = self.store().await; let (room, project) = store.unshare_project(project_id, message.sender_id)?;
let project = state.unregister_project(project_id, request.sender_id)?;
(state.user_id_for_connection(request.sender_id)?, project)
};
self.app_state.db.unregister_project(project_id).await?;
broadcast( broadcast(
request.sender_id, message.sender_id,
project.guests.keys().copied(), project.guest_connection_ids(),
|conn_id| { |conn_id| self.peer.send(conn_id, message.payload.clone()),
self.peer.send(
conn_id,
proto::UnregisterProject {
project_id: project_id.to_proto(),
},
)
},
); );
for (_, receipts) in project.join_requests { self.room_updated(room);
for receipt in receipts {
self.peer.respond(
receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {
reason: proto::join_project_response::decline::Reason::Closed
as i32,
},
)),
},
)?;
}
}
// Send out the `UpdateContacts` message before responding to the unregister
// request. This way, when the project's host can keep track of the project's
// remote id until after they've received the `UpdateContacts` message for
// themself.
self.update_user_contacts(user_id).await?;
response.send(proto::Ack {})?;
Ok(()) Ok(())
} }
@ -721,176 +925,94 @@ impl Server {
}; };
tracing::info!(%project_id, %host_user_id, %host_connection_id, "join project"); tracing::info!(%project_id, %host_user_id, %host_connection_id, "join project");
let has_contact = self
.app_state let mut store = self.store().await;
.db let (project, replica_id) = store.join_project(request.sender_id, project_id)?;
.has_contact(guest_user_id, host_user_id) let peer_count = project.guests.len();
.await?; let mut collaborators = Vec::with_capacity(peer_count);
if !has_contact { collaborators.push(proto::Collaborator {
return Err(anyhow!("no such project"))?; peer_id: project.host_connection_id.0,
replica_id: 0,
user_id: project.host.user_id.to_proto(),
});
let worktrees = project
.worktrees
.iter()
.map(|(id, worktree)| proto::WorktreeMetadata {
id: *id,
root_name: worktree.root_name.clone(),
visible: worktree.visible,
})
.collect::<Vec<_>>();
// Add all guests other than the requesting user's own connections as collaborators
for (guest_conn_id, guest) in &project.guests {
if request.sender_id != *guest_conn_id {
collaborators.push(proto::Collaborator {
peer_id: guest_conn_id.0,
replica_id: guest.replica_id as u32,
user_id: guest.user_id.to_proto(),
});
}
} }
self.store().await.request_join_project( for conn_id in project.connection_ids() {
guest_user_id, if conn_id != request.sender_id {
project_id, self.peer.send(
response.into_receipt(), conn_id,
)?; proto::AddProjectCollaborator {
self.peer.send( project_id: project_id.to_proto(),
host_connection_id, collaborator: Some(proto::Collaborator {
proto::RequestJoinProject { peer_id: request.sender_id.0,
project_id: project_id.to_proto(), replica_id: replica_id as u32,
requester_id: guest_user_id.to_proto(), user_id: guest_user_id.to_proto(),
}, }),
)?;
Ok(())
}
async fn respond_to_join_project_request(
self: Arc<Server>,
request: TypedEnvelope<proto::RespondToJoinProjectRequest>,
) -> Result<()> {
let host_user_id;
{
let mut state = self.store().await;
let project_id = ProjectId::from_proto(request.payload.project_id);
let project = state.project(project_id)?;
if project.host_connection_id != request.sender_id {
Err(anyhow!("no such connection"))?;
}
host_user_id = project.host.user_id;
let guest_user_id = UserId::from_proto(request.payload.requester_id);
if !request.payload.allow {
let receipts = state
.deny_join_project_request(request.sender_id, guest_user_id, project_id)
.ok_or_else(|| anyhow!("no such request"))?;
for receipt in receipts {
self.peer.respond(
receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {
reason: proto::join_project_response::decline::Reason::Declined
as i32,
},
)),
},
)?;
}
return Ok(());
}
let (receipts_with_replica_ids, project) = state
.accept_join_project_request(request.sender_id, guest_user_id, project_id)
.ok_or_else(|| anyhow!("no such request"))?;
let peer_count = project.guests.len();
let mut collaborators = Vec::with_capacity(peer_count);
collaborators.push(proto::Collaborator {
peer_id: project.host_connection_id.0,
replica_id: 0,
user_id: project.host.user_id.to_proto(),
});
let worktrees = project
.worktrees
.iter()
.map(|(id, worktree)| proto::WorktreeMetadata {
id: *id,
root_name: worktree.root_name.clone(),
visible: worktree.visible,
})
.collect::<Vec<_>>();
// Add all guests other than the requesting user's own connections as collaborators
for (guest_conn_id, guest) in &project.guests {
if receipts_with_replica_ids
.iter()
.all(|(receipt, _)| receipt.sender_id != *guest_conn_id)
{
collaborators.push(proto::Collaborator {
peer_id: guest_conn_id.0,
replica_id: guest.replica_id as u32,
user_id: guest.user_id.to_proto(),
});
}
}
for conn_id in project.connection_ids() {
for (receipt, replica_id) in &receipts_with_replica_ids {
if conn_id != receipt.sender_id {
self.peer.send(
conn_id,
proto::AddProjectCollaborator {
project_id: project_id.to_proto(),
collaborator: Some(proto::Collaborator {
peer_id: receipt.sender_id.0,
replica_id: *replica_id as u32,
user_id: guest_user_id.to_proto(),
}),
},
)?;
}
}
}
// First, we send the metadata associated with each worktree.
for (receipt, replica_id) in &receipts_with_replica_ids {
self.peer.respond(
*receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Accept(
proto::join_project_response::Accept {
worktrees: worktrees.clone(),
replica_id: *replica_id as u32,
collaborators: collaborators.clone(),
language_servers: project.language_servers.clone(),
},
)),
}, },
)?; )?;
} }
}
for (worktree_id, worktree) in &project.worktrees { // First, we send the metadata associated with each worktree.
#[cfg(any(test, feature = "test-support"))] response.send(proto::JoinProjectResponse {
const MAX_CHUNK_SIZE: usize = 2; worktrees: worktrees.clone(),
#[cfg(not(any(test, feature = "test-support")))] replica_id: replica_id as u32,
const MAX_CHUNK_SIZE: usize = 256; collaborators: collaborators.clone(),
language_servers: project.language_servers.clone(),
})?;
// Stream this worktree's entries. for (worktree_id, worktree) in &project.worktrees {
let message = proto::UpdateWorktree { #[cfg(any(test, feature = "test-support"))]
project_id: project_id.to_proto(), const MAX_CHUNK_SIZE: usize = 2;
worktree_id: *worktree_id, #[cfg(not(any(test, feature = "test-support")))]
root_name: worktree.root_name.clone(), const MAX_CHUNK_SIZE: usize = 256;
updated_entries: worktree.entries.values().cloned().collect(),
removed_entries: Default::default(),
scan_id: worktree.scan_id,
is_last_update: worktree.is_complete,
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
for (receipt, _) in &receipts_with_replica_ids {
self.peer.send(receipt.sender_id, update.clone())?;
}
}
// Stream this worktree's diagnostics. // Stream this worktree's entries.
for summary in worktree.diagnostic_summaries.values() { let message = proto::UpdateWorktree {
for (receipt, _) in &receipts_with_replica_ids { project_id: project_id.to_proto(),
self.peer.send( worktree_id: *worktree_id,
receipt.sender_id, root_name: worktree.root_name.clone(),
proto::UpdateDiagnosticSummary { updated_entries: worktree.entries.values().cloned().collect(),
project_id: project_id.to_proto(), removed_entries: Default::default(),
worktree_id: *worktree_id, scan_id: worktree.scan_id,
summary: Some(summary.clone()), is_last_update: worktree.is_complete,
}, };
)?; for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
} self.peer.send(request.sender_id, update.clone())?;
} }
// Stream this worktree's diagnostics.
for summary in worktree.diagnostic_summaries.values() {
self.peer.send(
request.sender_id,
proto::UpdateDiagnosticSummary {
project_id: project_id.to_proto(),
worktree_id: *worktree_id,
summary: Some(summary.clone()),
},
)?;
} }
} }
self.update_user_contacts(host_user_id).await?;
Ok(()) Ok(())
} }
@ -903,7 +1025,7 @@ impl Server {
let project; let project;
{ {
let mut store = self.store().await; let mut store = self.store().await;
project = store.leave_project(sender_id, project_id)?; project = store.leave_project(project_id, sender_id)?;
tracing::info!( tracing::info!(
%project_id, %project_id,
host_user_id = %project.host_user_id, host_user_id = %project.host_user_id,
@ -922,27 +1044,8 @@ impl Server {
) )
}); });
} }
if let Some(requester_id) = project.cancel_request {
self.peer.send(
project.host_connection_id,
proto::JoinProjectRequestCancelled {
project_id: project_id.to_proto(),
requester_id: requester_id.to_proto(),
},
)?;
}
if project.unshare {
self.peer.send(
project.host_connection_id,
proto::ProjectUnshared {
project_id: project_id.to_proto(),
},
)?;
}
} }
self.update_user_contacts(project.host_user_id).await?;
Ok(()) Ok(())
} }
@ -951,61 +1054,20 @@ impl Server {
request: TypedEnvelope<proto::UpdateProject>, request: TypedEnvelope<proto::UpdateProject>,
) -> Result<()> { ) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id); let project_id = ProjectId::from_proto(request.payload.project_id);
let user_id;
{ {
let mut state = self.store().await; let mut state = self.store().await;
user_id = state.user_id_for_connection(request.sender_id)?;
let guest_connection_ids = state let guest_connection_ids = state
.read_project(project_id, request.sender_id)? .read_project(project_id, request.sender_id)?
.guest_connection_ids(); .guest_connection_ids();
let unshared_project = state.update_project( let room =
project_id, state.update_project(project_id, &request.payload.worktrees, request.sender_id)?;
&request.payload.worktrees, broadcast(request.sender_id, guest_connection_ids, |connection_id| {
request.payload.online, self.peer
request.sender_id, .forward_send(request.sender_id, connection_id, request.payload.clone())
)?; });
self.room_updated(room);
if let Some(unshared_project) = unshared_project {
broadcast(
request.sender_id,
unshared_project.guests.keys().copied(),
|conn_id| {
self.peer.send(
conn_id,
proto::UnregisterProject {
project_id: project_id.to_proto(),
},
)
},
);
for (_, receipts) in unshared_project.pending_join_requests {
for receipt in receipts {
self.peer.respond(
receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {
reason:
proto::join_project_response::decline::Reason::Closed
as i32,
},
)),
},
)?;
}
}
} else {
broadcast(request.sender_id, guest_connection_ids, |connection_id| {
self.peer.forward_send(
request.sender_id,
connection_id,
request.payload.clone(),
)
});
}
}; };
self.update_user_contacts(user_id).await?;
Ok(()) Ok(())
} }
@ -1027,32 +1089,21 @@ impl Server {
) -> Result<()> { ) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id); let project_id = ProjectId::from_proto(request.payload.project_id);
let worktree_id = request.payload.worktree_id; let worktree_id = request.payload.worktree_id;
let (connection_ids, metadata_changed) = { let connection_ids = self.store().await.update_worktree(
let mut store = self.store().await; request.sender_id,
let (connection_ids, metadata_changed) = store.update_worktree( project_id,
request.sender_id, worktree_id,
project_id, &request.payload.root_name,
worktree_id, &request.payload.removed_entries,
&request.payload.root_name, &request.payload.updated_entries,
&request.payload.removed_entries, request.payload.scan_id,
&request.payload.updated_entries, request.payload.is_last_update,
request.payload.scan_id, )?;
request.payload.is_last_update,
)?;
(connection_ids, metadata_changed)
};
broadcast(request.sender_id, connection_ids, |connection_id| { broadcast(request.sender_id, connection_ids, |connection_id| {
self.peer self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone()) .forward_send(request.sender_id, connection_id, request.payload.clone())
}); });
if metadata_changed {
let user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
self.update_user_contacts(user_id).await?;
}
response.send(proto::Ack {})?; response.send(proto::Ack {})?;
Ok(()) Ok(())
} }

File diff suppressed because it is too large Load Diff

View 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"] }

View 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(),
})
}
}

View 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);
});
}

View File

@ -1,21 +1,15 @@
use client::{ContactRequestStatus, User, UserStore}; use client::{ContactRequestStatus, User, UserStore};
use gpui::{ use gpui::{
actions, elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext,
RenderContext, Task, View, ViewContext, ViewHandle, Task, View, ViewContext, ViewHandle,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use util::TryFutureExt; use util::TryFutureExt;
use workspace::Workspace;
use crate::render_icon_button;
actions!(contact_finder, [Toggle]);
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
Picker::<ContactFinder>::init(cx); Picker::<ContactFinder>::init(cx);
cx.add_action(ContactFinder::toggle);
} }
pub struct ContactFinder { pub struct ContactFinder {
@ -117,18 +111,21 @@ impl PickerDelegate for ContactFinder {
let icon_path = match request_status { let icon_path = match request_status {
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
"icons/check_8.svg" Some("icons/check_8.svg")
}
ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => {
"icons/x_mark_8.svg"
} }
ContactRequestStatus::RequestSent => Some("icons/x_mark_8.svg"),
ContactRequestStatus::RequestAccepted => None,
}; };
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) { let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
&theme.contact_finder.disabled_contact_button &theme.contact_finder.disabled_contact_button
} else { } else {
&theme.contact_finder.contact_button &theme.contact_finder.contact_button
}; };
let style = theme.picker.item.style_for(mouse_state, selected); let style = theme
.contact_finder
.picker
.item
.style_for(mouse_state, selected);
Flex::row() Flex::row()
.with_children(user.avatar.clone().map(|avatar| { .with_children(user.avatar.clone().map(|avatar| {
Image::new(avatar) Image::new(avatar)
@ -145,12 +142,21 @@ impl PickerDelegate for ContactFinder {
.left() .left()
.boxed(), .boxed(),
) )
.with_child( .with_children(icon_path.map(|icon_path| {
render_icon_button(button_style, icon_path) Svg::new(icon_path)
.with_color(button_style.color)
.constrained()
.with_width(button_style.icon_width)
.aligned()
.contained()
.with_style(button_style.container)
.constrained()
.with_width(button_style.button_width)
.with_height(button_style.button_width)
.aligned() .aligned()
.flex_float() .flex_float()
.boxed(), .boxed()
) }))
.contained() .contained()
.with_style(style.container) .with_style(style.container)
.constrained() .constrained()
@ -160,34 +166,16 @@ impl PickerDelegate for ContactFinder {
} }
impl ContactFinder { impl ContactFinder {
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx));
cx.subscribe(&finder, Self::on_event).detach();
finder
});
}
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self { pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let this = cx.weak_handle(); let this = cx.weak_handle();
Self { Self {
picker: cx.add_view(|cx| Picker::new(this, cx)), picker: cx.add_view(|cx| {
Picker::new(this, cx)
.with_theme(|cx| &cx.global::<Settings>().theme.contact_finder.picker)
}),
potential_contacts: Arc::from([]), potential_contacts: Arc::from([]),
user_store, user_store,
selected_index: 0, selected_index: 0,
} }
} }
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<Self>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Dismissed => {
workspace.dismiss_modal(cx);
}
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -49,10 +49,7 @@ impl View for ContactNotification {
self.user.clone(), self.user.clone(),
"wants to add you as a contact", "wants to add you as a contact",
Some("They won't know if you decline."), Some("They won't know if you decline."),
RespondToContactRequest { Dismiss(self.user.id),
user_id: self.user.id,
accept: false,
},
vec![ vec![
( (
"Decline", "Decline",

View 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),
}
}
}
}

View 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()
}
}

View File

@ -1,9 +1,7 @@
use crate::render_icon_button;
use client::User; use client::User;
use gpui::{ use gpui::{
elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text}, elements::*, platform::CursorStyle, Action, Element, ElementBox, MouseButton, RenderContext,
platform::CursorStyle, View,
Action, Element, ElementBox, MouseButton, RenderContext, View,
}; };
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
@ -53,11 +51,18 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
) )
.with_child( .with_child(
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| { MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
render_icon_button( let style = theme.dismiss_button.style_for(state, false);
theme.dismiss_button.style_for(state, false), Svg::new("icons/x_mark_thin_8.svg")
"icons/x_mark_thin_8.svg", .with_color(style.color)
) .constrained()
.boxed() .with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.boxed()
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(5.)) .with_padding(Padding::uniform(5.))

View 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()
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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"] }

View File

@ -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);
}
}
}

View File

@ -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());
}
}
}
}

View File

@ -1731,7 +1731,8 @@ impl Element for EditorElement {
layout: &mut Self::LayoutState, layout: &mut Self::LayoutState,
cx: &mut PaintContext, cx: &mut PaintContext,
) -> Self::PaintState { ) -> Self::PaintState {
cx.scene.push_layer(Some(bounds)); let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
cx.scene.push_layer(Some(visible_bounds));
let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size); let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size);
let text_bounds = RectF::new( let text_bounds = RectF::new(

View File

@ -786,6 +786,24 @@ impl AsyncAppContext {
self.update(|cx| cx.add_window(window_options, build_root_view)) self.update(|cx| cx.add_window(window_options, build_root_view))
} }
pub fn remove_window(&mut self, window_id: usize) {
self.update(|cx| cx.remove_window(window_id))
}
pub fn activate_window(&mut self, window_id: usize) {
self.update(|cx| cx.activate_window(window_id))
}
pub fn prompt(
&mut self,
window_id: usize,
level: PromptLevel,
msg: &str,
answers: &[&str],
) -> oneshot::Receiver<usize> {
self.update(|cx| cx.prompt(window_id, level, msg, answers))
}
pub fn platform(&self) -> Arc<dyn Platform> { pub fn platform(&self) -> Arc<dyn Platform> {
self.0.borrow().platform() self.0.borrow().platform()
} }
@ -1519,6 +1537,17 @@ impl MutableAppContext {
} }
} }
pub fn observe_default_global<G, F>(&mut self, observe: F) -> Subscription
where
G: Any + Default,
F: 'static + FnMut(&mut MutableAppContext),
{
if !self.has_global::<G>() {
self.set_global(G::default());
}
self.observe_global::<G, F>(observe)
}
pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription
where where
E: Entity, E: Entity,
@ -1887,6 +1916,10 @@ impl MutableAppContext {
}) })
} }
pub fn clear_globals(&mut self) {
self.cx.globals.clear();
}
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T> pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
where where
T: Entity, T: Entity,
@ -1967,6 +2000,10 @@ impl MutableAppContext {
}) })
} }
pub fn remove_status_bar_item(&mut self, id: usize) {
self.remove_window(id);
}
fn register_platform_window( fn register_platform_window(
&mut self, &mut self,
window_id: usize, window_id: usize,
@ -4650,6 +4687,12 @@ impl<T> PartialEq for WeakModelHandle<T> {
impl<T> Eq for WeakModelHandle<T> {} impl<T> Eq for WeakModelHandle<T> {}
impl<T: Entity> PartialEq<ModelHandle<T>> for WeakModelHandle<T> {
fn eq(&self, other: &ModelHandle<T>) -> bool {
self.model_id == other.model_id
}
}
impl<T> Clone for WeakModelHandle<T> { impl<T> Clone for WeakModelHandle<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {

View File

@ -271,9 +271,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
mut layout, mut layout,
} => { } => {
let bounds = RectF::new(origin, size); let bounds = RectF::new(origin, size);
let visible_bounds = visible_bounds
.intersection(bounds)
.unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
let paint = element.paint(bounds, visible_bounds, &mut layout, cx); let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
Lifecycle::PostPaint { Lifecycle::PostPaint {
element, element,
@ -292,9 +289,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
.. ..
} => { } => {
let bounds = RectF::new(origin, bounds.size()); let bounds = RectF::new(origin, bounds.size());
let visible_bounds = visible_bounds
.intersection(bounds)
.unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
let paint = element.paint(bounds, visible_bounds, &mut layout, cx); let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
Lifecycle::PostPaint { Lifecycle::PostPaint {
element, element,

View File

@ -241,11 +241,12 @@ impl Element for Flex {
remaining_space: &mut Self::LayoutState, remaining_space: &mut Self::LayoutState,
cx: &mut PaintContext, cx: &mut PaintContext,
) -> Self::PaintState { ) -> Self::PaintState {
let mut remaining_space = *remaining_space; let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
let mut remaining_space = *remaining_space;
let overflowing = remaining_space < 0.; let overflowing = remaining_space < 0.;
if overflowing { if overflowing {
cx.scene.push_layer(Some(bounds)); cx.scene.push_layer(Some(visible_bounds));
} }
if let Some(scroll_state) = &self.scroll_state { if let Some(scroll_state) = &self.scroll_state {

View File

@ -27,6 +27,8 @@ pub struct ImageStyle {
pub height: Option<f32>, pub height: Option<f32>,
#[serde(default)] #[serde(default)]
pub width: Option<f32>, pub width: Option<f32>,
#[serde(default)]
pub grayscale: bool,
} }
impl Image { impl Image {
@ -74,6 +76,7 @@ impl Element for Image {
bounds, bounds,
border: self.style.border, border: self.style.border,
corner_radius: self.style.corner_radius, corner_radius: self.style.corner_radius,
grayscale: self.style.grayscale,
data: self.data.clone(), data: self.data.clone(),
}); });
} }

View File

@ -261,7 +261,8 @@ impl Element for List {
scroll_top: &mut ListOffset, scroll_top: &mut ListOffset,
cx: &mut PaintContext, cx: &mut PaintContext,
) { ) {
cx.scene.push_layer(Some(bounds)); let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
cx.scene.push_layer(Some(visible_bounds));
cx.scene cx.scene
.push_mouse_region(MouseRegion::new::<Self>(10, 0, bounds).on_scroll({ .push_mouse_region(MouseRegion::new::<Self>(10, 0, bounds).on_scroll({

View File

@ -169,6 +169,7 @@ impl<Tag> Element for MouseEventHandler<Tag> {
_: &mut Self::LayoutState, _: &mut Self::LayoutState,
cx: &mut PaintContext, cx: &mut PaintContext,
) -> Self::PaintState { ) -> Self::PaintState {
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
let hit_bounds = self.hit_bounds(visible_bounds); let hit_bounds = self.hit_bounds(visible_bounds);
if let Some(style) = self.cursor_style { if let Some(style) = self.cursor_style {
cx.scene.push_cursor_region(CursorRegion { cx.scene.push_cursor_region(CursorRegion {

View File

@ -217,7 +217,11 @@ impl Element for Overlay {
)); ));
} }
self.child.paint(bounds.origin(), bounds, cx); self.child.paint(
bounds.origin(),
RectF::new(Vector2F::zero(), cx.window_size),
cx,
);
cx.scene.pop_stacking_context(); cx.scene.pop_stacking_context();
} }

View File

@ -284,7 +284,9 @@ impl Element for UniformList {
layout: &mut Self::LayoutState, layout: &mut Self::LayoutState,
cx: &mut PaintContext, cx: &mut PaintContext,
) -> Self::PaintState { ) -> Self::PaintState {
cx.scene.push_layer(Some(bounds)); let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
cx.scene.push_layer(Some(visible_bounds));
cx.scene.push_mouse_region( cx.scene.push_mouse_region(
MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({ MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({

View File

@ -44,6 +44,8 @@ pub trait Platform: Send + Sync {
fn unhide_other_apps(&self); fn unhide_other_apps(&self);
fn quit(&self); fn quit(&self);
fn screen_size(&self) -> Vector2F;
fn open_window( fn open_window(
&self, &self,
id: usize, id: usize,

View File

@ -2,7 +2,9 @@ use super::{
event::key_to_native, status_item::StatusItem, BoolExt as _, Dispatcher, FontSystem, Window, event::key_to_native, status_item::StatusItem, BoolExt as _, Dispatcher, FontSystem, Window,
}; };
use crate::{ use crate::{
executor, keymap, executor,
geometry::vector::{vec2f, Vector2F},
keymap,
platform::{self, CursorStyle}, platform::{self, CursorStyle},
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem, Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
}; };
@ -12,7 +14,7 @@ use cocoa::{
appkit::{ appkit::{
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
NSPasteboardTypeString, NSSavePanel, NSWindow, NSPasteboardTypeString, NSSavePanel, NSScreen, NSWindow,
}, },
base::{id, nil, selector, YES}, base::{id, nil, selector, YES},
foundation::{ foundation::{
@ -486,6 +488,14 @@ impl platform::Platform for MacPlatform {
} }
} }
fn screen_size(&self) -> Vector2F {
unsafe {
let screen = NSScreen::mainScreen(nil);
let frame = NSScreen::frame(screen);
vec2f(frame.size.width as f32, frame.size.height as f32)
}
}
fn open_window( fn open_window(
&self, &self,
id: usize, id: usize,

View File

@ -747,6 +747,7 @@ impl Renderer {
border_left: border_width * (image.border.left as usize as f32), border_left: border_width * (image.border.left as usize as f32),
border_color: image.border.color.to_uchar4(), border_color: image.border.color.to_uchar4(),
corner_radius, corner_radius,
grayscale: image.grayscale as u8,
}); });
} }
@ -769,6 +770,7 @@ impl Renderer {
border_left: 0., border_left: 0.,
border_color: Default::default(), border_color: Default::default(),
corner_radius: 0., corner_radius: 0.,
grayscale: false as u8,
}); });
} else { } else {
log::warn!("could not render glyph with id {}", image_glyph.id); log::warn!("could not render glyph with id {}", image_glyph.id);

View File

@ -90,6 +90,7 @@ typedef struct {
float border_left; float border_left;
vector_uchar4 border_color; vector_uchar4 border_color;
float corner_radius; float corner_radius;
uint8_t grayscale;
} GPUIImage; } GPUIImage;
typedef enum { typedef enum {

View File

@ -44,6 +44,7 @@ struct QuadFragmentInput {
float border_left; float border_left;
float4 border_color; float4 border_color;
float corner_radius; float corner_radius;
uchar grayscale; // only used in image shader
}; };
float4 quad_sdf(QuadFragmentInput input) { float4 quad_sdf(QuadFragmentInput input) {
@ -110,6 +111,7 @@ vertex QuadFragmentInput quad_vertex(
quad.border_left, quad.border_left,
coloru_to_colorf(quad.border_color), coloru_to_colorf(quad.border_color),
quad.corner_radius, quad.corner_radius,
0,
}; };
} }
@ -251,6 +253,7 @@ vertex QuadFragmentInput image_vertex(
image.border_left, image.border_left,
coloru_to_colorf(image.border_color), coloru_to_colorf(image.border_color),
image.corner_radius, image.corner_radius,
image.grayscale,
}; };
} }
@ -260,6 +263,13 @@ fragment float4 image_fragment(
) { ) {
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear); constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
input.background_color = atlas.sample(atlas_sampler, input.atlas_position); input.background_color = atlas.sample(atlas_sampler, input.atlas_position);
if (input.grayscale) {
float grayscale =
0.2126 * input.background_color.r +
0.7152 * input.background_color.g +
0.0722 * input.background_color.b;
input.background_color = float4(grayscale, grayscale, grayscale, input.background_color.a);
}
return quad_sdf(input); return quad_sdf(input);
} }
@ -289,6 +299,7 @@ vertex QuadFragmentInput surface_vertex(
0., 0.,
float4(0.), float4(0.),
0., 0.,
0,
}; };
} }

View File

@ -131,6 +131,10 @@ impl super::Platform for Platform {
fn quit(&self) {} fn quit(&self) {}
fn screen_size(&self) -> Vector2F {
vec2f(1024., 768.)
}
fn open_window( fn open_window(
&self, &self,
_: usize, _: usize,

View File

@ -172,6 +172,7 @@ pub struct Image {
pub bounds: RectF, pub bounds: RectF,
pub border: Border, pub border: Border,
pub corner_radius: f32, pub corner_radius: f32,
pub grayscale: bool,
pub data: Arc<ImageData>, pub data: Arc<ImageData>,
} }

View File

@ -91,7 +91,7 @@ pub fn run_test(
cx.update(|cx| cx.remove_all_windows()); cx.update(|cx| cx.remove_all_windows());
deterministic.run_until_parked(); deterministic.run_until_parked();
cx.update(|_| {}); // flush effects cx.update(|cx| cx.clear_globals());
leak_detector.lock().detect(); leak_detector.lock().detect();
if is_last_iteration { if is_last_iteration {

View File

@ -122,7 +122,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
cx_teardowns.extend(quote!( cx_teardowns.extend(quote!(
#cx_varname.update(|cx| cx.remove_all_windows()); #cx_varname.update(|cx| cx.remove_all_windows());
deterministic.run_until_parked(); deterministic.run_until_parked();
#cx_varname.update(|_| {}); // flush effects #cx_varname.update(|cx| cx.clear_globals());
)); ));
inner_fn_args.extend(quote!(&mut #cx_varname,)); inner_fn_args.extend(quote!(&mut #cx_varname,));
continue; continue;

View File

@ -19,6 +19,7 @@ pub struct Picker<D: PickerDelegate> {
query_editor: ViewHandle<Editor>, query_editor: ViewHandle<Editor>,
list_state: UniformListState, list_state: UniformListState,
max_size: Vector2F, max_size: Vector2F,
theme: Box<dyn FnMut(&AppContext) -> &theme::Picker>,
confirmed: bool, confirmed: bool,
} }
@ -51,8 +52,8 @@ impl<D: PickerDelegate> View for Picker<D> {
} }
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox { fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
let settings = cx.global::<Settings>(); let theme = (self.theme)(cx);
let container_style = settings.theme.picker.container; let container_style = theme.container;
let delegate = self.delegate.clone(); let delegate = self.delegate.clone();
let match_count = if let Some(delegate) = delegate.upgrade(cx.app) { let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
delegate.read(cx).match_count() delegate.read(cx).match_count()
@ -64,17 +65,14 @@ impl<D: PickerDelegate> View for Picker<D> {
.with_child( .with_child(
ChildView::new(&self.query_editor) ChildView::new(&self.query_editor)
.contained() .contained()
.with_style(settings.theme.picker.input_editor.container) .with_style(theme.input_editor.container)
.boxed(), .boxed(),
) )
.with_child( .with_child(
if match_count == 0 { if match_count == 0 {
Label::new( Label::new("No matches".into(), theme.empty.label.clone())
"No matches".into(), .contained()
settings.theme.picker.empty.label.clone(), .with_style(theme.empty.container)
)
.contained()
.with_style(settings.theme.picker.empty.container)
} else { } else {
UniformList::new( UniformList::new(
self.list_state.clone(), self.list_state.clone(),
@ -147,6 +145,7 @@ impl<D: PickerDelegate> Picker<D> {
list_state: Default::default(), list_state: Default::default(),
delegate, delegate,
max_size: vec2f(540., 420.), max_size: vec2f(540., 420.),
theme: Box::new(|cx| &cx.global::<Settings>().theme.picker),
confirmed: false, confirmed: false,
}; };
cx.defer(|this, cx| { cx.defer(|this, cx| {
@ -163,6 +162,14 @@ impl<D: PickerDelegate> Picker<D> {
self self
} }
pub fn with_theme<F>(mut self, theme: F) -> Self
where
F: 'static + FnMut(&AppContext) -> &theme::Picker,
{
self.theme = Box::new(theme);
self
}
pub fn query(&self, cx: &AppContext) -> String { pub fn query(&self, cx: &AppContext) -> String {
self.query_editor.read(cx).text(cx) self.query_editor.read(cx).text(cx)
} }

View File

@ -8,7 +8,7 @@ pub mod worktree;
mod project_tests; mod project_tests;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet}; use collections::{hash_map, BTreeMap, HashMap, HashSet};
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt}; use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
@ -35,7 +35,6 @@ use lsp::{
}; };
use lsp_command::*; use lsp_command::*;
use parking_lot::Mutex; use parking_lot::Mutex;
use postage::stream::Stream;
use postage::watch; use postage::watch;
use rand::prelude::*; use rand::prelude::*;
use search::SearchQuery; use search::SearchQuery;
@ -74,7 +73,6 @@ pub trait Item: Entity {
} }
pub struct ProjectStore { pub struct ProjectStore {
db: Arc<Db>,
projects: Vec<WeakModelHandle<Project>>, projects: Vec<WeakModelHandle<Project>>,
} }
@ -127,7 +125,6 @@ pub struct Project {
buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>, buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>,
buffers_being_formatted: HashSet<usize>, buffers_being_formatted: HashSet<usize>,
nonce: u128, nonce: u128,
initialized_persistent_state: bool,
_maintain_buffer_languages: Task<()>, _maintain_buffer_languages: Task<()>,
} }
@ -156,13 +153,8 @@ enum WorktreeHandle {
enum ProjectClientState { enum ProjectClientState {
Local { Local {
is_shared: bool, remote_id: Option<u64>,
remote_id_tx: watch::Sender<Option<u64>>, _detect_unshare: Task<Option<()>>,
remote_id_rx: watch::Receiver<Option<u64>>,
online_tx: watch::Sender<bool>,
online_rx: watch::Receiver<bool>,
_maintain_remote_id: Task<Option<()>>,
_maintain_online_status: Task<Option<()>>,
}, },
Remote { Remote {
sharing_has_stopped: bool, sharing_has_stopped: bool,
@ -174,7 +166,6 @@ enum ProjectClientState {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Collaborator { pub struct Collaborator {
pub user: Arc<User>,
pub peer_id: PeerId, pub peer_id: PeerId,
pub replica_id: ReplicaId, pub replica_id: ReplicaId,
} }
@ -197,8 +188,6 @@ pub enum Event {
RemoteIdChanged(Option<u64>), RemoteIdChanged(Option<u64>),
DisconnectedFromHost, DisconnectedFromHost,
CollaboratorLeft(PeerId), CollaboratorLeft(PeerId),
ContactRequestedJoin(Arc<User>),
ContactCancelledJoinRequest(Arc<User>),
} }
pub enum LanguageServerState { pub enum LanguageServerState {
@ -383,17 +372,14 @@ impl FormatTrigger {
impl Project { impl Project {
pub fn init(client: &Arc<Client>) { pub fn init(client: &Arc<Client>) {
client.add_model_message_handler(Self::handle_request_join_project);
client.add_model_message_handler(Self::handle_add_collaborator); client.add_model_message_handler(Self::handle_add_collaborator);
client.add_model_message_handler(Self::handle_buffer_reloaded); client.add_model_message_handler(Self::handle_buffer_reloaded);
client.add_model_message_handler(Self::handle_buffer_saved); client.add_model_message_handler(Self::handle_buffer_saved);
client.add_model_message_handler(Self::handle_start_language_server); client.add_model_message_handler(Self::handle_start_language_server);
client.add_model_message_handler(Self::handle_update_language_server); client.add_model_message_handler(Self::handle_update_language_server);
client.add_model_message_handler(Self::handle_remove_collaborator); client.add_model_message_handler(Self::handle_remove_collaborator);
client.add_model_message_handler(Self::handle_join_project_request_cancelled);
client.add_model_message_handler(Self::handle_update_project); client.add_model_message_handler(Self::handle_update_project);
client.add_model_message_handler(Self::handle_unregister_project); client.add_model_message_handler(Self::handle_unshare_project);
client.add_model_message_handler(Self::handle_project_unshared);
client.add_model_message_handler(Self::handle_create_buffer_for_peer); client.add_model_message_handler(Self::handle_create_buffer_for_peer);
client.add_model_message_handler(Self::handle_update_buffer_file); client.add_model_message_handler(Self::handle_update_buffer_file);
client.add_model_message_handler(Self::handle_update_buffer); client.add_model_message_handler(Self::handle_update_buffer);
@ -426,7 +412,6 @@ impl Project {
} }
pub fn local( pub fn local(
online: bool,
client: Arc<Client>, client: Arc<Client>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
project_store: ModelHandle<ProjectStore>, project_store: ModelHandle<ProjectStore>,
@ -435,41 +420,19 @@ impl Project {
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> ModelHandle<Self> { ) -> ModelHandle<Self> {
cx.add_model(|cx: &mut ModelContext<Self>| { cx.add_model(|cx: &mut ModelContext<Self>| {
let (remote_id_tx, remote_id_rx) = watch::channel(); let mut status = client.status();
let _maintain_remote_id = cx.spawn_weak({ let _detect_unshare = cx.spawn_weak(move |this, mut cx| {
let mut status_rx = client.clone().status(); async move {
move |this, mut cx| async move { let is_connected = status.next().await.map_or(false, |s| s.is_connected());
while let Some(status) = status_rx.recv().await { // Even if we're initially connected, any future change of the status means we momentarily disconnected.
let this = this.upgrade(&cx)?; if !is_connected || status.next().await.is_some() {
if status.is_connected() { if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.register(cx)) let _ = this.update(&mut cx, |this, cx| this.unshare(cx));
.await
.log_err()?;
} else {
this.update(&mut cx, |this, cx| this.unregister(cx))
.await
.log_err();
} }
} }
None Ok(())
}
});
let (online_tx, online_rx) = watch::channel_with(online);
let _maintain_online_status = cx.spawn_weak({
let mut online_rx = online_rx.clone();
move |this, mut cx| async move {
while let Some(online) = online_rx.recv().await {
let this = this.upgrade(&cx)?;
this.update(&mut cx, |this, cx| {
if !online {
this.unshared(cx);
}
this.metadata_changed(false, cx)
});
}
None
} }
.log_err()
}); });
let handle = cx.weak_handle(); let handle = cx.weak_handle();
@ -485,13 +448,8 @@ impl Project {
loading_local_worktrees: Default::default(), loading_local_worktrees: Default::default(),
buffer_snapshots: Default::default(), buffer_snapshots: Default::default(),
client_state: ProjectClientState::Local { client_state: ProjectClientState::Local {
is_shared: false, remote_id: None,
remote_id_tx, _detect_unshare,
remote_id_rx,
online_tx,
online_rx,
_maintain_remote_id,
_maintain_online_status,
}, },
opened_buffer: watch::channel(), opened_buffer: watch::channel(),
client_subscriptions: Vec::new(), client_subscriptions: Vec::new(),
@ -513,7 +471,6 @@ impl Project {
buffers_being_formatted: Default::default(), buffers_being_formatted: Default::default(),
next_language_server_id: 0, next_language_server_id: 0,
nonce: StdRng::from_entropy().gen(), nonce: StdRng::from_entropy().gen(),
initialized_persistent_state: false,
} }
}) })
} }
@ -535,24 +492,6 @@ impl Project {
}) })
.await?; .await?;
let response = match response.variant.ok_or_else(|| anyhow!("missing variant"))? {
proto::join_project_response::Variant::Accept(response) => response,
proto::join_project_response::Variant::Decline(decline) => {
match proto::join_project_response::decline::Reason::from_i32(decline.reason) {
Some(proto::join_project_response::decline::Reason::Declined) => {
Err(JoinProjectError::HostDeclined)?
}
Some(proto::join_project_response::decline::Reason::Closed) => {
Err(JoinProjectError::HostClosedProject)?
}
Some(proto::join_project_response::decline::Reason::WentOffline) => {
Err(JoinProjectError::HostWentOffline)?
}
None => Err(anyhow!("missing decline reason"))?,
}
}
};
let replica_id = response.replica_id as ReplicaId; let replica_id = response.replica_id as ReplicaId;
let mut worktrees = Vec::new(); let mut worktrees = Vec::new();
@ -629,7 +568,6 @@ impl Project {
buffers_being_formatted: Default::default(), buffers_being_formatted: Default::default(),
buffer_snapshots: Default::default(), buffer_snapshots: Default::default(),
nonce: StdRng::from_entropy().gen(), nonce: StdRng::from_entropy().gen(),
initialized_persistent_state: false,
}; };
for worktree in worktrees { for worktree in worktrees {
this.add_worktree(&worktree, cx); this.add_worktree(&worktree, cx);
@ -647,7 +585,7 @@ impl Project {
.await?; .await?;
let mut collaborators = HashMap::default(); let mut collaborators = HashMap::default();
for message in response.collaborators { for message in response.collaborators {
let collaborator = Collaborator::from_proto(message, &user_store, &mut cx).await?; let collaborator = Collaborator::from_proto(message);
collaborators.insert(collaborator.peer_id, collaborator); collaborators.insert(collaborator.peer_id, collaborator);
} }
@ -672,10 +610,9 @@ impl Project {
let http_client = client::test::FakeHttpClient::with_404_response(); let http_client = client::test::FakeHttpClient::with_404_response();
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let project_store = cx.add_model(|_| ProjectStore::new(Db::open_fake())); let project_store = cx.add_model(|_| ProjectStore::new());
let project = cx.update(|cx| { let project =
Project::local(true, client, user_store, project_store, languages, fs, cx) cx.update(|cx| Project::local(client, user_store, project_store, languages, fs, cx));
});
for path in root_paths { for path in root_paths {
let (tree, _) = project let (tree, _) = project
.update(cx, |project, cx| { .update(cx, |project, cx| {
@ -689,53 +626,6 @@ impl Project {
project project
} }
pub fn restore_state(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.is_remote() {
return Task::ready(Ok(()));
}
let db = self.project_store.read(cx).db.clone();
let keys = self.db_keys_for_online_state(cx);
let online_by_default = cx.global::<Settings>().projects_online_by_default;
let read_online = cx.background().spawn(async move {
let values = db.read(keys)?;
anyhow::Ok(
values
.into_iter()
.all(|e| e.map_or(online_by_default, |e| e == [true as u8])),
)
});
cx.spawn(|this, mut cx| async move {
let online = read_online.await.log_err().unwrap_or(false);
this.update(&mut cx, |this, cx| {
this.initialized_persistent_state = true;
if let ProjectClientState::Local { online_tx, .. } = &mut this.client_state {
let mut online_tx = online_tx.borrow_mut();
if *online_tx != online {
*online_tx = online;
drop(online_tx);
this.metadata_changed(false, cx);
}
}
});
Ok(())
})
}
fn persist_state(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.is_remote() || !self.initialized_persistent_state {
return Task::ready(Ok(()));
}
let db = self.project_store.read(cx).db.clone();
let keys = self.db_keys_for_online_state(cx);
let is_online = self.is_online();
cx.background().spawn(async move {
let value = &[is_online as u8];
db.write(keys.into_iter().map(|key| (key, value)))
})
}
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) { fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
let settings = cx.global::<Settings>(); let settings = cx.global::<Settings>();
@ -864,136 +754,9 @@ impl Project {
&self.fs &self.fs
} }
pub fn set_online(&mut self, online: bool, _: &mut ModelContext<Self>) {
if let ProjectClientState::Local { online_tx, .. } = &mut self.client_state {
let mut online_tx = online_tx.borrow_mut();
if *online_tx != online {
*online_tx = online;
}
}
}
pub fn is_online(&self) -> bool {
match &self.client_state {
ProjectClientState::Local { online_rx, .. } => *online_rx.borrow(),
ProjectClientState::Remote { .. } => true,
}
}
fn unregister(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
self.unshared(cx);
if let ProjectClientState::Local { remote_id_rx, .. } = &mut self.client_state {
if let Some(remote_id) = *remote_id_rx.borrow() {
let request = self.client.request(proto::UnregisterProject {
project_id: remote_id,
});
return cx.spawn(|this, mut cx| async move {
let response = request.await;
// Unregistering the project causes the server to send out a
// contact update removing this project from the host's list
// of online projects. Wait until this contact update has been
// processed before clearing out this project's remote id, so
// that there is no moment where this project appears in the
// contact metadata and *also* has no remote id.
this.update(&mut cx, |this, cx| {
this.user_store()
.update(cx, |store, _| store.contact_updates_done())
})
.await;
this.update(&mut cx, |this, cx| {
if let ProjectClientState::Local { remote_id_tx, .. } =
&mut this.client_state
{
*remote_id_tx.borrow_mut() = None;
}
this.client_subscriptions.clear();
this.metadata_changed(false, cx);
});
response.map(drop)
});
}
}
Task::ready(Ok(()))
}
fn register(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if let ProjectClientState::Local {
remote_id_rx,
online_rx,
..
} = &self.client_state
{
if remote_id_rx.borrow().is_some() {
return Task::ready(Ok(()));
}
let response = self.client.request(proto::RegisterProject {
online: *online_rx.borrow(),
});
cx.spawn(|this, mut cx| async move {
let remote_id = response.await?.project_id;
this.update(&mut cx, |this, cx| {
if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state {
*remote_id_tx.borrow_mut() = Some(remote_id);
}
this.metadata_changed(false, cx);
cx.emit(Event::RemoteIdChanged(Some(remote_id)));
this.client_subscriptions
.push(this.client.add_model_for_remote_entity(remote_id, cx));
Ok(())
})
})
} else {
Task::ready(Err(anyhow!("can't register a remote project")))
}
}
pub fn remote_id(&self) -> Option<u64> { pub fn remote_id(&self) -> Option<u64> {
match &self.client_state { match &self.client_state {
ProjectClientState::Local { remote_id_rx, .. } => *remote_id_rx.borrow(), ProjectClientState::Local { remote_id, .. } => *remote_id,
ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
}
}
pub fn next_remote_id(&self) -> impl Future<Output = u64> {
let mut id = None;
let mut watch = None;
match &self.client_state {
ProjectClientState::Local { remote_id_rx, .. } => watch = Some(remote_id_rx.clone()),
ProjectClientState::Remote { remote_id, .. } => id = Some(*remote_id),
}
async move {
if let Some(id) = id {
return id;
}
let mut watch = watch.unwrap();
loop {
let id = *watch.borrow();
if let Some(id) = id {
return id;
}
watch.next().await;
}
}
}
pub fn shared_remote_id(&self) -> Option<u64> {
match &self.client_state {
ProjectClientState::Local {
remote_id_rx,
is_shared,
..
} => {
if *is_shared {
*remote_id_rx.borrow()
} else {
None
}
}
ProjectClientState::Remote { remote_id, .. } => Some(*remote_id), ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
} }
} }
@ -1005,65 +768,50 @@ impl Project {
} }
} }
fn metadata_changed(&mut self, persist: bool, cx: &mut ModelContext<Self>) { fn metadata_changed(&mut self, cx: &mut ModelContext<Self>) {
if let ProjectClientState::Local { if let ProjectClientState::Local { remote_id, .. } = &self.client_state {
remote_id_rx,
online_rx,
..
} = &self.client_state
{
// Broadcast worktrees only if the project is online. // Broadcast worktrees only if the project is online.
let worktrees = if *online_rx.borrow() { let worktrees = self
self.worktrees .worktrees
.iter() .iter()
.filter_map(|worktree| { .filter_map(|worktree| {
worktree worktree
.upgrade(cx) .upgrade(cx)
.map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto()) .map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto())
}) })
.collect() .collect();
} else { if let Some(project_id) = *remote_id {
Default::default()
};
if let Some(project_id) = *remote_id_rx.borrow() {
let online = *online_rx.borrow();
self.client self.client
.send(proto::UpdateProject { .send(proto::UpdateProject {
project_id, project_id,
worktrees, worktrees,
online,
}) })
.log_err(); .log_err();
if online { let worktrees = self.visible_worktrees(cx).collect::<Vec<_>>();
let worktrees = self.visible_worktrees(cx).collect::<Vec<_>>(); let scans_complete =
let scans_complete = futures::future::join_all(worktrees.iter().filter_map(|worktree| {
futures::future::join_all(worktrees.iter().filter_map(|worktree| { Some(worktree.read(cx).as_local()?.scan_complete())
Some(worktree.read(cx).as_local()?.scan_complete()) }));
}));
let worktrees = worktrees.into_iter().map(|handle| handle.downgrade()); let worktrees = worktrees.into_iter().map(|handle| handle.downgrade());
cx.spawn_weak(move |_, cx| async move { cx.spawn_weak(move |_, cx| async move {
scans_complete.await; scans_complete.await;
cx.read(|cx| { cx.read(|cx| {
for worktree in worktrees { for worktree in worktrees {
if let Some(worktree) = worktree if let Some(worktree) = worktree
.upgrade(cx) .upgrade(cx)
.and_then(|worktree| worktree.read(cx).as_local()) .and_then(|worktree| worktree.read(cx).as_local())
{ {
worktree.send_extension_counts(project_id); worktree.send_extension_counts(project_id);
}
} }
}) }
}) })
.detach(); })
} .detach();
} }
self.project_store.update(cx, |_, cx| cx.notify()); self.project_store.update(cx, |_, cx| cx.notify());
if persist {
self.persist_state(cx).detach_and_log_err(cx);
}
cx.notify(); cx.notify();
} }
} }
@ -1101,23 +849,6 @@ impl Project {
.map(|tree| tree.read(cx).root_name()) .map(|tree| tree.read(cx).root_name())
} }
fn db_keys_for_online_state(&self, cx: &AppContext) -> Vec<String> {
self.worktrees
.iter()
.filter_map(|worktree| {
let worktree = worktree.upgrade(cx)?.read(cx);
if worktree.is_visible() {
Some(format!(
"project-path-online:{}",
worktree.as_local().unwrap().abs_path().to_string_lossy()
))
} else {
None
}
})
.collect::<Vec<_>>()
}
pub fn worktree_for_id( pub fn worktree_for_id(
&self, &self,
id: WorktreeId, id: WorktreeId,
@ -1321,142 +1052,106 @@ impl Project {
} }
} }
fn share(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> { pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if !self.is_online() { if let ProjectClientState::Local { remote_id, .. } = &mut self.client_state {
return Task::ready(Err(anyhow!("can't share an offline project"))); if remote_id.is_some() {
} return Task::ready(Err(anyhow!("project was already shared")));
let project_id;
if let ProjectClientState::Local {
remote_id_rx,
is_shared,
..
} = &mut self.client_state
{
if *is_shared {
return Task::ready(Ok(()));
}
*is_shared = true;
if let Some(id) = *remote_id_rx.borrow() {
project_id = id;
} else {
return Task::ready(Err(anyhow!("project hasn't been registered")));
}
} else {
return Task::ready(Err(anyhow!("can't share a remote project")));
};
for open_buffer in self.opened_buffers.values_mut() {
match open_buffer {
OpenBuffer::Strong(_) => {}
OpenBuffer::Weak(buffer) => {
if let Some(buffer) = buffer.upgrade(cx) {
*open_buffer = OpenBuffer::Strong(buffer);
}
}
OpenBuffer::Operations(_) => unreachable!(),
}
}
for worktree_handle in self.worktrees.iter_mut() {
match worktree_handle {
WorktreeHandle::Strong(_) => {}
WorktreeHandle::Weak(worktree) => {
if let Some(worktree) = worktree.upgrade(cx) {
*worktree_handle = WorktreeHandle::Strong(worktree);
}
}
}
}
let mut tasks = Vec::new();
for worktree in self.worktrees(cx).collect::<Vec<_>>() {
worktree.update(cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
tasks.push(worktree.share(project_id, cx));
});
}
for (server_id, status) in &self.language_server_statuses {
self.client
.send(proto::StartLanguageServer {
project_id,
server: Some(proto::LanguageServer {
id: *server_id as u64,
name: status.name.clone(),
}),
})
.log_err();
}
cx.spawn(|this, mut cx| async move {
for task in tasks {
task.await?;
}
this.update(&mut cx, |_, cx| cx.notify());
Ok(())
})
}
fn unshared(&mut self, cx: &mut ModelContext<Self>) {
if let ProjectClientState::Local { is_shared, .. } = &mut self.client_state {
if !*is_shared {
return;
} }
*is_shared = false; *remote_id = Some(project_id);
self.collaborators.clear();
self.shared_buffers.clear(); let mut worktree_share_tasks = Vec::new();
for worktree_handle in self.worktrees.iter_mut() {
if let WorktreeHandle::Strong(worktree) = worktree_handle {
let is_visible = worktree.update(cx, |worktree, _| {
worktree.as_local_mut().unwrap().unshare();
worktree.is_visible()
});
if !is_visible {
*worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
}
}
}
for open_buffer in self.opened_buffers.values_mut() { for open_buffer in self.opened_buffers.values_mut() {
if let OpenBuffer::Strong(buffer) = open_buffer { match open_buffer {
*open_buffer = OpenBuffer::Weak(buffer.downgrade()); OpenBuffer::Strong(_) => {}
OpenBuffer::Weak(buffer) => {
if let Some(buffer) = buffer.upgrade(cx) {
*open_buffer = OpenBuffer::Strong(buffer);
}
}
OpenBuffer::Operations(_) => unreachable!(),
} }
} }
for worktree_handle in self.worktrees.iter_mut() {
match worktree_handle {
WorktreeHandle::Strong(_) => {}
WorktreeHandle::Weak(worktree) => {
if let Some(worktree) = worktree.upgrade(cx) {
*worktree_handle = WorktreeHandle::Strong(worktree);
}
}
}
}
for worktree in self.worktrees(cx).collect::<Vec<_>>() {
worktree.update(cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
worktree_share_tasks.push(worktree.share(project_id, cx));
});
}
for (server_id, status) in &self.language_server_statuses {
self.client
.send(proto::StartLanguageServer {
project_id,
server: Some(proto::LanguageServer {
id: *server_id as u64,
name: status.name.clone(),
}),
})
.log_err();
}
self.client_subscriptions
.push(self.client.add_model_for_remote_entity(project_id, cx));
self.metadata_changed(cx);
cx.emit(Event::RemoteIdChanged(Some(project_id)));
cx.notify(); cx.notify();
cx.foreground().spawn(async move {
futures::future::try_join_all(worktree_share_tasks).await?;
Ok(())
})
} else { } else {
log::error!("attempted to unshare a remote project"); Task::ready(Err(anyhow!("can't share a remote project")))
} }
} }
pub fn respond_to_join_request( pub fn unshare(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
&mut self, if let ProjectClientState::Local { remote_id, .. } = &mut self.client_state {
requester_id: u64, if let Some(project_id) = remote_id.take() {
allow: bool, self.collaborators.clear();
cx: &mut ModelContext<Self>, self.shared_buffers.clear();
) { self.client_subscriptions.clear();
if let Some(project_id) = self.remote_id() {
let share = if self.is_online() && allow { for worktree_handle in self.worktrees.iter_mut() {
Some(self.share(cx)) if let WorktreeHandle::Strong(worktree) = worktree_handle {
} else { let is_visible = worktree.update(cx, |worktree, _| {
None worktree.as_local_mut().unwrap().unshare();
}; worktree.is_visible()
let client = self.client.clone(); });
cx.foreground() if !is_visible {
.spawn(async move { *worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
client.send(proto::RespondToJoinProjectRequest { }
requester_id,
project_id,
allow,
})?;
if let Some(share) = share {
share.await?;
} }
anyhow::Ok(()) }
})
.detach_and_log_err(cx); for open_buffer in self.opened_buffers.values_mut() {
if let OpenBuffer::Strong(buffer) = open_buffer {
*open_buffer = OpenBuffer::Weak(buffer.downgrade());
}
}
self.metadata_changed(cx);
cx.notify();
self.client.send(proto::UnshareProject { project_id })?;
}
Ok(())
} else {
Err(anyhow!("attempted to unshare a remote project"))
} }
} }
@ -1930,7 +1625,7 @@ impl Project {
) -> Option<()> { ) -> Option<()> {
match event { match event {
BufferEvent::Operation(operation) => { BufferEvent::Operation(operation) => {
if let Some(project_id) = self.shared_remote_id() { if let Some(project_id) = self.remote_id() {
let request = self.client.request(proto::UpdateBuffer { let request = self.client.request(proto::UpdateBuffer {
project_id, project_id,
buffer_id: buffer.read(cx).remote_id(), buffer_id: buffer.read(cx).remote_id(),
@ -2335,7 +2030,7 @@ impl Project {
) )
.ok(); .ok();
if let Some(project_id) = this.shared_remote_id() { if let Some(project_id) = this.remote_id() {
this.client this.client
.send(proto::StartLanguageServer { .send(proto::StartLanguageServer {
project_id, project_id,
@ -2742,7 +2437,7 @@ impl Project {
language_server_id: usize, language_server_id: usize,
event: proto::update_language_server::Variant, event: proto::update_language_server::Variant,
) { ) {
if let Some(project_id) = self.shared_remote_id() { if let Some(project_id) = self.remote_id() {
self.client self.client
.send(proto::UpdateLanguageServer { .send(proto::UpdateLanguageServer {
project_id, project_id,
@ -4472,7 +4167,7 @@ impl Project {
pub fn is_shared(&self) -> bool { pub fn is_shared(&self) -> bool {
match &self.client_state { match &self.client_state {
ProjectClientState::Local { is_shared, .. } => *is_shared, ProjectClientState::Local { remote_id, .. } => remote_id.is_some(),
ProjectClientState::Remote { .. } => false, ProjectClientState::Remote { .. } => false,
} }
} }
@ -4509,7 +4204,7 @@ impl Project {
let project_id = project.update(&mut cx, |project, cx| { let project_id = project.update(&mut cx, |project, cx| {
project.add_worktree(&worktree, cx); project.add_worktree(&worktree, cx);
project.shared_remote_id() project.remote_id()
}); });
if let Some(project_id) = project_id { if let Some(project_id) = project_id {
@ -4550,7 +4245,7 @@ impl Project {
false false
} }
}); });
self.metadata_changed(true, cx); self.metadata_changed(cx);
cx.notify(); cx.notify();
} }
@ -4578,7 +4273,7 @@ impl Project {
.push(WorktreeHandle::Weak(worktree.downgrade())); .push(WorktreeHandle::Weak(worktree.downgrade()));
} }
self.metadata_changed(true, cx); self.metadata_changed(cx);
cx.observe_release(worktree, |this, worktree, cx| { cx.observe_release(worktree, |this, worktree, cx| {
this.remove_worktree(worktree.id(), cx); this.remove_worktree(worktree.id(), cx);
cx.notify(); cx.notify();
@ -4641,7 +4336,7 @@ impl Project {
renamed_buffers.push((cx.handle(), old_path)); renamed_buffers.push((cx.handle(), old_path));
} }
if let Some(project_id) = self.shared_remote_id() { if let Some(project_id) = self.remote_id() {
self.client self.client
.send(proto::UpdateBufferFile { .send(proto::UpdateBufferFile {
project_id, project_id,
@ -4697,7 +4392,7 @@ impl Project {
Err(_) => return, Err(_) => return,
}; };
let shared_remote_id = self.shared_remote_id(); let remote_id = self.remote_id();
let client = self.client.clone(); let client = self.client.clone();
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
@ -4711,7 +4406,7 @@ impl Project {
buffer.remote_id() buffer.remote_id()
}); });
if let Some(project_id) = shared_remote_id { if let Some(project_id) = remote_id {
client client
.send(proto::UpdateDiffBase { .send(proto::UpdateDiffBase {
project_id, project_id,
@ -4811,47 +4506,20 @@ impl Project {
// RPC message handlers // RPC message handlers
async fn handle_request_join_project( async fn handle_unshare_project(
this: ModelHandle<Self>, this: ModelHandle<Self>,
message: TypedEnvelope<proto::RequestJoinProject>, _: TypedEnvelope<proto::UnshareProject>,
_: Arc<Client>, _: Arc<Client>,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<()> { ) -> Result<()> {
let user_id = message.payload.requester_id; this.update(&mut cx, |this, cx| {
if this.read_with(&cx, |project, _| { if this.is_local() {
project.collaborators.values().any(|c| c.user.id == user_id) this.unshare(cx)?;
}) { } else {
this.update(&mut cx, |this, cx| { this.disconnected_from_host(cx);
this.respond_to_join_request(user_id, true, cx) }
}); Ok(())
} else { })
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
let user = user_store
.update(&mut cx, |store, cx| store.fetch_user(user_id, cx))
.await?;
this.update(&mut cx, |_, cx| cx.emit(Event::ContactRequestedJoin(user)));
}
Ok(())
}
async fn handle_unregister_project(
this: ModelHandle<Self>,
_: TypedEnvelope<proto::UnregisterProject>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| this.disconnected_from_host(cx));
Ok(())
}
async fn handle_project_unshared(
this: ModelHandle<Self>,
_: TypedEnvelope<proto::ProjectUnshared>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| this.unshared(cx));
Ok(())
} }
async fn handle_add_collaborator( async fn handle_add_collaborator(
@ -4860,14 +4528,13 @@ impl Project {
_: Arc<Client>, _: Arc<Client>,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<()> { ) -> Result<()> {
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
let collaborator = envelope let collaborator = envelope
.payload .payload
.collaborator .collaborator
.take() .take()
.ok_or_else(|| anyhow!("empty collaborator"))?; .ok_or_else(|| anyhow!("empty collaborator"))?;
let collaborator = Collaborator::from_proto(collaborator, &user_store, &mut cx).await?; let collaborator = Collaborator::from_proto(collaborator);
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.collaborators this.collaborators
.insert(collaborator.peer_id, collaborator); .insert(collaborator.peer_id, collaborator);
@ -4902,27 +4569,6 @@ impl Project {
}) })
} }
async fn handle_join_project_request_cancelled(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::JoinProjectRequestCancelled>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let user = this
.update(&mut cx, |this, cx| {
this.user_store.update(cx, |user_store, cx| {
user_store.fetch_user(envelope.payload.requester_id, cx)
})
})
.await?;
this.update(&mut cx, |_, cx| {
cx.emit(Event::ContactCancelledJoinRequest(user));
});
Ok(())
}
async fn handle_update_project( async fn handle_update_project(
this: ModelHandle<Self>, this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::UpdateProject>, envelope: TypedEnvelope<proto::UpdateProject>,
@ -4954,7 +4600,7 @@ impl Project {
} }
} }
this.metadata_changed(true, cx); this.metadata_changed(cx);
for (id, _) in old_worktrees_by_id { for (id, _) in old_worktrees_by_id {
cx.emit(Event::WorktreeRemoved(id)); cx.emit(Event::WorktreeRemoved(id));
} }
@ -6182,9 +5828,8 @@ impl Project {
} }
impl ProjectStore { impl ProjectStore {
pub fn new(db: Arc<Db>) -> Self { pub fn new() -> Self {
Self { Self {
db,
projects: Default::default(), projects: Default::default(),
} }
} }
@ -6313,10 +5958,10 @@ impl Entity for Project {
self.project_store.update(cx, ProjectStore::prune_projects); self.project_store.update(cx, ProjectStore::prune_projects);
match &self.client_state { match &self.client_state {
ProjectClientState::Local { remote_id_rx, .. } => { ProjectClientState::Local { remote_id, .. } => {
if let Some(project_id) = *remote_id_rx.borrow() { if let Some(project_id) = *remote_id {
self.client self.client
.send(proto::UnregisterProject { project_id }) .send(proto::UnshareProject { project_id })
.log_err(); .log_err();
} }
} }
@ -6357,21 +6002,10 @@ impl Entity for Project {
} }
impl Collaborator { impl Collaborator {
fn from_proto( fn from_proto(message: proto::Collaborator) -> Self {
message: proto::Collaborator, Self {
user_store: &ModelHandle<UserStore>, peer_id: PeerId(message.peer_id),
cx: &mut AsyncAppContext, replica_id: message.replica_id as ReplicaId,
) -> impl Future<Output = Result<Self>> {
let user = user_store.update(cx, |user_store, cx| {
user_store.fetch_user(message.user_id, cx)
});
async move {
Ok(Self {
peer_id: PeerId(message.peer_id),
user: user.await?,
replica_id: message.replica_id as ReplicaId,
})
} }
} }
} }

View File

@ -10,107 +10,116 @@ message Envelope {
Error error = 5; Error error = 5;
Ping ping = 6; Ping ping = 6;
Test test = 7; Test test = 7;
CreateRoom create_room = 8;
CreateRoomResponse create_room_response = 9;
JoinRoom join_room = 10;
JoinRoomResponse join_room_response = 11;
LeaveRoom leave_room = 12;
Call call = 13;
IncomingCall incoming_call = 14;
CallCanceled call_canceled = 15;
CancelCall cancel_call = 16;
DeclineCall decline_call = 17;
UpdateParticipantLocation update_participant_location = 18;
RoomUpdated room_updated = 19;
RegisterProject register_project = 8; ShareProject share_project = 20;
RegisterProjectResponse register_project_response = 9; ShareProjectResponse share_project_response = 21;
UnregisterProject unregister_project = 10; UnshareProject unshare_project = 22;
RequestJoinProject request_join_project = 11; JoinProject join_project = 23;
RespondToJoinProjectRequest respond_to_join_project_request = 12; JoinProjectResponse join_project_response = 24;
JoinProjectRequestCancelled join_project_request_cancelled = 13; LeaveProject leave_project = 25;
JoinProject join_project = 14; AddProjectCollaborator add_project_collaborator = 26;
JoinProjectResponse join_project_response = 15; RemoveProjectCollaborator remove_project_collaborator = 27;
LeaveProject leave_project = 16;
AddProjectCollaborator add_project_collaborator = 17;
RemoveProjectCollaborator remove_project_collaborator = 18;
ProjectUnshared project_unshared = 19;
GetDefinition get_definition = 20; GetDefinition get_definition = 28;
GetDefinitionResponse get_definition_response = 21; GetDefinitionResponse get_definition_response = 29;
GetTypeDefinition get_type_definition = 22; GetTypeDefinition get_type_definition = 30;
GetTypeDefinitionResponse get_type_definition_response = 23; GetTypeDefinitionResponse get_type_definition_response = 31;
GetReferences get_references = 24; GetReferences get_references = 32;
GetReferencesResponse get_references_response = 25; GetReferencesResponse get_references_response = 33;
GetDocumentHighlights get_document_highlights = 26; GetDocumentHighlights get_document_highlights = 34;
GetDocumentHighlightsResponse get_document_highlights_response = 27; GetDocumentHighlightsResponse get_document_highlights_response = 35;
GetProjectSymbols get_project_symbols = 28; GetProjectSymbols get_project_symbols = 36;
GetProjectSymbolsResponse get_project_symbols_response = 29; GetProjectSymbolsResponse get_project_symbols_response = 37;
OpenBufferForSymbol open_buffer_for_symbol = 30; OpenBufferForSymbol open_buffer_for_symbol = 38;
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 31; OpenBufferForSymbolResponse open_buffer_for_symbol_response = 39;
UpdateProject update_project = 32; UpdateProject update_project = 40;
RegisterProjectActivity register_project_activity = 33; RegisterProjectActivity register_project_activity = 41;
UpdateWorktree update_worktree = 34; UpdateWorktree update_worktree = 42;
UpdateWorktreeExtensions update_worktree_extensions = 35; UpdateWorktreeExtensions update_worktree_extensions = 43;
CreateProjectEntry create_project_entry = 36; CreateProjectEntry create_project_entry = 44;
RenameProjectEntry rename_project_entry = 37; RenameProjectEntry rename_project_entry = 45;
CopyProjectEntry copy_project_entry = 38; CopyProjectEntry copy_project_entry = 46;
DeleteProjectEntry delete_project_entry = 39; DeleteProjectEntry delete_project_entry = 47;
ProjectEntryResponse project_entry_response = 40; ProjectEntryResponse project_entry_response = 48;
UpdateDiagnosticSummary update_diagnostic_summary = 41; UpdateDiagnosticSummary update_diagnostic_summary = 49;
StartLanguageServer start_language_server = 42; StartLanguageServer start_language_server = 50;
UpdateLanguageServer update_language_server = 43; UpdateLanguageServer update_language_server = 51;
OpenBufferById open_buffer_by_id = 44; OpenBufferById open_buffer_by_id = 52;
OpenBufferByPath open_buffer_by_path = 45; OpenBufferByPath open_buffer_by_path = 53;
OpenBufferResponse open_buffer_response = 46; OpenBufferResponse open_buffer_response = 54;
CreateBufferForPeer create_buffer_for_peer = 47; CreateBufferForPeer create_buffer_for_peer = 55;
UpdateBuffer update_buffer = 48; UpdateBuffer update_buffer = 56;
UpdateBufferFile update_buffer_file = 49; UpdateBufferFile update_buffer_file = 57;
SaveBuffer save_buffer = 50; SaveBuffer save_buffer = 58;
BufferSaved buffer_saved = 51; BufferSaved buffer_saved = 59;
BufferReloaded buffer_reloaded = 52; BufferReloaded buffer_reloaded = 60;
ReloadBuffers reload_buffers = 53; ReloadBuffers reload_buffers = 61;
ReloadBuffersResponse reload_buffers_response = 54; ReloadBuffersResponse reload_buffers_response = 62;
FormatBuffers format_buffers = 55; FormatBuffers format_buffers = 63;
FormatBuffersResponse format_buffers_response = 56; FormatBuffersResponse format_buffers_response = 64;
GetCompletions get_completions = 57; GetCompletions get_completions = 65;
GetCompletionsResponse get_completions_response = 58; GetCompletionsResponse get_completions_response = 66;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 59; ApplyCompletionAdditionalEdits apply_completion_additional_edits = 67;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 60; ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 68;
GetCodeActions get_code_actions = 61; GetCodeActions get_code_actions = 69;
GetCodeActionsResponse get_code_actions_response = 62; GetCodeActionsResponse get_code_actions_response = 70;
GetHover get_hover = 63; GetHover get_hover = 71;
GetHoverResponse get_hover_response = 64; GetHoverResponse get_hover_response = 72;
ApplyCodeAction apply_code_action = 65; ApplyCodeAction apply_code_action = 73;
ApplyCodeActionResponse apply_code_action_response = 66; ApplyCodeActionResponse apply_code_action_response = 74;
PrepareRename prepare_rename = 67; PrepareRename prepare_rename = 75;
PrepareRenameResponse prepare_rename_response = 68; PrepareRenameResponse prepare_rename_response = 76;
PerformRename perform_rename = 69; PerformRename perform_rename = 77;
PerformRenameResponse perform_rename_response = 70; PerformRenameResponse perform_rename_response = 78;
SearchProject search_project = 71; SearchProject search_project = 79;
SearchProjectResponse search_project_response = 72; SearchProjectResponse search_project_response = 80;
GetChannels get_channels = 73; GetChannels get_channels = 81;
GetChannelsResponse get_channels_response = 74; GetChannelsResponse get_channels_response = 82;
JoinChannel join_channel = 75; JoinChannel join_channel = 83;
JoinChannelResponse join_channel_response = 76; JoinChannelResponse join_channel_response = 84;
LeaveChannel leave_channel = 77; LeaveChannel leave_channel = 85;
SendChannelMessage send_channel_message = 78; SendChannelMessage send_channel_message = 86;
SendChannelMessageResponse send_channel_message_response = 79; SendChannelMessageResponse send_channel_message_response = 87;
ChannelMessageSent channel_message_sent = 80; ChannelMessageSent channel_message_sent = 88;
GetChannelMessages get_channel_messages = 81; GetChannelMessages get_channel_messages = 89;
GetChannelMessagesResponse get_channel_messages_response = 82; GetChannelMessagesResponse get_channel_messages_response = 90;
UpdateContacts update_contacts = 83; UpdateContacts update_contacts = 91;
UpdateInviteInfo update_invite_info = 84; UpdateInviteInfo update_invite_info = 92;
ShowContacts show_contacts = 85; ShowContacts show_contacts = 93;
GetUsers get_users = 86; GetUsers get_users = 94;
FuzzySearchUsers fuzzy_search_users = 87; FuzzySearchUsers fuzzy_search_users = 95;
UsersResponse users_response = 88; UsersResponse users_response = 96;
RequestContact request_contact = 89; RequestContact request_contact = 97;
RespondToContactRequest respond_to_contact_request = 90; RespondToContactRequest respond_to_contact_request = 98;
RemoveContact remove_contact = 91; RemoveContact remove_contact = 99;
Follow follow = 92; Follow follow = 100;
FollowResponse follow_response = 93; FollowResponse follow_response = 101;
UpdateFollowers update_followers = 94; UpdateFollowers update_followers = 102;
Unfollow unfollow = 95; Unfollow unfollow = 103;
GetPrivateUserInfo get_private_user_info = 96; GetPrivateUserInfo get_private_user_info = 104;
GetPrivateUserInfoResponse get_private_user_info_response = 97; GetPrivateUserInfoResponse get_private_user_info_response = 105;
UpdateDiffBase update_diff_base = 98; UpdateDiffBase update_diff_base = 106;
} }
} }
@ -128,70 +137,121 @@ message Test {
uint64 id = 1; uint64 id = 1;
} }
message RegisterProject { message CreateRoom {}
bool online = 1;
message CreateRoomResponse {
uint64 id = 1;
} }
message RegisterProjectResponse { message JoinRoom {
uint64 id = 1;
}
message JoinRoomResponse {
Room room = 1;
}
message LeaveRoom {
uint64 id = 1;
}
message Room {
repeated Participant participants = 1;
repeated uint64 pending_participant_user_ids = 2;
}
message Participant {
uint64 user_id = 1;
uint32 peer_id = 2;
repeated ParticipantProject projects = 3;
ParticipantLocation location = 4;
}
message ParticipantProject {
uint64 id = 1;
repeated string worktree_root_names = 2;
}
message ParticipantLocation {
oneof variant {
SharedProject shared_project = 1;
UnsharedProject unshared_project = 2;
External external = 3;
}
message SharedProject {
uint64 id = 1;
}
message UnsharedProject {}
message External {}
}
message Call {
uint64 room_id = 1;
uint64 recipient_user_id = 2;
optional uint64 initial_project_id = 3;
}
message IncomingCall {
uint64 room_id = 1;
uint64 caller_user_id = 2;
repeated uint64 participant_user_ids = 3;
optional ParticipantProject initial_project = 4;
}
message CallCanceled {}
message CancelCall {
uint64 room_id = 1;
uint64 recipient_user_id = 2;
}
message DeclineCall {
uint64 room_id = 1;
}
message UpdateParticipantLocation {
uint64 room_id = 1;
ParticipantLocation location = 2;
}
message RoomUpdated {
Room room = 1;
}
message ShareProject {
uint64 room_id = 1;
repeated WorktreeMetadata worktrees = 2;
}
message ShareProjectResponse {
uint64 project_id = 1; uint64 project_id = 1;
} }
message UnregisterProject { message UnshareProject {
uint64 project_id = 1; uint64 project_id = 1;
} }
message UpdateProject { message UpdateProject {
uint64 project_id = 1; uint64 project_id = 1;
repeated WorktreeMetadata worktrees = 2; repeated WorktreeMetadata worktrees = 2;
bool online = 3;
} }
message RegisterProjectActivity { message RegisterProjectActivity {
uint64 project_id = 1; uint64 project_id = 1;
} }
message RequestJoinProject {
uint64 requester_id = 1;
uint64 project_id = 2;
}
message RespondToJoinProjectRequest {
uint64 requester_id = 1;
uint64 project_id = 2;
bool allow = 3;
}
message JoinProjectRequestCancelled {
uint64 requester_id = 1;
uint64 project_id = 2;
}
message JoinProject { message JoinProject {
uint64 project_id = 1; uint64 project_id = 1;
} }
message JoinProjectResponse { message JoinProjectResponse {
oneof variant { uint32 replica_id = 1;
Accept accept = 1; repeated WorktreeMetadata worktrees = 2;
Decline decline = 2; repeated Collaborator collaborators = 3;
} repeated LanguageServer language_servers = 4;
message Accept {
uint32 replica_id = 1;
repeated WorktreeMetadata worktrees = 2;
repeated Collaborator collaborators = 3;
repeated LanguageServer language_servers = 4;
}
message Decline {
Reason reason = 1;
enum Reason {
Declined = 0;
Closed = 1;
WentOffline = 2;
}
}
} }
message LeaveProject { message LeaveProject {
@ -254,10 +314,6 @@ message RemoveProjectCollaborator {
uint32 peer_id = 2; uint32 peer_id = 2;
} }
message ProjectUnshared {
uint64 project_id = 1;
}
message GetDefinition { message GetDefinition {
uint64 project_id = 1; uint64 project_id = 1;
uint64 buffer_id = 2; uint64 buffer_id = 2;
@ -986,17 +1042,11 @@ message ChannelMessage {
message Contact { message Contact {
uint64 user_id = 1; uint64 user_id = 1;
repeated ProjectMetadata projects = 2; bool online = 2;
bool online = 3; bool busy = 3;
bool should_notify = 4; bool should_notify = 4;
} }
message ProjectMetadata {
uint64 id = 1;
repeated string visible_worktree_root_names = 3;
repeated uint64 guests = 4;
}
message WorktreeMetadata { message WorktreeMetadata {
uint64 id = 1; uint64 id = 1;
string root_name = 2; string root_name = 2;

View File

@ -33,7 +33,7 @@ impl fmt::Display for ConnectionId {
} }
} }
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct PeerId(pub u32); pub struct PeerId(pub u32);
impl fmt::Display for PeerId { impl fmt::Display for PeerId {
@ -394,7 +394,11 @@ impl Peer {
send?; send?;
let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?; let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?;
if let Some(proto::envelope::Payload::Error(error)) = &response.payload { if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
Err(anyhow!("RPC request failed - {}", error.message)) Err(anyhow!(
"RPC request {} failed - {}",
T::NAME,
error.message
))
} else { } else {
T::Response::from_envelope(response) T::Response::from_envelope(response)
.ok_or_else(|| anyhow!("received response of the wrong type")) .ok_or_else(|| anyhow!("received response of the wrong type"))

View File

@ -83,11 +83,16 @@ messages!(
(ApplyCompletionAdditionalEditsResponse, Background), (ApplyCompletionAdditionalEditsResponse, Background),
(BufferReloaded, Foreground), (BufferReloaded, Foreground),
(BufferSaved, Foreground), (BufferSaved, Foreground),
(RemoveContact, Foreground), (Call, Foreground),
(CallCanceled, Foreground),
(CancelCall, Foreground),
(ChannelMessageSent, Foreground), (ChannelMessageSent, Foreground),
(CopyProjectEntry, Foreground), (CopyProjectEntry, Foreground),
(CreateBufferForPeer, Foreground), (CreateBufferForPeer, Foreground),
(CreateProjectEntry, Foreground), (CreateProjectEntry, Foreground),
(CreateRoom, Foreground),
(CreateRoomResponse, Foreground),
(DeclineCall, Foreground),
(DeleteProjectEntry, Foreground), (DeleteProjectEntry, Foreground),
(Error, Foreground), (Error, Foreground),
(Follow, Foreground), (Follow, Foreground),
@ -116,14 +121,17 @@ messages!(
(GetProjectSymbols, Background), (GetProjectSymbols, Background),
(GetProjectSymbolsResponse, Background), (GetProjectSymbolsResponse, Background),
(GetUsers, Foreground), (GetUsers, Foreground),
(IncomingCall, Foreground),
(UsersResponse, Foreground), (UsersResponse, Foreground),
(JoinChannel, Foreground), (JoinChannel, Foreground),
(JoinChannelResponse, Foreground), (JoinChannelResponse, Foreground),
(JoinProject, Foreground), (JoinProject, Foreground),
(JoinProjectResponse, Foreground), (JoinProjectResponse, Foreground),
(JoinProjectRequestCancelled, Foreground), (JoinRoom, Foreground),
(JoinRoomResponse, Foreground),
(LeaveChannel, Foreground), (LeaveChannel, Foreground),
(LeaveProject, Foreground), (LeaveProject, Foreground),
(LeaveRoom, Foreground),
(OpenBufferById, Background), (OpenBufferById, Background),
(OpenBufferByPath, Background), (OpenBufferByPath, Background),
(OpenBufferForSymbol, Background), (OpenBufferForSymbol, Background),
@ -134,29 +142,28 @@ messages!(
(PrepareRename, Background), (PrepareRename, Background),
(PrepareRenameResponse, Background), (PrepareRenameResponse, Background),
(ProjectEntryResponse, Foreground), (ProjectEntryResponse, Foreground),
(ProjectUnshared, Foreground), (RemoveContact, Foreground),
(RegisterProjectResponse, Foreground),
(Ping, Foreground), (Ping, Foreground),
(RegisterProject, Foreground),
(RegisterProjectActivity, Foreground), (RegisterProjectActivity, Foreground),
(ReloadBuffers, Foreground), (ReloadBuffers, Foreground),
(ReloadBuffersResponse, Foreground), (ReloadBuffersResponse, Foreground),
(RemoveProjectCollaborator, Foreground), (RemoveProjectCollaborator, Foreground),
(RenameProjectEntry, Foreground), (RenameProjectEntry, Foreground),
(RequestContact, Foreground), (RequestContact, Foreground),
(RequestJoinProject, Foreground),
(RespondToContactRequest, Foreground), (RespondToContactRequest, Foreground),
(RespondToJoinProjectRequest, Foreground), (RoomUpdated, Foreground),
(SaveBuffer, Foreground), (SaveBuffer, Foreground),
(SearchProject, Background), (SearchProject, Background),
(SearchProjectResponse, Background), (SearchProjectResponse, Background),
(SendChannelMessage, Foreground), (SendChannelMessage, Foreground),
(SendChannelMessageResponse, Foreground), (SendChannelMessageResponse, Foreground),
(ShareProject, Foreground),
(ShareProjectResponse, Foreground),
(ShowContacts, Foreground), (ShowContacts, Foreground),
(StartLanguageServer, Foreground), (StartLanguageServer, Foreground),
(Test, Foreground), (Test, Foreground),
(Unfollow, Foreground), (Unfollow, Foreground),
(UnregisterProject, Foreground), (UnshareProject, Foreground),
(UpdateBuffer, Foreground), (UpdateBuffer, Foreground),
(UpdateBufferFile, Foreground), (UpdateBufferFile, Foreground),
(UpdateContacts, Foreground), (UpdateContacts, Foreground),
@ -164,6 +171,7 @@ messages!(
(UpdateFollowers, Foreground), (UpdateFollowers, Foreground),
(UpdateInviteInfo, Foreground), (UpdateInviteInfo, Foreground),
(UpdateLanguageServer, Foreground), (UpdateLanguageServer, Foreground),
(UpdateParticipantLocation, Foreground),
(UpdateProject, Foreground), (UpdateProject, Foreground),
(UpdateWorktree, Foreground), (UpdateWorktree, Foreground),
(UpdateWorktreeExtensions, Background), (UpdateWorktreeExtensions, Background),
@ -178,8 +186,12 @@ request_messages!(
ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEdits,
ApplyCompletionAdditionalEditsResponse ApplyCompletionAdditionalEditsResponse
), ),
(Call, Ack),
(CancelCall, Ack),
(CopyProjectEntry, ProjectEntryResponse), (CopyProjectEntry, ProjectEntryResponse),
(CreateProjectEntry, ProjectEntryResponse), (CreateProjectEntry, ProjectEntryResponse),
(CreateRoom, CreateRoomResponse),
(DeclineCall, Ack),
(DeleteProjectEntry, ProjectEntryResponse), (DeleteProjectEntry, ProjectEntryResponse),
(Follow, FollowResponse), (Follow, FollowResponse),
(FormatBuffers, FormatBuffersResponse), (FormatBuffers, FormatBuffersResponse),
@ -198,13 +210,14 @@ request_messages!(
(GetUsers, UsersResponse), (GetUsers, UsersResponse),
(JoinChannel, JoinChannelResponse), (JoinChannel, JoinChannelResponse),
(JoinProject, JoinProjectResponse), (JoinProject, JoinProjectResponse),
(JoinRoom, JoinRoomResponse),
(IncomingCall, Ack),
(OpenBufferById, OpenBufferResponse), (OpenBufferById, OpenBufferResponse),
(OpenBufferByPath, OpenBufferResponse), (OpenBufferByPath, OpenBufferResponse),
(OpenBufferForSymbol, OpenBufferForSymbolResponse), (OpenBufferForSymbol, OpenBufferForSymbolResponse),
(Ping, Ack), (Ping, Ack),
(PerformRename, PerformRenameResponse), (PerformRename, PerformRenameResponse),
(PrepareRename, PrepareRenameResponse), (PrepareRename, PrepareRenameResponse),
(RegisterProject, RegisterProjectResponse),
(ReloadBuffers, ReloadBuffersResponse), (ReloadBuffers, ReloadBuffersResponse),
(RequestContact, Ack), (RequestContact, Ack),
(RemoveContact, Ack), (RemoveContact, Ack),
@ -213,9 +226,10 @@ request_messages!(
(SaveBuffer, BufferSaved), (SaveBuffer, BufferSaved),
(SearchProject, SearchProjectResponse), (SearchProject, SearchProjectResponse),
(SendChannelMessage, SendChannelMessageResponse), (SendChannelMessage, SendChannelMessageResponse),
(ShareProject, ShareProjectResponse),
(Test, Test), (Test, Test),
(UnregisterProject, Ack),
(UpdateBuffer, Ack), (UpdateBuffer, Ack),
(UpdateParticipantLocation, Ack),
(UpdateWorktree, Ack), (UpdateWorktree, Ack),
); );
@ -241,24 +255,21 @@ entity_messages!(
GetReferences, GetReferences,
GetProjectSymbols, GetProjectSymbols,
JoinProject, JoinProject,
JoinProjectRequestCancelled,
LeaveProject, LeaveProject,
OpenBufferById, OpenBufferById,
OpenBufferByPath, OpenBufferByPath,
OpenBufferForSymbol, OpenBufferForSymbol,
PerformRename, PerformRename,
PrepareRename, PrepareRename,
ProjectUnshared,
RegisterProjectActivity, RegisterProjectActivity,
ReloadBuffers, ReloadBuffers,
RemoveProjectCollaborator, RemoveProjectCollaborator,
RenameProjectEntry, RenameProjectEntry,
RequestJoinProject,
SaveBuffer, SaveBuffer,
SearchProject, SearchProject,
StartLanguageServer, StartLanguageServer,
Unfollow, Unfollow,
UnregisterProject, UnshareProject,
UpdateBuffer, UpdateBuffer,
UpdateBufferFile, UpdateBufferFile,
UpdateDiagnosticSummary, UpdateDiagnosticSummary,

View File

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

View File

@ -726,6 +726,8 @@ impl Element for TerminalElement {
layout: &mut Self::LayoutState, layout: &mut Self::LayoutState,
cx: &mut gpui::PaintContext, cx: &mut gpui::PaintContext,
) -> Self::PaintState { ) -> Self::PaintState {
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
//Setup element stuff //Setup element stuff
let clip_bounds = Some(visible_bounds); let clip_bounds = Some(visible_bounds);

View File

@ -20,7 +20,7 @@ pub struct Theme {
pub context_menu: ContextMenu, pub context_menu: ContextMenu,
pub chat_panel: ChatPanel, pub chat_panel: ChatPanel,
pub contacts_popover: ContactsPopover, pub contacts_popover: ContactsPopover,
pub contacts_panel: ContactsPanel, pub contact_list: ContactList,
pub contact_finder: ContactFinder, pub contact_finder: ContactFinder,
pub project_panel: ProjectPanel, pub project_panel: ProjectPanel,
pub command_palette: CommandPalette, pub command_palette: CommandPalette,
@ -31,6 +31,8 @@ pub struct Theme {
pub breadcrumbs: ContainedText, pub breadcrumbs: ContainedText,
pub contact_notification: ContactNotification, pub contact_notification: ContactNotification,
pub update_notification: UpdateNotification, pub update_notification: UpdateNotification,
pub project_shared_notification: ProjectSharedNotification,
pub incoming_call_notification: IncomingCallNotification,
pub tooltip: TooltipStyle, pub tooltip: TooltipStyle,
pub terminal: TerminalStyle, pub terminal: TerminalStyle,
} }
@ -58,6 +60,7 @@ pub struct Workspace {
pub notifications: Notifications, pub notifications: Notifications,
pub joining_project_avatar: ImageStyle, pub joining_project_avatar: ImageStyle,
pub joining_project_message: ContainedText, pub joining_project_message: ContainedText,
pub external_location_message: ContainedText,
pub dock: Dock, pub dock: Dock,
} }
@ -72,8 +75,67 @@ pub struct Titlebar {
pub avatar_ribbon: AvatarRibbon, pub avatar_ribbon: AvatarRibbon,
pub offline_icon: OfflineIcon, pub offline_icon: OfflineIcon,
pub avatar: ImageStyle, pub avatar: ImageStyle,
pub inactive_avatar: ImageStyle,
pub sign_in_prompt: Interactive<ContainedText>, pub sign_in_prompt: Interactive<ContainedText>,
pub outdated_warning: ContainedText, pub outdated_warning: ContainedText,
pub share_button: Interactive<ContainedText>,
pub toggle_contacts_button: Interactive<IconButton>,
pub toggle_contacts_badge: ContainerStyle,
}
#[derive(Deserialize, Default)]
pub struct ContactsPopover {
#[serde(flatten)]
pub container: ContainerStyle,
pub height: f32,
pub width: f32,
pub invite_row_height: f32,
pub invite_row: Interactive<ContainedLabel>,
}
#[derive(Deserialize, Default)]
pub struct ContactList {
pub user_query_editor: FieldEditor,
pub user_query_editor_height: f32,
pub add_contact_button: IconButton,
pub header_row: Interactive<ContainedText>,
pub leave_call: Interactive<ContainedText>,
pub contact_row: Interactive<ContainerStyle>,
pub row_height: f32,
pub project_row: Interactive<ProjectRow>,
pub tree_branch: Interactive<TreeBranch>,
pub contact_avatar: ImageStyle,
pub contact_status_free: ContainerStyle,
pub contact_status_busy: ContainerStyle,
pub contact_username: ContainedText,
pub contact_button: Interactive<IconButton>,
pub contact_button_spacing: f32,
pub disabled_button: IconButton,
pub section_icon_size: f32,
pub calling_indicator: ContainedText,
}
#[derive(Deserialize, Default)]
pub struct ProjectRow {
#[serde(flatten)]
pub container: ContainerStyle,
pub name: ContainedText,
}
#[derive(Deserialize, Default, Clone, Copy)]
pub struct TreeBranch {
pub width: f32,
pub color: Color,
}
#[derive(Deserialize, Default)]
pub struct ContactFinder {
pub picker: Picker,
pub row_height: f32,
pub contact_avatar: ImageStyle,
pub contact_username: ContainerStyle,
pub contact_button: IconButton,
pub disabled_contact_button: IconButton,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default)]
@ -315,33 +377,6 @@ pub struct CommandPalette {
pub keystroke_spacing: f32, pub keystroke_spacing: f32,
} }
#[derive(Deserialize, Default)]
pub struct ContactsPopover {
pub background: Color,
}
#[derive(Deserialize, Default)]
pub struct ContactsPanel {
#[serde(flatten)]
pub container: ContainerStyle,
pub user_query_editor: FieldEditor,
pub user_query_editor_height: f32,
pub add_contact_button: IconButton,
pub header_row: Interactive<ContainedText>,
pub contact_row: Interactive<ContainerStyle>,
pub project_row: Interactive<ProjectRow>,
pub row_height: f32,
pub contact_avatar: ImageStyle,
pub contact_username: ContainedText,
pub contact_button: Interactive<IconButton>,
pub contact_button_spacing: f32,
pub disabled_button: IconButton,
pub tree_branch: Interactive<TreeBranch>,
pub private_button: Interactive<IconButton>,
pub section_icon_size: f32,
pub invite_row: Interactive<ContainedLabel>,
}
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
pub struct InviteLink { pub struct InviteLink {
#[serde(flatten)] #[serde(flatten)]
@ -351,21 +386,6 @@ pub struct InviteLink {
pub icon: Icon, pub icon: Icon,
} }
#[derive(Deserialize, Default, Clone, Copy)]
pub struct TreeBranch {
pub width: f32,
pub color: Color,
}
#[derive(Deserialize, Default)]
pub struct ContactFinder {
pub row_height: f32,
pub contact_avatar: ImageStyle,
pub contact_username: ContainerStyle,
pub contact_button: IconButton,
pub disabled_contact_button: IconButton,
}
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
pub struct Icon { pub struct Icon {
#[serde(flatten)] #[serde(flatten)]
@ -384,16 +404,6 @@ pub struct IconButton {
pub button_width: f32, pub button_width: f32,
} }
#[derive(Deserialize, Default)]
pub struct ProjectRow {
#[serde(flatten)]
pub container: ContainerStyle,
pub name: ContainedText,
pub guests: ContainerStyle,
pub guest_avatar: ImageStyle,
pub guest_avatar_spacing: f32,
}
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
pub struct ChatMessage { pub struct ChatMessage {
#[serde(flatten)] #[serde(flatten)]
@ -475,6 +485,40 @@ pub struct UpdateNotification {
pub dismiss_button: Interactive<IconButton>, pub dismiss_button: Interactive<IconButton>,
} }
#[derive(Deserialize, Default)]
pub struct ProjectSharedNotification {
pub window_height: f32,
pub window_width: f32,
#[serde(default)]
pub background: Color,
pub owner_container: ContainerStyle,
pub owner_avatar: ImageStyle,
pub owner_metadata: ContainerStyle,
pub owner_username: ContainedText,
pub message: ContainedText,
pub worktree_roots: ContainedText,
pub button_width: f32,
pub open_button: ContainedText,
pub dismiss_button: ContainedText,
}
#[derive(Deserialize, Default)]
pub struct IncomingCallNotification {
pub window_height: f32,
pub window_width: f32,
#[serde(default)]
pub background: Color,
pub caller_container: ContainerStyle,
pub caller_avatar: ImageStyle,
pub caller_metadata: ContainerStyle,
pub caller_username: ContainedText,
pub caller_message: ContainedText,
pub worktree_roots: ContainedText,
pub button_width: f32,
pub accept_button: ContainedText,
pub decline_button: ContainedText,
}
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default)]
pub struct Editor { pub struct Editor {
pub text_color: Color, pub text_color: Color,

View File

@ -8,11 +8,16 @@ path = "src/workspace.rs"
doctest = false doctest = false
[features] [features]
test-support = ["client/test-support", "project/test-support", "settings/test-support"] test-support = [
"call/test-support",
"client/test-support",
"project/test-support",
"settings/test-support"
]
[dependencies] [dependencies]
call = { path = "../call" }
client = { path = "../client" } client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" } collections = { path = "../collections" }
context_menu = { path = "../context_menu" } context_menu = { path = "../context_menu" }
drag_and_drop = { path = "../drag_and_drop" } drag_and_drop = { path = "../drag_and_drop" }
@ -33,6 +38,7 @@ serde_json = { version = "1.0", features = ["preserve_order"] }
smallvec = { version = "1.6", features = ["union"] } smallvec = { version = "1.6", features = ["union"] }
[dev-dependencies] [dev-dependencies]
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] } client = { path = "../client", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] } project = { path = "../project", features = ["test-support"] }

View File

@ -1,9 +1,10 @@
use crate::{FollowerStatesByLeader, Pane}; use crate::{FollowerStatesByLeader, JoinProject, Pane, Workspace};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use client::PeerId; use call::ActiveCall;
use collections::HashMap; use gpui::{
use gpui::{elements::*, Axis, Border, ViewHandle}; elements::*, Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle,
use project::Collaborator; };
use project::Project;
use serde::Deserialize; use serde::Deserialize;
use theme::Theme; use theme::Theme;
@ -56,11 +57,14 @@ impl PaneGroup {
pub(crate) fn render( pub(crate) fn render(
&self, &self,
project: &ModelHandle<Project>,
theme: &Theme, theme: &Theme,
follower_states: &FollowerStatesByLeader, follower_states: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>, active_call: Option<&ModelHandle<ActiveCall>>,
cx: &mut RenderContext<Workspace>,
) -> ElementBox { ) -> ElementBox {
self.root.render(theme, follower_states, collaborators) self.root
.render(project, theme, follower_states, active_call, cx)
} }
pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> { pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
@ -100,13 +104,16 @@ impl Member {
pub fn render( pub fn render(
&self, &self,
project: &ModelHandle<Project>,
theme: &Theme, theme: &Theme,
follower_states: &FollowerStatesByLeader, follower_states: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>, active_call: Option<&ModelHandle<ActiveCall>>,
cx: &mut RenderContext<Workspace>,
) -> ElementBox { ) -> ElementBox {
enum FollowIntoExternalProject {}
match self { match self {
Member::Pane(pane) => { Member::Pane(pane) => {
let mut border = Border::default();
let leader = follower_states let leader = follower_states
.iter() .iter()
.find_map(|(leader_id, follower_states)| { .find_map(|(leader_id, follower_states)| {
@ -116,21 +123,110 @@ impl Member {
None None
} }
}) })
.and_then(|leader_id| collaborators.get(leader_id)); .and_then(|leader_id| {
if let Some(leader) = leader { let room = active_call?.read(cx).room()?.read(cx);
let leader_color = theme let collaborator = project.read(cx).collaborators().get(leader_id)?;
.editor let participant = room.remote_participants().get(&leader_id)?;
.replica_selection_style(leader.replica_id) Some((collaborator.replica_id, participant))
.cursor; });
let mut border = Border::default();
let prompt = if let Some((replica_id, leader)) = leader {
let leader_color = theme.editor.replica_selection_style(replica_id).cursor;
border = Border::all(theme.workspace.leader_border_width, leader_color); border = Border::all(theme.workspace.leader_border_width, leader_color);
border border
.color .color
.fade_out(1. - theme.workspace.leader_border_opacity); .fade_out(1. - theme.workspace.leader_border_opacity);
border.overlay = true; border.overlay = true;
}
ChildView::new(pane).contained().with_border(border).boxed() match leader.location {
call::ParticipantLocation::SharedProject {
project_id: leader_project_id,
} => {
if Some(leader_project_id) == project.read(cx).remote_id() {
None
} else {
let leader_user = leader.user.clone();
let leader_user_id = leader.user.id;
Some(
MouseEventHandler::<FollowIntoExternalProject>::new(
pane.id(),
cx,
|_, _| {
Label::new(
format!(
"Follow {} on their active project",
leader_user.github_login,
),
theme
.workspace
.external_location_message
.text
.clone(),
)
.contained()
.with_style(
theme.workspace.external_location_message.container,
)
.boxed()
},
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id: leader_project_id,
follow_user_id: leader_user_id,
})
})
.aligned()
.bottom()
.right()
.boxed(),
)
}
}
call::ParticipantLocation::UnsharedProject => Some(
Label::new(
format!(
"{} is viewing an unshared Zed project",
leader.user.github_login
),
theme.workspace.external_location_message.text.clone(),
)
.contained()
.with_style(theme.workspace.external_location_message.container)
.aligned()
.bottom()
.right()
.boxed(),
),
call::ParticipantLocation::External => Some(
Label::new(
format!(
"{} is viewing a window outside of Zed",
leader.user.github_login
),
theme.workspace.external_location_message.text.clone(),
)
.contained()
.with_style(theme.workspace.external_location_message.container)
.aligned()
.bottom()
.right()
.boxed(),
),
}
} else {
None
};
Stack::new()
.with_child(ChildView::new(pane).contained().with_border(border).boxed())
.with_children(prompt)
.boxed()
} }
Member::Axis(axis) => axis.render(theme, follower_states, collaborators), Member::Axis(axis) => axis.render(project, theme, follower_states, active_call, cx),
} }
} }
@ -232,14 +328,16 @@ impl PaneAxis {
fn render( fn render(
&self, &self,
project: &ModelHandle<Project>,
theme: &Theme, theme: &Theme,
follower_state: &FollowerStatesByLeader, follower_state: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>, active_call: Option<&ModelHandle<ActiveCall>>,
cx: &mut RenderContext<Workspace>,
) -> ElementBox { ) -> ElementBox {
let last_member_ix = self.members.len() - 1; let last_member_ix = self.members.len() - 1;
Flex::new(self.axis) Flex::new(self.axis)
.with_children(self.members.iter().enumerate().map(|(ix, member)| { .with_children(self.members.iter().enumerate().map(|(ix, member)| {
let mut member = member.render(theme, follower_state, collaborators); let mut member = member.render(project, theme, follower_state, active_call, cx);
if ix < last_member_ix { if ix < last_member_ix {
let mut border = theme.workspace.pane_divider; let mut border = theme.workspace.pane_divider;
border.left = false; border.left = false;

View File

@ -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
}

View File

@ -10,28 +10,22 @@ pub mod searchable;
pub mod sidebar; pub mod sidebar;
mod status_bar; mod status_bar;
mod toolbar; mod toolbar;
mod waiting_room;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use client::{ use call::ActiveCall;
proto, Authenticate, Client, Contact, PeerId, Subscription, TypedEnvelope, User, UserStore, use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
};
use clock::ReplicaId;
use collections::{hash_map, HashMap, HashSet}; use collections::{hash_map, HashMap, HashSet};
use dock::{DefaultItemFactory, Dock, ToggleDockButton}; use dock::{DefaultItemFactory, Dock, ToggleDockButton};
use drag_and_drop::DragAndDrop; use drag_and_drop::DragAndDrop;
use futures::{channel::oneshot, FutureExt}; use futures::{channel::oneshot, FutureExt, StreamExt};
use gpui::{ use gpui::{
actions, actions,
color::Color,
elements::*, elements::*,
geometry::{rect::RectF, vector::vec2f, PathBuilder},
impl_actions, impl_internal_actions, impl_actions, impl_internal_actions,
json::{self, ToJson},
platform::{CursorStyle, WindowOptions}, platform::{CursorStyle, WindowOptions},
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
ModelContext, ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, ViewContext, ViewHandle, WeakViewHandle,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use log::{error, warn}; use log::{error, warn};
@ -52,7 +46,6 @@ use std::{
cell::RefCell, cell::RefCell,
fmt, fmt,
future::Future, future::Future,
ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
sync::{ sync::{
@ -64,7 +57,6 @@ use std::{
use theme::{Theme, ThemeRegistry}; use theme::{Theme, ThemeRegistry};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use util::ResultExt; use util::ResultExt;
use waiting_room::WaitingRoom;
type ProjectItemBuilders = HashMap< type ProjectItemBuilders = HashMap<
TypeId, TypeId,
@ -115,12 +107,6 @@ pub struct OpenPaths {
pub paths: Vec<PathBuf>, pub paths: Vec<PathBuf>,
} }
#[derive(Clone, Deserialize, PartialEq)]
pub struct ToggleProjectOnline {
#[serde(skip_deserializing)]
pub project: Option<ModelHandle<Project>>,
}
#[derive(Clone, Deserialize, PartialEq)] #[derive(Clone, Deserialize, PartialEq)]
pub struct ActivatePane(pub usize); pub struct ActivatePane(pub usize);
@ -129,8 +115,8 @@ pub struct ToggleFollow(pub PeerId);
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct JoinProject { pub struct JoinProject {
pub contact: Arc<Contact>, pub project_id: u64,
pub project_index: usize, pub follow_user_id: u64,
} }
impl_internal_actions!( impl_internal_actions!(
@ -142,7 +128,7 @@ impl_internal_actions!(
RemoveWorktreeFromProject RemoveWorktreeFromProject
] ]
); );
impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]); impl_actions!(workspace, [ActivatePane]);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) { pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
pane::init(cx); pane::init(cx);
@ -173,14 +159,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
} }
} }
}); });
cx.add_global_action({
let app_state = Arc::downgrade(&app_state);
move |action: &JoinProject, cx: &mut MutableAppContext| {
if let Some(app_state) = app_state.upgrade() {
join_project(action.contact.clone(), action.project_index, &app_state, cx);
}
}
});
cx.add_async_action(Workspace::toggle_follow); cx.add_async_action(Workspace::toggle_follow);
cx.add_async_action(Workspace::follow_next_collaborator); cx.add_async_action(Workspace::follow_next_collaborator);
@ -188,7 +166,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
cx.add_async_action(Workspace::save_all); cx.add_async_action(Workspace::save_all);
cx.add_action(Workspace::add_folder_to_project); cx.add_action(Workspace::add_folder_to_project);
cx.add_action(Workspace::remove_folder_from_project); cx.add_action(Workspace::remove_folder_from_project);
cx.add_action(Workspace::toggle_project_online);
cx.add_action( cx.add_action(
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| { |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
let pane = workspace.active_pane().clone(); let pane = workspace.active_pane().clone();
@ -957,7 +934,7 @@ impl AppState {
let languages = Arc::new(LanguageRegistry::test()); let languages = Arc::new(LanguageRegistry::test());
let http_client = client::test::FakeHttpClient::with_404_response(); let http_client = client::test::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone(), cx); let client = Client::new(http_client.clone(), cx);
let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); let project_store = cx.add_model(|_| ProjectStore::new());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let themes = ThemeRegistry::new((), cx.font_cache().clone()); let themes = ThemeRegistry::new((), cx.font_cache().clone());
Arc::new(Self { Arc::new(Self {
@ -984,7 +961,7 @@ pub struct Workspace {
weak_self: WeakViewHandle<Self>, weak_self: WeakViewHandle<Self>,
client: Arc<Client>, client: Arc<Client>,
user_store: ModelHandle<client::UserStore>, user_store: ModelHandle<client::UserStore>,
remote_entity_subscription: Option<Subscription>, remote_entity_subscription: Option<client::Subscription>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
modal: Option<AnyViewHandle>, modal: Option<AnyViewHandle>,
center: PaneGroup, center: PaneGroup,
@ -995,6 +972,7 @@ pub struct Workspace {
active_pane: ViewHandle<Pane>, active_pane: ViewHandle<Pane>,
last_active_center_pane: Option<ViewHandle<Pane>>, last_active_center_pane: Option<ViewHandle<Pane>>,
status_bar: ViewHandle<StatusBar>, status_bar: ViewHandle<StatusBar>,
titlebar_item: Option<AnyViewHandle>,
dock: Dock, dock: Dock,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>, notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
@ -1002,7 +980,9 @@ pub struct Workspace {
follower_states_by_leader: FollowerStatesByLeader, follower_states_by_leader: FollowerStatesByLeader,
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>, last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool, window_edited: bool,
active_call: Option<ModelHandle<ActiveCall>>,
_observe_current_user: Task<()>, _observe_current_user: Task<()>,
_active_call_observation: Option<gpui::Subscription>,
} }
#[derive(Default)] #[derive(Default)]
@ -1111,6 +1091,14 @@ impl Workspace {
drag_and_drop.register_container(weak_handle.clone()); drag_and_drop.register_container(weak_handle.clone());
}); });
let mut active_call = None;
let mut active_call_observation = None;
if cx.has_global::<ModelHandle<ActiveCall>>() {
let call = cx.global::<ModelHandle<ActiveCall>>().clone();
active_call_observation = Some(cx.observe(&call, |_, _, cx| cx.notify()));
active_call = Some(call);
}
let mut this = Workspace { let mut this = Workspace {
modal: None, modal: None,
weak_self: weak_handle, weak_self: weak_handle,
@ -1124,6 +1112,7 @@ impl Workspace {
active_pane: center_pane.clone(), active_pane: center_pane.clone(),
last_active_center_pane: Some(center_pane.clone()), last_active_center_pane: Some(center_pane.clone()),
status_bar, status_bar,
titlebar_item: None,
notifications: Default::default(), notifications: Default::default(),
client, client,
remote_entity_subscription: None, remote_entity_subscription: None,
@ -1136,7 +1125,9 @@ impl Workspace {
follower_states_by_leader: Default::default(), follower_states_by_leader: Default::default(),
last_leaders_by_pane: Default::default(), last_leaders_by_pane: Default::default(),
window_edited: false, window_edited: false,
active_call,
_observe_current_user, _observe_current_user,
_active_call_observation: active_call_observation,
}; };
this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
cx.defer(|this, cx| this.update_window_title(cx)); cx.defer(|this, cx| this.update_window_title(cx));
@ -1168,6 +1159,19 @@ impl Workspace {
&self.project &self.project
} }
pub fn client(&self) -> &Arc<Client> {
&self.client
}
pub fn set_titlebar_item(
&mut self,
item: impl Into<AnyViewHandle>,
cx: &mut ViewContext<Self>,
) {
self.titlebar_item = Some(item.into());
cx.notify();
}
/// Call the given callback with a workspace whose project is local. /// Call the given callback with a workspace whose project is local.
/// ///
/// If the given workspace has a local project, then it will be passed /// If the given workspace has a local project, then it will be passed
@ -1188,7 +1192,6 @@ impl Workspace {
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
let mut workspace = Workspace::new( let mut workspace = Workspace::new(
Project::local( Project::local(
false,
app_state.client.clone(), app_state.client.clone(),
app_state.user_store.clone(), app_state.user_store.clone(),
app_state.project_store.clone(), app_state.project_store.clone(),
@ -1238,7 +1241,7 @@ impl Workspace {
_: &CloseWindow, _: &CloseWindow,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> { ) -> Option<Task<Result<()>>> {
let prepare = self.prepare_to_close(cx); let prepare = self.prepare_to_close(false, cx);
Some(cx.spawn(|this, mut cx| async move { Some(cx.spawn(|this, mut cx| async move {
if prepare.await? { if prepare.await? {
this.update(&mut cx, |_, cx| { this.update(&mut cx, |_, cx| {
@ -1250,8 +1253,44 @@ impl Workspace {
})) }))
} }
pub fn prepare_to_close(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> { pub fn prepare_to_close(
self.save_all_internal(true, cx) &mut self,
quitting: bool,
cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> {
let active_call = self.active_call.clone();
let window_id = cx.window_id();
let workspace_count = cx
.window_ids()
.flat_map(|window_id| cx.root_view::<Workspace>(window_id))
.count();
cx.spawn(|this, mut cx| async move {
if let Some(active_call) = active_call {
if !quitting
&& workspace_count == 1
&& active_call.read_with(&cx, |call, _| call.room().is_some())
{
let answer = cx
.prompt(
window_id,
PromptLevel::Warning,
"Do you want to leave the current call?",
&["Close window and hang up", "Cancel"],
)
.next()
.await;
if answer == Some(1) {
return anyhow::Ok(false);
} else {
active_call.update(&mut cx, |call, cx| call.hang_up(cx))?;
}
}
}
Ok(this
.update(&mut cx, |this, cx| this.save_all_internal(true, cx))
.await?)
})
} }
fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> { fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
@ -1393,17 +1432,6 @@ impl Workspace {
.update(cx, |project, cx| project.remove_worktree(*worktree_id, cx)); .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
} }
fn toggle_project_online(&mut self, action: &ToggleProjectOnline, cx: &mut ViewContext<Self>) {
let project = action
.project
.clone()
.unwrap_or_else(|| self.project.clone());
project.update(cx, |project, cx| {
let public = !project.is_online();
project.set_online(public, cx);
});
}
fn project_path_for_path( fn project_path_for_path(
&self, &self,
abs_path: &Path, abs_path: &Path,
@ -2068,46 +2096,12 @@ impl Workspace {
None None
} }
fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> { pub fn is_following(&self, peer_id: PeerId) -> bool {
let theme = &cx.global::<Settings>().theme; self.follower_states_by_leader.contains_key(&peer_id)
match &*self.client.status().borrow() {
client::Status::ConnectionError
| client::Status::ConnectionLost
| client::Status::Reauthenticating { .. }
| client::Status::Reconnecting { .. }
| client::Status::ReconnectionError { .. } => Some(
Container::new(
Align::new(
ConstrainedBox::new(
Svg::new("icons/cloud_slash_12.svg")
.with_color(theme.workspace.titlebar.offline_icon.color)
.boxed(),
)
.with_width(theme.workspace.titlebar.offline_icon.width)
.boxed(),
)
.boxed(),
)
.with_style(theme.workspace.titlebar.offline_icon.container)
.boxed(),
),
client::Status::UpgradeRequired => Some(
Label::new(
"Please update Zed to collaborate".to_string(),
theme.workspace.titlebar.outdated_warning.text.clone(),
)
.contained()
.with_style(theme.workspace.titlebar.outdated_warning.container)
.aligned()
.boxed(),
),
_ => None,
}
} }
fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox { fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
let project = &self.project.read(cx); let project = &self.project.read(cx);
let replica_id = project.replica_id();
let mut worktree_root_names = String::new(); let mut worktree_root_names = String::new();
for (i, name) in project.worktree_root_names(cx).enumerate() { for (i, name) in project.worktree_root_names(cx).enumerate() {
if i > 0 { if i > 0 {
@ -2129,7 +2123,7 @@ impl Workspace {
enum TitleBar {} enum TitleBar {}
ConstrainedBox::new( ConstrainedBox::new(
MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| { MouseEventHandler::<TitleBar>::new(0, cx, |_, _| {
Container::new( Container::new(
Stack::new() Stack::new()
.with_child( .with_child(
@ -2138,21 +2132,10 @@ impl Workspace {
.left() .left()
.boxed(), .boxed(),
) )
.with_child( .with_children(
Align::new( self.titlebar_item
Flex::row() .as_ref()
.with_children(self.render_collaborators(theme, cx)) .map(|item| ChildView::new(item).aligned().right().boxed()),
.with_children(self.render_current_user(
self.user_store.read(cx).current_user().as_ref(),
replica_id,
theme,
cx,
))
.with_children(self.render_connection_status(cx))
.boxed(),
)
.right()
.boxed(),
) )
.boxed(), .boxed(),
) )
@ -2221,125 +2204,6 @@ impl Workspace {
} }
} }
fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
let mut collaborators = self
.project
.read(cx)
.collaborators()
.values()
.cloned()
.collect::<Vec<_>>();
collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id);
collaborators
.into_iter()
.filter_map(|collaborator| {
Some(self.render_avatar(
collaborator.user.avatar.clone()?,
collaborator.replica_id,
Some((collaborator.peer_id, &collaborator.user.github_login)),
theme,
cx,
))
})
.collect()
}
fn render_current_user(
&self,
user: Option<&Arc<User>>,
replica_id: ReplicaId,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let status = *self.client.status().borrow();
if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
Some(self.render_avatar(avatar, replica_id, None, theme, cx))
} else if matches!(status, client::Status::UpgradeRequired) {
None
} else {
Some(
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
let style = theme
.workspace
.titlebar
.sign_in_prompt
.style_for(state, false);
Label::new("Sign in".to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
.boxed(),
)
}
}
fn render_avatar(
&self,
avatar: Arc<ImageData>,
replica_id: ReplicaId,
peer: Option<(PeerId, &str)>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let replica_color = theme.editor.replica_selection_style(replica_id).cursor;
let is_followed = peer.map_or(false, |(peer_id, _)| {
self.follower_states_by_leader.contains_key(&peer_id)
});
let mut avatar_style = theme.workspace.titlebar.avatar;
if is_followed {
avatar_style.border = Border::all(1.0, replica_color);
}
let content = Stack::new()
.with_child(
Image::new(avatar)
.with_style(avatar_style)
.constrained()
.with_width(theme.workspace.titlebar.avatar_width)
.aligned()
.boxed(),
)
.with_child(
AvatarRibbon::new(replica_color)
.constrained()
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
.aligned()
.bottom()
.boxed(),
)
.constrained()
.with_width(theme.workspace.titlebar.avatar_width)
.contained()
.with_margin_left(theme.workspace.titlebar.avatar_margin)
.boxed();
if let Some((peer_id, peer_github_login)) = peer {
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleFollow(peer_id))
})
.with_tooltip::<ToggleFollow, _>(
peer_id.0 as usize,
if is_followed {
format!("Unfollow {}", peer_github_login)
} else {
format!("Follow {}", peer_github_login)
},
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed()
} else {
content
}
}
fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> { fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
if self.project.read(cx).is_read_only() { if self.project.read(cx).is_read_only() {
enum DisconnectedOverlay {} enum DisconnectedOverlay {}
@ -2698,6 +2562,7 @@ impl View for Workspace {
.with_child( .with_child(
Stack::new() Stack::new()
.with_child({ .with_child({
let project = self.project.clone();
Flex::row() Flex::row()
.with_children( .with_children(
if self.left_sidebar.read(cx).active_item().is_some() { if self.left_sidebar.read(cx).active_item().is_some() {
@ -2715,9 +2580,11 @@ impl View for Workspace {
Flex::column() Flex::column()
.with_child( .with_child(
FlexItem::new(self.center.render( FlexItem::new(self.center.render(
&project,
&theme, &theme,
&self.follower_states_by_leader, &self.follower_states_by_leader,
self.project.read(cx).collaborators(), self.active_call.as_ref(),
cx,
)) ))
.flex(1., true) .flex(1., true)
.boxed(), .boxed(),
@ -2814,87 +2681,6 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
} }
} }
pub struct AvatarRibbon {
color: Color,
}
impl AvatarRibbon {
pub fn new(color: Color) -> AvatarRibbon {
AvatarRibbon { color }
}
}
impl Element for AvatarRibbon {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
_: &mut gpui::LayoutContext,
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
(constraint.max, ())
}
fn paint(
&mut self,
bounds: gpui::geometry::rect::RectF,
_: gpui::geometry::rect::RectF,
_: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
let mut path = PathBuilder::new();
path.reset(bounds.lower_left());
path.curve_to(
bounds.origin() + vec2f(bounds.height(), 0.),
bounds.origin(),
);
path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
path.curve_to(bounds.lower_right(), bounds.upper_right());
path.line_to(bounds.lower_left());
cx.scene.push_path(path.build(self.color, None));
}
fn dispatch_event(
&mut self,
_: &gpui::Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut gpui::EventContext,
) -> bool {
false
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::MeasurementContext,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: gpui::geometry::rect::RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::DebugContext,
) -> gpui::json::Value {
json::json!({
"type": "AvatarRibbon",
"bounds": bounds.to_json(),
"color": self.color.to_json(),
})
}
}
impl std::fmt::Debug for OpenPaths { impl std::fmt::Debug for OpenPaths {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OpenPaths") f.debug_struct("OpenPaths")
@ -2964,7 +2750,6 @@ pub fn open_paths(
cx.add_window((app_state.build_window_options)(), |cx| { cx.add_window((app_state.build_window_options)(), |cx| {
let project = Project::local( let project = Project::local(
false,
app_state.client.clone(), app_state.client.clone(),
app_state.user_store.clone(), app_state.user_store.clone(),
app_state.project_store.clone(), app_state.project_store.clone(),
@ -2989,44 +2774,14 @@ pub fn open_paths(
}) })
.await; .await;
if let Some(project) = new_project {
project
.update(&mut cx, |project, cx| project.restore_state(cx))
.await
.log_err();
}
(workspace, items) (workspace, items)
}) })
} }
pub fn join_project(
contact: Arc<Contact>,
project_index: usize,
app_state: &Arc<AppState>,
cx: &mut MutableAppContext,
) {
let project_id = contact.projects[project_index].id;
for window_id in cx.window_ids().collect::<Vec<_>>() {
if let Some(workspace) = cx.root_view::<Workspace>(window_id) {
if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) {
cx.activate_window(window_id);
return;
}
}
}
cx.add_window((app_state.build_window_options)(), |cx| {
WaitingRoom::new(contact, project_index, app_state.clone(), cx)
});
}
fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) { fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
let mut workspace = Workspace::new( let mut workspace = Workspace::new(
Project::local( Project::local(
false,
app_state.client.clone(), app_state.client.clone(),
app_state.user_store.clone(), app_state.user_store.clone(),
app_state.project_store.clone(), app_state.project_store.clone(),
@ -3236,7 +2991,7 @@ mod tests {
// When there are no dirty items, there's nothing to do. // When there are no dirty items, there's nothing to do.
let item1 = cx.add_view(&workspace, |_| TestItem::new()); let item1 = cx.add_view(&workspace, |_| TestItem::new());
workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx)); workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx)); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
assert!(task.await.unwrap()); assert!(task.await.unwrap());
// When there are dirty untitled items, prompt to save each one. If the user // When there are dirty untitled items, prompt to save each one. If the user
@ -3256,7 +3011,7 @@ mod tests {
w.add_item(Box::new(item2.clone()), cx); w.add_item(Box::new(item2.clone()), cx);
w.add_item(Box::new(item3.clone()), cx); w.add_item(Box::new(item3.clone()), cx);
}); });
let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx)); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
cx.simulate_prompt_answer(window_id, 2 /* cancel */); cx.simulate_prompt_answer(window_id, 2 /* cancel */);
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();

View File

@ -19,15 +19,15 @@ activity_indicator = { path = "../activity_indicator" }
assets = { path = "../assets" } assets = { path = "../assets" }
auto_update = { path = "../auto_update" } auto_update = { path = "../auto_update" }
breadcrumbs = { path = "../breadcrumbs" } breadcrumbs = { path = "../breadcrumbs" }
call = { path = "../call" }
chat_panel = { path = "../chat_panel" } chat_panel = { path = "../chat_panel" }
cli = { path = "../cli" } cli = { path = "../cli" }
collab_ui = { path = "../collab_ui" }
collections = { path = "../collections" } collections = { path = "../collections" }
command_palette = { path = "../command_palette" } command_palette = { path = "../command_palette" }
context_menu = { path = "../context_menu" } context_menu = { path = "../context_menu" }
client = { path = "../client" } client = { path = "../client" }
clock = { path = "../clock" } clock = { path = "../clock" }
contacts_panel = { path = "../contacts_panel" }
contacts_status_item = { path = "../contacts_status_item" }
diagnostics = { path = "../diagnostics" } diagnostics = { path = "../diagnostics" }
editor = { path = "../editor" } editor = { path = "../editor" }
file_finder = { path = "../file_finder" } file_finder = { path = "../file_finder" }
@ -105,17 +105,19 @@ tree-sitter-html = "0.19.0"
url = "2.2" url = "2.2"
[dev-dependencies] [dev-dependencies]
text = { path = "../text", features = ["test-support"] } call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] } language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] } project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] }
text = { path = "../text", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }
env_logger = "0.9" env_logger = "0.9"
serde_json = { version = "1.0", features = ["preserve_order"] } serde_json = { version = "1.0", features = ["preserve_order"] }
unindent = "0.1.7" unindent = "0.1.7"

View File

@ -112,7 +112,6 @@ fn main() {
go_to_line::init(cx); go_to_line::init(cx);
file_finder::init(cx); file_finder::init(cx);
chat_panel::init(cx); chat_panel::init(cx);
contacts_panel::init(cx);
outline::init(cx); outline::init(cx);
project_symbols::init(cx); project_symbols::init(cx);
project_panel::init(cx); project_panel::init(cx);
@ -138,11 +137,11 @@ fn main() {
}) })
.detach(); .detach();
let project_store = cx.add_model(|_| ProjectStore::new());
let db = cx.background().block(db); let db = cx.background().block(db);
client.start_telemetry(db.clone()); client.start_telemetry(db.clone());
client.report_event("start app", Default::default()); client.report_event("start app", Default::default());
let project_store = cx.add_model(|_| ProjectStore::new(db.clone()));
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
languages, languages,
themes, themes,
@ -159,6 +158,7 @@ fn main() {
journal::init(app_state.clone(), cx); journal::init(app_state.clone(), cx);
theme_selector::init(app_state.clone(), cx); theme_selector::init(app_state.clone(), cx);
zed::init(&app_state, cx); zed::init(&app_state, cx);
collab_ui::init(app_state.clone(), cx);
cx.set_menus(menus::menus()); cx.set_menus(menus::menus());

View File

@ -244,10 +244,6 @@ pub fn menus() -> Vec<Menu<'static>> {
name: "Project Panel", name: "Project Panel",
action: Box::new(project_panel::ToggleFocus), action: Box::new(project_panel::ToggleFocus),
}, },
MenuItem::Action {
name: "Contacts Panel",
action: Box::new(contacts_panel::ToggleFocus),
},
MenuItem::Action { MenuItem::Action {
name: "Command Palette", name: "Command Palette",
action: Box::new(command_palette::Toggle), action: Box::new(command_palette::Toggle),

View File

@ -10,9 +10,8 @@ use anyhow::{anyhow, Context, Result};
use assets::Assets; use assets::Assets;
use breadcrumbs::Breadcrumbs; use breadcrumbs::Breadcrumbs;
pub use client; pub use client;
use collab_ui::CollabTitlebarItem;
use collections::VecDeque; use collections::VecDeque;
pub use contacts_panel;
use contacts_panel::ContactsPanel;
pub use editor; pub use editor;
use editor::{Editor, MultiBuffer}; use editor::{Editor, MultiBuffer};
use gpui::{ use gpui::{
@ -214,15 +213,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx); workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
}, },
); );
cx.add_action(
|workspace: &mut Workspace,
_: &contacts_panel::ToggleFocus,
cx: &mut ViewContext<Workspace>| {
workspace.toggle_sidebar_item_focus(SidebarSide::Right, 0, cx);
},
);
activity_indicator::init(cx); activity_indicator::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
settings::KeymapFileContent::load_defaults(cx); settings::KeymapFileContent::load_defaults(cx);
} }
@ -231,7 +224,8 @@ pub fn initialize_workspace(
app_state: &Arc<AppState>, app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
cx.subscribe(&cx.handle(), { let workspace_handle = cx.handle();
cx.subscribe(&workspace_handle, {
move |_, _, event, cx| { move |_, _, event, cx| {
if let workspace::Event::PaneAdded(pane) = event { if let workspace::Event::PaneAdded(pane) = event {
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
@ -285,16 +279,11 @@ pub fn initialize_workspace(
})); }));
}); });
let project_panel = ProjectPanel::new(workspace.project().clone(), cx); let collab_titlebar_item =
let contact_panel = cx.add_view(|cx| { cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx));
ContactsPanel::new( workspace.set_titlebar_item(collab_titlebar_item, cx);
app_state.user_store.clone(),
app_state.project_store.clone(),
workspace.weak_handle(),
cx,
)
});
let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
workspace.left_sidebar().update(cx, |sidebar, cx| { workspace.left_sidebar().update(cx, |sidebar, cx| {
sidebar.add_item( sidebar.add_item(
"icons/folder_tree_16.svg", "icons/folder_tree_16.svg",
@ -303,14 +292,6 @@ pub fn initialize_workspace(
cx, cx,
) )
}); });
workspace.right_sidebar().update(cx, |sidebar, cx| {
sidebar.add_item(
"icons/user_group_16.svg",
"Contacts Panel".to_string(),
contact_panel,
cx,
)
});
let diagnostic_summary = let diagnostic_summary =
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx)); cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
@ -363,7 +344,9 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
// If the user cancels any save prompt, then keep the app open. // If the user cancels any save prompt, then keep the app open.
for workspace in workspaces { for workspace in workspaces {
if !workspace if !workspace
.update(&mut cx, |workspace, cx| workspace.prepare_to_close(cx)) .update(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx)
})
.await? .await?
{ {
return Ok(()); return Ok(());
@ -1772,6 +1755,7 @@ mod tests {
let state = Arc::get_mut(&mut app_state).unwrap(); let state = Arc::get_mut(&mut app_state).unwrap();
state.initialize_workspace = initialize_workspace; state.initialize_workspace = initialize_workspace;
state.build_window_options = build_window_options; state.build_window_options = build_window_options;
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
workspace::init(app_state.clone(), cx); workspace::init(app_state.clone(), cx);
editor::init(cx); editor::init(cx);
pane::init(cx); pane::init(cx);

View File

@ -2,7 +2,6 @@ import Theme from "../themes/common/theme";
import chatPanel from "./chatPanel"; import chatPanel from "./chatPanel";
import { text } from "./components"; import { text } from "./components";
import contactFinder from "./contactFinder"; import contactFinder from "./contactFinder";
import contactsPanel from "./contactsPanel";
import contactsPopover from "./contactsPopover"; import contactsPopover from "./contactsPopover";
import commandPalette from "./commandPalette"; import commandPalette from "./commandPalette";
import editor from "./editor"; import editor from "./editor";
@ -14,8 +13,11 @@ import contextMenu from "./contextMenu";
import projectDiagnostics from "./projectDiagnostics"; import projectDiagnostics from "./projectDiagnostics";
import contactNotification from "./contactNotification"; import contactNotification from "./contactNotification";
import updateNotification from "./updateNotification"; import updateNotification from "./updateNotification";
import projectSharedNotification from "./projectSharedNotification";
import tooltip from "./tooltip"; import tooltip from "./tooltip";
import terminal from "./terminal"; import terminal from "./terminal";
import contactList from "./contactList";
import incomingCallNotification from "./incomingCallNotification";
export const panel = { export const panel = {
padding: { top: 12, bottom: 12 }, padding: { top: 12, bottom: 12 },
@ -36,7 +38,7 @@ export default function app(theme: Theme): Object {
projectPanel: projectPanel(theme), projectPanel: projectPanel(theme),
chatPanel: chatPanel(theme), chatPanel: chatPanel(theme),
contactsPopover: contactsPopover(theme), contactsPopover: contactsPopover(theme),
contactsPanel: contactsPanel(theme), contactList: contactList(theme),
contactFinder: contactFinder(theme), contactFinder: contactFinder(theme),
search: search(theme), search: search(theme),
breadcrumbs: { breadcrumbs: {
@ -47,6 +49,8 @@ export default function app(theme: Theme): Object {
}, },
contactNotification: contactNotification(theme), contactNotification: contactNotification(theme),
updateNotification: updateNotification(theme), updateNotification: updateNotification(theme),
projectSharedNotification: projectSharedNotification(theme),
incomingCallNotification: incomingCallNotification(theme),
tooltip: tooltip(theme), tooltip: tooltip(theme),
terminal: terminal(theme), terminal: terminal(theme),
}; };

View File

@ -1,8 +1,9 @@
import Theme from "../themes/common/theme"; import Theme from "../themes/common/theme";
import picker from "./picker"; import picker from "./picker";
import { backgroundColor, iconColor } from "./components"; import { backgroundColor, border, iconColor, player, text } from "./components";
export default function contactFinder(theme: Theme) { export default function contactFinder(theme: Theme) {
const sideMargin = 6;
const contactButton = { const contactButton = {
background: backgroundColor(theme, 100), background: backgroundColor(theme, 100),
color: iconColor(theme, "primary"), color: iconColor(theme, "primary"),
@ -12,7 +13,31 @@ export default function contactFinder(theme: Theme) {
}; };
return { return {
...picker(theme), picker: {
item: {
...picker(theme).item,
margin: { left: sideMargin, right: sideMargin }
},
empty: picker(theme).empty,
inputEditor: {
background: backgroundColor(theme, 500),
cornerRadius: 6,
text: text(theme, "mono", "primary"),
placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
selection: player(theme, 1).selection,
border: border(theme, "secondary"),
padding: {
bottom: 4,
left: 8,
right: 8,
top: 4,
},
margin: {
left: sideMargin,
right: sideMargin,
}
}
},
rowHeight: 28, rowHeight: 28,
contactAvatar: { contactAvatar: {
cornerRadius: 10, cornerRadius: 10,

View File

@ -1,18 +1,17 @@
import Theme from "../themes/common/theme"; import Theme from "../themes/common/theme";
import { panel } from "./app"; import { backgroundColor, border, borderColor, iconColor, player, text } from "./components";
import {
backgroundColor,
border,
borderColor,
iconColor,
player,
text,
} from "./components";
export default function contactsPanel(theme: Theme) { export default function contactList(theme: Theme) {
const nameMargin = 8; const nameMargin = 8;
const sidePadding = 12; const sidePadding = 12;
const contactButton = {
background: backgroundColor(theme, 100),
color: iconColor(theme, "primary"),
iconWidth: 8,
buttonWidth: 16,
cornerRadius: 8,
};
const projectRow = { const projectRow = {
guestAvatarSpacing: 4, guestAvatarSpacing: 4,
height: 24, height: 24,
@ -39,17 +38,7 @@ export default function contactsPanel(theme: Theme) {
}, },
}; };
const contactButton = {
background: backgroundColor(theme, 100),
color: iconColor(theme, "primary"),
iconWidth: 8,
buttonWidth: 16,
cornerRadius: 8,
};
return { return {
...panel,
padding: { top: panel.padding.top, bottom: 0 },
userQueryEditor: { userQueryEditor: {
background: backgroundColor(theme, 500), background: backgroundColor(theme, 500),
cornerRadius: 6, cornerRadius: 6,
@ -64,28 +53,20 @@ export default function contactsPanel(theme: Theme) {
top: 4, top: 4,
}, },
margin: { margin: {
left: sidePadding, left: 6
right: sidePadding,
}, },
}, },
userQueryEditorHeight: 32, userQueryEditorHeight: 33,
addContactButton: { addContactButton: {
margin: { left: 6, right: 12 },
color: iconColor(theme, "primary"), color: iconColor(theme, "primary"),
buttonWidth: 16, buttonWidth: 28,
iconWidth: 16, iconWidth: 16,
}, },
privateButton: {
iconWidth: 12,
color: iconColor(theme, "primary"),
cornerRadius: 5,
buttonWidth: 12,
},
rowHeight: 28, rowHeight: 28,
sectionIconSize: 8, sectionIconSize: 8,
headerRow: { headerRow: {
...text(theme, "mono", "secondary", { size: "sm" }), ...text(theme, "mono", "secondary", { size: "sm" }),
margin: { top: 14 }, margin: { top: 6 },
padding: { padding: {
left: sidePadding, left: sidePadding,
right: sidePadding, right: sidePadding,
@ -95,6 +76,26 @@ export default function contactsPanel(theme: Theme) {
background: backgroundColor(theme, 100, "active"), background: backgroundColor(theme, 100, "active"),
}, },
}, },
leaveCall: {
background: backgroundColor(theme, 100),
border: border(theme, "secondary"),
cornerRadius: 6,
margin: {
top: 1,
},
padding: {
top: 1,
bottom: 1,
left: 7,
right: 7,
},
...text(theme, "sans", "secondary", { size: "xs" }),
hover: {
...text(theme, "sans", "active", { size: "xs" }),
background: backgroundColor(theme, "on300", "hovered"),
border: border(theme, "primary"),
},
},
contactRow: { contactRow: {
padding: { padding: {
left: sidePadding, left: sidePadding,
@ -104,20 +105,22 @@ export default function contactsPanel(theme: Theme) {
background: backgroundColor(theme, 100, "active"), background: backgroundColor(theme, 100, "active"),
}, },
}, },
treeBranch: {
color: borderColor(theme, "active"),
width: 1,
hover: {
color: borderColor(theme, "active"),
},
active: {
color: borderColor(theme, "active"),
},
},
contactAvatar: { contactAvatar: {
cornerRadius: 10, cornerRadius: 10,
width: 18, width: 18,
}, },
contactStatusFree: {
cornerRadius: 4,
padding: 4,
margin: { top: 12, left: 12 },
background: iconColor(theme, "ok"),
},
contactStatusBusy: {
cornerRadius: 4,
padding: 4,
margin: { top: 12, left: 12 },
background: iconColor(theme, "error"),
},
contactUsername: { contactUsername: {
...text(theme, "mono", "primary", { size: "sm" }), ...text(theme, "mono", "primary", { size: "sm" }),
margin: { margin: {
@ -136,6 +139,19 @@ export default function contactsPanel(theme: Theme) {
background: backgroundColor(theme, 100), background: backgroundColor(theme, 100),
color: iconColor(theme, "muted"), color: iconColor(theme, "muted"),
}, },
callingIndicator: {
...text(theme, "mono", "muted", { size: "xs" })
},
treeBranch: {
color: borderColor(theme, "active"),
width: 1,
hover: {
color: borderColor(theme, "active"),
},
active: {
color: borderColor(theme, "active"),
},
},
projectRow: { projectRow: {
...projectRow, ...projectRow,
background: backgroundColor(theme, 300), background: backgroundColor(theme, 300),
@ -150,16 +166,5 @@ export default function contactsPanel(theme: Theme) {
background: backgroundColor(theme, 300, "active"), background: backgroundColor(theme, 300, "active"),
}, },
}, },
inviteRow: { }
padding: {
left: sidePadding,
right: sidePadding,
},
border: { top: true, width: 1, color: borderColor(theme, "primary") },
text: text(theme, "sans", "secondary", { size: "sm" }),
hover: {
text: text(theme, "sans", "active", { size: "sm" }),
},
},
};
} }

View File

@ -1,8 +1,28 @@
import Theme from "../themes/common/theme"; import Theme from "../themes/common/theme";
import { backgroundColor } from "./components"; import { backgroundColor, border, borderColor, popoverShadow, text } from "./components";
export default function workspace(theme: Theme) { export default function contactsPopover(theme: Theme) {
const sidePadding = 12;
return { return {
background: backgroundColor(theme, 300), background: backgroundColor(theme, 300, "base"),
cornerRadius: 6,
padding: { top: 6 },
margin: { top: -6 },
shadow: popoverShadow(theme),
border: border(theme, "primary"),
width: 300,
height: 400,
inviteRowHeight: 28,
inviteRow: {
padding: {
left: sidePadding,
right: sidePadding,
},
border: { top: true, width: 1, color: borderColor(theme, "primary") },
text: text(theme, "sans", "secondary", { size: "sm" }),
hover: {
text: text(theme, "sans", "active", { size: "sm" }),
},
},
} }
} }

View 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" })
},
};
}

View 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" })
},
};
}

View File

@ -16,6 +16,27 @@ export function workspaceBackground(theme: Theme) {
export default function workspace(theme: Theme) { export default function workspace(theme: Theme) {
const titlebarPadding = 6; const titlebarPadding = 6;
const titlebarButton = {
background: backgroundColor(theme, 100),
border: border(theme, "secondary"),
cornerRadius: 6,
margin: {
top: 1,
},
padding: {
top: 1,
bottom: 1,
left: 7,
right: 7,
},
...text(theme, "sans", "secondary", { size: "xs" }),
hover: {
...text(theme, "sans", "active", { size: "xs" }),
background: backgroundColor(theme, "on300", "hovered"),
border: border(theme, "primary"),
},
};
const avatarWidth = 18;
return { return {
background: backgroundColor(theme, 300), background: backgroundColor(theme, 300),
@ -27,6 +48,14 @@ export default function workspace(theme: Theme) {
padding: 12, padding: 12,
...text(theme, "sans", "primary", { size: "lg" }), ...text(theme, "sans", "primary", { size: "lg" }),
}, },
externalLocationMessage: {
background: backgroundColor(theme, "info"),
border: border(theme, "secondary"),
cornerRadius: 6,
padding: 12,
margin: { bottom: 8, right: 8 },
...text(theme, "sans", "secondary", { size: "xs" }),
},
leaderBorderOpacity: 0.7, leaderBorderOpacity: 0.7,
leaderBorderWidth: 2.0, leaderBorderWidth: 2.0,
tabBar: tabBar(theme), tabBar: tabBar(theme),
@ -52,7 +81,7 @@ export default function workspace(theme: Theme) {
}, },
statusBar: statusBar(theme), statusBar: statusBar(theme),
titlebar: { titlebar: {
avatarWidth: 18, avatarWidth,
avatarMargin: 8, avatarMargin: 8,
height: 33, height: 33,
background: backgroundColor(theme, 100), background: backgroundColor(theme, 100),
@ -62,12 +91,20 @@ export default function workspace(theme: Theme) {
}, },
title: text(theme, "sans", "primary"), title: text(theme, "sans", "primary"),
avatar: { avatar: {
cornerRadius: 10, cornerRadius: avatarWidth / 2,
border: { border: {
color: "#00000088", color: "#00000088",
width: 1, width: 1,
}, },
}, },
inactiveAvatar: {
cornerRadius: avatarWidth / 2,
border: {
color: "#00000088",
width: 1,
},
grayscale: true,
},
avatarRibbon: { avatarRibbon: {
height: 3, height: 3,
width: 12, width: 12,
@ -76,24 +113,7 @@ export default function workspace(theme: Theme) {
}, },
border: border(theme, "primary", { bottom: true, overlay: true }), border: border(theme, "primary", { bottom: true, overlay: true }),
signInPrompt: { signInPrompt: {
background: backgroundColor(theme, 100), ...titlebarButton
border: border(theme, "secondary"),
cornerRadius: 6,
margin: {
top: 1,
},
padding: {
top: 1,
bottom: 1,
left: 7,
right: 7,
},
...text(theme, "sans", "secondary", { size: "xs" }),
hover: {
...text(theme, "sans", "active", { size: "xs" }),
background: backgroundColor(theme, "on300", "hovered"),
border: border(theme, "primary"),
},
}, },
offlineIcon: { offlineIcon: {
color: iconColor(theme, "secondary"), color: iconColor(theme, "secondary"),
@ -118,6 +138,30 @@ export default function workspace(theme: Theme) {
}, },
cornerRadius: 6, cornerRadius: 6,
}, },
toggleContactsButton: {
cornerRadius: 6,
color: iconColor(theme, "secondary"),
iconWidth: 8,
buttonWidth: 20,
active: {
background: backgroundColor(theme, "on300", "active"),
color: iconColor(theme, "active"),
},
hover: {
background: backgroundColor(theme, "on300", "hovered"),
color: iconColor(theme, "active"),
},
},
toggleContactsBadge: {
cornerRadius: 3,
padding: 2,
margin: { top: 3, left: 3 },
border: { width: 1, color: workspaceBackground(theme) },
background: iconColor(theme, "feature"),
},
shareButton: {
...titlebarButton
}
}, },
toolbar: { toolbar: {
height: 34, height: 34,