From 0682a97f9c0f5961dd1a22ba2ff8e545cef76400 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 18 Dec 2021 12:53:05 +0100 Subject: Implement Timeline for that, we remove the distrox_lib::config::Config type which was unused anyways, because it makes the whole implementation a bit less complex. Signed-off-by: Matthias Beyer --- cli/src/profile.rs | 9 +++-- gui/Cargo.toml | 8 +++++ gui/src/app.rs | 57 ++++++++++++++++++++++++-------- gui/src/main.rs | 2 +- gui/src/post.rs | 47 ++++++++++++++++++++++++++ gui/src/timeline.rs | 86 +++++++++++++++++++++++++++++++++++++++++++----- gui/src/timeline_post.rs | 29 ---------------- lib/src/client.rs | 27 +++++---------- lib/src/config.rs | 10 ------ lib/src/lib.rs | 1 - lib/src/profile/mod.rs | 22 ++++++------- 11 files changed, 200 insertions(+), 98 deletions(-) create mode 100644 gui/src/post.rs delete mode 100644 gui/src/timeline_post.rs delete mode 100644 lib/src/config.rs diff --git a/cli/src/profile.rs b/cli/src/profile.rs index a3cfb07..87b1e54 100644 --- a/cli/src/profile.rs +++ b/cli/src/profile.rs @@ -6,7 +6,6 @@ use anyhow::Context; use anyhow::Result; use clap::ArgMatches; -use distrox_lib::config::Config; use distrox_lib::profile::Profile; use distrox_lib::types::Payload; @@ -25,7 +24,7 @@ async fn profile_create(matches: &ArgMatches) -> Result<()> { let state_dir = Profile::state_dir_path(&name)?; log::info!("Creating '{}' in {}", name, state_dir.display()); - let profile = Profile::create(&state_dir, &name, Config::default()).await?; + let profile = Profile::create(&state_dir, &name).await?; log::info!("Saving..."); profile.save().await?; @@ -45,7 +44,7 @@ async fn profile_serve(matches: &ArgMatches) -> Result<()> { let state_dir = Profile::state_dir_path(&name)?; log::info!("Loading '{}' from {}", name, state_dir.display()); - let profile = Profile::load(Config::default(), &name).await?; + let profile = Profile::load(&name).await?; log::info!("Profile loaded"); if let Some(head) = profile.head().as_ref() { log::info!("Profile HEAD = {}", head); @@ -86,7 +85,7 @@ async fn profile_post(matches: &ArgMatches) -> Result<()> { log::info!("Creating '{}' in {}", name, state_dir.display()); log::info!("Loading '{}' from {}", name, state_dir.display()); - let mut profile = Profile::load(Config::default(), &name).await?; + let mut profile = Profile::load(&name).await?; log::info!("Profile loaded"); log::info!("Profile HEAD = {:?}", profile.head()); @@ -107,7 +106,7 @@ async fn profile_cat(matches: &ArgMatches) -> Result<()> { log::info!("Creating '{}' in {}", name, state_dir.display()); log::info!("Loading '{}' from {}", name, state_dir.display()); - let profile = Profile::load(Config::default(), &name).await?; + let profile = Profile::load(&name).await?; log::info!("Profile loaded"); if let Some(head) = profile.head() { log::info!("Profile HEAD = {:?}", head); diff --git a/gui/Cargo.toml b/gui/Cargo.toml index 539d59a..30b6ac6 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -28,6 +28,7 @@ xdg = "2.4" tracing = "0.1" ctrlc = "3.2" mime = "0.3" +tokio = { version = "1", features = ["full", "rt", "macros"] } [dependencies.ipfs] git = "https://github.com/rs-ipfs/rust-ipfs/" @@ -39,6 +40,13 @@ rev = "6f3a608717e08330187871516b1ab54a9a2192a3" default-features = false features = ["glow", "tokio", "debug"] + +[dependencies.iced_native] +git = "https://github.com/iced-rs/iced" +rev = "6f3a608717e08330187871516b1ab54a9a2192a3" +default-features = false + + [dependencies.distrox-lib] path = "../lib" diff --git a/gui/src/app.rs b/gui/src/app.rs index f9dedaa..8f4d579 100644 --- a/gui/src/app.rs +++ b/gui/src/app.rs @@ -11,8 +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 { @@ -41,6 +42,11 @@ pub enum Message { PostCreated(cid::Cid), PostCreationFailed(String), + + PostLoaded((distrox_lib::types::Payload, String)), + PostLoadingFailed, + + TimelineScrolled(f32), } impl Application for Distrox { @@ -52,7 +58,7 @@ impl Application for Distrox { ( Distrox::Loading, iced::Command::perform(async move { - match Profile::load(Config::default(), &name).await { + match Profile::load(&name).await { Err(_) => Message::FailedToLoad, Ok(instance) => { Message::Loaded(Arc::new(instance)) @@ -76,7 +82,7 @@ impl Application for Distrox { scroll: scrollable::State::default(), input: text_input::State::default(), input_value: String::default(), - timeline: Timeline::new() + timeline: Timeline::new(), }; *self = Distrox::Loaded(state); } @@ -99,10 +105,10 @@ 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(); iced::Command::perform(async move { - profile.client().post_text_blob(input).await + client.post_text_blob(input).await }, |res| match res { Ok(cid) => Message::PostCreated(cid), @@ -111,6 +117,18 @@ impl Application for Distrox { } } + 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); + } + _ => {} } } @@ -149,17 +167,12 @@ impl Application for Distrox { .size(12) .on_submit(Message::CreatePost); - let content = Column::new() - .max_width(800) - .spacing(20) - .push(input) - .push(state.timeline.view()); + 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() } @@ -169,6 +182,24 @@ impl Application for Distrox { } } + fn subscription(&self) -> iced::Subscription { + 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 581877b..6120152 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -3,7 +3,7 @@ use anyhow::Result; mod app; mod cli; mod timeline; -mod timeline_post; +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 { + 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 index 82c8da1..8a13d5d 100644 --- a/gui/src/timeline.rs +++ b/gui/src/timeline.rs @@ -1,26 +1,96 @@ -use crate::timeline_post::TimelinePost; +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 + posts: Vec, + scrollable: ScrollableState, } impl Timeline { pub fn new() -> Self { Self { - posts: Vec::with_capacity(100), + posts: Vec::new(), + scrollable: ScrollableState::new(), } } - pub fn update(&mut self) { - self.posts.iter_mut().for_each(|mut post| post.update()); + pub fn push(&mut self, payload: Payload, content: String) { + self.posts.push(Post::new(payload, content)); } - pub fn view(&self) -> iced::Column { + pub fn view(&mut self) -> iced::Element { + 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(iced::Column::new(), |c, post| { - c.push(post.view()) + .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 iced_native::subscription::Recipe 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, _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), + }) + }) } } diff --git a/gui/src/timeline_post.rs b/gui/src/timeline_post.rs deleted file mode 100644 index 8b53898..0000000 --- a/gui/src/timeline_post.rs +++ /dev/null @@ -1,29 +0,0 @@ -#[derive(Debug)] -pub struct TimelinePost { - mime: mime::Mime, - content: PostContent, -} - -#[derive(Debug)] -pub enum PostContent { - Text(String) -} - -impl TimelinePost { - pub fn update(&mut self) { - () - } - - pub fn view(&self) -> iced::Row { - iced::Row::new() - .push({ - iced::Text::new(self.mime.as_ref().to_string()) - }) - .push({ - match self.content { - PostContent::Text(ref txt) => iced::Text::new(txt.clone()), - } - }) - .into() - } -} diff --git a/lib/src/client.rs b/lib/src/client.rs index 68a5622..a6a51d4 100644 --- a/lib/src/client.rs +++ b/lib/src/client.rs @@ -3,7 +3,6 @@ use std::convert::TryFrom; use anyhow::Result; use ipfs::Cid; -use crate::config::Config; use crate::ipfs_client::IpfsClient; use crate::types::Node; use crate::types::Payload; @@ -12,21 +11,17 @@ use crate::types::DateTime; #[derive(Clone)] pub struct Client { pub(crate) ipfs: IpfsClient, - config: Config, } impl std::fmt::Debug for Client { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "Client {{ config: {:?} }}", self.config) + write!(f, "Client {{ }}") } } impl Client { - pub fn new(ipfs: IpfsClient, config: Config) -> Self { - Client { - ipfs, - config - } + pub fn new(ipfs: IpfsClient) -> Self { + Client { ipfs } } pub async fn exit(self) -> Result<()> { @@ -132,7 +127,6 @@ mod tests { use cid::Cid; use crate::client::Client; - use crate::config::Config; use crate::ipfs_client::IpfsClient; use crate::types::DateTime; @@ -154,8 +148,7 @@ mod tests { async fn test_post_text_blob() { let _ = env_logger::try_init(); let ipfs = mk_ipfs().await; - let config = Config::default(); - let client = Client::new(ipfs, config); + let client = Client::new(ipfs); let cid = client.post_text_blob(String::from("text")).await; assert!(cid.is_ok()); @@ -168,8 +161,7 @@ mod tests { async fn test_post_text_node() { let _ = env_logger::try_init(); let ipfs = mk_ipfs().await; - let config = Config::default(); - let client = Client::new(ipfs, config); + let client = Client::new(ipfs); let datetime = mkdate(2021, 11, 27, 12, 30, 0); @@ -184,8 +176,7 @@ mod tests { async fn test_post_text_node_roundtrip() { let _ = env_logger::try_init(); let ipfs = mk_ipfs().await; - let config = Config::default(); - let client = Client::new(ipfs, config); + let client = Client::new(ipfs); let datetime = mkdate(2021, 11, 27, 12, 30, 0); @@ -222,8 +213,7 @@ mod tests { async fn test_post_text_chain() { let _ = env_logger::try_init(); let ipfs = mk_ipfs().await; - let config = Config::default(); - let client = Client::new(ipfs, config); + let client = Client::new(ipfs); let chain_elements = vec![ (mkdate(2021, 11, 27, 12, 30, 0), "text1", "bafyreidaxkxog3bssyxxjxlsubgg6wauxbobp7gwyucs6gwzyrtsavb7yu"), @@ -252,8 +242,7 @@ mod tests { async fn test_post_text_dag() { let _ = env_logger::try_init(); let ipfs = mk_ipfs().await; - let config = Config::default(); - let client = Client::new(ipfs, config); + let client = Client::new(ipfs); async fn post_chain(client: &Client, chain_elements: &Vec<(DateTime, &str, &str)>) { let mut prev: Option = None; diff --git a/lib/src/config.rs b/lib/src/config.rs deleted file mode 100644 index 0d25e78..0000000 --- a/lib/src/config.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[derive(Debug)] -pub struct Config { -} - -impl Default for Config { - fn default() -> Self { - Config { } - } -} - diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 0cccef9..04fc959 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,5 +1,4 @@ pub mod client; -pub mod config; pub mod consts; pub mod ipfs_client; pub mod profile; diff --git a/lib/src/profile/mod.rs b/lib/src/profile/mod.rs index 5f5abaa..39c4829 100644 --- a/lib/src/profile/mod.rs +++ b/lib/src/profile/mod.rs @@ -5,7 +5,6 @@ use anyhow::Context; use anyhow::Result; use crate::client::Client; -use crate::config::Config; use crate::ipfs_client::IpfsClient; mod state; @@ -20,7 +19,7 @@ pub struct Profile { } impl Profile { - pub async fn create(state_dir: &StateDir, name: &str, config: Config) -> Result { + pub async fn create(state_dir: &StateDir, name: &str) -> Result { let bootstrap = vec![]; // TODO let mdns = true; // TODO let keypair = ipfs::Keypair::generate_ed25519(); @@ -40,20 +39,20 @@ impl Profile { .start() .await?; tokio::task::spawn(fut); - Self::new(ipfs, config, name.to_string(), keypair).await + Self::new(ipfs, name.to_string(), keypair).await } - pub async fn new_inmemory(config: Config, name: &str) -> Result { + pub async fn new_inmemory(name: &str) -> Result { let mut opts = ipfs::IpfsOptions::inmemory_with_generated_keys(); opts.mdns = true; let keypair = opts.keypair.clone(); let (ipfs, fut): (ipfs::Ipfs<_>, _) = ipfs::UninitializedIpfs::<_>::new(opts).start().await.unwrap(); tokio::task::spawn(fut); - Self::new(ipfs, config, format!("inmemory-{}", name), keypair).await + Self::new(ipfs, format!("inmemory-{}", name), keypair).await } - async fn new(ipfs: IpfsClient, config: Config, profile_name: String, keypair: libp2p::identity::Keypair) -> Result { - let client = Client::new(ipfs, config); + async fn new(ipfs: IpfsClient, profile_name: String, keypair: libp2p::identity::Keypair) -> Result { + let client = Client::new(ipfs); let state = ProfileState::new(profile_name, keypair); Ok(Profile { state, client }) } @@ -123,7 +122,7 @@ impl Profile { .map_err(anyhow::Error::from) } - pub async fn load(config: Config, name: &str) -> Result { + pub async fn load(name: &str) -> Result { let state_dir_path = Self::state_dir_path(name)?; log::trace!("state_dir_path = {:?}", state_dir_path.display()); let state: ProfileState = ProfileStateSaveable::load_from_disk(&state_dir_path) @@ -156,7 +155,7 @@ impl Profile { log::debug!("Profile loading finished"); Ok(Profile { state, - client: Client::new(ipfs, config), + client: Client::new(ipfs), }) } @@ -171,12 +170,11 @@ impl Profile { mod tests { use super::*; use std::convert::TryFrom; - use crate::config::Config; #[tokio::test] async fn test_create_profile() { let _ = env_logger::try_init(); - let profile = Profile::new_inmemory(Config::default(), "test-create-profile").await; + let profile = Profile::new_inmemory("test-create-profile").await; assert!(profile.is_ok()); let exit = profile.unwrap().exit().await; assert!(exit.is_ok(), "Not cleanly exited: {:?}", exit); @@ -185,7 +183,7 @@ mod tests { #[tokio::test] async fn test_create_profile_and_helloworld() { let _ = env_logger::try_init(); - let profile = Profile::new_inmemory(Config::default(), "test-create-profile-and-helloworld").await; + let profile = Profile::new_inmemory("test-create-profile-and-helloworld").await; assert!(profile.is_ok()); let profile = profile.unwrap(); assert!(profile.head().is_none()); -- cgit v1.2.3