diff options
Diffstat (limited to 'gui/src')
-rw-r--r-- | gui/src/app.rs | 72 | ||||
-rw-r--r-- | gui/src/main.rs | 2 | ||||
-rw-r--r-- | gui/src/post.rs | 47 | ||||
-rw-r--r-- | gui/src/timeline.rs | 96 |
4 files changed, 204 insertions, 13 deletions
diff --git a/gui/src/app.rs b/gui/src/app.rs index d60f7b8..bc5fa17 100644 --- a/gui/src/app.rs +++ b/gui/src/app.rs @@ -11,7 +11,9 @@ use iced::scrollable; use iced::text_input; use distrox_lib::profile::Profile; -use distrox_lib::config::Config; +use crate::timeline::Timeline; +use crate::timeline::PostLoadingRecipe; +use crate::post::Post; #[derive(Debug)] enum Distrox { @@ -27,10 +29,11 @@ struct State { scroll: scrollable::State, input: text_input::State, input_value: String, + timeline: Timeline, } #[derive(Debug, Clone)] -enum Message { +pub enum Message { Loaded(Arc<Profile>), FailedToLoad, @@ -39,6 +42,11 @@ enum Message { PostCreated(cid::Cid), PostCreationFailed(String), + + PostLoaded((distrox_lib::types::Payload, String)), + PostLoadingFailed, + + TimelineScrolled(f32), } impl Application for Distrox { @@ -50,7 +58,7 @@ impl Application for Distrox { ( Distrox::Loading, iced::Command::perform(async move { - match Profile::new_inmemory(Config::default(), &name).await { + match Profile::load(&name).await { Err(_) => Message::FailedToLoad, Ok(instance) => { Message::Loaded(Arc::new(instance)) @@ -74,6 +82,7 @@ impl Application for Distrox { scroll: scrollable::State::default(), input: text_input::State::default(), input_value: String::default(), + timeline: Timeline::new(), }; *self = Distrox::Loaded(state); } @@ -96,10 +105,12 @@ impl Application for Distrox { Message::CreatePost => { if !state.input_value.is_empty() { - let profile = state.profile.clone(); let input = state.input_value.clone(); + let client = state.profile.client().clone(); + log::trace!("Posting..."); iced::Command::perform(async move { - profile.client().post_text_blob(input).await + log::trace!("Posting: '{}'", input); + client.post_text_blob(input).await }, |res| match res { Ok(cid) => Message::PostCreated(cid), @@ -108,6 +119,27 @@ impl Application for Distrox { } } + Message::PostCreated(cid) => { + state.input_value = String::new(); + log::info!("Post created: {}", cid); + } + + Message::PostCreationFailed(err) => { + log::error!("Post creation failed: {}", err); + } + + Message::PostLoaded((payload, content)) => { + state.timeline.push(payload, content); + } + + Message::PostLoadingFailed => { + log::error!("Failed to load some post, TODO: Better error logging"); + } + + Message::TimelineScrolled(f) => { + log::trace!("Timeline scrolled: {}", f); + } + _ => {} } } @@ -143,19 +175,15 @@ impl Application for Distrox { Message::InputChanged, ) .padding(15) - .size(30) + .size(12) .on_submit(Message::CreatePost); - let content = Column::new() - .max_width(800) - .spacing(20) - .push(input); + let timeline = state.timeline.view(); Scrollable::new(&mut state.scroll) .padding(40) - .push( - Container::new(content).width(Length::Fill).center_x(), - ) + .push(input) + .push(timeline) .into() } @@ -165,6 +193,24 @@ impl Application for Distrox { } } + fn subscription(&self) -> iced::Subscription<Self::Message> { + match self { + Distrox::Loaded(state) => { + let head = state.profile.head(); + + match head { + None => iced::Subscription::none(), + Some(head) => { + iced::Subscription::from_recipe({ + PostLoadingRecipe::new(state.profile.client().clone(), head.clone()) + }) + } + } + } + _ => iced::Subscription::none(), + } + } + } pub fn run(name: String) -> Result<()> { diff --git a/gui/src/main.rs b/gui/src/main.rs index 32d6d61..6120152 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -2,6 +2,8 @@ use anyhow::Result; mod app; mod cli; +mod timeline; +mod post; fn main() -> Result<()> { let _ = env_logger::try_init()?; diff --git a/gui/src/post.rs b/gui/src/post.rs new file mode 100644 index 0000000..11c5f6a --- /dev/null +++ b/gui/src/post.rs @@ -0,0 +1,47 @@ +use crate::app::Message; +use distrox_lib::types::Payload; + +#[derive(Clone, Debug)] +pub struct Post { + payload: Payload, + content: String, +} + +impl Post { + pub fn new(payload: Payload, content: String) -> Self { + Self { payload, content } + } + + pub fn view(&self) -> iced::Element<Message> { + iced::Column::new() + .push({ + iced::Row::new() + .height(iced::Length::Shrink) + .width(iced::Length::Fill) + .push({ + iced::Column::new() + .width(iced::Length::Fill) + .align_items(iced::Alignment::Start) + .push({ + iced::Text::new(self.payload.timestamp().inner().to_string()) + .size(10) + }) + }) + .push({ + iced::Column::new() + .width(iced::Length::Fill) + .align_items(iced::Alignment::End) + .push({ + iced::Text::new(self.payload.content().to_string()) + .size(10) + }) + }) + }) + .push(iced::rule::Rule::horizontal(10)) + .push({ + iced::Text::new(self.content.clone()).size(12) + }) + .push(iced::rule::Rule::horizontal(10)) + .into() + } +} diff --git a/gui/src/timeline.rs b/gui/src/timeline.rs new file mode 100644 index 0000000..8a13d5d --- /dev/null +++ b/gui/src/timeline.rs @@ -0,0 +1,96 @@ +use anyhow::Result; +use chrono::DateTime; +use chrono::Utc; +use futures::StreamExt; + +use iced_native::widget::scrollable::State as ScrollableState; + +use crate::app::Message; +use crate::post::Post; +use distrox_lib::client::Client; +use distrox_lib::stream::NodeStreamBuilder; +use distrox_lib::types::Payload; + +#[derive(Debug)] +pub struct Timeline { + posts: Vec<Post>, + scrollable: ScrollableState, +} + +impl Timeline { + pub fn new() -> Self { + Self { + posts: Vec::new(), + scrollable: ScrollableState::new(), + } + } + + pub fn push(&mut self, payload: Payload, content: String) { + self.posts.push(Post::new(payload, content)); + } + + pub fn view(&mut self) -> iced::Element<Message> { + let scrollable = iced::Scrollable::new(&mut self.scrollable) + .padding(10) + .spacing(20) + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .on_scroll(move |offset| { + Message::TimelineScrolled(offset) + }); + + self.posts + .iter() + .fold(scrollable, |scrollable, post| { + scrollable.push(post.view()) + }) + .into() + } +} + +pub struct PostLoadingRecipe { + client: Client, + head: cid::Cid, +} + +impl PostLoadingRecipe { + pub fn new(client: Client, head: cid::Cid) -> Self { + Self { client, head } + } +} + +// Make sure iced can use our download stream +impl<H, I> iced_native::subscription::Recipe<H, I> for PostLoadingRecipe +where + H: std::hash::Hasher, +{ + type Output = Message; + + fn hash(&self, state: &mut H) { + use std::hash::Hash; + self.head.to_bytes().hash(state); + } + + fn stream(self: Box<Self>, _input: futures::stream::BoxStream<'static, I>) -> futures::stream::BoxStream<'static, Self::Output> + { + log::debug!("Streaming posts starting at HEAD = {:?}", self.head); + Box::pin({ + NodeStreamBuilder::starting_from(self.head.clone()) + .into_stream(self.client.clone()) + .then(move |node| { + let client = self.client.clone(); + + async move { + let payload = client.get_payload(node?.payload()).await?; + let content = client.get_content_text(payload.content()).await?; + + Ok((payload, content)) + } + }) + .map(|res: Result<_>| match res { + Err(_) => Message::PostLoadingFailed, + Ok(p) => Message::PostLoaded(p), + }) + }) + } +} |