Skip to content

Commit c5b9545

Browse files
authored
Merge pull request #11 from roobscoob/message-grouping
Message Grouping
2 parents 415c6e5 + 473cef6 commit c5b9545

File tree

10 files changed

+142
-57
lines changed

10 files changed

+142
-57
lines changed

src/chat/src/message.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ pub trait Message: Clone {
55
fn get_content(&self) -> impl Element;
66
fn get_identifier(&self) -> String;
77
fn get_nonce(&self) -> Option<&String>;
8+
fn should_group(&self, previous: &Self) -> bool;
89
}
910

10-
pub trait MessageAuthor {
11+
pub trait MessageAuthor: PartialEq + Eq {
1112
fn get_display_name(&self) -> impl Element;
1213
fn get_icon(&self) -> String;
14+
fn get_id(&self) -> String;
1315
}

src/discord/src/channel/mod.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
use std::sync::Arc;
22

33
use scope_chat::channel::Channel;
4-
use tokio::sync::{broadcast, RwLock};
4+
use serenity::all::Timestamp;
5+
use tokio::sync::broadcast;
56

67
use crate::{
78
client::DiscordClient,
8-
message::{
9-
author::{DiscordMessageAuthor, DisplayName},
10-
content::DiscordMessageContent,
11-
DiscordMessage,
12-
},
9+
message::{content::DiscordMessageContent, DiscordMessage},
1310
snowflake::Snowflake,
1411
};
1512

@@ -55,6 +52,7 @@ impl Channel for DiscordChannel {
5552
author: self.client.user().clone(),
5653
id: Snowflake { content: 0 },
5754
nonce: Some(nonce),
55+
creation_time: Timestamp::now(),
5856
}
5957
}
6058
}

src/discord/src/client.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
use std::{
22
collections::HashMap,
3-
fs::File,
4-
rc::Rc,
53
sync::{Arc, OnceLock},
64
};
75

86
use serenity::{
97
all::{ChannelId, Context, CreateMessage, Event, EventHandler, GatewayIntents, Message, Nonce, RawEventHandler},
108
async_trait,
11-
futures::SinkExt,
129
};
13-
use std::io::Write;
14-
use tokio::sync::{broadcast, Mutex, RwLock};
10+
use tokio::sync::{broadcast, RwLock};
1511

1612
use crate::{
1713
message::{
1814
author::{DiscordMessageAuthor, DisplayName},
1915
content::DiscordMessageContent,
2016
DiscordMessage,
2117
},
22-
snowflake::{self, Snowflake},
18+
snowflake::Snowflake,
2319
};
2420

2521
#[derive(Default)]
@@ -75,7 +71,7 @@ struct RawClient(Arc<DiscordClient>);
7571

