1use std::sync::Arc;
2
3use client::User;
4use gpui::{hsla, AnyElement, ClickEvent};
5use ui::{prelude::*, Avatar, Tooltip};
6
7use crate::MessageId;
8
9pub enum UserOrAssistant {
10 User(Option<Arc<User>>),
11 Assistant,
12}
13
14#[derive(IntoElement)]
15pub struct ChatMessage {
16 id: MessageId,
17 player: UserOrAssistant,
18 messages: Vec<AnyElement>,
19 selected: bool,
20 collapsed: bool,
21 on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
22}
23
24impl ChatMessage {
25 pub fn new(
26 id: MessageId,
27 player: UserOrAssistant,
28 messages: Vec<AnyElement>,
29 collapsed: bool,
30 on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
31 ) -> Self {
32 Self {
33 id,
34 player,
35 messages,
36 selected: false,
37 collapsed,
38 on_collapse_handle_click,
39 }
40 }
41}
42
43impl Selectable for ChatMessage {
44 fn selected(mut self, selected: bool) -> Self {
45 self.selected = selected;
46 self
47 }
48}
49
50impl RenderOnce for ChatMessage {
51 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
52 let message_group = SharedString::from(format!("{}_group", self.id.0));
53
54 let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
55
56 let content_padding = Spacing::Small.rems(cx);
57 // Clamp the message height to exactly 1.5 lines when collapsed.
58 let collapsed_height = content_padding.to_pixels(cx.rem_size()) + cx.line_height() * 1.5;
59
60 let background_color = if let UserOrAssistant::User(_) = &self.player {
61 Some(cx.theme().colors().surface_background)
62 } else {
63 None
64 };
65
66 let (username, avatar_uri) = match self.player {
67 UserOrAssistant::Assistant => (
68 "Assistant".into(),
69 Some("https://zed.dev/assistant_avatar.png".into()),
70 ),
71 UserOrAssistant::User(Some(user)) => {
72 (user.github_login.clone(), Some(user.avatar_uri.clone()))
73 }
74 UserOrAssistant::User(None) => ("You".into(), None),
75 };
76
77 v_flex()
78 .group(message_group.clone())
79 .gap(Spacing::XSmall.rems(cx))
80 .p(Spacing::XSmall.rems(cx))
81 .when(self.selected, |element| {
82 element.bg(hsla(0.6, 0.67, 0.46, 0.12))
83 })
84 .rounded_lg()
85 .child(
86 h_flex()
87 .justify_between()
88 .px(content_padding)
89 .child(
90 h_flex()
91 .gap_2()
92 .map(|this| {
93 let avatar_size = rems_from_px(20.);
94 if let Some(avatar_uri) = avatar_uri {
95 this.child(Avatar::new(avatar_uri).size(avatar_size))
96 } else {
97 this.child(div().size(avatar_size))
98 }
99 })
100 .child(Label::new(username).color(Color::Muted)),
101 )
102 .child(
103 h_flex().visible_on_hover(message_group).child(
104 // temp icons
105 IconButton::new(
106 collapse_handle_id.clone(),
107 if self.collapsed {
108 IconName::ArrowUp
109 } else {
110 IconName::ArrowDown
111 },
112 )
113 .icon_size(IconSize::XSmall)
114 .icon_color(Color::Muted)
115 .on_click(self.on_collapse_handle_click)
116 .tooltip(|cx| Tooltip::text("Collapse Message", cx)),
117 ),
118 ),
119 )
120 .when(self.messages.len() > 0, |el| {
121 el.child(
122 h_flex().w_full().child(
123 v_flex()
124 .relative()
125 .overflow_hidden()
126 .w_full()
127 .p(content_padding)
128 .gap_3()
129 .text_ui(cx)
130 .rounded_lg()
131 .when_some(background_color, |this, background_color| {
132 this.bg(background_color)
133 })
134 .when(self.collapsed, |this| this.h(collapsed_height))
135 .children(self.messages),
136 ),
137 )
138 })
139 }
140}