Persist chat mentions

Max Brunsfeld created

Change summary

crates/channel/src/channel.rs                                  |   5 
crates/channel/src/channel_chat.rs                             |  54 +
crates/channel/src/channel_store_tests.rs                      |   5 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql |   8 
crates/collab/migrations/20231018102700_create_mentions.sql    |   7 
crates/collab/src/db.rs                                        |   1 
crates/collab/src/db/queries/messages.rs                       |  77 +
crates/collab/src/db/tables.rs                                 |   1 
crates/collab/src/db/tables/channel_message_mention.rs         |  43 
crates/collab/src/db/tests/buffer_tests.rs                     |   5 
crates/collab/src/db/tests/channel_tests.rs                    |  11 
crates/collab/src/db/tests/db_tests.rs                         |  12 
crates/collab/src/db/tests/feature_flag_tests.rs               |   2 
crates/collab/src/db/tests/message_tests.rs                    | 182 +++
crates/collab/src/rpc.rs                                       |   4 
crates/collab/src/tests/channel_message_tests.rs               |  58 
crates/collab/src/tests/randomized_test_helpers.rs             |   3 
crates/collab/src/tests/test_server.rs                         |   1 
crates/collab_ui/src/chat_panel.rs                             |   2 
crates/collab_ui/src/chat_panel/message_editor.rs              |  18 
crates/editor/src/editor.rs                                    |   6 
crates/rpc/proto/zed.proto                                     |   7 
22 files changed, 401 insertions(+), 111 deletions(-)

Detailed changes

crates/channel/src/channel.rs ๐Ÿ”—

@@ -7,7 +7,10 @@ use gpui::{AppContext, ModelHandle};
 use std::sync::Arc;
 
 pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
-pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
+pub use channel_chat::{
+    mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
+    MessageParams,
+};
 pub use channel_store::{
     Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
 };

crates/channel/src/channel_chat.rs ๐Ÿ”—

@@ -3,7 +3,7 @@ use anyhow::{anyhow, Result};
 use client::{
     proto,
     user::{User, UserStore},
-    Client, Subscription, TypedEnvelope,
+    Client, Subscription, TypedEnvelope, UserId,
 };
 use futures::lock::Mutex;
 use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
@@ -27,6 +27,12 @@ pub struct ChannelChat {
     _subscription: Subscription,
 }
 
+#[derive(Debug, PartialEq, Eq)]
+pub struct MessageParams {
+    pub text: String,
+    pub mentions: Vec<(Range<usize>, UserId)>,
+}
+
 #[derive(Clone, Debug)]
 pub struct ChannelMessage {
     pub id: ChannelMessageId,
@@ -34,6 +40,7 @@ pub struct ChannelMessage {
     pub timestamp: OffsetDateTime,
     pub sender: Arc<User>,
     pub nonce: u128,
+    pub mentions: Vec<(Range<usize>, UserId)>,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -120,12 +127,16 @@ impl ChannelChat {
         &self.channel
     }
 
+    pub fn client(&self) -> &Arc<Client> {
+        &self.rpc
+    }
+
     pub fn send_message(
         &mut self,
-        body: String,
+        message: MessageParams,
         cx: &mut ModelContext<Self>,
     ) -> Result<Task<Result<()>>> {
-        if body.is_empty() {
+        if message.text.is_empty() {
             Err(anyhow!("message body can't be empty"))?;
         }
 
@@ -142,9 +153,10 @@ impl ChannelChat {
             SumTree::from_item(
                 ChannelMessage {
                     id: pending_id,
-                    body: body.clone(),
+                    body: message.text.clone(),
                     sender: current_user,
                     timestamp: OffsetDateTime::now_utc(),
+                    mentions: message.mentions.clone(),
                     nonce,
                 },
                 &(),
@@ -158,8 +170,9 @@ impl ChannelChat {
             let outgoing_message_guard = outgoing_messages_lock.lock().await;
             let request = rpc.request(proto::SendChannelMessage {
                 channel_id,
-                body,
+                body: message.text,
                 nonce: Some(nonce.into()),
+                mentions: mentions_to_proto(&message.mentions),
             });
             let response = request.await?;
             drop(outgoing_message_guard);
@@ -284,6 +297,7 @@ impl ChannelChat {
                     let request = rpc.request(proto::SendChannelMessage {
                         channel_id,
                         body: pending_message.body,
+                        mentions: mentions_to_proto(&pending_message.mentions),
                         nonce: Some(pending_message.nonce.into()),
                     });
                     let response = request.await?;
@@ -471,6 +485,14 @@ impl ChannelMessage {
         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
@@ -509,6 +531,19 @@ impl ChannelMessage {
     }
 }
 
+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 as u64,
+        })
+        .collect()
+}
+
 impl sum_tree::Item for ChannelMessage {
     type Summary = ChannelMessageSummary;
 
@@ -547,3 +582,12 @@ impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
         self.0 += summary.count;
     }
 }
+
+impl<'a> From<&'a str> for MessageParams {
+    fn from(value: &'a str) -> Self {
+        Self {
+            text: value.into(),
+            mentions: Vec::new(),
+        }
+    }
+}

crates/channel/src/channel_store_tests.rs ๐Ÿ”—

@@ -194,6 +194,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
                     body: "a".into(),
                     timestamp: 1000,
                     sender_id: 5,
+                    mentions: vec![],
                     nonce: Some(1.into()),
                 },
                 proto::ChannelMessage {
@@ -201,6 +202,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
                     body: "b".into(),
                     timestamp: 1001,
                     sender_id: 6,
+                    mentions: vec![],
                     nonce: Some(2.into()),
                 },
             ],
@@ -247,6 +249,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
             body: "c".into(),
             timestamp: 1002,
             sender_id: 7,
+            mentions: vec![],
             nonce: Some(3.into()),
         }),
     });
