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}