diff options
Diffstat (limited to 'gui/src/app')
-rw-r--r-- | gui/src/app/handler.rs | 162 | ||||
-rw-r--r-- | gui/src/app/message.rs | 72 | ||||
-rw-r--r-- | gui/src/app/mod.rs | 259 |
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) +} + |