@@ -300,6 +303,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
                     timestamp: 998,
                     sender_id: 5,
                     nonce: Some(4.into()),
+                    mentions: vec![],
                 },
                 proto::ChannelMessage {
                     id: 9,
@@ -307,6 +311,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
                     timestamp: 999,
                     sender_id: 6,
                     nonce: Some(5.into()),
+                    mentions: vec![],
                 },
             ],
         },

crates/collab/migrations.sqlite/20221109000000_test_schema.sql ๐Ÿ”—

@@ -215,6 +215,14 @@ CREATE TABLE IF NOT EXISTS "channel_messages" (
 CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id");
 CREATE UNIQUE INDEX "index_channel_messages_on_nonce" ON "channel_messages" ("nonce");
 
+CREATE TABLE "channel_message_mentions" (
+    "message_id" INTEGER NOT NULL REFERENCES channel_messages (id) ON DELETE CASCADE,
+    "start_offset" INTEGER NOT NULL,
+    "end_offset" INTEGER NOT NULL,
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    PRIMARY KEY(message_id, start_offset)
+);
+
 CREATE TABLE "channel_paths" (
     "id_path" TEXT NOT NULL PRIMARY KEY,
     "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE

crates/collab/migrations/20231018102700_create_mentions.sql ๐Ÿ”—

@@ -0,0 +1,7 @@
+CREATE TABLE "channel_message_mentions" (
+    "message_id" INTEGER NOT NULL REFERENCES channel_messages (id) ON DELETE CASCADE,
+    "start_offset" INTEGER NOT NULL,
+    "end_offset" INTEGER NOT NULL,
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    PRIMARY KEY(message_id, start_offset)
+);

crates/collab/src/db.rs ๐Ÿ”—

@@ -418,7 +418,6 @@ pub struct WaitlistSummary {
 pub struct NewUserParams {
     pub github_login: String,
     pub github_user_id: i32,
-    pub invite_count: i32,
 }
 
 #[derive(Debug)]

crates/collab/src/db/queries/messages.rs ๐Ÿ”—

@@ -103,6 +103,7 @@ impl Database {
                     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,
@@ -111,6 +112,38 @@ impl Database {
             }
             drop(rows);
             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)
         })
         .await
@@ -121,6 +154,7 @@ impl Database {
         channel_id: ChannelId,
         user_id: UserId,
         body: &str,
+        mentions: &[proto::ChatMention],
         timestamp: OffsetDateTime,
         nonce: u128,
     ) -> Result<(MessageId, Vec<ConnectionId>, Vec<UserId>)> {
@@ -150,7 +184,7 @@ impl Database {
             let timestamp = timestamp.to_offset(time::UtcOffset::UTC);
             let timestamp = time::PrimitiveDateTime::new(timestamp.date(), timestamp.time());
 
-            let message = channel_message::Entity::insert(channel_message::ActiveModel {
+            let message_id = 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()),
@@ -164,7 +198,31 @@ impl Database {
                     .to_owned(),
             )
             .exec(&*tx)
-            .await?;
+            .await?
+            .last_insert_id;
+
+            let models = 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<_>>();
+            if !models.is_empty() {
+                channel_message_mention::Entity::insert_many(models)
+                    .exec(&*tx)
+                    .await?;
+            }
 
             #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
             enum QueryConnectionId {
@@ -172,22 +230,13 @@ impl Database {
             }
 
             // Observe this message for the sender
-            self.observe_channel_message_internal(
-                channel_id,
-                user_id,
-                message.last_insert_id,
-                &*tx,
-            )
-            .await?;
+            self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
+                .await?;
 
             let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
             channel_members.retain(|member| !participant_user_ids.contains(member));
 
-            Ok((
-                message.last_insert_id,
-                participant_connection_ids,
-                channel_members,
-            ))
+            Ok((message_id, participant_connection_ids, channel_members))
         })
         .await
     }

crates/collab/src/db/tables.rs ๐Ÿ”—

@@ -7,6 +7,7 @@ pub mod channel_buffer_collaborator;
 pub mod channel_chat_participant;
 pub mod channel_member;
 pub mod channel_message;
+pub mod channel_message_mention;
 pub mod channel_path;
 pub mod contact;
 pub mod feature_flag;

crates/collab/src/db/tables/channel_message_mention.rs ๐Ÿ”—

@@ -0,0 +1,43 @@
+use crate::db::{MessageId, UserId};
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "channel_message_mentions")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub message_id: MessageId,
+    #[sea_orm(primary_key)]
+    pub start_offset: i32,
+    pub end_offset: i32,
+    pub user_id: UserId,
+}
+
+impl ActiveModelBehavior for ActiveModel {}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::channel_message::Entity",
+        from = "Column::MessageId",
+        to = "super::channel_message::Column::Id"
+    )]
+    Message,
+    #[sea_orm(
+        belongs_to = "super::user::Entity",
+        from = "Column::UserId",
+        to = "super::user::Column::Id"
+    )]
+    MentionedUser,
+}
+
+impl Related<super::channel::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Message.def()
+    }
+}
+
+impl Related<super::user::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::MentionedUser.def()
+    }
+}

