CallControls holds the microphone, camera and a shared screen as toggles, then Leave. ParticipantTile shows a camera's frame from the host or the person's initials, their name with a mark when muted, and a ring while they speak. VideoCallGrid lays tiles in even rows as far as they fit across its measured width; a short last row centers at the full rows' width. ScreenShareView names who presents and offers Stop presenting for your own share, the call's tiles beside or below. VoiceCallBar tells where a call is, how long it has run and who is in it, with the microphone and Leave. IncomingCallDialog rings with the caller's picture; Accept or Decline answers each ring once, and Escape declines. HuddleIndicator rings while a huddle is live; a press joins. The recorder's device toggle and the camera's name chip now have one home in media. People: member names sort whatever their case, a status's words save trimmed, and a profile card's local time ticks.
228 lines
6.6 KiB
Rust
228 lines
6.6 KiB
Rust
use gpui::{
|
|
Context, IntoElement, ParentElement, Render, SharedString, Styled, TestAppContext, Window, div,
|
|
px,
|
|
};
|
|
|
|
use super::{press, settle, setup, tab_to};
|
|
use crate::{
|
|
data_display::Presence,
|
|
forms,
|
|
messaging::{ClearAfter, Member, MemberList, Status, StatusSetter, UserProfileCard},
|
|
};
|
|
|
|
/// Four members, and what the list asked.
|
|
struct Members {
|
|
asked: Vec<String>,
|
|
}
|
|
|
|
impl Render for Members {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let (select, activate) = (cx.entity(), cx.entity());
|
|
div().w(px(320.0)).child(
|
|
MemberList::new(
|
|
"members",
|
|
[
|
|
Member::new("dev", "Dev Rao", Presence::Offline),
|
|
Member::new("ben", "Ben Ito", Presence::Online).role("Admin"),
|
|
Member::new("ana", "Ana Lima", Presence::Away).status("🌴 Away till Monday"),
|
|
],
|
|
)
|
|
.on_select(move |key, _, cx| {
|
|
select.update(cx, |host, _| host.asked.push(format!("select {key}")))
|
|
})
|
|
.on_activate(move |key, _, cx| {
|
|
activate.update(cx, |host, _| host.asked.push(format!("activate {key}")))
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn an_arrow_selects_a_member_and_enter_acts_on_them(cx: &mut TestAppContext) {
|
|
setup(cx);
|
|
let (host, cx) = cx.add_window_view(|_, _| Members { asked: Vec::new() });
|
|
cx.update(|window, _| window.activate_window());
|
|
settle(cx);
|
|
tab_to(1, cx);
|
|
press("down", cx);
|
|
press("enter", cx);
|
|
tab_to(2, cx);
|
|
press("enter", cx);
|
|
assert_eq!(
|
|
host.read_with(cx, |host, _| host.asked.clone()),
|
|
["select ben", "activate ben", "activate dev"],
|
|
"online comes first by name, then offline"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "member ana twice")]
|
|
fn a_member_is_listed_once() {
|
|
let _ = MemberList::new(
|
|
"members",
|
|
[
|
|
Member::new("ana", "Ana", Presence::Online),
|
|
Member::new("ana", "Ana Lima", Presence::Away),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// A card with both ways to reach, and what it asked.
|
|
struct Card {
|
|
asked: Vec<&'static str>,
|
|
}
|
|
|
|
impl Render for Card {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let (message, call) = (cx.entity(), cx.entity());
|
|
div().w(px(300.0)).child(
|
|
UserProfileCard::new("card", "Ana Lima", Presence::Online)
|
|
.title("Architect")
|
|
.on_message(move |_, cx| message.update(cx, |host, _| host.asked.push("message")))
|
|
.on_call(move |_, cx| call.update(cx, |host, _| host.asked.push("call"))),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn the_cards_ways_to_reach_each_ask_their_own(cx: &mut TestAppContext) {
|
|
setup(cx);
|
|
let (host, cx) = cx.add_window_view(|_, _| Card { asked: Vec::new() });
|
|
cx.update(|window, _| window.activate_window());
|
|
settle(cx);
|
|
for nth in 1..=2 {
|
|
tab_to(nth, cx);
|
|
press("space", cx);
|
|
}
|
|
assert_eq!(
|
|
host.read_with(cx, |host, _| host.asked.clone()),
|
|
["message", "call"]
|
|
);
|
|
}
|
|
|
|
fn walk() -> Status {
|
|
Status {
|
|
emoji: Some("🚶".into()),
|
|
text: "On a walk".into(),
|
|
clear: ClearAfter::OneHour,
|
|
}
|
|
}
|
|
|
|
/// A status setter with one suggestion, the status the owner holds, and what was saved.
|
|
struct Setting {
|
|
status: Option<Status>,
|
|
saved: Vec<Status>,
|
|
cleared: usize,
|
|
}
|
|
|
|
impl Render for Setting {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let (save, clear) = (cx.entity(), cx.entity());
|
|
let setter = StatusSetter::new("status")
|
|
.suggestions([walk()])
|
|
.on_save(move |status, _, cx| {
|
|
save.update(cx, |host, _| host.saved.push(status.clone()))
|
|
})
|
|
.on_clear(move |_, cx| clear.update(cx, |host, _| host.cleared += 1));
|
|
let setter = match self.status.clone() {
|
|
Some(status) => setter.status(status),
|
|
None => setter,
|
|
};
|
|
div().w(px(360.0)).child(setter)
|
|
}
|
|
}
|
|
|
|
fn setting(
|
|
status: Option<Status>,
|
|
cx: &mut TestAppContext,
|
|
) -> (gpui::Entity<Setting>, &mut gpui::VisualTestContext) {
|
|
setup(cx);
|
|
cx.update(forms::bind_keys);
|
|
let (host, cx) = cx.add_window_view(|_, _| Setting {
|
|
status,
|
|
saved: Vec::new(),
|
|
cleared: 0,
|
|
});
|
|
cx.update(|window, _| window.activate_window());
|
|
settle(cx);
|
|
(host, cx)
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn a_suggestion_fills_the_status_and_save_hands_it_over(cx: &mut TestAppContext) {
|
|
let (host, cx) = setting(None, cx);
|
|
tab_to(3, cx);
|
|
press("space", cx);
|
|
tab_to(5, cx);
|
|
press("space", cx);
|
|
assert_eq!(host.read_with(cx, |host, _| host.saved.clone()), [walk()]);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn typed_words_save_trimmed(cx: &mut TestAppContext) {
|
|
let (host, cx) = setting(None, cx);
|
|
tab_to(2, cx);
|
|
cx.simulate_input(" Reading ");
|
|
settle(cx);
|
|
tab_to(5, cx);
|
|
press("space", cx);
|
|
let saved = Status {
|
|
emoji: None,
|
|
text: SharedString::from("Reading"),
|
|
clear: ClearAfter::Today,
|
|
};
|
|
assert_eq!(host.read_with(cx, |host, _| host.saved.clone()), [saved]);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn a_new_status_from_the_owner_starts_the_edits_over(cx: &mut TestAppContext) {
|
|
let (host, cx) = setting(None, cx);
|
|
tab_to(2, cx);
|
|
cx.simulate_input("Reading");
|
|
settle(cx);
|
|
host.update(cx, |host, cx| {
|
|
host.status = Some(walk());
|
|
cx.notify();
|
|
});
|
|
settle(cx);
|
|
tab_to(6, cx);
|
|
press("space", cx);
|
|
assert_eq!(host.read_with(cx, |host, _| host.saved.clone()), [walk()]);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn clear_status_asks_only_while_a_status_is_set(cx: &mut TestAppContext) {
|
|
let (host, cx) = setting(Some(walk()), cx);
|
|
tab_to(5, cx);
|
|
press("space", cx);
|
|
assert_eq!(host.read_with(cx, |host, _| host.cleared), 1);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn an_emoji_is_searched_and_picked_and_focus_comes_back(cx: &mut TestAppContext) {
|
|
let (host, cx) = setting(None, cx);
|
|
tab_to(1, cx);
|
|
press("enter", cx);
|
|
cx.update(|window, _| window.focus_next());
|
|
settle(cx);
|
|
cx.simulate_input("palm tree");
|
|
settle(cx);
|
|
press("enter", cx);
|
|
cx.update(|window, _| window.focus_next());
|
|
settle(cx);
|
|
cx.simulate_input("Away");
|
|
settle(cx);
|
|
tab_to(5, cx);
|
|
press("space", cx);
|
|
let saved = Status {
|
|
emoji: Some("🌴".into()),
|
|
text: "Away".into(),
|
|
clear: ClearAfter::Today,
|
|
};
|
|
assert_eq!(
|
|
host.read_with(cx, |host, _| host.saved.clone()),
|
|
[saved],
|
|
"the words follow the emoji field in Tab order"
|
|
);
|
|
}
|