7672
#[async_trait]
7773
impl RawEventHandler for RawClient {
78-
async fn raw_event(&self, ctx: Context, ev: serenity::model::prelude::Event) {
74+
async fn raw_event(&self, _: Context, ev: serenity::model::prelude::Event) {
7975
if let Event::Unknown(unk) = ev {
8076
if unk.kind == "READY" {
8177
if let Some(user) = unk.value.as_object().and_then(|obj| obj.get("user")).and_then(|u| u.as_object()) {
@@ -118,6 +114,7 @@ impl EventHandler for DiscordClient {
118114
author: DiscordMessageAuthor {
119115
display_name: DisplayName(msg.author.name.clone()),
120116
icon: msg.author.avatar_url().unwrap_or(msg.author.default_avatar_url()),
117+
id: msg.author.id.to_string(),
121118
},
122119
content: DiscordMessageContent {
123120
content: msg.content.clone(),
@@ -127,6 +124,7 @@ impl EventHandler for DiscordClient {
127124
Nonce::Number(n) => n.to_string(),
128125
Nonce::String(s) => s,
129126
}),
127+
creation_time: msg.timestamp,
130128
});
131129
}
132130
}

src/discord/src/message/author.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ use scope_chat::message::MessageAuthor;
55
pub struct DiscordMessageAuthor {
66
pub display_name: DisplayName,
77
pub icon: String,
8+
pub id: String,
89
}
910

11+
impl PartialEq for DiscordMessageAuthor {
12+
fn eq(&self, other: &Self) -> bool {
13+
self.id == other.id
14+
}
15+
}
16+
impl Eq for DiscordMessageAuthor {}
17+
1018
impl MessageAuthor for DiscordMessageAuthor {
1119
fn get_display_name(&self) -> impl Element {
1220
self.display_name.clone().into_element()
@@ -15,6 +23,10 @@ impl MessageAuthor for DiscordMessageAuthor {
1523
fn get_icon(&self) -> String {
1624
self.icon.clone()
1725
}
26+
27+
fn get_id(&self) -> String {
28+
self.id.clone()
29+
}
1830
}
1931

2032
#[derive(Clone, IntoElement)]

src/discord/src/message/content.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use gpui::{div, IntoElement, ParentElement, Render, RenderOnce, Styled, WindowContext};
1+
use gpui::{div, IntoElement, ParentElement, RenderOnce, Styled, WindowContext};
22

33
#[derive(Clone, IntoElement)]
44
pub struct DiscordMessageContent {

src/discord/src/message/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::time::Instant;
2+
13
use author::DiscordMessageAuthor;
24
use content::DiscordMessageContent;
35
use gpui::{Element, IntoElement};
@@ -14,6 +16,7 @@ pub struct DiscordMessage {
1416
pub author: DiscordMessageAuthor,
1517
pub id: Snowflake,
1618
pub nonce: Option<String>,
19+
pub creation_time: serenity::model::Timestamp,
1720
}
1821

1922
impl Message for DiscordMessage {
@@ -32,4 +35,10 @@ impl Message for DiscordMessage {
3235
fn get_nonce(&self) -> Option<&String> {
3336
self.nonce.as_ref()
3437
}
38+
39+
fn should_group(&self, previous: &Self) -> bool {
40+
const MAX_DISCORD_MESSAGE_GAP_SECS_FOR_GROUP: i64 = 5 * 60;
41+
42+
self.creation_time.signed_duration_since(&*previous.creation_time).num_seconds() <= MAX_DISCORD_MESSAGE_GAP_SECS_FOR_GROUP
43+
}
3544
}

src/ui/src/channel/message.rs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,67 @@
1-
use gpui::{div, img, rgb, IntoElement, ParentElement, Styled};
1+
use gpui::{div, img, rgb, Element, IntoElement, ParentElement, Styled};
22
use scope_chat::message::{Message, MessageAuthor};
33

4-
pub fn message(message: impl Message) -> impl IntoElement {
4+
#[derive(Clone)]
5+
pub struct MessageGroup<M: Message> {
6+
contents: Vec<M>,
7+
}
8+
9+
impl<M: Message> MessageGroup<M> {
10+
pub fn new(message: M) -> MessageGroup<M> {
11+
MessageGroup { contents: vec![message] }
12+
}
13+
14+
pub fn get_author<'s>(&'s self) -> &'s (impl MessageAuthor + 's) {
15+
self.contents.get(0).unwrap().get_author()
16+
}
17+
18+
pub fn add(&mut self, message: M) {
19+
// FIXME: This is scuffed, should be using PartialEq trait.
20+
if self.get_author().get_id() != message.get_author().get_id() {
21+
panic!("Authors must match in a message group")
22+
}
23+
24+
self.contents.push(message);
25+
}
26+
27+
pub fn contents<'s>(&'s self) -> impl IntoIterator<Item = impl Element + 's> {
28+
self.contents.iter().map(|v| v.get_content())
29+
}
30+
31+
pub fn find_matching(&self, nonce: &String) -> Option<usize> {
32+
for haystack in self.contents.iter().zip(0usize..) {
33+
if haystack.0.get_nonce().is_some() && haystack.0.get_nonce().unwrap() == nonce {
34+
return Some(haystack.1);
35+
}
36+
}
37+
38+
return None;
39+
}
40+
41+
pub fn size(&self) -> usize {
42+
self.contents.len()
43+
}
44+
45+
pub fn remove(&mut self, index: usize) {
46+
if self.size() == 1 {
47+
panic!("Cannot remove such that it would leave the group empty.");
48+
}
49+
50+
self.contents.remove(index);
51+
}
52+
53+
pub fn last(&self) -> &M {
54+
self.contents.last().unwrap()
55+
}
56+
}
57+
58+
pub fn message<M: Message>(message: MessageGroup<M>) -> impl IntoElement {
559
div()
660
.flex()
761
.flex_row()
862
.text_color(rgb(0xFFFFFF))
963
.gap_2()
1064
.p_2()
1165
.child(img(message.get_author().get_icon()).object_fit(gpui::ObjectFit::Fill).bg(rgb(0xFFFFFF)).rounded_full().w_12().h_12())
12-
.child(div().flex().flex_col().child(message.get_author().get_display_name()).child(message.get_content()))
66+
.child(div().flex().flex_col().child(message.get_author().get_display_name()).child(div().children(message.contents())))
1367
}

src/ui/src/channel/message_list.rs

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,71 @@
11
use gpui::{div, IntoElement, ListAlignment, ListState, ParentElement, Pixels};
22

3-
use scope_chat::message::Message;
3+
use scope_chat::message::{Message, MessageAuthor};
44

5-
use super::message::message;
5+
use super::message::{message, MessageGroup};
66

77
#[derive(Clone)]
88
pub struct MessageList<M: Message + 'static> {
9-
real_messages: Vec<M>,
10-
pending_messages: Vec<M>,
9+
messages: Vec<MessageGroup<M>>,
1110
}
1211

1312
impl<M: Message> MessageList<M> {
1413
pub fn new() -> MessageList<M> {
15-
Self {
16-
real_messages: Vec::default(),
17-
pending_messages: Vec::default(),
18-
}
14+
Self { messages: Vec::default() }
1915
}
2016

2117
pub fn add_external_message(&mut self, message: M) {
22-
if let Some((_, pending_index)) = self
23-
.pending_messages
24-
.iter()
25-
.zip(0..)
26-
.find(|(msg, _)| msg.get_nonce().map(|v1| message.get_nonce().map(|v2| v2 == v1).unwrap_or(false)).unwrap_or(false))
27-
{
28-
self.pending_messages.remove(pending_index);
18+
if let Some(nonce) = message.get_nonce() {
19+
let mut removal_index: Option<usize> = None;
20+
21+
for (group, index) in self.messages.iter_mut().zip(0..) {
22+
let matching = group.find_matching(nonce);
23+
24+
if let Some(matching) = matching {
25+
if group.size() == 1 {
26+
removal_index = Some(index);
27+
} else {
28+
group.remove(matching);
29+
}
30+
}
31+
}
32+
33+
if let Some(removal_index) = removal_index {
34+
self.messages.remove(removal_index);
35+
}
2936
}
3037

31-
self.real_messages.push(message);
38+
let last = self.messages.last_mut();
39+
40+
if last.is_some()
41+
&& last.as_ref().unwrap().get_author().get_id() == message.get_author().get_id()
42+
&& message.should_group(last.as_ref().unwrap().last())
43+
{
44+
last.unwrap().add(message);
45+
} else {
46+
self.messages.push(MessageGroup::new(message));
47+
}
3248
}
3349

3450
pub fn add_pending_message(&mut self, pending_message: M) {
35-
self.pending_messages.push(pending_message);
51+
let last = self.messages.last_mut();
52+
53+
if last.is_some()
54+
&& last.as_ref().unwrap().get_author().get_id() == pending_message.get_author().get_id()
55+
&& pending_message.should_group(last.as_ref().unwrap().last())
56+
{
57+
last.unwrap().add(pending_message);
58+
} else {
59+
self.messages.push(MessageGroup::new(pending_message));
60+
}
3661
}
3762

3863
pub fn length(&self) -> usize {
39-
self.real_messages.len() + self.pending_messages.len()
64+
self.messages.len()
4065
}
4166

42-
pub fn get(&self, index: usize) -> Option<&M> {
43-
if index >= self.real_messages.len() {
44-
self.pending_messages.get(index - self.real_messages.len())
45-
} else {
46-
self.real_messages.get(index)
47-
}
67+
pub fn get(&self, index: usize) -> Option<&MessageGroup<M>> {
68+
self.messages.get(index)
4869
}
4970

5071
pub fn create_list_state(&self) -> ListState {

src/ui/src/channel/mod.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
11
pub mod message;
22
pub mod message_list;
33

4-
use std::ops::Deref;
5-
64
use components::input::{InputEvent, TextInput};
7-
use gpui::{
8-
div, list, rgb, AppContext, Context, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, SharedString, Styled, View,
9-
VisualContext,
10-
};
11-
use message::message;
5+
use gpui::{div, list, Context, ListState, Model, ParentElement, Render, Styled, View, VisualContext};
126
use message_list::MessageList;
13-
use scope_backend_discord::message::DiscordMessage;
147
use scope_chat::{channel::Channel, message::Message};
158

169
pub struct ChannelView<M: Message + 'static> {
@@ -20,7 +13,7 @@ pub struct ChannelView<M: Message + 'static> {
2013
}
2114

2215
impl<M: Message + 'static> ChannelView<M> {
23-
pub fn create(ctx: &mut gpui::WindowContext, mut channel: impl Channel<Message = M> + 'static) -> View<Self> {
16+
pub fn create(ctx: &mut gpui::WindowContext, channel: impl Channel<Message = M> + 'static) -> View<Self> {
2417
let view = ctx.new_view(|ctx| {
2518
let state_model = ctx.new_model(|_cx| MessageList::<M>::new());
2619

@@ -94,7 +87,7 @@ impl<M: Message + 'static> ChannelView<M> {
9487
}
9588

9689
impl<M: Message + 'static> Render for ChannelView<M> {
97-
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
90+
fn render(&mut self, _: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
9891
div().flex().flex_col().w_full().h_full().child(list(self.list_state.clone()).w_full().h_full()).child(self.message_input.clone())
9992
}
10093
}

src/ui/src/main.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ use channel::ChannelView;
88
use components::theme::Theme;
99
use gpui::*;
1010
use scope_backend_discord::{channel::DiscordChannel, client::DiscordClient, message::DiscordMessage, snowflake::Snowflake};
11-
use scope_chat::channel::Channel;
12-
use scope_util::ResultExt;
1311

1412
struct Assets {
1513
base: PathBuf,
@@ -29,7 +27,7 @@ impl AssetSource for Assets {
2927

3028
actions!(main_menu, [Quit]);
3129

32-
fn init(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
30+
fn init(_: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
3331
components::init(cx);
3432

3533
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
@@ -46,7 +44,7 @@ async fn main() {
4644
let token = dotenv::var("DISCORD_TOKEN").expect("Must provide DISCORD_TOKEN in .env");
4745
let demo_channel_id = dotenv::var("DEMO_CHANNEL_ID").expect("Must provide DEMO_CHANNEL_ID in .env");
4846

49-
let mut client = DiscordClient::new(token).await;
47+
let client = DiscordClient::new(token).await;
5048

5149
let channel = DiscordChannel::new(
5250
client.clone(),
@@ -67,7 +65,7 @@ async fn main() {
6765

6866
Theme::sync_system_appearance(cx);
6967

70-
let window = cx.open_window(WindowOptions::default(), |cx| ChannelView::<DiscordMessage>::create(cx, channel)).unwrap();
68+
cx.open_window(WindowOptions::default(), |cx| ChannelView::<DiscordMessage>::create(cx, channel)).unwrap();
7169
},
7270
);
7371
}

0 commit comments

Comments
 (0)