crates/collab/src/db/tests/buffer_tests.rs ๐Ÿ”—

@@ -17,7 +17,6 @@ async fn test_channel_buffers(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user_a".into(),
                 github_user_id: 101,
-                invite_count: 0,
             },
         )
         .await
@@ -30,7 +29,6 @@ async fn test_channel_buffers(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user_b".into(),
                 github_user_id: 102,
-                invite_count: 0,
             },
         )
         .await
@@ -45,7 +43,6 @@ async fn test_channel_buffers(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user_c".into(),
                 github_user_id: 102,
-                invite_count: 0,
             },
         )
         .await
@@ -178,7 +175,6 @@ async fn test_channel_buffers_last_operations(db: &Database) {
             NewUserParams {
                 github_login: "user_a".into(),
                 github_user_id: 101,
-                invite_count: 0,
             },
         )
         .await
@@ -191,7 +187,6 @@ async fn test_channel_buffers_last_operations(db: &Database) {
             NewUserParams {
                 github_login: "user_b".into(),
                 github_user_id: 102,
-                invite_count: 0,
             },
         )
         .await

crates/collab/src/db/tests/channel_tests.rs ๐Ÿ”—

@@ -24,7 +24,6 @@ async fn test_channels(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user1".into(),
                 github_user_id: 5,
-                invite_count: 0,
             },
         )
         .await
