summaryrefslogtreecommitdiffstats
path: root/gui/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src/app')
-rw-r--r--gui/src/app/handler.rs162
-rw-r--r--gui/src/app/message.rs72
-rw-r--r--gui/src/app/mod.rs259
3 files changed, 493 insertions, 0 deletions
diff --git a/gui/src/app/handler.rs b/gui/src/app/handler.rs
new file mode 100644
index 0000000..a64b34b
--- /dev/null
+++ b/gui/src/app/handler.rs
@@ -0,0 +1,162 @@
+use iced::text_input;
+use iced::scrollable;
+use tokio::sync::RwLock;
+
+use crate::app::Distrox;
+use crate::app::Message;
+use crate::timeline::Timeline;
+
+pub(super) fn handle_message(app: &mut Distrox, message: Message) -> iced::Command<Message> {
+ log::trace!("Received message: {}", message.description());
+
+ if let Distrox::Loading { gossip_subscription_recv } = app {
+ if let Message::Loaded(profile) = message {
+ *app = Distrox::Loaded {
+ profile,
+
+ // Don't even try to think what hoops I am jumping through here...
+ gossip_subscription_recv: std::mem::replace(gossip_subscription_recv, RwLock::new(tokio::sync::oneshot::channel().1)),
+ scroll: scrollable::State::default(),
+ input: text_input::State::default(),
+ input_value: String::default(),
+ timeline: Timeline::new(),
+ log_visible: false,
+ log: std::collections::VecDeque::with_capacity(1000),
+ };
+ return iced::Command::none()
+ }
+ }
+
+ match (app, message) {
+ (Distrox::Loading { .. }, _) => iced::Command::none(),
+
+ (Distrox::Loaded { input_value, .. }, Message::InputChanged(input)) => {
+ *input_value = input;
+ iced::Command::none()
+ },
+
+ (Distrox::Loaded { profile, input_value, .. }, Message::CreatePost) => {
+ if !input_value.is_empty() {
+ let input = input_value.clone();
+ let profile = profile.clone();
+ log::trace!("Posting...");
+ iced::Command::perform(async move {
+ log::trace!("Posting: '{}'", input);
+ profile.write().await.post_text(input).await
+ },
+ |res| match res {
+ Ok(cid) => Message::PostCreated(cid),
+ Err(e) => Message::PostCreationFailed(e.to_string())
+ })
+ } else {
+ iced::Command::none()
+ }
+ },
+
+ (Distrox::Loaded { profile, input_value, .. }, Message::PostCreated(cid)) => {
+ *input_value = String::new();
+ log::info!("Post created: {}", cid);
+
+ let profile = profile.clone();
+ iced::Command::perform(async move {
+ if let Err(e) = profile.read().await.save().await {
+ Message::ProfileStateSavingFailed(e.to_string())
+ } else {
+ Message::ProfileStateSaved
+ }
+ }, |m: Message| -> Message { m })
+ },
+
+ (_, Message::ProfileStateSaved) => {
+ log::info!("Profile state saved");
+ iced::Command::none()
+ },
+
+ (_, Message::ProfileStateSavingFailed(e)) => {
+ log::error!("Saving profile failed: {}", e);
+ iced::Command::none()
+ },
+
+ (_, Message::PostCreationFailed(err)) => {
+ log::error!("Post creation failed: {}", err);
+ iced::Command::none()
+ },
+
+ (Distrox::Loaded { timeline, .. }, Message::PostLoaded((payload, content))) => {
+ timeline.push(payload, content);
+ iced::Command::none()
+ },
+
+ (_, Message::PostLoadingFailed) => {
+ log::error!("Failed to load some post, TODO: Better error logging");
+ iced::Command::none()
+ },
+
+ (_, Message::TimelineScrolled(f)) => {
+ log::trace!("Timeline scrolled: {}", f);
+ iced::Command::none()
+ },
+
+ (Distrox::Loaded { log_visible, .. }, Message::ToggleLog) => {
+ log::trace!("Log toggled");
+ *log_visible = !*log_visible;
+ iced::Command::none()
+ },
+
+ (_, Message::GossipMessage(source, msg)) => {
+ log::trace!("Received Gossip from {}: {:?}", source, msg);
+ iced::Command::perform(async {
+ Message::GossipHandled(msg)
+ }, |m: Message| -> Message { m })
+ },
+
+ (Distrox::Loaded { log, .. }, Message::GossipHandled(msg)) => {
+ use distrox_lib::gossip::GossipMessage;
+
+ log::trace!("Gossip handled, adding to log: {:?}", msg);
+ let msg = match msg {
+ GossipMessage::CurrentProfileState { peer_id, cid } => {
+ format!("Peer {:?} is at {:?}", peer_id, cid)
+ }
+ };
+ log.push_back(msg);
+ while log.len() > 1000 {
+ let _ = log.pop_front();
+ }
+ iced::Command::none()
+ },
+
+ (Distrox::Loaded { profile, .. }, Message::PublishGossipAboutMe) => {
+ let profile = profile.clone();
+ iced::Command::perform(async move {
+ if let Err(e) = profile.read().await.gossip_own_state("distrox".to_string()).await {
+ Message::GossippingFailed(e.to_string())
+ } else {
+ Message::OwnStateGossipped
+ }
+ }, |m: Message| -> Message { m })
+ },
+
+ (Distrox::Loaded { log, .. }, Message::OwnStateGossipped) => {
+ log::trace!("Gossipped own state");
+ log.push_back("Gossipped own state".to_string());
+ iced::Command::none()
+ },
+
+ (Distrox::Loaded { log, .. }, Message::GossippingFailed(e)) => {
+ log::trace!("Gossipped failed: {}", e);
+ log.push_back(format!("Gossipped failed: {}", e));
+ iced::Command::none()
+ },
+
+ (Distrox::Loaded { .. }, msg) => {
+ log::warn!("Unhandled message: {:?}", msg);
+ iced::Command::none()
+ }
+
+ (Distrox::FailedToStart, _) => {
+ unimplemented!()
+ },
+ }
+}
+
diff --git a/gui/src/app/message.rs b/gui/src/app/message.rs
new file mode 100644
index 0000000..60d9433
--- /dev/null
+++ b/gui/src/app/message.rs
@@ -0,0 +1,72 @@
+use std::sync::Arc;
+
+use cid::Cid;
+use tokio::sync::RwLock;
+
+use distrox_lib::gossip::GossipMessage;
+use distrox_lib::profile::Profile;
+use distrox_lib::types::Payload;
+
+use crate::gossip::GossipRecipe;
+
+#[derive(Clone, Debug)]
+pub enum Message {
+ Loaded(Arc<RwLock<Profile>>),
+ FailedToLoad(String),
+ ProfileStateSaved,
+ ProfileStateSavingFailed(String),
+
+ ToggleLog,
+
+ GossipMessage(ipfs::PeerId, GossipMessage),
+ GossipSubscriptionFailed(String),
+ GossipHandled(GossipMessage),
+
+ PublishGossipAboutMe,
+ OwnStateGossipped,
+ GossippingFailed(String),
+
+ InputChanged(String),
+ CreatePost,
+
+ PostCreated(Cid),
+ PostCreationFailed(String),
+
+ PostLoaded((Payload, String)),
+ PostLoadingFailed,
+
+ TimelineScrolled(f32),
+}
+
+impl Message {
+ pub fn description(&self) -> &'static str {
+ match self {
+ Message::Loaded(_) => "Loaded",
+ Message::FailedToLoad(_) => "FailedToLoad",
+ Message::ProfileStateSaved => "ProfileStateSaved",
+ Message::ProfileStateSavingFailed(_) => "ProfileStateSavingFailed",
+
+ Message::ToggleLog => "ToggleLog",
+
+ Message::GossipMessage(_, _) => "GossipMessage",
+ Message::GossipSubscriptionFailed(_) => "GossipSubscriptionFailed",
+ Message::GossipHandled(_) => "GossipHandled",
+
+ Message::PublishGossipAboutMe => "PublishGossipAboutMe",
+ Message::OwnStateGossipped => "OwnStateGossipped",
+ Message::GossippingFailed(_) => "GossippingFailed",
+
+ Message::InputChanged(_) => "InputChanged",
+ Message::CreatePost => "CreatePost",
+
+ Message::PostCreated(_) => "PostCreated",
+ Message::PostCreationFailed(_) => "PostCreationFailed",
+
+ Message::PostLoaded(_) => "PostLoaded",
+ Message::PostLoadingFailed => "PostLoadingFailed",
+
+ Message::TimelineScrolled(_) => "TimelineScrolled",
+ }
+ }
+}
+
diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs
new file mode 100644
index 0000000..f66bd7c
--- /dev/null
+++ b/gui/src/app/mod.rs
@@ -0,0 +1,259 @@
+use std::sync::Arc;
+
+use anyhow::Result;
+use iced::Application;
+use iced::Column;
+use iced::Container;
+use iced::Length;
+use iced::Row;
+use iced::Scrollable;
+use iced::TextInput;
+use iced::scrollable;
+use iced::text_input;
+use distrox_lib::profile::Profile;
+use tokio::sync::RwLock;
+
+use crate::timeline::Timeline;
+use crate::timeline::PostLoadingRecipe;
+
+mod message;
+pub use message::Message;
+
+mod handler;
+
+
+use crate::gossip::GossipRecipe;
+
+#[derive(Debug)]
+pub(crate) enum Distrox {
+ Loading {
+ gossip_subscription_recv: RwLock<tokio::sync::oneshot::Receiver<GossipRecipe>>,
+ },
+ Loaded {
+ profile: Arc<RwLock<Profile>>,
+ gossip_subscription_recv: RwLock<tokio::sync::oneshot::Receiver<GossipRecipe>>,
+
+ scroll: scrollable::State,
+ input: text_input::State,
+ input_value: String,
+ timeline: Timeline,
+
+ log_visible: bool,
+ log: std::collections::VecDeque<String>,
+ },
+ FailedToStart,
+}
+
+impl Application for Distrox {
+ type Executor = iced::executor::Default; // tokio
+ type Message = Message;
+ type Flags = String;
+
+ fn new(name: String) -> (Self, iced::Command<Self::Message>) {
+ let (gossip_subscription_sender, gossip_subscription_recv) = tokio::sync::oneshot::channel();
+ (
+ Distrox::Loading {
+ gossip_subscription_recv: RwLock::new(gossip_subscription_recv),
+ },
+
+ iced::Command::perform(async move {
+ let profile = match Profile::load(&name).await {
+ Err(e) => return Message::FailedToLoad(e.to_string()),
+ Ok(instance) => Arc::new(RwLock::new(instance)),
+ };
+
+ if let Err(e) = profile
+ .read()
+ .await
+ .client()
+ .pubsub_subscribe("distrox".to_string())
+ .await
+ .map_err(anyhow::Error::from)
+ .map(|stream| {
+ log::trace!("Subscription to 'distrox' pubsub channel worked");
+ GossipRecipe::new(profile.clone(), stream)
+ })
+ .and_then(|s| gossip_subscription_sender.send(s).map_err(|_| anyhow::anyhow!("Failed to initialize gossipping module")))
+ {
+ log::error!("Failed to load gossip recipe");
+ return Message::FailedToLoad(e.to_string())
+ }
+
+ Message::Loaded(profile)
+ }, |m: Message| -> Message { m })
+ )
+ }
+
+ fn title(&self) -> String {
+ String::from("distrox")
+ }
+
+ fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> {
+ handler::handle_message(self, message)
+ }
+
+ fn view(&mut self) -> iced::Element<Self::Message> {
+ match self {
+ Distrox::Loading { .. } => {
+ let text = iced::Text::new("Loading");
+
+ let content = Column::new()
+ .spacing(20)
+ .push(text);
+
+ Container::new(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+
+ Distrox::Loaded { input, input_value, timeline, scroll, log_visible, log, .. } => {
+ let left_column = Column::new()
+ .into();
+
+ let mid_column = Column::new()
+ .push({
+ let input = TextInput::new(
+ input,
+ "What do you want to tell the world?",
+ input_value,
+ Message::InputChanged,
+ )
+ .padding(15)
+ .size(12)
+ .on_submit(Message::CreatePost);
+
+ let timeline = timeline.view();
+
+ Scrollable::new(scroll)
+ .padding(40)
+ .push(input)
+ .push(timeline)
+ })
+ .into();
+
+ let right_column = Column::new()
+ .into();
+
+ let content = Row::with_children(vec![
+ left_column,
+ mid_column,
+ right_column
+ ])
+ .spacing(20)
+ .height(Length::Fill)
+ .width(Length::Fill);
+
+ let content = Column::new()
+ .height(Length::Fill)
+ .width(Length::Fill)
+ .push(content);
+
+ if *log_visible {
+ let log = Column::with_children({
+ log.iter()
+ .map(iced::Text::new)
+ .map(|txt| txt.size(8))
+ .map(iced::Element::from)
+ .collect()
+ });
+ content.push(log)
+ } else {
+ content
+ }.into()
+ }
+
+ Distrox::FailedToStart => {
+ unimplemented!()
+ }
+ }
+ }
+
+ fn subscription(&self) -> iced::Subscription<Self::Message> {
+ let post_loading_subs = match self {
+ Distrox::Loaded { profile, .. } => {
+ let profile = match profile.try_read() {
+ Err(_) => return iced::Subscription::none(),
+ Ok(p) => p,
+ };
+
+ match profile.head() {
+ None => iced::Subscription::none(),
+ Some(head) => {
+ iced::Subscription::from_recipe({
+ PostLoadingRecipe::new(profile.client().clone(), head.clone())
+ })
+ }
+ }
+ }
+ _ => iced::Subscription::none(),
+ };
+
+ let keyboard_subs = {
+ use iced_native::event::Event;
+
+ iced_native::subscription::events_with(|event, _| {
+ match event {
+ Event::Keyboard(iced_native::keyboard::Event::KeyPressed { key_code, .. }) => {
+ if key_code == iced_native::keyboard::KeyCode::F11 {
+ Some(Message::ToggleLog)
+ } else {
+ None
+ }
+ },
+ _ => None,
+ }
+ })
+ };
+
+ let gossip_sub = match self {
+ Distrox::Loaded { gossip_subscription_recv, .. } => {
+ match gossip_subscription_recv.try_write() {
+ Err(_) => None,
+ Ok(mut sub) => sub.try_recv()
+ .ok()
+ .map(|sub| iced::Subscription::from_recipe(sub)),
+ }
+ },
+ _ => None,
+ };
+
+ let gossip_sending_sub = {
+ iced::time::every(std::time::Duration::from_secs(5))
+ .map(|_| Message::PublishGossipAboutMe)
+ };
+
+ let mut subscriptions = vec![
+ post_loading_subs,
+ keyboard_subs,
+ gossip_sending_sub,
+ ];
+
+ if let Some(gossip_sub) = gossip_sub {
+ subscriptions.push(gossip_sub);
+ }
+
+ iced::Subscription::batch(subscriptions)
+ }
+
+}
+
+pub fn run(name: String) -> Result<()> {
+ let settings = iced::Settings {
+ window: iced::window::Settings {
+ resizable: true,
+ decorations: true,
+ transparent: false,
+ always_on_top: false,
+ ..iced::window::Settings::default()
+ },
+ flags: name,
+ exit_on_close_request: true,
+ ..iced::Settings::default()
+ };
+
+ Distrox::run(settings).map_err(anyhow::Error::from)
+}
+