1pub mod action;
2mod consts;
3pub mod message;
4pub mod state;
5pub mod style;
6
7use crate::{
8 action::{ChatEvent, MessageAction},
9 consts::*,
10 message::ChatMessage,
11 state::ChatState,
12 style::ChatTheme,
13};
14
15use iced::{
16 Alignment::{self},
17 Element,
18 Length::{self, Fill},
19 Task, Theme,
20 alignment::Horizontal::Right,
21 widget::{button, column, container, row, scrollable, text},
22};
23
24pub struct ChatWidget<'a, M>
25where
26 M: ChatMessage,
27{
28 state: &'a ChatState<M>,
29 actions: Vec<MessageAction>,
30 theme: ChatTheme,
31}
32
33impl<'a, M> ChatWidget<'a, M>
34where
35 M: ChatMessage + Clone + 'static,
36{
37 pub fn new(theme: &Theme, state: &'a ChatState<M>) -> Self {
38 Self {
39 state: state,
40 actions: Vec::new(),
41 theme: ChatTheme::get_default(theme),
42 }
43 }
44
45 pub fn with_custom_theme(mut self, theme: ChatTheme) -> Self {
46 self.theme = theme;
47 self
48 }
49
50 pub fn with_actions(mut self, actions: Vec<MessageAction>) -> Self {
51 self.actions = actions;
52 self
53 }
54
55 pub fn scroll_to_bottom() -> Task<ChatEvent> {
56 scrollable::snap_to(
57 scrollable::Id::new(SCROLLABLE_ID),
58 scrollable::RelativeOffset::END,
59 )
60 }
61
62 pub fn view(self) -> Element<'static, ChatEvent> {
63 let theme = self.theme;
64 let actions = self.actions;
65
66 let ids = self.state.get_messages_ids();
67
68 let mut messages_ordered = ids
69 .iter()
70 .filter_map(|id| self.state.get_message(id))
71 .collect::<Vec<&M>>();
72 messages_ordered.sort_by_key(|msg| msg.timestamp());
73
74 let messages_ordered = messages_ordered
75 .iter()
76 .fold(column![].spacing(theme.spacing()), |col, msg| {
77 col.push(Self::render_message_owned(msg, &theme, actions.clone()))
78 });
79
80 let scrollable_messages = scrollable(
81 container(messages_ordered)
82 .padding(theme.padding())
83 .width(Length::Fill),
84 )
85 .height(Length::Fill);
86
87 let scrollable_messages = scrollable_messages.id(scrollable::Id::new(SCROLLABLE_ID));
88
89 let bg_color = theme.background_color();
90 let widget = container(scrollable_messages)
91 .width(Length::Fill)
92 .height(Length::Fill)
93 .style(move |_theme| container::Style {
94 background: Some(bg_color.into()),
95 ..Default::default()
96 })
97 .into();
98
99 widget
100 }
101
102 fn render_message_owned(
103 msg: &M,
104 theme: &ChatTheme,
105 actions: Vec<MessageAction>, ) -> Element<'static, ChatEvent> {
107 let style = if msg.is_own_message() {
108 theme.own_message_style()
109 } else {
110 theme.other_message_style()
111 };
112
113 let is_own = msg.is_own_message();
114 let msg_id = msg.id().to_string();
115 let author = msg.author_id().to_string();
116 let content = msg.content().to_string();
117
118 let author_text_size = style.author_text_size();
120 let content_text_size = style.content_text_size();
121 let text_color = style.text_color();
122 let padding = style.padding();
123 let background = style.background();
124 let border_radius = style.border_radius();
125
126 let mut message_col = column![]
127 .spacing(4.0)
128 .push(text(author).size(author_text_size).color(text_color))
129 .push(text(content).size(content_text_size).color(text_color));
130
131 if !actions.is_empty() {
132 let actions_row = actions
133 .into_iter() .fold(row![].spacing(5.0), |row_acc, action| {
135 let action_id = action.id; let label = action.label; let msg_id_clone = msg_id.clone();
138 row_acc.push(
139 button(text(label).size(12))
140 .on_press(ChatEvent::ActionClicked {
141 message_id: msg_id_clone,
142 action_id,
143 })
144 .padding(5),
145 )
146 });
147 message_col = message_col.push(actions_row);
148 }
149
150 let timestamp_text_size = style.timestamp_text_size();
151 let timestamp = msg.timestamp();
152 let timestamp_text = format!(
153 "{}",
154 timestamp.format(style.time_stamp_format()).to_string()
155 );
156 let timestamp_container = container(
157 text(timestamp_text)
158 .size(timestamp_text_size)
159 .color(style.time_stamp_text_color()),
160 )
161 .align_x(Right);
162
163 let timestamp_row = row![timestamp_container];
164 message_col = message_col.push(timestamp_row);
165
166 let message_container = container(message_col)
167 .padding(padding)
168 .style(move |_theme| container::Style {
169 background: Some(iced::Background::Color(background)),
170 border: iced::Border {
171 radius: border_radius,
172 ..Default::default()
173 },
174 ..Default::default()
175 });
176
177 let message_wrapper = container(message_container).width(Fill).align_x(if is_own {
178 Right
179 } else {
180 iced::alignment::Horizontal::Left
181 });
182
183 let row_widget: iced::widget::Row<'static, ChatEvent> = row![message_wrapper].width(Fill);
184
185 if is_own {
186 row_widget.align_y(Alignment::End).into()
187 } else {
188 row_widget.align_y(Alignment::Start).into()
189 }
190 }
191}