chat_panel.rs

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