Skip to main content

ui/components/
feed.rs

1use gpui::AnyElement;
2
3use crate::Avatar;
4use crate::prelude::*;
5
6/// A vertical activity timeline: a left connecting line with avatar + text +
7/// timestamp entries (Tailwind "Feed").
8#[derive(IntoElement, RegisterComponent)]
9pub struct Feed {
10    entries: Vec<(AnyElement, SharedString, SharedString)>,
11}
12
13impl Feed {
14    pub fn new() -> Self {
15        Self {
16            entries: Vec::new(),
17        }
18    }
19
20    pub fn entry(
21        mut self,
22        avatar: impl IntoElement,
23        content: impl Into<SharedString>,
24        timestamp: impl Into<SharedString>,
25    ) -> Self {
26        self.entries
27            .push((avatar.into_any_element(), content.into(), timestamp.into()));
28        self
29    }
30}
31
32impl Default for Feed {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl RenderOnce for Feed {
39    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
40        v_flex()
41            .w_full()
42            .border_l_1()
43            .border_color(semantic::border_muted(cx))
44            .pl_4()
45            .children(
46                self.entries
47                    .into_iter()
48                    .map(|(avatar, content, timestamp)| {
49                        h_flex().items_start().gap_3().mb_4().child(avatar).child(
50                            v_flex().gap_0p5().child(Label::new(content)).child(
51                                Label::new(timestamp)
52                                    .size(LabelSize::XSmall)
53                                    .color(Color::Muted),
54                            ),
55                        )
56                    }),
57            )
58    }
59}
60
61impl Component for Feed {
62    fn scope() -> ComponentScope {
63        ComponentScope::DataDisplay
64    }
65
66    fn description() -> Option<&'static str> {
67        Some("A vertical activity timeline with a connecting line and avatar/text entries.")
68    }
69
70    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
71        let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4";
72
73        Some(
74            v_flex()
75                .gap_6()
76                .child(
77                    Feed::new()
78                        .entry(
79                            Avatar::new(example_avatar).size(px(32.)),
80                            "Jane Cooper created the project",
81                            "1h ago",
82                        )
83                        .entry(
84                            Avatar::new(example_avatar).size(px(32.)),
85                            "Cody Fisher commented on an issue",
86                            "3h ago",
87                        )
88                        .entry(
89                            Avatar::new(example_avatar).size(px(32.)),
90                            "Jenny Wilson closed the milestone",
91                            "1d ago",
92                        ),
93                )
94                .into_any_element(),
95        )
96    }
97}