Remove Chat (#37789)

Conrad Irwin created

At RustConf we were demo'ing zed, and it continually popped open the
chat panel.

We're usually inured to this because the Chat panel doesn't open unless
a Guest
is in the channel, but it made me sad that we were showing a long stream
of
vacuous comments and unresponded to questions on every demo screen.

We may bring chat back in the future, but we need more thought on the
UX, and
we need to rebuild the backend to not use the existing collab server
that we're
trying to move off of.

Release Notes:

- Removed the chat feature from Zed (Sorry to the 5 of you who use this
on the regular!)

Change summary

Cargo.lock                                        |    5 
assets/settings/default.json                      |   10 
crates/auto_update_ui/src/auto_update_ui.rs       |    3 
crates/channel/Cargo.toml                         |    2 
crates/channel/src/channel.rs                     |    6 
crates/channel/src/channel_chat.rs                |  861 ----------
crates/channel/src/channel_store.rs               |  149 -
crates/channel/src/channel_store_tests.rs         |  201 --
crates/collab/src/db.rs                           |    3 
crates/collab/src/db/queries.rs                   |    1 
crates/collab/src/db/queries/channels.rs          |    8 
crates/collab/src/db/queries/messages.rs          |  725 --------
crates/collab/src/db/queries/rooms.rs             |    1 
crates/collab/src/db/tests.rs                     |   11 
crates/collab/src/db/tests/channel_tests.rs       |   37 
crates/collab/src/db/tests/message_tests.rs       |  421 -----
crates/collab/src/rpc.rs                          |  318 ---
crates/collab/src/tests.rs                        |    1 
crates/collab/src/tests/channel_message_tests.rs  |  725 --------
crates/collab_ui/Cargo.toml                       |    3 
crates/collab_ui/src/chat_panel.rs                | 1380 -----------------
crates/collab_ui/src/chat_panel/message_editor.rs |  548 ------
crates/collab_ui/src/collab_panel.rs              |  100 -
crates/collab_ui/src/collab_ui.rs                 |    7 
crates/collab_ui/src/notification_panel.rs        |  118 -
crates/collab_ui/src/panel_settings.rs            |   46 
crates/notifications/Cargo.toml                   |    1 
crates/notifications/src/notification_store.rs    |   68 
crates/proto/proto/channel.proto                  |    5 
crates/rpc/src/notification.rs                    |   11 
crates/vim/src/command.rs                         |    1 
crates/zed/src/zed.rs                             |   14 
docs/src/configuring-zed.md                       |   22 
docs/src/visual-customization.md                  |    8 
34 files changed, 54 insertions(+), 5,766 deletions(-)

Detailed changes

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",

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,

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?;

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

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<Client>, user_store: Entity<UserStore>, cx: &mut App) {
     channel_store::init(client, user_store, cx);
     channel_buffer::init(&client.clone().into());
-    channel_chat::init(&client.clone().into());
 }

crates/channel/src/channel_chat.rs 🔗

