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.