chat_panel.rs

  1use crate::{
  2    channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
  3    editor::Editor,
  4    theme,
  5    util::ResultExt,
  6    Settings,
  7};
  8use gpui::{
  9    action,
 10    elements::*,
 11    keymap::Binding,
 12    views::{ItemType, Select, SelectStyle},
 13    AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, View,
 14    ViewContext, ViewHandle,
 15};
 16use postage::watch;
 17use time::{OffsetDateTime, UtcOffset};
 18
 19const MESSAGE_LOADING_THRESHOLD: usize = 50;
 20
 21pub struct ChatPanel {
 22    channel_list: ModelHandle<ChannelList>,
 23    active_channel: Option<(ModelHandle<Channel>, Subscription)>,
 24    message_list: ListState,
 25    input_editor: ViewHandle<Editor>,
 26    channel_select: ViewHandle<Select>,
 27    settings: watch::Receiver<Settings>,
 28    local_timezone: UtcOffset,
 29}
 30
 31pub enum Event {}
 32
 33action!(Send);
 34action!(LoadMoreMessages);
 35
 36pub fn init(cx: &mut MutableAppContext) {
 37    cx.add_action(ChatPanel::send);
 38    cx.add_action(ChatPanel::load_more_messages);
 39
 40    cx.add_bindings(vec![Binding::new("enter", Send, Some("ChatPanel"))]);
 41}
 42
 43impl ChatPanel {
 44    pub fn new(
 45        channel_list: ModelHandle<ChannelList>,
 46        settings: watch::Receiver<Settings>,
 47        cx: &mut ViewContext<Self>,
 48    ) -> Self {
 49        let input_editor = cx.add_view(|cx| {
 50            Editor::auto_height(settings.clone(), cx).with_style({
 51                let settings = settings.clone();
 52                move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
 53            })
 54        });
 55        let channel_select = cx.add_view(|cx| {
 56            let channel_list = channel_list.clone();
 57            Select::new(0, cx, {
 58                let settings = settings.clone();
 59                move |ix, item_type, is_hovered, cx| {
 60                    Self::render_channel_name(
 61                        &channel_list,
 62                        ix,
 63                        item_type,
 64                        is_hovered,
 65                        &settings.borrow().theme.chat_panel.channel_select,
 66                        cx,
 67                    )
 68                }
 69            })
 70            .with_style({
 71                let settings = settings.clone();
 72                move |_| {
 73                    let theme = &settings.borrow().theme.chat_panel.channel_select;
 74                    SelectStyle {
 75                        header: theme.header.container.clone(),
 76                        menu: theme.menu.clone(),
 77                    }
 78                }
 79            })
 80        });
 81
 82        let mut message_list = ListState::new(0, Orientation::Bottom, 1000., {
 83            let this = cx.handle().downgrade();
 84            move |ix, cx| {
 85                let this = this.upgrade(cx).unwrap().read(cx);
 86                let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
 87                this.render_message(message)
 88            }
 89        });
 90        message_list.set_scroll_handler(|visible_range, cx| {
 91            if visible_range.start < MESSAGE_LOADING_THRESHOLD {
 92                cx.dispatch_action(LoadMoreMessages);
 93            }
 94        });
 95
 96        let mut this = Self {
 97            channel_list,
 98            active_channel: Default::default(),
 99            message_list,
100            input_editor,
101            channel_select,
102            settings,
103            local_timezone: cx.platform().local_timezone(),
104        };
105
106        this.init_active_channel(cx);
107        cx.observe(&this.channel_list, |this, _, cx| {
108            this.init_active_channel(cx);
109        })
110        .detach();
111        cx.observe(&this.channel_select, |this, channel_select, cx| {
112            let selected_ix = channel_select.read(cx).selected_index();
113            let selected_channel = this.channel_list.update(cx, |channel_list, cx| {
114                let available_channels = channel_list.available_channels()?;
115                let channel_id = available_channels.get(selected_ix)?.id;
116                channel_list.get_channel(channel_id, cx)
117            });
118            if let Some(selected_channel) = selected_channel {
119                this.set_active_channel(selected_channel, cx);
120            }
121        })
122        .detach();
123
124        this
125    }
126
127    fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
128        let (active_channel, channel_count) = self.channel_list.update(cx, |list, cx| {
129            let channel_count;
130            let mut active_channel = None;
131
132            if let Some(available_channels) = list.available_channels() {
133                channel_count = available_channels.len();
134                if self.active_channel.is_none() {
135                    if let Some(channel_id) = available_channels.first().map(|channel| channel.id) {
136                        active_channel = list.get_channel(channel_id, cx);
137                    }
138                }
139            } else {
140                channel_count = 0;
141            }
142
143            (active_channel, channel_count)
144        });
145
146        if let Some(active_channel) = active_channel {
147            self.set_active_channel(active_channel, cx);
148        } else {
149            self.active_channel = None;
150        }
151
152        self.channel_select.update(cx, |select, cx| {
153            select.set_item_count(channel_count, cx);
154        });
155    }
156
157    fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
158        if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
159            {
160                let channel = channel.read(cx);
161                self.message_list.reset(channel.message_count());
162                let placeholder = format!("Message #{}", channel.name());
163                self.input_editor.update(cx, move |editor, cx| {
164                    editor.set_placeholder_text(placeholder, cx);
165                });
166            }
167            let subscription = cx.subscribe(&channel, Self::channel_did_change);
168            self.active_channel = Some((channel, subscription));
169        }
170    }
171
172    fn channel_did_change(
173        &mut self,
174        _: ModelHandle<Channel>,
175        event: &ChannelEvent,
176        cx: &mut ViewContext<Self>,
177    ) {
178        match event {
179            ChannelEvent::MessagesAdded {
180                old_range,
181                new_count,
182            } => {
183                self.message_list.splice(old_range.clone(), *new_count);
184            }
185        }
186        cx.notify();
187    }
188
189    fn render_active_channel_messages(&self) -> ElementBox {
190        let messages = if self.active_channel.is_some() {
191            List::new(self.message_list.clone()).boxed()
192        } else {
193            Empty::new().boxed()
194        };
195
196        Expanded::new(1., messages).boxed()
197    }
198
199    fn render_message(&self, message: &ChannelMessage) -> ElementBox {
200        let now = OffsetDateTime::now_utc();
201        let settings = self.settings.borrow();
202        let theme = &settings.theme.chat_panel.message;
203        Container::new(
204            Flex::column()
205                .with_child(
206                    Flex::row()
207                        .with_child(
208                            Container::new(
209                                Label::new(
210                                    message.sender.github_login.clone(),
211                                    theme.sender.text.clone(),
212                                )
213                                .boxed(),
214                            )
215                            .with_style(&theme.sender.container)
216                            .boxed(),
217                        )
218                        .with_child(
219                            Container::new(
220                                Label::new(
221                                    format_timestamp(message.timestamp, now, self.local_timezone),
222                                    theme.timestamp.text.clone(),
223                                )
224                                .boxed(),
225                            )
226                            .with_style(&theme.timestamp.container)
227                            .boxed(),
228                        )
229                        .boxed(),
230                )
231                .with_child(Text::new(message.body.clone(), theme.body.clone()).boxed())
232                .boxed(),
233        )
234        .with_style(&theme.container)
235        .boxed()
236    }
237
238    fn render_input_box(&self) -> ElementBox {
239        ConstrainedBox::new(ChildView::new(self.input_editor.id()).boxed())
240            .with_max_height(100.)
241            .boxed()
242    }
243
244    fn render_channel_name(
245        channel_list: &ModelHandle<ChannelList>,
246        ix: usize,
247        item_type: ItemType,
248        is_hovered: bool,
249        theme: &theme::ChannelSelect,
250        cx: &AppContext,
251    ) -> ElementBox {
252        let channel = &channel_list.read(cx).available_channels().unwrap()[ix];
253        let theme = match (item_type, is_hovered) {
254            (ItemType::Header, _) => &theme.header,
255            (ItemType::Selected, false) => &theme.active_item,
256            (ItemType::Selected, true) => &theme.hovered_active_item,
257            (ItemType::Unselected, false) => &theme.item,
258            (ItemType::Unselected, true) => &theme.hovered_item,
259        };
260        Container::new(
261            Flex::row()
262                .with_child(
263                    Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed())
264                        .with_style(&theme.hash.container)
265                        .boxed(),
266                )
267                .with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed())
268                .boxed(),
269        )
270        .with_style(&theme.container)
271        .boxed()
272    }
273
274    fn send(&mut self, _: &Send, cx: &mut ViewContext<Self>) {
275        if let Some((channel, _)) = self.active_channel.as_ref() {
276            let body = self.input_editor.update(cx, |editor, cx| {
277                let body = editor.text(cx);
278                editor.clear(cx);
279                body
280            });
281
282            if let Some(task) = channel
283                .update(cx, |channel, cx| channel.send_message(body, cx))
284                .log_err()
285            {
286                task.detach();
287            }
288        }
289    }
290
291    fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
292        if let Some((channel, _)) = self.active_channel.as_ref() {
293            channel.update(cx, |channel, cx| {
294                channel.load_more_messages(cx);
295            })
296        }
297    }
298}
299
300impl Entity for ChatPanel {
301    type Event = Event;
302}
303
304impl View for ChatPanel {
305    fn ui_name() -> &'static str {
306        "ChatPanel"
307    }
308
309    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
310        let theme = &self.settings.borrow().theme;
311        Container::new(
312            Flex::column()
313                .with_child(
314                    Container::new(ChildView::new(self.channel_select.id()).boxed())
315                        .with_style(&theme.chat_panel.channel_select.container)
316                        .boxed(),
317                )
318                .with_child(self.render_active_channel_messages())
319                .with_child(self.render_input_box())
320                .boxed(),
321        )
322        .with_style(&theme.chat_panel.container)
323        .boxed()
324    }
325
326    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
327        cx.focus(&self.input_editor);
328    }
329}
330
331fn format_timestamp(
332    mut timestamp: OffsetDateTime,
333    mut now: OffsetDateTime,
334    local_timezone: UtcOffset,
335) -> String {
336    timestamp = timestamp.to_offset(local_timezone);
337    now = now.to_offset(local_timezone);
338
339    let today = now.date();
340    let date = timestamp.date();
341    let mut hour = timestamp.hour();
342    let mut part = "am";
343    if hour > 12 {
344        hour -= 12;
345        part = "pm";
346    }
347    if date == today {
348        format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
349    } else if date.next_day() == Some(today) {
350        format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
351    } else {
352        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
353    }
354}