public inbox for pmg-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Stoiko Ivanov <s.ivanov@proxmox.com>
To: Dominik Csapak <d.csapak@proxmox.com>
Cc: pmg-devel@lists.proxmox.com
Subject: Re: [PATCH pmg-yew-quarantine-gui v2] mail view: show mark seen/unseen depending on mail state
Date: Fri, 19 Jun 2026 11:45:26 +0200	[thread overview]
Message-ID: <20260619114526.503074a2@rosa.proxmox.com> (raw)
In-Reply-To: <20260619093152.1440414-1-d.csapak@proxmox.com>

Thanks for the quick iteration!

verified the change and gave it another spin - still works as advertised:
Reviewed-by: Stoiko Ivanov <s.ivanov@proxmox.com>
Tested-by: Stoiko Ivanov <s.ivanov@proxmox.com>


On Fri, 19 Jun 2026 11:31:22 +0200
Dominik Csapak <d.csapak@proxmox.com> wrote:

> 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>
> ---
> changes from v1:
> * use to_ascii_lowercase instead of wrong uppercase.
> 
>  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..5414e33 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_lowercase().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 95a0611..6ea3e1d 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),





      reply	other threads:[~2026-06-19  9:46 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-19  9:31 [PATCH pmg-yew-quarantine-gui v2] mail view: show mark seen/unseen depending on mail state Dominik Csapak
2026-06-19  9:45 ` Stoiko Ivanov [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260619114526.503074a2@rosa.proxmox.com \
    --to=s.ivanov@proxmox.com \
    --cc=d.csapak@proxmox.com \
    --cc=pmg-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal