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