WIP: Render active call in contacts popover

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2022-10-07 17:01:48 +02:00
parent 96c5bb8c39
commit f9fb3f78b2
8 changed files with 198 additions and 82 deletions

View file

@ -4,7 +4,7 @@ use crate::{
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
use collections::{HashMap, HashSet}; use collections::{BTreeMap, HashSet};
use futures::StreamExt; use futures::StreamExt;
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
use project::Project; use project::Project;
@ -19,8 +19,9 @@ pub enum Event {
pub struct Room { pub struct Room {
id: u64, id: u64,
status: RoomStatus, status: RoomStatus,
remote_participants: HashMap<PeerId, RemoteParticipant>, remote_participants: BTreeMap<PeerId, RemoteParticipant>,
pending_users: Vec<Arc<User>>, pending_participants: Vec<Arc<User>>,
participant_user_ids: HashSet<u64>,
pending_call_count: usize, pending_call_count: usize,
leave_when_empty: bool, leave_when_empty: bool,
client: Arc<Client>, client: Arc<Client>,
@ -62,8 +63,9 @@ impl Room {
Self { Self {
id, id,
status: RoomStatus::Online, status: RoomStatus::Online,
participant_user_ids: Default::default(),
remote_participants: Default::default(), remote_participants: Default::default(),
pending_users: Default::default(), pending_participants: Default::default(),
pending_call_count: 0, pending_call_count: 0,
subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)],
leave_when_empty: false, leave_when_empty: false,
@ -131,7 +133,7 @@ impl Room {
fn should_leave(&self) -> bool { fn should_leave(&self) -> bool {
self.leave_when_empty self.leave_when_empty
&& self.pending_room_update.is_none() && self.pending_room_update.is_none()
&& self.pending_users.is_empty() && self.pending_participants.is_empty()
&& self.remote_participants.is_empty() && self.remote_participants.is_empty()
&& self.pending_call_count == 0 && self.pending_call_count == 0
} }
@ -144,6 +146,8 @@ impl Room {
cx.notify(); cx.notify();
self.status = RoomStatus::Offline; self.status = RoomStatus::Offline;
self.remote_participants.clear(); self.remote_participants.clear();
self.pending_participants.clear();
self.participant_user_ids.clear();
self.subscriptions.clear(); self.subscriptions.clear();
self.client.send(proto::LeaveRoom { id: self.id })?; self.client.send(proto::LeaveRoom { id: self.id })?;
Ok(()) Ok(())
@ -157,12 +161,16 @@ impl Room {
self.status self.status
} }
pub fn remote_participants(&self) -> &HashMap<PeerId, RemoteParticipant> { pub fn remote_participants(&self) -> &BTreeMap<PeerId, RemoteParticipant> {
&self.remote_participants &self.remote_participants
} }
pub fn pending_users(&self) -> &[Arc<User>] { pub fn pending_participants(&self) -> &[Arc<User>] {
&self.pending_users &self.pending_participants
}
pub fn contains_participant(&self, user_id: u64) -> bool {
self.participant_user_ids.contains(&user_id)
} }
async fn handle_room_updated( async fn handle_room_updated(
@ -187,27 +195,29 @@ impl Room {
room.participants room.participants
.retain(|participant| Some(participant.user_id) != self.client.user_id()); .retain(|participant| Some(participant.user_id) != self.client.user_id());
let participant_user_ids = room let remote_participant_user_ids = room
.participants .participants
.iter() .iter()
.map(|p| p.user_id) .map(|p| p.user_id)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let (participants, pending_users) = self.user_store.update(cx, move |user_store, cx| { let (remote_participants, pending_participants) =
( self.user_store.update(cx, move |user_store, cx| {
user_store.get_users(participant_user_ids, cx), (
user_store.get_users(room.pending_user_ids, 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 { self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
let (participants, pending_users) = futures::join!(participants, pending_users); let (remote_participants, pending_participants) =
futures::join!(remote_participants, pending_participants);
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
if let Some(participants) = participants.log_err() { this.participant_user_ids.clear();
let mut seen_participants = HashSet::default();
if let Some(participants) = remote_participants.log_err() {
for (participant, user) in room.participants.into_iter().zip(participants) { for (participant, user) in room.participants.into_iter().zip(participants) {
let peer_id = PeerId(participant.peer_id); let peer_id = PeerId(participant.peer_id);
seen_participants.insert(peer_id); this.participant_user_ids.insert(participant.user_id);
let existing_project_ids = this let existing_project_ids = this
.remote_participants .remote_participants
@ -234,19 +244,18 @@ impl Room {
); );
} }
for participant_peer_id in this.remote_participants.retain(|_, participant| {
this.remote_participants.keys().copied().collect::<Vec<_>>() this.participant_user_ids.contains(&participant.user.id)
{ });
if !seen_participants.contains(&participant_peer_id) {
this.remote_participants.remove(&participant_peer_id);
}
}
cx.notify(); cx.notify();
} }
if let Some(pending_users) = pending_users.log_err() { if let Some(pending_participants) = pending_participants.log_err() {
this.pending_users = pending_users; this.pending_participants = pending_participants;
for participant in &this.pending_participants {
this.participant_user_ids.insert(participant.id);
}
cx.notify(); cx.notify();
} }
@ -254,6 +263,8 @@ impl Room {
if this.should_leave() { if this.should_leave() {
let _ = this.leave(cx); let _ = this.leave(cx);
} }
this.check_invariants();
}); });
})); }));
@ -261,6 +272,24 @@ impl Room {
Ok(()) 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( pub(crate) fn call(
&mut self, &mut self,
recipient_user_id: u64, recipient_user_id: u64,

View file

@ -6475,7 +6475,7 @@ fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomP
.map(|(_, participant)| participant.user.github_login.clone()) .map(|(_, participant)| participant.user.github_login.clone())
.collect(), .collect(),
pending: room pending: room
.pending_users() .pending_participants()
.iter() .iter()
.map(|user| user.github_login.clone()) .map(|user| user.github_login.clone())
.collect(), .collect(),

View file

@ -229,7 +229,7 @@ impl Store {
.retain(|participant| participant.peer_id != connection_id.0); .retain(|participant| participant.peer_id != connection_id.0);
if prev_participant_count == room.participants.len() { if prev_participant_count == room.participants.len() {
if connected_user.connection_ids.is_empty() { if connected_user.connection_ids.is_empty() {
room.pending_user_ids room.pending_participant_user_ids
.retain(|pending_user_id| *pending_user_id != user_id.to_proto()); .retain(|pending_user_id| *pending_user_id != user_id.to_proto());
result.room_id = Some(room_id); result.room_id = Some(room_id);
connected_user.active_call = None; connected_user.active_call = None;
@ -239,7 +239,7 @@ impl Store {
connected_user.active_call = None; connected_user.active_call = None;
} }
if room.participants.is_empty() && room.pending_user_ids.is_empty() { if room.participants.is_empty() && room.pending_participant_user_ids.is_empty() {
self.rooms.remove(&room_id); self.rooms.remove(&room_id);
} }
} else { } else {
@ -432,10 +432,11 @@ impl Store {
.get_mut(&room_id) .get_mut(&room_id)
.ok_or_else(|| anyhow!("no such room"))?; .ok_or_else(|| anyhow!("no such room"))?;
anyhow::ensure!( anyhow::ensure!(
room.pending_user_ids.contains(&user_id.to_proto()), room.pending_participant_user_ids
.contains(&user_id.to_proto()),
anyhow!("no such room") anyhow!("no such room")
); );
room.pending_user_ids room.pending_participant_user_ids
.retain(|pending| *pending != user_id.to_proto()); .retain(|pending| *pending != user_id.to_proto());
room.participants.push(proto::Participant { room.participants.push(proto::Participant {
user_id: user_id.to_proto(), user_id: user_id.to_proto(),
@ -490,7 +491,7 @@ impl Store {
.ok_or_else(|| anyhow!("no such room"))?; .ok_or_else(|| anyhow!("no such room"))?;
room.participants room.participants
.retain(|participant| participant.peer_id != connection_id.0); .retain(|participant| participant.peer_id != connection_id.0);
if room.participants.is_empty() && room.pending_user_ids.is_empty() { if room.participants.is_empty() && room.pending_participant_user_ids.is_empty() {
self.rooms.remove(&room_id); self.rooms.remove(&room_id);
} }
@ -537,12 +538,13 @@ impl Store {
"no such room" "no such room"
); );
anyhow::ensure!( anyhow::ensure!(
room.pending_user_ids room.pending_participant_user_ids
.iter() .iter()
.all(|user_id| UserId::from_proto(*user_id) != recipient_user_id), .all(|user_id| UserId::from_proto(*user_id) != recipient_user_id),
"cannot call the same user more than once" "cannot call the same user more than once"
); );
room.pending_user_ids.push(recipient_user_id.to_proto()); room.pending_participant_user_ids
.push(recipient_user_id.to_proto());
if let Some(initial_project_id) = initial_project_id { if let Some(initial_project_id) = initial_project_id {
let project = self let project = self
@ -589,7 +591,7 @@ impl Store {
.rooms .rooms
.get_mut(&room_id) .get_mut(&room_id)
.ok_or_else(|| anyhow!("no such room"))?; .ok_or_else(|| anyhow!("no such room"))?;
room.pending_user_ids room.pending_participant_user_ids
.retain(|user_id| UserId::from_proto(*user_id) != to_user_id); .retain(|user_id| UserId::from_proto(*user_id) != to_user_id);
Ok(room) Ok(room)
} }
@ -635,7 +637,7 @@ impl Store {
.rooms .rooms
.get_mut(&room_id) .get_mut(&room_id)
.ok_or_else(|| anyhow!("no such room"))?; .ok_or_else(|| anyhow!("no such room"))?;
room.pending_user_ids room.pending_participant_user_ids
.retain(|user_id| UserId::from_proto(*user_id) != recipient_user_id); .retain(|user_id| UserId::from_proto(*user_id) != recipient_user_id);
let recipient = self.connected_users.get_mut(&recipient_user_id).unwrap(); let recipient = self.connected_users.get_mut(&recipient_user_id).unwrap();
@ -663,7 +665,7 @@ impl Store {
.rooms .rooms
.get_mut(&active_call.room_id) .get_mut(&active_call.room_id)
.ok_or_else(|| anyhow!("no such room"))?; .ok_or_else(|| anyhow!("no such room"))?;
room.pending_user_ids room.pending_participant_user_ids
.retain(|user_id| UserId::from_proto(*user_id) != recipient_user_id); .retain(|user_id| UserId::from_proto(*user_id) != recipient_user_id);
Ok((room, recipient_connection_ids)) Ok((room, recipient_connection_ids))
} else { } else {
@ -1115,7 +1117,7 @@ impl Store {
} }
for (room_id, room) in &self.rooms { for (room_id, room) in &self.rooms {
for pending_user_id in &room.pending_user_ids { for pending_user_id in &room.pending_participant_user_ids {
assert!( assert!(
self.connected_users self.connected_users
.contains_key(&UserId::from_proto(*pending_user_id)), .contains_key(&UserId::from_proto(*pending_user_id)),
@ -1140,7 +1142,7 @@ impl Store {
} }
assert!( assert!(
!room.pending_user_ids.is_empty() || !room.participants.is_empty(), !room.pending_participant_user_ids.is_empty() || !room.participants.is_empty(),
"room can't be empty" "room can't be empty"
); );
} }

View file

@ -2,7 +2,7 @@ use std::sync::Arc;
use crate::contact_finder; use crate::contact_finder;
use call::ActiveCall; use call::ActiveCall;
use client::{Contact, User, UserStore}; use client::{Contact, PeerId, User, UserStore};
use editor::{Cancel, Editor}; use editor::{Cancel, Editor};
use fuzzy::{match_strings, StringMatchCandidate}; use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{ use gpui::{
@ -41,6 +41,7 @@ struct Call {
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
enum Section { enum Section {
ActiveCall,
Requests, Requests,
Online, Online,
Offline, Offline,
@ -49,6 +50,7 @@ enum Section {
#[derive(Clone)] #[derive(Clone)]
enum ContactEntry { enum ContactEntry {
Header(Section), Header(Section),
CallParticipant { user: Arc<User>, is_pending: bool },
IncomingRequest(Arc<User>), IncomingRequest(Arc<User>),
OutgoingRequest(Arc<User>), OutgoingRequest(Arc<User>),
Contact(Arc<Contact>), Contact(Arc<Contact>),
@ -62,6 +64,11 @@ impl PartialEq for ContactEntry {
return section_1 == section_2; return section_1 == section_2;
} }
} }
ContactEntry::CallParticipant { user: user_1, .. } => {
if let ContactEntry::CallParticipant { user: user_2, .. } = other {
return user_1.id == user_2.id;
}
}
ContactEntry::IncomingRequest(user_1) => { ContactEntry::IncomingRequest(user_1) => {
if let ContactEntry::IncomingRequest(user_2) = other { if let ContactEntry::IncomingRequest(user_2) = other {
return user_1.id == user_2.id; return user_1.id == user_2.id;
@ -157,6 +164,9 @@ impl ContactsPopover {
cx, cx,
) )
} }
ContactEntry::CallParticipant { user, is_pending } => {
Self::render_call_participant(user, *is_pending, &theme.contacts_popover)
}
ContactEntry::IncomingRequest(user) => Self::render_contact_request( ContactEntry::IncomingRequest(user) => Self::render_contact_request(
user.clone(), user.clone(),
this.user_store.clone(), this.user_store.clone(),
@ -186,7 +196,7 @@ impl ContactsPopover {
let active_call = ActiveCall::global(cx); let active_call = ActiveCall::global(cx);
let mut subscriptions = Vec::new(); let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx))); subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx)));
subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify())); subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx)));
let mut this = Self { let mut this = Self {
list_state, list_state,
@ -291,6 +301,66 @@ impl ContactsPopover {
let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
self.entries.clear(); self.entries.clear();
if let Some(room) = ActiveCall::global(cx).read(cx).room() {
let room = room.read(cx);
self.entries.push(ContactEntry::Header(Section::ActiveCall));
if !self.collapsed_sections.contains(&Section::ActiveCall) {
// Populate remote participants.
self.match_candidates.clear();
self.match_candidates
.extend(
room.remote_participants()
.iter()
.map(|(peer_id, participant)| StringMatchCandidate {
id: peer_id.0 as usize,
string: participant.user.github_login.clone(),
char_bag: participant.user.github_login.chars().collect(),
}),
);
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
true,
usize::MAX,
&Default::default(),
executor.clone(),
));
self.entries.extend(matches.iter().map(|mat| {
ContactEntry::CallParticipant {
user: room.remote_participants()[&PeerId(mat.candidate_id as u32)]
.user
.clone(),
is_pending: false,
}
}));
// Populate pending participants.
self.match_candidates.clear();
self.match_candidates
.extend(room.pending_participants().iter().enumerate().map(
|(id, participant)| StringMatchCandidate {
id,
string: participant.github_login.clone(),
char_bag: participant.github_login.chars().collect(),
},
));
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
true,
usize::MAX,
&Default::default(),
executor.clone(),
));
self.entries
.extend(matches.iter().map(|mat| ContactEntry::CallParticipant {
user: room.pending_participants()[mat.candidate_id].clone(),
is_pending: true,
}));
}
}
let mut request_entries = Vec::new(); let mut request_entries = Vec::new();
let incoming = user_store.incoming_contact_requests(); let incoming = user_store.incoming_contact_requests();
if !incoming.is_empty() { if !incoming.is_empty() {
@ -359,7 +429,6 @@ impl ContactsPopover {
let contacts = user_store.contacts(); let contacts = user_store.contacts();
if !contacts.is_empty() { if !contacts.is_empty() {
// Always put the current user first.
self.match_candidates.clear(); self.match_candidates.clear();
self.match_candidates self.match_candidates
.extend( .extend(
@ -382,9 +451,16 @@ impl ContactsPopover {
executor.clone(), executor.clone(),
)); ));
let (online_contacts, offline_contacts) = matches let (mut online_contacts, offline_contacts) = matches
.iter() .iter()
.partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online); .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
if let Some(room) = ActiveCall::global(cx).read(cx).room() {
let room = room.read(cx);
online_contacts.retain(|contact| {
let contact = &contacts[contact.candidate_id];
!room.contains_participant(contact.user.id)
});
}
for (matches, section) in [ for (matches, section) in [
(online_contacts, Section::Online), (online_contacts, Section::Online),
@ -416,41 +492,46 @@ impl ContactsPopover {
cx.notify(); cx.notify();
} }
fn render_active_call(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> { fn render_call_participant(
let room = ActiveCall::global(cx).read(cx).room()?; user: &User,
let theme = &cx.global::<Settings>().theme.contacts_popover; is_pending: bool,
theme: &theme::ContactsPopover,
Some( ) -> ElementBox {
Flex::column() Flex::row()
.with_children(room.read(cx).pending_users().iter().map(|user| { .with_children(user.avatar.clone().map(|avatar| {
Flex::row() Image::new(avatar)
.with_children(user.avatar.clone().map(|avatar| { .with_style(theme.contact_avatar)
Image::new(avatar) .aligned()
.with_style(theme.contact_avatar) .left()
.aligned() .boxed()
.left() }))
.boxed() .with_child(
})) Label::new(
.with_child( user.github_login.clone(),
Label::new( theme.contact_username.text.clone(),
user.github_login.clone(), )
theme.contact_username.text.clone(), .contained()
) .with_style(theme.contact_username.container)
.contained()
.with_style(theme.contact_username.container)
.aligned()
.left()
.flex(1., true)
.boxed(),
)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(theme.contact_row.default)
.boxed()
}))
.boxed(), .boxed(),
) )
.with_children(if is_pending {
Some(
Label::new(
"Calling...".to_string(),
theme.calling_indicator.text.clone(),
)
.contained()
.with_style(theme.calling_indicator.container)
.aligned()
.flex_float()
.boxed(),
)
} else {
None
})
.constrained()
.with_height(theme.row_height)
.boxed()
} }
fn render_header( fn render_header(
@ -464,6 +545,7 @@ impl ContactsPopover {
let header_style = theme.header_row.style_for(Default::default(), is_selected); let header_style = theme.header_row.style_for(Default::default(), is_selected);
let text = match section { let text = match section {
Section::ActiveCall => "Call",
Section::Requests => "Requests", Section::Requests => "Requests",
Section::Online => "Online", Section::Online => "Online",
Section::Offline => "Offline", Section::Offline => "Offline",
@ -751,7 +833,6 @@ impl View for ContactsPopover {
.with_height(theme.contacts_popover.user_query_editor_height) .with_height(theme.contacts_popover.user_query_editor_height)
.boxed(), .boxed(),
) )
.with_children(self.render_active_call(cx))
.with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) .with_child(List::new(self.list_state.clone()).flex(1., false).boxed())
.with_children( .with_children(
self.user_store self.user_store

View file

@ -154,7 +154,7 @@ message LeaveRoom {
message Room { message Room {
repeated Participant participants = 1; repeated Participant participants = 1;
repeated uint64 pending_user_ids = 2; repeated uint64 pending_participant_user_ids = 2;
} }
message Participant { message Participant {

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 {

View file

@ -105,6 +105,7 @@ pub struct ContactsPopover {
pub private_button: Interactive<IconButton>, pub private_button: Interactive<IconButton>,
pub section_icon_size: f32, pub section_icon_size: f32,
pub invite_row: Interactive<ContainedLabel>, pub invite_row: Interactive<ContainedLabel>,
pub calling_indicator: ContainedText,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default)]

View file

@ -171,5 +171,8 @@ export default function contactsPopover(theme: Theme) {
text: text(theme, "sans", "active", { size: "sm" }), text: text(theme, "sans", "active", { size: "sm" }),
}, },
}, },
callingIndicator: {
...text(theme, "mono", "primary", { size: "xs" }),
}
} }
} }