@@ -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<ChannelMessage>,
-    acknowledged_message_ids: HashSet<u64>,
-    channel_store: Entity<ChannelStore>,
-    loaded_all_messages: bool,
-    last_acknowledged_id: Option<u64>,
-    next_pending_message_id: usize,
-    first_loaded_message_id: Option<u64>,
-    user_store: Entity<UserStore>,
-    rpc: Arc<Client>,
-    outgoing_messages_lock: Arc<Mutex<()>>,
-    rng: StdRng,
-    _subscription: Subscription,
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub struct MessageParams {
-    pub text: String,
-    pub mentions: Vec<(Range<usize>, UserId)>,
-    pub reply_to_message_id: Option<u64>,
-}
-
-#[derive(Clone, Debug)]
-pub struct ChannelMessage {
-    pub id: ChannelMessageId,
-    pub body: String,
-    pub timestamp: OffsetDateTime,
-    pub sender: Arc<User>,
-    pub nonce: u128,
-    pub mentions: Vec<(Range<usize>, UserId)>,
-    pub reply_to_message_id: Option<u64>,
-    pub edited_at: Option<OffsetDateTime>,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum ChannelMessageId {
-    Saved(u64),
-    Pending(usize),
-}
-
-impl From<ChannelMessageId> for Option<u64> {
-    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<usize>,
-        new_count: usize,
-    },
-    UpdateMessage {
-        message_id: ChannelMessageId,
-        message_ix: usize,
-    },
-    NewMessage {
-        channel_id: ChannelId,
-        message_id: u64,
-    },
-}
-
-impl EventEmitter<ChannelChatEvent> 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>,
-        channel_store: Entity<ChannelStore>,
-        user_store: Entity<UserStore>,
-        client: Arc<Client>,
-        cx: &mut AsyncApp,
-    ) -> Result<Entity<Self>> {
-        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<Arc<Channel>> {
-        self.channel_store
-            .read(cx)
-            .channel_for_id(self.channel_id)
-            .cloned()
-    }
-
-    pub fn client(&self) -> &Arc<Client> {
-        &self.rpc
-    }
-
-    pub fn send_message(
-        &mut self,
-        message: MessageParams,
-        cx: &mut Context<Self>,
-    ) -> Result<Task<Result<u64>>> {
-        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<Self>) -> Task<Result<()>> {
-        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<Self>,
-    ) -> Result<Task<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<Self>) -> Option<Task<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<u64> {
-        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<Self>,
-        message_id: u64,
-        mut cx: AsyncApp,
-    ) -> Option<usize> {
-        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::<Dimensions<ChannelMessageId, Count>>(&());
-                        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<Self>) {
-        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<Self>,
-        user_store: Entity<UserStore>,
-        rpc: Arc<Client>,
-        proto_messages: Vec<proto::ChannelMessage>,
-        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<u64> = 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::<Vec<_>>();
-
-        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<Self>) {
-        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::<Vec<_>>()
-                })?;
-
-                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<ChannelMessage> {
-        &self.messages
-    }
-
-    pub fn message(&self, ix: usize) -> &ChannelMessage {
-        let mut cursor = self.messages.cursor::<Count>(&());
-        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<usize>) -> impl Iterator<Item = &ChannelMessage> {
-        let mut cursor = self.messages.cursor::<Count>(&());
-        cursor.seek(&Count(range.start), Bias::Right);
-        cursor.take(range.len())
-    }
-
-    pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
-        let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
-        cursor.seek(&ChannelMessageId::Pending(0), Bias::Left);
-        cursor
-    }
-
-    async fn handle_message_sent(
-        this: Entity<Self>,
-        message: TypedEnvelope<proto::ChannelMessageSent>,
-        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<Self>,
-        message: TypedEnvelope<proto::RemoveChannelMessage>,
-        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<Self>,
-        message: TypedEnvelope<proto::ChannelMessageUpdate>,
-        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<ChannelMessage>, cx: &mut Context<Self>) {
-        if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
-            let nonces = messages
-                .cursor::<()>(&())
-                .map(|m| m.nonce)
-                .collect::<HashSet<_>>();
-
-            let mut old_cursor = self
-                .messages
-                .cursor::<Dimensions<ChannelMessageId, Count>>(&());
-            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::<Range<usize>>::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<Self>) {
-        let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
-        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<usize>, u64)>,
-        edited_at: Option<OffsetDateTime>,
-        cx: &mut Context<Self>,
-    ) {
-        let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
-        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<proto::ChannelMessage>,
-    user_store: &Entity<UserStore>,
-    cx: &mut AsyncApp,
-) -> Result<SumTree<ChannelMessage>> {
-    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<UserStore>,
-        cx: &mut AsyncApp,
-    ) -> Result<Self> {
-        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<OffsetDateTime> {
-            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<proto::ChannelMessage>,
-        user_store: &Entity<UserStore>,
-        cx: &mut AsyncApp,
-    ) -> Result<Vec<Self>> {
-        let unique_user_ids = proto_messages
-            .iter()
-            .map(|m| m.sender_id)
-            .collect::<HashSet<_>>()
-            .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<usize>, UserId)]) -> Vec<proto::ChatMention> {
-    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,
-        }
-    }
-}

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<proto::UpdateChannels>,
     opened_buffers: HashMap<ChannelId, OpenEntityHandle<ChannelBuffer>>,
