Skip to content

Instantly share code, notes, and snippets.

@intendednull
Last active July 3, 2020 06:13
Show Gist options
  • Save intendednull/5393984aaf415a446b09745f8f1093ae to your computer and use it in GitHub Desktop.
Save intendednull/5393984aaf415a446b09745f8f1093ae to your computer and use it in GitHub Desktop.
// Copyright 2020 Noah Corona
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::rc::Rc;
use yew::format::Json;
use yew::services::{storage::Area, StorageService};
use yew::worker::*;
use yew::{html, Callback, Children, Component, ComponentLink, Html, Properties, ShouldRender};
use yewtil::future::LinkFuture;
use my_app_model::{AccountError, UserCurrent, UserLogout, UserPublic};
use crate::client::request;
const STORAGE_KEY: &str = "my_app.storage.state";
type Setter = Box<dyn FnOnce(&mut AppState)>;
pub enum Request {
SetState(Setter),
GetState,
Subscribe,
UnSubscribe,
}
pub enum Response {
State(Rc<AppState>),
}
pub enum Msg {
SetState(Setter),
SyncState,
}
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct AppState {
pub user: Option<UserPublic>,
pub sidebar_toggled: bool,
}
impl AppState {
fn toggle_sidebar(&mut self) {
self.sidebar_toggled = !self.sidebar_toggled;
}
}
pub struct AppStateService {
link: AgentLink<AppStateService>,
storage: Option<StorageService>,
pub state: Rc<AppState>,
subscriptions: HashSet<HandlerId>,
}
impl Agent for AppStateService {
type Reach = Context<Self>;
type Message = Msg;
type Input = Request;
type Output = Response;
fn create(link: AgentLink<Self>) -> Self {
let storage = StorageService::new(Area::Session)
.map_err(|e| {
log::error!("Storage service error: {}", e);
e
})
.ok();
let mut state = Self {
state: Default::default(),
subscriptions: Default::default(),
storage,
link,
};
state.load_state();
// Sync state with server
state.link.send_message(Msg::SyncState);
state
}
fn update(&mut self, msg: Self::Message) {
match msg {
Msg::SetState(setter) => self.set_state(setter),
Msg::SyncState => {
self.update_user();
}
}
}
fn handle_input(&mut self, msg: Self::Input, who: HandlerId) {
match msg {
Request::SetState(setter) => {
self.set_state(setter);
log::info!("State changed: {:?}", self.state);
// Will be notified if subscribed, only send here if it isn't.
if !self.subscriptions.contains(&who) {
self.link.respond(who, Response::State(self.state.clone()));
}
}
Request::GetState => {
self.link.respond(who, Response::State(self.state.clone()));
}
Request::Subscribe => {
self.subscriptions.insert(who);
log::info!("Subscriptions: {:?}", self.subscriptions);
self.link.respond(who, Response::State(self.state.clone()));
}
Request::UnSubscribe => {
self.subscriptions.remove(&who);
log::info!("Subscriptions: {:?}", self.subscriptions);
}
}
}
}
impl AppStateService {
fn set_state(&mut self, setter: impl FnOnce(&mut AppState)) {
setter(Rc::make_mut(&mut self.state));
for who in self.subscriptions.iter() {
self.link
.respond(who.clone(), Response::State(self.state.clone()));
}
self.save_state();
}
fn update_user(&self) {
self.get_current_user(|result| {
Msg::SetState(Box::new(move |state: &mut AppState| {
state.user = result.clone().ok();
}))
});
}
fn get_current_user(&self, cb: impl Fn(Result<UserPublic, AccountError>) -> Msg + 'static) {
let future = request(UserCurrent, move |result: Result<_, AccountError>| {
cb(result)
});
self.link.send_future(future);
}
fn load_state(&mut self) {
if let Some(Json(Ok(state))) = self.storage.as_mut().map(|s| s.restore(STORAGE_KEY)) {
self.state = state;
log::trace!("Loaded state from storage");
} else {
log::error!("Error loading app state from session storage");
}
}
fn save_state(&mut self) {
if let Some(storage) = &mut self.storage {
storage.store(STORAGE_KEY, Json(&self.state));
log::trace!("Done saving storage");
} else {
log::error!("Error saving app state to session storage");
}
}
}
/// Manages state for a component with StateProps.
pub struct StateManager<T>
where
T: Component<Properties = StateProps>,
{
link: ComponentLink<Self>,
props: Props,
state_service: Box<dyn Bridge<AppStateService>>,
state: Rc<AppState>,
cb_setter: Callback<Setter>,
cb_logout: Callback<()>,
}
pub enum StateManagerMsg {
SetState(Rc<AppState>),
Update(Setter),
Logout,
}
#[derive(Properties, Clone)]
pub struct Props {
#[prop_or_default]
children: Children,
}
impl<T> Component for StateManager<T>
where
T: Component<Properties = StateProps>,
{
type Message = StateManagerMsg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
let mut state_service = AppStateService::bridge(link.callback(|msg| match msg {
Response::State(state) => StateManagerMsg::SetState(state),
}));
state_service.send(Request::Subscribe);
StateManager {
state: Default::default(),
cb_setter: link.callback(|setter| StateManagerMsg::Update(setter)),
cb_logout: link.callback(|_| StateManagerMsg::Logout),
state_service,
props,
link,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
StateManagerMsg::SetState(state) => {
self.state = state;
true
}
StateManagerMsg::Update(setter) => {
self.state_service.send(Request::SetState(setter));
false
}
StateManagerMsg::Logout => {
let future = request(UserLogout, |result: Result<(), AccountError>| {
StateManagerMsg::Update(Box::new(move |state| {
if result.is_ok() {
state.user = None;
}
}))
});
self.link.send_future(future);
false
}
}
}
fn view(&self) -> Html {
let props = StateProps {
state: self.state.clone(),
set_state: self.cb_setter.clone(),
logout: self.cb_logout.clone(),
children: self.props.children.clone(),
};
html! {
<T with props />
}
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
}
impl<T> std::ops::Drop for StateManager<T>
where
T: Component<Properties = StateProps>,
{
fn drop(&mut self) {
self.state_service.send(Request::UnSubscribe);
}
}
#[derive(Default, Properties, Clone, PartialEq)]
pub struct StateProps {
pub state: Rc<AppState>,
pub set_state: Callback<Setter>,
pub logout: Callback<()>,
#[prop_or_default]
pub children: Children,
}
impl StateProps {
/// Send message to toggle sidebar.
pub fn toggle_sidebar(&self) {
self.set_state.emit(Box::new(AppState::toggle_sidebar));
}
/// Create callback to toggle sidebar.
pub fn cb_toggle_sidebar<T>(&self) -> Callback<T> {
self.set_state
.reform(|_| Box::new(AppState::toggle_sidebar))
}
pub fn set_state(&self, setter: impl FnOnce(&mut AppState) + 'static) {
self.set_state.emit(Box::new(setter))
}
pub fn logout(&self) {
self.logout.emit(())
}
/// Create callback to toggle sidebar.
pub fn cb_logout<T>(&self) -> Callback<T> {
self.logout.reform(|_| ())
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment