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