diff --git a/Cargo.lock b/Cargo.lock
index 5fe28590a14fe4e8874022972a3baf3e7d6b7c4f..e817fed0dbfe37f8ac0ba05bf0f9de79a1ef8997 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1504,6 +1504,7 @@ dependencies = [
"lsp",
"nanoid",
"node_runtime",
+ "notifications",
"parking_lot 0.11.2",
"pretty_assertions",
"project",
@@ -1559,13 +1560,17 @@ dependencies = [
"fuzzy",
"gpui",
"language",
+ "lazy_static",
"log",
"menu",
+ "notifications",
"picker",
"postage",
+ "pretty_assertions",
"project",
"recent_projects",
"rich_text",
+ "rpc",
"schemars",
"serde",
"serde_derive",
@@ -1573,6 +1578,7 @@ dependencies = [
"theme",
"theme_selector",
"time",
+ "tree-sitter-markdown",
"util",
"vcs_menu",
"workspace",
@@ -4730,6 +4736,26 @@ dependencies = [
"minimal-lexical",
]
+[[package]]
+name = "notifications"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "channel",
+ "client",
+ "clock",
+ "collections",
+ "db",
+ "feature_flags",
+ "gpui",
+ "rpc",
+ "settings",
+ "sum_tree",
+ "text",
+ "time",
+ "util",
+]
+
[[package]]
name = "ntapi"
version = "0.3.7"
@@ -6404,8 +6430,10 @@ dependencies = [
"rsa 0.4.0",
"serde",
"serde_derive",
+ "serde_json",
"smol",
"smol-timeout",
+ "strum",
"tempdir",
"tracing",
"util",
@@ -6626,6 +6654,12 @@ dependencies = [
"untrusted",
]
+[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
[[package]]
name = "rustybuzz"
version = "0.3.0"
@@ -7700,6 +7734,22 @@ name = "strum"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.37",
+]
[[package]]
name = "subtle"
@@ -10098,6 +10148,7 @@ dependencies = [
"log",
"lsp",
"node_runtime",
+ "notifications",
"num_cpus",
"outline",
"parking_lot 0.11.2",
diff --git a/Cargo.toml b/Cargo.toml
index 995cd15edd45d6c983fa20c19c6ba82749300260..cf977b8fe6dcecd39641f07802001afa1456d220 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -47,6 +47,7 @@ members = [
"crates/media",
"crates/menu",
"crates/node_runtime",
+ "crates/notifications",
"crates/outline",
"crates/picker",
"crates/plugin",
@@ -112,6 +113,7 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
smallvec = { version = "1.6", features = ["union"] }
smol = { version = "1.2" }
+strum = { version = "0.25.0", features = ["derive"] }
sysinfo = "0.29.10"
tempdir = { version = "0.3.7" }
thiserror = { version = "1.0.29" }
diff --git a/assets/icons/bell.svg b/assets/icons/bell.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ea1c6dd42e8821b632f6de97d143a7b9f4b97fd2
--- /dev/null
+++ b/assets/icons/bell.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 4143e5dd41f1a1596ea532433d91cfaffe35f92a..e70b56335915c8b4b2397dcae73def3d0a7bcda3 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -142,6 +142,14 @@
// Default width of the channels panel.
"default_width": 240
},
+ "notification_panel": {
+ // Whether to show the collaboration panel button in the status bar.
+ "button": true,
+ // Where to dock channels panel. Can be 'left' or 'right'.
+ "dock": "right",
+ // Default width of the channels panel.
+ "default_width": 240
+ },
"assistant": {
// Whether to show the assistant panel button in the status bar.
"button": true,
diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs
index d31d4b3c8c9e77e94661835c06ea234c70ded416..b6db304a70c31deab55aec61d6f5912f8bfd3e20 100644
--- a/crates/channel/src/channel.rs
+++ b/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,
};
diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs
index 734182886b3bebeacd03dbc177bf8ffcb8ab64e2..ca344c409f5df1d09c830fbecc5b649fbdd3d844 100644
--- a/crates/channel/src/channel_chat.rs
+++ b/crates/channel/src/channel_chat.rs
@@ -3,12 +3,17 @@ 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};
use rand::prelude::*;
-use std::{collections::HashSet, mem, ops::Range, sync::Arc};
+use std::{
+ collections::HashSet,
+ mem,
+ ops::{ControlFlow, Range},
+ sync::Arc,
+};
use sum_tree::{Bias, SumTree};
use time::OffsetDateTime;
use util::{post_inc, ResultExt as _, TryFutureExt};
@@ -16,6 +21,7 @@ use util::{post_inc, ResultExt as _, TryFutureExt};
pub struct ChannelChat {
channel: Arc,
messages: SumTree,
+ acknowledged_message_ids: HashSet,
channel_store: ModelHandle,
loaded_all_messages: bool,
last_acknowledged_id: Option,
@@ -27,6 +33,12 @@ pub struct ChannelChat {
_subscription: Subscription,
}
+#[derive(Debug, PartialEq, Eq)]
+pub struct MessageParams {
+ pub text: String,
+ pub mentions: Vec<(Range, UserId)>,
+}
+
#[derive(Clone, Debug)]
pub struct ChannelMessage {
pub id: ChannelMessageId,
@@ -34,6 +46,7 @@ pub struct ChannelMessage {
pub timestamp: OffsetDateTime,
pub sender: Arc,
pub nonce: u128,
+ pub mentions: Vec<(Range, UserId)>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -105,6 +118,7 @@ impl ChannelChat {
rpc: client,
outgoing_messages_lock: Default::default(),
messages: Default::default(),
+ acknowledged_message_ids: Default::default(),
loaded_all_messages,
next_pending_message_id: 0,
last_acknowledged_id: None,
@@ -120,12 +134,16 @@ impl ChannelChat {
&self.channel
}
+ pub fn client(&self) -> &Arc {
+ &self.rpc
+ }
+
pub fn send_message(
&mut self,
- body: String,
+ message: MessageParams,
cx: &mut ModelContext,
- ) -> Result>> {
- if body.is_empty() {
+ ) -> Result>> {
+ if message.text.is_empty() {
Err(anyhow!("message body can't be empty"))?;
}
@@ -142,9 +160,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,20 +177,18 @@ 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);
- let message = ChannelMessage::from_proto(
- response.message.ok_or_else(|| anyhow!("invalid message"))?,
- &user_store,
- &mut cx,
- )
- .await?;
+ let response = response.message.ok_or_else(|| anyhow!("invalid message"))?;
+ let id = response.id;
+ let message = ChannelMessage::from_proto(response, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
- Ok(())
+ Ok(id)
})
}))
}
@@ -191,41 +208,76 @@ impl ChannelChat {
})
}
- pub fn load_more_messages(&mut self, cx: &mut ModelContext) -> bool {
- if !self.loaded_all_messages {
- let rpc = self.rpc.clone();
- let user_store = self.user_store.clone();
- let channel_id = self.channel.id;
- if let Some(before_message_id) =
- self.messages.first().and_then(|message| match message.id {
- ChannelMessageId::Saved(id) => Some(id),
- ChannelMessageId::Pending(_) => None,
- })
- {
- cx.spawn(|this, mut cx| {
- async move {
- let response = rpc
- .request(proto::GetChannelMessages {
- channel_id,
- before_message_id,
- })
- .await?;
- let loaded_all_messages = response.done;
- let messages =
- messages_from_proto(response.messages, &user_store, &mut cx).await?;
- this.update(&mut cx, |this, cx| {
- this.loaded_all_messages = loaded_all_messages;
- this.insert_messages(messages, cx);
- });
- anyhow::Ok(())
+ pub fn load_more_messages(&mut self, cx: &mut ModelContext) -> 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(|this, mut cx| {
+ async move {
+ let response = rpc
+ .request(proto::GetChannelMessages {
+ channel_id,
+ before_message_id,
+ })
+ .await?;
+ let loaded_all_messages = response.done;
+ let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
+ this.update(&mut cx, |this, cx| {
+ this.loaded_all_messages = loaded_all_messages;
+ this.insert_messages(messages, cx);
+ });
+ anyhow::Ok(())
+ }
+ .log_err()
+ }))
+ }
+
+ pub fn first_loaded_message_id(&mut self) -> Option {
+ self.messages.first().and_then(|message| match message.id {
+ ChannelMessageId::Saved(id) => Some(id),
+ ChannelMessageId::Pending(_) => None,
+ })
+ }
+
+ /// 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: ModelHandle,
+ message_id: u64,
+ mut cx: AsyncAppContext,
+ ) -> Option {
+ loop {
+ let step = chat.update(&mut cx, |chat, cx| {
+ if let Some(first_id) = chat.first_loaded_message_id() {
+ if first_id <= message_id {
+ let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>();
+ let message_id = ChannelMessageId::Saved(message_id);
+ cursor.seek(&message_id, Bias::Left, &());
+ return ControlFlow::Break(
+ if cursor
+ .item()
+ .map_or(false, |message| message.id == message_id)
+ {
+ Some(cursor.start().1 .0)
+ } else {
+ None
+ },
+ );
}
- .log_err()
- })
- .detach();
- return true;
+ }
+ ControlFlow::Continue(chat.load_more_messages(cx))
+ });
+ match step {
+ ControlFlow::Break(ix) => return ix,
+ ControlFlow::Continue(task) => task?.await?,
}
}
- false
}
pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext) {
@@ -284,6 +336,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?;
@@ -319,6 +372,17 @@ impl ChannelChat {
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,
+ message_id: id,
+ })
+ .ok();
+ }
+ }
+
pub fn messages_in_range(&self, range: Range) -> impl Iterator- {
let mut cursor = self.messages.cursor::();
cursor.seek(&Count(range.start), Bias::Right, &());
@@ -451,22 +515,7 @@ async fn messages_from_proto(
user_store: &ModelHandle,
cx: &mut AsyncAppContext,
) -> Result> {
- let unique_user_ids = proto_messages
- .iter()
- .map(|m| m.sender_id)
- .collect::>()
- .into_iter()
- .collect();
- user_store
- .update(cx, |user_store, cx| {
- user_store.get_users(unique_user_ids, cx)
- })
- .await?;
-
- let mut messages = Vec::with_capacity(proto_messages.len());
- for message in proto_messages {
- messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
- }
+ let messages = ChannelMessage::from_proto_vec(proto_messages, user_store, cx).await?;
let mut result = SumTree::new();
result.extend(messages, &());
Ok(result)
@@ -486,6 +535,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
@@ -498,6 +555,43 @@ impl ChannelMessage {
pub fn is_pending(&self) -> bool {
matches!(self.id, ChannelMessageId::Pending(_))
}
+
+ pub async fn from_proto_vec(
+ proto_messages: Vec,
+ user_store: &ModelHandle,
+ cx: &mut AsyncAppContext,
+ ) -> Result> {
+ let unique_user_ids = proto_messages
+ .iter()
+ .map(|m| m.sender_id)
+ .collect::>()
+ .into_iter()
+ .collect();
+ user_store
+ .update(cx, |user_store, cx| {
+ user_store.get_users(unique_user_ids, cx)
+ })
+ .await?;
+
+ let mut messages = Vec::with_capacity(proto_messages.len());
+ for message in proto_messages {
+ messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
+ }
+ Ok(messages)
+ }
+}
+
+pub fn mentions_to_proto(mentions: &[(Range, UserId)]) -> Vec {
+ mentions
+ .iter()
+ .map(|(range, user_id)| proto::ChatMention {
+ range: Some(proto::Range {
+ start: range.start as u64,
+ end: range.end as u64,
+ }),
+ user_id: *user_id as u64,
+ })
+ .collect()
}
impl sum_tree::Item for ChannelMessage {
@@ -538,3 +632,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(),
+ }
+ }
+}
diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs
index 9c80dcc2b742b07430a752286a2008bc5cfd05b2..221b84529706a36aa22371fedf2e8ceb5cc11987 100644
--- a/crates/channel/src/channel_store.rs
+++ b/crates/channel/src/channel_store.rs
@@ -1,6 +1,6 @@
mod channel_index;
-use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
+use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
use anyhow::{anyhow, Result};
use channel_index::ChannelIndex;
use client::{Client, Subscription, User, UserId, UserStore};
@@ -153,9 +153,6 @@ impl ChannelStore {
this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx));
}
}
- if status.is_connected() {
- } else {
- }
}
Some(())
});
@@ -242,6 +239,12 @@ impl ChannelStore {
self.channel_index.by_id().values().nth(ix)
}
+ pub fn has_channel_invitation(&self, channel_id: ChannelId) -> bool {
+ self.channel_invitations
+ .iter()
+ .any(|channel| channel.id == channel_id)
+ }
+
pub fn channel_invitations(&self) -> &[Arc] {
&self.channel_invitations
}
@@ -274,6 +277,33 @@ impl ChannelStore {
)
}
+ pub fn fetch_channel_messages(
+ &self,
+ message_ids: Vec,
+ cx: &mut ModelContext,
+ ) -> Task>> {
+ let request = if message_ids.is_empty() {
+ None
+ } else {
+ Some(
+ self.client
+ .request(proto::GetChannelMessagesById { message_ids }),
+ )
+ };
+ cx.spawn_weak(|this, mut cx| async move {
+ if let Some(request) = request {
+ let response = request.await?;
+ let this = this
+ .upgrade(&cx)
+ .ok_or_else(|| anyhow!("channel store dropped"))?;
+ let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
+ ChannelMessage::from_proto_vec(response.messages, &user_store, &mut cx).await
+ } else {
+ Ok(Vec::new())
+ }
+ })
+ }
+
pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option {
self.channel_index
.by_id()
@@ -694,14 +724,15 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
accept: bool,
- ) -> impl Future