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