Cargo.lock 🔗
@@ -1558,6 +1558,7 @@ dependencies = [
"fuzzy",
"gpui",
"language",
+ "lazy_static",
"log",
"menu",
"notifications",
Max Brunsfeld created
Cargo.lock | 1
crates/collab/src/db/queries/channels.rs | 18 +
crates/collab_ui/Cargo.toml | 1
crates/collab_ui/src/chat_panel.rs | 69 ++---
crates/collab_ui/src/chat_panel/message_editor.rs | 218 +++++++++++++++++
crates/rpc/proto/zed.proto | 7
crates/rpc/src/proto.rs | 2
crates/theme/src/theme.rs | 1
styles/src/style_tree/chat_panel.ts | 1
9 files changed, 271 insertions(+), 47 deletions(-)
@@ -1558,6 +1558,7 @@ dependencies = [
"fuzzy",
"gpui",
"language",
+ "lazy_static",
"log",
"menu",
"notifications",
@@ -552,7 +552,8 @@ impl Database {
user_id: UserId,
) -> Result<Vec<proto::ChannelMember>> {
self.transaction(|tx| async move {
- self.check_user_is_channel_admin(channel_id, user_id, &*tx)
+ let user_membership = self
+ .check_user_is_channel_member(channel_id, user_id, &*tx)
.await?;
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
@@ -613,6 +614,14 @@ impl Database {
});
}
+ // If the user is not an admin, don't give them all of the details
+ if !user_membership.admin {
+ rows.retain_mut(|row| {
+ row.admin = false;
+ row.kind != proto::channel_member::Kind::Invitee as i32
+ });
+ }
+
Ok(rows)
})
.await
@@ -644,9 +653,9 @@ impl Database {
channel_id: ChannelId,
user_id: UserId,
tx: &DatabaseTransaction,
- ) -> Result<()> {
+ ) -> Result<channel_member::Model> {
let channel_ids = self.get_channel_ancestors(channel_id, tx).await?;
- channel_member::Entity::find()
+ Ok(channel_member::Entity::find()
.filter(
channel_member::Column::ChannelId
.is_in(channel_ids)
@@ -654,8 +663,7 @@ impl Database {
)
.one(&*tx)
.await?
- .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?;
- Ok(())
+ .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?)
}
pub async fn check_user_is_channel_admin(
@@ -54,6 +54,7 @@ zed-actions = {path = "../zed-actions"}
anyhow.workspace = true
futures.workspace = true
+lazy_static.workspace = true
log.workspace = true
schemars.workspace = true
postage.workspace = true
@@ -18,8 +18,9 @@ use gpui::{
AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
};
-use language::{language_settings::SoftWrap, LanguageRegistry};
+use language::LanguageRegistry;
use menu::Confirm;
+use message_editor::MessageEditor;
use project::Fs;
use rich_text::RichText;
use serde::{Deserialize, Serialize};
@@ -33,6 +34,8 @@ use workspace::{
Workspace,
};
+mod message_editor;
+
const MESSAGE_LOADING_THRESHOLD: usize = 50;
const CHAT_PANEL_KEY: &'static str = "ChatPanel";
@@ -42,7 +45,7 @@ pub struct ChatPanel {
languages: Arc<LanguageRegistry>,
active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
message_list: ListState<ChatPanel>,
- input_editor: ViewHandle<Editor>,
+ input_editor: ViewHandle<MessageEditor>,
channel_select: ViewHandle<Select>,
local_timezone: UtcOffset,
fs: Arc<dyn Fs>,
@@ -87,13 +90,18 @@ impl ChatPanel {
let languages = workspace.app_state().languages.clone();
let input_editor = cx.add_view(|cx| {
- let mut editor = Editor::auto_height(
- 4,
- Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
+ MessageEditor::new(
+ languages.clone(),
+ channel_store.clone(),
+ cx.add_view(|cx| {
+ Editor::auto_height(
+ 4,
+ Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
+ cx,
+ )
+ }),
cx,
- );
- editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
- editor
+ )
});
let workspace_handle = workspace.weak_handle();
@@ -138,7 +146,6 @@ impl ChatPanel {
client,
channel_store,
languages,
-
active_chat: Default::default(),
pending_serialization: Task::ready(None),
message_list,
@@ -187,25 +194,6 @@ impl ChatPanel {
})
.detach();
- let markdown = this.languages.language_for_name("Markdown");
- cx.spawn(|this, mut cx| async move {
- let markdown = markdown.await?;
-
- this.update(&mut cx, |this, cx| {
- this.input_editor.update(cx, |editor, cx| {
- editor.buffer().update(cx, |multi_buffer, cx| {
- multi_buffer
- .as_singleton()
- .unwrap()
- .update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx))
- })
- })
- })?;
-
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
-
this
})
}
@@ -269,15 +257,15 @@ impl ChatPanel {
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) {
- let id = chat.read(cx).channel().id;
- {
+ let id = {
let chat = chat.read(cx);
+ let channel = chat.channel().clone();
self.message_list.reset(chat.message_count());
- let placeholder = format!("Message #{}", chat.channel().name);
- self.input_editor.update(cx, move |editor, cx| {
- editor.set_placeholder_text(placeholder, cx);
+ self.input_editor.update(cx, |editor, cx| {
+ editor.set_channel(channel.clone(), cx);
});
- }
+ channel.id
+ };
let subscription = cx.subscribe(&chat, Self::channel_did_change);
self.active_chat = Some((chat, subscription));
self.acknowledge_last_message(cx);
@@ -606,14 +594,12 @@ impl ChatPanel {
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = self.active_chat.as_ref() {
- let body = self.input_editor.update(cx, |editor, cx| {
- let body = editor.text(cx);
- editor.clear(cx);
- body
- });
+ let message = self
+ .input_editor
+ .update(cx, |editor, cx| editor.take_message(cx));
if let Some(task) = chat
- .update(cx, |chat, cx| chat.send_message(body, cx))
+ .update(cx, |chat, cx| chat.send_message(message.text, cx))
.log_err()
{
task.detach();
@@ -747,7 +733,8 @@ impl View for ChatPanel {
*self.client.status().borrow(),
client::Status::Connected { .. }
) {
- cx.focus(&self.input_editor);
+ let editor = self.input_editor.read(cx).editor.clone();
+ cx.focus(&editor);
}
}
@@ -0,0 +1,218 @@
+use channel::{Channel, ChannelStore};
+use client::UserId;
+use collections::HashMap;
+use editor::{AnchorRangeExt, Editor};
+use gpui::{
+ elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
+ ViewContext, ViewHandle, WeakViewHandle,
+};
+use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
+use lazy_static::lazy_static;
+use project::search::SearchQuery;
+use std::{ops::Range, sync::Arc, time::Duration};
+
+const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
+
+lazy_static! {
+ static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
+ "@[-_\\w]+",
+ false,
+ false,
+ Default::default(),
+ Default::default()
+ )
+ .unwrap();
+}
+
+pub struct MessageEditor {
+ pub editor: ViewHandle<Editor>,
+ channel_store: ModelHandle<ChannelStore>,
+ users: HashMap<String, UserId>,
+ mentions: Vec<UserId>,
+ mentions_task: Option<Task<()>>,
+ channel: Option<Arc<Channel>>,
+}
+
+pub struct ChatMessage {
+ pub text: String,
+ pub mentions: Vec<(Range<usize>, UserId)>,
+}
+
+impl MessageEditor {
+ pub fn new(
+ language_registry: Arc<LanguageRegistry>,
+ channel_store: ModelHandle<ChannelStore>,
+ editor: ViewHandle<Editor>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ editor.update(cx, |editor, cx| {
+ editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+ });
+
+ let buffer = editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .expect("message editor must be singleton");
+
+ cx.subscribe(&buffer, Self::on_buffer_event).detach();
+ cx.subscribe(&editor, |_, _, event, cx| {
+ if let editor::Event::Focused = event {
+ eprintln!("focused");
+ cx.notify()
+ }
+ })
+ .detach();
+
+ let markdown = language_registry.language_for_name("Markdown");
+ cx.app_context()
+ .spawn(|mut cx| async move {
+ let markdown = markdown.await?;
+ buffer.update(&mut cx, |buffer, cx| {
+ buffer.set_language(Some(markdown), cx)
+ });
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+
+ Self {
+ editor,
+ channel_store,
+ users: HashMap::default(),
+ channel: None,
+ mentions: Vec::new(),
+ mentions_task: None,
+ }
+ }
+
+ pub fn set_channel(&mut self, channel: Arc<Channel>, cx: &mut ViewContext<Self>) {
+ self.editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text(format!("Message #{}", channel.name), cx);
+ });
+ self.channel = Some(channel);
+ self.refresh_users(cx);
+ }
+
+ pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(channel) = &self.channel {
+ let members = self.channel_store.update(cx, |store, cx| {
+ store.get_channel_member_details(channel.id, cx)
+ });
+ cx.spawn(|this, mut cx| async move {
+ let members = members.await?;
+ this.update(&mut cx, |this, _| {
+ this.users.clear();
+ this.users.extend(
+ members
+ .into_iter()
+ .map(|member| (member.user.github_login.clone(), member.user.id)),
+ );
+ })?;
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+
+ pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> ChatMessage {
+ self.editor.update(cx, |editor, cx| {
+ let highlights = editor.text_highlights::<Self>(cx);
+ let text = editor.text(cx);
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ let mentions = if let Some((_, ranges)) = highlights {
+ ranges
+ .iter()
+ .map(|range| range.to_offset(&snapshot))
+ .zip(self.mentions.iter().copied())
+ .collect()
+ } else {
+ Vec::new()
+ };
+
+ editor.clear(cx);
+ self.mentions.clear();
+
+ ChatMessage { text, mentions }
+ })
+ }
+
+ fn on_buffer_event(
+ &mut self,
+ buffer: ModelHandle<Buffer>,
+ event: &language::Event,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let language::Event::Reparsed | language::Event::Edited = event {
+ let buffer = buffer.read(cx).snapshot();
+ self.mentions_task = Some(cx.spawn(|this, cx| async move {
+ cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
+ Self::find_mentions(this, buffer, cx).await;
+ }));
+ }
+ }
+
+ async fn find_mentions(
+ this: WeakViewHandle<MessageEditor>,
+ buffer: BufferSnapshot,
+ mut cx: AsyncAppContext,
+ ) {
+ let (buffer, ranges) = cx
+ .background()
+ .spawn(async move {
+ let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
+ (buffer, ranges)
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ let mut anchor_ranges = Vec::new();
+ let mut mentioned_user_ids = Vec::new();
+ let mut text = String::new();
+
+ this.editor.update(cx, |editor, cx| {
+ let multi_buffer = editor.buffer().read(cx).snapshot(cx);
+ for range in ranges {
+ text.clear();
+ text.extend(buffer.text_for_range(range.clone()));
+ if let Some(username) = text.strip_prefix("@") {
+ if let Some(user_id) = this.users.get(username) {
+ let start = multi_buffer.anchor_after(range.start);
+ let end = multi_buffer.anchor_after(range.end);
+
+ mentioned_user_ids.push(*user_id);
+ anchor_ranges.push(start..end);
+ }
+ }
+ }
+
+ editor.clear_highlights::<Self>(cx);
+ editor.highlight_text::<Self>(
+ anchor_ranges,
+ theme::current(cx).chat_panel.mention_highlight,
+ cx,
+ )
+ });
+
+ this.mentions = mentioned_user_ids;
+ this.mentions_task.take();
+ })
+ .ok();
+ }
+}
+
+impl Entity for MessageEditor {
+ type Event = ();
+}
+
+impl View for MessageEditor {
+ fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
+ ChildView::new(&self.editor, cx).into_any()
+ }
+
+ fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+ if cx.is_self_focused() {
+ cx.focus(&self.editor);
+ }
+ }
+}
@@ -178,7 +178,8 @@ message Envelope {
NewNotification new_notification = 148;
GetNotifications get_notifications = 149;
GetNotificationsResponse get_notifications_response = 150;
- DeleteNotification delete_notification = 151; // Current max
+ DeleteNotification delete_notification = 151;
+ MarkNotificationsRead mark_notifications_read = 152; // Current max
}
}
@@ -1595,6 +1596,10 @@ message DeleteNotification {
uint64 notification_id = 1;
}
+message MarkNotificationsRead {
+ repeated uint64 notification_ids = 1;
+}
+
message Notification {
uint64 id = 1;
uint64 timestamp = 2;
@@ -210,6 +210,7 @@ messages!(
(LeaveProject, Foreground),
(LeaveRoom, Foreground),
(LinkChannel, Foreground),
+ (MarkNotificationsRead, Foreground),
(MoveChannel, Foreground),
(NewNotification, Foreground),
(OnTypeFormatting, Background),
@@ -326,6 +327,7 @@ request_messages!(
(LeaveChannelBuffer, Ack),
(LeaveRoom, Ack),
(LinkChannel, Ack),
+ (MarkNotificationsRead, Ack),
(MoveChannel, Ack),
(OnTypeFormatting, OnTypeFormattingResponse),
(OpenBufferById, OpenBufferResponse),
@@ -638,6 +638,7 @@ pub struct ChatPanel {
pub avatar: AvatarStyle,
pub avatar_container: ContainerStyle,
pub message: ChatMessage,
+ pub mention_highlight: HighlightStyle,
pub continuation_message: ChatMessage,
pub last_message_bottom_spacing: f32,
pub pending_message: ChatMessage,
@@ -91,6 +91,7 @@ export default function chat_panel(): any {
top: 4,
},
},
+ mention_highlight: { weight: 'bold' },
message: {
...interactive({
base: {