@@ -38,7 +37,6 @@ async fn test_channels(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user2".into(),
                 github_user_id: 6,
-                invite_count: 0,
             },
         )
         .await
@@ -179,7 +177,6 @@ async fn test_joining_channels(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user1".into(),
                 github_user_id: 5,
-                invite_count: 0,
             },
         )
         .await
@@ -192,7 +189,6 @@ async fn test_joining_channels(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user2".into(),
                 github_user_id: 6,
-                invite_count: 0,
             },
         )
         .await
@@ -246,7 +242,6 @@ async fn test_channel_invites(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user1".into(),
                 github_user_id: 5,
-                invite_count: 0,
             },
         )
         .await
@@ -259,7 +254,6 @@ async fn test_channel_invites(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user2".into(),
                 github_user_id: 6,
-                invite_count: 0,
             },
         )
         .await
@@ -273,7 +267,6 @@ async fn test_channel_invites(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user3".into(),
                 github_user_id: 7,
-                invite_count: 0,
             },
         )
         .await
@@ -385,7 +378,6 @@ async fn test_channel_renames(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user1".into(),
                 github_user_id: 5,
-                invite_count: 0,
             },
         )
         .await
@@ -399,7 +391,6 @@ async fn test_channel_renames(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user2".into(),
                 github_user_id: 6,
-                invite_count: 0,
             },
         )
         .await
@@ -444,7 +435,6 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user1".into(),
                 github_user_id: 5,
-                invite_count: 0,
             },
         )
         .await
@@ -802,7 +792,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user1".into(),
                 github_user_id: 5,
-                invite_count: 0,
             },
         )
         .await

crates/collab/src/db/tests/db_tests.rs ๐Ÿ”—

@@ -22,7 +22,6 @@ async fn test_get_users(db: &Arc<Database>) {
                 NewUserParams {
                     github_login: format!("user{i}"),
                     github_user_id: i,
-                    invite_count: 0,
                 },
             )
             .await
@@ -88,7 +87,6 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "login1".into(),
                 github_user_id: 101,
-                invite_count: 0,
             },
         )
         .await
@@ -101,7 +99,6 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "login2".into(),
                 github_user_id: 102,
-                invite_count: 0,
             },
         )
         .await
@@ -156,7 +153,6 @@ async fn test_create_access_tokens(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "u1".into(),
                 github_user_id: 1,
-                invite_count: 0,
             },
         )
         .await
@@ -238,7 +234,6 @@ async fn test_add_contacts(db: &Arc<Database>) {
                 NewUserParams {
                     github_login: format!("user{i}"),
                     github_user_id: i,
-                    invite_count: 0,
                 },
             )
             .await
@@ -400,7 +395,6 @@ async fn test_metrics_id(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "person1".into(),
                 github_user_id: 101,
-                invite_count: 5,
             },
         )
         .await
@@ -416,7 +410,6 @@ async fn test_metrics_id(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "person2".into(),
                 github_user_id: 102,
-                invite_count: 5,
             },
         )
         .await
@@ -445,7 +438,6 @@ async fn test_project_count(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "admin".into(),
                 github_user_id: 0,
-                invite_count: 0,
             },
         )
         .await
@@ -457,7 +449,6 @@ async fn test_project_count(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user".into(),
                 github_user_id: 1,
-                invite_count: 0,
             },
         )
         .await
@@ -539,7 +530,6 @@ async fn test_fuzzy_search_users() {
             NewUserParams {
                 github_login: github_login.into(),
                 github_user_id: i as i32,
-                invite_count: 0,
             },
         )
         .await
@@ -581,7 +571,6 @@ async fn test_non_matching_release_channels(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "admin".into(),
                 github_user_id: 0,
-                invite_count: 0,
             },
         )
         .await
@@ -593,7 +582,6 @@ async fn test_non_matching_release_channels(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user".into(),
                 github_user_id: 1,
-                invite_count: 0,
             },
         )
         .await

crates/collab/src/db/tests/feature_flag_tests.rs ๐Ÿ”—

@@ -18,7 +18,6 @@ async fn test_get_user_flags(db: &Arc<Database>) {
             NewUserParams {
                 github_login: format!("user1"),
                 github_user_id: 1,
-                invite_count: 0,
             },
         )
         .await
