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