summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Beyer <mail@beyermatthias.de>2021-12-18 17:32:41 +0100
committerMatthias Beyer <mail@beyermatthias.de>2021-12-18 17:32:41 +0100
commit1c90ee16661006c8cbfd628a93608566a9bc6b76 (patch)
treea6ac58c5013df6b7e663f533c8ae8f0cbf4b112e
parentaa5c1f79dcdfae35f10835ea43bac6f451d7149d (diff)
parent19ea5662c661d5a7ab3eeeafa1f7a7846ea4f762 (diff)
Merge branch 'gui-timeline'
-rw-r--r--cli/src/profile.rs11
-rw-r--r--gui/Cargo.toml9
-rw-r--r--gui/src/app.rs72
-rw-r--r--gui/src/main.rs2
-rw-r--r--gui/src/post.rs47
-rw-r--r--gui/src/timeline.rs96
-rw-r--r--lib/src/client.rs28
-rw-r--r--lib/src/config.rs10
-rw-r--r--lib/src/lib.rs1
-rw-r--r--lib/src/profile/mod.rs26
-rw-r--r--lib/src/stream.rs2
-rw-r--r--lib/src/types/payload.rs2
12 files changed, 241 insertions, 65 deletions
diff --git a/cli/src/profile.rs b/cli/src/profile.rs
index a0025b6..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,12 +106,12 @@ 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);
NodeStreamBuilder::starting_from(head.clone())
- .into_stream(profile.client())
+ .into_stream(profile.client().clone())
.then(|node| async {
match node {
Err(e) => Err(e),
diff --git a/gui/Cargo.toml b/gui/Cargo.toml
index e3699a8..30b6ac6 100644
--- a/gui/Cargo.toml
+++ b/gui/Cargo.toml
@@ -27,6 +27,8 @@ getset = "0.1"
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/"
@@ -38,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 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),
+ })
+ })
+ }
+}
diff --git a/lib/src/client.rs b/lib/src/client.rs
index da8ad94..a6a51d4 100644
--- a/lib/src/client.rs
+++ b/lib/src/client.rs
@@ -3,29 +3,25 @@ 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;
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<()> {
@@ -131,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;
@@ -153,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());
@@ -167,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);
@@ -183,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);
@@ -221,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"),
@@ -251,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<ipfs::Cid> = 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 050d53d..39c4829 100644
--- a/lib/src/profile/mod.rs
+++ b/lib/src/profile/mod.rs
@@ -5,22 +5,21 @@ use anyhow::Context;
use anyhow::Result;
use crate::client::Client;
-use crate::config::Config;
use crate::ipfs_client::IpfsClient;
mod state;
use state::*;
-#[derive(Debug, getset::Getters)]
+#[derive(Debug, getset::Getters, getset::MutGetters)]
pub struct Profile {
state: ProfileState,
- #[getset(get = "pub")]
+ #[getset(get = "pub", get_mut = "pub")]
client: Client,
}
impl Profile {
- pub async fn create(state_dir: &StateDir, name: &str, config: Config) -> Result<Self> {
+ pub async fn create(state_dir: &StateDir, name: &str) -> Result<Self> {
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<Self> {
+ pub async fn new_inmemory(name: &str) -> Result<Self> {
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<Self> {
- let client = Client::new(ipfs, config);
+ async fn new(ipfs: IpfsClient, profile_name: String, keypair: libp2p::identity::Keypair) -> Result<Self> {
+ 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<Self> {
+ pub async fn load(name: &str) -> Result<Self> {
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());
diff --git a/lib/src/stream.rs b/lib/src/stream.rs
index 01548e6..2dd2781 100644
--- a/lib/src/stream.rs
+++ b/lib/src/stream.rs
@@ -15,7 +15,7 @@ impl NodeStreamBuilder {
}
}
- pub fn into_stream<'a>(self, client: &'a Client) -> impl futures::stream::Stream<Item = Result<Node>> + 'a {
+ pub fn into_stream(self, client: Client) -> impl futures::stream::Stream<Item = Result<Node>> {
futures::stream::unfold((client, self.state), move |(client, mut state)| {
async move {
if let Some(node_cid) = state.pop() {
diff --git a/lib/src/types/payload.rs b/lib/src/types/payload.rs
index a11b215..33b2342 100644
--- a/lib/src/types/payload.rs
+++ b/lib/src/types/payload.rs
@@ -4,7 +4,7 @@ use anyhow::Result;
use crate::types::DateTime;
-#[derive(Debug, Eq, PartialEq, getset::Getters)]
+#[derive(Clone, Debug, Eq, PartialEq, getset::Getters)]
pub struct Payload {
// TODO: Make this a mime::Mime, but as this type does not impl Serialize/Deserialize, we
// cannot do this trivially yet