From 8a465b53f74134b97584c8f9d6aaf772f8397329 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sat, 16 Nov 2024 03:52:00 -0500 Subject: [PATCH 1/7] 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 d2319dff2e0090cb5a3f129b7cb94d9a4377f3ef Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sat, 16 Nov 2024 03:54:52 -0500 Subject: [PATCH 2/7] 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 00cd9eac63a02f9eed4b395cf789259c887a69fc Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sat, 16 Nov 2024 04:17:47 -0500 Subject: [PATCH 3/7] added padding --- src/ui/src/channel/message.rs | 3 +-- src/ui/src/channel/mod.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ui/src/channel/message.rs b/src/ui/src/channel/message.rs index 5bfe97f..ee6075b 100644 --- a/src/ui/src/channel/message.rs +++ b/src/ui/src/channel/message.rs @@ -56,8 +56,7 @@ pub fn message(message: MessageGroup) -> impl IntoElement { .flex() .flex_row() .text_color(rgb(0xFFFFFF)) - .gap_2() - .p_2() + .gap_4() .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(div().children(message.contents()))) } diff --git a/src/ui/src/channel/mod.rs b/src/ui/src/channel/mod.rs index 55b73fc..35ea1b1 100644 --- a/src/ui/src/channel/mod.rs +++ b/src/ui/src/channel/mod.rs @@ -88,6 +88,6 @@ impl ChannelView { impl Render for ChannelView { 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()) + div().flex().flex_col().w_full().h_full().p_6().gap_6().child(list(self.list_state.clone()).w_full().h_full()).child(self.message_input.clone()) } } From d3eab45a25683e8cfa3b49cc56373081a7218461 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sat, 16 Nov 2024 04:48:13 -0500 Subject: [PATCH 4/7] reworked UI to use a custom title bar --- assets/.gitkeep | 0 assets/brand/scope-round-200.png | Bin 0 -> 15108 bytes assets/icons/a-large-small.svg | 1 + assets/icons/arrow-down.svg | 1 + assets/icons/arrow-left.svg | 4 + assets/icons/arrow-right.svg | 4 + assets/icons/arrow-up.svg | 1 + assets/icons/asterisk.svg | 1 + assets/icons/bell.svg | 1 + assets/icons/calendar.svg | 1 + assets/icons/check.svg | 3 + assets/icons/chevron-down.svg | 3 + assets/icons/chevron-left.svg | 3 + assets/icons/chevron-right.svg | 3 + assets/icons/chevron-up.svg | 1 + assets/icons/chevrons-up-down.svg | 4 + assets/icons/circle-check.svg | 1 + assets/icons/circle-x.svg | 1 + assets/icons/close.svg | 1 + assets/icons/copy.svg | 1 + assets/icons/dash.svg | 3 + assets/icons/delete.svg | 1 + assets/icons/ellipsis-vertical.svg | 1 + assets/icons/ellipsis.svg | 1 + assets/icons/eye-off.svg | 6 ++ assets/icons/eye.svg | 4 + assets/icons/github.svg | 1 + assets/icons/globe.svg | 1 + assets/icons/heart-off.svg | 1 + assets/icons/heart.svg | 1 + assets/icons/inbox.svg | 1 + assets/icons/info.svg | 1 + assets/icons/loader-circle.svg | 3 + assets/icons/loader.svg | 10 +++ assets/icons/maximize.svg | 1 + assets/icons/menu.svg | 1 + assets/icons/minimize.svg | 6 ++ assets/icons/minus.svg | 3 + assets/icons/moon.svg | 1 + assets/icons/palette.svg | 1 + assets/icons/panel-bottom-open.svg | 1 + assets/icons/panel-bottom.svg | 1 + assets/icons/panel-left-open.svg | 1 + assets/icons/panel-left.svg | 1 + assets/icons/panel-right-open.svg | 1 + assets/icons/panel-right.svg | 1 + assets/icons/plus.svg | 1 + assets/icons/search.svg | 4 + assets/icons/sort-ascending.svg | 4 + assets/icons/sort-descending.svg | 4 + assets/icons/star-off.svg | 1 + assets/icons/star.svg | 1 + assets/icons/sun.svg | 1 + assets/icons/thumbs-down.svg | 1 + assets/icons/thumbs-up.svg | 1 + assets/icons/triangle-alert.svg | 1 + assets/icons/window-close.svg | 9 ++ assets/icons/window-maximize.svg | 9 ++ assets/icons/window-minimize.svg | 9 ++ assets/icons/window-restore.svg | 10 +++ src/ui/src/app.rs | 60 +++++++++++++ src/ui/src/channel/message.rs | 1 + src/ui/src/channel/mod.rs | 134 ++++++++++++++--------------- src/ui/src/main.rs | 39 ++++----- 64 files changed, 290 insertions(+), 89 deletions(-) create mode 100644 assets/.gitkeep create mode 100644 assets/brand/scope-round-200.png create mode 100644 assets/icons/a-large-small.svg create mode 100644 assets/icons/arrow-down.svg create mode 100644 assets/icons/arrow-left.svg create mode 100644 assets/icons/arrow-right.svg create mode 100644 assets/icons/arrow-up.svg create mode 100644 assets/icons/asterisk.svg create mode 100644 assets/icons/bell.svg create mode 100644 assets/icons/calendar.svg create mode 100644 assets/icons/check.svg create mode 100644 assets/icons/chevron-down.svg create mode 100644 assets/icons/chevron-left.svg create mode 100644 assets/icons/chevron-right.svg create mode 100644 assets/icons/chevron-up.svg create mode 100644 assets/icons/chevrons-up-down.svg create mode 100644 assets/icons/circle-check.svg create mode 100644 assets/icons/circle-x.svg create mode 100644 assets/icons/close.svg create mode 100644 assets/icons/copy.svg create mode 100644 assets/icons/dash.svg create mode 100644 assets/icons/delete.svg create mode 100644 assets/icons/ellipsis-vertical.svg create mode 100644 assets/icons/ellipsis.svg create mode 100644 assets/icons/eye-off.svg create mode 100644 assets/icons/eye.svg create mode 100644 assets/icons/github.svg create mode 100644 assets/icons/globe.svg create mode 100644 assets/icons/heart-off.svg create mode 100644 assets/icons/heart.svg create mode 100644 assets/icons/inbox.svg create mode 100644 assets/icons/info.svg create mode 100644 assets/icons/loader-circle.svg create mode 100644 assets/icons/loader.svg create mode 100644 assets/icons/maximize.svg create mode 100644 assets/icons/menu.svg create mode 100644 assets/icons/minimize.svg create mode 100644 assets/icons/minus.svg create mode 100644 assets/icons/moon.svg create mode 100644 assets/icons/palette.svg create mode 100644 assets/icons/panel-bottom-open.svg create mode 100644 assets/icons/panel-bottom.svg create mode 100644 assets/icons/panel-left-open.svg create mode 100644 assets/icons/panel-left.svg create mode 100644 assets/icons/panel-right-open.svg create mode 100644 assets/icons/panel-right.svg create mode 100644 assets/icons/plus.svg create mode 100644 assets/icons/search.svg create mode 100644 assets/icons/sort-ascending.svg create mode 100644 assets/icons/sort-descending.svg create mode 100644 assets/icons/star-off.svg create mode 100644 assets/icons/star.svg create mode 100644 assets/icons/sun.svg create mode 100644 assets/icons/thumbs-down.svg create mode 100644 assets/icons/thumbs-up.svg create mode 100644 assets/icons/triangle-alert.svg create mode 100644 assets/icons/window-close.svg create mode 100644 assets/icons/window-maximize.svg create mode 100644 assets/icons/window-minimize.svg create mode 100644 assets/icons/window-restore.svg create mode 100644 src/ui/src/app.rs diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/brand/scope-round-200.png b/assets/brand/scope-round-200.png new file mode 100644 index 0000000000000000000000000000000000000000..05c4c0087ccdbabc72ed7fc82662bd73775efd47 GIT binary patch literal 15108 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 Z-$~Y6>I~ZY?^`Q`oRqR;t++Ax{{WHS$g2PV literal 0 HcmV?d00001 diff --git a/assets/icons/a-large-small.svg b/assets/icons/a-large-small.svg new file mode 100644 index 0000000..cfba2de --- /dev/null +++ b/assets/icons/a-large-small.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/arrow-down.svg b/assets/icons/arrow-down.svg new file mode 100644 index 0000000..bb5b04c --- /dev/null +++ b/assets/icons/arrow-down.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/arrow-left.svg b/assets/icons/arrow-left.svg new file mode 100644 index 0000000..ed03495 --- /dev/null +++ b/assets/icons/arrow-left.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg new file mode 100644 index 0000000..519fffd --- /dev/null +++ b/assets/icons/arrow-right.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/arrow-up.svg b/assets/icons/arrow-up.svg new file mode 100644 index 0000000..ad12519 --- /dev/null +++ b/assets/icons/arrow-up.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/asterisk.svg b/assets/icons/asterisk.svg new file mode 100644 index 0000000..b2d266f --- /dev/null +++ b/assets/icons/asterisk.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/bell.svg b/assets/icons/bell.svg new file mode 100644 index 0000000..eecc3ef --- /dev/null +++ b/assets/icons/bell.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/calendar.svg b/assets/icons/calendar.svg new file mode 100644 index 0000000..2724a48 --- /dev/null +++ b/assets/icons/calendar.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/check.svg b/assets/icons/check.svg new file mode 100644 index 0000000..16bb379 --- /dev/null +++ b/assets/icons/check.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/chevron-down.svg b/assets/icons/chevron-down.svg new file mode 100644 index 0000000..1d316c7 --- /dev/null +++ b/assets/icons/chevron-down.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/chevron-left.svg b/assets/icons/chevron-left.svg new file mode 100644 index 0000000..f27e97c --- /dev/null +++ b/assets/icons/chevron-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/chevron-right.svg b/assets/icons/chevron-right.svg new file mode 100644 index 0000000..c1b76ee --- /dev/null +++ b/assets/icons/chevron-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/chevron-up.svg b/assets/icons/chevron-up.svg new file mode 100644 index 0000000..f1bb1ad --- /dev/null +++ b/assets/icons/chevron-up.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/chevrons-up-down.svg b/assets/icons/chevrons-up-down.svg new file mode 100644 index 0000000..106fac0 --- /dev/null +++ b/assets/icons/chevrons-up-down.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/circle-check.svg b/assets/icons/circle-check.svg new file mode 100644 index 0000000..4a7e101 --- /dev/null +++ b/assets/icons/circle-check.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/circle-x.svg b/assets/icons/circle-x.svg new file mode 100644 index 0000000..803c8cb --- /dev/null +++ b/assets/icons/circle-x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/close.svg b/assets/icons/close.svg new file mode 100644 index 0000000..346ec0b --- /dev/null +++ b/assets/icons/close.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg new file mode 100644 index 0000000..f3b629c --- /dev/null +++ b/assets/icons/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/dash.svg b/assets/icons/dash.svg new file mode 100644 index 0000000..a1264e7 --- /dev/null +++ b/assets/icons/dash.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/delete.svg b/assets/icons/delete.svg new file mode 100644 index 0000000..eb058d9 --- /dev/null +++ b/assets/icons/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/ellipsis-vertical.svg b/assets/icons/ellipsis-vertical.svg new file mode 100644 index 0000000..c75a25a --- /dev/null +++ b/assets/icons/ellipsis-vertical.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg new file mode 100644 index 0000000..07dcab5 --- /dev/null +++ b/assets/icons/ellipsis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/eye-off.svg b/assets/icons/eye-off.svg new file mode 100644 index 0000000..abf1280 --- /dev/null +++ b/assets/icons/eye-off.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg new file mode 100644 index 0000000..4b3a970 --- /dev/null +++ b/assets/icons/eye.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/github.svg b/assets/icons/github.svg new file mode 100644 index 0000000..92adec9 --- /dev/null +++ b/assets/icons/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/globe.svg b/assets/icons/globe.svg new file mode 100644 index 0000000..2082a43 --- /dev/null +++ b/assets/icons/globe.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/heart-off.svg b/assets/icons/heart-off.svg new file mode 100644 index 0000000..68cda02 --- /dev/null +++ b/assets/icons/heart-off.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/heart.svg b/assets/icons/heart.svg new file mode 100644 index 0000000..2f267c9 --- /dev/null +++ b/assets/icons/heart.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/inbox.svg b/assets/icons/inbox.svg new file mode 100644 index 0000000..15d6fae --- /dev/null +++ b/assets/icons/inbox.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/info.svg b/assets/icons/info.svg new file mode 100644 index 0000000..8f9c59a --- /dev/null +++ b/assets/icons/info.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/loader-circle.svg b/assets/icons/loader-circle.svg new file mode 100644 index 0000000..12cb72f --- /dev/null +++ b/assets/icons/loader-circle.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/loader.svg b/assets/icons/loader.svg new file mode 100644 index 0000000..f6ff93f --- /dev/null +++ b/assets/icons/loader.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg new file mode 100644 index 0000000..b3504b5 --- /dev/null +++ b/assets/icons/maximize.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/menu.svg b/assets/icons/menu.svg new file mode 100644 index 0000000..6598697 --- /dev/null +++ b/assets/icons/menu.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg new file mode 100644 index 0000000..3f81a42 --- /dev/null +++ b/assets/icons/minimize.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/minus.svg b/assets/icons/minus.svg new file mode 100644 index 0000000..ab04a16 --- /dev/null +++ b/assets/icons/minus.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/moon.svg b/assets/icons/moon.svg new file mode 100644 index 0000000..dfadc20 --- /dev/null +++ b/assets/icons/moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/palette.svg b/assets/icons/palette.svg new file mode 100644 index 0000000..b50674d --- /dev/null +++ b/assets/icons/palette.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/panel-bottom-open.svg b/assets/icons/panel-bottom-open.svg new file mode 100644 index 0000000..df77e5b --- /dev/null +++ b/assets/icons/panel-bottom-open.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/panel-bottom.svg b/assets/icons/panel-bottom.svg new file mode 100644 index 0000000..ebe599c --- /dev/null +++ b/assets/icons/panel-bottom.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/panel-left-open.svg b/assets/icons/panel-left-open.svg new file mode 100644 index 0000000..579e458 --- /dev/null +++ b/assets/icons/panel-left-open.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/panel-left.svg b/assets/icons/panel-left.svg new file mode 100644 index 0000000..2eed266 --- /dev/null +++ b/assets/icons/panel-left.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/panel-right-open.svg b/assets/icons/panel-right-open.svg new file mode 100644 index 0000000..3b5ff0b --- /dev/null +++ b/assets/icons/panel-right-open.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/panel-right.svg b/assets/icons/panel-right.svg new file mode 100644 index 0000000..d29a4a5 --- /dev/null +++ b/assets/icons/panel-right.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg new file mode 100644 index 0000000..651bbd4 --- /dev/null +++ b/assets/icons/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/search.svg b/assets/icons/search.svg new file mode 100644 index 0000000..a658349 --- /dev/null +++ b/assets/icons/search.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/sort-ascending.svg b/assets/icons/sort-ascending.svg new file mode 100644 index 0000000..9be1b97 --- /dev/null +++ b/assets/icons/sort-ascending.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/sort-descending.svg b/assets/icons/sort-descending.svg new file mode 100644 index 0000000..ab65056 --- /dev/null +++ b/assets/icons/sort-descending.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/star-off.svg b/assets/icons/star-off.svg new file mode 100644 index 0000000..a262060 --- /dev/null +++ b/assets/icons/star-off.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/star.svg b/assets/icons/star.svg new file mode 100644 index 0000000..8d93131 --- /dev/null +++ b/assets/icons/star.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/sun.svg b/assets/icons/sun.svg new file mode 100644 index 0000000..668d778 --- /dev/null +++ b/assets/icons/sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/thumbs-down.svg b/assets/icons/thumbs-down.svg new file mode 100644 index 0000000..816273e --- /dev/null +++ b/assets/icons/thumbs-down.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/thumbs-up.svg b/assets/icons/thumbs-up.svg new file mode 100644 index 0000000..01fae76 --- /dev/null +++ b/assets/icons/thumbs-up.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/triangle-alert.svg b/assets/icons/triangle-alert.svg new file mode 100644 index 0000000..1861c2e --- /dev/null +++ b/assets/icons/triangle-alert.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/window-close.svg b/assets/icons/window-close.svg new file mode 100644 index 0000000..62b6b0c --- /dev/null +++ b/assets/icons/window-close.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/window-maximize.svg b/assets/icons/window-maximize.svg new file mode 100644 index 0000000..9da4cd7 --- /dev/null +++ b/assets/icons/window-maximize.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/window-minimize.svg b/assets/icons/window-minimize.svg new file mode 100644 index 0000000..708e27b --- /dev/null +++ b/assets/icons/window-minimize.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/window-restore.svg b/assets/icons/window-restore.svg new file mode 100644 index 0000000..896b6b6 --- /dev/null +++ b/assets/icons/window-restore.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/ui/src/app.rs b/src/ui/src/app.rs new file mode 100644 index 0000000..e2f0d2a --- /dev/null +++ b/src/ui/src/app.rs @@ -0,0 +1,60 @@ +use gpui::{div, img, rgb, Context, Model, ParentElement, Render, Styled, View, ViewContext, VisualContext}; +use scope_backend_discord::{channel::DiscordChannel, client::DiscordClient, message::DiscordMessage, snowflake::Snowflake}; + +use crate::channel::ChannelView; + +pub struct App { + channel: Model>>>, +} + +impl App { + pub fn new(ctx: &mut ViewContext<'_, Self>) -> App { + 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 context = ctx.to_async(); + + let channel = ctx.new_model(|_| None); + + let async_channel = channel.clone(); + + ctx + .foreground_executor() + .spawn(async move { + let client = DiscordClient::new(token).await; + + let channel = DiscordChannel::new( + client.clone(), + Snowflake { + content: demo_channel_id.parse().unwrap(), + }, + ) + .await; + + let view = context.new_view(|cx| ChannelView::::create(cx, channel)).unwrap(); + + async_channel.update(&mut context, |a, b| { + *a = Some(view); + b.notify() + }) + }) + .detach(); + + App { channel } + } +} + +impl Render for App { + fn render(&mut self, cx: &mut gpui::ViewContext) -> 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()); + } + + 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")); + + div().w_full().h_full().flex().flex_col().child(title_bar).child(content) + } +} diff --git a/src/ui/src/channel/message.rs b/src/ui/src/channel/message.rs index ee6075b..8a725fa 100644 --- a/src/ui/src/channel/message.rs +++ b/src/ui/src/channel/message.rs @@ -57,6 +57,7 @@ pub fn message(message: MessageGroup) -> impl IntoElement { .flex_row() .text_color(rgb(0xFFFFFF)) .gap_4() + .pb_6() .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(div().children(message.contents()))) } diff --git a/src/ui/src/channel/mod.rs b/src/ui/src/channel/mod.rs index 35ea1b1..8ebbb0d 100644 --- a/src/ui/src/channel/mod.rs +++ b/src/ui/src/channel/mod.rs @@ -13,81 +13,77 @@ pub struct ChannelView { } impl ChannelView { - 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()); - - let async_model = state_model.clone(); - let mut async_ctx = ctx.to_async(); - let channel_listener = channel.clone(); - - ctx - .foreground_executor() - .spawn(async move { - loop { - let message = channel_listener.get_receiver().recv().await.unwrap(); - - async_model - .update(&mut async_ctx, |data, ctx| { - data.add_external_message(message); - ctx.notify(); - }) - .unwrap(); - } - }) - .detach(); - - ctx - .observe(&state_model, |this: &mut ChannelView, model, cx| { - this.list_state = model.read(cx).create_list_state(); - cx.notify(); - }) - .detach(); - - let message_input = ctx.new_view(|cx| { - let mut input = components::input::TextInput::new(cx); - - input.set_size(components::Size::Large, cx); - - input - }); - - ctx - .subscribe(&message_input, move |channel_view, text_input, input_event, ctx| match input_event { - InputEvent::PressEnter => { - let content = text_input.read(ctx).text().to_string(); - let channel_sender = channel.clone(); - - text_input.update(ctx, |text_input, cx| { - text_input.set_text("", cx); - }); - - let nonce = random_string::generate(20, random_string::charsets::ALPHANUMERIC); - let pending = channel_sender.send_message(content, nonce); - - channel_view.list_model.update(ctx, move |v, _| { - v.add_pending_message(pending); - }); - channel_view.list_state = channel_view.list_model.read(ctx).create_list_state(); - ctx.notify(); - } - _ => {} - }) - .detach(); - - ChannelView:: { - list_state: state_model.read(ctx).create_list_state(), - list_model: state_model, - message_input, - } + pub fn create(ctx: &mut gpui::ViewContext<'_, ChannelView>, channel: impl Channel + 'static) -> Self { + let state_model = ctx.new_model(|_cx| MessageList::::new()); + + let async_model = state_model.clone(); + let mut async_ctx = ctx.to_async(); + let channel_listener = channel.clone(); + + ctx + .foreground_executor() + .spawn(async move { + loop { + let message = channel_listener.get_receiver().recv().await.unwrap(); + + async_model + .update(&mut async_ctx, |data, ctx| { + data.add_external_message(message); + ctx.notify(); + }) + .unwrap(); + } + }) + .detach(); + + ctx + .observe(&state_model, |this: &mut ChannelView, model, cx| { + this.list_state = model.read(cx).create_list_state(); + cx.notify(); + }) + .detach(); + + let message_input = ctx.new_view(|cx| { + let mut input = components::input::TextInput::new(cx); + + input.set_size(components::Size::Large, cx); + + input }); - view + ctx + .subscribe(&message_input, move |channel_view, text_input, input_event, ctx| match input_event { + InputEvent::PressEnter => { + let content = text_input.read(ctx).text().to_string(); + let channel_sender = channel.clone(); + + text_input.update(ctx, |text_input, cx| { + text_input.set_text("", cx); + }); + + let nonce = random_string::generate(20, random_string::charsets::ALPHANUMERIC); + let pending = channel_sender.send_message(content, nonce); + + channel_view.list_model.update(ctx, move |v, _| { + v.add_pending_message(pending); + }); + channel_view.list_state = channel_view.list_model.read(ctx).create_list_state(); + ctx.notify(); + } + _ => {} + }) + .detach(); + + ChannelView:: { + list_state: state_model.read(ctx).create_list_state(), + list_model: state_model, + message_input, + } } } impl Render for ChannelView { fn render(&mut self, _: &mut gpui::ViewContext) -> impl gpui::IntoElement { - div().flex().flex_col().w_full().h_full().p_6().gap_6().child(list(self.list_state.clone()).w_full().h_full()).child(self.message_input.clone()) + div().flex().flex_col().w_full().h_full().p_6().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 64c21c1..ffafc85 100644 --- a/src/ui/src/main.rs +++ b/src/ui/src/main.rs @@ -1,13 +1,12 @@ +pub mod app; pub mod app_state; pub mod channel; use std::{fs, path::PathBuf, sync::Arc}; use app_state::AppState; -use channel::ChannelView; use components::theme::Theme; use gpui::*; -use scope_backend_discord::{channel::DiscordChannel, client::DiscordClient, message::DiscordMessage, snowflake::Snowflake}; struct Assets { base: PathBuf, @@ -41,21 +40,12 @@ async fn main() { let app_state = Arc::new(AppState {}); - 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 client = DiscordClient::new(token).await; - - let channel = DiscordChannel::new( - client.clone(), - Snowflake { - content: demo_channel_id.parse().unwrap(), - }, - ) - .await; - - App::new().with_assets(Assets { base: PathBuf::from("img") }).with_http_client(Arc::new(reqwest_client::ReqwestClient::new())).run( - move |cx: &mut AppContext| { + App::new() + .with_assets(Assets { + base: PathBuf::from("assets"), + }) + .with_http_client(Arc::new(reqwest_client::ReqwestClient::new())) + .run(move |cx: &mut AppContext| { AppState::set_global(Arc::downgrade(&app_state), cx); if let Err(e) = init(app_state.clone(), cx) { @@ -65,7 +55,16 @@ async fn main() { Theme::sync_system_appearance(cx); - cx.open_window(WindowOptions::default(), |cx| ChannelView::::create(cx, channel)).unwrap(); - }, - ); + let opts = WindowOptions { + window_decorations: Some(WindowDecorations::Client), + titlebar: Some(TitlebarOptions { + appears_transparent: true, + title: Some(SharedString::new_static("scope")), + ..Default::default() + }), + ..Default::default() + }; + + cx.open_window(opts, |cx| cx.new_view(|cx| crate::app::App::new(cx))).unwrap(); + }); } From 7ec66bd7f2278aa49e03c01ad0359464c5995f42 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sat, 16 Nov 2024 05:02:30 -0500 Subject: [PATCH 5/7] add proper wrapping --- src/ui/src/channel/message.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/src/channel/message.rs b/src/ui/src/channel/message.rs index 8a725fa..3e25571 100644 --- a/src/ui/src/channel/message.rs +++ b/src/ui/src/channel/message.rs @@ -58,6 +58,6 @@ pub fn message(message: MessageGroup) -> impl IntoElement { .text_color(rgb(0xFFFFFF)) .gap_4() .pb_6() - .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(div().children(message.contents()))) + .child(img(message.get_author().get_icon()).flex_shrink_0().object_fit(gpui::ObjectFit::Fill).bg(rgb(0xFFFFFF)).rounded_full().w_12().h_12()) + .child(div().flex().min_w_0().flex_shrink().flex_col().child(message.get_author().get_display_name()).children(message.contents())) } From e76609607bf37fb67c3676cdffb23e8b645782ea Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sat, 16 Nov 2024 05:05:23 -0500 Subject: [PATCH 6/7] attempt to make the user name format with ellipsis --- src/ui/src/channel/message.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/ui/src/channel/message.rs b/src/ui/src/channel/message.rs index 3e25571..c654792 100644 --- a/src/ui/src/channel/message.rs +++ b/src/ui/src/channel/message.rs @@ -59,5 +59,15 @@ pub fn message(message: MessageGroup) -> impl IntoElement { .gap_4() .pb_6() .child(img(message.get_author().get_icon()).flex_shrink_0().object_fit(gpui::ObjectFit::Fill).bg(rgb(0xFFFFFF)).rounded_full().w_12().h_12()) - .child(div().flex().min_w_0().flex_shrink().flex_col().child(message.get_author().get_display_name()).children(message.contents())) + .child( + div() + .flex() + .min_w_0() + .flex_shrink() + .flex_col() + // enabling this, and thus enabling ellipsis causes a consistent panic!? + // .child(div().text_ellipsis().min_w_0().child(message.get_author().get_display_name())) + .child(div().min_w_0().child(message.get_author().get_display_name())) + .children(message.contents()), + ) } From c566150a81394893b85eaddf9fbe96267aed1c44 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sat, 16 Nov 2024 12:13:26 -0500 Subject: [PATCH 7/7] Embedded assets Co-authored-by: Alistair Smith --- Cargo.lock | 1 + src/ui/Cargo.toml | 1 + src/ui/src/main.rs | 58 +++++++++++++++++++++------------------------- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b02c2eb..8f125f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5438,6 +5438,7 @@ dependencies = [ "log", "random-string", "reqwest_client", + "rust-embed", "scope-backend-discord", "scope-chat", "scope-util", diff --git a/src/ui/Cargo.toml b/src/ui/Cargo.toml index 4c44451..1dfbc21 100644 --- a/src/ui/Cargo.toml +++ b/src/ui/Cargo.toml @@ -27,6 +27,7 @@ tokio = { version = "1.41.1", features = ["full"] } components = { package = "ui", git = "https://github.com/longbridgeapp/gpui-component", version = "0.1.0" } log = "0.4.22" random-string = "1.1.0" +rust-embed = "8.5.0" [features] default = ["gpui/x11"] diff --git a/src/ui/src/main.rs b/src/ui/src/main.rs index ffafc85..dd67808 100644 --- a/src/ui/src/main.rs +++ b/src/ui/src/main.rs @@ -7,20 +7,19 @@ use std::{fs, path::PathBuf, sync::Arc}; use app_state::AppState; use components::theme::Theme; use gpui::*; +use http_client::anyhow; -struct Assets { - base: PathBuf, -} +#[derive(rust_embed::RustEmbed)] +#[folder = "../../assets"] +struct Assets; impl AssetSource for Assets { fn load(&self, path: &str) -> Result>> { - fs::read(self.base.join(path)).map(|data| Some(std::borrow::Cow::Owned(data))).map_err(|e| e.into()) + Self::get(path).map(|f| Some(f.data)).ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) } fn list(&self, path: &str) -> Result> { - fs::read_dir(self.base.join(path)) - .map(|entries| entries.filter_map(|entry| entry.ok().and_then(|entry| entry.file_name().into_string().ok()).map(SharedString::from)).collect()) - .map_err(|e| e.into()) + Ok(Self::iter().filter_map(|p| if p.starts_with(path) { Some(p.into()) } else { None }).collect()) } } @@ -40,31 +39,26 @@ async fn main() { let app_state = Arc::new(AppState {}); - App::new() - .with_assets(Assets { - base: PathBuf::from("assets"), - }) - .with_http_client(Arc::new(reqwest_client::ReqwestClient::new())) - .run(move |cx: &mut AppContext| { - AppState::set_global(Arc::downgrade(&app_state), cx); - - if let Err(e) = init(app_state.clone(), cx) { - log::error!("{}", e); - return; - } - - Theme::sync_system_appearance(cx); - - let opts = WindowOptions { - window_decorations: Some(WindowDecorations::Client), - titlebar: Some(TitlebarOptions { - appears_transparent: true, - title: Some(SharedString::new_static("scope")), - ..Default::default() - }), + App::new().with_assets(Assets).with_http_client(Arc::new(reqwest_client::ReqwestClient::new())).run(move |cx: &mut AppContext| { + AppState::set_global(Arc::downgrade(&app_state), cx); + + if let Err(e) = init(app_state.clone(), cx) { + log::error!("{}", e); + return; + } + + Theme::sync_system_appearance(cx); + + let opts = WindowOptions { + window_decorations: Some(WindowDecorations::Client), + titlebar: Some(TitlebarOptions { + appears_transparent: true, + title: Some(SharedString::new_static("scope")), ..Default::default() - }; + }), + ..Default::default() + }; - cx.open_window(opts, |cx| cx.new_view(|cx| crate::app::App::new(cx))).unwrap(); - }); + cx.open_window(opts, |cx| cx.new_view(|cx| crate::app::App::new(cx))).unwrap(); + }); }