@@ -32,7 +31,6 @@ async fn test_get_user_flags(db: &Arc<Database>) {
             NewUserParams {
                 github_login: format!("user2"),
                 github_user_id: 2,
-                invite_count: 0,
             },
         )
         .await

crates/collab/src/db/tests/message_tests.rs ๐Ÿ”—

@@ -2,6 +2,7 @@ use crate::{
     db::{Database, MessageId, NewUserParams},
     test_both_dbs,
 };
+use channel::mentions_to_proto;
 use std::sync::Arc;
 use time::OffsetDateTime;
 
@@ -19,7 +20,6 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user".into(),
                 github_user_id: 1,
-                invite_count: 0,
             },
         )
         .await
@@ -35,11 +35,18 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
     let mut all_messages = Vec::new();
     for i in 0..10 {
         all_messages.push(
-            db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
-                .await
-                .unwrap()
-                .0
-                .to_proto(),
+            db.create_channel_message(
+                channel,
+                user,
+                &i.to_string(),
+                &[],
+                OffsetDateTime::now_utc(),
+                i,
+            )
+            .await
+            .unwrap()
+            .0
+            .to_proto(),
         );
     }
 
@@ -81,7 +88,6 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user".into(),
                 github_user_id: 1,
-                invite_count: 0,
             },
         )
         .await
@@ -96,19 +102,19 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
         .unwrap();
 
     let msg1_id = db
-        .create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1)
+        .create_channel_message(channel, user, "1", &[], OffsetDateTime::now_utc(), 1)
         .await
         .unwrap();
     let msg2_id = db
-        .create_channel_message(channel, user, "2", OffsetDateTime::now_utc(), 2)
+        .create_channel_message(channel, user, "2", &[], OffsetDateTime::now_utc(), 2)
         .await
         .unwrap();
     let msg3_id = db
-        .create_channel_message(channel, user, "3", OffsetDateTime::now_utc(), 1)
+        .create_channel_message(channel, user, "3", &[], OffsetDateTime::now_utc(), 1)
         .await
         .unwrap();
     let msg4_id = db
-        .create_channel_message(channel, user, "4", OffsetDateTime::now_utc(), 2)
+        .create_channel_message(channel, user, "4", &[], OffsetDateTime::now_utc(), 2)
         .await
         .unwrap();
 
@@ -118,12 +124,12 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
 }
 
 test_both_dbs!(
-    test_channel_message_new_notification,
-    test_channel_message_new_notification_postgres,
-    test_channel_message_new_notification_sqlite
+    test_unseen_channel_messages,
+    test_unseen_channel_messages_postgres,
+    test_unseen_channel_messages_sqlite
 );
 
-async fn test_channel_message_new_notification(db: &Arc<Database>) {
+async fn test_unseen_channel_messages(db: &Arc<Database>) {
     let user = db
         .create_user(
             "user_a@example.com",
@@ -131,7 +137,6 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user_a".into(),
                 github_user_id: 1,
-                invite_count: 0,
             },
         )
         .await
@@ -143,8 +148,7 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
             false,
             NewUserParams {
                 github_login: "user_b".into(),
-                github_user_id: 1,
-                invite_count: 0,
+                github_user_id: 2,
             },
         )
         .await
@@ -152,21 +156,17 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
         .user_id;
 
     let channel_1 = db.create_channel("channel", None, user).await.unwrap();
-
     let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
 
     db.invite_channel_member(channel_1, observer, user, false)
         .await
         .unwrap();
-
-    db.respond_to_channel_invite(channel_1, observer, true)
+    db.invite_channel_member(channel_2, observer, user, false)
         .await
         .unwrap();
-
-    db.invite_channel_member(channel_2, observer, user, false)
+    db.respond_to_channel_invite(channel_1, observer, true)
         .await
         .unwrap();
-
     db.respond_to_channel_invite(channel_2, observer, true)
         .await
         .unwrap();
@@ -179,17 +179,17 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
         .unwrap();
 
     let _ = db