-    opened_chats: HashMap<ChannelId, OpenEntityHandle<ChannelChat>>,
     client: Arc<Client>,
     did_subscribe: bool,
     channels_loaded: (watch::Sender<bool>, watch::Receiver<bool>),
@@ -63,10 +62,8 @@ pub struct Channel {
 
 #[derive(Default, Debug)]
 pub struct ChannelState {
-    latest_chat_message: Option<u64>,
     latest_notes_version: NotesVersion,
     observed_notes_version: NotesVersion,
-    observed_chat_message: Option<u64>,
     role: Option<ChannelRole>,
 }
 
@@ -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<u64>,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<ChannelMessage>>> {
-        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<u64>) {
-        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<u64> {
-        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>,
-    ) {
-        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>,
-    ) {
-        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<Self>,
-    ) -> Task<Result<Entity<ChannelChat>>> {
-        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<u64> {
-        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);

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::<proto::JoinChannelChat>().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::<proto::GetUsers>().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::<Vec<_>>(),
-            &[
-                ("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::<proto::GetUsers>().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::<Vec<_>>(),
-            &[("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::<proto::GetChannelMessages>().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::<Vec<_>>(),
-            &[
-                ("user-5".into(), "y".into()),
-                ("maxbrunsfeld".into(), "z".into())
-            ]
-        );
-    });
-}
-
 fn init_test(cx: &mut App) -> Entity<ChannelStore> {
     let settings_store = SettingsStore::test(cx);
     cx.set_global(settings_store);

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<Channel>,
 
     pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
-    pub observed_channel_messages: Vec<proto::ChannelMessageId>,
     pub latest_buffer_versions: Vec<proto::ChannelBufferVersion>,
-    pub latest_channel_messages: Vec<proto::ChannelMessageId>,
 }
 
 #[derive(Debug)]

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;

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

crates/collab/src/db/queries/messages.rs 🔗

@@ -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<MessageId>,
-    ) -> Result<Vec<proto::ChannelMessage>> {
-        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<Vec<proto::ChannelMessage>> {
-        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::<ChannelId, channel::Model>::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<channel_message::Model>,
-        tx: &DatabaseTransaction,
-    ) -> Result<Vec<proto::ChannelMessage>> {
-        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::<Vec<_>>();
-        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<Vec<tables::channel_message_mention::ActiveModel>> {
-        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::<Vec<_>>())
-    }
-
-    /// 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<MessageId>,
-    ) -> Result<CreatedChannelMessage> {
-        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::<HashSet<_>>();
-
-                    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<NotificationBatch> {
-        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<Vec<proto::ChannelMessageId>> {
-        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<Vec<proto::ChannelMessageId>> {
-        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<i32> {
-        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<ConnectionId>, Vec<NotificationId>)> {
-        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<UpdatedChannelMessage> {
-        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::<HashSet<_>>();
-            // 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(&notification.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::<Vec<_>>(),
-                updated_mention_notifications,
-            })
-        })
-        .await
-    }
-}

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

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

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<Database>) {
     )
 }
 
