2023-09-08 20:28:19 +00:00
|
|
|
use crate::ChatPanelSettings;
|
2023-09-08 01:06:05 +00:00
|
|
|
use anyhow::Result;
|
2023-09-14 23:22:21 +00:00
|
|
|
use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
|
2023-09-07 18:16:51 +00:00
|
|
|
use client::Client;
|
2023-09-08 01:06:05 +00:00
|
|
|
use db::kvp::KEY_VALUE_STORE;
|
2023-09-07 18:16:51 +00:00
|
|
|
use editor::Editor;
|
|
|
|
use gpui::{
|
|
|
|
actions,
|
|
|
|
elements::*,
|
|
|
|
platform::{CursorStyle, MouseButton},
|
2023-09-08 01:06:05 +00:00
|
|
|
serde_json,
|
2023-09-07 18:16:51 +00:00
|
|
|
views::{ItemType, Select, SelectStyle},
|
2023-09-08 01:06:05 +00:00
|
|
|
AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
|
|
|
|
ViewContext, ViewHandle, WeakViewHandle,
|
2023-09-07 18:16:51 +00:00
|
|
|
};
|
|
|
|
use language::language_settings::SoftWrap;
|
|
|
|
use menu::Confirm;
|
2023-09-08 01:06:05 +00:00
|
|
|
use project::Fs;
|
|
|
|
use serde::{Deserialize, Serialize};
|
2023-09-08 20:28:19 +00:00
|
|
|
use settings::SettingsStore;
|
2023-09-07 18:16:51 +00:00
|
|
|
use std::sync::Arc;
|
2023-09-14 23:22:21 +00:00
|
|
|
use theme::{IconButton, Theme};
|
2023-09-07 18:16:51 +00:00
|
|
|
use time::{OffsetDateTime, UtcOffset};
|
|
|
|
use util::{ResultExt, TryFutureExt};
|
2023-09-08 01:06:05 +00:00
|
|
|
use workspace::{
|
|
|
|
dock::{DockPosition, Panel},
|
|
|
|
Workspace,
|
|
|
|
};
|
2023-09-07 18:16:51 +00:00
|
|
|
|
|
|
|
const MESSAGE_LOADING_THRESHOLD: usize = 50;
|
2023-09-08 01:06:05 +00:00
|
|
|
const CHAT_PANEL_KEY: &'static str = "ChatPanel";
|
2023-09-07 18:16:51 +00:00
|
|
|
|
|
|
|
pub struct ChatPanel {
|
|
|
|
client: Arc<Client>,
|
|
|
|
channel_store: ModelHandle<ChannelStore>,
|
2023-09-11 20:44:41 +00:00
|
|
|
active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
|
2023-09-07 18:16:51 +00:00
|
|
|
message_list: ListState<ChatPanel>,
|
|
|
|
input_editor: ViewHandle<Editor>,
|
|
|
|
channel_select: ViewHandle<Select>,
|
|
|
|
local_timezone: UtcOffset,
|
2023-09-08 01:06:05 +00:00
|
|
|
fs: Arc<dyn Fs>,
|
|
|
|
width: Option<f32>,
|
|
|
|
pending_serialization: Task<Option<()>>,
|
2023-09-08 20:28:19 +00:00
|
|
|
subscriptions: Vec<gpui::Subscription>,
|
2023-09-08 01:06:05 +00:00
|
|
|
has_focus: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
struct SerializedChatPanel {
|
|
|
|
width: Option<f32>,
|
2023-09-07 18:16:51 +00:00
|
|
|
}
|
|
|
|
|
2023-09-08 01:06:05 +00:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum Event {
|
|
|
|
DockPositionChanged,
|
|
|
|
Focus,
|
|
|
|
Dismissed,
|
|
|
|
}
|
2023-09-07 18:16:51 +00:00
|
|
|
|
2023-09-08 01:06:05 +00:00
|
|
|
actions!(chat_panel, [LoadMoreMessages, ToggleFocus]);
|
2023-09-07 18:16:51 +00:00
|
|
|
|
|
|
|
pub fn init(cx: &mut AppContext) {
|
|
|
|
cx.add_action(ChatPanel::send);
|
|
|
|
cx.add_action(ChatPanel::load_more_messages);
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ChatPanel {
|
2023-09-08 01:06:05 +00:00
|
|
|
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
|
|
|
let fs = workspace.app_state().fs.clone();
|
|
|
|
let client = workspace.app_state().client.clone();
|
|
|
|
let channel_store = workspace.app_state().channel_store.clone();
|
|
|
|
|
2023-09-07 18:16:51 +00:00
|
|
|
let input_editor = cx.add_view(|cx| {
|
|
|
|
let mut editor = Editor::auto_height(
|
|
|
|
4,
|
|
|
|
Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
|
|
|
|
cx,
|
|
|
|
);
|
|
|
|
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
|
|
|
editor
|
|
|
|
});
|
2023-09-08 01:06:05 +00:00
|
|
|
|
2023-09-07 18:16:51 +00:00
|
|
|
let channel_select = cx.add_view(|cx| {
|
2023-09-08 01:06:05 +00:00
|
|
|
let channel_store = channel_store.clone();
|
2023-09-07 18:16:51 +00:00
|
|
|
Select::new(0, cx, {
|
|
|
|
move |ix, item_type, is_hovered, cx| {
|
|
|
|
Self::render_channel_name(
|
2023-09-08 01:06:05 +00:00
|
|
|
&channel_store,
|
2023-09-07 18:16:51 +00:00
|
|
|
ix,
|
|
|
|
item_type,
|
|
|
|
is_hovered,
|
|
|
|
&theme::current(cx).chat_panel.channel_select,
|
|
|
|
cx,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.with_style(move |cx| {
|
|
|
|
let style = &theme::current(cx).chat_panel.channel_select;
|
|
|
|
SelectStyle {
|
|
|
|
header: style.header.container,
|
|
|
|
menu: style.menu,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let mut message_list =
|
|
|
|
ListState::<Self>::new(0, Orientation::Bottom, 1000., move |this, ix, cx| {
|
2023-09-14 23:22:21 +00:00
|
|
|
this.render_message(ix, cx)
|
2023-09-07 18:16:51 +00:00
|
|
|
});
|
|
|
|
message_list.set_scroll_handler(|visible_range, this, cx| {
|
|
|
|
if visible_range.start < MESSAGE_LOADING_THRESHOLD {
|
|
|
|
this.load_more_messages(&LoadMoreMessages, cx);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-09-08 01:06:05 +00:00
|
|
|
cx.add_view(|cx| {
|
|
|
|
let mut this = Self {
|
|
|
|
fs,
|
|
|
|
client,
|
|
|
|
channel_store,
|
2023-09-11 20:44:41 +00:00
|
|
|
active_chat: Default::default(),
|
2023-09-08 01:06:05 +00:00
|
|
|
pending_serialization: Task::ready(None),
|
|
|
|
message_list,
|
|
|
|
input_editor,
|
|
|
|
channel_select,
|
|
|
|
local_timezone: cx.platform().local_timezone(),
|
|
|
|
has_focus: false,
|
2023-09-08 20:28:19 +00:00
|
|
|
subscriptions: Vec::new(),
|
2023-09-08 01:06:05 +00:00
|
|
|
width: None,
|
|
|
|
};
|
2023-09-07 18:16:51 +00:00
|
|
|
|
2023-09-08 20:28:19 +00:00
|
|
|
let mut old_dock_position = this.position(cx);
|
|
|
|
this.subscriptions
|
|
|
|
.push(
|
|
|
|
cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
|
|
|
|
let new_dock_position = this.position(cx);
|
|
|
|
if new_dock_position != old_dock_position {
|
|
|
|
old_dock_position = new_dock_position;
|
|
|
|
cx.emit(Event::DockPositionChanged);
|
|
|
|
}
|
|
|
|
cx.notify();
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
2023-09-07 18:16:51 +00:00
|
|
|
this.init_active_channel(cx);
|
2023-09-08 01:06:05 +00:00
|
|
|
cx.observe(&this.channel_store, |this, _, cx| {
|
|
|
|
this.init_active_channel(cx);
|
|
|
|
})
|
|
|
|
.detach();
|
|
|
|
|
|
|
|
cx.observe(&this.channel_select, |this, channel_select, cx| {
|
|
|
|
let selected_ix = channel_select.read(cx).selected_index();
|
2023-09-11 20:44:41 +00:00
|
|
|
|
2023-09-08 01:06:05 +00:00
|
|
|
let selected_channel_id = this
|
|
|
|
.channel_store
|
|
|
|
.read(cx)
|
|
|
|
.channel_at_index(selected_ix)
|
|
|
|
.map(|e| e.1.id);
|
|
|
|
if let Some(selected_channel_id) = selected_channel_id {
|
2023-09-09 00:06:39 +00:00
|
|
|
this.select_channel(selected_channel_id, cx)
|
|
|
|
.detach_and_log_err(cx);
|
2023-09-08 01:06:05 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.detach();
|
|
|
|
|
|
|
|
this
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn load(
|
|
|
|
workspace: WeakViewHandle<Workspace>,
|
|
|
|
cx: AsyncAppContext,
|
|
|
|
) -> Task<Result<ViewHandle<Self>>> {
|
|
|
|
cx.spawn(|mut cx| async move {
|
|
|
|
let serialized_panel = if let Some(panel) = cx
|
|
|
|
.background()
|
|
|
|
.spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
|
|
|
|
.await
|
|
|
|
.log_err()
|
|
|
|
.flatten()
|
|
|
|
{
|
|
|
|
Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
|
|
|
|
workspace.update(&mut cx, |workspace, cx| {
|
|
|
|
let panel = Self::new(workspace, cx);
|
|
|
|
if let Some(serialized_panel) = serialized_panel {
|
|
|
|
panel.update(cx, |panel, cx| {
|
|
|
|
panel.width = serialized_panel.width;
|
|
|
|
cx.notify();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
panel
|
|
|
|
})
|
2023-09-07 18:16:51 +00:00
|
|
|
})
|
2023-09-08 01:06:05 +00:00
|
|
|
}
|
2023-09-07 18:16:51 +00:00
|
|
|
|
2023-09-08 01:06:05 +00:00
|
|
|
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
|
|
|
let width = self.width;
|
|
|
|
self.pending_serialization = cx.background().spawn(
|
|
|
|
async move {
|
|
|
|
KEY_VALUE_STORE
|
|
|
|
.write_kvp(
|
|
|
|
CHAT_PANEL_KEY.into(),
|
|
|
|
serde_json::to_string(&SerializedChatPanel { width })?,
|
|
|
|
)
|
|
|
|
.await?;
|
|
|
|
anyhow::Ok(())
|
|
|
|
}
|
|
|
|
.log_err(),
|
|
|
|
);
|
2023-09-07 18:16:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
|
|
|
|
let channel_count = self.channel_store.read(cx).channel_count();
|
|
|
|
self.message_list.reset(0);
|
2023-09-11 20:44:41 +00:00
|
|
|
self.active_chat = None;
|
2023-09-07 18:16:51 +00:00
|
|
|
self.channel_select.update(cx, |select, cx| {
|
|
|
|
select.set_item_count(channel_count, cx);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-09-11 20:44:41 +00:00
|
|
|
fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
|
|
|
|
if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
|
2023-09-09 00:06:39 +00:00
|
|
|
let id = chat.read(cx).channel().id;
|
2023-09-07 18:16:51 +00:00
|
|
|
{
|
2023-09-09 00:06:39 +00:00
|
|
|
let chat = chat.read(cx);
|
|
|
|
self.message_list.reset(chat.message_count());
|
|
|
|
let placeholder = format!("Message #{}", chat.channel().name);
|
2023-09-07 18:16:51 +00:00
|
|
|
self.input_editor.update(cx, move |editor, cx| {
|
|
|
|
editor.set_placeholder_text(placeholder, cx);
|
|
|
|
});
|
|
|
|
}
|
2023-09-09 00:06:39 +00:00
|
|
|
let subscription = cx.subscribe(&chat, Self::channel_did_change);
|
2023-09-11 20:44:41 +00:00
|
|
|
self.active_chat = Some((chat, subscription));
|
2023-09-09 00:06:39 +00:00
|
|
|
self.channel_select.update(cx, |select, cx| {
|
|
|
|
if let Some(ix) = self.channel_store.read(cx).index_of_channel(id) {
|
|
|
|
select.set_selected_index(ix, cx);
|
|
|
|
}
|
|
|
|
});
|
2023-09-09 00:29:16 +00:00
|
|
|
cx.notify();
|
2023-09-07 18:16:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn channel_did_change(
|
|
|
|
&mut self,
|
|
|
|
_: ModelHandle<ChannelChat>,
|
|
|
|
event: &ChannelChatEvent,
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
) {
|
|
|
|
match event {
|
|
|
|
ChannelChatEvent::MessagesUpdated {
|
|
|
|
old_range,
|
|
|
|
new_count,
|
|
|
|
} => {
|
|
|
|
self.message_list.splice(old_range.clone(), *new_count);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cx.notify();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
|
|
|
let theme = theme::current(cx);
|
|
|
|
Flex::column()
|
|
|
|
.with_child(
|
|
|
|
ChildView::new(&self.channel_select, cx)
|
|
|
|
.contained()
|
|
|
|
.with_style(theme.chat_panel.channel_select.container),
|
|
|
|
)
|
|
|
|
.with_child(self.render_active_channel_messages())
|
|
|
|
.with_child(self.render_input_box(&theme, cx))
|
|
|
|
.into_any()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn render_active_channel_messages(&self) -> AnyElement<Self> {
|
2023-09-11 20:44:41 +00:00
|
|
|
let messages = if self.active_chat.is_some() {
|
2023-09-07 18:16:51 +00:00
|
|
|
List::new(self.message_list.clone()).into_any()
|
|
|
|
} else {
|
|
|
|
Empty::new().into_any()
|
|
|
|
};
|
|
|
|
|
|
|
|
messages.flex(1., true).into_any()
|
|
|
|
}
|
|
|
|
|
2023-09-14 23:22:21 +00:00
|
|
|
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
|
|
|
let message = self.active_chat.as_ref().unwrap().0.read(cx).message(ix);
|
|
|
|
|
2023-09-07 18:16:51 +00:00
|
|
|
let now = OffsetDateTime::now_utc();
|
|
|
|
let theme = theme::current(cx);
|
2023-09-14 23:22:21 +00:00
|
|
|
let style = if message.is_pending() {
|
2023-09-07 18:16:51 +00:00
|
|
|
&theme.chat_panel.pending_message
|
|
|
|
} else {
|
|
|
|
&theme.chat_panel.message
|
|
|
|
};
|
|
|
|
|
2023-09-14 23:22:21 +00:00
|
|
|
let belongs_to_user = Some(message.sender.id) == self.client.user_id();
|
|
|
|
let message_id_to_remove =
|
|
|
|
if let (ChannelMessageId::Saved(id), true) = (message.id, belongs_to_user) {
|
|
|
|
Some(id)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
|
|
|
|
enum DeleteMessage {}
|
|
|
|
|
|
|
|
let body = message.body.clone();
|
2023-09-07 18:16:51 +00:00
|
|
|
Flex::column()
|
|
|
|
.with_child(
|
|
|
|
Flex::row()
|
|
|
|
.with_child(
|
|
|
|
Label::new(
|
|
|
|
message.sender.github_login.clone(),
|
2023-09-14 23:22:21 +00:00
|
|
|
style.sender.text.clone(),
|
2023-09-07 18:16:51 +00:00
|
|
|
)
|
|
|
|
.contained()
|
2023-09-14 23:22:21 +00:00
|
|
|
.with_style(style.sender.container),
|
2023-09-07 18:16:51 +00:00
|
|
|
)
|
|
|
|
.with_child(
|
|
|
|
Label::new(
|
|
|
|
format_timestamp(message.timestamp, now, self.local_timezone),
|
2023-09-14 23:22:21 +00:00
|
|
|
style.timestamp.text.clone(),
|
2023-09-07 18:16:51 +00:00
|
|
|
)
|
|
|
|
.contained()
|
2023-09-14 23:22:21 +00:00
|
|
|
.with_style(style.timestamp.container),
|
|
|
|
)
|
|
|
|
.with_children(message_id_to_remove.map(|id| {
|
|
|
|
MouseEventHandler::new::<DeleteMessage, _>(
|
|
|
|
id as usize,
|
|
|
|
cx,
|
|
|
|
|mouse_state, _| {
|
|
|
|
let button_style =
|
|
|
|
theme.collab_panel.contact_button.style_for(mouse_state);
|
|
|
|
render_icon_button(button_style, "icons/x.svg")
|
|
|
|
.aligned()
|
|
|
|
.into_any()
|
|
|
|
},
|
|
|
|
)
|
|
|
|
.with_padding(Padding::uniform(2.))
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
|
|
this.remove_message(id, cx);
|
|
|
|
})
|
|
|
|
.flex_float()
|
|
|
|
})),
|
2023-09-07 18:16:51 +00:00
|
|
|
)
|
2023-09-14 23:22:21 +00:00
|
|
|
.with_child(Text::new(body, style.body.clone()))
|
2023-09-07 18:16:51 +00:00
|
|
|
.contained()
|
2023-09-14 23:22:21 +00:00
|
|
|
.with_style(style.container)
|
2023-09-07 18:16:51 +00:00
|
|
|
.into_any()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
|
|
|
|
ChildView::new(&self.input_editor, cx)
|
|
|
|
.contained()
|
|
|
|
.with_style(theme.chat_panel.input_editor.container)
|
|
|
|
.into_any()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn render_channel_name(
|
|
|
|
channel_store: &ModelHandle<ChannelStore>,
|
|
|
|
ix: usize,
|
|
|
|
item_type: ItemType,
|
|
|
|
is_hovered: bool,
|
|
|
|
theme: &theme::ChannelSelect,
|
|
|
|
cx: &AppContext,
|
|
|
|
) -> AnyElement<Select> {
|
|
|
|
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<Theme>,
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
) -> AnyElement<Self> {
|
|
|
|
enum SignInPromptLabel {}
|
|
|
|
|
|
|
|
MouseEventHandler::new::<SignInPromptLabel, _>(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<Self>) {
|
2023-09-11 20:44:41 +00:00
|
|
|
if let Some((chat, _)) = self.active_chat.as_ref() {
|
2023-09-07 18:16:51 +00:00
|
|
|
let body = self.input_editor.update(cx, |editor, cx| {
|
|
|
|
let body = editor.text(cx);
|
|
|
|
editor.clear(cx);
|
|
|
|
body
|
|
|
|
});
|
|
|
|
|
2023-09-11 20:44:41 +00:00
|
|
|
if let Some(task) = chat
|
|
|
|
.update(cx, |chat, cx| chat.send_message(body, cx))
|
2023-09-07 18:16:51 +00:00
|
|
|
.log_err()
|
|
|
|
{
|
|
|
|
task.detach();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-14 23:22:21 +00:00
|
|
|
fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
|
|
|
|
if let Some((chat, _)) = self.active_chat.as_ref() {
|
|
|
|
chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-07 18:16:51 +00:00
|
|
|
fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
|
2023-09-11 20:44:41 +00:00
|
|
|
if let Some((chat, _)) = self.active_chat.as_ref() {
|
|
|
|
chat.update(cx, |channel, cx| {
|
2023-09-07 18:16:51 +00:00
|
|
|
channel.load_more_messages(cx);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2023-09-09 00:06:39 +00:00
|
|
|
|
|
|
|
pub fn select_channel(
|
|
|
|
&mut self,
|
|
|
|
selected_channel_id: u64,
|
|
|
|
cx: &mut ViewContext<ChatPanel>,
|
|
|
|
) -> Task<Result<()>> {
|
2023-09-11 20:44:41 +00:00
|
|
|
if let Some((chat, _)) = &self.active_chat {
|
|
|
|
if chat.read(cx).channel().id == selected_channel_id {
|
|
|
|
return Task::ready(Ok(()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-09 00:06:39 +00:00
|
|
|
let open_chat = self.channel_store.update(cx, |store, cx| {
|
|
|
|
store.open_channel_chat(selected_channel_id, cx)
|
|
|
|
});
|
|
|
|
cx.spawn(|this, mut cx| async move {
|
|
|
|
let chat = open_chat.await?;
|
|
|
|
this.update(&mut cx, |this, cx| {
|
2023-09-11 20:44:41 +00:00
|
|
|
this.set_active_chat(chat, cx);
|
2023-09-09 00:06:39 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2023-09-07 18:16:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Entity for ChatPanel {
|
|
|
|
type Event = Event;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl View for ChatPanel {
|
|
|
|
fn ui_name() -> &'static str {
|
|
|
|
"ChatPanel"
|
|
|
|
}
|
|
|
|
|
|
|
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
|
|
|
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<Self>) {
|
|
|
|
if matches!(
|
|
|
|
*self.client.status().borrow(),
|
|
|
|
client::Status::Connected { .. }
|
|
|
|
) {
|
|
|
|
cx.focus(&self.input_editor);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-08 01:06:05 +00:00
|
|
|
impl Panel for ChatPanel {
|
|
|
|
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
2023-09-08 20:28:19 +00:00
|
|
|
settings::get::<ChatPanelSettings>(cx).dock
|
2023-09-08 01:06:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn position_is_valid(&self, position: DockPosition) -> bool {
|
|
|
|
matches!(position, DockPosition::Left | DockPosition::Right)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
2023-09-08 20:28:19 +00:00
|
|
|
settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
|
|
|
|
settings.dock = Some(position)
|
|
|
|
});
|
2023-09-08 01:06:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn size(&self, cx: &gpui::WindowContext) -> f32 {
|
|
|
|
self.width
|
2023-09-08 20:28:19 +00:00
|
|
|
.unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
|
2023-09-08 01:06:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
|
|
|
|
self.width = size;
|
|
|
|
self.serialize(cx);
|
|
|
|
cx.notify();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
|
2023-09-08 20:28:19 +00:00
|
|
|
settings::get::<ChatPanelSettings>(cx)
|
2023-09-08 01:06:05 +00:00
|
|
|
.button
|
|
|
|
.then(|| "icons/conversations.svg")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
|
|
|
|
("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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-07 18:16:51 +00:00
|
|
|
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())
|
|
|
|
}
|
|
|
|
}
|
2023-09-14 23:22:21 +00:00
|
|
|
|
|
|
|
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<ChatPanel> {
|
|
|
|
Svg::new(svg_path)
|
|
|
|
.with_color(style.color)
|
|
|
|
.constrained()
|
|
|
|
.with_width(style.icon_width)
|
|
|
|
.aligned()
|
|
|
|
.constrained()
|
|
|
|
.with_width(style.button_width)
|
|
|
|
.with_height(style.button_width)
|
|
|
|
.contained()
|
|
|
|
.with_style(style.container)
|
|
|
|
}
|