Skip to content
Closed
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
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/chat/src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ pub trait Channel: Clone {

fn send_message(&self, content: String, nonce: String) -> Self::Message;
}

pub trait ChannelMetadata {
fn get_name(&self) -> String;
fn get_id(&self) -> String;
fn get_icon(&self) -> Option<String>;
}
1 change: 1 addition & 0 deletions src/discord/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ scope-chat = { version = "0.1.0", path = "../chat" }
serenity = { git = "https://github.com/scopeclient/serenity", version = "0.13.0" }
tokio = "1.41.1"
chrono.workspace = true
serde_json = "1.0.133"
64 changes: 64 additions & 0 deletions src/discord/src/channel/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use crate::client::DiscordClient;
use crate::snowflake::Snowflake;
use scope_chat::channel::ChannelMetadata;
use serenity::all::Channel;
use std::sync::Arc;

#[derive(Clone)]
pub struct DiscordChannelMetadata {
client: Arc<DiscordClient>,
id: Snowflake,
channel: Option<Channel>,
name: Option<String>,
icon: Option<String>,
}

impl ChannelMetadata for DiscordChannelMetadata {
fn get_name(&self) -> String {
if let Some(name) = &self.name {
return name.clone();
}
match &self.channel {
Some(Channel::Guild(guild_channel)) => guild_channel.name.clone(),
Some(Channel::Private(private_channel)) => private_channel.name(),
None => unreachable!("Either name or channel should be set"),
other => {
unreachable!(
"According to the code, there are only two types of channels: Guild and Private. Got: {:?}",
other
)
}
}
}

fn get_id(&self) -> String {
self.id.to_string()
}

fn get_icon(&self) -> Option<String> {
self.icon.clone()
}
}

impl DiscordChannelMetadata {
pub async fn create(client: Arc<DiscordClient>, id: Snowflake) -> Option<Self> {
let channel = client.get_channel(id).await?;
Some(DiscordChannelMetadata {
id,
client,
channel: Some(channel),
name: None,
icon: None,
})
}

pub async fn new(client: Arc<DiscordClient>, id: Snowflake, name: String, icon: String) -> Self {
DiscordChannelMetadata {
id,
client,
channel: None,
name: Some(name),
icon: Some(icon),
}
}
}
4 changes: 4 additions & 0 deletions src/discord/src/channel/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod metadata;

use std::sync::Arc;

use scope_chat::channel::Channel;
Expand All @@ -10,6 +12,8 @@ use crate::{
snowflake::Snowflake,
};

pub use metadata::DiscordChannelMetadata;

