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