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
@@ -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()
|
||||
}
|
||||
@@ -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),
|
||||
))
|
||||
}
|
||||
Reference in new issue
Block a user