chat_panel.rs

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