Make chat prettier (to my eyes at least)

This commit is contained in:
Conrad Irwin 2024-01-13 21:37:13 -07:00
parent c2ff9fe2da
commit f6ef07e716
4 changed files with 101 additions and 30 deletions

View file

@ -144,7 +144,7 @@ impl ChannelChat {
message: MessageParams, message: MessageParams,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Result<Task<Result<u64>>> { ) -> Result<Task<Result<u64>>> {
if message.text.is_empty() { if message.text.trim().is_empty() {
Err(anyhow!("message body can't be empty"))?; Err(anyhow!("message body can't be empty"))?;
} }
@ -174,6 +174,8 @@ impl ChannelChat {
let user_store = self.user_store.clone(); let user_store = self.user_store.clone();
let rpc = self.rpc.clone(); let rpc = self.rpc.clone();
let outgoing_messages_lock = self.outgoing_messages_lock.clone(); let outgoing_messages_lock = self.outgoing_messages_lock.clone();
// todo - handle messages that fail to send (e.g. >1024 chars)
Ok(cx.spawn(move |this, mut cx| async move { Ok(cx.spawn(move |this, mut cx| async move {
let outgoing_message_guard = outgoing_messages_lock.lock().await; let outgoing_message_guard = outgoing_messages_lock.lock().await;
let request = rpc.request(proto::SendChannelMessage { let request = rpc.request(proto::SendChannelMessage {

View file

@ -8,8 +8,9 @@ use db::kvp::KEY_VALUE_STORE;
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
actions, div, list, prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, actions, div, list, prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext,
ClickEvent, ElementId, EventEmitter, FocusHandle, FocusableView, ListOffset, ListScrollEvent, ClickEvent, DismissEvent, ElementId, EventEmitter, FocusHandle, FocusableView, FontWeight,
ListState, Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView, ListOffset, ListScrollEvent, ListState, Model, Render, Subscription, Task, View, ViewContext,
VisualContext, WeakView,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use menu::Confirm; use menu::Confirm;
@ -22,7 +23,8 @@ use settings::{Settings, SettingsStore};
use std::sync::Arc; use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use ui::{ use ui::{
prelude::*, Avatar, Button, IconButton, IconName, Key, KeyBinding, Label, TabBar, Tooltip, popover_menu, prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, Key, KeyBinding,
Label, TabBar, Tooltip,
}; };
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
use workspace::{ use workspace::{
@ -60,6 +62,7 @@ pub struct ChatPanel {
is_scrolled_to_bottom: bool, is_scrolled_to_bottom: bool,
markdown_data: HashMap<ChannelMessageId, RichText>, markdown_data: HashMap<ChannelMessageId, RichText>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
open_context_menu: Option<(u64, Subscription)>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -128,6 +131,7 @@ impl ChatPanel {
width: None, width: None,
markdown_data: Default::default(), markdown_data: Default::default(),
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
open_context_menu: None,
}; };
let mut old_dock_position = this.position(cx); let mut old_dock_position = this.position(cx);
@ -348,50 +352,100 @@ impl ChatPanel {
ChannelMessageId::Saved(id) => ("saved-message", id).into(), ChannelMessageId::Saved(id) => ("saved-message", id).into(),
ChannelMessageId::Pending(id) => ("pending-message", id).into(), ChannelMessageId::Pending(id) => ("pending-message", id).into(),
}; };
let this = cx.view().clone();
v_stack() v_stack()
.w_full() .w_full()
.id(element_id)
.relative() .relative()
.overflow_hidden() .overflow_hidden()
.group("")
.when(!is_continuation_from_previous, |this| { .when(!is_continuation_from_previous, |this| {
this.child( this.pt_3().child(
h_stack() h_stack()
.gap_2() .child(
.child(Avatar::new(message.sender.avatar_uri.clone())) div().absolute().child(
.child(Label::new(message.sender.github_login.clone())) Avatar::new(message.sender.avatar_uri.clone())
.size(cx.rem_size() * 1.5),
),
)
.child(
div()
.pl(cx.rem_size() * 1.5 + px(6.0))
.pr(px(8.0))
.font_weight(FontWeight::BOLD)
.child(Label::new(message.sender.github_login.clone())),
)
.child( .child(
Label::new(format_timestamp( Label::new(format_timestamp(
message.timestamp, message.timestamp,
now, now,
self.local_timezone, self.local_timezone,
)) ))
.size(LabelSize::Small)
.color(Color::Muted), .color(Color::Muted),
), ),
) )
}) })
.when(!is_continuation_to_next, |this| .when(is_continuation_from_previous, |this| this.pt_1())
// HACK: This should really be a margin, but margins seem to get collapsed. .child(
this.pb_2()) v_stack()
.w_full()
.text_ui_sm()
.id(element_id)
.group("")
.child(text.element("body".into(), cx)) .child(text.element("body".into(), cx))
.child( .child(
div() div()
.absolute() .absolute()
.top_1() .z_index(1)
.right_2() .right_0()
.w_8() .w_6()
.visible_on_hover("") .bg(cx.theme().colors().panel_background)
.when(!self.has_open_menu(message_id_to_remove), |el| {
el.visible_on_hover("")
})
.children(message_id_to_remove.map(|message_id| { .children(message_id_to_remove.map(|message_id| {
IconButton::new(("remove", message_id), IconName::XCircle).on_click( popover_menu(("menu", message_id))
cx.listener(move |this, _, cx| { .trigger(IconButton::new(
this.remove_message(message_id, cx); ("trigger", message_id),
}), IconName::Ellipsis,
) ))
.menu(move |cx| {
Some(Self::render_message_menu(&this, message_id, cx))
})
})), })),
),
) )
} }
fn has_open_menu(&self, message_id: Option<u64>) -> bool {
match self.open_context_menu.as_ref() {
Some((id, _)) => Some(*id) == message_id,
None => false,
}
}
fn render_message_menu(
this: &View<Self>,
message_id: u64,
cx: &mut WindowContext,
) -> View<ContextMenu> {
let menu = {
let this = this.clone();
ContextMenu::build(cx, move |menu, _| {
menu.entry("Delete message", None, move |cx| {
this.update(cx, |this, cx| this.remove_message(message_id, cx))
})
})
};
this.update(cx, |this, cx| {
let subscription = cx.subscribe(&menu, |this: &mut Self, _, _: &DismissEvent, _| {
this.open_context_menu = None;
});
this.open_context_menu = Some((message_id, subscription));
});
menu
}
fn render_markdown_with_mentions( fn render_markdown_with_mentions(
language_registry: &Arc<LanguageRegistry>, language_registry: &Arc<LanguageRegistry>,
current_user_id: u64, current_user_id: u64,

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position, DefiniteLength, Display, Fill, FlexDirection, FontWeight, Hsla, JustifyContent, Length,
SharedString, StyleRefinement, Visibility, WhiteSpace, Position, SharedString, StyleRefinement, Visibility, WhiteSpace,
}; };
use crate::{BoxShadow, TextStyleRefinement}; use crate::{BoxShadow, TextStyleRefinement};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
@ -494,6 +494,13 @@ pub trait Styled: Sized {
self self
} }
fn font_weight(mut self, weight: FontWeight) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
.font_weight = Some(weight);
self
}
fn text_bg(mut self, bg: impl Into<Hsla>) -> Self { fn text_bg(mut self, bg: impl Into<Hsla>) -> Self {
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)

View file

@ -26,6 +26,7 @@ pub enum AvatarShape {
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Avatar { pub struct Avatar {
image: Img, image: Img,
size: Option<Pixels>,
border_color: Option<Hsla>, border_color: Option<Hsla>,
is_available: Option<bool>, is_available: Option<bool>,
} }
@ -36,7 +37,7 @@ impl RenderOnce for Avatar {
self = self.shape(AvatarShape::Circle); self = self.shape(AvatarShape::Circle);
} }
let size = cx.rem_size(); let size = self.size.unwrap_or_else(|| cx.rem_size());
div() div()
.size(size + px(2.)) .size(size + px(2.))
@ -78,6 +79,7 @@ impl Avatar {
image: img(src), image: img(src),
is_available: None, is_available: None,
border_color: None, border_color: None,
size: None,
} }
} }
@ -124,4 +126,10 @@ impl Avatar {
self.is_available = is_available.into(); self.is_available = is_available.into();
self self
} }
/// Size overrides the avatar size. By default they are 1rem.
pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
self.size = size.into();
self
}
} }