-test_both_dbs!(
-    test_guest_access,
-    test_guest_access_postgres,
-    test_guest_access_sqlite
-);
-
-async fn test_guest_access(db: &Arc<Database>) {
-    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<Channel>, expected: &[(ChannelId, &[ChannelId])]) {
     let actual = actual

crates/collab/src/db/tests/message_tests.rs 🔗

@@ -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<Database>) {
-    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::<Vec<_>>();
-    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::<Vec<_>>();
-    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<Database>) {
-    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::<Vec<_>>();
-    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<Database>) {
-    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<Database>) {
-    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::<Vec<_>>();
-    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())]),
-            ),
-        ]
-    );
-}

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<proto::SendChannelMessage>,
-    session: MessageContext,
+    _request: proto::SendChannelMessage,
+    _response: Response<proto::SendChannelMessage>,
+    _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<proto::RemoveChannelMessage>,
-    session: MessageContext,
+    _request: proto::RemoveChannelMessage,
+    _response: Response<proto::RemoveChannelMessage>,
+    _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<proto::UpdateChannelMessage>,
-    session: MessageContext,
+    _request: proto::UpdateChannelMessage,
+    _response: Response<proto::UpdateChannelMessage>,
+    _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<proto::JoinChannelChat>,
-    session: MessageContext,
+    _request: proto::JoinChannelChat,
+    _response: Response<proto::JoinChannelChat>,
+    _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<proto::GetChannelMessages>,
-    session: MessageContext,
+    _request: proto::GetChannelMessages,
+    _response: Response<proto::GetChannelMessages>,
+    _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<proto::GetChannelMessagesById>,
-    session: MessageContext,
+    _request: proto::GetChannelMessagesById,
+    _response: Response<proto::GetChannelMessagesById>,
+    _session: MessageContext,
 ) -> Result<()> {
-    let message_ids = request
-        .message_ids
-        .iter()
-        .map(|id| MessageId::from_proto(*id))
-        .collect::<Vec<_>>();
-    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

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;

crates/collab/src/tests/channel_message_tests.rs 🔗

@@ -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<_>>(),
-                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<ChannelChat>, messages: &[&str], cx: &mut TestAppContext) {
-    assert_eq!(
-        chat.read_with(cx, |chat, _| {
-            chat.messages()
-                .iter()
-                .map(|m| m.body.clone())
-                .collect::<Vec<_>>()
-        }),
-        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);
-    });
-}

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

crates/collab_ui/src/chat_panel.rs 🔗

