From 6b2798015b6db87182e8c14b5f20b79808d335d5 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sat, 16 Nov 2024 03:52:00 -0500 Subject: [PATCH 1/3] Message Grouping --- src/chat/src/message.rs | 3 +- src/discord/src/client.rs | 2 + src/discord/src/message/author.rs | 12 ++++++ src/ui/src/channel/message.rs | 56 ++++++++++++++++++++++++-- src/ui/src/channel/message_list.rs | 63 ++++++++++++++++++------------ 5 files changed, 108 insertions(+), 28 deletions(-) diff --git a/src/chat/src/message.rs b/src/chat/src/message.rs index 7cf0a9e..9756922 100644 --- a/src/chat/src/message.rs +++ b/src/chat/src/message.rs @@ -7,7 +7,8 @@ pub trait Message: Clone { fn get_nonce(&self) -> Option<&String>; } -pub trait MessageAuthor { +pub trait MessageAuthor: PartialEq + Eq { fn get_display_name(&self) -> impl Element; fn get_icon(&self) -> String; + fn get_id(&self) -> String; } diff --git a/src/discord/src/client.rs b/src/discord/src/client.rs index c232f75..bd12ea4 100644 --- a/src/discord/src/client.rs +++ b/src/discord/src/client.rs @@ -87,6 +87,7 @@ impl RawEventHandler for RawClient { user.get("id").unwrap().as_str().unwrap(), user.get("avatar").unwrap().as_str().unwrap() ), + id: user.get("id").unwrap().as_str().unwrap().to_owned(), }); } } @@ -107,6 +108,7 @@ impl EventHandler for DiscordClient { author: DiscordMessageAuthor { display_name: DisplayName(msg.author.name.clone()), icon: msg.author.avatar_url().unwrap_or(msg.author.default_avatar_url()), + id: msg.author.id.to_string(), }, content: DiscordMessageContent { content: msg.content.clone(), diff --git a/src/discord/src/message/author.rs b/src/discord/src/message/author.rs index 6cd8ec1..ed1b234 100644 --- a/src/discord/src/message/author.rs +++ b/src/discord/src/message/author.rs @@ -5,8 +5,16 @@ use scope_chat::message::MessageAuthor; pub struct DiscordMessageAuthor { pub display_name: DisplayName, pub icon: String, + pub id: String, } +impl PartialEq for DiscordMessageAuthor { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} +impl Eq for DiscordMessageAuthor {} + impl MessageAuthor for DiscordMessageAuthor { fn get_display_name(&self) -> impl Element { self.display_name.clone().into_element() @@ -15,6 +23,10 @@ impl MessageAuthor for DiscordMessageAuthor { fn get_icon(&self) -> String { self.icon.clone() } + + fn get_id(&self) -> String { + self.id.clone() + } } #[derive(Clone, IntoElement)] diff --git a/src/ui/src/channel/message.rs b/src/ui/src/channel/message.rs index 5b6afda..5bfe97f 100644 --- a/src/ui/src/channel/message.rs +++ b/src/ui/src/channel/message.rs @@ -1,7 +1,57 @@ -use gpui::{div, img, rgb, IntoElement, ParentElement, Styled}; +use gpui::{div, img, rgb, Element, IntoElement, ParentElement, Styled}; use scope_chat::message::{Message, MessageAuthor}; -pub fn message(message: impl Message) -> impl IntoElement { +#[derive(Clone)] +pub struct MessageGroup { + contents: Vec, +} + +impl MessageGroup { + pub fn new(message: M) -> MessageGroup { + MessageGroup { contents: vec![message] } + } + + pub fn get_author<'s>(&'s self) -> &'s (impl MessageAuthor + 's) { + self.contents.get(0).unwrap().get_author() + } + + pub fn add(&mut self, message: M) { + // FIXME: This is scuffed, should be using PartialEq trait. + if self.get_author().get_id() != message.get_author().get_id() { + panic!("Authors must match in a message group") + } + + self.contents.push(message); + } + + pub fn contents<'s>(&'s self) -> impl IntoIterator { + self.contents.iter().map(|v| v.get_content()) + } + + pub fn find_matching(&self, nonce: &String) -> Option { + for haystack in self.contents.iter().zip(0usize..) { + if haystack.0.get_nonce().is_some() && haystack.0.get_nonce().unwrap() == nonce { + return Some(haystack.1); + } + } + + return None; + } + + pub fn size(&self) -> usize { + self.contents.len() + } + + pub fn remove(&mut self, index: usize) { + if self.size() == 1 { + panic!("Cannot remove such that it would leave the group empty."); + } + + self.contents.remove(index); + } +} + +pub fn message(message: MessageGroup) -> impl IntoElement { div() .flex() .flex_row() @@ -9,5 +59,5 @@ pub fn message(message: impl Message) -> impl IntoElement { .gap_2() .p_2() .child(img(message.get_author().get_icon()).object_fit(gpui::ObjectFit::Fill).bg(rgb(0xFFFFFF)).rounded_full().w_12().h_12()) - .child(div().flex().flex_col().child(message.get_author().get_display_name()).child(message.get_content())) + .child(div().flex().flex_col().child(message.get_author().get_display_name()).child(div().children(message.contents()))) } diff --git a/src/ui/src/channel/message_list.rs b/src/ui/src/channel/message_list.rs index 2efb7ef..ce86d3a 100644 --- a/src/ui/src/channel/message_list.rs +++ b/src/ui/src/channel/message_list.rs @@ -1,50 +1,65 @@ use gpui::{div, IntoElement, ListAlignment, ListState, ParentElement, Pixels}; -use scope_chat::message::Message; +use scope_chat::message::{Message, MessageAuthor}; -use super::message::message; +use super::message::{message, MessageGroup}; #[derive(Clone)] pub struct MessageList { - real_messages: Vec, - pending_messages: Vec, + messages: Vec>, } impl MessageList { pub fn new() -> MessageList { - Self { - real_messages: Vec::default(), - pending_messages: Vec::default(), - } + Self { messages: Vec::default() } } pub fn add_external_message(&mut self, message: M) { - if let Some((_, pending_index)) = self - .pending_messages - .iter() - .zip(0..) - .find(|(msg, _)| msg.get_nonce().map(|v1| message.get_nonce().map(|v2| v2 == v1).unwrap_or(false)).unwrap_or(false)) - { - self.pending_messages.remove(pending_index); + if let Some(nonce) = message.get_nonce() { + let mut removal_index: Option = None; + + for (group, index) in self.messages.iter_mut().zip(0..) { + let matching = group.find_matching(nonce); + + if let Some(matching) = matching { + if group.size() == 1 { + removal_index = Some(index); + } else { + group.remove(matching); + } + } + } + + if let Some(removal_index) = removal_index { + self.messages.remove(removal_index); + } } - self.real_messages.push(message); + let last = self.messages.last_mut(); + + if last.is_some() && last.as_ref().unwrap().get_author().get_id() == message.get_author().get_id() { + last.unwrap().add(message); + } else { + self.messages.push(MessageGroup::new(message)); + } } pub fn add_pending_message(&mut self, pending_message: M) { - self.pending_messages.push(pending_message); + let last = self.messages.last_mut(); + + if last.is_some() && last.as_ref().unwrap().get_author().get_id() == pending_message.get_author().get_id() { + last.unwrap().add(pending_message); + } else { + self.messages.push(MessageGroup::new(pending_message)); + } } pub fn length(&self) -> usize { - self.real_messages.len() + self.pending_messages.len() + self.messages.len() } - pub fn get(&self, index: usize) -> Option<&M> { - if index >= self.real_messages.len() { - self.pending_messages.get(index - self.real_messages.len()) - } else { - self.real_messages.get(index) - } + pub fn get(&self, index: usize) -> Option<&MessageGroup> { + self.messages.get(index) } pub fn create_list_state(&self) -> ListState { From 4eef5d860895982e10bd6cc9de0ef28852ad8482 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sat, 16 Nov 2024 03:54:52 -0500 Subject: [PATCH 2/3] get rid of all the warnings --- src/discord/src/channel/mod.rs | 8 ++------ src/discord/src/client.rs | 10 +++------- src/discord/src/message/content.rs | 2 +- src/ui/src/channel/mod.rs | 13 +++---------- src/ui/src/main.rs | 8 +++----- 5 files changed, 12 insertions(+), 29 deletions(-) diff --git a/src/discord/src/channel/mod.rs b/src/discord/src/channel/mod.rs index 7a4359f..320e4c4 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -1,15 +1,11 @@ use std::sync::Arc; use scope_chat::channel::Channel; -use tokio::sync::{broadcast, RwLock}; +use tokio::sync::broadcast; use crate::{ client::DiscordClient, - message::{ - author::{DiscordMessageAuthor, DisplayName}, - content::DiscordMessageContent, - DiscordMessage, - }, + message::{content::DiscordMessageContent, DiscordMessage}, snowflake::Snowflake, }; diff --git a/src/discord/src/client.rs b/src/discord/src/client.rs index bd12ea4..8c414ee 100644 --- a/src/discord/src/client.rs +++ b/src/discord/src/client.rs @@ -1,17 +1,13 @@ use std::{ collections::HashMap, - fs::File, - rc::Rc, sync::{Arc, OnceLock}, }; use serenity::{ all::{ChannelId, Context, CreateMessage, Event, EventHandler, GatewayIntents, Message, Nonce, RawEventHandler}, async_trait, - futures::SinkExt, }; -use std::io::Write; -use tokio::sync::{broadcast, Mutex, RwLock}; +use tokio::sync::{broadcast, RwLock}; use crate::{ message::{ @@ -19,7 +15,7 @@ use crate::{ content::DiscordMessageContent, DiscordMessage, }, - snowflake::{self, Snowflake}, + snowflake::Snowflake, }; #[derive(Default)] @@ -75,7 +71,7 @@ struct RawClient(Arc); #[async_trait] impl RawEventHandler for RawClient { - async fn raw_event(&self, ctx: Context, ev: serenity::model::prelude::Event) { + async fn raw_event(&self, _: Context, ev: serenity::model::prelude::Event) { if let Event::Unknown(unk) = ev { if unk.kind == "READY" { let user = unk.value.as_object().unwrap().get("user").unwrap().as_object().unwrap(); diff --git a/src/discord/src/message/content.rs b/src/discord/src/message/content.rs index 2289177..c22ba17 100644 --- a/src/discord/src/message/content.rs +++ b/src/discord/src/message/content.rs @@ -1,4 +1,4 @@ -use gpui::{div, IntoElement, ParentElement, Render, RenderOnce, Styled, WindowContext}; +use gpui::{div, IntoElement, ParentElement, RenderOnce, Styled, WindowContext}; #[derive(Clone, IntoElement)] pub struct DiscordMessageContent { diff --git a/src/ui/src/channel/mod.rs b/src/ui/src/channel/mod.rs index 050be6a..55b73fc 100644 --- a/src/ui/src/channel/mod.rs +++ b/src/ui/src/channel/mod.rs @@ -1,16 +1,9 @@ pub mod message; pub mod message_list; -use std::ops::Deref; - use components::input::{InputEvent, TextInput}; -use gpui::{ - div, list, rgb, AppContext, Context, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, SharedString, Styled, View, - VisualContext, -}; -use message::message; +use gpui::{div, list, Context, ListState, Model, ParentElement, Render, Styled, View, VisualContext}; use message_list::MessageList; -use scope_backend_discord::message::DiscordMessage; use scope_chat::{channel::Channel, message::Message}; pub struct ChannelView { @@ -20,7 +13,7 @@ pub struct ChannelView { } impl ChannelView { - pub fn create(ctx: &mut gpui::WindowContext, mut channel: impl Channel + 'static) -> View { + pub fn create(ctx: &mut gpui::WindowContext, channel: impl Channel + 'static) -> View { let view = ctx.new_view(|ctx| { let state_model = ctx.new_model(|_cx| MessageList::::new()); @@ -94,7 +87,7 @@ impl ChannelView { } impl Render for ChannelView { - fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { + fn render(&mut self, _: &mut gpui::ViewContext) -> impl gpui::IntoElement { div().flex().flex_col().w_full().h_full().child(list(self.list_state.clone()).w_full().h_full()).child(self.message_input.clone()) } } diff --git a/src/ui/src/main.rs b/src/ui/src/main.rs index 6304ae1..64c21c1 100644 --- a/src/ui/src/main.rs +++ b/src/ui/src/main.rs @@ -8,8 +8,6 @@ use channel::ChannelView; use components::theme::Theme; use gpui::*; use scope_backend_discord::{channel::DiscordChannel, client::DiscordClient, message::DiscordMessage, snowflake::Snowflake}; -use scope_chat::channel::Channel; -use scope_util::ResultExt; struct Assets { base: PathBuf, @@ -29,7 +27,7 @@ impl AssetSource for Assets { actions!(main_menu, [Quit]); -fn init(app_state: Arc, cx: &mut AppContext) -> Result<()> { +fn init(_: Arc, cx: &mut AppContext) -> Result<()> { components::init(cx); cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); @@ -46,7 +44,7 @@ async fn main() { let token = dotenv::var("DISCORD_TOKEN").expect("Must provide DISCORD_TOKEN in .env"); let demo_channel_id = dotenv::var("DEMO_CHANNEL_ID").expect("Must provide DEMO_CHANNEL_ID in .env"); - let mut client = DiscordClient::new(token).await; + let client = DiscordClient::new(token).await; let channel = DiscordChannel::new( client.clone(), @@ -67,7 +65,7 @@ async fn main() { Theme::sync_system_appearance(cx); - let window = cx.open_window(WindowOptions::default(), |cx| ChannelView::::create(cx, channel)).unwrap(); + cx.open_window(WindowOptions::default(), |cx| ChannelView::::create(cx, channel)).unwrap(); }, ); } From 9026478c409e5e3853e9409e149727594deb7a3b Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sat, 16 Nov 2024 11:40:17 -0500 Subject: [PATCH 3/3] added grouping gap --- src/chat/src/message.rs | 1 + src/discord/src/channel/mod.rs | 2 ++ src/discord/src/client.rs | 1 + src/discord/src/message/mod.rs | 9 +++++++++ src/ui/src/channel/message.rs | 4 ++++ src/ui/src/channel/message_list.rs | 10 ++++++++-- 6 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/chat/src/message.rs b/src/chat/src/message.rs index 9756922..ecaccc0 100644 --- a/src/chat/src/message.rs +++ b/src/chat/src/message.rs @@ -5,6 +5,7 @@ pub trait Message: Clone { fn get_content(&self) -> impl Element; fn get_identifier(&self) -> String; fn get_nonce(&self) -> Option<&String>; + fn should_group(&self, previous: &Self) -> bool; } pub trait MessageAuthor: PartialEq + Eq { diff --git a/src/discord/src/channel/mod.rs b/src/discord/src/channel/mod.rs index 320e4c4..3c17de7 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use scope_chat::channel::Channel; +use serenity::all::Timestamp; use tokio::sync::broadcast; use crate::{ @@ -51,6 +52,7 @@ impl Channel for DiscordChannel { author: self.client.user().clone(), id: Snowflake { content: 0 }, nonce: Some(nonce), + creation_time: Timestamp::now(), } } } diff --git a/src/discord/src/client.rs b/src/discord/src/client.rs index 8c414ee..177392f 100644 --- a/src/discord/src/client.rs +++ b/src/discord/src/client.rs @@ -114,6 +114,7 @@ impl EventHandler for DiscordClient { Nonce::Number(n) => n.to_string(), Nonce::String(s) => s, }), + creation_time: msg.timestamp, }); } } diff --git a/src/discord/src/message/mod.rs b/src/discord/src/message/mod.rs index 650a1a4..231fba4 100644 --- a/src/discord/src/message/mod.rs +++ b/src/discord/src/message/mod.rs @@ -1,3 +1,5 @@ +use std::time::Instant; + use author::DiscordMessageAuthor; use content::DiscordMessageContent; use gpui::{Element, IntoElement}; @@ -14,6 +16,7 @@ pub struct DiscordMessage { pub author: DiscordMessageAuthor, pub id: Snowflake, pub nonce: Option, + pub creation_time: serenity::model::Timestamp, } impl Message for DiscordMessage { @@ -32,4 +35,10 @@ impl Message for DiscordMessage { fn get_nonce(&self) -> Option<&String> { self.nonce.as_ref() } + + fn should_group(&self, previous: &Self) -> bool { + const MAX_DISCORD_MESSAGE_GAP_SECS_FOR_GROUP: i64 = 5 * 60; + + self.creation_time.signed_duration_since(&*previous.creation_time).num_seconds() <= MAX_DISCORD_MESSAGE_GAP_SECS_FOR_GROUP + } } diff --git a/src/ui/src/channel/message.rs b/src/ui/src/channel/message.rs index 5bfe97f..e84de0d 100644 --- a/src/ui/src/channel/message.rs +++ b/src/ui/src/channel/message.rs @@ -49,6 +49,10 @@ impl MessageGroup { self.contents.remove(index); } + + pub fn last(&self) -> &M { + self.contents.last().unwrap() + } } pub fn message(message: MessageGroup) -> impl IntoElement { diff --git a/src/ui/src/channel/message_list.rs b/src/ui/src/channel/message_list.rs index ce86d3a..6148d56 100644 --- a/src/ui/src/channel/message_list.rs +++ b/src/ui/src/channel/message_list.rs @@ -37,7 +37,10 @@ impl MessageList { let last = self.messages.last_mut(); - if last.is_some() && last.as_ref().unwrap().get_author().get_id() == message.get_author().get_id() { + if last.is_some() + && last.as_ref().unwrap().get_author().get_id() == message.get_author().get_id() + && message.should_group(last.as_ref().unwrap().last()) + { last.unwrap().add(message); } else { self.messages.push(MessageGroup::new(message)); @@ -47,7 +50,10 @@ impl MessageList { pub fn add_pending_message(&mut self, pending_message: M) { let last = self.messages.last_mut(); - if last.is_some() && last.as_ref().unwrap().get_author().get_id() == pending_message.get_author().get_id() { + if last.is_some() + && last.as_ref().unwrap().get_author().get_id() == pending_message.get_author().get_id() + && pending_message.should_group(last.as_ref().unwrap().last()) + { last.unwrap().add(pending_message); } else { self.messages.push(MessageGroup::new(pending_message));