use super::DataSource;
use super::layout::into_node;
use crate::action::{Action, ActionMeta, Error, Result};
use crate::ctx::{BoxFuture, Ctx};
use crate::i18n::Text;
use crate::render::{Render, escape};
use crate::screen::{Component, Node};
use serde::{Deserialize, Serialize};
use serde_json::{Value as Json, json};
use std::collections::BTreeMap;
use std::marker::PhantomData;
into_node!(Reactions);
pub const MAX_REACTIONS: usize = 12;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReactionPress {
pub target: String,
pub reaction: String,
pub pressed: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReactionState {
pub counts: BTreeMap<String, u64>,
pub mine: Vec<String>,
}
impl ReactionState {
pub fn count(mut self, reaction: impl Into<String>, count: u64) -> Self {
self.counts
.insert(reaction.into(), count);
self
}
pub fn mine(mut self, reaction: impl Into<String>) -> Self {
self.mine.push(reaction.into());
self
}
}
struct ReactionSource<A> {
target: String,
allowed: Vec<&'static str>,
_action: PhantomData<fn() -> A>,
}
#[derive(Deserialize)]
struct ClientPress {
reaction: String,
#[serde(default)]
pressed: bool,
}
impl<A> DataSource for ReactionSource<A>
where
A: Action<Input = ReactionPress, Output = Result<ReactionState>>,
{
fn action(&self) -> ActionMeta {
A::META
}
fn fetch(&self, ctx: Ctx, input: Json) -> BoxFuture<Result<Json>> {
let press = serde_json::from_value::<ClientPress>(input)
.ok()
.filter(|p| {
self.allowed
.contains(&p.reaction.as_str())
})
.map(|p| ReactionPress {
target: self.target.clone(),
reaction: p.reaction,
pressed: p.pressed,
});
Box::pin(async move {
let press = press.ok_or(Error::NotFound)?;
let state = A::call(ctx.clone(), press).await??;
crate::privacy::client_json(&state, &ctx).map_err(|e| Error::Internal(e.to_string()))
})
}
}
pub struct Reactions {
label: Option<Text>,
target: String,
reactions: Vec<(&'static str, Text, &'static str)>,
state: ReactionState,
source: Box<dyn DataSource>,
make: fn(String, Vec<&'static str>) -> Box<dyn DataSource>,
action: ActionMeta,
}
fn make_source<A>(target: String, allowed: Vec<&'static str>) -> Box<dyn DataSource>
where
A: Action<Input = ReactionPress, Output = Result<ReactionState>>,
{
Box::new(ReactionSource::<A> {
target,
allowed,
_action: PhantomData,
})
}
impl Reactions {
pub fn of<A>(_action: A, target: impl Into<String>) -> Self
where
A: Action<Input = ReactionPress, Output = Result<ReactionState>>,
{
let target = target.into();
Reactions {
label: None,
source: make_source::<A>(target.clone(), Vec::new()),
target,
reactions: Vec::new(),
state: ReactionState::default(),
make: make_source::<A>,
action: A::META,
}
}
pub fn reaction(
mut self,
key: &'static str,
name: impl Into<Text>,
symbol: &'static str,
) -> Self {
if self.reactions.len() < MAX_REACTIONS
&& !self
.reactions
.iter()
.any(|(k, _, _)| *k == key)
{
self.reactions
.push((key, name.into(), symbol));
let keys = self
.reactions
.iter()
.map(|(k, _, _)| *k)
.collect();
self.source = (self.make)(self.target.clone(), keys);
}
self
}
pub fn state(mut self, state: ReactionState) -> Self {
self.state = state;
self
}
pub fn label(mut self, label: impl Into<Text>) -> Self {
self.label = Some(label.into());
self
}
}
impl Component for Reactions {
fn render(&self, out: &mut String, r: &Render) {
let label = match &self.label {
Some(l) => r.text(l),
None => r.text(&Text::key("zeyn.ui.reactions.label")),
};
out.push_str(&format!(
r#"<div class="z-reactions" role="group" aria-label="{label}" data-zeyn-reactions data-failed="{}">"#,
r.text(&Text::key("zeyn.ui.reactions.failed"))
));
for (key, name, symbol) in &self.reactions {
let count = self
.state
.counts
.get(*key)
.copied()
.unwrap_or(0);
let mine = self.state.mine.iter().any(|m| m == key);
out.push_str(&format!(
r#"<button type="button" class="z-reaction" aria-pressed="{mine}" data-reaction="{}" data-testid="{}.reaction.{}"><span class="z-reaction-symbol" aria-hidden="true">{}</span><span class="z-sr-only">{}</span><span class="z-reaction-count">{count}</span></button>"#,
escape(key),
r.scope,
escape(key),
escape(symbol),
r.text(name)
));
}
out.push_str("</div>");
}
fn schema(&self) -> Json {
json!({
"type": "reactions",
"action": self.action,
"target": self.target,
"reactions": self
.reactions
.iter()
.map(|(k, n, s)| json!({ "key": k, "name": n, "symbol": s }))
.collect::<Vec<_>>(),
"state": self.state,
})
}
fn source(&self, key: &str) -> Option<&dyn DataSource> {
(key == "react").then_some(self.source.as_ref())
}
}