use crate::ChatPanelSettings; use anyhow::Result; use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelStore}; use client::Client; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use gpui::{ actions, elements::*, platform::{CursorStyle, MouseButton}, serde_json, views::{ItemType, Select, SelectStyle}, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::language_settings::SoftWrap; use menu::Confirm; use project::Fs; use serde::{Deserialize, Serialize}; use settings::SettingsStore; use std::sync::Arc; use theme::Theme; use time::{OffsetDateTime, UtcOffset}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, Workspace, }; const MESSAGE_LOADING_THRESHOLD: usize = 50; const CHAT_PANEL_KEY: &'static str = "ChatPanel"; pub struct ChatPanel { client: Arc, channel_store: ModelHandle, active_channel: Option<(ModelHandle, Subscription)>, message_list: ListState, input_editor: ViewHandle, channel_select: ViewHandle { let channel = &channel_store.read(cx).channel_at_index(ix).unwrap().1; let theme = match (item_type, is_hovered) { (ItemType::Header, _) => &theme.header, (ItemType::Selected, false) => &theme.active_item, (ItemType::Selected, true) => &theme.hovered_active_item, (ItemType::Unselected, false) => &theme.item, (ItemType::Unselected, true) => &theme.hovered_item, }; Flex::row() .with_child( Label::new("#".to_string(), theme.hash.text.clone()) .contained() .with_style(theme.hash.container), ) .with_child(Label::new(channel.name.clone(), theme.name.clone())) .contained() .with_style(theme.container) .into_any() } fn render_sign_in_prompt( &self, theme: &Arc, cx: &mut ViewContext, ) -> AnyElement { enum SignInPromptLabel {} MouseEventHandler::new::(0, cx, |mouse_state, _| { Label::new( "Sign in to use chat".to_string(), theme .chat_panel .sign_in_prompt .style_for(mouse_state) .clone(), ) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { let client = this.client.clone(); cx.spawn(|this, mut cx| async move { if client .authenticate_and_connect(true, &cx) .log_err() .await .is_some() { this.update(&mut cx, |this, cx| { if cx.handle().is_focused(cx) { cx.focus(&this.input_editor); } }) .ok(); } }) .detach(); }) .aligned() .into_any() } fn send(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some((channel, _)) = self.active_channel.as_ref() { let body = self.input_editor.update(cx, |editor, cx| { let body = editor.text(cx); editor.clear(cx); body }); if let Some(task) = channel .update(cx, |channel, cx| channel.send_message(body, cx)) .log_err() { task.detach(); } } } fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext) { if let Some((channel, _)) = self.active_channel.as_ref() { channel.update(cx, |channel, cx| { channel.load_more_messages(cx); }) } } } impl Entity for ChatPanel { type Event = Event; } impl View for ChatPanel { fn ui_name() -> &'static str { "ChatPanel" } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let theme = theme::current(cx); let element = if self.client.user_id().is_some() { self.render_channel(cx) } else { self.render_sign_in_prompt(&theme, cx) }; element .contained() .with_style(theme.chat_panel.container) .constrained() .with_min_width(150.) .into_any() } fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { if matches!( *self.client.status().borrow(), client::Status::Connected { .. } ) { cx.focus(&self.input_editor); } } } impl Panel for ChatPanel { fn position(&self, cx: &gpui::WindowContext) -> DockPosition { settings::get::(cx).dock } fn position_is_valid(&self, position: DockPosition) -> bool { matches!(position, DockPosition::Left | DockPosition::Right) } fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { settings::update_settings_file::(self.fs.clone(), cx, move |settings| { settings.dock = Some(position) }); } fn size(&self, cx: &gpui::WindowContext) -> f32 { self.width .unwrap_or_else(|| settings::get::(cx).default_width) } fn set_size(&mut self, size: Option, cx: &mut ViewContext) { self.width = size; self.serialize(cx); cx.notify(); } fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> { settings::get::(cx) .button .then(|| "icons/conversations.svg") } fn icon_tooltip(&self) -> (String, Option>) { ("Chat Panel".to_string(), Some(Box::new(ToggleFocus))) } fn should_change_position_on_event(event: &Self::Event) -> bool { matches!(event, Event::DockPositionChanged) } fn has_focus(&self, _cx: &gpui::WindowContext) -> bool { self.has_focus } fn is_focus_event(event: &Self::Event) -> bool { matches!(event, Event::Focus) } } fn format_timestamp( mut timestamp: OffsetDateTime, mut now: OffsetDateTime, local_timezone: UtcOffset, ) -> String { timestamp = timestamp.to_offset(local_timezone); now = now.to_offset(local_timezone); let today = now.date(); let date = timestamp.date(); let mut hour = timestamp.hour(); let mut part = "am"; if hour > 12 { hour -= 12; part = "pm"; } if date == today { format!("{:02}:{:02}{}", hour, timestamp.minute(), part) } else if date.next_day() == Some(today) { format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part) } else { format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year()) } }