chat panel ++ (#4044)

- Update chat panel with current channel
- Open chat panel for guests
- Open chat when joining a channel with guests
- Some tweaks for chat panels
- Don't lose focus on default panel state
- Make chat prettier (to my eyes at least)
- Fix multiple mentions in one message
- Show a border when scrolled in chat
- Fix re-docking chat panel
- Move settings subscription to dock

[[PR Description]]

Release Notes:

- Opens chat by default when joining a public channel
- Improves chat panel UI
This commit is contained in:
Conrad Irwin 2024-01-14 13:54:10 -07:00 committed by GitHub
commit 29ce109211
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 273 additions and 254 deletions

View File

@ -40,7 +40,7 @@ use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset a
use project::Project; use project::Project;
use search::{buffer_search::DivRegistrar, BufferSearchBar}; use search::{buffer_search::DivRegistrar, BufferSearchBar};
use semantic_index::{SemanticIndex, SemanticIndexStatus}; use semantic_index::{SemanticIndex, SemanticIndexStatus};
use settings::{Settings, SettingsStore}; use settings::Settings;
use std::{ use std::{
cell::Cell, cell::Cell,
cmp, cmp,
@ -165,7 +165,7 @@ impl AssistantPanel {
cx.on_focus_in(&focus_handle, Self::focus_in).detach(); cx.on_focus_in(&focus_handle, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, Self::focus_out).detach(); cx.on_focus_out(&focus_handle, Self::focus_out).detach();
let mut this = Self { Self {
workspace: workspace_handle, workspace: workspace_handle,
active_editor_index: Default::default(), active_editor_index: Default::default(),
prev_active_editor_index: Default::default(), prev_active_editor_index: Default::default(),
@ -190,20 +190,7 @@ impl AssistantPanel {
_watch_saved_conversations, _watch_saved_conversations,
semantic_index, semantic_index,
retrieve_context_in_next_inline_assist: false, retrieve_context_in_next_inline_assist: false,
}; }
let mut old_dock_position = this.position(cx);
this.subscriptions =
vec![cx.observe_global::<SettingsStore>(move |this, cx| {
let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position {
old_dock_position = new_dock_position;
cx.emit(PanelEvent::ChangePosition);
}
cx.notify();
})];
this
}) })
}) })
}) })
@ -3133,6 +3120,7 @@ mod tests {
use crate::MessageId; use crate::MessageId;
use ai::test::FakeCompletionProvider; use ai::test::FakeCompletionProvider;
use gpui::AppContext; use gpui::AppContext;
use settings::SettingsStore;
#[gpui::test] #[gpui::test]
fn test_inserting_and_removing_messages(cx: &mut AppContext) { fn test_inserting_and_removing_messages(cx: &mut AppContext) {

View File

@ -442,6 +442,8 @@ impl ActiveCall {
.location .location
.as_ref() .as_ref()
.and_then(|location| location.upgrade()); .and_then(|location| location.upgrade());
let channel_id = room.read(cx).channel_id();
cx.emit(Event::RoomJoined { channel_id });
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx)) room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
} }
} else { } else {

View File

@ -26,6 +26,9 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event { pub enum Event {
RoomJoined {
channel_id: Option<u64>,
},
ParticipantLocationChanged { ParticipantLocationChanged {
participant_id: proto::PeerId, participant_id: proto::PeerId,
}, },
@ -49,7 +52,9 @@ pub enum Event {
RemoteProjectInvitationDiscarded { RemoteProjectInvitationDiscarded {
project_id: u64, project_id: u64,
}, },
Left, Left {
channel_id: Option<u64>,
},
} }
pub struct Room { pub struct Room {
@ -357,7 +362,9 @@ impl Room {
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> { pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify(); cx.notify();
cx.emit(Event::Left); cx.emit(Event::Left {
channel_id: self.channel_id(),
});
self.leave_internal(cx) self.leave_internal(cx)
} }
@ -598,6 +605,14 @@ impl Room {
.map(|participant| participant.role) .map(|participant| participant.role)
} }
pub fn contains_guests(&self) -> bool {
self.local_participant.role == proto::ChannelRole::Guest
|| self
.remote_participants
.values()
.any(|p| p.role == proto::ChannelRole::Guest)
}
pub fn local_participant_is_admin(&self) -> bool { pub fn local_participant_is_admin(&self) -> bool {
self.local_participant.role == proto::ChannelRole::Admin self.local_participant.role == proto::ChannelRole::Admin
} }

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

@ -256,6 +256,7 @@ impl Database {
message_id = result.last_insert_id; message_id = result.last_insert_id;
let mentioned_user_ids = let mentioned_user_ids =
mentions.iter().map(|m| m.user_id).collect::<HashSet<_>>(); mentions.iter().map(|m| m.user_id).collect::<HashSet<_>>();
let mentions = mentions let mentions = mentions
.iter() .iter()
.filter_map(|mention| { .filter_map(|mention| {

View File

@ -1,15 +1,15 @@
use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings}; use crate::{collab_panel, is_channels_feature_enabled, ChatPanelSettings};
use anyhow::Result; use anyhow::Result;
use call::ActiveCall; use call::{room, ActiveCall};
use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
use client::Client; use client::Client;
use collections::HashMap; use collections::HashMap;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
actions, div, list, prelude::*, px, AnyElement, AppContext, AsyncWindowContext, ClickEvent, actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, DismissEvent,
ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, Model, Render, ElementId, EventEmitter, FocusHandle, FocusableView, FontWeight, ListOffset, ListScrollEvent,
Subscription, Task, View, ViewContext, VisualContext, WeakView, ListState, Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use menu::Confirm; use menu::Confirm;
@ -17,10 +17,13 @@ use message_editor::MessageEditor;
use project::Fs; use project::Fs;
use rich_text::RichText; use rich_text::RichText;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use ui::{prelude::*, Avatar, Button, IconButton, IconName, Label, TabBar, Tooltip}; use ui::{
popover_menu, prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label,
TabBar,
};
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
@ -54,9 +57,10 @@ pub struct ChatPanel {
active: bool, active: bool,
pending_serialization: Task<Option<()>>, pending_serialization: Task<Option<()>>,
subscriptions: Vec<gpui::Subscription>, subscriptions: Vec<gpui::Subscription>,
workspace: WeakView<Workspace>,
is_scrolled_to_bottom: bool, is_scrolled_to_bottom: bool,
markdown_data: HashMap<ChannelMessageId, RichText>, markdown_data: HashMap<ChannelMessageId, RichText>,
focus_handle: FocusHandle,
open_context_menu: Option<(u64, Subscription)>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -64,13 +68,6 @@ struct SerializedChatPanel {
width: Option<Pixels>, width: Option<Pixels>,
} }
#[derive(Debug)]
pub enum Event {
DockPositionChanged,
Focus,
Dismissed,
}
actions!(chat_panel, [ToggleFocus]); actions!(chat_panel, [ToggleFocus]);
impl ChatPanel { impl ChatPanel {
@ -89,8 +86,6 @@ impl ChatPanel {
) )
}); });
let workspace_handle = workspace.weak_handle();
cx.new_view(|cx: &mut ViewContext<Self>| { cx.new_view(|cx: &mut ViewContext<Self>| {
let view = cx.view().downgrade(); let view = cx.view().downgrade();
let message_list = let message_list =
@ -108,7 +103,7 @@ impl ChatPanel {
if event.visible_range.start < MESSAGE_LOADING_THRESHOLD { if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
this.load_more_messages(cx); this.load_more_messages(cx);
} }
this.is_scrolled_to_bottom = event.visible_range.end == event.count; this.is_scrolled_to_bottom = !event.is_scrolled;
})); }));
let mut this = Self { let mut this = Self {
@ -122,22 +117,37 @@ impl ChatPanel {
message_editor: input_editor, message_editor: input_editor,
local_timezone: cx.local_timezone(), local_timezone: cx.local_timezone(),
subscriptions: Vec::new(), subscriptions: Vec::new(),
workspace: workspace_handle,
is_scrolled_to_bottom: true, is_scrolled_to_bottom: true,
active: false, active: false,
width: None, width: None,
markdown_data: Default::default(), markdown_data: Default::default(),
focus_handle: cx.focus_handle(),
open_context_menu: None,
}; };
let mut old_dock_position = this.position(cx); this.subscriptions.push(cx.subscribe(
this.subscriptions.push(cx.observe_global::<SettingsStore>( &ActiveCall::global(cx),
move |this: &mut Self, cx| { move |this: &mut Self, call, event: &room::Event, cx| match event {
let new_dock_position = this.position(cx); room::Event::RoomJoined { channel_id } => {
if new_dock_position != old_dock_position { if let Some(channel_id) = channel_id {
old_dock_position = new_dock_position; this.select_channel(*channel_id, None, cx)
cx.emit(Event::DockPositionChanged); .detach_and_log_err(cx);
if call
.read(cx)
.room()
.is_some_and(|room| room.read(cx).contains_guests())
{
cx.emit(PanelEvent::Activate)
}
}
} }
cx.notify(); room::Event::Left { channel_id } => {
if channel_id == &this.channel_id(cx) {
cx.emit(PanelEvent::Close)
}
}
_ => {}
}, },
)); ));
@ -145,6 +155,12 @@ impl ChatPanel {
}) })
} }
pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
self.active_chat
.as_ref()
.map(|(chat, _)| chat.read(cx).channel_id)
}
pub fn is_scrolled_to_bottom(&self) -> bool { pub fn is_scrolled_to_bottom(&self) -> bool {
self.is_scrolled_to_bottom self.is_scrolled_to_bottom
} }
@ -259,53 +275,9 @@ impl ChatPanel {
} }
} }
fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
v_stack()
.full()
.on_action(cx.listener(Self::send))
.child(
h_stack().z_index(1).child(
TabBar::new("chat_header")
.child(
h_stack()
.w_full()
.h(rems(ui::Tab::HEIGHT_IN_REMS))
.px_2()
.child(Label::new(
self.active_chat
.as_ref()
.and_then(|c| {
Some(format!("#{}", c.0.read(cx).channel(cx)?.name))
})
.unwrap_or_default(),
)),
)
.end_child(
IconButton::new("notes", IconName::File)
.on_click(cx.listener(Self::open_notes))
.tooltip(|cx| Tooltip::text("Open notes", cx)),
)
.end_child(
IconButton::new("call", IconName::AudioOn)
.on_click(cx.listener(Self::join_call))
.tooltip(|cx| Tooltip::text("Join call", cx)),
),
),
)
.child(div().flex_grow().px_2().py_1().map(|this| {
if self.active_chat.is_some() {
this.child(list(self.message_list.clone()).full())
} else {
this
}
}))
.child(h_stack().p_2().child(self.message_editor.clone()))
.into_any()
}
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_chat = &self.active_chat.as_ref().unwrap().0; let active_chat = &self.active_chat.as_ref().unwrap().0;
let (message, is_continuation_from_previous, is_continuation_to_next, is_admin) = let (message, is_continuation_from_previous, is_admin) =
active_chat.update(cx, |active_chat, cx| { active_chat.update(cx, |active_chat, cx| {
let is_admin = self let is_admin = self
.channel_store .channel_store
@ -314,13 +286,9 @@ impl ChatPanel {
let last_message = active_chat.message(ix.saturating_sub(1)); let last_message = active_chat.message(ix.saturating_sub(1));
let this_message = active_chat.message(ix).clone(); let this_message = active_chat.message(ix).clone();
let next_message =
active_chat.message(ix.saturating_add(1).min(active_chat.message_count() - 1));
let is_continuation_from_previous = last_message.id != this_message.id let is_continuation_from_previous = last_message.id != this_message.id
&& last_message.sender.id == this_message.sender.id; && last_message.sender.id == this_message.sender.id;
let is_continuation_to_next = this_message.id != next_message.id
&& this_message.sender.id == next_message.sender.id;
if let ChannelMessageId::Saved(id) = this_message.id { if let ChannelMessageId::Saved(id) = this_message.id {
if this_message if this_message
@ -332,12 +300,7 @@ impl ChatPanel {
} }
} }
( (this_message, is_continuation_from_previous, is_admin)
this_message,
is_continuation_from_previous,
is_continuation_to_next,
is_admin,
)
}); });
let _is_pending = message.is_pending(); let _is_pending = message.is_pending();
@ -360,50 +323,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.
this.pb_2())
.child(text.element("body".into(), cx))
.child( .child(
div() v_stack()
.absolute() .w_full()
.top_1() .text_ui_sm()
.right_2() .id(element_id)
.w_8() .group("")
.visible_on_hover("") .child(text.element("body".into(), cx))
.children(message_id_to_remove.map(|message_id| { .child(
IconButton::new(("remove", message_id), IconName::XCircle).on_click( div()
cx.listener(move |this, _, cx| { .absolute()
this.remove_message(message_id, cx); .z_index(1)
}), .right_0()
) .w_6()
})), .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| {
popover_menu(("menu", message_id))
.trigger(IconButton::new(
("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,
@ -421,44 +434,6 @@ impl ChatPanel {
rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None) rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
} }
fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_stack()
.gap_2()
.p_4()
.child(
Button::new("sign-in", "Sign in")
.style(ButtonStyle::Filled)
.icon_color(Color::Muted)
.icon(IconName::Github)
.icon_position(IconPosition::Start)
.full_width()
.on_click(cx.listener(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, |_, cx| {
cx.focus_self();
})
.ok();
}
})
.detach();
})),
)
.child(
div().flex().w_full().items_center().child(
Label::new("Sign in to chat.")
.color(Color::Muted)
.size(LabelSize::Small),
),
)
}
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) { fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = self.active_chat.as_ref() { if let Some((chat, _)) = self.active_chat.as_ref() {
let message = self let message = self
@ -535,50 +510,93 @@ impl ChatPanel {
Ok(()) Ok(())
}) })
} }
fn open_notes(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = &self.active_chat {
let channel_id = chat.read(cx).channel_id;
if let Some(workspace) = self.workspace.upgrade() {
ChannelView::open(channel_id, workspace, cx).detach();
}
}
}
fn join_call(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = &self.active_chat {
let channel_id = chat.read(cx).channel_id;
ActiveCall::global(cx)
.update(cx, |call, cx| call.join_channel(channel_id, cx))
.detach_and_log_err(cx);
}
}
} }
impl EventEmitter<Event> for ChatPanel {}
impl Render for ChatPanel { impl Render for ChatPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_stack() v_stack()
.size_full() .track_focus(&self.focus_handle)
.map(|this| match (self.client.user_id(), self.active_chat()) { .full()
(Some(_), Some(_)) => this.child(self.render_channel(cx)), .on_action(cx.listener(Self::send))
(Some(_), None) => this.child( .child(
div().p_4().child( h_stack().z_index(1).child(
Label::new("Select a channel to chat in.") TabBar::new("chat_header").child(
.size(LabelSize::Small) h_stack()
.color(Color::Muted), .w_full()
.h(rems(ui::Tab::HEIGHT_IN_REMS))
.px_2()
.child(Label::new(
self.active_chat
.as_ref()
.and_then(|c| {
Some(format!("#{}", c.0.read(cx).channel(cx)?.name))
})
.unwrap_or("Chat".to_string()),
)),
), ),
), ),
(None, _) => this.child(self.render_sign_in_prompt(cx)), )
}) .child(div().flex_grow().px_2().pt_1().map(|this| {
.min_w(px(150.)) if self.active_chat.is_some() {
this.child(list(self.message_list.clone()).full())
} else {
this.child(
div()
.p_4()
.child(
Label::new("Select a channel to chat in.")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
div().pt_1().w_full().items_center().child(
Button::new("toggle-collab", "Open")
.full_width()
.key_binding(KeyBinding::for_action(
&collab_panel::ToggleFocus,
cx,
))
.on_click(|_, cx| {
cx.dispatch_action(
collab_panel::ToggleFocus.boxed_clone(),
)
}),
),
),
)
}
}))
.child(
h_stack()
.when(!self.is_scrolled_to_bottom, |el| {
el.border_t_1().border_color(cx.theme().colors().border)
})
.p_2()
.map(|el| {
if self.active_chat.is_some() {
el.child(self.message_editor.clone())
} else {
el.child(
div()
.rounded_md()
.h_7()
.w_full()
.bg(cx.theme().colors().editor_background),
)
}
}),
)
.into_any()
} }
} }
impl FocusableView for ChatPanel { impl FocusableView for ChatPanel {
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
self.message_editor.read(cx).focus_handle(cx) if self.active_chat.is_some() {
self.message_editor.read(cx).focus_handle(cx)
} else {
self.focus_handle.clone()
}
} }
} }
@ -613,7 +631,7 @@ impl Panel for ChatPanel {
if active { if active {
self.acknowledge_last_message(cx); self.acknowledge_last_message(cx);
if !is_channels_feature_enabled(cx) { if !is_channels_feature_enabled(cx) {
cx.emit(Event::Dismissed); cx.emit(PanelEvent::Close);
} }
} }
} }

