chat_panel.rs

  1use crate::{channel_view::ChannelView, ChatPanelSettings};
  2use anyhow::Result;
  3use call::ActiveCall;
  4use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
  5use client::Client;
  6use collections::HashMap;
  7use db::kvp::KEY_VALUE_STORE;
  8use editor::Editor;
  9use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
 10use gpui::{
 11    actions,
 12    elements::*,
 13    platform::{CursorStyle, MouseButton},
 14    serde_json,
 15    views::{ItemType, Select, SelectStyle},
 16    AnyViewHandle, AppContext, AsyncAppContext, Entity, ImageData, ModelHandle, Subscription, Task,
 17    View, ViewContext, ViewHandle, WeakViewHandle,
 18};
 19use language::{language_settings::SoftWrap, LanguageRegistry};
 20use menu::Confirm;
 21use project::Fs;
 22use rich_text::RichText;
 23use serde::{Deserialize, Serialize};
 24use settings::SettingsStore;
 25use std::sync::Arc;
 26use theme::{IconButton, Theme};
 27use time::{OffsetDateTime, UtcOffset};
 28use util::{ResultExt, TryFutureExt};
 29use workspace::{
 30    dock::{DockPosition, Panel},
 31    Workspace,
 32};
 33
 34const MESSAGE_LOADING_THRESHOLD: usize = 50;
 35const CHAT_PANEL_KEY: &'static str = "ChatPanel";
 36
 37pub struct ChatPanel {
 38    client: Arc<Client>,
 39    channel_store: ModelHandle<ChannelStore>,
 40    languages: Arc<LanguageRegistry>,
 41    active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
 42    message_list: ListState<ChatPanel>,
 43    input_editor: ViewHandle<Editor>,
 44    channel_select: ViewHandle<Select>,
 45    local_timezone: UtcOffset,
 46    fs: Arc<dyn Fs>,
 47    width: Option<f32>,
 48    active: bool,
 49    pending_serialization: Task<Option<()>>,
 50    subscriptions: Vec<gpui::Subscription>,
 51    workspace: WeakViewHandle<Workspace>,
 52    has_focus: bool,
 53    markdown_data: HashMap<ChannelMessageId, RichText>,
 54}
 55
 56#[derive(Serialize, Deserialize)]
 57struct SerializedChatPanel {
 58    width: Option<f32>,
 59}
 60
 61#[derive(Debug)]
 62pub enum Event {
 63    DockPositionChanged,
 64    Focus,
 65    Dismissed,
 66}
 67
 68actions!(
 69    chat_panel,
 70    [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
 71);
 72
 73pub fn init(cx: &mut AppContext) {
 74    cx.add_action(ChatPanel::send);
 75    cx.add_action(ChatPanel::load_more_messages);
 76    cx.add_action(ChatPanel::open_notes);
 77    cx.add_action(ChatPanel::join_call);
 78}
 79
 80impl ChatPanel {
 81    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
 82        let fs = workspace.app_state().fs.clone();
 83        let client = workspace.app_state().client.clone();
 84        let channel_store = ChannelStore::global(cx);
 85        let languages = workspace.app_state().languages.clone();
 86
 87        let input_editor = cx.add_view(|cx| {
 88            let mut editor = Editor::auto_height(
 89                4,
 90                Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
 91                cx,
 92            );
 93            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
 94            editor
 95        });
 96
 97        let workspace_handle = workspace.weak_handle();
 98
 99        let channel_select = cx.add_view(|cx| {
100            let channel_store = channel_store.clone();
101            let workspace = workspace_handle.clone();
102            Select::new(0, cx, {
103                move |ix, item_type, is_hovered, cx| {
104                    Self::render_channel_name(
105                        &channel_store,
106                        ix,
107                        item_type,
108                        is_hovered,
109                        workspace,
110                        cx,
111                    )
112                }
113            })
114            .with_style(move |cx| {
115                let style = &theme::current(cx).chat_panel.channel_select;
116                SelectStyle {
117                    header: Default::default(),
118                    menu: style.menu,
119                }
120            })
121        });
122
123        let mut message_list =
124            ListState::<Self>::new(0, Orientation::Bottom, 1000., move |this, ix, cx| {
125                this.render_message(ix, cx)
126            });
127        message_list.set_scroll_handler(|visible_range, this, cx| {
128            if visible_range.start < MESSAGE_LOADING_THRESHOLD {
129                this.load_more_messages(&LoadMoreMessages, cx);
130            }
131        });
132
133        cx.add_view(|cx| {
134            let mut this = Self {
135                fs,
136                client,
137                channel_store,
138                languages,
139
140                active_chat: Default::default(),
141                pending_serialization: Task::ready(None),
142                message_list,
143                input_editor,
144                channel_select,
145                local_timezone: cx.platform().local_timezone(),
146                has_focus: false,
147                subscriptions: Vec::new(),
148                workspace: workspace_handle,
149                active: false,
150                width: None,
151                markdown_data: Default::default(),
152            };
153
154            let mut old_dock_position = this.position(cx);
155            this.subscriptions
156                .push(
157                    cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
158                        let new_dock_position = this.position(cx);
159                        if new_dock_position != old_dock_position {
160                            old_dock_position = new_dock_position;
161                            cx.emit(Event::DockPositionChanged);
162                        }
163                        cx.notify();
164                    }),
165                );
166
167            this.update_channel_count(cx);
168            cx.observe(&this.channel_store, |this, _, cx| {
169                this.update_channel_count(cx)
170            })
171            .detach();
172
173            cx.observe(&this.channel_select, |this, channel_select, cx| {
174                let selected_ix = channel_select.read(cx).selected_index();
175
176                let selected_channel_id = this
177                    .channel_store
178                    .read(cx)
179                    .channel_at(selected_ix)
180                    .map(|e| e.id);
181                if let Some(selected_channel_id) = selected_channel_id {
182                    this.select_channel(selected_channel_id, cx)
183                        .detach_and_log_err(cx);
184                }
185            })
186            .detach();
187
188            let markdown = this.languages.language_for_name("Markdown");
189            cx.spawn(|this, mut cx| async move {
190                let markdown = markdown.await?;
191
192                this.update(&mut cx, |this, cx| {
193                    this.input_editor.update(cx, |editor, cx| {
194                        editor.buffer().update(cx, |multi_buffer, cx| {
195                            multi_buffer
196                                .as_singleton()
197                                .unwrap()
198                                .update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx))
199                        })
200                    })
201                })?;
202
203                anyhow::Ok(())
204            })
205            .detach_and_log_err(cx);
206
207            this
208        })
209    }
210
211    pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
212        self.active_chat.as_ref().map(|(chat, _)| chat.clone())
213    }
214
215    pub fn load(
216        workspace: WeakViewHandle<Workspace>,
217        cx: AsyncAppContext,
218    ) -> Task<Result<ViewHandle<Self>>> {
219        cx.spawn(|mut cx| async move {
220            let serialized_panel = if let Some(panel) = cx
221                .background()
222                .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
223                .await
224                .log_err()
225                .flatten()
226            {
227                Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
228            } else {
229                None
230            };
231
232            workspace.update(&mut cx, |workspace, cx| {
233                let panel = Self::new(workspace, cx);
234                if let Some(serialized_panel) = serialized_panel {
235                    panel.update(cx, |panel, cx| {
236                        panel.width = serialized_panel.width;
237                        cx.notify();
238                    });
239                }
240                panel
241            })
242        })
243    }
244
245    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
246        let width = self.width;
247        self.pending_serialization = cx.background().spawn(
248            async move {
249                KEY_VALUE_STORE
250                    .write_kvp(
251                        CHAT_PANEL_KEY.into(),
252                        serde_json::to_string(&SerializedChatPanel { width })?,
253                    )
254                    .await?;
255                anyhow::Ok(())
256            }
257            .log_err(),
258        );
259    }
260
261    fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
262        let channel_count = self.channel_store.read(cx).channel_count();
263        self.channel_select.update(cx, |select, cx| {
264            select.set_item_count(channel_count, cx);
265        });
266    }
267
268    fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
269        if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
270            let id = chat.read(cx).channel().id;
271            {
272                let chat = chat.read(cx);
273                self.message_list.reset(chat.message_count());
274                let placeholder = format!("Message #{}", chat.channel().name);
275                self.input_editor.update(cx, move |editor, cx| {
276                    editor.set_placeholder_text(placeholder, cx);
277                });
278            }
279            let subscription = cx.subscribe(&chat, Self::channel_did_change);
280            self.active_chat = Some((chat, subscription));
281            self.acknowledge_last_message(cx);
282            self.channel_select.update(cx, |select, cx| {
283                if let Some(ix) = self.channel_store.read(cx).index_of_channel(id) {
284                    select.set_selected_index(ix, cx);
285                }
286            });
287            cx.notify();
288        }
289    }
290
291    fn channel_did_change(
292        &mut self,
293        _: ModelHandle<ChannelChat>,
294        event: &ChannelChatEvent,
295        cx: &mut ViewContext<Self>,
296    ) {
297        match event {
298            ChannelChatEvent::MessagesUpdated {
299                old_range,
300                new_count,
301            } => {
302                self.message_list.splice(old_range.clone(), *new_count);
303                if self.active {
304                    self.acknowledge_last_message(cx);
305                }
306            }
307            ChannelChatEvent::NewMessage {
308                channel_id,
309                message_id,
310            } => {
311                if !self.active {
312                    self.channel_store.update(cx, |store, cx| {
313                        store.new_message(*channel_id, *message_id, cx)
314                    })
315                }
316            }
317        }
318        cx.notify();
319    }
320
321    fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
322        if self.active {
323            if let Some((chat, _)) = &self.active_chat {
324                chat.update(cx, |chat, cx| {
325                    chat.acknowledge_last_message(cx);
326                });
327            }
328        }
329    }
330
331    fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
332        let theme = theme::current(cx);
333        Flex::column()
334            .with_child(
335                ChildView::new(&self.channel_select, cx)
336                    .contained()
337                    .with_style(theme.chat_panel.channel_select.container),
338            )
339            .with_child(self.render_active_channel_messages(&theme))
340            .with_child(self.render_input_box(&theme, cx))
341            .into_any()
342    }
343
344    fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
345        let messages = if self.active_chat.is_some() {
346            List::new(self.message_list.clone())
347                .contained()
348                .with_style(theme.chat_panel.list)
349                .into_any()
350        } else {
351            Empty::new().into_any()
352        };
353
354        messages.flex(1., true).into_any()
355    }
356
357    fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
358        let (message, is_continuation, is_last, is_admin) = {
359            let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
360            let is_admin = self
361                .channel_store
362                .read(cx)
363                .is_user_admin(active_chat.channel().id);
364            let last_message = active_chat.message(ix.saturating_sub(1));
365            let this_message = active_chat.message(ix);
366            let is_continuation = last_message.id != this_message.id
367                && this_message.sender.id == last_message.sender.id;
368
369            (
370                active_chat.message(ix).clone(),
371                is_continuation,
372                active_chat.message_count() == ix + 1,
373                is_admin,
374            )
375        };
376
377        let is_pending = message.is_pending();
378        let text = self
379            .markdown_data
380            .entry(message.id)
381            .or_insert_with(|| rich_text::render_markdown(message.body, &self.languages, None));
382
383        let now = OffsetDateTime::now_utc();
384        let theme = theme::current(cx);
385        let style = if is_pending {
386            &theme.chat_panel.pending_message
387        } else if is_continuation {
388            &theme.chat_panel.continuation_message
389        } else {
390            &theme.chat_panel.message
391        };
392
393        let belongs_to_user = Some(message.sender.id) == self.client.user_id();
394        let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
395            (message.id, belongs_to_user || is_admin)
396        {
397            Some(id)
398        } else {
399            None
400        };
401
402        enum MessageBackgroundHighlight {}
403        MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
404            let container = style.container.style_for(state);
405            if is_continuation {
406                Flex::row()
407                    .with_child(
408                        text.element(
409                            theme.editor.syntax.clone(),
410                            style.body.clone(),
411                            theme.editor.document_highlight_read_background,
412                            cx,
413                        )
414                        .flex(1., true),
415                    )
416                    .with_child(render_remove(message_id_to_remove, cx, &theme))
417                    .contained()
418                    .with_style(*container)
419                    .with_margin_bottom(if is_last {
420                        theme.chat_panel.last_message_bottom_spacing
421                    } else {
422                        0.
423                    })
424                    .into_any()
425            } else {
426                Flex::column()
427                    .with_child(
428                        Flex::row()
429                            .with_child(
430                                Flex::row()
431                                    .with_child(render_avatar(
432                                        message.sender.avatar.clone(),
433                                        &theme,
434                                    ))
435                                    .with_child(
436                                        Label::new(
437                                            message.sender.github_login.clone(),
438                                            style.sender.text.clone(),
439                                        )
440                                        .contained()
441                                        .with_style(style.sender.container),
442                                    )
443                                    .with_child(
444                                        Label::new(
445                                            format_timestamp(
446                                                message.timestamp,
447                                                now,
448                                                self.local_timezone,
449                                            ),
450                                            style.timestamp.text.clone(),
451                                        )
452                                        .contained()
453                                        .with_style(style.timestamp.container),
454                                    )
455                                    .align_children_center()
456                                    .flex(1., true),
457                            )
458                            .with_child(render_remove(message_id_to_remove, cx, &theme))
459                            .align_children_center(),
460                    )
461                    .with_child(
462                        Flex::row()
463                            .with_child(
464                                text.element(
465                                    theme.editor.syntax.clone(),
466                                    style.body.clone(),
467                                    theme.editor.document_highlight_read_background,
468                                    cx,
469                                )
470                                .flex(1., true),
471                            )
472                            // Add a spacer to make everything line up
473                            .with_child(render_remove(None, cx, &theme)),
474                    )
475                    .contained()
476                    .with_style(*container)
477                    .with_margin_bottom(if is_last {
478                        theme.chat_panel.last_message_bottom_spacing
479                    } else {
480                        0.
481                    })
482                    .into_any()
483            }
484        })
485        .into_any()
486    }
487
488    fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
489        ChildView::new(&self.input_editor, cx)
490            .contained()
491            .with_style(theme.chat_panel.input_editor.container)
492            .into_any()
493    }
494
495    fn render_channel_name(
496        channel_store: &ModelHandle<ChannelStore>,
497        ix: usize,
498        item_type: ItemType,
499        is_hovered: bool,
500        workspace: WeakViewHandle<Workspace>,
501        cx: &mut ViewContext<Select>,
502    ) -> AnyElement<Select> {
503        let theme = theme::current(cx);
504        let tooltip_style = &theme.tooltip;
505        let theme = &theme.chat_panel;
506        let style = match (&item_type, is_hovered) {
507            (ItemType::Header, _) => &theme.channel_select.header,
508            (ItemType::Selected, _) => &theme.channel_select.active_item,
509            (ItemType::Unselected, false) => &theme.channel_select.item,
510            (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
511        };
512
513        let channel = &channel_store.read(cx).channel_at(ix).unwrap();
514        let channel_id = channel.id;
515
516        let mut row = Flex::row()
517            .with_child(
518                Label::new("#".to_string(), style.hash.text.clone())
519                    .contained()
520                    .with_style(style.hash.container),
521            )
522            .with_child(Label::new(channel.name.clone(), style.name.clone()));
523
524        if matches!(item_type, ItemType::Header) {
525            row.add_children([
526                MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
527                    render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
528                })
529                .on_click(MouseButton::Left, move |_, _, cx| {
530                    if let Some(workspace) = workspace.upgrade(cx) {
531                        ChannelView::open(channel_id, workspace, cx).detach();
532                    }
533                })
534                .with_tooltip::<OpenChannelNotes>(
535                    channel_id as usize,
536                    "Open Notes",
537                    Some(Box::new(OpenChannelNotes)),
538                    tooltip_style.clone(),
539                    cx,
540                )
541                .flex_float(),
542                MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
543                    render_icon_button(
544                        theme.icon_button.style_for(mouse_state),
545                        "icons/speaker-loud.svg",
546                    )
547                })
548                .on_click(MouseButton::Left, move |_, _, cx| {
549                    ActiveCall::global(cx)
550                        .update(cx, |call, cx| call.join_channel(channel_id, cx))
551                        .detach_and_log_err(cx);
552                })
553                .with_tooltip::<ActiveCall>(
554                    channel_id as usize,
555                    "Join Call",
556                    Some(Box::new(JoinCall)),
557                    tooltip_style.clone(),
558                    cx,
559                )
560                .flex_float(),
561            ]);
562        }
563
564        row.align_children_center()
565            .contained()
566            .with_style(style.container)
567            .into_any()
568    }
569
570    fn render_sign_in_prompt(
571        &self,
572        theme: &Arc<Theme>,
573        cx: &mut ViewContext<Self>,
574    ) -> AnyElement<Self> {
575        enum SignInPromptLabel {}
576
577        MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
578            Label::new(
579                "Sign in to use chat".to_string(),
580                theme
581                    .chat_panel
582                    .sign_in_prompt
583                    .style_for(mouse_state)
584                    .clone(),
585            )
586        })
587        .with_cursor_style(CursorStyle::PointingHand)
588        .on_click(MouseButton::Left, move |_, this, cx| {
589            let client = this.client.clone();
590            cx.spawn(|this, mut cx| async move {
591                if client
592                    .authenticate_and_connect(true, &cx)
593                    .log_err()
594                    .await
595                    .is_some()
596                {
597                    this.update(&mut cx, |this, cx| {
598                        if cx.handle().is_focused(cx) {
599                            cx.focus(&this.input_editor);
600                        }
601                    })
602                    .ok();
603                }
604            })
605            .detach();
606        })
607        .aligned()
608        .into_any()
609    }
610
611    fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
612        if let Some((chat, _)) = self.active_chat.as_ref() {
613            let body = self.input_editor.update(cx, |editor, cx| {
614                let body = editor.text(cx);
615                editor.clear(cx);
616                body
617            });
618
619            if let Some(task) = chat
620                .update(cx, |chat, cx| chat.send_message(body, cx))
621                .log_err()
622            {
623                task.detach();
624            }
625        }
626    }
627
628    fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
629        if let Some((chat, _)) = self.active_chat.as_ref() {
630            chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
631        }
632    }
633
634    fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
635        if let Some((chat, _)) = self.active_chat.as_ref() {
636            chat.update(cx, |channel, cx| {
637                channel.load_more_messages(cx);
638            })
639        }
640    }
641
642    pub fn select_channel(
643        &mut self,
644        selected_channel_id: u64,
645        cx: &mut ViewContext<ChatPanel>,
646    ) -> Task<Result<()>> {
647        if let Some((chat, _)) = &self.active_chat {
648            if chat.read(cx).channel().id == selected_channel_id {
649                return Task::ready(Ok(()));
650            }
651        }
652
653        let open_chat = self.channel_store.update(cx, |store, cx| {
654            store.open_channel_chat(selected_channel_id, cx)
655        });
656        cx.spawn(|this, mut cx| async move {
657            let chat = open_chat.await?;
658            this.update(&mut cx, |this, cx| {
659                this.markdown_data = Default::default();
660                this.set_active_chat(chat, cx);
661            })
662        })
663    }
664
665    fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
666        if let Some((chat, _)) = &self.active_chat {
667            let channel_id = chat.read(cx).channel().id;
668            if let Some(workspace) = self.workspace.upgrade(cx) {
669                ChannelView::open(channel_id, workspace, cx).detach();
670            }
671        }
672    }
673
674    fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
675        if let Some((chat, _)) = &self.active_chat {
676            let channel_id = chat.read(cx).channel().id;
677            ActiveCall::global(cx)
678                .update(cx, |call, cx| call.join_channel(channel_id, cx))
679                .detach_and_log_err(cx);
680        }
681    }
682}
683
684fn render_avatar(avatar: Option<Arc<ImageData>>, theme: &Arc<Theme>) -> AnyElement<ChatPanel> {
685    let avatar_style = theme.chat_panel.avatar;
686
687    avatar
688        .map(|avatar| {
689            Image::from_data(avatar)
690                .with_style(avatar_style.image)
691                .aligned()
692                .contained()
693                .with_corner_radius(avatar_style.outer_corner_radius)
694                .constrained()
695                .with_width(avatar_style.outer_width)
696                .with_height(avatar_style.outer_width)
697                .into_any()
698        })
699        .unwrap_or_else(|| {
700            Empty::new()
701                .constrained()
702                .with_width(avatar_style.outer_width)
703                .into_any()
704        })
705        .contained()
706        .with_style(theme.chat_panel.avatar_container)
707        .into_any()
708}
709
710fn render_remove(
711    message_id_to_remove: Option<u64>,
712    cx: &mut ViewContext<'_, '_, ChatPanel>,
713    theme: &Arc<Theme>,
714) -> AnyElement<ChatPanel> {
715    enum DeleteMessage {}
716
717    message_id_to_remove
718        .map(|id| {
719            MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
720                let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
721                render_icon_button(button_style, "icons/x.svg")
722                    .aligned()
723                    .into_any()
724            })
725            .with_padding(Padding::uniform(2.))
726            .with_cursor_style(CursorStyle::PointingHand)
727            .on_click(MouseButton::Left, move |_, this, cx| {
728                this.remove_message(id, cx);
729            })
730            .flex_float()
731            .into_any()
732        })
733        .unwrap_or_else(|| {
734            let style = theme.chat_panel.icon_button.default;
735
736            Empty::new()
737                .constrained()
738                .with_width(style.icon_width)
739                .aligned()
740                .constrained()
741                .with_width(style.button_width)
742                .with_height(style.button_width)
743                .contained()
744                .with_uniform_padding(2.)
745                .flex_float()
746                .into_any()
747        })
748}
749
750impl Entity for ChatPanel {
751    type Event = Event;
752}
753
754impl View for ChatPanel {
755    fn ui_name() -> &'static str {
756        "ChatPanel"
757    }
758
759    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
760        let theme = theme::current(cx);
761        let element = if self.client.user_id().is_some() {
762            self.render_channel(cx)
763        } else {
764            self.render_sign_in_prompt(&theme, cx)
765        };
766        element
767            .contained()
768            .with_style(theme.chat_panel.container)
769            .constrained()
770            .with_min_width(150.)
771            .into_any()
772    }
773
774    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
775        self.has_focus = true;
776        if matches!(
777            *self.client.status().borrow(),
778            client::Status::Connected { .. }
779        ) {
780            cx.focus(&self.input_editor);
781        }
782    }
783
784    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
785        self.has_focus = false;
786    }
787}
788
789impl Panel for ChatPanel {
790    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
791        settings::get::<ChatPanelSettings>(cx).dock
792    }
793
794    fn position_is_valid(&self, position: DockPosition) -> bool {
795        matches!(position, DockPosition::Left | DockPosition::Right)
796    }
797
798    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
799        settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
800            settings.dock = Some(position)
801        });
802    }
803
804    fn size(&self, cx: &gpui::WindowContext) -> f32 {
805        self.width
806            .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
807    }
808
809    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
810        self.width = size;
811        self.serialize(cx);
812        cx.notify();
813    }
814
815    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
816        self.active = active;
817        if active {
818            self.acknowledge_last_message(cx);
819            if !is_chat_feature_enabled(cx) {
820                cx.emit(Event::Dismissed);
821            }
822        }
823    }
824
825    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
826        (settings::get::<ChatPanelSettings>(cx).button && is_chat_feature_enabled(cx))
827            .then(|| "icons/conversations.svg")
828    }
829
830    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
831        ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
832    }
833
834    fn should_change_position_on_event(event: &Self::Event) -> bool {
835        matches!(event, Event::DockPositionChanged)
836    }
837
838    fn should_close_on_event(event: &Self::Event) -> bool {
839        matches!(event, Event::Dismissed)
840    }
841
842    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
843        self.has_focus
844    }
845
846    fn is_focus_event(event: &Self::Event) -> bool {
847        matches!(event, Event::Focus)
848    }
849}
850
851fn is_chat_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
852    cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
853}
854
855fn format_timestamp(
856    mut timestamp: OffsetDateTime,
857    mut now: OffsetDateTime,
858    local_timezone: UtcOffset,
859) -> String {
860    timestamp = timestamp.to_offset(local_timezone);
861    now = now.to_offset(local_timezone);
862
863    let today = now.date();
864    let date = timestamp.date();
865    let mut hour = timestamp.hour();
866    let mut part = "am";
867    if hour > 12 {
868        hour -= 12;
869        part = "pm";
870    }
871    if date == today {
872        format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
873    } else if date.next_day() == Some(today) {
874        format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
875    } else {
876        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
877    }
878}
879
880fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
881    Svg::new(svg_path)
882        .with_color(style.color)
883        .constrained()
884        .with_width(style.icon_width)
885        .aligned()
886        .constrained()
887        .with_width(style.button_width)
888        .with_height(style.button_width)
889        .contained()
890        .with_style(style.container)
891}