Detailed changes
@@ -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",
@@ -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,
@@ -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?;
@@ -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
@@ -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());
}
@@ -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,
- }
- }
-}
@@ -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);
@@ -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);
@@ -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)]
@@ -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;
@@ -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,
})
}
@@ -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(¬ification.recipient_id.to_proto()) {
- if let Some(notification) =
- self::notifications::model_to_proto(self, notification).log_err()
- {
- updated_mention_notifications.push(notification);
- }
- } else {
- deleted_notification_ids.insert(notification.id);
- }
- }
-
- let mut notifications = Vec::new();
- for mentioned_user in new_mention_user_ids {
- notifications.extend(
- self.create_notification(
- UserId::from_proto(mentioned_user),
- rpc::Notification::ChannelMessageMention {
- message_id: message_id.to_proto(),
- sender_id: user_id.to_proto(),
- channel_id: channel_id.to_proto(),
- },
- false,
- &tx,
- )
- .await?,
- );
- }
-
- Ok(UpdatedChannelMessage {
- message_id,
- participant_connection_ids,
- notifications,
- reply_to_message_id: channel_message.reply_to_message_id,
- timestamp: channel_message.sent_at,
- deleted_mention_notification_ids: deleted_notification_ids
- .into_iter()
- .collect::<Vec<_>>(),
- updated_mention_notifications,
- })
- })
- .await
- }
-}
@@ -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
@@ -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,
- }
-}
@@ -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
@@ -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())]),
- ),
- ]
- );
-}
@@ -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
@@ -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;
@@ -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);
- });
-}
@@ -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
@@ -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()
- ),
- ]
- );
- }
-}
@@ -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()
- },
- ))
- }
-}
@@ -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;
@@ -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);
@@ -1,4 +1,4 @@
-use crate::{NotificationPanelSettings, chat_panel::ChatPanel};
+use crate::NotificationPanelSettings;
use anyhow::Result;
use channel::ChannelStore;
use client::{ChannelId, Client, Notification, User, UserStore};
@@ -6,8 +6,8 @@ use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use futures::StreamExt;
use gpui::{
- AnyElement, App, AsyncWindowContext, ClickEvent, Context, CursorStyle, DismissEvent, Element,
- Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment,
+ AnyElement, App, AsyncWindowContext, ClickEvent, Context, DismissEvent, Element, Entity,
+ EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment,
ListScrollEvent, ListState, ParentElement, Render, StatefulInteractiveElement, Styled, Task,
WeakEntity, Window, actions, div, img, list, px,
};
@@ -71,7 +71,6 @@ pub struct NotificationPresenter {
pub text: String,
pub icon: &'static str,
pub needs_response: bool,
- pub can_navigate: bool,
}
actions!(
@@ -234,7 +233,6 @@ impl NotificationPanel {
actor,
text,
needs_response,
- can_navigate,
..
} = self.present_notification(entry, cx)?;
@@ -269,14 +267,6 @@ impl NotificationPanel {
.py_1()
.gap_2()
.hover(|style| style.bg(cx.theme().colors().element_hover))
- .when(can_navigate, |el| {
- el.cursor(CursorStyle::PointingHand).on_click({
- let notification = notification.clone();
- cx.listener(move |this, _, window, cx| {
- this.did_click_notification(¬ification, window, cx)
- })
- })
- })
.children(actor.map(|actor| {
img(actor.avatar_uri.clone())
.flex_none()
@@ -369,7 +359,6 @@ impl NotificationPanel {
text: format!("{} wants to add you as a contact", requester.github_login),
needs_response: user_store.has_incoming_contact_request(requester.id),
actor: Some(requester),
- can_navigate: false,
})
}
Notification::ContactRequestAccepted { responder_id } => {
@@ -379,7 +368,6 @@ impl NotificationPanel {
text: format!("{} accepted your contact invite", responder.github_login),
needs_response: false,
actor: Some(responder),
- can_navigate: false,
})
}
Notification::ChannelInvitation {
@@ -396,29 +384,6 @@ impl NotificationPanel {
),
needs_response: channel_store.has_channel_invitation(ChannelId(channel_id)),
actor: Some(inviter),
- can_navigate: false,
- })
- }
- Notification::ChannelMessageMention {
- sender_id,
- channel_id,
- message_id,
- } => {
- let sender = user_store.get_cached_user(sender_id)?;
- let channel = channel_store.channel_for_id(ChannelId(channel_id))?;
- let message = self
- .notification_store
- .read(cx)
- .channel_message_for_id(message_id)?;
- Some(NotificationPresenter {
- icon: "icons/conversations.svg",
- text: format!(
- "{} mentioned you in #{}:\n{}",
- sender.github_login, channel.name, message.body,
- ),
- needs_response: false,
- actor: Some(sender),
- can_navigate: true,
})
}
}
@@ -433,9 +398,7 @@ impl NotificationPanel {
) {
let should_mark_as_read = match notification {
Notification::ContactRequestAccepted { .. } => true,
- Notification::ContactRequest { .. }
- | Notification::ChannelInvitation { .. }
- | Notification::ChannelMessageMention { .. } => false,
+ Notification::ContactRequest { .. } | Notification::ChannelInvitation { .. } => false,
};
if should_mark_as_read {
@@ -457,55 +420,6 @@ impl NotificationPanel {
}
}
- fn did_click_notification(
- &mut self,
- notification: &Notification,
- window: &mut Window,
- cx: &mut Context<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, .. } = ¬ification
- && 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();
})
@@ -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;
@@ -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
@@ -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(¬ification)
- {
- let fetch_message_task = this.channel_store.update(cx, |this, cx| {
- this.fetch_channel_messages(vec![message_id], cx)
- });
-
- cx.spawn(async move |this, cx| {
- let messages = fetch_message_task.await?;
- this.update(cx, move |this, cx| {
- for message in messages {
- this.channel_messages.insert(message_id, message);
- }
- cx.notify();
- })
- })
- .detach_and_log_err(cx)
- }
- Ok(())
- })?
- }
-
async fn add_notifications(
this: Entity<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()
@@ -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 {
@@ -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();
@@ -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"),
@@ -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",
@@ -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
@@ -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.