View File

@ -26,7 +26,7 @@ use menu::{Cancel, Confirm, SelectNext, SelectPrev};
use project::{Fs, Project}; use project::{Fs, Project};
use rpc::proto::{self, PeerId}; use rpc::proto::{self, PeerId};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{mem, sync::Arc}; use std::{mem, sync::Arc};
use theme::{ActiveTheme, ThemeSettings}; use theme::{ActiveTheme, ThemeSettings};
@ -254,19 +254,6 @@ impl CollabPanel {
this.update_entries(false, cx); this.update_entries(false, cx);
// Update the dock position when the setting changes.
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(PanelEvent::ChangePosition);
}
cx.notify();
},
));
let active_call = ActiveCall::global(cx); let active_call = ActiveCall::global(cx);
this.subscriptions this.subscriptions
.push(cx.observe(&this.user_store, |this, _, cx| { .push(cx.observe(&this.user_store, |this, _, cx| {

View File

@ -58,7 +58,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
} }
} }
room::Event::Left => { room::Event::Left { .. } => {
for (_, windows) in notification_windows.drain() { for (_, windows) in notification_windows.drain() {
for window in windows { for window in windows {
window window

View File

@ -43,6 +43,7 @@ pub enum ListAlignment {
pub struct ListScrollEvent { pub struct ListScrollEvent {
pub visible_range: Range<usize>, pub visible_range: Range<usize>,
pub count: usize, pub count: usize,
pub is_scrolled: bool,
} }
#[derive(Clone)] #[derive(Clone)]
@ -253,6 +254,7 @@ impl StateInner {
&ListScrollEvent { &ListScrollEvent {
visible_range, visible_range,
count: self.items.summary().count, count: self.items.summary().count,
is_scrolled: self.logical_scroll_top.is_some(),
}, },
cx, cx,
); );

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

@ -1,6 +1,6 @@
pub mod file_associations; pub mod file_associations;
mod project_panel_settings; mod project_panel_settings;
use settings::{Settings, SettingsStore}; use settings::Settings;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor}; use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
@ -246,18 +246,6 @@ impl ProjectPanel {
}; };
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
// Update the dock position when the setting changes.
let mut old_dock_position = this.position(cx);
ProjectPanelSettings::register(cx);
cx.observe_global::<SettingsStore>(move |this, cx| {
let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position {
old_dock_position = new_dock_position;
cx.emit(PanelEvent::ChangePosition);
}
})
.detach();
this this
}); });

View File

@ -39,6 +39,7 @@ pub struct RichText {
/// Allows one to specify extra links to the rendered markdown, which can be used /// Allows one to specify extra links to the rendered markdown, which can be used
/// for e.g. mentions. /// for e.g. mentions.
#[derive(Debug)]
pub struct Mention { pub struct Mention {
pub range: Range<usize>, pub range: Range<usize>,
pub is_self_mention: bool, pub is_self_mention: bool,
@ -113,20 +114,21 @@ pub fn render_markdown_mut(
if let Some(language) = &current_language { if let Some(language) = &current_language {
render_code(text, highlights, t.as_ref(), language); render_code(text, highlights, t.as_ref(), language);
} else { } else {
if let Some(mention) = mentions.first() { while let Some(mention) = mentions.first() {
if source_range.contains_inclusive(&mention.range) { if !source_range.contains_inclusive(&mention.range) {
mentions = &mentions[1..]; break;
let range = (prev_len + mention.range.start - source_range.start)
..(prev_len + mention.range.end - source_range.start);
highlights.push((
range.clone(),
if mention.is_self_mention {
Highlight::SelfMention
} else {
Highlight::Mention
},
));
} }
mentions = &mentions[1..];
let range = (prev_len + mention.range.start - source_range.start)
..(prev_len + mention.range.end - source_range.start);
highlights.push((
range.clone(),
if mention.is_self_mention {
Highlight::SelfMention
} else {
Highlight::Mention
},
));
} }
text.push_str(t.as_ref()); text.push_str(t.as_ref());

View File

@ -11,7 +11,7 @@ use itertools::Itertools;
use project::{Fs, ProjectEntryId}; use project::{Fs, ProjectEntryId};
use search::{buffer_search::DivRegistrar, BufferSearchBar}; use search::{buffer_search::DivRegistrar, BufferSearchBar};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::Settings;
use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings}; use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
use ui::{h_stack, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip}; use ui::{h_stack, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip};
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
@ -159,15 +159,6 @@ impl TerminalPanel {
height: None, height: None,
_subscriptions: subscriptions, _subscriptions: subscriptions,
}; };
let mut old_dock_position = this.position(cx);
cx.observe_global::<SettingsStore>(move |this, cx| {
let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position {
old_dock_position = new_dock_position;
cx.emit(PanelEvent::ChangePosition);
}
})
.detach();
this this
} }

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

View File

@ -7,6 +7,7 @@ use gpui::{
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::SettingsStore;
use std::sync::Arc; use std::sync::Arc;
use ui::{h_stack, ContextMenu, IconButton, Tooltip}; use ui::{h_stack, ContextMenu, IconButton, Tooltip};
use ui::{prelude::*, right_click_menu}; use ui::{prelude::*, right_click_menu};
@ -14,7 +15,6 @@ use ui::{prelude::*, right_click_menu};
const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.); const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.);
pub enum PanelEvent { pub enum PanelEvent {
ChangePosition,
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
Activate, Activate,
@ -177,7 +177,7 @@ impl DockPosition {
struct PanelEntry { struct PanelEntry {
panel: Arc<dyn PanelHandle>, panel: Arc<dyn PanelHandle>,
_subscriptions: [Subscription; 2], _subscriptions: [Subscription; 3],
} }
pub struct PanelButtons { pub struct PanelButtons {
@ -321,9 +321,15 @@ impl Dock {
) { ) {
let subscriptions = [ let subscriptions = [
cx.observe(&panel, |_, _, cx| cx.notify()), cx.observe(&panel, |_, _, cx| cx.notify()),
cx.subscribe(&panel, move |this, panel, event, cx| match event { cx.observe_global::<SettingsStore>({
PanelEvent::ChangePosition => { let workspace = workspace.clone();
let panel = panel.clone();
move |this, cx| {
let new_position = panel.read(cx).position(cx); let new_position = panel.read(cx).position(cx);
if new_position == this.position {
return;
}
let Ok(new_dock) = workspace.update(cx, |workspace, cx| { let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
if panel.is_zoomed(cx) { if panel.is_zoomed(cx) {
@ -354,6 +360,8 @@ impl Dock {
} }
}); });
} }
}),
cx.subscribe(&panel, move |this, panel, event, cx| match event {
PanelEvent::ZoomIn => { PanelEvent::ZoomIn => {
this.set_panel_zoomed(&panel.to_any(), true, cx); this.set_panel_zoomed(&panel.to_any(), true, cx);
if !panel.focus_handle(cx).contains_focused(cx) { if !panel.focus_handle(cx).contains_focused(cx) {
@ -737,7 +745,7 @@ pub mod test {
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) { fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
self.position = position; self.position = position;
cx.emit(PanelEvent::ChangePosition); cx.update_global::<SettingsStore, _>(|_, _| {});
} }
fn size(&self, _: &WindowContext) -> Pixels { fn size(&self, _: &WindowContext) -> Pixels {