-        .create_channel_message(channel_1, user, "1_1", OffsetDateTime::now_utc(), 1)
+        .create_channel_message(channel_1, user, "1_1", &[], OffsetDateTime::now_utc(), 1)
         .await
         .unwrap();
 
     let (second_message, _, _) = db
-        .create_channel_message(channel_1, user, "1_2", OffsetDateTime::now_utc(), 2)
+        .create_channel_message(channel_1, user, "1_2", &[], OffsetDateTime::now_utc(), 2)
         .await
         .unwrap();
 
     let (third_message, _, _) = db
-        .create_channel_message(channel_1, user, "1_3", OffsetDateTime::now_utc(), 3)
+        .create_channel_message(channel_1, user, "1_3", &[], OffsetDateTime::now_utc(), 3)
         .await
         .unwrap();
 
@@ -198,7 +198,7 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
         .unwrap();
 
     let (fourth_message, _, _) = db
-        .create_channel_message(channel_2, user, "2_1", OffsetDateTime::now_utc(), 4)
+        .create_channel_message(channel_2, user, "2_1", &[], OffsetDateTime::now_utc(), 4)
         .await
         .unwrap();
 
@@ -295,3 +295,129 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
         }]
     );
 }
+
+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 = db
+        .create_user(
+            "user_a@example.com",
+            false,
+            NewUserParams {
+                github_login: "user_a".into(),
+                github_user_id: 1,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let user_b = db
+        .create_user(
+            "user_b@example.com",
+            false,
+            NewUserParams {
+                github_login: "user_b".into(),
+                github_user_id: 2,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let user_c = db
+        .create_user(
+            "user_b@example.com",
+            false,
+            NewUserParams {
+                github_login: "user_c".into(),
+                github_user_id: 3,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let channel = db.create_channel("channel", None, user_a).await.unwrap();
+    db.invite_channel_member(channel, user_b, user_a, false)
+        .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,
+    )
+    .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,
+    )
+    .await
+    .unwrap();
+    db.create_channel_message(
+        channel,
+        user_a,
+        "umm",
+        &mentions_to_proto(&[]),
+        OffsetDateTime::now_utc(),
+        3,
+    )
+    .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,
+    )
+    .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 ๐Ÿ”—

@@ -2885,6 +2885,8 @@ async fn send_channel_message(
         return Err(anyhow!("message can't be blank"))?;
     }
 
+    // TODO: adjust mentions if body is trimmed
+
     let timestamp = OffsetDateTime::now_utc();
     let nonce = request
         .nonce
@@ -2898,6 +2900,7 @@ async fn send_channel_message(
             channel_id,
             session.user_id,
             &body,
+            &request.mentions,
             timestamp,
             nonce.clone().into(),
         )
@@ -2906,6 +2909,7 @@ async fn send_channel_message(
         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),
     };

crates/collab/src/tests/channel_message_tests.rs ๐Ÿ”—

@@ -1,5 +1,5 @@
 use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
-use channel::{ChannelChat, ChannelMessageId};
+use channel::{ChannelChat, ChannelMessageId, MessageParams};
 use collab_ui::chat_panel::ChatPanel;
 use gpui::{executor::Deterministic, BorrowAppContext, ModelHandle, TestAppContext};
 use std::sync::Arc;
@@ -8,20 +8,22 @@ use workspace::dock::Panel;
 #[gpui::test]
 async fn test_basic_channel_messages(
     deterministic: Arc<Deterministic>,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
+    mut cx_a: &mut TestAppContext,
+    mut cx_b: &mut TestAppContext,
+    mut cx_c: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
     let mut server = TestServer::start(&deterministic).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)],
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
         )
         .await;
 
@@ -37,7 +39,16 @@ async fn test_basic_channel_messages(
         .unwrap();
 
     channel_chat_a
-        .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
+        .update(cx_a, |c, cx| {
+            c.send_message(
+                MessageParams {
+                    text: "hi @user_c!".into(),
+                    mentions: vec![(3..10, client_c.id())],
+                },
+                cx,
+            )
+            .unwrap()
+        })
         .await
         .unwrap();
     channel_chat_a
@@ -52,15 +63,34 @@ async fn test_basic_channel_messages(
         .unwrap();
 
     deterministic.run_until_parked();
-    channel_chat_a.update(cx_a, |c, _| {
-        assert_eq!(
-            c.messages()
-                .iter()
-                .map(|m| m.body.as_str())
-                .collect::<Vec<_>>(),
-            vec!["one", "two", "three"]
-        );
-    })
+
+    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(),
+            );
+        });
+    }
 }
 
 #[gpui::test]

