chat_panel.rs

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