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:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-26 22:08:25 -04:00
1 parent 5bbfdcd766
commit 37e56c7ab7
14 files changed
+1308 -7

No files matched your search

+20
View File
@@ -8,6 +8,7 @@ use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, Wi
use jiff::{SignedDuration, Timestamp};
mod messages;
mod people;
use super::Page;
use crate::{
@@ -34,6 +35,22 @@ const SCRIPT: &[Step] = &[
Step::HoverAt("messaging-conversation", 200.0, 89.0),
Step::Wait(200),
Step::Shot("run-time"),
Step::DownAt("messaging-members", 200.0, 91.0),
Step::UpAt("messaging-members", 200.0, 91.0),
Step::Wait(200),
Step::Shot("member-chosen"),
Step::DownAt("messaging-status", 35.0, 48.0),
Step::UpAt("messaging-status", 35.0, 48.0),
Step::Wait(200),
Step::Key("tab"),
Step::Type("palm tree"),
Step::Wait(200),
Step::Shot("status-emoji"),
Step::Key("enter"),
Step::Key("tab"),
Step::Type("On leave till Monday"),
Step::Wait(200),
Step::Shot("status-draft"),
];
/// The demo's chats: which is open and what waits unread in each.
@@ -218,5 +235,8 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
.child(messages::conversation(window, cx))
.child(messages::thread(window, cx))
.child(messages::receipts(cx))
.child(people::members(window, cx))
.child(people::statuses(cx))
.child(people::setter(window, cx))
.into_any_element()
}
+204
View File
@@ -0,0 +1,204 @@
use ely_gpui_component::{
data_display::Presence,
messaging::{
ClearAfter, Member, MemberList, OnlineStatus, Status, StatusSetter, UserProfileCard,
},
theme::{ActiveTheme, Radius, TextSize},
};
use gpui::{App, ElementId, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use jiff::tz::TimeZone;
use crate::{
probe::probe,
ui::{change, keep, section},
};
/// A member's card: their title and the zone they work in.
struct Person {
member: Member,
title: &'static str,
zone: &'static str,
}
fn people() -> Vec<Person> {
let person = |member: Member, title, zone| Person {
member,
title,
zone,
};
vec![
person(
Member::new("ana", "Ana Lima", Presence::Away).status("🌴 Back on Monday"),
"Architect",
"Europe/Lisbon",
),
person(
Member::new("ben", "Ben Ito", Presence::Online).role("Admin"),
"Structural engineer",
"Asia/Tokyo",
),
person(
Member::new("chloe", "Chloé Martin", Presence::Busy).status("🎧 Drawing the stair"),
"Lead designer",
"Europe/Paris",
),
person(
Member::new("dev", "Dev Rao", Presence::Offline),
"Model maker",
"Asia/Kolkata",
),
person(
Member::new("eli", "Eli Park", Presence::Online).role("Owner"),
"Principal",
"America/New_York",
),
person(
Member::new("farah", "Farah Haddad", Presence::Offline),
"Landscape architect",
"Asia/Dubai",
),
]
}
pub fn members(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let chosen = keep(
"messaging-member",
|| SharedString::from("chloe"),
window,
cx,
);
let key = chosen.read(cx).clone();
let all = people();
let shown = all
.iter()
.find(|person| person.member.key == key)
.expect("the chosen member is listed");
let member = &shown.member;
let zone = TimeZone::get(shown.zone).expect("a known zone");
let card = UserProfileCard::new(
(ElementId::from("messaging-profile"), member.key.to_string()),
member.name.clone(),
member.presence,
)
.title(shown.title)
.zone(zone)
.on_message(|_, _| log::info!("gallery: message"))
.on_call(|_, _| log::info!("gallery: call"));
let card = match member.status.clone() {
Some(status) => card.status(status),
None => card,
};
let theme = cx.theme();
let frame = div()
.w(px(280.))
.p_4()
.border_1()
.border_color(theme.colors.border)
.rounded(theme.radius(Radius::Lg))
.child(card);
section(
"MemberList / UserProfileCard",
"Members online and offline by name, each group under its count; offline members go quiet. A press or an arrow shows a member's card: their standing, title and status, the time where they are, and ways to reach them.",
cx,
)
.child(probe(
"messaging-members",
div()
.flex()
.flex_wrap()
.items_start()
.gap_6()
.child(
div().w(px(300.)).child(
MemberList::new(
"messaging-members",
all.iter().map(|person| person.member.clone()),
)
.selected(key)
.on_select(move |key, _, cx| change(&chosen, cx, |chosen| *chosen = key.clone()))
.on_activate(|key, _, _| log::info!("gallery: message {key}")),
),
)
.child(frame),
))
}
pub fn statuses(cx: &mut App) -> impl IntoElement + use<> {
section(
"OnlineStatus",
"Where someone stands, as a dot in its color and its word.",
cx,
)
.child(probe(
"messaging-online",
div().flex().flex_wrap().gap_6().children(
[
Presence::Online,
Presence::Away,
Presence::Busy,
Presence::Offline,
]
.map(OnlineStatus::new),
),
))
}
fn suggestion(emoji: &str, text: &str, clear: ClearAfter) -> Status {
Status {
emoji: Some(SharedString::from(emoji.to_string())),
text: SharedString::from(text.to_string()),
clear,
}
}
pub fn setter(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let kept = keep("messaging-status", || None::<Status>, window, cx);
let status = kept.read(cx).clone();
let (saving, clearing) = (kept.clone(), kept);
let theme = cx.theme();
let now = match &status {
Some(status) => {
let words = match &status.emoji {
Some(emoji) => format!("{emoji} {}", status.text),
None => status.text.to_string(),
};
format!("{words} · {}", status.clear.label())
}
None => "No status set.".to_string(),
};
let setter = StatusSetter::new("messaging-status")
.suggestions([
suggestion("📅", "In a meeting", ClearAfter::OneHour),
suggestion("🚌", "Commuting", ClearAfter::ThirtyMinutes),
suggestion("🤒", "Out sick", ClearAfter::Today),
suggestion("🌴", "On leave", ClearAfter::Never),
])
.on_save(move |status, _, cx| {
let status = status.clone();
change(&saving, cx, |kept| *kept = Some(status))
})
.on_clear(move |_, cx| change(&clearing, cx, |kept| *kept = None));
let setter = match status {
Some(status) => setter.status(status),
None => setter,
};
let shown = div()
.text_size(theme.text_size(TextSize::Sm))
.text_color(theme.colors.fg_muted)
.child(now);
section(
"StatusSetter",
"An emoji, a few words, and when they clear. A suggestion fills all three; Save sets the status, and Clear status removes it.",
cx,
)
.child(probe(
"messaging-status",
div()
.w(px(380.))
.flex()
.flex_col()
.gap_4()
.child(shown)
.child(setter),
))
}