use crate::config;
use crate::groups::{self, Groups};
use crate::schema::Message;
use anyhow::Result;
use std::path::PathBuf;
fn path(hub_key: &str, role: &str) -> Result<PathBuf> {
let role = if role.is_empty() { "_" } else { role };
Ok(config::home()?.join(".confer").join("inbox").join(hub_key).join(format!("{role}.json")))
}
pub fn load(hub_key: &str, role: &str) -> Option<String> {
let txt = std::fs::read_to_string(path(hub_key, role).ok()?).ok()?;
serde_json::from_str::<serde_json::Value>(&txt)
.ok()?
.get("read")
.and_then(|c| c.as_str())
.map(String::from)
}
fn save(hub_key: &str, role: &str, id: &str) -> Result<()> {
let p = path(hub_key, role)?;
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&p, serde_json::json!({ "read": id }).to_string())?;
Ok(())
}
pub fn advance(hub_key: &str, role: &str, id: &str) -> Result<()> {
if role.is_empty() || id.is_empty() {
return Ok(());
}
match load(hub_key, role) {
Some(cur) if cur.as_str() >= id => Ok(()), _ => save(hub_key, role, id),
}
}
pub fn unread_for_me<'a>(
msgs: &'a [Message],
me: &str,
groups: &Groups,
frontier: Option<&str>,
) -> Vec<&'a Message> {
let mut out: Vec<&Message> = msgs
.iter()
.filter(|m| {
m.front.from != me
&& groups::directly_addressed(m, me, groups)
&& frontier.is_none_or(|f| m.front.id.as_str() > f)
})
.collect();
out.sort_by(|a, b| a.front.id.cmp(&b.front.id));
out
}
pub fn latest_id(msgs: &[Message]) -> Option<String> {
msgs.iter().map(|m| m.front.id.clone()).max()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::Frontmatter;
fn msg(id: &str, from: &str, to: &[&str]) -> Message {
Message {
front: Frontmatter {
id: id.into(),
from: from.into(),
msg_type: "note".into(),
ts: "2026-07-11T00:00:00Z".into(),
host: None,
to: to.iter().map(|s| s.to_string()).collect(),
cc: vec![],
priority: None,
topic: None,
reply_to: None,
of: None,
supersedes: None,
resolution: None,
defer: false,
via: None,
src: None,
summary: Some("s".into()),
refs: vec![],
},
body: String::new(),
}
}
#[test]
fn unread_is_direct_mail_past_the_frontier_excluding_broadcasts_and_self() {
let g = Groups::new();
let msgs = vec![
msg("01A", "bob", &["alice"]), msg("01B", "bob", &["all"]), msg("01C", "carol", &["alice"]), msg("01D", "alice", &["bob"]), msg("01E", "bob", &["carol"]), ];
let u = unread_for_me(&msgs, "alice", &g, None);
assert_eq!(u.iter().map(|m| m.front.id.as_str()).collect::<Vec<_>>(), vec!["01A", "01C"]);
let u = unread_for_me(&msgs, "alice", &g, Some("01A"));
assert_eq!(u.iter().map(|m| m.front.id.as_str()).collect::<Vec<_>>(), vec!["01C"]);
assert!(unread_for_me(&msgs, "alice", &g, Some("01C")).is_empty());
}
}