pub struct DiscordChannel {
channel_id: Snowflake,
receiver: broadcast::Receiver<DiscordMessage>,
Expand Down
101 changes: 79 additions & 22 deletions src/discord/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
use std::{
collections::HashMap, sync::{Arc, OnceLock}
use crate::channel::DiscordChannelMetadata;
use crate::{
message::{
author::{DiscordMessageAuthor, DisplayName},
content::DiscordMessageContent,
DiscordMessage,
},
snowflake::Snowflake,
};

use scope_chat::channel::ChannelMetadata;
use serde_json::Map;
use serenity::all::Channel;
use serenity::json::Value;
use serenity::{
all::{Cache, ChannelId, Context, CreateMessage, Event, EventHandler, GatewayIntents, Http, Message, Nonce, RawEventHandler}, async_trait
all::{Cache, ChannelId, Context, CreateMessage, Event, EventHandler, GatewayIntents, Http, Message, Nonce, RawEventHandler},
async_trait,
};
use tokio::sync::{broadcast, RwLock};

use crate::{
message::{
author::{DiscordMessageAuthor, DisplayName}, content::DiscordMessageContent, DiscordMessage
}, snowflake::Snowflake
use std::{
collections::HashMap,
sync::{Arc, OnceLock},
};
use tokio::sync::{broadcast, RwLock};

#[allow(dead_code)]
struct SerenityClient {
Expand All @@ -24,6 +32,8 @@ struct SerenityClient {
#[derive(Default)]
pub struct DiscordClient {
channel_message_event_handlers: RwLock<HashMap<Snowflake, Vec<broadcast::Sender<DiscordMessage>>>>,
channel_updates_event_handler: RwLock<Vec<broadcast::Sender<DiscordChannelMetadata>>>,
direct_message_channels: RwLock<Vec<DiscordChannelMetadata>>,
client: OnceLock<SerenityClient>,
user: OnceLock<DiscordMessageAuthor>,
}
Expand Down Expand Up @@ -65,6 +75,10 @@ impl DiscordClient {
self.channel_message_event_handlers.write().await.entry(channel).or_default().push(sender);
}

pub async fn set_channel_update_sender(&self, sender: broadcast::Sender<DiscordChannelMetadata>) {
self.channel_updates_event_handler.write().await.push(sender);
}

pub async fn send_message(&self, channel_id: Snowflake, content: String, nonce: String) {
ChannelId::new(channel_id.content)
.send_message(
Expand All @@ -74,37 +88,80 @@ impl DiscordClient {
.await
.unwrap();
}

pub async fn get_channel(&self, channel_id: Snowflake) -> Option<Channel> {
ChannelId::new(channel_id.content).to_channel(self.discord().http.clone()).await.ok()
}

pub async fn list_direct_message_channels(&self) -> Vec<DiscordChannelMetadata> {
self.direct_message_channels.read().await.clone()
}
}

fn avatar_by_user_value(user: &Map<String, Value>, user_id: String) -> String {
user
.get("avatar")
.and_then(|avatar| avatar.as_str())
.map(|avatar| format!("https://cdn.discordapp.com/avatars/{}/{}", user_id, avatar))
.unwrap_or_else(|| {
format!(
"https://cdn.discordapp.com/embed/avatars/{}.png",
(user_id.parse::<u64>().unwrap_or(0) % 5)
)
})
}

struct RawClient(Arc<DiscordClient>);

#[async_trait]
impl RawEventHandler for RawClient {
async fn raw_event(&self, _: Context, ev: serenity::model::prelude::Event) {
async fn raw_event(&self, _: Context, ev: 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()) {
let username = user.get("username").and_then(|u| u.as_str()).unwrap_or("Unknown User").to_owned();

let user_id = user.get("id").and_then(|id| id.as_str()).unwrap_or_default();

let icon = user
.get("avatar")
.and_then(|avatar| avatar.as_str())
.map(|avatar| format!("https://cdn.discordapp.com/avatars/{}/{}", user_id, avatar))
.unwrap_or_else(|| {
format!(
"https://cdn.discordapp.com/embed/avatars/{}.png",
(user_id.parse::<u64>().unwrap_or(0) % 5)
)
});
let icon = avatar_by_user_value(user, user_id.to_owned());

self.0.user.get_or_init(|| DiscordMessageAuthor {
display_name: DisplayName(username),
icon,
id: user_id.to_owned(),
});
}

if let Some(private_channels) = unk.value.as_object().and_then(|obj| obj.get("private_channels")).and_then(|channels| channels.as_array()) {
for channel in private_channels {
if let Some(user) = channel
.get("recipients")
.and_then(|recipients| recipients.as_array())
.and_then(|recipients| recipients.get(0))
.and_then(|recipient| recipient.as_object())
{
let channel_id = channel.get("id").and_then(|id| id.as_str()).unwrap_or_default().to_owned();

let user_id = user.get("id").and_then(|id| id.as_str()).unwrap_or_default().to_owned();
let user_name = user.get("username").and_then(|name| name.as_str()).unwrap_or_default().to_owned();
let chat_icon = avatar_by_user_value(user, user_id.to_owned());

let channel = DiscordChannelMetadata::new(
self.0.clone(),
Snowflake {
content: channel_id.parse().unwrap(),
},
user_name,
chat_icon,
)
.await;

self.0.direct_message_channels.write().await.push(channel.clone());
self.0.channel_updates_event_handler.read().await.iter().for_each(|sender| {
let _ = sender.send(channel.clone());
});
}
}
}
}
}
}
Expand Down
43 changes: 28 additions & 15 deletions src/ui/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use crate::channel::ChannelView;
use crate::sidebar::Sidebar;
use components::theme::ActiveTheme;
use gpui::prelude::FluentBuilder;
use gpui::{div, img, rgb, Context, Model, ParentElement, Render, Styled, View, ViewContext, VisualContext};
use scope_backend_discord::channel::DiscordChannelMetadata;
use scope_backend_discord::{channel::DiscordChannel, client::DiscordClient, message::DiscordMessage, snowflake::Snowflake};

use crate::channel::ChannelView;

pub struct App {
channel: Model<Option<View<ChannelView<DiscordMessage>>>>,
sidebar: Model<View<Sidebar<DiscordChannelMetadata>>>,
}

impl App {
Expand All @@ -14,11 +17,13 @@ impl App {
let demo_channel_id = dotenv::var("DEMO_CHANNEL_ID").expect("Must provide DEMO_CHANNEL_ID in .env");

let mut context = ctx.to_async();

let channel = ctx.new_model(|_| None);

let async_channel = channel.clone();

let sidebar_view = ctx.new_view(|cx| Sidebar::create(cx, None));
let sidebar = ctx.new_model(|cx| sidebar_view);
let async_sidebar = sidebar.clone();

ctx
.foreground_executor()
.spawn(async move {
Expand All @@ -32,26 +37,34 @@ impl App {
)
.await;

let view = context.new_view(|cx| ChannelView::<DiscordMessage>::create(cx, channel)).unwrap();
let channel_view = context.new_view(|cx| ChannelView::<DiscordMessage>::create(cx, channel)).unwrap();

let _ = async_channel.update(&mut context, |a, b| {
*a = Some(channel_view);
b.notify()
});

async_channel.update(&mut context, |a, b| {
*a = Some(view);
let sidebar_view = context.new_view(|cx| Sidebar::create(cx, Some(client))).unwrap();
let _ = async_sidebar.update(&mut context, |a, b| {
*a = sidebar_view;
b.notify()
})
});
})
.detach();

App { channel }
App { channel, sidebar }
}
}

impl Render for App {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
let mut content = div().w_full().h_full();

if let Some(channel) = self.channel.read(cx).as_ref() {
content = content.child(channel.clone());
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement {
let content = div()
.w_full()
.h_full()
.flex()
.gap_2()
.child(self.sidebar.read(cx).clone())
.when_some(self.channel.read(cx).clone(), |div, view| div.child(view));

let title_bar = components::TitleBar::new()
.child(div().flex().flex_row().text_color(rgb(0xFFFFFF)).gap_2().child(img("brand/scope-round-200.png").w_6().h_6()).child("Scope"));
Expand Down
1 change: 1 addition & 0 deletions src/ui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod app;
pub mod app_state;
pub mod channel;
pub mod menu;
pub mod sidebar;

use std::sync::Arc;

Expand Down
30 changes: 30 additions & 0 deletions src/ui/src/sidebar/channel_tree/channel_entry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use components::theme::ActiveTheme;
use gpui::{div, img, IntoElement, ParentElement, RenderOnce, Styled, WindowContext};
use scope_chat::channel::ChannelMetadata;
use std::sync::Arc;

#[derive(IntoElement)]
pub struct ChannelEntry {
meta: Arc<dyn ChannelMetadata>,
}

impl ChannelEntry {
pub fn new(meta: Arc<dyn ChannelMetadata>) -> Self {
Self { meta }
}
}

impl RenderOnce for ChannelEntry {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
div()
.px_2()
.py_1()
.my_1()
.flex()
.items_center()
.gap_2()
.bg(cx.theme().list)
.child(img(self.meta.get_icon().unwrap()).flex_shrink_0().object_fit(gpui::ObjectFit::Fill).rounded_full().w_4().h_4())
.child(self.meta.get_name())
}
}
Loading
Loading