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// }