chat_panel.rs

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