diff --git a/.cargo/config.toml b/.cargo/config.toml
index 35049cbcb1..9da6b3be08 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,2 +1,6 @@
[alias]
xtask = "run --package xtask --"
+
+[build]
+# v0 mangling scheme provides more detailed backtraces around closures
+rustflags = ["-C", "symbol-mangling-version=v0"]
diff --git a/Cargo.lock b/Cargo.lock
index 2f549c568d..c620cb2b88 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -79,18 +79,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
-version = "0.7.20"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "aho-corasick"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a"
+checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0"
dependencies = [
"memchr",
]
@@ -101,6 +92,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
+ "client",
"collections",
"ctor",
"editor",
@@ -127,6 +119,7 @@ dependencies = [
"theme",
"tiktoken-rs 0.4.5",
"util",
+ "uuid 1.4.1",
"workspace",
]
@@ -1554,6 +1547,7 @@ dependencies = [
"settings",
"theme",
"theme_selector",
+ "time 0.3.27",
"util",
"vcs_menu",
"workspace",
@@ -2329,7 +2323,7 @@ checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555"
name = "editor"
version = "0.1.0"
dependencies = [
- "aho-corasick 0.7.20",
+ "aho-corasick",
"anyhow",
"client",
"clock",
@@ -3077,7 +3071,7 @@ version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d"
dependencies = [
- "aho-corasick 1.0.4",
+ "aho-corasick",
"bstr",
"fnv",
"log",
@@ -5459,7 +5453,7 @@ dependencies = [
name = "project"
version = "0.1.0"
dependencies = [
- "aho-corasick 0.7.20",
+ "aho-corasick",
"anyhow",
"async-trait",
"backtrace",
@@ -5969,7 +5963,7 @@ version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a"
dependencies = [
- "aho-corasick 1.0.4",
+ "aho-corasick",
"memchr",
"regex-automata 0.3.6",
"regex-syntax 0.7.4",
@@ -5990,7 +5984,7 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69"
dependencies = [
- "aho-corasick 1.0.4",
+ "aho-corasick",
"memchr",
"regex-syntax 0.7.4",
]
diff --git a/assets/icons/logo_96.svg b/assets/icons/logo_96.svg
new file mode 100644
index 0000000000..dc98bb8bc2
--- /dev/null
+++ b/assets/icons/logo_96.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/user_group_16.svg b/assets/icons/user_group_16.svg
new file mode 100644
index 0000000000..aa99277646
--- /dev/null
+++ b/assets/icons/user_group_16.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json
index 44dbf2533f..c824fc7589 100644
--- a/assets/keymaps/vim.json
+++ b/assets/keymaps/vim.json
@@ -455,6 +455,7 @@
"shift-d": "vim::VisualDelete",
"shift-x": "vim::VisualDelete",
"y": "vim::VisualYank",
+ "shift-y": "vim::VisualYank",
"p": "vim::Paste",
"shift-p": [
"vim::Paste",
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 6739819e71..86def54d32 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -131,6 +131,14 @@
// Default width of the channels panel.
"default_width": 240
},
+ "chat_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/ai/Cargo.toml b/crates/ai/Cargo.toml
index d96e470d5c..8002b0d35d 100644
--- a/crates/ai/Cargo.toml
+++ b/crates/ai/Cargo.toml
@@ -9,6 +9,7 @@ path = "src/ai.rs"
doctest = false
[dependencies]
+client = { path = "../client" }
collections = { path = "../collections"}
editor = { path = "../editor" }
fs = { path = "../fs" }
@@ -19,6 +20,7 @@ search = { path = "../search" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
+uuid = { version = "1.1.2", features = ["v4"] }
workspace = { path = "../workspace" }
anyhow.workspace = true
diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs
index 7d9b93b0a7..dfd9a523b4 100644
--- a/crates/ai/src/ai.rs
+++ b/crates/ai/src/ai.rs
@@ -61,6 +61,7 @@ struct SavedMessage {
#[derive(Serialize, Deserialize)]
struct SavedConversation {
+ id: Option,
zed: String,
version: String,
text: String,
diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs
index 7fa66f26fb..263382c03e 100644
--- a/crates/ai/src/assistant.rs
+++ b/crates/ai/src/assistant.rs
@@ -6,6 +6,7 @@ use crate::{
};
use anyhow::{anyhow, Result};
use chrono::{DateTime, Local};
+use client::{telemetry::AssistantKind, ClickhouseEvent, TelemetrySettings};
use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{
display_map::{
@@ -48,6 +49,7 @@ use theme::{
AssistantStyle,
};
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
+use uuid::Uuid;
use workspace::{
dock::{DockPosition, Panel},
searchable::Direction,
@@ -296,6 +298,7 @@ impl AssistantPanel {
self.include_conversation_in_next_inline_assist,
self.inline_prompt_history.clone(),
codegen.clone(),
+ self.workspace.clone(),
cx,
);
cx.focus_self();
@@ -724,6 +727,7 @@ impl AssistantPanel {
self.api_key.clone(),
self.languages.clone(),
self.fs.clone(),
+ self.workspace.clone(),
cx,
)
});
@@ -1059,6 +1063,7 @@ impl AssistantPanel {
}
let fs = self.fs.clone();
+ let workspace = self.workspace.clone();
let api_key = self.api_key.clone();
let languages = self.languages.clone();
cx.spawn(|this, mut cx| async move {
@@ -1073,8 +1078,9 @@ impl AssistantPanel {
if let Some(ix) = this.editor_index_for_path(&path, cx) {
this.set_active_editor_index(Some(ix), cx);
} else {
- let editor = cx
- .add_view(|cx| ConversationEditor::for_conversation(conversation, fs, cx));
+ let editor = cx.add_view(|cx| {
+ ConversationEditor::for_conversation(conversation, fs, workspace, cx)
+ });
this.add_conversation(editor, cx);
}
})?;
@@ -1348,6 +1354,7 @@ struct Summary {
}
struct Conversation {
+ id: Option,
buffer: ModelHandle,
message_anchors: Vec,
messages_metadata: HashMap,
@@ -1398,6 +1405,7 @@ impl Conversation {
let model = settings.default_open_ai_model.clone();
let mut this = Self {
+ id: Some(Uuid::new_v4().to_string()),
message_anchors: Default::default(),
messages_metadata: Default::default(),
next_message_id: Default::default(),
@@ -1435,6 +1443,7 @@ impl Conversation {
fn serialize(&self, cx: &AppContext) -> SavedConversation {
SavedConversation {
+ id: self.id.clone(),
zed: "conversation".into(),
version: SavedConversation::VERSION.into(),
text: self.buffer.read(cx).text(),
@@ -1462,6 +1471,10 @@ impl Conversation {
language_registry: Arc,
cx: &mut ModelContext,
) -> Self {
+ let id = match saved_conversation.id {
+ Some(id) => Some(id),
+ None => Some(Uuid::new_v4().to_string()),
+ };
let model = saved_conversation.model;
let markdown = language_registry.language_for_name("Markdown");
let mut message_anchors = Vec::new();
@@ -1491,6 +1504,7 @@ impl Conversation {
});
let mut this = Self {
+ id,
message_anchors,
messages_metadata: saved_conversation.message_metadata,
next_message_id,
@@ -2108,6 +2122,7 @@ struct ScrollPosition {
struct ConversationEditor {
conversation: ModelHandle,
fs: Arc,
+ workspace: WeakViewHandle,
editor: ViewHandle,
blocks: HashSet,
scroll_position: Option,
@@ -2119,15 +2134,17 @@ impl ConversationEditor {
api_key: Rc>>,
language_registry: Arc,
fs: Arc,
+ workspace: WeakViewHandle,
cx: &mut ViewContext,
) -> Self {
let conversation = cx.add_model(|cx| Conversation::new(api_key, language_registry, cx));
- Self::for_conversation(conversation, fs, cx)
+ Self::for_conversation(conversation, fs, workspace, cx)
}
fn for_conversation(
conversation: ModelHandle,
fs: Arc,
+ workspace: WeakViewHandle,
cx: &mut ViewContext,
) -> Self {
let editor = cx.add_view(|cx| {
@@ -2150,6 +2167,7 @@ impl ConversationEditor {
blocks: Default::default(),
scroll_position: None,
fs,
+ workspace,
_subscriptions,
};
this.update_message_headers(cx);
@@ -2157,6 +2175,13 @@ impl ConversationEditor {
}
fn assist(&mut self, _: &Assist, cx: &mut ViewContext) {
+ report_assistant_event(
+ self.workspace.clone(),
+ self.conversation.read(cx).id.clone(),
+ AssistantKind::Panel,
+ cx,
+ );
+
let cursors = self.cursors(cx);
let user_messages = self.conversation.update(cx, |conversation, cx| {
@@ -2665,6 +2690,7 @@ enum InlineAssistantEvent {
struct InlineAssistant {
id: usize,
prompt_editor: ViewHandle,
+ workspace: WeakViewHandle,
confirmed: bool,
has_focus: bool,
include_conversation: bool,
@@ -2780,6 +2806,7 @@ impl InlineAssistant {
include_conversation: bool,
prompt_history: VecDeque,
codegen: ModelHandle,
+ workspace: WeakViewHandle,
cx: &mut ViewContext,
) -> Self {
let prompt_editor = cx.add_view(|cx| {
@@ -2801,6 +2828,7 @@ impl InlineAssistant {
Self {
id,
prompt_editor,
+ workspace,
confirmed: false,
has_focus: false,
include_conversation,
@@ -2859,6 +2887,8 @@ impl InlineAssistant {
if self.confirmed {
cx.emit(InlineAssistantEvent::Dismissed);
} else {
+ report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx);
+
let prompt = self.prompt_editor.read(cx).text(cx);
self.prompt_editor.update(cx, |editor, cx| {
editor.set_read_only(true);
@@ -3347,3 +3377,30 @@ mod tests {
.collect()
}
}
+
+fn report_assistant_event(
+ workspace: WeakViewHandle,
+ conversation_id: Option,
+ assistant_kind: AssistantKind,
+ cx: &AppContext,
+) {
+ let Some(workspace) = workspace.upgrade(cx) else {
+ return;
+ };
+
+ let client = workspace.read(cx).project().read(cx).client();
+ let telemetry = client.telemetry();
+
+ let model = settings::get::(cx)
+ .default_open_ai_model
+ .clone();
+
+ let event = ClickhouseEvent::Assistant {
+ conversation_id,
+ kind: assistant_kind,
+ model: model.full_name(),
+ };
+ let telemetry_settings = *settings::get::(cx);
+
+ telemetry.report_clickhouse_event(event, telemetry_settings)
+}
diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs
index cc7445dbcc..e7899ab2d8 100644
--- a/crates/call/src/room.rs
+++ b/crates/call/src/room.rs
@@ -172,7 +172,7 @@ impl Room {
cx.spawn(|this, mut cx| async move {
connect.await?;
- if !cx.read(|cx| settings::get::(cx).mute_on_join) {
+ if !cx.read(Self::mute_on_join) {
this.update(&mut cx, |this, cx| this.share_microphone(cx))
.await?;
}
@@ -301,6 +301,10 @@ impl Room {
})
}
+ pub fn mute_on_join(cx: &AppContext) -> bool {
+ settings::get::(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
+ }
+
fn from_join_response(
response: proto::JoinRoomResponse,
client: Arc,
@@ -1124,7 +1128,7 @@ impl Room {
self.live_kit
.as_ref()
.and_then(|live_kit| match &live_kit.microphone_track {
- LocalTrack::None => Some(settings::get::(cx).mute_on_join),
+ LocalTrack::None => Some(Self::mute_on_join(cx)),
LocalTrack::Pending { muted, .. } => Some(*muted),
LocalTrack::Published { muted, .. } => Some(*muted),
})
diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml
index c2191fdfa3..00e9135bc1 100644
--- a/crates/channel/Cargo.toml
+++ b/crates/channel/Cargo.toml
@@ -47,5 +47,6 @@ tempfile = "3"
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
+client = { path = "../client", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs
index 15631b7dd3..37f1c0ce44 100644
--- a/crates/channel/src/channel.rs
+++ b/crates/channel/src/channel.rs
@@ -1,14 +1,18 @@
+mod channel_buffer;
+mod channel_chat;
mod channel_store;
-pub mod channel_buffer;
-use std::sync::Arc;
+pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent};
+pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
+pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore};
-pub use channel_store::*;
use client::Client;
+use std::sync::Arc;
#[cfg(test)]
mod channel_store_tests;
pub fn init(client: &Arc) {
channel_buffer::init(client);
+ channel_chat::init(client);
}
diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs
index e11282cf79..06f9093fb5 100644
--- a/crates/channel/src/channel_buffer.rs
+++ b/crates/channel/src/channel_buffer.rs
@@ -23,13 +23,13 @@ pub struct ChannelBuffer {
subscription: Option,
}
-pub enum Event {
+pub enum ChannelBufferEvent {
CollaboratorsChanged,
Disconnected,
}
impl Entity for ChannelBuffer {
- type Event = Event;
+ type Event = ChannelBufferEvent;
fn release(&mut self, _: &mut AppContext) {
if self.connected {
@@ -101,7 +101,7 @@ impl ChannelBuffer {
}
}
self.collaborators = collaborators;
- cx.emit(Event::CollaboratorsChanged);
+ cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
}
@@ -141,7 +141,7 @@ impl ChannelBuffer {
this.update(&mut cx, |this, cx| {
this.collaborators.push(collaborator);
- cx.emit(Event::CollaboratorsChanged);
+ cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
});
@@ -165,7 +165,7 @@ impl ChannelBuffer {
true
}
});
- cx.emit(Event::CollaboratorsChanged);
+ cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
});
@@ -185,7 +185,7 @@ impl ChannelBuffer {
break;
}
}
- cx.emit(Event::CollaboratorsChanged);
+ cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
});
@@ -230,7 +230,7 @@ impl ChannelBuffer {
if self.connected {
self.connected = false;
self.subscription.take();
- cx.emit(Event::Disconnected);
+ cx.emit(ChannelBufferEvent::Disconnected);
cx.notify()
}
}
diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs
new file mode 100644
index 0000000000..8e03a3b6fd
--- /dev/null
+++ b/crates/channel/src/channel_chat.rs
@@ -0,0 +1,505 @@
+use crate::Channel;
+use anyhow::{anyhow, Result};
+use client::{
+ proto,
+ user::{User, UserStore},
+ Client, Subscription, TypedEnvelope,
+};
+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 sum_tree::{Bias, SumTree};
+use time::OffsetDateTime;
+use util::{post_inc, ResultExt as _, TryFutureExt};
+
+pub struct ChannelChat {
+ channel: Arc,
+ messages: SumTree,
+ loaded_all_messages: bool,
+ next_pending_message_id: usize,
+ user_store: ModelHandle,
+ rpc: Arc,
+ outgoing_messages_lock: Arc>,
+ rng: StdRng,
+ _subscription: Subscription,
+}
+
+#[derive(Clone, Debug)]
+pub struct ChannelMessage {
+ pub id: ChannelMessageId,
+ pub body: String,
+ pub timestamp: OffsetDateTime,
+ pub sender: Arc,
+ pub nonce: u128,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum ChannelMessageId {
+ Saved(u64),
+ Pending(usize),
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct ChannelMessageSummary {
+ max_id: ChannelMessageId,
+ count: usize,
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct Count(usize);
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum ChannelChatEvent {
+ MessagesUpdated {
+ old_range: Range,
+ new_count: usize,
+ },
+}
+
+pub fn init(client: &Arc) {
+ client.add_model_message_handler(ChannelChat::handle_message_sent);
+ client.add_model_message_handler(ChannelChat::handle_message_removed);
+}
+
+impl Entity for ChannelChat {
+ type Event = ChannelChatEvent;
+
+ fn release(&mut self, _: &mut AppContext) {
+ self.rpc
+ .send(proto::LeaveChannelChat {
+ channel_id: self.channel.id,
+ })
+ .log_err();
+ }
+}
+
+impl ChannelChat {
+ pub async fn new(
+ channel: Arc,
+ user_store: ModelHandle,
+ client: Arc,
+ mut cx: AsyncAppContext,
+ ) -> Result> {
+ let channel_id = channel.id;
+ let subscription = client.subscribe_to_entity(channel_id).unwrap();
+
+ let response = client
+ .request(proto::JoinChannelChat { channel_id })
+ .await?;
+ let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
+ let loaded_all_messages = response.done;
+
+ Ok(cx.add_model(|cx| {
+ let mut this = Self {
+ channel,
+ user_store,
+ rpc: client,
+ outgoing_messages_lock: Default::default(),
+ messages: Default::default(),
+ loaded_all_messages,
+ next_pending_message_id: 0,
+ rng: StdRng::from_entropy(),
+ _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
+ };
+ this.insert_messages(messages, cx);
+ this
+ }))
+ }
+
+ pub fn channel(&self) -> &Arc {
+ &self.channel
+ }
+
+ pub fn send_message(
+ &mut self,
+ body: String,
+ cx: &mut ModelContext,
+ ) -> Result>> {
+ if body.is_empty() {
+ Err(anyhow!("message body can't be empty"))?;
+ }
+
+ let current_user = self
+ .user_store
+ .read(cx)
+ .current_user()
+ .ok_or_else(|| anyhow!("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.gen();
+ self.insert_messages(
+ SumTree::from_item(
+ ChannelMessage {
+ id: pending_id,
+ body: body.clone(),
+ sender: current_user,
+ timestamp: OffsetDateTime::now_utc(),
+ nonce,
+ },
+ &(),
+ ),
+ cx,
+ );
+ let user_store = self.user_store.clone();
+ let rpc = self.rpc.clone();
+ let outgoing_messages_lock = self.outgoing_messages_lock.clone();
+ Ok(cx.spawn(|this, mut cx| async move {
+ let outgoing_message_guard = outgoing_messages_lock.lock().await;
+ let request = rpc.request(proto::SendChannelMessage {
+ channel_id,
+ body,
+ nonce: Some(nonce.into()),
+ });
+ 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?;
+ this.update(&mut cx, |this, cx| {
+ this.insert_messages(SumTree::from_item(message, &()), cx);
+ Ok(())
+ })
+ }))
+ }
+
+ pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext) -> Task> {
+ let response = self.rpc.request(proto::RemoveChannelMessage {
+ channel_id: self.channel.id,
+ message_id: id,
+ });
+ cx.spawn(|this, mut cx| async move {
+ response.await?;
+
+ this.update(&mut cx, |this, cx| {
+ this.message_removed(id, cx);
+ Ok(())
+ })
+ })
+ }
+
+ 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(())
+ }
+ .log_err()
+ })
+ .detach();
+ return true;
+ }
+ }
+ false
+ }
+
+ pub fn rejoin(&mut self, cx: &mut ModelContext) {
+ let user_store = self.user_store.clone();
+ let rpc = self.rpc.clone();
+ let channel_id = self.channel.id;
+ cx.spawn(|this, mut cx| {
+ async move {
+ let response = rpc.request(proto::JoinChannelChat { channel_id }).await?;
+ let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
+ let loaded_all_messages = response.done;
+
+ let pending_messages = this.update(&mut cx, |this, cx| {
+ if let Some((first_new_message, last_old_message)) =
+ messages.first().zip(this.messages.last())
+ {
+ if first_new_message.id > last_old_message.id {
+ let old_messages = mem::take(&mut this.messages);
+ cx.emit(ChannelChatEvent::MessagesUpdated {
+ old_range: 0..old_messages.summary().count,
+ new_count: 0,
+ });
+ this.loaded_all_messages = loaded_all_messages;
+ }
+ }
+
+ this.insert_messages(messages, cx);
+ if loaded_all_messages {
+ this.loaded_all_messages = loaded_all_messages;
+ }
+
+ this.pending_messages().cloned().collect::>()
+ });
+
+ for pending_message in pending_messages {
+ let request = rpc.request(proto::SendChannelMessage {
+ channel_id,
+ body: pending_message.body,
+ nonce: Some(pending_message.nonce.into()),
+ });
+ let response = request.await?;
+ let message = ChannelMessage::from_proto(
+ response.message.ok_or_else(|| anyhow!("invalid message"))?,
+ &user_store,
+ &mut cx,
+ )
+ .await?;
+ this.update(&mut cx, |this, cx| {
+ this.insert_messages(SumTree::from_item(message, &()), cx);
+ });
+ }
+
+ anyhow::Ok(())
+ }
+ .log_err()
+ })
+ .detach();
+ }
+
+ pub fn message_count(&self) -> usize {
+ self.messages.summary().count
+ }
+
+ pub fn messages(&self) -> &SumTree {
+ &self.messages
+ }
+
+ pub fn message(&self, ix: usize) -> &ChannelMessage {
+ let mut cursor = self.messages.cursor::();
+ cursor.seek(&Count(ix), Bias::Right, &());
+ cursor.item().unwrap()
+ }
+
+ pub fn messages_in_range(&self, range: Range) -> impl Iterator- {
+ let mut cursor = self.messages.cursor::();
+ cursor.seek(&Count(range.start), Bias::Right, &());
+ cursor.take(range.len())
+ }
+
+ pub fn pending_messages(&self) -> impl Iterator
- {
+ let mut cursor = self.messages.cursor::();
+ cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &());
+ cursor
+ }
+
+ async fn handle_message_sent(
+ this: ModelHandle,
+ message: TypedEnvelope,
+ _: Arc,
+ mut cx: AsyncAppContext,
+ ) -> Result<()> {
+ let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
+ let message = message
+ .payload
+ .message
+ .ok_or_else(|| anyhow!("empty message"))?;
+
+ 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)
+ });
+
+ Ok(())
+ }
+
+ async fn handle_message_removed(
+ this: ModelHandle,
+ message: TypedEnvelope,
+ _: Arc,
+ mut cx: AsyncAppContext,
+ ) -> Result<()> {
+ this.update(&mut cx, |this, cx| {
+ this.message_removed(message.payload.message_id, cx)
+ });
+ Ok(())
+ }
+
+ fn insert_messages(&mut self, messages: SumTree, cx: &mut ModelContext) {
+ if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
+ let nonces = messages
+ .cursor::<()>()
+ .map(|m| m.nonce)
+ .collect::>();
+
+ let mut old_cursor = self.messages.cursor::<(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::>::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().map_or(false, |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 ModelContext) {
+ let mut cursor = self.messages.cursor::();
+ let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &());
+ if let Some(item) = cursor.item() {
+ if item.id == ChannelMessageId::Saved(id) {
+ let ix = messages.summary().count;
+ cursor.next(&());
+ messages.append(cursor.suffix(&()), &());
+ drop(cursor);
+ self.messages = messages;
+ cx.emit(ChannelChatEvent::MessagesUpdated {
+ old_range: ix..ix + 1,
+ new_count: 0,
+ });
+ }
+ }
+ }
+}
+
+async fn messages_from_proto(
+ 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?);
+ }
+ let mut result = SumTree::new();
+ result.extend(messages, &());
+ Ok(result)
+}
+
+impl ChannelMessage {
+ pub async fn from_proto(
+ message: proto::ChannelMessage,
+ user_store: &ModelHandle,
+ cx: &mut AsyncAppContext,
+ ) -> Result {
+ let sender = user_store
+ .update(cx, |user_store, cx| {
+ user_store.get_user(message.sender_id, cx)
+ })
+ .await?;
+ Ok(ChannelMessage {
+ id: ChannelMessageId::Saved(message.id),
+ body: message.body,
+ timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
+ sender,
+ nonce: message
+ .nonce
+ .ok_or_else(|| anyhow!("nonce is required"))?
+ .into(),
+ })
+ }
+
+ pub fn is_pending(&self) -> bool {
+ matches!(self.id, ChannelMessageId::Pending(_))
+ }
+}
+
+impl sum_tree::Item for ChannelMessage {
+ type Summary = ChannelMessageSummary;
+
+ fn summary(&self) -> 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 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 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 add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
+ self.0 += summary.count;
+ }
+}
diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs
index a4c8da6df4..e61e520b47 100644
--- a/crates/channel/src/channel_store.rs
+++ b/crates/channel/src/channel_store.rs
@@ -1,4 +1,4 @@
-use crate::channel_buffer::ChannelBuffer;
+use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
use anyhow::{anyhow, Result};
use client::{Client, Subscription, User, UserId, UserStore};
use collections::{hash_map, HashMap, HashSet};
@@ -20,7 +20,8 @@ pub struct ChannelStore {
channels_with_admin_privileges: HashSet,
outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender,
- opened_buffers: HashMap,
+ opened_buffers: HashMap>,
+ opened_chats: HashMap>,
client: Arc,
user_store: ModelHandle,
_rpc_subscription: Subscription,
@@ -50,15 +51,9 @@ impl Entity for ChannelStore {
type Event = ChannelEvent;
}
-pub enum ChannelMemberStatus {
- Invited,
- Member,
- NotMember,
-}
-
-enum OpenedChannelBuffer {
- Open(WeakModelHandle),
- Loading(Shared, Arc>>>),
+enum OpenedModelHandle {
+ Open(WeakModelHandle),
+ Loading(Shared, Arc>>>),
}
impl ChannelStore {
@@ -94,6 +89,7 @@ impl ChannelStore {
channels_with_admin_privileges: Default::default(),
outgoing_invites: Default::default(),
opened_buffers: Default::default(),
+ opened_chats: Default::default(),
update_channels_tx,
client,
user_store,
@@ -115,6 +111,10 @@ impl ChannelStore {
}
}
+ pub fn client(&self) -> Arc {
+ self.client.clone()
+ }
+
pub fn has_children(&self, channel_id: ChannelId) -> bool {
self.channel_paths.iter().any(|path| {
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
@@ -129,6 +129,12 @@ impl ChannelStore {
self.channel_paths.len()
}
+ pub fn index_of_channel(&self, channel_id: ChannelId) -> Option {
+ self.channel_paths
+ .iter()
+ .position(|path| path.ends_with(&[channel_id]))
+ }
+
pub fn channels(&self) -> impl '_ + Iterator
- )> {
self.channel_paths.iter().map(move |path| {
let id = path.last().unwrap();
@@ -154,7 +160,7 @@ impl ChannelStore {
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, cx: &AppContext) -> bool {
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
- if let OpenedChannelBuffer::Open(buffer) = buffer {
+ if let OpenedModelHandle::Open(buffer) = buffer {
return buffer.upgrade(cx).is_some();
}
}
@@ -166,24 +172,62 @@ impl ChannelStore {
channel_id: ChannelId,
cx: &mut ModelContext,
) -> Task>> {
- // Make sure that a given channel buffer is only opened once per
- // app instance, even if this method is called multiple times
- // with the same channel id while the first task is still running.
+ let client = self.client.clone();
+ self.open_channel_resource(
+ channel_id,
+ |this| &mut this.opened_buffers,
+ |channel, cx| ChannelBuffer::new(channel, client, cx),
+ cx,
+ )
+ }
+
+ pub fn open_channel_chat(
+ &mut self,
+ channel_id: ChannelId,
+ cx: &mut ModelContext,
+ ) -> Task>> {
+ let client = self.client.clone();
+ let user_store = self.user_store.clone();
+ self.open_channel_resource(
+ channel_id,
+ |this| &mut this.opened_chats,
+ |channel, cx| ChannelChat::new(channel, user_store, client, cx),
+ cx,
+ )
+ }
+
+ /// Asynchronously open a given resource associated with a channel.
+ ///
+ /// Make sure that the resource is only opened once, even if this method
+ /// is called multiple times with the same channel id while the first task
+ /// is still running.
+ fn open_channel_resource(
+ &mut self,
+ channel_id: ChannelId,
+ get_map: fn(&mut Self) -> &mut HashMap>,
+ load: F,
+ cx: &mut ModelContext,
+ ) -> Task>>
+ where
+ F: 'static + FnOnce(Arc, AsyncAppContext) -> Fut,
+ Fut: Future