@@ -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::<ChatPanel>(window, cx);
-        });
-    })
-    .detach();
-}
-
-pub struct ChatPanel {
-    client: Arc<Client>,
-    channel_store: Entity<ChannelStore>,
-    languages: Arc<LanguageRegistry>,
-    message_list: ListState,
-    active_chat: Option<(Entity<ChannelChat>, Subscription)>,
-    message_editor: Entity<MessageEditor>,
-    local_timezone: UtcOffset,
-    fs: Arc<dyn Fs>,
-    width: Option<Pixels>,
-    active: bool,
-    pending_serialization: Task<Option<()>>,
-    subscriptions: Vec<gpui::Subscription>,
-    is_scrolled_to_bottom: bool,
-    markdown_data: HashMap<ChannelMessageId, RichText>,
-    focus_handle: FocusHandle,
-    open_context_menu: Option<(u64, Subscription)>,
-    highlighted_message: Option<(u64, Task<()>)>,
-    last_acknowledged_message_id: Option<u64>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedChatPanel {
-    width: Option<Pixels>,
-}
-
-actions!(
-    chat_panel,
-    [
-        /// Toggles focus on the chat panel.
-        ToggleFocus
-    ]
-);
-
-impl ChatPanel {
-    pub fn new(
-        workspace: &mut Workspace,
-        window: &mut Window,
-        cx: &mut Context<Workspace>,
-    ) -> Entity<Self> {
-        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<ChannelId> {
-        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<Entity<ChannelChat>> {
-        self.active_chat.as_ref().map(|(chat, _)| chat.clone())
-    }
-
-    pub fn load(
-        workspace: WeakEntity<Workspace>,
-        cx: AsyncWindowContext,
-    ) -> Task<Result<Entity<Self>>> {
-        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::<SerializedChatPanel>(&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<Self>) {
-        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<ChannelChat>, cx: &mut Context<Self>) {
-        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<ChannelChat>,
-        event: &ChannelChatEvent,
-        cx: &mut Context<Self>,
-    ) {
-        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<Self>) {
-        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<ChannelMessageId>,
-        reply_to_message: &Option<ChannelMessage>,
-        cx: &mut Context<Self>,
-    ) -> 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<Self>,
-    ) -> 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<u64>) -> bool {
-        match self.open_context_menu.as_ref() {
-            Some((id, _)) => Some(*id) == message_id,
-            None => false,
-        }
-    }
-
-    fn render_popover_button(&self, cx: &mut Context<Self>, child: Stateful<Div>) -> 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<u64>,
-        can_delete_message: bool,
-        can_edit_message: bool,
-        cx: &mut Context<Self>,
-    ) -> 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<Self>,
-        message_id: u64,
-        can_delete_message: bool,
-        window: &mut Window,
-        cx: &mut App,
-    ) -> Entity<ContextMenu> {
-        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<LanguageRegistry>,
-        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::<Vec<_>>();
-
-        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<Self>) {
-        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<Self>) {
-        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<Self>) {
-        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<u64>,
-        cx: &mut Context<ChatPanel>,
-    ) -> Task<Result<()>> {
-        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>) {
-        self.message_editor
-            .update(cx, |editor, _| editor.clear_reply_to_message_id());
-    }
-
-    fn cancel_edit_message(&mut self, cx: &mut Context<Self>) {
-        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<Self>) -> 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<Self>) {
-        settings::update_settings_file::<ChatPanelSettings>(
-            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<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
-        self.width = size;
-        self.serialize(cx);
-        cx.notify();
-    }
-
-    fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut Context<Self>) {
-        self.active = active;
-        if active {
-            self.acknowledge_last_message(cx);
-        }
-    }
-
-    fn persistent_name() -> &'static str {
-        "ChatPanel"
-    }
-
-    fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {
-        self.enabled(cx).then(|| ui::IconName::Chat)
-    }
-
-    fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> {
-        Some("Chat Panel")
-    }
-
-    fn toggle_action(&self) -> Box<dyn gpui::Action> {
-        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<PanelEvent> 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()
-                ),
-            ]
-        );
-    }
-}

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -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<SearchQuery> = LazyLock::new(|| {
-    SearchQuery::regex(
-        "@[-_\\w]+",
-        false,
-        false,
-        false,
-        false,
-        Default::default(),
-        Default::default(),
-        false,
-        None,
-    )
-    .unwrap()
-});
-
-pub struct MessageEditor {
-    pub editor: Entity<Editor>,
-    user_store: Entity<UserStore>,
-    channel_chat: Option<Entity<ChannelChat>>,
-    mentions: Vec<UserId>,
-    mentions_task: Option<Task<()>>,
-    reply_to_message_id: Option<u64>,
-    edit_message_id: Option<u64>,
-}
-
-struct MessageEditorCompletionProvider(WeakEntity<MessageEditor>);
-
-impl CompletionProvider for MessageEditorCompletionProvider {
-    fn completions(
-        &self,
-        _excerpt_id: ExcerptId,
-        buffer: &Entity<Buffer>,
-        buffer_position: language::Anchor,
-        _: editor::CompletionContext,
-        _window: &mut Window,
-        cx: &mut Context<Editor>,
-    ) -> Task<Result<Vec<CompletionResponse>>> {
-        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<Buffer>,
-        _position: language::Anchor,
-        text: &str,
-        _trigger_in_words: bool,
-        _menu_is_open: bool,
-        _cx: &mut Context<Editor>,
-    ) -> bool {
-        text == "@"
-    }
-}
-
-impl MessageEditor {
-    pub fn new(
-        language_registry: Arc<LanguageRegistry>,
-        user_store: Entity<UserStore>,
-        channel_chat: Option<Entity<ChannelChat>>,
-        editor: Entity<Editor>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> 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::<settings::SettingsStore>(|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<u64> {
-        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<u64> {
-        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<ChannelChat>, cx: &mut Context<Self>) {
-        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<Self>) -> MessageParams {
-        self.editor.update(cx, |editor, cx| {
-            let highlights = editor.text_highlights::<Self>(cx);
-            let text = editor.text(cx);
-            let snapshot = editor.buffer().read(cx).snapshot(cx);
-            let mentions = if let Some((_, ranges)) = highlights {
-                ranges
-                    .iter()
-                    .map(|range| range.to_offset(&snapshot))
-                    .zip(self.mentions.iter().copied())
-                    .collect()
-            } else {
-                Vec::new()
-            };
-
-            editor.clear(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<Buffer>,
-        event: &language::BufferEvent,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        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<Buffer>,
-        end_anchor: Anchor,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<CompletionResponse>>> {
-        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<Anchor>,
-        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::<Vec<_>>();
-
-        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<Buffer>,
-        end_anchor: Anchor,
-        cx: &mut Context<Self>,
-    ) -> Option<(Anchor, String, Vec<StringMatchCandidate>)> {
-        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::<String>());
-                }
-                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::<Vec<_>>();
-
-        Some((start_anchor, query, candidates))
-    }
-
-    fn collect_emoji_candidates(
-        &mut self,
-        buffer: &Entity<Buffer>,
-        end_anchor: Anchor,
-        cx: &mut Context<Self>,
-    ) -> Option<(Anchor, String, &'static [StringMatchCandidate])> {
-        static EMOJI_FUZZY_MATCH_CANDIDATES: LazyLock<Vec<StringMatchCandidate>> =
-            LazyLock::new(|| {
-                emojis::iter()
-                    .flat_map(|s| s.shortcodes())
-                    .map(|emoji| StringMatchCandidate::new(0, emoji))
-                    .collect::<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 == ':' {
-                    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::<String>());
-                    }
-
-                    // 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::<String>();
-                    if util::word_consists_of_emojis(containing_word.as_str()) {
-                        return Some(query.chars().rev().collect::<String>());
-                    }
-                    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<MessageEditor>,
-        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::<Self>(cx);
-                editor.highlight_text::<Self>(
-                    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<Self>) -> 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()
-                },
-            ))
-    }
-}

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<Self>,
-    ) -> 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<Self>,
-    ) {
-        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::<ChatPanel>(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<Self>) {
         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;

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<AppState>, 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);

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(&notification, 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<Self>,
-    ) {
-        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::<ChatPanel>(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<Self>) -> bool {
-        if !self.active {
-            return false;
-        }
-
-        if let Notification::ChannelMessageMention { channel_id, .. } = &notification
-            && let Some(workspace) = self.workspace.upgrade()
-        {
-            return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(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<NotificationStore>,
@@ -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<Self>,
     ) {
-        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<Arc<User>>,
     text: String,
     workspace: WeakEntity<Workspace>,
@@ -799,22 +705,10 @@ impl WorkspaceNotification for NotificationToast {}
 impl NotificationToast {
     fn focus_notification_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
         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::<NotificationPanel>(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::<NotificationPanel>(window, cx)
                 })
                 .ok();
         })

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<ChatPanelButton>,
-    /// Where to dock the panel.
-    ///
-    /// Default: right
-    pub dock: Option<DockPosition>,
-    /// Default width of the panel in pixels.
-    ///
-    /// Default: 240
-    pub default_width: Option<f32>,
-}
-
 #[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<Self::FileContent>,
-        _: &mut gpui::App,
-    ) -> anyhow::Result<Self> {
-        sources.json_merge()
-    }
-
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
-}
-
 impl Settings for NotificationPanelSettings {
     type FileContent = NotificationPanelSettingsContent;
 

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

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<Client>,
     user_store: Entity<UserStore>,
-    channel_messages: HashMap<u64, ChannelMessage>,
     channel_store: Entity<ChannelStore>,
     notifications: SumTree<NotificationEntry>,
     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<Self>) -> Option<Task<Result<()>>> {
         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<Self>,
-        envelope: TypedEnvelope<proto::UpdateNotification>,
-        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(&notification)
-            {
-                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<Self>,
         notifications: Vec<proto::Notification>,
@@ -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()

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 {

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();

crates/vim/src/command.rs 🔗

@@ -1268,7 +1268,6 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
         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"),

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::<collab_ui::collab_panel::CollabPanel>(window, cx);
             },
         )
-        .register_action(
-            |workspace: &mut Workspace,
-             _: &collab_ui::chat_panel::ToggleFocus,
-             window: &mut Window,
-             cx: &mut Context<Workspace>| {
-                workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(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",

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

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.