From 3a437fd888ad584970445854d8ad94af0f2a6fe6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 8 Sep 2025 21:53:17 -0600 Subject: [PATCH] Remove Chat (#37789) At RustConf we were demo'ing zed, and it continually popped open the chat panel. We're usually inured to this because the Chat panel doesn't open unless a Guest is in the channel, but it made me sad that we were showing a long stream of vacuous comments and unresponded to questions on every demo screen. We may bring chat back in the future, but we need more thought on the UX, and we need to rebuild the backend to not use the existing collab server that we're trying to move off of. Release Notes: - Removed the chat feature from Zed (Sorry to the 5 of you who use this on the regular!) --- Cargo.lock | 5 - assets/settings/default.json | 10 - crates/auto_update_ui/src/auto_update_ui.rs | 3 +- crates/channel/Cargo.toml | 2 - crates/channel/src/channel.rs | 6 - crates/channel/src/channel_chat.rs | 861 ---------- crates/channel/src/channel_store.rs | 149 +- crates/channel/src/channel_store_tests.rs | 201 +-- crates/collab/src/db.rs | 3 - crates/collab/src/db/queries.rs | 1 - crates/collab/src/db/queries/channels.rs | 8 - crates/collab/src/db/queries/messages.rs | 725 --------- crates/collab/src/db/queries/rooms.rs | 1 - crates/collab/src/db/tests.rs | 11 +- crates/collab/src/db/tests/channel_tests.rs | 37 +- crates/collab/src/db/tests/message_tests.rs | 421 ----- crates/collab/src/rpc.rs | 318 +--- crates/collab/src/tests.rs | 1 - .../collab/src/tests/channel_message_tests.rs | 725 --------- crates/collab_ui/Cargo.toml | 3 - crates/collab_ui/src/chat_panel.rs | 1380 ----------------- .../src/chat_panel/message_editor.rs | 548 ------- crates/collab_ui/src/collab_panel.rs | 100 +- crates/collab_ui/src/collab_ui.rs | 7 +- crates/collab_ui/src/notification_panel.rs | 118 +- crates/collab_ui/src/panel_settings.rs | 46 - crates/notifications/Cargo.toml | 1 - .../notifications/src/notification_store.rs | 68 +- crates/proto/proto/channel.proto | 5 +- crates/rpc/src/notification.rs | 11 - crates/vim/src/command.rs | 1 - crates/zed/src/zed.rs | 14 - docs/src/configuring-zed.md | 22 - docs/src/visual-customization.md | 8 - 34 files changed, 54 insertions(+), 5766 deletions(-) delete mode 100644 crates/channel/src/channel_chat.rs delete mode 100644 crates/collab/src/db/queries/messages.rs delete mode 100644 crates/collab/src/db/tests/message_tests.rs delete mode 100644 crates/collab/src/tests/channel_message_tests.rs delete mode 100644 crates/collab_ui/src/chat_panel.rs delete mode 100644 crates/collab_ui/src/chat_panel/message_editor.rs diff --git a/Cargo.lock b/Cargo.lock index eee80226fa69e8fa076246f34d7009f135352e4c..3c25fc5b008332d3c63ceb52b36d7b4b44a132cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2883,11 +2883,9 @@ dependencies = [ "language", "log", "postage", - "rand 0.9.1", "release_channel", "rpc", "settings", - "sum_tree", "text", "time", "util", @@ -3375,12 +3373,10 @@ dependencies = [ "collections", "db", "editor", - "emojis", "futures 0.3.31", "fuzzy", "gpui", "http_client", - "language", "log", "menu", "notifications", @@ -3388,7 +3384,6 @@ dependencies = [ "pretty_assertions", "project", "release_channel", - "rich_text", "rpc", "schemars", "serde", diff --git a/assets/settings/default.json b/assets/settings/default.json index 63a11403d3dd4b30926a6a1f32e86dadf3804054..2f04687925374992bbea42e13218e52635ec23a5 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -740,16 +740,6 @@ // Default width of the collaboration panel. "default_width": 240 }, - "chat_panel": { - // When to show the chat panel button in the status bar. - // Can be 'never', 'always', or 'when_in_call', - // or a boolean (interpreted as 'never'/'always'). - "button": "when_in_call", - // Where to dock the chat panel. Can be 'left' or 'right'. - "dock": "right", - // Default width of the chat panel. - "default_width": 240 - }, "git_panel": { // Whether to show the git panel button in the status bar. "button": true, diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index 729ab425d0d78faa3f13f62a3a785e3c2a60e830..efac14968ea48d93ae35089d239916de1f0a5253 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -1,5 +1,4 @@ use auto_update::AutoUpdater; -use client::proto::UpdateNotification; use editor::{Editor, MultiBuffer}; use gpui::{App, Context, DismissEvent, Entity, Window, actions, prelude::*}; use http_client::HttpClient; @@ -138,6 +137,8 @@ pub fn notify_if_app_was_updated(cx: &mut App) { return; } + struct UpdateNotification; + let should_show_notification = updater.read(cx).should_show_update_notification(cx); cx.spawn(async move |cx| { let should_show_notification = should_show_notification.await?; diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index 962847f3f1cf21f361b6e2f1b9299c0c66992b3e..ab6e1dfc2b8dd0f89c4e6cd03e5ee66840003d6a 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -25,11 +25,9 @@ gpui.workspace = true language.workspace = true log.workspace = true postage.workspace = true -rand.workspace = true release_channel.workspace = true rpc.workspace = true settings.workspace = true -sum_tree.workspace = true text.workspace = true time.workspace = true util.workspace = true diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index 63865c574ecc36da27e18f02ccb8c44138cef3ba..6cc5a0e8815a4f24f41b3677622f8c200a4f59d9 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -1,5 +1,4 @@ mod channel_buffer; -mod channel_chat; mod channel_store; use client::{Client, UserStore}; @@ -7,10 +6,6 @@ use gpui::{App, Entity}; use std::sync::Arc; pub use channel_buffer::{ACKNOWLEDGE_DEBOUNCE_INTERVAL, ChannelBuffer, ChannelBufferEvent}; -pub use channel_chat::{ - ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, MessageParams, - mentions_to_proto, -}; pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore}; #[cfg(test)] @@ -19,5 +14,4 @@ mod channel_store_tests; pub fn init(client: &Arc, user_store: Entity, cx: &mut App) { channel_store::init(client, user_store, cx); channel_buffer::init(&client.clone().into()); - channel_chat::init(&client.clone().into()); } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs deleted file mode 100644 index 776499c8760f13fbd2903780b1e234f8755d9860..0000000000000000000000000000000000000000 --- a/crates/channel/src/channel_chat.rs +++ /dev/null @@ -1,861 +0,0 @@ -use crate::{Channel, ChannelStore}; -use anyhow::{Context as _, Result}; -use client::{ - ChannelId, Client, Subscription, TypedEnvelope, UserId, proto, - user::{User, UserStore}, -}; -use collections::HashSet; -use futures::lock::Mutex; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity}; -use rand::prelude::*; -use rpc::AnyProtoClient; -use std::{ - ops::{ControlFlow, Range}, - sync::Arc, -}; -use sum_tree::{Bias, Dimensions, SumTree}; -use time::OffsetDateTime; -use util::{ResultExt as _, TryFutureExt, post_inc}; - -pub struct ChannelChat { - pub channel_id: ChannelId, - messages: SumTree, - acknowledged_message_ids: HashSet, - channel_store: Entity, - loaded_all_messages: bool, - last_acknowledged_id: Option, - next_pending_message_id: usize, - first_loaded_message_id: Option, - user_store: Entity, - rpc: Arc, - outgoing_messages_lock: Arc>, - rng: StdRng, - _subscription: Subscription, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct MessageParams { - pub text: String, - pub mentions: Vec<(Range, UserId)>, - pub reply_to_message_id: Option, -} - -#[derive(Clone, Debug)] -pub struct ChannelMessage { - pub id: ChannelMessageId, - pub body: String, - pub timestamp: OffsetDateTime, - pub sender: Arc, - pub nonce: u128, - pub mentions: Vec<(Range, UserId)>, - pub reply_to_message_id: Option, - pub edited_at: Option, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ChannelMessageId { - Saved(u64), - Pending(usize), -} - -impl From for Option { - fn from(val: ChannelMessageId) -> Self { - match val { - ChannelMessageId::Saved(id) => Some(id), - ChannelMessageId::Pending(_) => None, - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct ChannelMessageSummary { - max_id: ChannelMessageId, - count: usize, -} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -struct Count(usize); - -#[derive(Clone, Debug, PartialEq)] -pub enum ChannelChatEvent { - MessagesUpdated { - old_range: Range, - new_count: usize, - }, - UpdateMessage { - message_id: ChannelMessageId, - message_ix: usize, - }, - NewMessage { - channel_id: ChannelId, - message_id: u64, - }, -} - -impl EventEmitter for ChannelChat {} -pub fn init(client: &AnyProtoClient) { - client.add_entity_message_handler(ChannelChat::handle_message_sent); - client.add_entity_message_handler(ChannelChat::handle_message_removed); - client.add_entity_message_handler(ChannelChat::handle_message_updated); -} - -impl ChannelChat { - pub async fn new( - channel: Arc, - channel_store: Entity, - user_store: Entity, - client: Arc, - cx: &mut AsyncApp, - ) -> Result> { - let channel_id = channel.id; - let subscription = client.subscribe_to_entity(channel_id.0).unwrap(); - - let response = client - .request(proto::JoinChannelChat { - channel_id: channel_id.0, - }) - .await?; - - let handle = cx.new(|cx| { - cx.on_release(Self::release).detach(); - Self { - channel_id: channel.id, - user_store: user_store.clone(), - channel_store, - rpc: client.clone(), - outgoing_messages_lock: Default::default(), - messages: Default::default(), - acknowledged_message_ids: Default::default(), - loaded_all_messages: false, - next_pending_message_id: 0, - last_acknowledged_id: None, - rng: StdRng::from_os_rng(), - first_loaded_message_id: None, - _subscription: subscription.set_entity(&cx.entity(), &cx.to_async()), - } - })?; - Self::handle_loaded_messages( - handle.downgrade(), - user_store, - client, - response.messages, - response.done, - cx, - ) - .await?; - Ok(handle) - } - - fn release(&mut self, _: &mut App) { - self.rpc - .send(proto::LeaveChannelChat { - channel_id: self.channel_id.0, - }) - .log_err(); - } - - pub fn channel(&self, cx: &App) -> Option> { - self.channel_store - .read(cx) - .channel_for_id(self.channel_id) - .cloned() - } - - pub fn client(&self) -> &Arc { - &self.rpc - } - - pub fn send_message( - &mut self, - message: MessageParams, - cx: &mut Context, - ) -> Result>> { - anyhow::ensure!( - !message.text.trim().is_empty(), - "message body can't be empty" - ); - - let current_user = self - .user_store - .read(cx) - .current_user() - .context("current_user is not present")?; - - let channel_id = self.channel_id; - let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id)); - let nonce = self.rng.random(); - self.insert_messages( - SumTree::from_item( - ChannelMessage { - id: pending_id, - body: message.text.clone(), - sender: current_user, - timestamp: OffsetDateTime::now_utc(), - mentions: message.mentions.clone(), - nonce, - reply_to_message_id: message.reply_to_message_id, - edited_at: None, - }, - &(), - ), - cx, - ); - let user_store = self.user_store.clone(); - let rpc = self.rpc.clone(); - let outgoing_messages_lock = self.outgoing_messages_lock.clone(); - - // todo - handle messages that fail to send (e.g. >1024 chars) - Ok(cx.spawn(async move |this, cx| { - let outgoing_message_guard = outgoing_messages_lock.lock().await; - let request = rpc.request(proto::SendChannelMessage { - channel_id: channel_id.0, - body: message.text, - nonce: Some(nonce.into()), - mentions: mentions_to_proto(&message.mentions), - reply_to_message_id: message.reply_to_message_id, - }); - let response = request.await?; - drop(outgoing_message_guard); - let response = response.message.context("invalid message")?; - let id = response.id; - let message = ChannelMessage::from_proto(response, &user_store, cx).await?; - this.update(cx, |this, cx| { - this.insert_messages(SumTree::from_item(message, &()), cx); - if this.first_loaded_message_id.is_none() { - this.first_loaded_message_id = Some(id); - } - })?; - Ok(id) - })) - } - - pub fn remove_message(&mut self, id: u64, cx: &mut Context) -> Task> { - let response = self.rpc.request(proto::RemoveChannelMessage { - channel_id: self.channel_id.0, - message_id: id, - }); - cx.spawn(async move |this, cx| { - response.await?; - this.update(cx, |this, cx| { - this.message_removed(id, cx); - })?; - Ok(()) - }) - } - - pub fn update_message( - &mut self, - id: u64, - message: MessageParams, - cx: &mut Context, - ) -> Result>> { - self.message_update( - ChannelMessageId::Saved(id), - message.text.clone(), - message.mentions.clone(), - Some(OffsetDateTime::now_utc()), - cx, - ); - - let nonce: u128 = self.rng.random(); - - let request = self.rpc.request(proto::UpdateChannelMessage { - channel_id: self.channel_id.0, - message_id: id, - body: message.text, - nonce: Some(nonce.into()), - mentions: mentions_to_proto(&message.mentions), - }); - Ok(cx.spawn(async move |_, _| { - request.await?; - Ok(()) - })) - } - - pub fn load_more_messages(&mut self, cx: &mut Context) -> Option>> { - if self.loaded_all_messages { - return None; - } - - let rpc = self.rpc.clone(); - let user_store = self.user_store.clone(); - let channel_id = self.channel_id; - let before_message_id = self.first_loaded_message_id()?; - Some(cx.spawn(async move |this, cx| { - async move { - let response = rpc - .request(proto::GetChannelMessages { - channel_id: channel_id.0, - before_message_id, - }) - .await?; - Self::handle_loaded_messages( - this, - user_store, - rpc, - response.messages, - response.done, - cx, - ) - .await?; - - anyhow::Ok(()) - } - .log_err() - .await - })) - } - - pub fn first_loaded_message_id(&mut self) -> Option { - self.first_loaded_message_id - } - - /// Load a message by its id, if it's already stored locally. - pub fn find_loaded_message(&self, id: u64) -> Option<&ChannelMessage> { - self.messages.iter().find(|message| match message.id { - ChannelMessageId::Saved(message_id) => message_id == id, - ChannelMessageId::Pending(_) => false, - }) - } - - /// Load all of the chat messages since a certain message id. - /// - /// For now, we always maintain a suffix of the channel's messages. - pub async fn load_history_since_message( - chat: Entity, - message_id: u64, - mut cx: AsyncApp, - ) -> Option { - loop { - let step = chat - .update(&mut cx, |chat, cx| { - if let Some(first_id) = chat.first_loaded_message_id() - && first_id <= message_id - { - let mut cursor = chat - .messages - .cursor::>(&()); - let message_id = ChannelMessageId::Saved(message_id); - cursor.seek(&message_id, Bias::Left); - return ControlFlow::Break( - if cursor - .item() - .is_some_and(|message| message.id == message_id) - { - Some(cursor.start().1.0) - } else { - None - }, - ); - } - ControlFlow::Continue(chat.load_more_messages(cx)) - }) - .log_err()?; - match step { - ControlFlow::Break(ix) => return ix, - ControlFlow::Continue(task) => task?.await?, - } - } - } - - pub fn acknowledge_last_message(&mut self, cx: &mut Context) { - if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id - && self - .last_acknowledged_id - .is_none_or(|acknowledged_id| acknowledged_id < latest_message_id) - { - self.rpc - .send(proto::AckChannelMessage { - channel_id: self.channel_id.0, - message_id: latest_message_id, - }) - .ok(); - self.last_acknowledged_id = Some(latest_message_id); - self.channel_store.update(cx, |store, cx| { - store.acknowledge_message_id(self.channel_id, latest_message_id, cx); - }); - } - } - - async fn handle_loaded_messages( - this: WeakEntity, - user_store: Entity, - rpc: Arc, - proto_messages: Vec, - loaded_all_messages: bool, - cx: &mut AsyncApp, - ) -> Result<()> { - let loaded_messages = messages_from_proto(proto_messages, &user_store, cx).await?; - - let first_loaded_message_id = loaded_messages.first().map(|m| m.id); - let loaded_message_ids = this.read_with(cx, |this, _| { - let mut loaded_message_ids: HashSet = HashSet::default(); - for message in loaded_messages.iter() { - if let Some(saved_message_id) = message.id.into() { - loaded_message_ids.insert(saved_message_id); - } - } - for message in this.messages.iter() { - if let Some(saved_message_id) = message.id.into() { - loaded_message_ids.insert(saved_message_id); - } - } - loaded_message_ids - })?; - - let missing_ancestors = loaded_messages - .iter() - .filter_map(|message| { - if let Some(ancestor_id) = message.reply_to_message_id - && !loaded_message_ids.contains(&ancestor_id) - { - return Some(ancestor_id); - } - None - }) - .collect::>(); - - let loaded_ancestors = if missing_ancestors.is_empty() { - None - } else { - let response = rpc - .request(proto::GetChannelMessagesById { - message_ids: missing_ancestors, - }) - .await?; - Some(messages_from_proto(response.messages, &user_store, cx).await?) - }; - this.update(cx, |this, cx| { - this.first_loaded_message_id = first_loaded_message_id.and_then(|msg_id| msg_id.into()); - this.loaded_all_messages = loaded_all_messages; - this.insert_messages(loaded_messages, cx); - if let Some(loaded_ancestors) = loaded_ancestors { - this.insert_messages(loaded_ancestors, cx); - } - })?; - - Ok(()) - } - - pub fn rejoin(&mut self, cx: &mut Context) { - let user_store = self.user_store.clone(); - let rpc = self.rpc.clone(); - let channel_id = self.channel_id; - cx.spawn(async move |this, cx| { - async move { - let response = rpc - .request(proto::JoinChannelChat { - channel_id: channel_id.0, - }) - .await?; - Self::handle_loaded_messages( - this.clone(), - user_store.clone(), - rpc.clone(), - response.messages, - response.done, - cx, - ) - .await?; - - let pending_messages = this.read_with(cx, |this, _| { - this.pending_messages().cloned().collect::>() - })?; - - for pending_message in pending_messages { - let request = rpc.request(proto::SendChannelMessage { - channel_id: channel_id.0, - body: pending_message.body, - mentions: mentions_to_proto(&pending_message.mentions), - nonce: Some(pending_message.nonce.into()), - reply_to_message_id: pending_message.reply_to_message_id, - }); - let response = request.await?; - let message = ChannelMessage::from_proto( - response.message.context("invalid message")?, - &user_store, - cx, - ) - .await?; - this.update(cx, |this, cx| { - this.insert_messages(SumTree::from_item(message, &()), cx); - })?; - } - - anyhow::Ok(()) - } - .log_err() - .await - }) - .detach(); - } - - pub fn message_count(&self) -> usize { - self.messages.summary().count - } - - pub fn messages(&self) -> &SumTree { - &self.messages - } - - pub fn message(&self, ix: usize) -> &ChannelMessage { - let mut cursor = self.messages.cursor::(&()); - cursor.seek(&Count(ix), Bias::Right); - cursor.item().unwrap() - } - - pub fn acknowledge_message(&mut self, id: u64) { - if self.acknowledged_message_ids.insert(id) { - self.rpc - .send(proto::AckChannelMessage { - channel_id: self.channel_id.0, - message_id: id, - }) - .ok(); - } - } - - pub fn messages_in_range(&self, range: Range) -> impl Iterator { - let mut cursor = self.messages.cursor::(&()); - cursor.seek(&Count(range.start), Bias::Right); - cursor.take(range.len()) - } - - pub fn pending_messages(&self) -> impl Iterator { - let mut cursor = self.messages.cursor::(&()); - cursor.seek(&ChannelMessageId::Pending(0), Bias::Left); - cursor - } - - async fn handle_message_sent( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let user_store = this.read_with(&cx, |this, _| this.user_store.clone())?; - let message = message.payload.message.context("empty message")?; - let message_id = message.id; - - let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?; - this.update(&mut cx, |this, cx| { - this.insert_messages(SumTree::from_item(message, &()), cx); - cx.emit(ChannelChatEvent::NewMessage { - channel_id: this.channel_id, - message_id, - }) - })?; - - Ok(()) - } - - async fn handle_message_removed( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - this.message_removed(message.payload.message_id, cx) - })?; - Ok(()) - } - - async fn handle_message_updated( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let user_store = this.read_with(&cx, |this, _| this.user_store.clone())?; - let message = message.payload.message.context("empty message")?; - - let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?; - - this.update(&mut cx, |this, cx| { - this.message_update( - message.id, - message.body, - message.mentions, - message.edited_at, - cx, - ) - })?; - Ok(()) - } - - fn insert_messages(&mut self, messages: SumTree, cx: &mut Context) { - if let Some((first_message, last_message)) = messages.first().zip(messages.last()) { - let nonces = messages - .cursor::<()>(&()) - .map(|m| m.nonce) - .collect::>(); - - let mut old_cursor = self - .messages - .cursor::>(&()); - let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left); - let start_ix = old_cursor.start().1.0; - let removed_messages = old_cursor.slice(&last_message.id, Bias::Right); - let removed_count = removed_messages.summary().count; - let new_count = messages.summary().count; - let end_ix = start_ix + removed_count; - - new_messages.append(messages, &()); - - let mut ranges = Vec::>::new(); - if new_messages.last().unwrap().is_pending() { - new_messages.append(old_cursor.suffix(), &()); - } else { - new_messages.append( - old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left), - &(), - ); - - while let Some(message) = old_cursor.item() { - let message_ix = old_cursor.start().1.0; - if nonces.contains(&message.nonce) { - if ranges.last().is_some_and(|r| r.end == message_ix) { - ranges.last_mut().unwrap().end += 1; - } else { - ranges.push(message_ix..message_ix + 1); - } - } else { - new_messages.push(message.clone(), &()); - } - old_cursor.next(); - } - } - - drop(old_cursor); - self.messages = new_messages; - - for range in ranges.into_iter().rev() { - cx.emit(ChannelChatEvent::MessagesUpdated { - old_range: range, - new_count: 0, - }); - } - cx.emit(ChannelChatEvent::MessagesUpdated { - old_range: start_ix..end_ix, - new_count, - }); - - cx.notify(); - } - } - - fn message_removed(&mut self, id: u64, cx: &mut Context) { - let mut cursor = self.messages.cursor::(&()); - let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left); - if let Some(item) = cursor.item() - && item.id == ChannelMessageId::Saved(id) - { - let deleted_message_ix = messages.summary().count; - cursor.next(); - messages.append(cursor.suffix(), &()); - drop(cursor); - self.messages = messages; - - // If the message that was deleted was the last acknowledged message, - // replace the acknowledged message with an earlier one. - self.channel_store.update(cx, |store, _| { - let summary = self.messages.summary(); - if summary.count == 0 { - store.set_acknowledged_message_id(self.channel_id, None); - } else if deleted_message_ix == summary.count - && let ChannelMessageId::Saved(id) = summary.max_id - { - store.set_acknowledged_message_id(self.channel_id, Some(id)); - } - }); - - cx.emit(ChannelChatEvent::MessagesUpdated { - old_range: deleted_message_ix..deleted_message_ix + 1, - new_count: 0, - }); - } - } - - fn message_update( - &mut self, - id: ChannelMessageId, - body: String, - mentions: Vec<(Range, u64)>, - edited_at: Option, - cx: &mut Context, - ) { - let mut cursor = self.messages.cursor::(&()); - let mut messages = cursor.slice(&id, Bias::Left); - let ix = messages.summary().count; - - if let Some(mut message_to_update) = cursor.item().cloned() { - message_to_update.body = body; - message_to_update.mentions = mentions; - message_to_update.edited_at = edited_at; - messages.push(message_to_update, &()); - cursor.next(); - } - - messages.append(cursor.suffix(), &()); - drop(cursor); - self.messages = messages; - - cx.emit(ChannelChatEvent::UpdateMessage { - message_ix: ix, - message_id: id, - }); - - cx.notify(); - } -} - -async fn messages_from_proto( - proto_messages: Vec, - user_store: &Entity, - cx: &mut AsyncApp, -) -> Result> { - let messages = ChannelMessage::from_proto_vec(proto_messages, user_store, cx).await?; - let mut result = SumTree::default(); - result.extend(messages, &()); - Ok(result) -} - -impl ChannelMessage { - pub async fn from_proto( - message: proto::ChannelMessage, - user_store: &Entity, - cx: &mut AsyncApp, - ) -> Result { - let sender = user_store - .update(cx, |user_store, cx| { - user_store.get_user(message.sender_id, cx) - })? - .await?; - - let edited_at = message.edited_at.and_then(|t| -> Option { - if let Ok(a) = OffsetDateTime::from_unix_timestamp(t as i64) { - return Some(a); - } - - None - }); - - Ok(ChannelMessage { - id: ChannelMessageId::Saved(message.id), - body: message.body, - mentions: message - .mentions - .into_iter() - .filter_map(|mention| { - let range = mention.range?; - Some((range.start as usize..range.end as usize, mention.user_id)) - }) - .collect(), - timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?, - sender, - nonce: message.nonce.context("nonce is required")?.into(), - reply_to_message_id: message.reply_to_message_id, - edited_at, - }) - } - - pub fn is_pending(&self) -> bool { - matches!(self.id, ChannelMessageId::Pending(_)) - } - - pub async fn from_proto_vec( - proto_messages: Vec, - user_store: &Entity, - cx: &mut AsyncApp, - ) -> Result> { - let unique_user_ids = proto_messages - .iter() - .map(|m| m.sender_id) - .collect::>() - .into_iter() - .collect(); - user_store - .update(cx, |user_store, cx| { - user_store.get_users(unique_user_ids, cx) - })? - .await?; - - let mut messages = Vec::with_capacity(proto_messages.len()); - for message in proto_messages { - messages.push(ChannelMessage::from_proto(message, user_store, cx).await?); - } - Ok(messages) - } -} - -pub fn mentions_to_proto(mentions: &[(Range, UserId)]) -> Vec { - mentions - .iter() - .map(|(range, user_id)| proto::ChatMention { - range: Some(proto::Range { - start: range.start as u64, - end: range.end as u64, - }), - user_id: *user_id, - }) - .collect() -} - -impl sum_tree::Item for ChannelMessage { - type Summary = ChannelMessageSummary; - - fn summary(&self, _cx: &()) -> Self::Summary { - ChannelMessageSummary { - max_id: self.id, - count: 1, - } - } -} - -impl Default for ChannelMessageId { - fn default() -> Self { - Self::Saved(0) - } -} - -impl sum_tree::Summary for ChannelMessageSummary { - type Context = (); - - fn zero(_cx: &Self::Context) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self, _: &()) { - self.max_id = summary.max_id; - self.count += summary.count; - } -} - -impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId { - fn zero(_cx: &()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) { - debug_assert!(summary.max_id > *self); - *self = summary.max_id; - } -} - -impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count { - fn zero(_cx: &()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) { - self.0 += summary.count; - } -} - -impl<'a> From<&'a str> for MessageParams { - fn from(value: &'a str) -> Self { - Self { - text: value.into(), - mentions: Vec::new(), - reply_to_message_id: None, - } - } -} diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index daa8a91c7c8952804c854b170d0bc2e1aa817631..e983d03e0d6758f681de9e4a3e6fd13dc7075b01 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1,6 +1,6 @@ mod channel_index; -use crate::{ChannelMessage, channel_buffer::ChannelBuffer, channel_chat::ChannelChat}; +use crate::channel_buffer::ChannelBuffer; use anyhow::{Context as _, Result, anyhow}; use channel_index::ChannelIndex; use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore}; @@ -41,7 +41,6 @@ pub struct ChannelStore { outgoing_invites: HashSet<(ChannelId, UserId)>, update_channels_tx: mpsc::UnboundedSender, opened_buffers: HashMap>, - opened_chats: HashMap>, client: Arc, did_subscribe: bool, channels_loaded: (watch::Sender, watch::Receiver), @@ -63,10 +62,8 @@ pub struct Channel { #[derive(Default, Debug)] pub struct ChannelState { - latest_chat_message: Option, latest_notes_version: NotesVersion, observed_notes_version: NotesVersion, - observed_chat_message: Option, role: Option, } @@ -196,7 +193,6 @@ impl ChannelStore { channel_participants: Default::default(), outgoing_invites: Default::default(), opened_buffers: Default::default(), - opened_chats: Default::default(), update_channels_tx, client, user_store, @@ -362,89 +358,12 @@ impl ChannelStore { ) } - pub fn fetch_channel_messages( - &self, - message_ids: Vec, - cx: &mut Context, - ) -> Task>> { - let request = if message_ids.is_empty() { - None - } else { - Some( - self.client - .request(proto::GetChannelMessagesById { message_ids }), - ) - }; - cx.spawn(async move |this, cx| { - if let Some(request) = request { - let response = request.await?; - let this = this.upgrade().context("channel store dropped")?; - let user_store = this.read_with(cx, |this, _| this.user_store.clone())?; - ChannelMessage::from_proto_vec(response.messages, &user_store, cx).await - } else { - Ok(Vec::new()) - } - }) - } - pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> bool { self.channel_states .get(&channel_id) .is_some_and(|state| state.has_channel_buffer_changed()) } - pub fn has_new_messages(&self, channel_id: ChannelId) -> bool { - self.channel_states - .get(&channel_id) - .is_some_and(|state| state.has_new_messages()) - } - - pub fn set_acknowledged_message_id(&mut self, channel_id: ChannelId, message_id: Option) { - if let Some(state) = self.channel_states.get_mut(&channel_id) { - state.latest_chat_message = message_id; - } - } - - pub fn last_acknowledge_message_id(&self, channel_id: ChannelId) -> Option { - self.channel_states.get(&channel_id).and_then(|state| { - if let Some(last_message_id) = state.latest_chat_message - && state - .last_acknowledged_message_id() - .is_some_and(|id| id < last_message_id) - { - return state.last_acknowledged_message_id(); - } - - None - }) - } - - pub fn acknowledge_message_id( - &mut self, - channel_id: ChannelId, - message_id: u64, - cx: &mut Context, - ) { - self.channel_states - .entry(channel_id) - .or_default() - .acknowledge_message_id(message_id); - cx.notify(); - } - - pub fn update_latest_message_id( - &mut self, - channel_id: ChannelId, - message_id: u64, - cx: &mut Context, - ) { - self.channel_states - .entry(channel_id) - .or_default() - .update_latest_message_id(message_id); - cx.notify(); - } - pub fn acknowledge_notes_version( &mut self, channel_id: ChannelId, @@ -473,23 +392,6 @@ impl ChannelStore { cx.notify() } - pub fn open_channel_chat( - &mut self, - channel_id: ChannelId, - cx: &mut Context, - ) -> Task>> { - let client = self.client.clone(); - let user_store = self.user_store.clone(); - let this = cx.entity(); - self.open_channel_resource( - channel_id, - "chat", - |this| &mut this.opened_chats, - async move |channel, cx| ChannelChat::new(channel, this, user_store, client, cx).await, - cx, - ) - } - /// Asynchronously open a given resource associated with a channel. /// /// Make sure that the resource is only opened once, even if this method @@ -931,13 +833,6 @@ impl ChannelStore { cx, ); } - for message_id in message.payload.observed_channel_message_id { - this.acknowledge_message_id( - ChannelId(message_id.channel_id), - message_id.message_id, - cx, - ); - } for membership in message.payload.channel_memberships { if let Some(role) = ChannelRole::from_i32(membership.role) { this.channel_states @@ -957,16 +852,6 @@ impl ChannelStore { self.outgoing_invites.clear(); self.disconnect_channel_buffers_task.take(); - for chat in self.opened_chats.values() { - if let OpenEntityHandle::Open(chat) = chat - && let Some(chat) = chat.upgrade() - { - chat.update(cx, |chat, cx| { - chat.rejoin(cx); - }); - } - } - let mut buffer_versions = Vec::new(); for buffer in self.opened_buffers.values() { if let OpenEntityHandle::Open(buffer) = buffer @@ -1094,7 +979,6 @@ impl ChannelStore { self.channel_participants.clear(); self.outgoing_invites.clear(); self.opened_buffers.clear(); - self.opened_chats.clear(); self.disconnect_channel_buffers_task = None; self.channel_states.clear(); } @@ -1131,7 +1015,6 @@ impl ChannelStore { let channels_changed = !payload.channels.is_empty() || !payload.delete_channels.is_empty() - || !payload.latest_channel_message_ids.is_empty() || !payload.latest_channel_buffer_versions.is_empty(); if channels_changed { @@ -1181,13 +1064,6 @@ impl ChannelStore { .update_latest_notes_version(latest_buffer_version.epoch, &version) } - for latest_channel_message in payload.latest_channel_message_ids { - self.channel_states - .entry(ChannelId(latest_channel_message.channel_id)) - .or_default() - .update_latest_message_id(latest_channel_message.message_id); - } - self.channels_loaded.0.try_send(true).log_err(); } @@ -1251,29 +1127,6 @@ impl ChannelState { .changed_since(&self.observed_notes_version.version)) } - fn has_new_messages(&self) -> bool { - let latest_message_id = self.latest_chat_message; - let observed_message_id = self.observed_chat_message; - - latest_message_id.is_some_and(|latest_message_id| { - latest_message_id > observed_message_id.unwrap_or_default() - }) - } - - fn last_acknowledged_message_id(&self) -> Option { - self.observed_chat_message - } - - fn acknowledge_message_id(&mut self, message_id: u64) { - let observed = self.observed_chat_message.get_or_insert(message_id); - *observed = (*observed).max(message_id); - } - - fn update_latest_message_id(&mut self, message_id: u64) { - self.latest_chat_message = - Some(message_id.max(self.latest_chat_message.unwrap_or_default())); - } - fn acknowledge_notes_version(&mut self, epoch: u64, version: &clock::Global) { if self.observed_notes_version.epoch == epoch { self.observed_notes_version.version.join(version); diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 2a914330847053bc044da07e11642906b65a3159..fbdfe9f8b59f2b5e47720bb497c56b47c8abb77e 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -1,9 +1,7 @@ -use crate::channel_chat::ChannelChatEvent; - use super::*; -use client::{Client, UserStore, test::FakeServer}; +use client::{Client, UserStore}; use clock::FakeSystemClock; -use gpui::{App, AppContext as _, Entity, SemanticVersion, TestAppContext}; +use gpui::{App, AppContext as _, Entity, SemanticVersion}; use http_client::FakeHttpClient; use rpc::proto::{self}; use settings::SettingsStore; @@ -235,201 +233,6 @@ fn test_dangling_channel_paths(cx: &mut App) { assert_channels(&channel_store, &[(0, "a".to_string())], cx); } -#[gpui::test] -async fn test_channel_messages(cx: &mut TestAppContext) { - let user_id = 5; - let channel_id = 5; - let channel_store = cx.update(init_test); - let client = channel_store.read_with(cx, |s, _| s.client()); - let server = FakeServer::for_client(user_id, &client, cx).await; - - // Get the available channels. - server.send(proto::UpdateChannels { - channels: vec![proto::Channel { - id: channel_id, - name: "the-channel".to_string(), - visibility: proto::ChannelVisibility::Members as i32, - parent_path: vec![], - channel_order: 1, - }], - ..Default::default() - }); - cx.executor().run_until_parked(); - cx.update(|cx| { - assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx); - }); - - // Join a channel and populate its existing messages. - let channel = channel_store.update(cx, |store, cx| { - let channel_id = store.ordered_channels().next().unwrap().1.id; - store.open_channel_chat(channel_id, cx) - }); - let join_channel = server.receive::().await.unwrap(); - server.respond( - join_channel.receipt(), - proto::JoinChannelChatResponse { - messages: vec![ - proto::ChannelMessage { - id: 10, - body: "a".into(), - timestamp: 1000, - sender_id: 5, - mentions: vec![], - nonce: Some(1.into()), - reply_to_message_id: None, - edited_at: None, - }, - proto::ChannelMessage { - id: 11, - body: "b".into(), - timestamp: 1001, - sender_id: 6, - mentions: vec![], - nonce: Some(2.into()), - reply_to_message_id: None, - edited_at: None, - }, - ], - done: false, - }, - ); - - cx.executor().start_waiting(); - - // Client requests all users for the received messages - let mut get_users = server.receive::().await.unwrap(); - get_users.payload.user_ids.sort(); - assert_eq!(get_users.payload.user_ids, vec![6]); - server.respond( - get_users.receipt(), - proto::UsersResponse { - users: vec![proto::User { - id: 6, - github_login: "maxbrunsfeld".into(), - avatar_url: "http://avatar.com/maxbrunsfeld".into(), - name: None, - }], - }, - ); - - let channel = channel.await.unwrap(); - channel.update(cx, |channel, _| { - assert_eq!( - channel - .messages_in_range(0..2) - .map(|message| (message.sender.github_login.clone(), message.body.clone())) - .collect::>(), - &[ - ("user-5".into(), "a".into()), - ("maxbrunsfeld".into(), "b".into()) - ] - ); - }); - - // Receive a new message. - server.send(proto::ChannelMessageSent { - channel_id, - message: Some(proto::ChannelMessage { - id: 12, - body: "c".into(), - timestamp: 1002, - sender_id: 7, - mentions: vec![], - nonce: Some(3.into()), - reply_to_message_id: None, - edited_at: None, - }), - }); - - // Client requests user for message since they haven't seen them yet - let get_users = server.receive::().await.unwrap(); - assert_eq!(get_users.payload.user_ids, vec![7]); - server.respond( - get_users.receipt(), - proto::UsersResponse { - users: vec![proto::User { - id: 7, - github_login: "as-cii".into(), - avatar_url: "http://avatar.com/as-cii".into(), - name: None, - }], - }, - ); - - assert_eq!( - channel.next_event(cx).await, - ChannelChatEvent::MessagesUpdated { - old_range: 2..2, - new_count: 1, - } - ); - channel.update(cx, |channel, _| { - assert_eq!( - channel - .messages_in_range(2..3) - .map(|message| (message.sender.github_login.clone(), message.body.clone())) - .collect::>(), - &[("as-cii".into(), "c".into())] - ) - }); - - // Scroll up to view older messages. - channel.update(cx, |channel, cx| { - channel.load_more_messages(cx).unwrap().detach(); - }); - let get_messages = server.receive::().await.unwrap(); - assert_eq!(get_messages.payload.channel_id, 5); - assert_eq!(get_messages.payload.before_message_id, 10); - server.respond( - get_messages.receipt(), - proto::GetChannelMessagesResponse { - done: true, - messages: vec![ - proto::ChannelMessage { - id: 8, - body: "y".into(), - timestamp: 998, - sender_id: 5, - nonce: Some(4.into()), - mentions: vec![], - reply_to_message_id: None, - edited_at: None, - }, - proto::ChannelMessage { - id: 9, - body: "z".into(), - timestamp: 999, - sender_id: 6, - nonce: Some(5.into()), - mentions: vec![], - reply_to_message_id: None, - edited_at: None, - }, - ], - }, - ); - - assert_eq!( - channel.next_event(cx).await, - ChannelChatEvent::MessagesUpdated { - old_range: 0..0, - new_count: 2, - } - ); - channel.update(cx, |channel, _| { - assert_eq!( - channel - .messages_in_range(0..2) - .map(|message| (message.sender.github_login.clone(), message.body.clone())) - .collect::>(), - &[ - ("user-5".into(), "y".into()), - ("maxbrunsfeld".into(), "z".into()) - ] - ); - }); -} - fn init_test(cx: &mut App) -> Entity { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index f39da309dde4d4f9b2bebe4d117869f78225112d..6ec57ce95e1863d973624f57947b28fffec042b1 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -26,7 +26,6 @@ use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use std::ops::RangeInclusive; use std::{ - fmt::Write as _, future::Future, marker::PhantomData, ops::{Deref, DerefMut}, @@ -486,9 +485,7 @@ pub struct ChannelsForUser { pub invited_channels: Vec, pub observed_buffer_versions: Vec, - pub observed_channel_messages: Vec, pub latest_buffer_versions: Vec, - pub latest_channel_messages: Vec, } #[derive(Debug)] diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 95e45dc00451dae27a98fd68c492f1047dea9804..7b457a5da438e0a9ab7c6cd79368b2845e962318 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -7,7 +7,6 @@ pub mod contacts; pub mod contributors; pub mod embeddings; pub mod extensions; -pub mod messages; pub mod notifications; pub mod projects; pub mod rooms; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 5e296e0a3b8e3cb16bd0a1820688d808e10a8193..4bb82865e73968e2861777d5cd0f700675366e81 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -618,25 +618,17 @@ impl Database { } drop(rows); - let latest_channel_messages = self.latest_channel_messages(&channel_ids, tx).await?; - let observed_buffer_versions = self .observed_channel_buffer_changes(&channel_ids_by_buffer_id, user_id, tx) .await?; - let observed_channel_messages = self - .observed_channel_messages(&channel_ids, user_id, tx) - .await?; - Ok(ChannelsForUser { channel_memberships, channels, invited_channels, channel_participants, latest_buffer_versions, - latest_channel_messages, observed_buffer_versions, - observed_channel_messages, }) } diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs deleted file mode 100644 index 38e100053c0e88311aacd69a14fd8cb98e43ee28..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/queries/messages.rs +++ /dev/null @@ -1,725 +0,0 @@ -use super::*; -use anyhow::Context as _; -use rpc::Notification; -use sea_orm::{SelectColumns, TryInsertResult}; -use time::OffsetDateTime; -use util::ResultExt; - -impl Database { - /// Inserts a record representing a user joining the chat for a given channel. - pub async fn join_channel_chat( - &self, - channel_id: ChannelId, - connection_id: ConnectionId, - user_id: UserId, - ) -> Result<()> { - self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &tx).await?; - self.check_user_is_channel_participant(&channel, user_id, &tx) - .await?; - channel_chat_participant::ActiveModel { - id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel_id), - user_id: ActiveValue::Set(user_id), - connection_id: ActiveValue::Set(connection_id.id as i32), - connection_server_id: ActiveValue::Set(ServerId(connection_id.owner_id as i32)), - } - .insert(&*tx) - .await?; - Ok(()) - }) - .await - } - - /// Removes `channel_chat_participant` records associated with the given connection ID. - pub async fn channel_chat_connection_lost( - &self, - connection_id: ConnectionId, - tx: &DatabaseTransaction, - ) -> Result<()> { - channel_chat_participant::Entity::delete_many() - .filter( - Condition::all() - .add( - channel_chat_participant::Column::ConnectionServerId - .eq(connection_id.owner_id), - ) - .add(channel_chat_participant::Column::ConnectionId.eq(connection_id.id)), - ) - .exec(tx) - .await?; - Ok(()) - } - - /// Removes `channel_chat_participant` records associated with the given user ID so they - /// will no longer get chat notifications. - pub async fn leave_channel_chat( - &self, - channel_id: ChannelId, - connection_id: ConnectionId, - _user_id: UserId, - ) -> Result<()> { - self.transaction(|tx| async move { - channel_chat_participant::Entity::delete_many() - .filter( - Condition::all() - .add( - channel_chat_participant::Column::ConnectionServerId - .eq(connection_id.owner_id), - ) - .add(channel_chat_participant::Column::ConnectionId.eq(connection_id.id)) - .add(channel_chat_participant::Column::ChannelId.eq(channel_id)), - ) - .exec(&*tx) - .await?; - - Ok(()) - }) - .await - } - - /// Retrieves the messages in the specified channel. - /// - /// Use `before_message_id` to paginate through the channel's messages. - pub async fn get_channel_messages( - &self, - channel_id: ChannelId, - user_id: UserId, - count: usize, - before_message_id: Option, - ) -> Result> { - self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &tx).await?; - self.check_user_is_channel_participant(&channel, user_id, &tx) - .await?; - - let mut condition = - Condition::all().add(channel_message::Column::ChannelId.eq(channel_id)); - - if let Some(before_message_id) = before_message_id { - condition = condition.add(channel_message::Column::Id.lt(before_message_id)); - } - - let rows = channel_message::Entity::find() - .filter(condition) - .order_by_desc(channel_message::Column::Id) - .limit(count as u64) - .all(&*tx) - .await?; - - self.load_channel_messages(rows, &tx).await - }) - .await - } - - /// Returns the channel messages with the given IDs. - pub async fn get_channel_messages_by_id( - &self, - user_id: UserId, - message_ids: &[MessageId], - ) -> Result> { - self.transaction(|tx| async move { - let rows = channel_message::Entity::find() - .filter(channel_message::Column::Id.is_in(message_ids.iter().copied())) - .order_by_desc(channel_message::Column::Id) - .all(&*tx) - .await?; - - let mut channels = HashMap::::default(); - for row in &rows { - channels.insert( - row.channel_id, - self.get_channel_internal(row.channel_id, &tx).await?, - ); - } - - for (_, channel) in channels { - self.check_user_is_channel_participant(&channel, user_id, &tx) - .await?; - } - - let messages = self.load_channel_messages(rows, &tx).await?; - Ok(messages) - }) - .await - } - - async fn load_channel_messages( - &self, - rows: Vec, - tx: &DatabaseTransaction, - ) -> Result> { - let mut messages = rows - .into_iter() - .map(|row| { - let nonce = row.nonce.as_u64_pair(); - proto::ChannelMessage { - id: row.id.to_proto(), - sender_id: row.sender_id.to_proto(), - body: row.body, - timestamp: row.sent_at.assume_utc().unix_timestamp() as u64, - mentions: vec![], - nonce: Some(proto::Nonce { - upper_half: nonce.0, - lower_half: nonce.1, - }), - reply_to_message_id: row.reply_to_message_id.map(|id| id.to_proto()), - edited_at: row - .edited_at - .map(|t| t.assume_utc().unix_timestamp() as u64), - } - }) - .collect::>(); - messages.reverse(); - - let mut mentions = channel_message_mention::Entity::find() - .filter(channel_message_mention::Column::MessageId.is_in(messages.iter().map(|m| m.id))) - .order_by_asc(channel_message_mention::Column::MessageId) - .order_by_asc(channel_message_mention::Column::StartOffset) - .stream(tx) - .await?; - - let mut message_ix = 0; - while let Some(mention) = mentions.next().await { - let mention = mention?; - let message_id = mention.message_id.to_proto(); - while let Some(message) = messages.get_mut(message_ix) { - if message.id < message_id { - message_ix += 1; - } else { - if message.id == message_id { - message.mentions.push(proto::ChatMention { - range: Some(proto::Range { - start: mention.start_offset as u64, - end: mention.end_offset as u64, - }), - user_id: mention.user_id.to_proto(), - }); - } - break; - } - } - } - - Ok(messages) - } - - fn format_mentions_to_entities( - &self, - message_id: MessageId, - body: &str, - mentions: &[proto::ChatMention], - ) -> Result> { - Ok(mentions - .iter() - .filter_map(|mention| { - let range = mention.range.as_ref()?; - if !body.is_char_boundary(range.start as usize) - || !body.is_char_boundary(range.end as usize) - { - return None; - } - Some(channel_message_mention::ActiveModel { - message_id: ActiveValue::Set(message_id), - start_offset: ActiveValue::Set(range.start as i32), - end_offset: ActiveValue::Set(range.end as i32), - user_id: ActiveValue::Set(UserId::from_proto(mention.user_id)), - }) - }) - .collect::>()) - } - - /// Creates a new channel message. - pub async fn create_channel_message( - &self, - channel_id: ChannelId, - user_id: UserId, - body: &str, - mentions: &[proto::ChatMention], - timestamp: OffsetDateTime, - nonce: u128, - reply_to_message_id: Option, - ) -> Result { - self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &tx).await?; - self.check_user_is_channel_participant(&channel, user_id, &tx) - .await?; - - let mut rows = channel_chat_participant::Entity::find() - .filter(channel_chat_participant::Column::ChannelId.eq(channel_id)) - .stream(&*tx) - .await?; - - let mut is_participant = false; - let mut participant_connection_ids = HashSet::default(); - let mut participant_user_ids = Vec::new(); - while let Some(row) = rows.next().await { - let row = row?; - if row.user_id == user_id { - is_participant = true; - } - participant_user_ids.push(row.user_id); - participant_connection_ids.insert(row.connection()); - } - drop(rows); - - if !is_participant { - Err(anyhow!("not a chat participant"))?; - } - - let timestamp = timestamp.to_offset(time::UtcOffset::UTC); - let timestamp = time::PrimitiveDateTime::new(timestamp.date(), timestamp.time()); - - let result = channel_message::Entity::insert(channel_message::ActiveModel { - channel_id: ActiveValue::Set(channel_id), - sender_id: ActiveValue::Set(user_id), - body: ActiveValue::Set(body.to_string()), - sent_at: ActiveValue::Set(timestamp), - nonce: ActiveValue::Set(Uuid::from_u128(nonce)), - id: ActiveValue::NotSet, - reply_to_message_id: ActiveValue::Set(reply_to_message_id), - edited_at: ActiveValue::NotSet, - }) - .on_conflict( - OnConflict::columns([ - channel_message::Column::SenderId, - channel_message::Column::Nonce, - ]) - .do_nothing() - .to_owned(), - ) - .do_nothing() - .exec(&*tx) - .await?; - - let message_id; - let mut notifications = Vec::new(); - match result { - TryInsertResult::Inserted(result) => { - message_id = result.last_insert_id; - let mentioned_user_ids = - mentions.iter().map(|m| m.user_id).collect::>(); - - let mentions = self.format_mentions_to_entities(message_id, body, mentions)?; - if !mentions.is_empty() { - channel_message_mention::Entity::insert_many(mentions) - .exec(&*tx) - .await?; - } - - for mentioned_user in mentioned_user_ids { - notifications.extend( - self.create_notification( - UserId::from_proto(mentioned_user), - rpc::Notification::ChannelMessageMention { - message_id: message_id.to_proto(), - sender_id: user_id.to_proto(), - channel_id: channel_id.to_proto(), - }, - false, - &tx, - ) - .await?, - ); - } - - self.observe_channel_message_internal(channel_id, user_id, message_id, &tx) - .await?; - } - _ => { - message_id = channel_message::Entity::find() - .filter(channel_message::Column::Nonce.eq(Uuid::from_u128(nonce))) - .one(&*tx) - .await? - .context("failed to insert message")? - .id; - } - } - - Ok(CreatedChannelMessage { - message_id, - participant_connection_ids, - notifications, - }) - }) - .await - } - - pub async fn observe_channel_message( - &self, - channel_id: ChannelId, - user_id: UserId, - message_id: MessageId, - ) -> Result { - self.transaction(|tx| async move { - self.observe_channel_message_internal(channel_id, user_id, message_id, &tx) - .await?; - let mut batch = NotificationBatch::default(); - batch.extend( - self.mark_notification_as_read( - user_id, - &Notification::ChannelMessageMention { - message_id: message_id.to_proto(), - sender_id: Default::default(), - channel_id: Default::default(), - }, - &tx, - ) - .await?, - ); - Ok(batch) - }) - .await - } - - async fn observe_channel_message_internal( - &self, - channel_id: ChannelId, - user_id: UserId, - message_id: MessageId, - tx: &DatabaseTransaction, - ) -> Result<()> { - observed_channel_messages::Entity::insert(observed_channel_messages::ActiveModel { - user_id: ActiveValue::Set(user_id), - channel_id: ActiveValue::Set(channel_id), - channel_message_id: ActiveValue::Set(message_id), - }) - .on_conflict( - OnConflict::columns([ - observed_channel_messages::Column::ChannelId, - observed_channel_messages::Column::UserId, - ]) - .update_column(observed_channel_messages::Column::ChannelMessageId) - .action_cond_where(observed_channel_messages::Column::ChannelMessageId.lt(message_id)) - .to_owned(), - ) - // TODO: Try to upgrade SeaORM so we don't have to do this hack around their bug - .exec_without_returning(tx) - .await?; - Ok(()) - } - - pub async fn observed_channel_messages( - &self, - channel_ids: &[ChannelId], - user_id: UserId, - tx: &DatabaseTransaction, - ) -> Result> { - let rows = observed_channel_messages::Entity::find() - .filter(observed_channel_messages::Column::UserId.eq(user_id)) - .filter( - observed_channel_messages::Column::ChannelId - .is_in(channel_ids.iter().map(|id| id.0)), - ) - .all(tx) - .await?; - - Ok(rows - .into_iter() - .map(|message| proto::ChannelMessageId { - channel_id: message.channel_id.to_proto(), - message_id: message.channel_message_id.to_proto(), - }) - .collect()) - } - - pub async fn latest_channel_messages( - &self, - channel_ids: &[ChannelId], - tx: &DatabaseTransaction, - ) -> Result> { - let mut values = String::new(); - for id in channel_ids { - if !values.is_empty() { - values.push_str(", "); - } - write!(&mut values, "({})", id).unwrap(); - } - - if values.is_empty() { - return Ok(Vec::default()); - } - - let sql = format!( - r#" - SELECT - * - FROM ( - SELECT - *, - row_number() OVER ( - PARTITION BY channel_id - ORDER BY id DESC - ) as row_number - FROM channel_messages - WHERE - channel_id in ({values}) - ) AS messages - WHERE - row_number = 1 - "#, - ); - - let stmt = Statement::from_string(self.pool.get_database_backend(), sql); - let mut last_messages = channel_message::Model::find_by_statement(stmt) - .stream(tx) - .await?; - - let mut results = Vec::new(); - while let Some(result) = last_messages.next().await { - let message = result?; - results.push(proto::ChannelMessageId { - channel_id: message.channel_id.to_proto(), - message_id: message.id.to_proto(), - }); - } - - Ok(results) - } - - fn get_notification_kind_id_by_name(&self, notification_kind: &str) -> Option { - self.notification_kinds_by_id - .iter() - .find(|(_, kind)| **kind == notification_kind) - .map(|kind| kind.0.0) - } - - /// Removes the channel message with the given ID. - pub async fn remove_channel_message( - &self, - channel_id: ChannelId, - message_id: MessageId, - user_id: UserId, - ) -> Result<(Vec, Vec)> { - self.transaction(|tx| async move { - let mut rows = channel_chat_participant::Entity::find() - .filter(channel_chat_participant::Column::ChannelId.eq(channel_id)) - .stream(&*tx) - .await?; - - let mut is_participant = false; - let mut participant_connection_ids = Vec::new(); - while let Some(row) = rows.next().await { - let row = row?; - if row.user_id == user_id { - is_participant = true; - } - participant_connection_ids.push(row.connection()); - } - drop(rows); - - if !is_participant { - Err(anyhow!("not a chat participant"))?; - } - - let result = channel_message::Entity::delete_by_id(message_id) - .filter(channel_message::Column::SenderId.eq(user_id)) - .exec(&*tx) - .await?; - - if result.rows_affected == 0 { - let channel = self.get_channel_internal(channel_id, &tx).await?; - if self - .check_user_is_channel_admin(&channel, user_id, &tx) - .await - .is_ok() - { - let result = channel_message::Entity::delete_by_id(message_id) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("no such message"))?; - } - } else { - Err(anyhow!("operation could not be completed"))?; - } - } - - let notification_kind_id = - self.get_notification_kind_id_by_name("ChannelMessageMention"); - - let existing_notifications = notification::Entity::find() - .filter(notification::Column::EntityId.eq(message_id)) - .filter(notification::Column::Kind.eq(notification_kind_id)) - .select_column(notification::Column::Id) - .all(&*tx) - .await?; - - let existing_notification_ids = existing_notifications - .into_iter() - .map(|notification| notification.id) - .collect(); - - // remove all the mention notifications for this message - notification::Entity::delete_many() - .filter(notification::Column::EntityId.eq(message_id)) - .filter(notification::Column::Kind.eq(notification_kind_id)) - .exec(&*tx) - .await?; - - Ok((participant_connection_ids, existing_notification_ids)) - }) - .await - } - - /// Updates the channel message with the given ID, body and timestamp(edited_at). - pub async fn update_channel_message( - &self, - channel_id: ChannelId, - message_id: MessageId, - user_id: UserId, - body: &str, - mentions: &[proto::ChatMention], - edited_at: OffsetDateTime, - ) -> Result { - self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &tx).await?; - self.check_user_is_channel_participant(&channel, user_id, &tx) - .await?; - - let mut rows = channel_chat_participant::Entity::find() - .filter(channel_chat_participant::Column::ChannelId.eq(channel_id)) - .stream(&*tx) - .await?; - - let mut is_participant = false; - let mut participant_connection_ids = Vec::new(); - let mut participant_user_ids = Vec::new(); - while let Some(row) = rows.next().await { - let row = row?; - if row.user_id == user_id { - is_participant = true; - } - participant_user_ids.push(row.user_id); - participant_connection_ids.push(row.connection()); - } - drop(rows); - - if !is_participant { - Err(anyhow!("not a chat participant"))?; - } - - let channel_message = channel_message::Entity::find_by_id(message_id) - .filter(channel_message::Column::SenderId.eq(user_id)) - .one(&*tx) - .await?; - - let Some(channel_message) = channel_message else { - Err(anyhow!("Channel message not found"))? - }; - - let edited_at = edited_at.to_offset(time::UtcOffset::UTC); - let edited_at = time::PrimitiveDateTime::new(edited_at.date(), edited_at.time()); - - let updated_message = channel_message::ActiveModel { - body: ActiveValue::Set(body.to_string()), - edited_at: ActiveValue::Set(Some(edited_at)), - reply_to_message_id: ActiveValue::Unchanged(channel_message.reply_to_message_id), - id: ActiveValue::Unchanged(message_id), - channel_id: ActiveValue::Unchanged(channel_id), - sender_id: ActiveValue::Unchanged(user_id), - sent_at: ActiveValue::Unchanged(channel_message.sent_at), - nonce: ActiveValue::Unchanged(channel_message.nonce), - }; - - let result = channel_message::Entity::update_many() - .set(updated_message) - .filter(channel_message::Column::Id.eq(message_id)) - .filter(channel_message::Column::SenderId.eq(user_id)) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - return Err(anyhow!( - "Attempted to edit a message (id: {message_id}) which does not exist anymore." - ))?; - } - - // we have to fetch the old mentions, - // so we don't send a notification when the message has been edited that you are mentioned in - let old_mentions = channel_message_mention::Entity::find() - .filter(channel_message_mention::Column::MessageId.eq(message_id)) - .all(&*tx) - .await?; - - // remove all existing mentions - channel_message_mention::Entity::delete_many() - .filter(channel_message_mention::Column::MessageId.eq(message_id)) - .exec(&*tx) - .await?; - - let new_mentions = self.format_mentions_to_entities(message_id, body, mentions)?; - if !new_mentions.is_empty() { - // insert new mentions - channel_message_mention::Entity::insert_many(new_mentions) - .exec(&*tx) - .await?; - } - - let mut update_mention_user_ids = HashSet::default(); - let mut new_mention_user_ids = - mentions.iter().map(|m| m.user_id).collect::>(); - // Filter out users that were mentioned before - for mention in &old_mentions { - if new_mention_user_ids.contains(&mention.user_id.to_proto()) { - update_mention_user_ids.insert(mention.user_id.to_proto()); - } - - new_mention_user_ids.remove(&mention.user_id.to_proto()); - } - - let notification_kind_id = - self.get_notification_kind_id_by_name("ChannelMessageMention"); - - let existing_notifications = notification::Entity::find() - .filter(notification::Column::EntityId.eq(message_id)) - .filter(notification::Column::Kind.eq(notification_kind_id)) - .all(&*tx) - .await?; - - // determine which notifications should be updated or deleted - let mut deleted_notification_ids = HashSet::default(); - let mut updated_mention_notifications = Vec::new(); - for notification in existing_notifications { - if update_mention_user_ids.contains(¬ification.recipient_id.to_proto()) { - if let Some(notification) = - self::notifications::model_to_proto(self, notification).log_err() - { - updated_mention_notifications.push(notification); - } - } else { - deleted_notification_ids.insert(notification.id); - } - } - - let mut notifications = Vec::new(); - for mentioned_user in new_mention_user_ids { - notifications.extend( - self.create_notification( - UserId::from_proto(mentioned_user), - rpc::Notification::ChannelMessageMention { - message_id: message_id.to_proto(), - sender_id: user_id.to_proto(), - channel_id: channel_id.to_proto(), - }, - false, - &tx, - ) - .await?, - ); - } - - Ok(UpdatedChannelMessage { - message_id, - participant_connection_ids, - notifications, - reply_to_message_id: channel_message.reply_to_message_id, - timestamp: channel_message.sent_at, - deleted_mention_notification_ids: deleted_notification_ids - .into_iter() - .collect::>(), - updated_mention_notifications, - }) - }) - .await - } -} diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 0713ac2cb2810797b319b53583bc8c0e1756fe68..b4cca2a2b15de0c10a641e847c32d2dfe300deb2 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -1193,7 +1193,6 @@ impl Database { self.transaction(|tx| async move { self.room_connection_lost(connection, &tx).await?; self.channel_buffer_connection_lost(connection, &tx).await?; - self.channel_chat_connection_lost(connection, &tx).await?; Ok(()) }) .await diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index f8560edda78217e6a5e09a5c2e66e0f436ca0477..25e03f1320a25455ede347b43477761d591fbd57 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -7,7 +7,6 @@ mod db_tests; mod embedding_tests; mod extension_tests; mod feature_flag_tests; -mod message_tests; mod user_tests; use crate::migrations::run_database_migrations; @@ -21,7 +20,7 @@ use sqlx::migrate::MigrateDatabase; use std::{ sync::{ Arc, - atomic::{AtomicI32, AtomicU32, Ordering::SeqCst}, + atomic::{AtomicI32, Ordering::SeqCst}, }, time::Duration, }; @@ -224,11 +223,3 @@ async fn new_test_user(db: &Arc, email: &str) -> UserId { .unwrap() .user_id } - -static TEST_CONNECTION_ID: AtomicU32 = AtomicU32::new(1); -fn new_test_connection(server: ServerId) -> ConnectionId { - ConnectionId { - id: TEST_CONNECTION_ID.fetch_add(1, SeqCst), - owner_id: server.0 as u32, - } -} diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 1dd16fb50a8d002d01e27cec0a959fd9ea9ecde7..705dbba5ead0170acd629149b8d77b847a5784b0 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -1,7 +1,7 @@ use crate::{ db::{ Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId, - tests::{assert_channel_tree_matches, channel_tree, new_test_connection, new_test_user}, + tests::{assert_channel_tree_matches, channel_tree, new_test_user}, }, test_both_dbs, }; @@ -949,41 +949,6 @@ async fn test_user_is_channel_participant(db: &Arc) { ) } -test_both_dbs!( - test_guest_access, - test_guest_access_postgres, - test_guest_access_sqlite -); - -async fn test_guest_access(db: &Arc) { - let server = db.create_server("test").await.unwrap(); - - let admin = new_test_user(db, "admin@example.com").await; - let guest = new_test_user(db, "guest@example.com").await; - let guest_connection = new_test_connection(server); - - let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); - db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin) - .await - .unwrap(); - - assert!( - db.join_channel_chat(zed_channel, guest_connection, guest) - .await - .is_err() - ); - - db.join_channel(zed_channel, guest, guest_connection) - .await - .unwrap(); - - assert!( - db.join_channel_chat(zed_channel, guest_connection, guest) - .await - .is_ok() - ) -} - #[track_caller] fn assert_channel_tree(actual: Vec, expected: &[(ChannelId, &[ChannelId])]) { let actual = actual diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs deleted file mode 100644 index e20473d3bdd4179309c4d392f1df93f20f1e928c..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/tests/message_tests.rs +++ /dev/null @@ -1,421 +0,0 @@ -use super::new_test_user; -use crate::{ - db::{ChannelRole, Database, MessageId}, - test_both_dbs, -}; -use channel::mentions_to_proto; -use std::sync::Arc; -use time::OffsetDateTime; - -test_both_dbs!( - test_channel_message_retrieval, - test_channel_message_retrieval_postgres, - test_channel_message_retrieval_sqlite -); - -async fn test_channel_message_retrieval(db: &Arc) { - let user = new_test_user(db, "user@example.com").await; - let channel = db.create_channel("channel", None, user).await.unwrap().0; - - let owner_id = db.create_server("test").await.unwrap().0 as u32; - db.join_channel_chat(channel.id, rpc::ConnectionId { owner_id, id: 0 }, user) - .await - .unwrap(); - - let mut all_messages = Vec::new(); - for i in 0..10 { - all_messages.push( - db.create_channel_message( - channel.id, - user, - &i.to_string(), - &[], - OffsetDateTime::now_utc(), - i, - None, - ) - .await - .unwrap() - .message_id - .to_proto(), - ); - } - - let messages = db - .get_channel_messages(channel.id, user, 3, None) - .await - .unwrap() - .into_iter() - .map(|message| message.id) - .collect::>(); - assert_eq!(messages, &all_messages[7..10]); - - let messages = db - .get_channel_messages( - channel.id, - user, - 4, - Some(MessageId::from_proto(all_messages[6])), - ) - .await - .unwrap() - .into_iter() - .map(|message| message.id) - .collect::>(); - assert_eq!(messages, &all_messages[2..6]); -} - -test_both_dbs!( - test_channel_message_nonces, - test_channel_message_nonces_postgres, - test_channel_message_nonces_sqlite -); - -async fn test_channel_message_nonces(db: &Arc) { - let user_a = new_test_user(db, "user_a@example.com").await; - let user_b = new_test_user(db, "user_b@example.com").await; - let user_c = new_test_user(db, "user_c@example.com").await; - let channel = db.create_root_channel("channel", user_a).await.unwrap(); - db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member) - .await - .unwrap(); - db.invite_channel_member(channel, user_c, user_a, ChannelRole::Member) - .await - .unwrap(); - db.respond_to_channel_invite(channel, user_b, true) - .await - .unwrap(); - db.respond_to_channel_invite(channel, user_c, true) - .await - .unwrap(); - - let owner_id = db.create_server("test").await.unwrap().0 as u32; - db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user_a) - .await - .unwrap(); - db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 1 }, user_b) - .await - .unwrap(); - - // As user A, create messages that reuse the same nonces. The requests - // succeed, but return the same ids. - let id1 = db - .create_channel_message( - channel, - user_a, - "hi @user_b", - &mentions_to_proto(&[(3..10, user_b.to_proto())]), - OffsetDateTime::now_utc(), - 100, - None, - ) - .await - .unwrap() - .message_id; - let id2 = db - .create_channel_message( - channel, - user_a, - "hello, fellow users", - &mentions_to_proto(&[]), - OffsetDateTime::now_utc(), - 200, - None, - ) - .await - .unwrap() - .message_id; - let id3 = db - .create_channel_message( - channel, - user_a, - "bye @user_c (same nonce as first message)", - &mentions_to_proto(&[(4..11, user_c.to_proto())]), - OffsetDateTime::now_utc(), - 100, - None, - ) - .await - .unwrap() - .message_id; - let id4 = db - .create_channel_message( - channel, - user_a, - "omg (same nonce as second message)", - &mentions_to_proto(&[]), - OffsetDateTime::now_utc(), - 200, - None, - ) - .await - .unwrap() - .message_id; - - // As a different user, reuse one of the same nonces. This request succeeds - // and returns a different id. - let id5 = db - .create_channel_message( - channel, - user_b, - "omg @user_a (same nonce as user_a's first message)", - &mentions_to_proto(&[(4..11, user_a.to_proto())]), - OffsetDateTime::now_utc(), - 100, - None, - ) - .await - .unwrap() - .message_id; - - assert_ne!(id1, id2); - assert_eq!(id1, id3); - assert_eq!(id2, id4); - assert_ne!(id5, id1); - - let messages = db - .get_channel_messages(channel, user_a, 5, None) - .await - .unwrap() - .into_iter() - .map(|m| (m.id, m.body, m.mentions)) - .collect::>(); - assert_eq!( - messages, - &[ - ( - id1.to_proto(), - "hi @user_b".into(), - mentions_to_proto(&[(3..10, user_b.to_proto())]), - ), - ( - id2.to_proto(), - "hello, fellow users".into(), - mentions_to_proto(&[]) - ), - ( - id5.to_proto(), - "omg @user_a (same nonce as user_a's first message)".into(), - mentions_to_proto(&[(4..11, user_a.to_proto())]), - ), - ] - ); -} - -test_both_dbs!( - test_unseen_channel_messages, - test_unseen_channel_messages_postgres, - test_unseen_channel_messages_sqlite -); - -async fn test_unseen_channel_messages(db: &Arc) { - let user = new_test_user(db, "user_a@example.com").await; - let observer = new_test_user(db, "user_b@example.com").await; - - let channel_1 = db.create_root_channel("channel", user).await.unwrap(); - let channel_2 = db.create_root_channel("channel-2", user).await.unwrap(); - - db.invite_channel_member(channel_1, observer, user, ChannelRole::Member) - .await - .unwrap(); - db.invite_channel_member(channel_2, observer, user, ChannelRole::Member) - .await - .unwrap(); - - db.respond_to_channel_invite(channel_1, observer, true) - .await - .unwrap(); - db.respond_to_channel_invite(channel_2, observer, true) - .await - .unwrap(); - - let owner_id = db.create_server("test").await.unwrap().0 as u32; - let user_connection_id = rpc::ConnectionId { owner_id, id: 0 }; - - db.join_channel_chat(channel_1, user_connection_id, user) - .await - .unwrap(); - - let _ = db - .create_channel_message( - channel_1, - user, - "1_1", - &[], - OffsetDateTime::now_utc(), - 1, - None, - ) - .await - .unwrap(); - - let _ = db - .create_channel_message( - channel_1, - user, - "1_2", - &[], - OffsetDateTime::now_utc(), - 2, - None, - ) - .await - .unwrap(); - - let third_message = db - .create_channel_message( - channel_1, - user, - "1_3", - &[], - OffsetDateTime::now_utc(), - 3, - None, - ) - .await - .unwrap() - .message_id; - - db.join_channel_chat(channel_2, user_connection_id, user) - .await - .unwrap(); - - let fourth_message = db - .create_channel_message( - channel_2, - user, - "2_1", - &[], - OffsetDateTime::now_utc(), - 4, - None, - ) - .await - .unwrap() - .message_id; - - // Check that observer has new messages - let latest_messages = db - .transaction(|tx| async move { - db.latest_channel_messages(&[channel_1, channel_2], &tx) - .await - }) - .await - .unwrap(); - - assert_eq!( - latest_messages, - [ - rpc::proto::ChannelMessageId { - channel_id: channel_1.to_proto(), - message_id: third_message.to_proto(), - }, - rpc::proto::ChannelMessageId { - channel_id: channel_2.to_proto(), - message_id: fourth_message.to_proto(), - }, - ] - ); -} - -test_both_dbs!( - test_channel_message_mentions, - test_channel_message_mentions_postgres, - test_channel_message_mentions_sqlite -); - -async fn test_channel_message_mentions(db: &Arc) { - let user_a = new_test_user(db, "user_a@example.com").await; - let user_b = new_test_user(db, "user_b@example.com").await; - let user_c = new_test_user(db, "user_c@example.com").await; - - let channel = db - .create_channel("channel", None, user_a) - .await - .unwrap() - .0 - .id; - db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member) - .await - .unwrap(); - db.respond_to_channel_invite(channel, user_b, true) - .await - .unwrap(); - - let owner_id = db.create_server("test").await.unwrap().0 as u32; - let connection_id = rpc::ConnectionId { owner_id, id: 0 }; - db.join_channel_chat(channel, connection_id, user_a) - .await - .unwrap(); - - db.create_channel_message( - channel, - user_a, - "hi @user_b and @user_c", - &mentions_to_proto(&[(3..10, user_b.to_proto()), (15..22, user_c.to_proto())]), - OffsetDateTime::now_utc(), - 1, - None, - ) - .await - .unwrap(); - db.create_channel_message( - channel, - user_a, - "bye @user_c", - &mentions_to_proto(&[(4..11, user_c.to_proto())]), - OffsetDateTime::now_utc(), - 2, - None, - ) - .await - .unwrap(); - db.create_channel_message( - channel, - user_a, - "umm", - &mentions_to_proto(&[]), - OffsetDateTime::now_utc(), - 3, - None, - ) - .await - .unwrap(); - db.create_channel_message( - channel, - user_a, - "@user_b, stop.", - &mentions_to_proto(&[(0..7, user_b.to_proto())]), - OffsetDateTime::now_utc(), - 4, - None, - ) - .await - .unwrap(); - - let messages = db - .get_channel_messages(channel, user_b, 5, None) - .await - .unwrap() - .into_iter() - .map(|m| (m.body, m.mentions)) - .collect::>(); - assert_eq!( - &messages, - &[ - ( - "hi @user_b and @user_c".into(), - mentions_to_proto(&[(3..10, user_b.to_proto()), (15..22, user_c.to_proto())]), - ), - ( - "bye @user_c".into(), - mentions_to_proto(&[(4..11, user_c.to_proto())]), - ), - ("umm".into(), mentions_to_proto(&[]),), - ( - "@user_b, stop.".into(), - mentions_to_proto(&[(0..7, user_b.to_proto())]), - ), - ] - ); -} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 9e4dfd4854b4de67de522bfbbd1160fe880a05cb..e19c59f9974f243a585b02baac8d87dc82e0d405 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -4,10 +4,9 @@ use crate::api::{CloudflareIpCountryHeader, SystemIdHeader}; use crate::{ AppState, Error, Result, auth, db::{ - self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser, - CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId, - NotificationId, ProjectId, RejoinedProject, RemoveChannelMemberResult, - RespondToChannelInvite, RoomId, ServerId, UpdatedChannelMessage, User, UserId, + self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser, Database, + InviteMemberResult, MembershipUpdated, NotificationId, ProjectId, RejoinedProject, + RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, User, UserId, }, executor::Executor, }; @@ -66,7 +65,6 @@ use std::{ }, time::{Duration, Instant}, }; -use time::OffsetDateTime; use tokio::sync::{Semaphore, watch}; use tower::ServiceBuilder; use tracing::{ @@ -80,8 +78,6 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); // kubernetes gives terminated pods 10s to shutdown gracefully. After they're gone, we can clean up old resources. pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(15); -const MESSAGE_COUNT_PER_PAGE: usize = 100; -const MAX_MESSAGE_LEN: usize = 1024; const NOTIFICATION_COUNT_PER_PAGE: usize = 50; const MAX_CONCURRENT_CONNECTIONS: usize = 512; @@ -3597,235 +3593,36 @@ fn send_notifications( /// Send a message to the channel async fn send_channel_message( - request: proto::SendChannelMessage, - response: Response, - session: MessageContext, + _request: proto::SendChannelMessage, + _response: Response, + _session: MessageContext, ) -> Result<()> { - // Validate the message body. - let body = request.body.trim().to_string(); - if body.len() > MAX_MESSAGE_LEN { - return Err(anyhow!("message is too long"))?; - } - if body.is_empty() { - return Err(anyhow!("message can't be blank"))?; - } - - // TODO: adjust mentions if body is trimmed - - let timestamp = OffsetDateTime::now_utc(); - let nonce = request.nonce.context("nonce can't be blank")?; - - let channel_id = ChannelId::from_proto(request.channel_id); - let CreatedChannelMessage { - message_id, - participant_connection_ids, - notifications, - } = session - .db() - .await - .create_channel_message( - channel_id, - session.user_id(), - &body, - &request.mentions, - timestamp, - nonce.clone().into(), - request.reply_to_message_id.map(MessageId::from_proto), - ) - .await?; - - let message = proto::ChannelMessage { - sender_id: session.user_id().to_proto(), - id: message_id.to_proto(), - body, - mentions: request.mentions, - timestamp: timestamp.unix_timestamp() as u64, - nonce: Some(nonce), - reply_to_message_id: request.reply_to_message_id, - edited_at: None, - }; - broadcast( - Some(session.connection_id), - participant_connection_ids.clone(), - |connection| { - session.peer.send( - connection, - proto::ChannelMessageSent { - channel_id: channel_id.to_proto(), - message: Some(message.clone()), - }, - ) - }, - ); - response.send(proto::SendChannelMessageResponse { - message: Some(message), - })?; - - let pool = &*session.connection_pool().await; - let non_participants = - pool.channel_connection_ids(channel_id) - .filter_map(|(connection_id, _)| { - if participant_connection_ids.contains(&connection_id) { - None - } else { - Some(connection_id) - } - }); - broadcast(None, non_participants, |peer_id| { - session.peer.send( - peer_id, - proto::UpdateChannels { - latest_channel_message_ids: vec![proto::ChannelMessageId { - channel_id: channel_id.to_proto(), - message_id: message_id.to_proto(), - }], - ..Default::default() - }, - ) - }); - send_notifications(pool, &session.peer, notifications); - - Ok(()) + Err(anyhow!("chat has been removed in the latest version of Zed").into()) } /// Delete a channel message async fn remove_channel_message( - request: proto::RemoveChannelMessage, - response: Response, - session: MessageContext, + _request: proto::RemoveChannelMessage, + _response: Response, + _session: MessageContext, ) -> Result<()> { - let channel_id = ChannelId::from_proto(request.channel_id); - let message_id = MessageId::from_proto(request.message_id); - let (connection_ids, existing_notification_ids) = session - .db() - .await - .remove_channel_message(channel_id, message_id, session.user_id()) - .await?; - - broadcast( - Some(session.connection_id), - connection_ids, - move |connection| { - session.peer.send(connection, request.clone())?; - - for notification_id in &existing_notification_ids { - session.peer.send( - connection, - proto::DeleteNotification { - notification_id: (*notification_id).to_proto(), - }, - )?; - } - - Ok(()) - }, - ); - response.send(proto::Ack {})?; - Ok(()) + Err(anyhow!("chat has been removed in the latest version of Zed").into()) } async fn update_channel_message( - request: proto::UpdateChannelMessage, - response: Response, - session: MessageContext, + _request: proto::UpdateChannelMessage, + _response: Response, + _session: MessageContext, ) -> Result<()> { - let channel_id = ChannelId::from_proto(request.channel_id); - let message_id = MessageId::from_proto(request.message_id); - let updated_at = OffsetDateTime::now_utc(); - let UpdatedChannelMessage { - message_id, - participant_connection_ids, - notifications, - reply_to_message_id, - timestamp, - deleted_mention_notification_ids, - updated_mention_notifications, - } = session - .db() - .await - .update_channel_message( - channel_id, - message_id, - session.user_id(), - request.body.as_str(), - &request.mentions, - updated_at, - ) - .await?; - - let nonce = request.nonce.clone().context("nonce can't be blank")?; - - let message = proto::ChannelMessage { - sender_id: session.user_id().to_proto(), - id: message_id.to_proto(), - body: request.body.clone(), - mentions: request.mentions.clone(), - timestamp: timestamp.assume_utc().unix_timestamp() as u64, - nonce: Some(nonce), - reply_to_message_id: reply_to_message_id.map(|id| id.to_proto()), - edited_at: Some(updated_at.unix_timestamp() as u64), - }; - - response.send(proto::Ack {})?; - - let pool = &*session.connection_pool().await; - broadcast( - Some(session.connection_id), - participant_connection_ids, - |connection| { - session.peer.send( - connection, - proto::ChannelMessageUpdate { - channel_id: channel_id.to_proto(), - message: Some(message.clone()), - }, - )?; - - for notification_id in &deleted_mention_notification_ids { - session.peer.send( - connection, - proto::DeleteNotification { - notification_id: (*notification_id).to_proto(), - }, - )?; - } - - for notification in &updated_mention_notifications { - session.peer.send( - connection, - proto::UpdateNotification { - notification: Some(notification.clone()), - }, - )?; - } - - Ok(()) - }, - ); - - send_notifications(pool, &session.peer, notifications); - - Ok(()) + Err(anyhow!("chat has been removed in the latest version of Zed").into()) } /// Mark a channel message as read async fn acknowledge_channel_message( - request: proto::AckChannelMessage, - session: MessageContext, + _request: proto::AckChannelMessage, + _session: MessageContext, ) -> Result<()> { - let channel_id = ChannelId::from_proto(request.channel_id); - let message_id = MessageId::from_proto(request.message_id); - let notifications = session - .db() - .await - .observe_channel_message(channel_id, session.user_id(), message_id) - .await?; - send_notifications( - &*session.connection_pool().await, - &session.peer, - notifications, - ); - Ok(()) + Err(anyhow!("chat has been removed in the latest version of Zed").into()) } /// Mark a buffer version as synced @@ -3878,84 +3675,37 @@ async fn get_supermaven_api_key( /// Start receiving chat updates for a channel async fn join_channel_chat( - request: proto::JoinChannelChat, - response: Response, - session: MessageContext, + _request: proto::JoinChannelChat, + _response: Response, + _session: MessageContext, ) -> Result<()> { - let channel_id = ChannelId::from_proto(request.channel_id); - - let db = session.db().await; - db.join_channel_chat(channel_id, session.connection_id, session.user_id()) - .await?; - let messages = db - .get_channel_messages(channel_id, session.user_id(), MESSAGE_COUNT_PER_PAGE, None) - .await?; - response.send(proto::JoinChannelChatResponse { - done: messages.len() < MESSAGE_COUNT_PER_PAGE, - messages, - })?; - Ok(()) + Err(anyhow!("chat has been removed in the latest version of Zed").into()) } /// Stop receiving chat updates for a channel async fn leave_channel_chat( - request: proto::LeaveChannelChat, - session: MessageContext, + _request: proto::LeaveChannelChat, + _session: MessageContext, ) -> Result<()> { - let channel_id = ChannelId::from_proto(request.channel_id); - session - .db() - .await - .leave_channel_chat(channel_id, session.connection_id, session.user_id()) - .await?; - Ok(()) + Err(anyhow!("chat has been removed in the latest version of Zed").into()) } /// Retrieve the chat history for a channel async fn get_channel_messages( - request: proto::GetChannelMessages, - response: Response, - session: MessageContext, + _request: proto::GetChannelMessages, + _response: Response, + _session: MessageContext, ) -> Result<()> { - let channel_id = ChannelId::from_proto(request.channel_id); - let messages = session - .db() - .await - .get_channel_messages( - channel_id, - session.user_id(), - MESSAGE_COUNT_PER_PAGE, - Some(MessageId::from_proto(request.before_message_id)), - ) - .await?; - response.send(proto::GetChannelMessagesResponse { - done: messages.len() < MESSAGE_COUNT_PER_PAGE, - messages, - })?; - Ok(()) + Err(anyhow!("chat has been removed in the latest version of Zed").into()) } /// Retrieve specific chat messages async fn get_channel_messages_by_id( - request: proto::GetChannelMessagesById, - response: Response, - session: MessageContext, + _request: proto::GetChannelMessagesById, + _response: Response, + _session: MessageContext, ) -> Result<()> { - let message_ids = request - .message_ids - .iter() - .map(|id| MessageId::from_proto(*id)) - .collect::>(); - let messages = session - .db() - .await - .get_channel_messages_by_id(session.user_id(), &message_ids) - .await?; - response.send(proto::GetChannelMessagesResponse { - done: messages.len() < MESSAGE_COUNT_PER_PAGE, - messages, - })?; - Ok(()) + Err(anyhow!("chat has been removed in the latest version of Zed").into()) } /// Retrieve the current users notifications @@ -4095,7 +3845,6 @@ fn build_update_user_channels(channels: &ChannelsForUser) -> proto::UpdateUserCh }) .collect(), observed_channel_buffer_version: channels.observed_buffer_versions.clone(), - observed_channel_message_id: channels.observed_channel_messages.clone(), } } @@ -4107,7 +3856,6 @@ fn build_channels_update(channels: ChannelsForUser) -> proto::UpdateChannels { } update.latest_channel_buffer_versions = channels.latest_buffer_versions; - update.latest_channel_message_ids = channels.latest_channel_messages; for (channel_id, participants) in channels.channel_participants { update diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index ddf245b06f322b5c62ba83d56f05fbca65e2ba9d..7d07360b8042ed54a9f19a82a2876e448e8a14a4 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -6,7 +6,6 @@ use gpui::{Entity, TestAppContext}; mod channel_buffer_tests; mod channel_guest_tests; -mod channel_message_tests; mod channel_tests; mod editor_tests; mod following_tests; diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs deleted file mode 100644 index dbc5cd86c2582719bbb0782e1b3630f08e4cacaf..0000000000000000000000000000000000000000 --- a/crates/collab/src/tests/channel_message_tests.rs +++ /dev/null @@ -1,725 +0,0 @@ -use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; -use channel::{ChannelChat, ChannelMessageId, MessageParams}; -use collab_ui::chat_panel::ChatPanel; -use gpui::{BackgroundExecutor, Entity, TestAppContext}; -use rpc::Notification; -use workspace::dock::Panel; - -#[gpui::test] -async fn test_basic_channel_messages( - executor: BackgroundExecutor, - mut cx_a: &mut TestAppContext, - mut cx_b: &mut TestAppContext, - mut cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; - - let channel_id = server - .make_channel( - "the-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b), (&client_c, cx_c)], - ) - .await; - - let channel_chat_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - let channel_chat_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - - let message_id = channel_chat_a - .update(cx_a, |c, cx| { - c.send_message( - MessageParams { - text: "hi @user_c!".into(), - mentions: vec![(3..10, client_c.id())], - reply_to_message_id: None, - }, - cx, - ) - .unwrap() - }) - .await - .unwrap(); - channel_chat_a - .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap()) - .await - .unwrap(); - - executor.run_until_parked(); - channel_chat_b - .update(cx_b, |c, cx| c.send_message("three".into(), cx).unwrap()) - .await - .unwrap(); - - executor.run_until_parked(); - - let channel_chat_c = client_c - .channel_store() - .update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - - for (chat, cx) in [ - (&channel_chat_a, &mut cx_a), - (&channel_chat_b, &mut cx_b), - (&channel_chat_c, &mut cx_c), - ] { - chat.update(*cx, |c, _| { - assert_eq!( - c.messages() - .iter() - .map(|m| (m.body.as_str(), m.mentions.as_slice())) - .collect::>(), - vec![ - ("hi @user_c!", [(3..10, client_c.id())].as_slice()), - ("two", &[]), - ("three", &[]) - ], - "results for user {}", - c.client().id(), - ); - }); - } - - client_c.notification_store().update(cx_c, |store, _| { - assert_eq!(store.notification_count(), 2); - assert_eq!(store.unread_notification_count(), 1); - assert_eq!( - store.notification_at(0).unwrap().notification, - Notification::ChannelMessageMention { - message_id, - sender_id: client_a.id(), - channel_id: channel_id.0, - } - ); - assert_eq!( - store.notification_at(1).unwrap().notification, - Notification::ChannelInvitation { - channel_id: channel_id.0, - channel_name: "the-channel".to_string(), - inviter_id: client_a.id() - } - ); - }); -} - -#[gpui::test] -async fn test_rejoin_channel_chat( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let channel_id = server - .make_channel( - "the-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b)], - ) - .await; - - let channel_chat_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - let channel_chat_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - - channel_chat_a - .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) - .await - .unwrap(); - channel_chat_b - .update(cx_b, |c, cx| c.send_message("two".into(), cx).unwrap()) - .await - .unwrap(); - - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - - // While client A is disconnected, clients A and B both send new messages. - channel_chat_a - .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap()) - .await - .unwrap_err(); - channel_chat_a - .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap()) - .await - .unwrap_err(); - channel_chat_b - .update(cx_b, |c, cx| c.send_message("five".into(), cx).unwrap()) - .await - .unwrap(); - channel_chat_b - .update(cx_b, |c, cx| c.send_message("six".into(), cx).unwrap()) - .await - .unwrap(); - - // Client A reconnects. - server.allow_connections(); - executor.advance_clock(RECONNECT_TIMEOUT); - - // Client A fetches the messages that were sent while they were disconnected - // and resends their own messages which failed to send. - let expected_messages = &["one", "two", "five", "six", "three", "four"]; - assert_messages(&channel_chat_a, expected_messages, cx_a); - assert_messages(&channel_chat_b, expected_messages, cx_b); -} - -#[gpui::test] -async fn test_remove_channel_message( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; - - let channel_id = server - .make_channel( - "the-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b), (&client_c, cx_c)], - ) - .await; - - let channel_chat_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - let channel_chat_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - - // Client A sends some messages. - channel_chat_a - .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) - .await - .unwrap(); - let msg_id_2 = channel_chat_a - .update(cx_a, |c, cx| { - c.send_message( - MessageParams { - text: "two @user_b".to_string(), - mentions: vec![(4..12, client_b.id())], - reply_to_message_id: None, - }, - cx, - ) - .unwrap() - }) - .await - .unwrap(); - channel_chat_a - .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap()) - .await - .unwrap(); - - // Clients A and B see all of the messages. - executor.run_until_parked(); - let expected_messages = &["one", "two @user_b", "three"]; - assert_messages(&channel_chat_a, expected_messages, cx_a); - assert_messages(&channel_chat_b, expected_messages, cx_b); - - // Ensure that client B received a notification for the mention. - client_b.notification_store().read_with(cx_b, |store, _| { - assert_eq!(store.notification_count(), 2); - let entry = store.notification_at(0).unwrap(); - assert_eq!( - entry.notification, - Notification::ChannelMessageMention { - message_id: msg_id_2, - sender_id: client_a.id(), - channel_id: channel_id.0, - } - ); - }); - - // Client A deletes one of their messages. - channel_chat_a - .update(cx_a, |c, cx| { - let ChannelMessageId::Saved(id) = c.message(1).id else { - panic!("message not saved") - }; - c.remove_message(id, cx) - }) - .await - .unwrap(); - - // Client B sees that the message is gone. - executor.run_until_parked(); - let expected_messages = &["one", "three"]; - assert_messages(&channel_chat_a, expected_messages, cx_a); - assert_messages(&channel_chat_b, expected_messages, cx_b); - - // Client C joins the channel chat, and does not see the deleted message. - let channel_chat_c = client_c - .channel_store() - .update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - assert_messages(&channel_chat_c, expected_messages, cx_c); - - // Ensure we remove the notifications when the message is removed - client_b.notification_store().read_with(cx_b, |store, _| { - // First notification is the channel invitation, second would be the mention - // notification, which should now be removed. - assert_eq!(store.notification_count(), 1); - }); -} - -#[track_caller] -fn assert_messages(chat: &Entity, messages: &[&str], cx: &mut TestAppContext) { - assert_eq!( - chat.read_with(cx, |chat, _| { - chat.messages() - .iter() - .map(|m| m.body.clone()) - .collect::>() - }), - messages - ); -} - -#[gpui::test] -async fn test_channel_message_changes( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let channel_id = server - .make_channel( - "the-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b)], - ) - .await; - - // Client A sends a message, client B should see that there is a new message. - let channel_chat_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - - channel_chat_a - .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) - .await - .unwrap(); - - executor.run_until_parked(); - - let b_has_messages = cx_b.update(|cx| { - client_b - .channel_store() - .read(cx) - .has_new_messages(channel_id) - }); - - assert!(b_has_messages); - - // Opening the chat should clear the changed flag. - cx_b.update(|cx| { - collab_ui::init(&client_b.app_state, cx); - }); - let project_b = client_b.build_empty_local_project(cx_b); - let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); - - let chat_panel_b = workspace_b.update_in(cx_b, ChatPanel::new); - chat_panel_b - .update_in(cx_b, |chat_panel, window, cx| { - chat_panel.set_active(true, window, cx); - chat_panel.select_channel(channel_id, None, cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - let b_has_messages = cx_b.update(|_, cx| { - client_b - .channel_store() - .read(cx) - .has_new_messages(channel_id) - }); - - assert!(!b_has_messages); - - // Sending a message while the chat is open should not change the flag. - channel_chat_a - .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap()) - .await - .unwrap(); - - executor.run_until_parked(); - - let b_has_messages = cx_b.update(|_, cx| { - client_b - .channel_store() - .read(cx) - .has_new_messages(channel_id) - }); - - assert!(!b_has_messages); - - // Sending a message while the chat is closed should change the flag. - chat_panel_b.update_in(cx_b, |chat_panel, window, cx| { - chat_panel.set_active(false, window, cx); - }); - - // Sending a message while the chat is open should not change the flag. - channel_chat_a - .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap()) - .await - .unwrap(); - - executor.run_until_parked(); - - let b_has_messages = cx_b.update(|_, cx| { - client_b - .channel_store() - .read(cx) - .has_new_messages(channel_id) - }); - - assert!(b_has_messages); - - // Closing the chat should re-enable change tracking - cx_b.update(|_, _| drop(chat_panel_b)); - - channel_chat_a - .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap()) - .await - .unwrap(); - - executor.run_until_parked(); - - let b_has_messages = cx_b.update(|_, cx| { - client_b - .channel_store() - .read(cx) - .has_new_messages(channel_id) - }); - - assert!(b_has_messages); -} - -#[gpui::test] -async fn test_chat_replies(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { - let mut server = TestServer::start(cx_a.executor()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let channel_id = server - .make_channel( - "the-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b)], - ) - .await; - - // Client A sends a message, client B should see that there is a new message. - let channel_chat_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - - let channel_chat_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - - let msg_id = channel_chat_a - .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) - .await - .unwrap(); - - cx_a.run_until_parked(); - - let reply_id = channel_chat_b - .update(cx_b, |c, cx| { - c.send_message( - MessageParams { - text: "reply".into(), - reply_to_message_id: Some(msg_id), - mentions: Vec::new(), - }, - cx, - ) - .unwrap() - }) - .await - .unwrap(); - - cx_a.run_until_parked(); - - channel_chat_a.update(cx_a, |channel_chat, _| { - assert_eq!( - channel_chat - .find_loaded_message(reply_id) - .unwrap() - .reply_to_message_id, - Some(msg_id), - ) - }); -} - -#[gpui::test] -async fn test_chat_editing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { - let mut server = TestServer::start(cx_a.executor()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let channel_id = server - .make_channel( - "the-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b)], - ) - .await; - - // Client A sends a message, client B should see that there is a new message. - let channel_chat_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - - let channel_chat_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - - let msg_id = channel_chat_a - .update(cx_a, |c, cx| { - c.send_message( - MessageParams { - text: "Initial message".into(), - reply_to_message_id: None, - mentions: Vec::new(), - }, - cx, - ) - .unwrap() - }) - .await - .unwrap(); - - cx_a.run_until_parked(); - - channel_chat_a - .update(cx_a, |c, cx| { - c.update_message( - msg_id, - MessageParams { - text: "Updated body".into(), - reply_to_message_id: None, - mentions: Vec::new(), - }, - cx, - ) - .unwrap() - }) - .await - .unwrap(); - - cx_a.run_until_parked(); - cx_b.run_until_parked(); - - channel_chat_a.update(cx_a, |channel_chat, _| { - let update_message = channel_chat.find_loaded_message(msg_id).unwrap(); - - assert_eq!(update_message.body, "Updated body"); - assert_eq!(update_message.mentions, Vec::new()); - }); - channel_chat_b.update(cx_b, |channel_chat, _| { - let update_message = channel_chat.find_loaded_message(msg_id).unwrap(); - - assert_eq!(update_message.body, "Updated body"); - assert_eq!(update_message.mentions, Vec::new()); - }); - - // test mentions are updated correctly - - client_b.notification_store().read_with(cx_b, |store, _| { - assert_eq!(store.notification_count(), 1); - let entry = store.notification_at(0).unwrap(); - assert!(matches!( - entry.notification, - Notification::ChannelInvitation { .. } - ),); - }); - - channel_chat_a - .update(cx_a, |c, cx| { - c.update_message( - msg_id, - MessageParams { - text: "Updated body including a mention for @user_b".into(), - reply_to_message_id: None, - mentions: vec![(37..45, client_b.id())], - }, - cx, - ) - .unwrap() - }) - .await - .unwrap(); - - cx_a.run_until_parked(); - cx_b.run_until_parked(); - - channel_chat_a.update(cx_a, |channel_chat, _| { - assert_eq!( - channel_chat.find_loaded_message(msg_id).unwrap().body, - "Updated body including a mention for @user_b", - ) - }); - channel_chat_b.update(cx_b, |channel_chat, _| { - assert_eq!( - channel_chat.find_loaded_message(msg_id).unwrap().body, - "Updated body including a mention for @user_b", - ) - }); - client_b.notification_store().read_with(cx_b, |store, _| { - assert_eq!(store.notification_count(), 2); - let entry = store.notification_at(0).unwrap(); - assert_eq!( - entry.notification, - Notification::ChannelMessageMention { - message_id: msg_id, - sender_id: client_a.id(), - channel_id: channel_id.0, - } - ); - }); - - // Test update message and keep the mention and check that the body is updated correctly - - channel_chat_a - .update(cx_a, |c, cx| { - c.update_message( - msg_id, - MessageParams { - text: "Updated body v2 including a mention for @user_b".into(), - reply_to_message_id: None, - mentions: vec![(37..45, client_b.id())], - }, - cx, - ) - .unwrap() - }) - .await - .unwrap(); - - cx_a.run_until_parked(); - cx_b.run_until_parked(); - - channel_chat_a.update(cx_a, |channel_chat, _| { - assert_eq!( - channel_chat.find_loaded_message(msg_id).unwrap().body, - "Updated body v2 including a mention for @user_b", - ) - }); - channel_chat_b.update(cx_b, |channel_chat, _| { - assert_eq!( - channel_chat.find_loaded_message(msg_id).unwrap().body, - "Updated body v2 including a mention for @user_b", - ) - }); - - client_b.notification_store().read_with(cx_b, |store, _| { - let message = store.channel_message_for_id(msg_id); - assert!(message.is_some()); - assert_eq!( - message.unwrap().body, - "Updated body v2 including a mention for @user_b" - ); - assert_eq!(store.notification_count(), 2); - let entry = store.notification_at(0).unwrap(); - assert_eq!( - entry.notification, - Notification::ChannelMessageMention { - message_id: msg_id, - sender_id: client_a.id(), - channel_id: channel_id.0, - } - ); - }); - - // If we remove a mention from a message the corresponding mention notification - // should also be removed. - - channel_chat_a - .update(cx_a, |c, cx| { - c.update_message( - msg_id, - MessageParams { - text: "Updated body without a mention".into(), - reply_to_message_id: None, - mentions: vec![], - }, - cx, - ) - .unwrap() - }) - .await - .unwrap(); - - cx_a.run_until_parked(); - cx_b.run_until_parked(); - - channel_chat_a.update(cx_a, |channel_chat, _| { - assert_eq!( - channel_chat.find_loaded_message(msg_id).unwrap().body, - "Updated body without a mention", - ) - }); - channel_chat_b.update(cx_b, |channel_chat, _| { - assert_eq!( - channel_chat.find_loaded_message(msg_id).unwrap().body, - "Updated body without a mention", - ) - }); - client_b.notification_store().read_with(cx_b, |store, _| { - // First notification is the channel invitation, second would be the mention - // notification, which should now be removed. - assert_eq!(store.notification_count(), 1); - }); -} diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 46ba3ae49639a77dc1e93d0422290fd333acb3ad..34e40d767ea5a9cab115b4186a642ee234337845 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -37,18 +37,15 @@ client.workspace = true collections.workspace = true db.workspace = true editor.workspace = true -emojis.workspace = true futures.workspace = true fuzzy.workspace = true gpui.workspace = true -language.workspace = true log.workspace = true menu.workspace = true notifications.workspace = true picker.workspace = true project.workspace = true release_channel.workspace = true -rich_text.workspace = true rpc.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs deleted file mode 100644 index 8aaf6c0aa21f0b677ec091880fd8be674be1d6fe..0000000000000000000000000000000000000000 --- a/crates/collab_ui/src/chat_panel.rs +++ /dev/null @@ -1,1380 +0,0 @@ -use crate::{ChatPanelButton, ChatPanelSettings, collab_panel}; -use anyhow::Result; -use call::{ActiveCall, room}; -use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, ChannelStore}; -use client::{ChannelId, Client}; -use collections::HashMap; -use db::kvp::KEY_VALUE_STORE; -use editor::{Editor, actions}; -use gpui::{ - Action, App, AsyncWindowContext, ClipboardItem, Context, CursorStyle, DismissEvent, ElementId, - Entity, EventEmitter, FocusHandle, Focusable, FontWeight, HighlightStyle, ListOffset, - ListScrollEvent, ListState, Render, Stateful, Subscription, Task, WeakEntity, Window, actions, - div, list, prelude::*, px, -}; -use language::LanguageRegistry; -use menu::Confirm; -use message_editor::MessageEditor; -use project::Fs; -use rich_text::{Highlight, RichText}; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::{sync::Arc, time::Duration}; -use time::{OffsetDateTime, UtcOffset}; -use ui::{ - Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, PopoverMenu, Tab, TabBar, - Tooltip, prelude::*, -}; -use util::{ResultExt, TryFutureExt}; -use workspace::{ - Workspace, - dock::{DockPosition, Panel, PanelEvent}, -}; - -mod message_editor; - -const MESSAGE_LOADING_THRESHOLD: usize = 50; -const CHAT_PANEL_KEY: &str = "ChatPanel"; - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - }) - .detach(); -} - -pub struct ChatPanel { - client: Arc, - channel_store: Entity, - languages: Arc, - message_list: ListState, - active_chat: Option<(Entity, Subscription)>, - message_editor: Entity, - local_timezone: UtcOffset, - fs: Arc, - width: Option, - active: bool, - pending_serialization: Task>, - subscriptions: Vec, - is_scrolled_to_bottom: bool, - markdown_data: HashMap, - focus_handle: FocusHandle, - open_context_menu: Option<(u64, Subscription)>, - highlighted_message: Option<(u64, Task<()>)>, - last_acknowledged_message_id: Option, -} - -#[derive(Serialize, Deserialize)] -struct SerializedChatPanel { - width: Option, -} - -actions!( - chat_panel, - [ - /// Toggles focus on the chat panel. - ToggleFocus - ] -); - -impl ChatPanel { - pub fn new( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let fs = workspace.app_state().fs.clone(); - let client = workspace.app_state().client.clone(); - let channel_store = ChannelStore::global(cx); - let user_store = workspace.app_state().user_store.clone(); - let languages = workspace.app_state().languages.clone(); - - let input_editor = cx.new(|cx| { - MessageEditor::new( - languages.clone(), - user_store.clone(), - None, - cx.new(|cx| Editor::auto_height(1, 4, window, cx)), - window, - cx, - ) - }); - - cx.new(|cx| { - let message_list = ListState::new(0, gpui::ListAlignment::Bottom, px(1000.)); - - message_list.set_scroll_handler(cx.listener( - |this: &mut Self, event: &ListScrollEvent, _, cx| { - if event.visible_range.start < MESSAGE_LOADING_THRESHOLD { - this.load_more_messages(cx); - } - this.is_scrolled_to_bottom = !event.is_scrolled; - }, - )); - - let local_offset = chrono::Local::now().offset().local_minus_utc(); - let mut this = Self { - fs, - client, - channel_store, - languages, - message_list, - active_chat: Default::default(), - pending_serialization: Task::ready(None), - message_editor: input_editor, - local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(), - subscriptions: Vec::new(), - is_scrolled_to_bottom: true, - active: false, - width: None, - markdown_data: Default::default(), - focus_handle: cx.focus_handle(), - open_context_menu: None, - highlighted_message: None, - last_acknowledged_message_id: None, - }; - - if let Some(channel_id) = ActiveCall::global(cx) - .read(cx) - .room() - .and_then(|room| room.read(cx).channel_id()) - { - this.select_channel(channel_id, None, cx) - .detach_and_log_err(cx); - } - - this.subscriptions.push(cx.subscribe( - &ActiveCall::global(cx), - move |this: &mut Self, call, event: &room::Event, cx| match event { - room::Event::RoomJoined { channel_id } => { - if let Some(channel_id) = channel_id { - this.select_channel(*channel_id, None, cx) - .detach_and_log_err(cx); - - if call - .read(cx) - .room() - .is_some_and(|room| room.read(cx).contains_guests()) - { - cx.emit(PanelEvent::Activate) - } - } - } - room::Event::RoomLeft { channel_id } => { - if channel_id == &this.channel_id(cx) { - cx.emit(PanelEvent::Close) - } - } - _ => {} - }, - )); - - this - }) - } - - pub fn channel_id(&self, cx: &App) -> Option { - self.active_chat - .as_ref() - .map(|(chat, _)| chat.read(cx).channel_id) - } - - pub fn is_scrolled_to_bottom(&self) -> bool { - self.is_scrolled_to_bottom - } - - pub fn active_chat(&self) -> Option> { - self.active_chat.as_ref().map(|(chat, _)| chat.clone()) - } - - pub fn load( - workspace: WeakEntity, - cx: AsyncWindowContext, - ) -> Task>> { - cx.spawn(async move |cx| { - 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::(&panel)?) - } else { - None - }; - - workspace.update_in(cx, |workspace, window, cx| { - let panel = Self::new(workspace, window, cx); - if let Some(serialized_panel) = serialized_panel { - panel.update(cx, |panel, cx| { - panel.width = serialized_panel.width.map(|r| r.round()); - cx.notify(); - }); - } - panel - }) - }) - } - - fn serialize(&mut self, cx: &mut Context) { - 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(), - ); - } - - fn set_active_chat(&mut self, chat: Entity, cx: &mut Context) { - if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) { - self.markdown_data.clear(); - self.message_list.reset(chat.read(cx).message_count()); - self.message_editor.update(cx, |editor, cx| { - editor.set_channel_chat(chat.clone(), cx); - editor.clear_reply_to_message_id(); - }); - let subscription = cx.subscribe(&chat, Self::channel_did_change); - self.active_chat = Some((chat, subscription)); - self.acknowledge_last_message(cx); - cx.notify(); - } - } - - fn channel_did_change( - &mut self, - _: Entity, - event: &ChannelChatEvent, - cx: &mut Context, - ) { - match event { - ChannelChatEvent::MessagesUpdated { - old_range, - new_count, - } => { - self.message_list.splice(old_range.clone(), *new_count); - if self.active { - self.acknowledge_last_message(cx); - } - } - ChannelChatEvent::UpdateMessage { - message_id, - message_ix, - } => { - self.message_list.splice(*message_ix..*message_ix + 1, 1); - self.markdown_data.remove(message_id); - } - ChannelChatEvent::NewMessage { - channel_id, - message_id, - } => { - if !self.active { - self.channel_store.update(cx, |store, cx| { - store.update_latest_message_id(*channel_id, *message_id, cx) - }) - } - } - } - cx.notify(); - } - - fn acknowledge_last_message(&mut self, cx: &mut Context) { - if self.active - && self.is_scrolled_to_bottom - && let Some((chat, _)) = &self.active_chat - { - if let Some(channel_id) = self.channel_id(cx) { - self.last_acknowledged_message_id = self - .channel_store - .read(cx) - .last_acknowledge_message_id(channel_id); - } - - chat.update(cx, |chat, cx| { - chat.acknowledge_last_message(cx); - }); - } - } - - fn render_replied_to_message( - &mut self, - message_id: Option, - reply_to_message: &Option, - cx: &mut Context, - ) -> impl IntoElement { - let reply_to_message = match reply_to_message { - None => { - return div().child( - h_flex() - .text_ui_xs(cx) - .my_0p5() - .px_0p5() - .gap_x_1() - .rounded_sm() - .child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted)) - .when(reply_to_message.is_none(), |el| { - el.child( - Label::new("Message has been deleted...") - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - }), - ); - } - Some(val) => val, - }; - - let user_being_replied_to = reply_to_message.sender.clone(); - let message_being_replied_to = reply_to_message.clone(); - - let message_element_id: ElementId = match message_id { - Some(ChannelMessageId::Saved(id)) => ("reply-to-saved-message-container", id).into(), - Some(ChannelMessageId::Pending(id)) => { - ("reply-to-pending-message-container", id).into() - } // This should never happen - None => ("composing-reply-container").into(), - }; - - let current_channel_id = self.channel_id(cx); - let reply_to_message_id = reply_to_message.id; - - div().child( - h_flex() - .id(message_element_id) - .text_ui_xs(cx) - .my_0p5() - .px_0p5() - .gap_x_1() - .rounded_sm() - .overflow_hidden() - .hover(|style| style.bg(cx.theme().colors().element_background)) - .child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted)) - .child(Avatar::new(user_being_replied_to.avatar_uri.clone()).size(rems(0.7))) - .child( - Label::new(format!("@{}", user_being_replied_to.github_login)) - .size(LabelSize::XSmall) - .weight(FontWeight::SEMIBOLD) - .color(Color::Muted), - ) - .child( - div().overflow_y_hidden().child( - Label::new(message_being_replied_to.body.replace('\n', " ")) - .size(LabelSize::XSmall) - .color(Color::Default), - ), - ) - .cursor(CursorStyle::PointingHand) - .tooltip(Tooltip::text("Go to message")) - .on_click(cx.listener(move |chat_panel, _, _, cx| { - if let Some(channel_id) = current_channel_id { - chat_panel - .select_channel(channel_id, reply_to_message_id.into(), cx) - .detach_and_log_err(cx) - } - })), - ) - } - - fn render_message( - &mut self, - ix: usize, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let active_chat = &self.active_chat.as_ref().unwrap().0; - let (message, is_continuation_from_previous, is_admin) = - active_chat.update(cx, |active_chat, cx| { - let is_admin = self - .channel_store - .read(cx) - .is_channel_admin(active_chat.channel_id); - - let last_message = active_chat.message(ix.saturating_sub(1)); - let this_message = active_chat.message(ix).clone(); - - let duration_since_last_message = this_message.timestamp - last_message.timestamp; - let is_continuation_from_previous = last_message.sender.id - == this_message.sender.id - && last_message.id != this_message.id - && duration_since_last_message < Duration::from_secs(5 * 60); - - if let ChannelMessageId::Saved(id) = this_message.id - && this_message - .mentions - .iter() - .any(|(_, user_id)| Some(*user_id) == self.client.user_id()) - { - active_chat.acknowledge_message(id); - } - - (this_message, is_continuation_from_previous, is_admin) - }); - - let _is_pending = message.is_pending(); - - let belongs_to_user = Some(message.sender.id) == self.client.user_id(); - let can_delete_message = belongs_to_user || is_admin; - let can_edit_message = belongs_to_user; - - let element_id: ElementId = match message.id { - ChannelMessageId::Saved(id) => ("saved-message", id).into(), - ChannelMessageId::Pending(id) => ("pending-message", id).into(), - }; - - let mentioning_you = message - .mentions - .iter() - .any(|m| Some(m.1) == self.client.user_id()); - - let message_id = match message.id { - ChannelMessageId::Saved(id) => Some(id), - ChannelMessageId::Pending(_) => None, - }; - - let reply_to_message = message - .reply_to_message_id - .and_then(|id| active_chat.read(cx).find_loaded_message(id)) - .cloned(); - - let replied_to_you = - reply_to_message.as_ref().map(|m| m.sender.id) == self.client.user_id(); - - let is_highlighted_message = self - .highlighted_message - .as_ref() - .is_some_and(|(id, _)| Some(id) == message_id.as_ref()); - let background = if is_highlighted_message { - cx.theme().status().info_background - } else if mentioning_you || replied_to_you { - cx.theme().colors().background - } else { - cx.theme().colors().panel_background - }; - - let reply_to_message_id = self.message_editor.read(cx).reply_to_message_id(); - - v_flex() - .w_full() - .relative() - .group("") - .when(!is_continuation_from_previous, |this| this.pt_2()) - .child( - div() - .group("") - .bg(background) - .rounded_sm() - .overflow_hidden() - .px_1p5() - .py_0p5() - .when_some(reply_to_message_id, |el, reply_id| { - el.when_some(message_id, |el, message_id| { - el.when(reply_id == message_id, |el| { - el.bg(cx.theme().colors().element_selected) - }) - }) - }) - .when(!self.has_open_menu(message_id), |this| { - this.hover(|style| style.bg(cx.theme().colors().element_hover)) - }) - .when(message.reply_to_message_id.is_some(), |el| { - el.child(self.render_replied_to_message( - Some(message.id), - &reply_to_message, - cx, - )) - .when(is_continuation_from_previous, |this| this.mt_2()) - }) - .when( - !is_continuation_from_previous || message.reply_to_message_id.is_some(), - |this| { - this.child( - h_flex() - .gap_2() - .text_ui_sm(cx) - .child( - Avatar::new(message.sender.avatar_uri.clone()) - .size(rems(1.)), - ) - .child( - Label::new(message.sender.github_login.clone()) - .size(LabelSize::Small) - .weight(FontWeight::BOLD), - ) - .child( - Label::new(time_format::format_localized_timestamp( - message.timestamp, - OffsetDateTime::now_utc(), - self.local_timezone, - time_format::TimestampFormat::EnhancedAbsolute, - )) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - }, - ) - .when(mentioning_you || replied_to_you, |this| this.my_0p5()) - .map(|el| { - let text = self.markdown_data.entry(message.id).or_insert_with(|| { - Self::render_markdown_with_mentions( - &self.languages, - self.client.id(), - &message, - self.local_timezone, - cx, - ) - }); - el.child( - v_flex() - .w_full() - .text_ui_sm(cx) - .id(element_id) - .child(text.element("body".into(), window, cx)), - ) - .when(self.has_open_menu(message_id), |el| { - el.bg(cx.theme().colors().element_selected) - }) - }), - ) - .when( - self.last_acknowledged_message_id - .is_some_and(|l| Some(l) == message_id), - |this| { - this.child( - h_flex() - .py_2() - .gap_1() - .items_center() - .child(div().w_full().h_0p5().bg(cx.theme().colors().border)) - .child( - div() - .px_1() - .rounded_sm() - .text_ui_xs(cx) - .bg(cx.theme().colors().background) - .child("New messages"), - ) - .child(div().w_full().h_0p5().bg(cx.theme().colors().border)), - ) - }, - ) - .child( - self.render_popover_buttons(message_id, can_delete_message, can_edit_message, cx) - .mt_neg_2p5(), - ) - .into_any_element() - } - - fn has_open_menu(&self, message_id: Option) -> bool { - match self.open_context_menu.as_ref() { - Some((id, _)) => Some(*id) == message_id, - None => false, - } - } - - fn render_popover_button(&self, cx: &mut Context, child: Stateful
) -> Div { - div() - .w_6() - .bg(cx.theme().colors().element_background) - .hover(|style| style.bg(cx.theme().colors().element_hover).rounded_sm()) - .child(child) - } - - fn render_popover_buttons( - &self, - message_id: Option, - can_delete_message: bool, - can_edit_message: bool, - cx: &mut Context, - ) -> Div { - h_flex() - .absolute() - .right_2() - .overflow_hidden() - .rounded_sm() - .border_color(cx.theme().colors().element_selected) - .border_1() - .when(!self.has_open_menu(message_id), |el| { - el.visible_on_hover("") - }) - .bg(cx.theme().colors().element_background) - .when_some(message_id, |el, message_id| { - el.child( - self.render_popover_button( - cx, - div() - .id("reply") - .child( - IconButton::new(("reply", message_id), IconName::ReplyArrowRight) - .on_click(cx.listener(move |this, _, window, cx| { - this.cancel_edit_message(cx); - - this.message_editor.update(cx, |editor, cx| { - editor.set_reply_to_message_id(message_id); - window.focus(&editor.focus_handle(cx)); - }) - })), - ) - .tooltip(Tooltip::text("Reply")), - ), - ) - }) - .when_some(message_id, |el, message_id| { - el.when(can_edit_message, |el| { - el.child( - self.render_popover_button( - cx, - div() - .id("edit") - .child( - IconButton::new(("edit", message_id), IconName::Pencil) - .on_click(cx.listener(move |this, _, window, cx| { - this.message_editor.update(cx, |editor, cx| { - editor.clear_reply_to_message_id(); - - let message = this - .active_chat() - .and_then(|active_chat| { - active_chat - .read(cx) - .find_loaded_message(message_id) - }) - .cloned(); - - if let Some(message) = message { - let buffer = editor - .editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .expect("message editor must be singleton"); - - buffer.update(cx, |buffer, cx| { - buffer.set_text(message.body.clone(), cx) - }); - - editor.set_edit_message_id(message_id); - editor.focus_handle(cx).focus(window); - } - }) - })), - ) - .tooltip(Tooltip::text("Edit")), - ), - ) - }) - }) - .when_some(message_id, |el, message_id| { - let this = cx.entity(); - - el.child( - self.render_popover_button( - cx, - div() - .child( - PopoverMenu::new(("menu", message_id)) - .trigger(IconButton::new( - ("trigger", message_id), - IconName::Ellipsis, - )) - .menu(move |window, cx| { - Some(Self::render_message_menu( - &this, - message_id, - can_delete_message, - window, - cx, - )) - }), - ) - .id("more") - .tooltip(Tooltip::text("More")), - ), - ) - }) - } - - fn render_message_menu( - this: &Entity, - message_id: u64, - can_delete_message: bool, - window: &mut Window, - cx: &mut App, - ) -> Entity { - let menu = { - ContextMenu::build(window, cx, move |menu, window, _| { - menu.entry( - "Copy message text", - None, - window.handler_for(this, move |this, _, cx| { - if let Some(message) = this.active_chat().and_then(|active_chat| { - active_chat.read(cx).find_loaded_message(message_id) - }) { - let text = message.body.clone(); - cx.write_to_clipboard(ClipboardItem::new_string(text)) - } - }), - ) - .when(can_delete_message, |menu| { - menu.entry( - "Delete message", - None, - window.handler_for(this, move |this, _, cx| { - this.remove_message(message_id, cx) - }), - ) - }) - }) - }; - this.update(cx, |this, cx| { - let subscription = cx.subscribe_in( - &menu, - window, - |this: &mut Self, _, _: &DismissEvent, _, _| { - this.open_context_menu = None; - }, - ); - this.open_context_menu = Some((message_id, subscription)); - }); - menu - } - - fn render_markdown_with_mentions( - language_registry: &Arc, - current_user_id: u64, - message: &channel::ChannelMessage, - local_timezone: UtcOffset, - cx: &App, - ) -> RichText { - let mentions = message - .mentions - .iter() - .map(|(range, user_id)| rich_text::Mention { - range: range.clone(), - is_self_mention: *user_id == current_user_id, - }) - .collect::>(); - - const MESSAGE_EDITED: &str = " (edited)"; - - let mut body = message.body.clone(); - - if message.edited_at.is_some() { - body.push_str(MESSAGE_EDITED); - } - - let mut rich_text = RichText::new(body, &mentions, language_registry); - - if message.edited_at.is_some() { - let range = (rich_text.text.len() - MESSAGE_EDITED.len())..rich_text.text.len(); - rich_text.highlights.push(( - range.clone(), - Highlight::Highlight(HighlightStyle { - color: Some(cx.theme().colors().text_muted), - ..Default::default() - }), - )); - - if let Some(edit_timestamp) = message.edited_at { - let edit_timestamp_text = time_format::format_localized_timestamp( - edit_timestamp, - OffsetDateTime::now_utc(), - local_timezone, - time_format::TimestampFormat::Absolute, - ); - - rich_text.custom_ranges.push(range); - rich_text.set_tooltip_builder_for_custom_ranges(move |_, _, _, cx| { - Some(Tooltip::simple(edit_timestamp_text.clone(), cx)) - }) - } - } - rich_text - } - - fn send(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { - if let Some((chat, _)) = self.active_chat.as_ref() { - let message = self - .message_editor - .update(cx, |editor, cx| editor.take_message(window, cx)); - - if let Some(id) = self.message_editor.read(cx).edit_message_id() { - self.message_editor.update(cx, |editor, _| { - editor.clear_edit_message_id(); - }); - - if let Some(task) = chat - .update(cx, |chat, cx| chat.update_message(id, message, cx)) - .log_err() - { - task.detach(); - } - } else if let Some(task) = chat - .update(cx, |chat, cx| chat.send_message(message, cx)) - .log_err() - { - task.detach(); - } - } - } - - fn remove_message(&mut self, id: u64, cx: &mut Context) { - if let Some((chat, _)) = self.active_chat.as_ref() { - chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach()) - } - } - - fn load_more_messages(&mut self, cx: &mut Context) { - if let Some((chat, _)) = self.active_chat.as_ref() { - chat.update(cx, |channel, cx| { - if let Some(task) = channel.load_more_messages(cx) { - task.detach(); - } - }) - } - } - - pub fn select_channel( - &mut self, - selected_channel_id: ChannelId, - scroll_to_message_id: Option, - cx: &mut Context, - ) -> Task> { - let open_chat = self - .active_chat - .as_ref() - .and_then(|(chat, _)| { - (chat.read(cx).channel_id == selected_channel_id) - .then(|| Task::ready(anyhow::Ok(chat.clone()))) - }) - .unwrap_or_else(|| { - self.channel_store.update(cx, |store, cx| { - store.open_channel_chat(selected_channel_id, cx) - }) - }); - - cx.spawn(async move |this, cx| { - let chat = open_chat.await?; - let highlight_message_id = scroll_to_message_id; - let scroll_to_message_id = this.update(cx, |this, cx| { - this.set_active_chat(chat.clone(), cx); - - scroll_to_message_id.or(this.last_acknowledged_message_id) - })?; - - if let Some(message_id) = scroll_to_message_id - && let Some(item_ix) = - ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone()) - .await - { - this.update(cx, |this, cx| { - if let Some(highlight_message_id) = highlight_message_id { - let task = cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(2)).await; - this.update(cx, |this, cx| { - this.highlighted_message.take(); - cx.notify(); - }) - .ok(); - }); - - this.highlighted_message = Some((highlight_message_id, task)); - } - - if this.active_chat.as_ref().is_some_and(|(c, _)| *c == chat) { - this.message_list.scroll_to(ListOffset { - item_ix, - offset_in_item: px(0.0), - }); - cx.notify(); - } - })?; - } - - Ok(()) - }) - } - - fn close_reply_preview(&mut self, cx: &mut Context) { - self.message_editor - .update(cx, |editor, _| editor.clear_reply_to_message_id()); - } - - fn cancel_edit_message(&mut self, cx: &mut Context) { - self.message_editor.update(cx, |editor, cx| { - // only clear the editor input if we were editing a message - if editor.edit_message_id().is_none() { - return; - } - - editor.clear_edit_message_id(); - - let buffer = editor - .editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .expect("message editor must be singleton"); - - buffer.update(cx, |buffer, cx| buffer.set_text("", cx)); - }); - } -} - -impl Render for ChatPanel { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let channel_id = self - .active_chat - .as_ref() - .map(|(c, _)| c.read(cx).channel_id); - let message_editor = self.message_editor.read(cx); - - let reply_to_message_id = message_editor.reply_to_message_id(); - let edit_message_id = message_editor.edit_message_id(); - - v_flex() - .key_context("ChatPanel") - .track_focus(&self.focus_handle) - .size_full() - .on_action(cx.listener(Self::send)) - .child( - h_flex().child( - TabBar::new("chat_header").child( - h_flex() - .w_full() - .h(Tab::container_height(cx)) - .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()), - )), - ), - ), - ) - .child(div().flex_grow().px_2().map(|this| { - if self.active_chat.is_some() { - this.child( - list( - self.message_list.clone(), - cx.processor(Self::render_message), - ) - .size_full(), - ) - } else { - this.child( - div() - .size_full() - .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, - window, - cx, - )) - .on_click(|_, window, cx| { - window.dispatch_action( - collab_panel::ToggleFocus.boxed_clone(), - cx, - ) - }), - ), - ), - ) - } - })) - .when(!self.is_scrolled_to_bottom, |el| { - el.child(div().border_t_1().border_color(cx.theme().colors().border)) - }) - .when_some(edit_message_id, |el, _| { - el.child( - h_flex() - .px_2() - .text_ui_xs(cx) - .justify_between() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child("Editing message") - .child( - IconButton::new("cancel-edit-message", IconName::Close) - .shape(ui::IconButtonShape::Square) - .tooltip(Tooltip::text("Cancel edit message")) - .on_click(cx.listener(move |this, _, _, cx| { - this.cancel_edit_message(cx); - })), - ), - ) - }) - .when_some(reply_to_message_id, |el, reply_to_message_id| { - let reply_message = self - .active_chat() - .and_then(|active_chat| { - active_chat - .read(cx) - .find_loaded_message(reply_to_message_id) - }) - .cloned(); - - el.when_some(reply_message, |el, reply_message| { - let user_being_replied_to = reply_message.sender; - - el.child( - h_flex() - .when(!self.is_scrolled_to_bottom, |el| { - el.border_t_1().border_color(cx.theme().colors().border) - }) - .justify_between() - .overflow_hidden() - .items_start() - .py_1() - .px_2() - .bg(cx.theme().colors().background) - .child( - div().flex_shrink().overflow_hidden().child( - h_flex() - .id(("reply-preview", reply_to_message_id)) - .child(Label::new("Replying to ").size(LabelSize::Small)) - .child( - Label::new(format!( - "@{}", - user_being_replied_to.github_login - )) - .size(LabelSize::Small) - .weight(FontWeight::BOLD), - ) - .when_some(channel_id, |this, channel_id| { - this.cursor_pointer().on_click(cx.listener( - move |chat_panel, _, _, cx| { - chat_panel - .select_channel( - channel_id, - reply_to_message_id.into(), - cx, - ) - .detach_and_log_err(cx) - }, - )) - }), - ), - ) - .child( - IconButton::new("close-reply-preview", IconName::Close) - .shape(ui::IconButtonShape::Square) - .tooltip(Tooltip::text("Close reply")) - .on_click(cx.listener(move |this, _, _, cx| { - this.close_reply_preview(cx); - })), - ), - ) - }) - }) - .children( - Some( - h_flex() - .p_2() - .on_action(cx.listener(|this, _: &actions::Cancel, _, cx| { - this.cancel_edit_message(cx); - this.close_reply_preview(cx); - })) - .map(|el| el.child(self.message_editor.clone())), - ) - .filter(|_| self.active_chat.is_some()), - ) - .into_any() - } -} - -impl Focusable for ChatPanel { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - if self.active_chat.is_some() { - self.message_editor.read(cx).focus_handle(cx) - } else { - self.focus_handle.clone() - } - } -} - -impl Panel for ChatPanel { - fn position(&self, _: &Window, cx: &App) -> DockPosition { - ChatPanelSettings::get_global(cx).dock - } - - fn position_is_valid(&self, position: DockPosition) -> bool { - matches!(position, DockPosition::Left | DockPosition::Right) - } - - fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { - settings::update_settings_file::( - self.fs.clone(), - cx, - move |settings, _| settings.dock = Some(position), - ); - } - - fn size(&self, _: &Window, cx: &App) -> Pixels { - self.width - .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width) - } - - fn set_size(&mut self, size: Option, _: &mut Window, cx: &mut Context) { - self.width = size; - self.serialize(cx); - cx.notify(); - } - - fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut Context) { - self.active = active; - if active { - self.acknowledge_last_message(cx); - } - } - - fn persistent_name() -> &'static str { - "ChatPanel" - } - - fn icon(&self, _window: &Window, cx: &App) -> Option { - self.enabled(cx).then(|| ui::IconName::Chat) - } - - fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> { - Some("Chat Panel") - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } - - fn starts_open(&self, _: &Window, cx: &App) -> bool { - ActiveCall::global(cx) - .read(cx) - .room() - .is_some_and(|room| room.read(cx).contains_guests()) - } - - fn activation_priority(&self) -> u32 { - 7 - } - - fn enabled(&self, cx: &App) -> bool { - match ChatPanelSettings::get_global(cx).button { - ChatPanelButton::Never => false, - ChatPanelButton::Always => true, - ChatPanelButton::WhenInCall => { - let is_in_call = ActiveCall::global(cx) - .read(cx) - .room() - .is_some_and(|room| room.read(cx).contains_guests()); - - self.active || is_in_call - } - } - } -} - -impl EventEmitter for ChatPanel {} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::HighlightStyle; - use pretty_assertions::assert_eq; - use rich_text::Highlight; - use time::OffsetDateTime; - use util::test::marked_text_ranges; - - #[gpui::test] - fn test_render_markdown_with_mentions(cx: &mut App) { - let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); - let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false); - let message = channel::ChannelMessage { - id: ChannelMessageId::Saved(0), - body, - timestamp: OffsetDateTime::now_utc(), - sender: Arc::new(client::User { - github_login: "fgh".into(), - avatar_uri: "avatar_fgh".into(), - id: 103, - name: None, - }), - nonce: 5, - mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)], - reply_to_message_id: None, - edited_at: None, - }; - - let message = ChatPanel::render_markdown_with_mentions( - &language_registry, - 102, - &message, - UtcOffset::UTC, - cx, - ); - - // Note that the "'" was replaced with ’ due to smart punctuation. - let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false); - assert_eq!(message.text, body); - assert_eq!( - message.highlights, - vec![ - ( - ranges[0].clone(), - HighlightStyle { - font_style: Some(gpui::FontStyle::Italic), - ..Default::default() - } - .into() - ), - (ranges[1].clone(), Highlight::Mention), - ( - ranges[2].clone(), - HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - } - .into() - ), - (ranges[3].clone(), Highlight::SelfMention) - ] - ); - } - - #[gpui::test] - fn test_render_markdown_with_auto_detect_links(cx: &mut App) { - let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); - let message = channel::ChannelMessage { - id: ChannelMessageId::Saved(0), - body: "Here is a link https://zed.dev to zeds website".to_string(), - timestamp: OffsetDateTime::now_utc(), - sender: Arc::new(client::User { - github_login: "fgh".into(), - avatar_uri: "avatar_fgh".into(), - id: 103, - name: None, - }), - nonce: 5, - mentions: Vec::new(), - reply_to_message_id: None, - edited_at: None, - }; - - let message = ChatPanel::render_markdown_with_mentions( - &language_registry, - 102, - &message, - UtcOffset::UTC, - cx, - ); - - // Note that the "'" was replaced with ’ due to smart punctuation. - let (body, ranges) = - marked_text_ranges("Here is a link «https://zed.dev» to zeds website", false); - assert_eq!(message.text, body); - assert_eq!(1, ranges.len()); - assert_eq!( - message.highlights, - vec![( - ranges[0].clone(), - HighlightStyle { - underline: Some(gpui::UnderlineStyle { - thickness: 1.0.into(), - ..Default::default() - }), - ..Default::default() - } - .into() - ),] - ); - } - - #[gpui::test] - fn test_render_markdown_with_auto_detect_links_and_additional_formatting(cx: &mut App) { - let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); - let message = channel::ChannelMessage { - id: ChannelMessageId::Saved(0), - body: "**Here is a link https://zed.dev to zeds website**".to_string(), - timestamp: OffsetDateTime::now_utc(), - sender: Arc::new(client::User { - github_login: "fgh".into(), - avatar_uri: "avatar_fgh".into(), - id: 103, - name: None, - }), - nonce: 5, - mentions: Vec::new(), - reply_to_message_id: None, - edited_at: None, - }; - - let message = ChatPanel::render_markdown_with_mentions( - &language_registry, - 102, - &message, - UtcOffset::UTC, - cx, - ); - - // Note that the "'" was replaced with ’ due to smart punctuation. - let (body, ranges) = marked_text_ranges( - "«Here is a link »«https://zed.dev»« to zeds website»", - false, - ); - assert_eq!(message.text, body); - assert_eq!(3, ranges.len()); - assert_eq!( - message.highlights, - vec![ - ( - ranges[0].clone(), - HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - } - .into() - ), - ( - ranges[1].clone(), - HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - underline: Some(gpui::UnderlineStyle { - thickness: 1.0.into(), - ..Default::default() - }), - ..Default::default() - } - .into() - ), - ( - ranges[2].clone(), - HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - } - .into() - ), - ] - ); - } -} diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs deleted file mode 100644 index 3864ca69d88dd8231aa4b2f5b656c11f41b07282..0000000000000000000000000000000000000000 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ /dev/null @@ -1,548 +0,0 @@ -use anyhow::{Context as _, Result}; -use channel::{ChannelChat, ChannelStore, MessageParams}; -use client::{UserId, UserStore}; -use collections::HashSet; -use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId}; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - AsyncApp, AsyncWindowContext, Context, Entity, Focusable, FontStyle, FontWeight, - HighlightStyle, IntoElement, Render, Task, TextStyle, WeakEntity, Window, -}; -use language::{ - Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, ToOffset, - language_settings::SoftWrap, -}; -use project::{ - Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource, search::SearchQuery, -}; -use settings::Settings; -use std::{ - ops::Range, - rc::Rc, - sync::{Arc, LazyLock}, - time::Duration, -}; -use theme::ThemeSettings; -use ui::{TextSize, prelude::*}; - -use crate::panel_settings::MessageEditorSettings; - -const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50); - -static MENTIONS_SEARCH: LazyLock = LazyLock::new(|| { - SearchQuery::regex( - "@[-_\\w]+", - false, - false, - false, - false, - Default::default(), - Default::default(), - false, - None, - ) - .unwrap() -}); - -pub struct MessageEditor { - pub editor: Entity, - user_store: Entity, - channel_chat: Option>, - mentions: Vec, - mentions_task: Option>, - reply_to_message_id: Option, - edit_message_id: Option, -} - -struct MessageEditorCompletionProvider(WeakEntity); - -impl CompletionProvider for MessageEditorCompletionProvider { - fn completions( - &self, - _excerpt_id: ExcerptId, - buffer: &Entity, - buffer_position: language::Anchor, - _: editor::CompletionContext, - _window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Some(handle) = self.0.upgrade() else { - return Task::ready(Ok(Vec::new())); - }; - handle.update(cx, |message_editor, cx| { - message_editor.completions(buffer, buffer_position, cx) - }) - } - - fn is_completion_trigger( - &self, - _buffer: &Entity, - _position: language::Anchor, - text: &str, - _trigger_in_words: bool, - _menu_is_open: bool, - _cx: &mut Context, - ) -> bool { - text == "@" - } -} - -impl MessageEditor { - pub fn new( - language_registry: Arc, - user_store: Entity, - channel_chat: Option>, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let this = cx.entity().downgrade(); - editor.update(cx, |editor, cx| { - editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); - editor.set_offset_content(false, cx); - editor.set_use_autoclose(false); - editor.set_show_gutter(false, cx); - editor.set_show_wrap_guides(false, cx); - editor.set_show_indent_guides(false, cx); - editor.set_completion_provider(Some(Rc::new(MessageEditorCompletionProvider(this)))); - editor.set_auto_replace_emoji_shortcode( - MessageEditorSettings::get_global(cx) - .auto_replace_emoji_shortcode - .unwrap_or_default(), - ); - }); - - let buffer = editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .expect("message editor must be singleton"); - - cx.subscribe_in(&buffer, window, Self::on_buffer_event) - .detach(); - cx.observe_global::(|this, cx| { - this.editor.update(cx, |editor, cx| { - editor.set_auto_replace_emoji_shortcode( - MessageEditorSettings::get_global(cx) - .auto_replace_emoji_shortcode - .unwrap_or_default(), - ) - }) - }) - .detach(); - - let markdown = language_registry.language_for_name("Markdown"); - cx.spawn_in(window, async move |_, cx| { - let markdown = markdown.await.context("failed to load Markdown language")?; - buffer.update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx)) - }) - .detach_and_log_err(cx); - - Self { - editor, - user_store, - channel_chat, - mentions: Vec::new(), - mentions_task: None, - reply_to_message_id: None, - edit_message_id: None, - } - } - - pub fn reply_to_message_id(&self) -> Option { - self.reply_to_message_id - } - - pub fn set_reply_to_message_id(&mut self, reply_to_message_id: u64) { - self.reply_to_message_id = Some(reply_to_message_id); - } - - pub fn clear_reply_to_message_id(&mut self) { - self.reply_to_message_id = None; - } - - pub fn edit_message_id(&self) -> Option { - self.edit_message_id - } - - pub fn set_edit_message_id(&mut self, edit_message_id: u64) { - self.edit_message_id = Some(edit_message_id); - } - - pub fn clear_edit_message_id(&mut self) { - self.edit_message_id = None; - } - - pub fn set_channel_chat(&mut self, chat: Entity, cx: &mut Context) { - let channel_id = chat.read(cx).channel_id; - self.channel_chat = Some(chat); - let channel_name = ChannelStore::global(cx) - .read(cx) - .channel_for_id(channel_id) - .map(|channel| channel.name.clone()); - self.editor.update(cx, |editor, cx| { - if let Some(channel_name) = channel_name { - editor.set_placeholder_text(format!("Message #{channel_name}"), cx); - } else { - editor.set_placeholder_text("Message Channel", cx); - } - }); - } - - pub fn take_message(&mut self, window: &mut Window, cx: &mut Context) -> MessageParams { - self.editor.update(cx, |editor, cx| { - let highlights = editor.text_highlights::(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(window, cx); - self.mentions.clear(); - let reply_to_message_id = std::mem::take(&mut self.reply_to_message_id); - - MessageParams { - text, - mentions, - reply_to_message_id, - } - }) - } - - fn on_buffer_event( - &mut self, - buffer: &Entity, - event: &language::BufferEvent, - window: &mut Window, - cx: &mut Context, - ) { - if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event { - let buffer = buffer.read(cx).snapshot(); - self.mentions_task = Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(MENTIONS_DEBOUNCE_INTERVAL) - .await; - Self::find_mentions(this, buffer, cx).await; - })); - } - } - - fn completions( - &mut self, - buffer: &Entity, - end_anchor: Anchor, - cx: &mut Context, - ) -> Task>> { - if let Some((start_anchor, query, candidates)) = - self.collect_mention_candidates(buffer, end_anchor, cx) - && !candidates.is_empty() - { - return cx.spawn(async move |_, cx| { - let completion_response = Self::completions_for_candidates( - cx, - query.as_str(), - &candidates, - start_anchor..end_anchor, - Self::completion_for_mention, - ) - .await; - Ok(vec![completion_response]) - }); - } - - if let Some((start_anchor, query, candidates)) = - self.collect_emoji_candidates(buffer, end_anchor, cx) - && !candidates.is_empty() - { - return cx.spawn(async move |_, cx| { - let completion_response = Self::completions_for_candidates( - cx, - query.as_str(), - candidates, - start_anchor..end_anchor, - Self::completion_for_emoji, - ) - .await; - Ok(vec![completion_response]) - }); - } - - Task::ready(Ok(vec![CompletionResponse { - completions: Vec::new(), - display_options: CompletionDisplayOptions::default(), - is_incomplete: false, - }])) - } - - async fn completions_for_candidates( - cx: &AsyncApp, - query: &str, - candidates: &[StringMatchCandidate], - range: Range, - completion_fn: impl Fn(&StringMatch) -> (String, CodeLabel), - ) -> CompletionResponse { - const LIMIT: usize = 10; - let matches = fuzzy::match_strings( - candidates, - query, - true, - true, - LIMIT, - &Default::default(), - cx.background_executor().clone(), - ) - .await; - - let completions = matches - .into_iter() - .map(|mat| { - let (new_text, label) = completion_fn(&mat); - Completion { - replace_range: range.clone(), - new_text, - label, - icon_path: None, - confirm: None, - documentation: None, - insert_text_mode: None, - source: CompletionSource::Custom, - } - }) - .collect::>(); - - CompletionResponse { - is_incomplete: completions.len() >= LIMIT, - display_options: CompletionDisplayOptions::default(), - completions, - } - } - - fn completion_for_mention(mat: &StringMatch) -> (String, CodeLabel) { - let label = CodeLabel { - filter_range: 1..mat.string.len() + 1, - text: format!("@{}", mat.string), - runs: Vec::new(), - }; - (mat.string.clone(), label) - } - - fn completion_for_emoji(mat: &StringMatch) -> (String, CodeLabel) { - let emoji = emojis::get_by_shortcode(&mat.string).unwrap(); - let label = CodeLabel { - filter_range: 1..mat.string.len() + 1, - text: format!(":{}: {}", mat.string, emoji), - runs: Vec::new(), - }; - (emoji.to_string(), label) - } - - fn collect_mention_candidates( - &mut self, - buffer: &Entity, - end_anchor: Anchor, - cx: &mut Context, - ) -> Option<(Anchor, String, Vec)> { - let end_offset = end_anchor.to_offset(buffer.read(cx)); - - let query = buffer.read_with(cx, |buffer, _| { - let mut query = String::new(); - for ch in buffer.reversed_chars_at(end_offset).take(100) { - if ch == '@' { - return Some(query.chars().rev().collect::()); - } - if ch.is_whitespace() || !ch.is_ascii() { - break; - } - query.push(ch); - } - None - })?; - - let start_offset = end_offset - query.len(); - let start_anchor = buffer.read(cx).anchor_before(start_offset); - - let mut names = HashSet::default(); - if let Some(chat) = self.channel_chat.as_ref() { - let chat = chat.read(cx); - for participant in ChannelStore::global(cx) - .read(cx) - .channel_participants(chat.channel_id) - { - names.insert(participant.github_login.clone()); - } - for message in chat - .messages_in_range(chat.message_count().saturating_sub(100)..chat.message_count()) - { - names.insert(message.sender.github_login.clone()); - } - } - - let candidates = names - .into_iter() - .map(|user| StringMatchCandidate::new(0, &user)) - .collect::>(); - - Some((start_anchor, query, candidates)) - } - - fn collect_emoji_candidates( - &mut self, - buffer: &Entity, - end_anchor: Anchor, - cx: &mut Context, - ) -> Option<(Anchor, String, &'static [StringMatchCandidate])> { - static EMOJI_FUZZY_MATCH_CANDIDATES: LazyLock> = - LazyLock::new(|| { - emojis::iter() - .flat_map(|s| s.shortcodes()) - .map(|emoji| StringMatchCandidate::new(0, emoji)) - .collect::>() - }); - - let end_offset = end_anchor.to_offset(buffer.read(cx)); - - let query = buffer.read_with(cx, |buffer, _| { - let mut query = String::new(); - for ch in buffer.reversed_chars_at(end_offset).take(100) { - if ch == ':' { - let next_char = buffer - .reversed_chars_at(end_offset - query.len() - 1) - .next(); - // Ensure we are at the start of the message or that the previous character is a whitespace - if next_char.is_none() || next_char.unwrap().is_whitespace() { - return Some(query.chars().rev().collect::()); - } - - // If the previous character is not a whitespace, we are in the middle of a word - // and we only want to complete the shortcode if the word is made up of other emojis - let mut containing_word = String::new(); - for ch in buffer - .reversed_chars_at(end_offset - query.len() - 1) - .take(100) - { - if ch.is_whitespace() { - break; - } - containing_word.push(ch); - } - let containing_word = containing_word.chars().rev().collect::(); - if util::word_consists_of_emojis(containing_word.as_str()) { - return Some(query.chars().rev().collect::()); - } - break; - } - if ch.is_whitespace() || !ch.is_ascii() { - break; - } - query.push(ch); - } - None - })?; - - let start_offset = end_offset - query.len() - 1; - let start_anchor = buffer.read(cx).anchor_before(start_offset); - - Some((start_anchor, query, &EMOJI_FUZZY_MATCH_CANDIDATES)) - } - - async fn find_mentions( - this: WeakEntity, - buffer: BufferSnapshot, - cx: &mut AsyncWindowContext, - ) { - let (buffer, ranges) = cx - .background_spawn(async move { - let ranges = MENTIONS_SEARCH.search(&buffer, None).await; - (buffer, ranges) - }) - .await; - - this.update(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('@') - && let Some(user) = this - .user_store - .read(cx) - .cached_user_by_github_login(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::(cx); - editor.highlight_text::( - anchor_ranges, - HighlightStyle { - font_weight: Some(FontWeight::BOLD), - ..Default::default() - }, - cx, - ) - }); - - this.mentions = mentioned_user_ids; - this.mentions_task.take(); - }) - .ok(); - } - - pub(crate) fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { - self.editor.read(cx).focus_handle(cx) - } -} - -impl Render for MessageEditor { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: if self.editor.read(cx).read_only(cx) { - cx.theme().colors().text_disabled - } else { - cx.theme().colors().text - }, - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features.clone(), - font_fallbacks: settings.ui_font.fallbacks.clone(), - font_size: TextSize::Small.rems(cx).into(), - font_weight: settings.ui_font.weight, - font_style: FontStyle::Normal, - line_height: relative(1.3), - ..Default::default() - }; - - div() - .w_full() - .px_2() - .py_1() - .bg(cx.theme().colors().editor_background) - .rounded_sm() - .child(EditorElement::new( - &self.editor, - EditorStyle { - local_player: cx.theme().players().local(), - text: text_style, - ..Default::default() - }, - )) - } -} diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 90096542942e18ff9a0355d6319e5dcf590a870c..b9ef535f1dbc781405cfe74584ca03f461f66c34 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2,7 +2,7 @@ mod channel_modal; mod contact_finder; use self::channel_modal::ChannelModal; -use crate::{CollaborationPanelSettings, channel_view::ChannelView, chat_panel::ChatPanel}; +use crate::{CollaborationPanelSettings, channel_view::ChannelView}; use anyhow::Context as _; use call::ActiveCall; use channel::{Channel, ChannelEvent, ChannelStore}; @@ -38,7 +38,7 @@ use util::{ResultExt, TryFutureExt, maybe}; use workspace::{ Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace, dock::{DockPosition, Panel, PanelEvent}, - notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt}, + notifications::{DetachAndPromptErr, NotifyResultExt}, }; actions!( @@ -261,9 +261,6 @@ enum ListEntry { ChannelNotes { channel_id: ChannelId, }, - ChannelChat { - channel_id: ChannelId, - }, ChannelEditor { depth: usize, }, @@ -495,7 +492,6 @@ impl CollabPanel { && let Some(channel_id) = room.channel_id() { self.entries.push(ListEntry::ChannelNotes { channel_id }); - self.entries.push(ListEntry::ChannelChat { channel_id }); } // Populate the active user. @@ -1089,39 +1085,6 @@ impl CollabPanel { .tooltip(Tooltip::text("Open Channel Notes")) } - fn render_channel_chat( - &self, - channel_id: ChannelId, - is_selected: bool, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let channel_store = self.channel_store.read(cx); - let has_messages_notification = channel_store.has_new_messages(channel_id); - ListItem::new("channel-chat") - .toggle_state(is_selected) - .on_click(cx.listener(move |this, _, window, cx| { - this.join_channel_chat(channel_id, window, cx); - })) - .start_slot( - h_flex() - .relative() - .gap_1() - .child(render_tree_branch(false, false, window, cx)) - .child(IconButton::new(0, IconName::Chat)) - .children(has_messages_notification.then(|| { - div() - .w_1p5() - .absolute() - .right(px(2.)) - .top(px(4.)) - .child(Indicator::dot().color(Color::Info)) - })), - ) - .child(Label::new("chat")) - .tooltip(Tooltip::text("Open Chat")) - } - fn has_subchannels(&self, ix: usize) -> bool { self.entries.get(ix).is_some_and(|entry| { if let ListEntry::Channel { has_children, .. } = entry { @@ -1296,13 +1259,6 @@ impl CollabPanel { this.open_channel_notes(channel_id, window, cx) }), ) - .entry( - "Open Chat", - None, - window.handler_for(&this, move |this, window, cx| { - this.join_channel_chat(channel_id, window, cx) - }), - ) .entry( "Copy Channel Link", None, @@ -1632,9 +1588,6 @@ impl CollabPanel { ListEntry::ChannelNotes { channel_id } => { self.open_channel_notes(*channel_id, window, cx) } - ListEntry::ChannelChat { channel_id } => { - self.join_channel_chat(*channel_id, window, cx) - } ListEntry::OutgoingRequest(_) => {} ListEntry::ChannelEditor { .. } => {} } @@ -2258,28 +2211,6 @@ impl CollabPanel { .detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None) } - fn join_channel_chat( - &mut self, - channel_id: ChannelId, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = workspace.focus_panel::(window, cx) { - panel.update(cx, |panel, cx| { - panel - .select_channel(channel_id, None, cx) - .detach_and_notify_err(window, cx); - }); - } - }); - }); - } - fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut Context) { let channel_store = self.channel_store.read(cx); let Some(channel) = channel_store.channel_for_id(channel_id) else { @@ -2398,9 +2329,6 @@ impl CollabPanel { ListEntry::ChannelNotes { channel_id } => self .render_channel_notes(*channel_id, is_selected, window, cx) .into_any_element(), - ListEntry::ChannelChat { channel_id } => self - .render_channel_chat(*channel_id, is_selected, window, cx) - .into_any_element(), } } @@ -2781,7 +2709,6 @@ impl CollabPanel { let disclosed = has_children.then(|| self.collapsed_channels.binary_search(&channel.id).is_err()); - let has_messages_notification = channel_store.has_new_messages(channel_id); let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id); const FACEPILE_LIMIT: usize = 3; @@ -2909,21 +2836,6 @@ impl CollabPanel { .rounded_l_sm() .gap_1() .px_1() - .child( - IconButton::new("channel_chat", IconName::Chat) - .style(ButtonStyle::Filled) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(if has_messages_notification { - Color::Default - } else { - Color::Muted - }) - .on_click(cx.listener(move |this, _, window, cx| { - this.join_channel_chat(channel_id, window, cx) - })) - .tooltip(Tooltip::text("Open channel chat")), - ) .child( IconButton::new("channel_notes", IconName::Reader) .style(ButtonStyle::Filled) @@ -3183,14 +3095,6 @@ impl PartialEq for ListEntry { return channel_id == other_id; } } - ListEntry::ChannelChat { channel_id } => { - if let ListEntry::ChannelChat { - channel_id: other_id, - } = other - { - return channel_id == other_id; - } - } ListEntry::ChannelInvite(channel_1) => { if let ListEntry::ChannelInvite(channel_2) = other { return channel_1.id == channel_2.id; diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index b369d324adb617907d80b773e0982c1723b1bae6..f75dd663c838c84f167b3070b50a4e1f44e9aa2d 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,5 +1,4 @@ pub mod channel_view; -pub mod chat_panel; pub mod collab_panel; pub mod notification_panel; pub mod notifications; @@ -13,9 +12,7 @@ use gpui::{ WindowDecorations, WindowKind, WindowOptions, point, }; use panel_settings::MessageEditorSettings; -pub use panel_settings::{ - ChatPanelButton, ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings, -}; +pub use panel_settings::{CollaborationPanelSettings, NotificationPanelSettings}; use release_channel::ReleaseChannel; use settings::Settings; use ui::px; @@ -23,12 +20,10 @@ use workspace::AppState; pub fn init(app_state: &Arc, cx: &mut App) { CollaborationPanelSettings::register(cx); - ChatPanelSettings::register(cx); NotificationPanelSettings::register(cx); MessageEditorSettings::register(cx); channel_view::init(cx); - chat_panel::init(cx); collab_panel::init(cx); notification_panel::init(cx); notifications::init(app_state, cx); diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index bf6fc3b224c54cd1512011987f73623c58d33c32..9731b89521e29ebda21ad5ce2cfca6e0531ae437 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -1,4 +1,4 @@ -use crate::{NotificationPanelSettings, chat_panel::ChatPanel}; +use crate::NotificationPanelSettings; use anyhow::Result; use channel::ChannelStore; use client::{ChannelId, Client, Notification, User, UserStore}; @@ -6,8 +6,8 @@ use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::StreamExt; use gpui::{ - AnyElement, App, AsyncWindowContext, ClickEvent, Context, CursorStyle, DismissEvent, Element, - Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, + AnyElement, App, AsyncWindowContext, ClickEvent, Context, DismissEvent, Element, Entity, + EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement, Render, StatefulInteractiveElement, Styled, Task, WeakEntity, Window, actions, div, img, list, px, }; @@ -71,7 +71,6 @@ pub struct NotificationPresenter { pub text: String, pub icon: &'static str, pub needs_response: bool, - pub can_navigate: bool, } actions!( @@ -234,7 +233,6 @@ impl NotificationPanel { actor, text, needs_response, - can_navigate, .. } = self.present_notification(entry, cx)?; @@ -269,14 +267,6 @@ impl NotificationPanel { .py_1() .gap_2() .hover(|style| style.bg(cx.theme().colors().element_hover)) - .when(can_navigate, |el| { - el.cursor(CursorStyle::PointingHand).on_click({ - let notification = notification.clone(); - cx.listener(move |this, _, window, cx| { - this.did_click_notification(¬ification, window, cx) - }) - }) - }) .children(actor.map(|actor| { img(actor.avatar_uri.clone()) .flex_none() @@ -369,7 +359,6 @@ impl NotificationPanel { text: format!("{} wants to add you as a contact", requester.github_login), needs_response: user_store.has_incoming_contact_request(requester.id), actor: Some(requester), - can_navigate: false, }) } Notification::ContactRequestAccepted { responder_id } => { @@ -379,7 +368,6 @@ impl NotificationPanel { text: format!("{} accepted your contact invite", responder.github_login), needs_response: false, actor: Some(responder), - can_navigate: false, }) } Notification::ChannelInvitation { @@ -396,29 +384,6 @@ impl NotificationPanel { ), needs_response: channel_store.has_channel_invitation(ChannelId(channel_id)), actor: Some(inviter), - can_navigate: false, - }) - } - Notification::ChannelMessageMention { - sender_id, - channel_id, - message_id, - } => { - let sender = user_store.get_cached_user(sender_id)?; - let channel = channel_store.channel_for_id(ChannelId(channel_id))?; - let message = self - .notification_store - .read(cx) - .channel_message_for_id(message_id)?; - Some(NotificationPresenter { - icon: "icons/conversations.svg", - text: format!( - "{} mentioned you in #{}:\n{}", - sender.github_login, channel.name, message.body, - ), - needs_response: false, - actor: Some(sender), - can_navigate: true, }) } } @@ -433,9 +398,7 @@ impl NotificationPanel { ) { let should_mark_as_read = match notification { Notification::ContactRequestAccepted { .. } => true, - Notification::ContactRequest { .. } - | Notification::ChannelInvitation { .. } - | Notification::ChannelMessageMention { .. } => false, + Notification::ContactRequest { .. } | Notification::ChannelInvitation { .. } => false, }; if should_mark_as_read { @@ -457,55 +420,6 @@ impl NotificationPanel { } } - fn did_click_notification( - &mut self, - notification: &Notification, - window: &mut Window, - cx: &mut Context, - ) { - if let Notification::ChannelMessageMention { - message_id, - channel_id, - .. - } = notification.clone() - && let Some(workspace) = self.workspace.upgrade() - { - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = workspace.focus_panel::(window, cx) { - panel.update(cx, |panel, cx| { - panel - .select_channel(ChannelId(channel_id), Some(message_id), cx) - .detach_and_log_err(cx); - }); - } - }); - }); - } - } - - fn is_showing_notification(&self, notification: &Notification, cx: &mut Context) -> bool { - if !self.active { - return false; - } - - if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification - && let Some(workspace) = self.workspace.upgrade() - { - return if let Some(panel) = workspace.read(cx).panel::(cx) { - let panel = panel.read(cx); - panel.is_scrolled_to_bottom() - && panel - .active_chat() - .is_some_and(|chat| chat.read(cx).channel_id.0 == *channel_id) - } else { - false - }; - } - - false - } - fn on_notification_event( &mut self, _: &Entity, @@ -515,9 +429,7 @@ impl NotificationPanel { ) { match event { NotificationEvent::NewNotification { entry } => { - if !self.is_showing_notification(&entry.notification, cx) { - self.unseen_notifications.push(entry.clone()); - } + self.unseen_notifications.push(entry.clone()); self.add_toast(entry, window, cx); } NotificationEvent::NotificationRemoved { entry } @@ -541,10 +453,6 @@ impl NotificationPanel { window: &mut Window, cx: &mut Context, ) { - if self.is_showing_notification(&entry.notification, cx) { - return; - } - let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx) else { return; @@ -568,7 +476,6 @@ impl NotificationPanel { workspace.show_notification(id, cx, |cx| { let workspace = cx.entity().downgrade(); cx.new(|cx| NotificationToast { - notification_id, actor, text, workspace, @@ -781,7 +688,6 @@ impl Panel for NotificationPanel { } pub struct NotificationToast { - notification_id: u64, actor: Option>, text: String, workspace: WeakEntity, @@ -799,22 +705,10 @@ impl WorkspaceNotification for NotificationToast {} impl NotificationToast { fn focus_notification_panel(&self, window: &mut Window, cx: &mut Context) { let workspace = self.workspace.clone(); - let notification_id = self.notification_id; window.defer(cx, move |window, cx| { workspace .update(cx, |workspace, cx| { - if let Some(panel) = workspace.focus_panel::(window, cx) { - panel.update(cx, |panel, cx| { - let store = panel.notification_store.read(cx); - if let Some(entry) = store.notification_for_id(notification_id) { - panel.did_click_notification( - &entry.clone().notification, - window, - cx, - ); - } - }); - } + workspace.focus_panel::(window, cx) }) .ok(); }) diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index 81d441167c06ef75c7e251dffefc55ff099a48e8..98559ffd34006bf2f65427a899fd1fe5d41a4d11 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -11,39 +11,6 @@ pub struct CollaborationPanelSettings { pub default_width: Pixels, } -#[derive(Clone, Copy, Default, Serialize, Deserialize, JsonSchema, Debug)] -#[serde(rename_all = "snake_case")] -pub enum ChatPanelButton { - Never, - Always, - #[default] - WhenInCall, -} - -#[derive(Deserialize, Debug)] -pub struct ChatPanelSettings { - pub button: ChatPanelButton, - pub dock: DockPosition, - pub default_width: Pixels, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[settings_key(key = "chat_panel")] -pub struct ChatPanelSettingsContent { - /// When to show the panel button in the status bar. - /// - /// Default: only when in a call - pub button: Option, - /// Where to dock the panel. - /// - /// Default: right - pub dock: Option, - /// Default width of the panel in pixels. - /// - /// Default: 240 - pub default_width: Option, -} - #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] #[settings_key(key = "collaboration_panel")] pub struct PanelSettingsContent { @@ -108,19 +75,6 @@ impl Settings for CollaborationPanelSettings { fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } -impl Settings for ChatPanelSettings { - type FileContent = ChatPanelSettingsContent; - - fn load( - sources: SettingsSources, - _: &mut gpui::App, - ) -> anyhow::Result { - sources.json_merge() - } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} -} - impl Settings for NotificationPanelSettings { type FileContent = NotificationPanelSettingsContent; diff --git a/crates/notifications/Cargo.toml b/crates/notifications/Cargo.toml index baf5444ef4903dd1d0efc64e7553abe3ed414720..39acfe2b384c8a2264c5c2dac91024edad89d33a 100644 --- a/crates/notifications/Cargo.toml +++ b/crates/notifications/Cargo.toml @@ -24,7 +24,6 @@ test-support = [ anyhow.workspace = true channel.workspace = true client.workspace = true -collections.workspace = true component.workspace = true db.workspace = true gpui.workspace = true diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index af2601bd181089a4952529ab4f315aa148e25121..7db17da9ff92bce492cc8414be8db28c219d61e7 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -1,7 +1,6 @@ use anyhow::{Context as _, Result}; -use channel::{ChannelMessage, ChannelMessageId, ChannelStore}; +use channel::ChannelStore; use client::{ChannelId, Client, UserStore}; -use collections::HashMap; use db::smol::stream::StreamExt; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task}; use rpc::{Notification, TypedEnvelope, proto}; @@ -22,7 +21,6 @@ impl Global for GlobalNotificationStore {} pub struct NotificationStore { client: Arc, user_store: Entity, - channel_messages: HashMap, channel_store: Entity, notifications: SumTree, loaded_all_notifications: bool, @@ -100,12 +98,10 @@ impl NotificationStore { channel_store: ChannelStore::global(cx), notifications: Default::default(), loaded_all_notifications: false, - channel_messages: Default::default(), _watch_connection_status: watch_connection_status, _subscriptions: vec![ client.add_message_handler(cx.weak_entity(), Self::handle_new_notification), client.add_message_handler(cx.weak_entity(), Self::handle_delete_notification), - client.add_message_handler(cx.weak_entity(), Self::handle_update_notification), ], user_store, client, @@ -120,10 +116,6 @@ impl NotificationStore { self.notifications.summary().unread_count } - pub fn channel_message_for_id(&self, id: u64) -> Option<&ChannelMessage> { - self.channel_messages.get(&id) - } - // Get the nth newest notification. pub fn notification_at(&self, ix: usize) -> Option<&NotificationEntry> { let count = self.notifications.summary().count; @@ -185,7 +177,6 @@ impl NotificationStore { fn handle_connect(&mut self, cx: &mut Context) -> Option>> { self.notifications = Default::default(); - self.channel_messages = Default::default(); cx.notify(); self.load_more_notifications(true, cx) } @@ -223,35 +214,6 @@ impl NotificationStore { })? } - async fn handle_update_notification( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - if let Some(notification) = envelope.payload.notification - && let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) = - Notification::from_proto(¬ification) - { - let fetch_message_task = this.channel_store.update(cx, |this, cx| { - this.fetch_channel_messages(vec![message_id], cx) - }); - - cx.spawn(async move |this, cx| { - let messages = fetch_message_task.await?; - this.update(cx, move |this, cx| { - for message in messages { - this.channel_messages.insert(message_id, message); - } - cx.notify(); - }) - }) - .detach_and_log_err(cx) - } - Ok(()) - })? - } - async fn add_notifications( this: Entity, notifications: Vec, @@ -259,7 +221,6 @@ impl NotificationStore { cx: &mut AsyncApp, ) -> Result<()> { let mut user_ids = Vec::new(); - let mut message_ids = Vec::new(); let notifications = notifications .into_iter() @@ -293,29 +254,14 @@ impl NotificationStore { } => { user_ids.push(contact_id); } - Notification::ChannelMessageMention { - sender_id, - message_id, - .. - } => { - user_ids.push(sender_id); - message_ids.push(message_id); - } } } - let (user_store, channel_store) = this.read_with(cx, |this, _| { - (this.user_store.clone(), this.channel_store.clone()) - })?; + let user_store = this.read_with(cx, |this, _| this.user_store.clone())?; user_store .update(cx, |store, cx| store.get_users(user_ids, cx))? .await?; - let messages = channel_store - .update(cx, |store, cx| { - store.fetch_channel_messages(message_ids, cx) - })? - .await?; this.update(cx, |this, cx| { if options.clear_old { cx.emit(NotificationEvent::NotificationsUpdated { @@ -323,7 +269,6 @@ impl NotificationStore { new_count: 0, }); this.notifications = SumTree::default(); - this.channel_messages.clear(); this.loaded_all_notifications = false; } @@ -331,15 +276,6 @@ impl NotificationStore { this.loaded_all_notifications = true; } - this.channel_messages - .extend(messages.into_iter().filter_map(|message| { - if let ChannelMessageId::Saved(id) = message.id { - Some((id, message)) - } else { - None - } - })); - this.splice_notifications( notifications .into_iter() diff --git a/crates/proto/proto/channel.proto b/crates/proto/proto/channel.proto index 324380048a4b649257b4cb2511612abf0fdd9f96..cada21cd5b7ede4730f2f4e71e98fb9a3dc12ff0 100644 --- a/crates/proto/proto/channel.proto +++ b/crates/proto/proto/channel.proto @@ -23,16 +23,17 @@ message UpdateChannels { repeated Channel channel_invitations = 5; repeated uint64 remove_channel_invitations = 6; repeated ChannelParticipants channel_participants = 7; - repeated ChannelMessageId latest_channel_message_ids = 8; repeated ChannelBufferVersion latest_channel_buffer_versions = 9; + reserved 8; reserved 10 to 15; } message UpdateUserChannels { - repeated ChannelMessageId observed_channel_message_id = 1; repeated ChannelBufferVersion observed_channel_buffer_version = 2; repeated ChannelMembership channel_memberships = 3; + + reserved 1; } message ChannelMembership { diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index 338ef33c8abf7bc694a07ecacfbdb94711a6924b..50364c738798d21e8f66e37086bcaf309bdedfb0 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -32,12 +32,6 @@ pub enum Notification { channel_name: String, inviter_id: u64, }, - ChannelMessageMention { - #[serde(rename = "entity_id")] - message_id: u64, - sender_id: u64, - channel_id: u64, - }, } impl Notification { @@ -91,11 +85,6 @@ mod tests { channel_name: "the-channel".into(), inviter_id: 50, }, - Notification::ChannelMessageMention { - sender_id: 200, - channel_id: 30, - message_id: 1, - }, ] { let message = notification.to_proto(); let deserialized = Notification::from_proto(&message).unwrap(); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index eda483988b4e8a01affa9c85d0cad7657def61eb..576fe5f634e04c8f4c5ac3dc9ce2ad206d169abb 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1268,7 +1268,6 @@ fn generate_commands(_: &App) -> Vec { VimCommand::str(("te", "rm"), "terminal_panel::Toggle"), VimCommand::str(("T", "erm"), "terminal_panel::Toggle"), VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"), - VimCommand::str(("Ch", "at"), "chat_panel::ToggleFocus"), VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"), VimCommand::str(("A", "I"), "agent::ToggleFocus"), VimCommand::str(("G", "it"), "git_panel::ToggleFocus"), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 355925d2289daac0c6346b6b92d45942c4724357..e14cbb10e85883231c1fd165d7c4f9d9beca155b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -553,8 +553,6 @@ fn initialize_panels( let git_panel = GitPanel::load(workspace_handle.clone(), cx.clone()); let channels_panel = collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); - let chat_panel = - collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); let notification_panel = collab_ui::notification_panel::NotificationPanel::load( workspace_handle.clone(), cx.clone(), @@ -567,7 +565,6 @@ fn initialize_panels( terminal_panel, git_panel, channels_panel, - chat_panel, notification_panel, debug_panel, ) = futures::try_join!( @@ -576,7 +573,6 @@ fn initialize_panels( git_panel, terminal_panel, channels_panel, - chat_panel, notification_panel, debug_panel, )?; @@ -587,7 +583,6 @@ fn initialize_panels( workspace.add_panel(terminal_panel, window, cx); workspace.add_panel(git_panel, window, cx); workspace.add_panel(channels_panel, window, cx); - workspace.add_panel(chat_panel, window, cx); workspace.add_panel(notification_panel, window, cx); workspace.add_panel(debug_panel, window, cx); })?; @@ -865,14 +860,6 @@ fn register_actions( workspace.toggle_panel_focus::(window, cx); }, ) - .register_action( - |workspace: &mut Workspace, - _: &collab_ui::chat_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::(window, cx); - }, - ) .register_action( |workspace: &mut Workspace, _: &collab_ui::notification_panel::ToggleFocus, @@ -4475,7 +4462,6 @@ mod tests { "branches", "buffer_search", "channel_modal", - "chat_panel", "cli", "client", "collab", diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 56b4de832862439b93d8a0359dbf8284226e1671..dd6a74d1d0b97c210d9d0dd494a3c746a7a03e20 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -4395,28 +4395,6 @@ Visit [the Configuration page](./ai/configuration.md) under the AI section to le - `dock`: Where to dock the collaboration panel. Can be `left` or `right` - `default_width`: Default width of the collaboration panel -## Chat Panel - -- Description: Customizations for the chat panel. -- Setting: `chat_panel` -- Default: - -```json -{ - "chat_panel": { - "button": "when_in_call", - "dock": "right", - "default_width": 240 - } -} -``` - -**Options** - -- `button`: When to show the chat panel button in the status bar. Can be `never`, `always`, or `when_in_call`. -- `dock`: Where to dock the chat panel. Can be 'left' or 'right' -- `default_width`: Default width of the chat panel - ## Debugger - Description: Configuration for debugger panel and settings diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 47c72e80f5ea0ca6ce8576e29c51ff9e44041eb5..073911fd60d441c38c361144e033591b3eed433a 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -94,7 +94,6 @@ To disable this behavior use: // "project_panel": {"button": false }, // "outline_panel": {"button": false }, // "collaboration_panel": {"button": false }, - // "chat_panel": {"button": "never" }, // "git_panel": {"button": false }, // "notification_panel": {"button": false }, // "agent": {"button": false }, @@ -554,13 +553,6 @@ See [Terminal settings](./configuring-zed.md#terminal) for additional non-visual }, "show_call_status_icon": true, // Shown call status in the OS status bar. - // Chat Panel - "chat_panel": { - "button": "when_in_call", // status bar icon (true, false, when_in_call) - "dock": "right", // Where to dock: left, right - "default_width": 240 // Default width of the chat panel - }, - // Notification Panel "notification_panel": { // Whether to show the notification panel button in the status bar.