1use crate::{
2 channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
3 editor::Editor,
4 theme,
5 util::ResultExt,
6 Settings,
7};
8use gpui::{
9 action,
10 elements::*,
11 keymap::Binding,
12 views::{ItemType, Select, SelectStyle},
13 AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, View,
14 ViewContext, ViewHandle,
15};
16use postage::watch;
17use time::{OffsetDateTime, UtcOffset};
18
19const MESSAGE_LOADING_THRESHOLD: usize = 50;
20
21pub struct ChatPanel {
22 channel_list: ModelHandle<ChannelList>,
23 active_channel: Option<(ModelHandle<Channel>, Subscription)>,
24 message_list: ListState,
25 input_editor: ViewHandle<Editor>,
26 channel_select: ViewHandle<Select>,
27 settings: watch::Receiver<Settings>,
28 local_timezone: UtcOffset,
29}
30
31pub enum Event {}
32
33action!(Send);
34action!(LoadMoreMessages);
35
36pub fn init(cx: &mut MutableAppContext) {
37 cx.add_action(ChatPanel::send);
38 cx.add_action(ChatPanel::load_more_messages);
39
40 cx.add_bindings(vec![Binding::new("enter", Send, Some("ChatPanel"))]);
41}
42
43impl ChatPanel {
44 pub fn new(
45 channel_list: ModelHandle<ChannelList>,
46 settings: watch::Receiver<Settings>,
47 cx: &mut ViewContext<Self>,
48 ) -> Self {
49 let input_editor = cx.add_view(|cx| {
50 Editor::auto_height(settings.clone(), cx).with_style({
51 let settings = settings.clone();
52 move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
53 })
54 });
55 let channel_select = cx.add_view(|cx| {
56 let channel_list = channel_list.clone();
57 Select::new(0, cx, {
58 let settings = settings.clone();
59 move |ix, item_type, is_hovered, cx| {
60 Self::render_channel_name(
61 &channel_list,
62 ix,
63 item_type,
64 is_hovered,
65 &settings.borrow().theme.chat_panel.channel_select,
66 cx,
67 )
68 }
69 })
70 .with_style({
71 let settings = settings.clone();
72 move |_| {
73 let theme = &settings.borrow().theme.chat_panel.channel_select;
74 SelectStyle {
75 header: theme.header.container.clone(),
76 menu: theme.menu.clone(),
77 }
78 }
79 })
80 });
81
82 let mut message_list = ListState::new(0, Orientation::Bottom, 1000., {
83 let this = cx.handle().downgrade();
84 move |ix, cx| {
85 let this = this.upgrade(cx).unwrap().read(cx);
86 let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
87 this.render_message(message)
88 }
89 });
90 message_list.set_scroll_handler(|visible_range, cx| {
91 if visible_range.start < MESSAGE_LOADING_THRESHOLD {
92 cx.dispatch_action(LoadMoreMessages);
93 }
94 });
95
96 let mut this = Self {
97 channel_list,
98 active_channel: Default::default(),
99 message_list,
100 input_editor,
101 channel_select,
102 settings,
103 local_timezone: cx.platform().local_timezone(),
104 };
105
106 this.init_active_channel(cx);
107 cx.observe(&this.channel_list, |this, _, cx| {
108 this.init_active_channel(cx);
109 })
110 .detach();
111 cx.observe(&this.channel_select, |this, channel_select, cx| {
112 let selected_ix = channel_select.read(cx).selected_index();
113 let selected_channel = this.channel_list.update(cx, |channel_list, cx| {
114 let available_channels = channel_list.available_channels()?;
115 let channel_id = available_channels.get(selected_ix)?.id;
116 channel_list.get_channel(channel_id, cx)
117 });
118 if let Some(selected_channel) = selected_channel {
119 this.set_active_channel(selected_channel, cx);
120 }
121 })
122 .detach();
123
124 this
125 }
126
127 fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
128 let (active_channel, channel_count) = self.channel_list.update(cx, |list, cx| {
129 let channel_count;
130 let mut active_channel = None;
131
132 if let Some(available_channels) = list.available_channels() {
133 channel_count = available_channels.len();
134 if self.active_channel.is_none() {
135 if let Some(channel_id) = available_channels.first().map(|channel| channel.id) {
136 active_channel = list.get_channel(channel_id, cx);
137 }
138 }
139 } else {
140 channel_count = 0;
141 }
142
143 (active_channel, channel_count)
144 });
145
146 if let Some(active_channel) = active_channel {
147 self.set_active_channel(active_channel, cx);
148 } else {
149 self.active_channel = None;
150 }
151
152 self.channel_select.update(cx, |select, cx| {
153 select.set_item_count(channel_count, cx);
154 });
155 }
156
157 fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
158 if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
159 {
160 let channel = channel.read(cx);
161 self.message_list.reset(channel.message_count());
162 let placeholder = format!("Message #{}", channel.name());
163 self.input_editor.update(cx, move |editor, cx| {
164 editor.set_placeholder_text(placeholder, cx);
165 });
166 }
167 let subscription = cx.subscribe(&channel, Self::channel_did_change);
168 self.active_channel = Some((channel, subscription));
169 }
170 }
171
172 fn channel_did_change(
173 &mut self,
174 _: ModelHandle<Channel>,
175 event: &ChannelEvent,
176 cx: &mut ViewContext<Self>,
177 ) {
178 match event {
179 ChannelEvent::MessagesAdded {
180 old_range,
181 new_count,
182 } => {
183 self.message_list.splice(old_range.clone(), *new_count);
184 }
185 }
186 cx.notify();
187 }
188
189 fn render_active_channel_messages(&self) -> ElementBox {
190 let messages = if self.active_channel.is_some() {
191 List::new(self.message_list.clone()).boxed()
192 } else {
193 Empty::new().boxed()
194 };
195
196 Expanded::new(1., messages).boxed()
197 }
198
199 fn render_message(&self, message: &ChannelMessage) -> ElementBox {
200 let now = OffsetDateTime::now_utc();
201 let settings = self.settings.borrow();
202 let theme = &settings.theme.chat_panel.message;
203 Container::new(
204 Flex::column()
205 .with_child(
206 Flex::row()
207 .with_child(
208 Container::new(
209 Label::new(
210 message.sender.github_login.clone(),
211 theme.sender.text.clone(),
212 )
213 .boxed(),
214 )
215 .with_style(&theme.sender.container)
216 .boxed(),
217 )
218 .with_child(
219 Container::new(
220 Label::new(
221 format_timestamp(message.timestamp, now, self.local_timezone),
222 theme.timestamp.text.clone(),
223 )
224 .boxed(),
225 )
226 .with_style(&theme.timestamp.container)
227 .boxed(),
228 )
229 .boxed(),
230 )
231 .with_child(Text::new(message.body.clone(), theme.body.clone()).boxed())
232 .boxed(),
233 )
234 .with_style(&theme.container)
235 .boxed()
236 }
237
238 fn render_input_box(&self) -> ElementBox {
239 ConstrainedBox::new(ChildView::new(self.input_editor.id()).boxed())
240 .with_max_height(100.)
241 .boxed()
242 }
243
244 fn render_channel_name(
245 channel_list: &ModelHandle<ChannelList>,
246 ix: usize,
247 item_type: ItemType,
248 is_hovered: bool,
249 theme: &theme::ChannelSelect,
250 cx: &AppContext,
251 ) -> ElementBox {
252 let channel = &channel_list.read(cx).available_channels().unwrap()[ix];
253 let theme = match (item_type, is_hovered) {
254 (ItemType::Header, _) => &theme.header,
255 (ItemType::Selected, false) => &theme.active_item,
256 (ItemType::Selected, true) => &theme.hovered_active_item,
257 (ItemType::Unselected, false) => &theme.item,
258 (ItemType::Unselected, true) => &theme.hovered_item,
259 };
260 Container::new(
261 Flex::row()
262 .with_child(
263 Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed())
264 .with_style(&theme.hash.container)
265 .boxed(),
266 )
267 .with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed())
268 .boxed(),
269 )
270 .with_style(&theme.container)
271 .boxed()
272 }
273
274 fn send(&mut self, _: &Send, cx: &mut ViewContext<Self>) {
275 if let Some((channel, _)) = self.active_channel.as_ref() {
276 let body = self.input_editor.update(cx, |editor, cx| {
277 let body = editor.text(cx);
278 editor.clear(cx);
279 body
280 });
281
282 if let Some(task) = channel
283 .update(cx, |channel, cx| channel.send_message(body, cx))
284 .log_err()
285 {
286 task.detach();
287 }
288 }
289 }
290
291 fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
292 if let Some((channel, _)) = self.active_channel.as_ref() {
293 channel.update(cx, |channel, cx| {
294 channel.load_more_messages(cx);
295 })
296 }
297 }
298}
299
300impl Entity for ChatPanel {
301 type Event = Event;
302}
303
304impl View for ChatPanel {
305 fn ui_name() -> &'static str {
306 "ChatPanel"
307 }
308
309 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
310 let theme = &self.settings.borrow().theme;
311 Container::new(
312 Flex::column()
313 .with_child(
314 Container::new(ChildView::new(self.channel_select.id()).boxed())
315 .with_style(&theme.chat_panel.channel_select.container)
316 .boxed(),
317 )
318 .with_child(self.render_active_channel_messages())
319 .with_child(self.render_input_box())
320 .boxed(),
321 )
322 .with_style(&theme.chat_panel.container)
323 .boxed()
324 }
325
326 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
327 cx.focus(&self.input_editor);
328 }
329}
330
331fn format_timestamp(
332 mut timestamp: OffsetDateTime,
333 mut now: OffsetDateTime,
334 local_timezone: UtcOffset,
335) -> String {
336 timestamp = timestamp.to_offset(local_timezone);
337 now = now.to_offset(local_timezone);
338
339 let today = now.date();
340 let date = timestamp.date();
341 let mut hour = timestamp.hour();
342 let mut part = "am";
343 if hour > 12 {
344 hour -= 12;
345 part = "pm";
346 }
347 if date == today {
348 format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
349 } else if date.next_day() == Some(today) {
350 format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
351 } else {
352 format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
353 }
354}