* [PATCH pmg-yew-quarantine-gui] mail view: show mark seen/unseen depending on mail state
@ 2026-06-09 10:02 Dominik Csapak
0 siblings, 0 replies; only message in thread
From: Dominik Csapak @ 2026-06-09 10:02 UTC (permalink / raw)
To: pmg-devel
instead of always trying to show both. The content info was already
loaded for the 'load images' checkbox, so reuse that to also fetch the
seen status and show the actions depending on that.
This has to be repeated whenever the seen state possibly has changed.
While at it, refactor the deserialize_flexible_bool helper, and include
the variants that were used for the 'external_images' flag.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/main.rs | 15 ++++++
src/page_mail_view.rs | 113 +++++++++++++++++++++++++++---------------
src/spam_list.rs | 14 +-----
3 files changed, 88 insertions(+), 54 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index 5eacd4d..8646f58 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -243,3 +243,18 @@ fn main() {
yew::Renderer::<PmgQuarantineApp>::new().render();
}
+
+// deserialization helper for boolean values that can be 1/0/yes/on/off/etc.
+pub(crate) fn deserialize_flexible_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ Ok(match Value::deserialize(deserializer)? {
+ Value::Bool(b) => b,
+ Value::Number(n) => n.as_i64().is_some_and(|v| v != 0),
+ Value::String(s) => {
+ matches!(s.to_ascii_uppercase().as_str(), "1" | "on" | "yes" | "true")
+ }
+ _ => false,
+ })
+}
diff --git a/src/page_mail_view.rs b/src/page_mail_view.rs
index bdc5ffa..8f017b2 100644
--- a/src/page_mail_view.rs
+++ b/src/page_mail_view.rs
@@ -1,35 +1,39 @@
use std::rc::Rc;
use anyhow::Error;
+use serde::Deserialize;
use serde_json::{json, Value};
use yew::virtual_dom::{VComp, VNode};
use proxmox_yew_comp::http_get;
use pwt::dom::get_system_prefer_dark_mode;
-use pwt::prelude::*;
use pwt::state::{SharedState, Theme, ThemeObserver};
use pwt::touch::{ApplicationBar, FabMenu, FabMenuEntry, Scaffold, SnackBar, SnackBarContextExt};
use pwt::widget::form::Checkbox;
use pwt::widget::{get_unique_element_id, FieldLabel, Row};
+use pwt::{prelude::*, AsyncPool};
-use crate::{mail_action, MailAction, QuarantineReload};
-
-// whether the mail has external images the on-demand mode blocks, so the
-// "Load images" toggle is only offered when it would actually fetch something
-async fn mail_has_external_images(id: &str) -> bool {
- match http_get::<Value>("/quarantine/content", Some(json!({ "id": id }))).await {
- // a boolean can arrive as a JSON bool, number or one of the strings the
- // PVE::JSONSchema boolean type accepts (1/on/yes/true vs 0/off/no/false)
- Ok(data) => match &data["external_images"] {
- Value::Bool(b) => *b,
- Value::Number(n) => n.as_i64().unwrap_or(0) != 0,
- Value::String(s) => {
- matches!(s.to_ascii_lowercase().as_str(), "1" | "on" | "yes" | "true")
- }
- _ => false,
- },
- Err(_) => false,
+use crate::{deserialize_flexible_bool, mail_action, MailAction, QuarantineReload};
+
+/// Result of /quarantine/content, with relevant fields only
+#[derive(Clone, PartialEq, Deserialize)]
+pub struct MailContentInfo {
+ #[serde(default, deserialize_with = "deserialize_flexible_bool")]
+ external_images: bool,
+ #[serde(default, deserialize_with = "deserialize_flexible_bool")]
+ seen: bool,
+}
+
+// get the email info we need
+async fn get_mail_info(id: &str) -> Option<MailContentInfo> {
+ // ignore errors
+ match http_get::<MailContentInfo>("/quarantine/content", Some(json!({ "id": id }))).await {
+ Ok(res) => Some(res),
+ Err(err) => {
+ log::error!("could not retrieve mail-content-info for {id}: {err}");
+ None
+ }
}
}
@@ -46,17 +50,18 @@ impl PageMailView {
pub enum Msg {
ActionResult(MailAction, Result<Value, Error>),
- DarkmodeFilter(bool), // on/off
- DarkmodeChange(bool), // on/off
- LoadImages(bool), // on/off
- ExternalImages(bool), // mail has external images to load
+ DarkmodeFilter(bool), // on/off
+ DarkmodeChange(bool), // on/off
+ LoadImages(bool), // on/off
+ MailContentInfo(Option<MailContentInfo>), // Loaded mail content info
}
pub struct PmgPageMailView {
show_dark_mode_filter: bool,
dark_mode_filter: bool,
load_images: bool,
- show_load_images: bool,
+ mail_content_res: Option<MailContentInfo>,
_theme_observer: ThemeObserver,
+ async_pool: AsyncPool,
reload: Option<SharedState<usize>>,
}
@@ -97,6 +102,14 @@ impl PmgPageMailView {
</iframe>
}
}
+
+ fn reload_info(&self, ctx: &Context<Self>) {
+ let link = ctx.link().clone();
+ let id = ctx.props().id.clone();
+ self.async_pool.spawn(async move {
+ link.send_message(Msg::MailContentInfo(get_mail_info(&id).await));
+ });
+ }
}
impl Component for PmgPageMailView {
@@ -121,20 +134,17 @@ impl Component for PmgPageMailView {
.context::<QuarantineReload>(Callback::noop())
.map(|(reload, _handle)| reload.0);
- let link = ctx.link().clone();
- let id = ctx.props().id.clone();
- wasm_bindgen_futures::spawn_local(async move {
- link.send_message(Msg::ExternalImages(mail_has_external_images(&id).await));
- });
-
- Self {
+ let this = Self {
dark_mode_filter,
show_dark_mode_filter: dark_mode_filter,
load_images: false,
- show_load_images: false,
+ mail_content_res: None,
_theme_observer,
+ async_pool: AsyncPool::new(),
reload,
- }
+ };
+ this.reload_info(ctx);
+ this
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
@@ -146,6 +156,9 @@ impl Component for PmgPageMailView {
if let Some(reload) = &self.reload {
**reload.write() += 1;
}
+ if matches!(action, MailAction::MarkUnseen | MailAction::MarkSeen) {
+ self.reload_info(ctx);
+ }
tr!("Action '{0}' successful", action)
}
Err(err) => err.to_string(),
@@ -172,16 +185,16 @@ impl Component for PmgPageMailView {
self.load_images = load_images;
changed
}
- Msg::ExternalImages(show_load_images) => {
- let changed = self.show_load_images != show_load_images;
- self.show_load_images = show_load_images;
+ Msg::MailContentInfo(result) => {
+ let changed = self.mail_content_res != result;
+ self.mail_content_res = result;
changed
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
- let fab = FabMenu::new()
+ let mut fab = FabMenu::new()
.main_icon_class("fa fa-bars")
.with_child(FabMenuEntry::new(
tr!("Deliver"),
@@ -202,21 +215,39 @@ impl Component for PmgPageMailView {
tr!("Blocklist"),
"fa fa-times",
self.action_callback(ctx, MailAction::Blocklist),
- ))
- .with_child(FabMenuEntry::new(
+ ));
+
+ // show both when we don't have any info
+ let (show_mark_seen, show_mark_unseen) = match &self.mail_content_res {
+ Some(info) => (!info.seen, info.seen),
+ None => (true, true),
+ };
+
+ if show_mark_seen {
+ fab.add_child(FabMenuEntry::new(
tr!("Mark as Seen"),
"fa fa-eye",
self.action_callback(ctx, MailAction::MarkSeen),
- ))
- .with_child(FabMenuEntry::new(
+ ));
+ }
+
+ if show_mark_unseen {
+ fab.add_child(FabMenuEntry::new(
tr!("Mark as Unseen"),
"fa fa-eye-slash",
self.action_callback(ctx, MailAction::MarkUnseen),
));
+ }
let mut app_bar = ApplicationBar::new().title(tr!("Preview"));
- if self.show_load_images {
+ let show_load_images = self
+ .mail_content_res
+ .as_ref()
+ .map(|info| info.external_images)
+ .unwrap_or(false);
+
+ if show_load_images {
let id = get_unique_element_id();
app_bar.add_action(
Row::new()
diff --git a/src/spam_list.rs b/src/spam_list.rs
index a7f1bd2..3f67179 100644
--- a/src/spam_list.rs
+++ b/src/spam_list.rs
@@ -21,7 +21,7 @@ use pwt::{
use proxmox_yew_comp::http_get;
use pwt::widget::Column;
-use crate::{mail_action, MailAction, QuarantineReload};
+use crate::{deserialize_flexible_bool, mail_action, MailAction, QuarantineReload};
#[derive(Copy, Clone, Serialize, Default, PartialEq)]
pub struct SpamListParam {
@@ -53,18 +53,6 @@ pub struct MailInfo {
pub time: i64,
}
-fn deserialize_flexible_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
-where
- D: serde::Deserializer<'de>,
-{
- Ok(match Value::deserialize(deserializer)? {
- Value::Bool(b) => b,
- Value::Number(n) => n.as_i64().is_some_and(|v| v != 0),
- Value::String(s) => s == "1" || s.eq_ignore_ascii_case("true"),
- _ => false,
- })
-}
-
#[derive(Clone)]
pub enum ListEntry {
Date(String),
--
2.47.3
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-06-09 10:02 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-09 10:02 [PATCH pmg-yew-quarantine-gui] mail view: show mark seen/unseen depending on mail state Dominik Csapak
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox