Files
Ely-GPUI-Components/examples/gallery/pages/messaging/mod.rs
T
ZacharyZhang-NY dd5e36cd94 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.
2026-09-26 22:08:25 -04:00

243 lines
8.2 KiB
Rust

use ely_gpui_component::{
data_display::Presence,
messaging::{
ChannelHeader, ChannelItem, ChannelList, DirectMessageItem, PinnedMessage, PinnedMessages,
},
};
use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use jiff::{SignedDuration, Timestamp};
mod messages;
mod people;
use super::Page;
use crate::{
probe::probe,
script::Step,
ui::{change, keep, section},
};
pub const PAGE: Page = Page {
number: 28,
slug: "messaging",
title: "Messaging",
summary: "Channels and people: the lists and heads of chats, the messages in them, the people behind them, and calls.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::DownAt("messaging-channels", 120.0, 150.0),
Step::UpAt("messaging-channels", 120.0, 150.0),
Step::Key("down"),
Step::Wait(200),
Step::Shot("channel-opened"),
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.
#[derive(Clone)]
struct Chats {
open: SharedString,
unread: Vec<(SharedString, u32)>,
}
impl Chats {
fn unread(&self, key: &str) -> u32 {
self.unread
.iter()
.find(|(each, _)| each.as_ref() == key)
.map_or(0, |(_, count)| *count)
}
}
fn channels(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let chats = keep(
"messaging-chats",
|| Chats {
open: "design".into(),
unread: vec![
("atrium".into(), 4),
("general".into(), 12),
("random".into(), 31),
("ben".into(), 2),
],
},
window,
cx,
);
let now = chats.read(cx).clone();
let channel = |key: &'static str| ChannelItem::new(key, key).unread(now.unread(key));
let open = chats.clone();
section(
"ChannelList / ChatList",
"Channels and people in sections, each a list: a press, an arrow or Enter opens a chat and clears what waited. Unread names stand out; a muted channel keeps quiet.",
cx,
)
.child(probe(
"messaging-channels",
div().w(px(260.)).child(
ChannelList::new("messaging-channels")
.section("Starred", [("atrium", channel("atrium").pinned(true))])
.section(
"Channels",
[
("design", channel("design")),
("general", channel("general")),
("launch", channel("launch").private()),
("random", channel("random").muted(true)),
],
)
.section(
"Direct messages",
[
("ana", DirectMessageItem::new("ana", "Ana Lima", Presence::Online)),
("ben", DirectMessageItem::new("ben", "Ben Ito", Presence::Away).unread(now.unread("ben"))),
("chloe", DirectMessageItem::new("chloe", "Chloé Martin", Presence::Busy)),
("dev", DirectMessageItem::new("dev", "Dev Rao", Presence::Offline)),
],
)
.open(now.open)
.on_open(move |key, _, cx| {
change(&open, cx, |chats| {
chats.open = key.clone();
chats.unread.retain(|(each, _)| each != key);
})
}),
),
))
}
fn items(cx: &mut App) -> impl IntoElement + use<> {
section(
"ChannelItem / DirectMessageItem",
"A channel by its mark, a hash or a lock, with its unread count, quiet when muted, pinned with a pin; a person with their presence on their initials.",
cx,
)
.child(probe(
"messaging-items",
div()
.w(px(520.))
.flex()
.gap_6()
.child(
div()
.flex_1()
.min_w_0()
.flex()
.flex_col()
.child(ChannelItem::new("item-unread", "design").unread(3))
.child(ChannelItem::new("item-private", "launch").private())
.child(ChannelItem::new("item-muted", "random").unread(31).muted(true))
.child(ChannelItem::new("item-pinned", "atrium").pinned(true)),
)
.child(
div()
.flex_1()
.min_w_0()
.flex()
.flex_col()
.child(DirectMessageItem::new("dm-on", "Ana Lima", Presence::Online).unread(1))
.child(DirectMessageItem::new("dm-away", "Ben Ito", Presence::Away))
.child(DirectMessageItem::new("dm-busy", "Chloé Martin", Presence::Busy))
.child(DirectMessageItem::new("dm-off", "Dev Rao", Presence::Offline)),
),
))
}
fn header(cx: &mut App) -> impl IntoElement + use<> {
section(
"ChannelHeader",
"A channel's name and topic, and beside them its members, its pins and a call; the actions drop below on a narrow window.",
cx,
)
.child(probe(
"messaging-header",
div().w(px(640.)).child(
ChannelHeader::new("messaging-header", "design")
.topic("Where the atrium takes its shape: drawings, samples and the week's reviews")
.members(12, |_, _| log::info!("gallery: members"))
.pins(3, |_, _| log::info!("gallery: pins"))
.on_call(|_, _| log::info!("gallery: call")),
),
))
}
fn pins(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let now = Timestamp::now();
let pin = |key: &str, author: &str, hours: i64, text: &str| PinnedMessage {
key: SharedString::from(key.to_string()),
author: SharedString::from(author.to_string()),
at: now - SignedDuration::from_hours(hours),
text: SharedString::from(text.to_string()),
};
let all = vec![
pin(
"review",
"Ana Lima",
3,
"Friday's review moves to 10:00, in the model room.",
),
pin(
"samples",
"Ben Ito",
30,
"Plaster samples are in the stair hall. Please leave notes on the backs.",
),
pin(
"brief",
"Chloé Martin",
200,
"The brief, final: light from above, one stair, olive trees by the windows.",
),
];
let kept = keep("messaging-pins", move || all, window, cx);
let shown = kept.read(cx).clone();
section(
"PinnedMessages",
"Messages pinned in a chat, newest first: a press, an arrow or Enter opens one where it stands; Unpin lets one go.",
cx,
)
.child(probe(
"messaging-pins",
div().w(px(420.)).child(
PinnedMessages::new("messaging-pins", shown)
.on_open(|key, _, _| log::info!("gallery: open {key}"))
.on_unpin(move |key, _, cx| change(&kept, cx, |all| all.retain(|pin| pin.key != *key))),
),
))
}
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(channels(window, cx))
.child(items(cx))
.child(header(cx))
.child(pins(window, cx))
.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()
}