crates/collab_ui/src/chat_panel.rs ๐Ÿ”—

@@ -599,7 +599,7 @@ impl ChatPanel {
                 .update(cx, |editor, cx| editor.take_message(cx));
 
             if let Some(task) = chat
-                .update(cx, |chat, cx| chat.send_message(message.text, cx))
+                .update(cx, |chat, cx| chat.send_message(message, cx))
                 .log_err()
             {
                 task.detach();

crates/collab_ui/src/chat_panel/message_editor.rs ๐Ÿ”—

@@ -1,4 +1,4 @@
-use channel::{Channel, ChannelMembership, ChannelStore};
+use channel::{Channel, ChannelMembership, ChannelStore, MessageParams};
 use client::UserId;
 use collections::HashMap;
 use editor::{AnchorRangeExt, Editor};
@@ -9,7 +9,7 @@ use gpui::{
 use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
 use lazy_static::lazy_static;
 use project::search::SearchQuery;
-use std::{ops::Range, sync::Arc, time::Duration};
+use std::{sync::Arc, time::Duration};
 
 const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
 
@@ -33,12 +33,6 @@ pub struct MessageEditor {
     channel: Option<Arc<Channel>>,
 }
 
-#[derive(Debug, PartialEq, Eq)]
-pub struct ChatMessage {
-    pub text: String,
-    pub mentions: Vec<(Range<usize>, UserId)>,
-}
-
 impl MessageEditor {
     pub fn new(
         language_registry: Arc<LanguageRegistry>,
@@ -102,7 +96,7 @@ impl MessageEditor {
         }
     }
 
-    pub fn set_members(&mut self, members: Vec<ChannelMembership>, cx: &mut ViewContext<Self>) {
+    pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
         self.users.clear();
         self.users.extend(
             members
@@ -111,7 +105,7 @@ impl MessageEditor {
         );
     }
 
-    pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> ChatMessage {
+    pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
         self.editor.update(cx, |editor, cx| {
             let highlights = editor.text_highlights::<Self>(cx);
             let text = editor.text(cx);
@@ -129,7 +123,7 @@ impl MessageEditor {
             editor.clear(cx);
             self.mentions.clear();
 
-            ChatMessage { text, mentions }
+            MessageParams { text, mentions }
         })
     }
 
@@ -264,7 +258,7 @@ mod tests {
             let (text, ranges) = marked_text_ranges("Hello, ยซ@a-bยป! Have you met ยซ@C_Dยป?", false);
             assert_eq!(
                 editor.take_message(cx),
-                ChatMessage {
+                MessageParams {
                     text,
                     mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
                 }

crates/editor/src/editor.rs ๐Ÿ”—

@@ -3286,8 +3286,10 @@ impl Editor {
                     i = 0;
                 } else if pair_state.range.start.to_offset(buffer) > range.end {
                     break;
-                } else if pair_state.selection_id == selection.id {
-                    enclosing = Some(pair_state);
+                } else {
+                    if pair_state.selection_id == selection.id {
+                        enclosing = Some(pair_state);
+                    }
                     i += 1;
                 }
             }

crates/rpc/proto/zed.proto ๐Ÿ”—

@@ -1088,6 +1088,7 @@ message SendChannelMessage {
     uint64 channel_id = 1;
     string body = 2;
     Nonce nonce = 3;
+    repeated ChatMention mentions = 4;
 }
 
 message RemoveChannelMessage {
@@ -1149,6 +1150,12 @@ message ChannelMessage {
     uint64 timestamp = 3;
     uint64 sender_id = 4;
     Nonce nonce = 5;
+    repeated ChatMention mentions = 6;
+}
+
+message ChatMention {
+    Range range = 1;
+    uint64 user_id = 2;
 }
 
 message RejoinChannelBuffers {