chat_panel.rs

  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}