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