Messaging · People: members, a profile card, online status and a status setter
MemberList sets members online and offline by name, each group a list under its count. A press or an arrow chooses one; Enter or a double press acts on them; offline members go quiet. UserProfileCard shows a person's picture with presence, name and standing, title, status, the time in their zone, and Message and Call, each only with a handler. OnlineStatus draws a presence's dot and word; Presence now names itself and gives its color, in data_display, its one home. StatusSetter holds a draft seeded from the owner's status: an emoji from a picker field, words in its own text field, and when they clear. Suggestions fill all three; Save hands the status over and Clear status removes it. The emoji field reuses the date pickers' field, whose trigger ignores the Enter release after a pick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5bbfdcd766
commit
37e56c7ab7
14 files changed
+1308
-7
No files matched your search
@@ -10,6 +10,7 @@ use super::{
|
||||
use crate::{data_display::Presence, primitives::FocusNext, theme::Theme};
|
||||
|
||||
mod messages;
|
||||
mod people;
|
||||
|
||||
fn setup(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
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_as_they_stand(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"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user