Messaging · Channels: the chat list, its rows, a channel's head and its pins

ChannelList sets chats down in sections, each a list: a press, an arrow
or Enter opens one, and the open chat is lit. A title names one section,
and a section holds a chat. ChannelItem shows a hash or a lock, its
unread count, a pin when pinned, and goes quiet when muted.
DirectMessageItem shows a person's initials with their presence. Unread
names stand out through ListItem's new strong; muted channels and
offline people go quiet through its quiet. ChannelHeader holds the name
and topic beside members, pins and a call, wrapping by the header rule.
PinnedMessages lists pins newest first; a press, an arrow or Enter opens
one, and Unpin lets one go. With none it shows a line and takes no Tab
stop.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 21:30:17 -04:00
1 parent 85df47fd5c
commit cd14b32dfe
11 files changed
+1188 -17

No files matched your search

+214
View File
@@ -0,0 +1,214 @@
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};
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"),
];
/// 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))
.into_any_element()
}
+2
View File
@@ -17,6 +17,7 @@ mod layout;
mod lists;
mod media;
mod menus;
mod messaging;
mod motion;
mod navigation;
mod overlays;
@@ -69,6 +70,7 @@ pub const ALL: &[Page] = &[
generative::PAGE,
media::PAGE,
files::PAGE,
messaging::PAGE,
theme::PAGE,
];