Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/chat/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ 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 {
pub trait MessageAuthor: PartialEq + Eq {
fn get_display_name(&self) -> impl Element;
fn get_icon(&self) -> String;
fn get_id(&self) -> String;
}
10 changes: 4 additions & 6 deletions src/discord/src/channel/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
use std::sync::Arc;

use scope_chat::channel::Channel;
use tokio::sync::{broadcast, RwLock};
use serenity::all::Timestamp;
use tokio::sync::broadcast;

use crate::{
client::DiscordClient,
message::{
author::{DiscordMessageAuthor, DisplayName},
content::DiscordMessageContent,
DiscordMessage,
},
message::{content::DiscordMessageContent, DiscordMessage},
snowflake::Snowflake,
};

Expand Down Expand Up @@ -55,6 +52,7 @@ impl Channel for DiscordChannel {
author: self.client.user().clone(),
id: Snowflake { content: 0 },
nonce: Some(nonce),
creation_time: Timestamp::now(),
}
}
}
Expand Down
12 changes: 5 additions & 7 deletions src/discord/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
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::{
author::{DiscordMessageAuthor, DisplayName},
content::DiscordMessageContent,
DiscordMessage,
},
snowflake::{self, Snowflake},
snowflake::Snowflake,
};

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

#[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" {
if let Some(user) = unk.value.as_object().and_then(|obj| obj.get("user")).and_then(|u| u.as_object()) {
Expand Down Expand Up @@ -118,6 +114,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(),
Expand All @@ -127,6 +124,7 @@ impl EventHandler for DiscordClient {
Nonce::Number(n) => n.to_string(),
Nonce::String(s) => s,
}),
creation_time: msg.timestamp,
});
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/discord/src/message/author.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion src/discord/src/message/content.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions src/discord/src/message/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Instant;

use author::DiscordMessageAuthor;
use content::DiscordMessageContent;
use gpui::{Element, IntoElement};
Expand All @@ -14,6 +16,7 @@ pub struct DiscordMessage {
pub author: DiscordMessageAuthor,
pub id: Snowflake,
pub nonce: Option<String>,
pub creation_time: serenity::model::Timestamp,
}

impl Message for DiscordMessage {
Expand All @@ -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
}
}
60 changes: 57 additions & 3 deletions src/ui/src/channel/message.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,67 @@
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<M: Message> {
contents: Vec<M>,
}

impl<M: Message> MessageGroup<M> {
pub fn new(message: M) -> MessageGroup<M> {
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<Item = impl Element + 's> {
self.contents.iter().map(|v| v.get_content())
}

pub fn find_matching(&self, nonce: &String) -> Option<usize> {
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 last(&self) -> &M {
self.contents.last().unwrap()
}
}

pub fn message<M: Message>(message: MessageGroup<M>) -> impl IntoElement {
div()
.flex()
.flex_row()
.text_color(rgb(0xFFFFFF))
.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())))
}
69 changes: 45 additions & 24 deletions src/ui/src/channel/message_list.rs
Original file line number Diff line number Diff line change
@@ -1,50 +1,71 @@
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<M: Message + 'static> {
real_messages: Vec<M>,
pending_messages: Vec<M>,
messages: Vec<MessageGroup<M>>,
}

impl<M: Message> MessageList<M> {
pub fn new() -> MessageList<M> {
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<usize> = 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()
&& message.should_group(last.as_ref().unwrap().last())
{
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()
&& pending_message.should_group(last.as_ref().unwrap().last())
{
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<M>> {
self.messages.get(index)
}

pub fn create_list_state(&self) -> ListState {
Expand Down
13 changes: 3 additions & 10 deletions src/ui/src/channel/mod.rs
Original file line number Diff line number Diff line change
@@ -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<M: Message + 'static> {
Expand All @@ -20,7 +13,7 @@ pub struct ChannelView<M: Message + 'static> {
}

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

Expand Down Expand Up @@ -94,7 +87,7 @@ impl<M: Message + 'static> ChannelView<M> {
}

impl<M: Message + 'static> Render for ChannelView<M> {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
fn render(&mut self, _: &mut gpui::ViewContext<Self>) -> 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())
}
}
8 changes: 3 additions & 5 deletions src/ui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,7 +27,7 @@ impl AssetSource for Assets {

actions!(main_menu, [Quit]);

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

cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
Expand All @@ -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(),
Expand All @@ -67,7 +65,7 @@ async fn main() {

Theme::sync_system_appearance(cx);

let window = cx.open_window(WindowOptions::default(), |cx| ChannelView::<DiscordMessage>::create(cx, channel)).unwrap();
cx.open_window(WindowOptions::default(), |cx| ChannelView::<DiscordMessage>::create(cx, channel)).unwrap();
},
);
}
Loading