Files
Ely-GPUI-Components/examples/gallery/pages/messaging/mod.rs
T
ZacharyZhang-NYandClaude Opus 5.5 5bbfdcd766 Messaging · Messages: messages in runs, threads, the unread line and receipts
ChatMessage sets a message under its author's picture, name and clock
time. The next by the same author within five minutes, on the same day,
joins the run: only its words, the time in the gutter on hover.
UnreadDivider marks where unread messages start. MessageThread tells who
replied, how many and when the last came; with a handler it opens on a
press, Enter or Space. ThreadPanel holds the first message, a rule
counting the replies, the replies and a composer, its body scrolling.
ReadReceipt says sending, sent, delivered or read, and who saw it in a
group. MessageReactions and TypingIndicator point to collab and motion.

gpui recolors nothing on hover: text takes its color at layout. The
gutter time rests invisible and turns visible through a group hover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:48:17 -04:00

223 lines
7.5 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;
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"),
];
/// 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))
.into_any_element()
}