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