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}