chat_panel.rs

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