Files
mesh-catalog/modules/mesh-mail-reader/cmd/mesh-mail-reader/list.go
T
jochen 4f3ebe370b issue 1859: answer the mail page's greeting and reading pane from mail reading
ADR 0515 §1, §5: list also answers the mails the mail signal told of today, and each line its mail's folder
and UID; entries takes an id and answers that one entry whole, with its sender's address, tasks, promises
and read-later, for the reading pane's corrections.
2026-10-22 20:00:44 +02:00

212 lines
7.2 KiB
Go

package main
// The operator's list (novox/hq ADR 0515 §1, ADR 0517 §1, §2, §4; issue 1858): a pure function over the
// entries, the handlings and the day, answered by list. Days are the operator's (days.go, Europe/Brussels).
//
// - A task falls under today when it is due today or before, this week when due by the end of Sunday, and
// later when due after that or not at all.
// - A promise falls under its kind: you promised, waiting on others, on its way.
// - A reminded item is hidden until the day it shows again, and that day it is under today; the mail
// itself reminded hides all its items, and shows them, or itself when it has none, under today that day.
// - A handled item is gone from its section; handled today, it is under handled today with its undo. The
// mail itself handled takes all its items with it.
// - Good to know holds the entries with no task or promise, not read later, not a scam warning and not
// handled; read later the read-later ones; a scam warning is in its own place and nowhere else.
import (
"sort"
"strconv"
"time"
)
// DefaultListLimit bounds each section when list is not asked another limit.
const DefaultListLimit = 50
// ListItem is one line of the list: an item of an entry, with what the page shows of its mail.
type ListItem struct {
Entry Entry
Item string // ItemMail, "task n" or "promise n"
Kind string // "task", "promise" or "mail"
Line string
Due time.Time // a task's due date or a promise's date; zero for none
Promise PromiseKind
Who string
Reminded bool // shown under today because it was reminded
Handled time.Time // for handled today
}
// OperatorList is the list for one mode.
type OperatorList struct {
Today, ThisWeek, Later []ListItem
YouPromised, WaitingOnOthers, OnItsWay []ListItem
GoodToKnow, ReadLater, ScamWarnings []ListItem // mail items
HandledToday []ListItem
// ToldToday is the mails the mail signal told of today, for the page's greeting (ADR 0515 §1, ADR 0461):
// an entry told and judged today (the signal is sent once a batch, right after it judged, tell.go).
ToldToday []ListItem
}
// BuildList builds the list of the entries of accounts (every account when accounts is empty) for the day of
// now.
func BuildList(entries []Entry, handlings []Handling, accounts []string, now time.Time) OperatorList {
today := dayOf(now)
sunday := endOfWeek(today)
byItem := map[int64]map[string]Handling{}
for _, h := range handlings {
if byItem[h.Entry] == nil {
byItem[h.Entry] = map[string]Handling{}
}
byItem[h.Entry][h.Item] = h
}
wanted := map[string]bool{}
for _, a := range accounts {
wanted[a] = true
}
var l OperatorList
for _, e := range entries {
if len(wanted) > 0 && !wanted[e.Account] {
continue
}
hs := byItem[e.ID]
mail := hs[ItemMail]
mailItem := ListItem{Entry: e, Item: ItemMail, Kind: "mail", Line: e.Summary}
if e.Told && dayOf(e.Judged).Equal(today) {
l.ToldToday = append(l.ToldToday, mailItem)
}
if !mail.Handled.IsZero() {
if dayOf(mail.Handled).Equal(today) {
mailItem.Handled = mail.Handled
l.HandledToday = append(l.HandledToday, mailItem)
}
continue // the mail itself handled takes all its items with it
}
if !mail.ShowsAgain.IsZero() && mail.ShowsAgain.After(today) {
continue // the mail itself reminded: hidden, all of it, until its day
}
mailToday := mail.ShowsAgain.Equal(today)
if e.Mark == MarkScamWarning {
l.ScamWarnings = append(l.ScamWarnings, mailItem)
continue
}
items := itemsOf(e)
for _, it := range items {
h := hs[it.Item]
switch {
case !h.Handled.IsZero():
if dayOf(h.Handled).Equal(today) {
it.Handled = h.Handled
l.HandledToday = append(l.HandledToday, it)
}
case !h.ShowsAgain.IsZero() && h.ShowsAgain.After(today):
// reminded: hidden until its day
case h.ShowsAgain.Equal(today) || mailToday:
it.Reminded = true
l.Today = append(l.Today, it)
case it.Kind == "task":
switch {
case !it.Due.IsZero() && !it.Due.After(today):
l.Today = append(l.Today, it)
case !it.Due.IsZero() && !it.Due.After(sunday):
l.ThisWeek = append(l.ThisWeek, it)
default:
l.Later = append(l.Later, it)
}
default:
switch it.Promise {
case YouPromised:
l.YouPromised = append(l.YouPromised, it)
case WaitingOnOthers:
l.WaitingOnOthers = append(l.WaitingOnOthers, it)
default:
l.OnItsWay = append(l.OnItsWay, it)
}
}
}
switch {
case len(items) == 0 && mailToday:
mailItem.Reminded = true
l.Today = append(l.Today, mailItem)
case e.ReadLater:
l.ReadLater = append(l.ReadLater, mailItem)
case len(items) == 0:
l.GoodToKnow = append(l.GoodToKnow, mailItem)
}
}
for _, section := range [][]ListItem{l.Today, l.ThisWeek, l.Later, l.YouPromised, l.WaitingOnOthers, l.OnItsWay} {
sortByDue(section)
}
for _, section := range [][]ListItem{l.GoodToKnow, l.ReadLater, l.ScamWarnings, l.ToldToday} {
sort.SliceStable(section, func(i, j int) bool { return section[i].Entry.Date.After(section[j].Entry.Date) })
}
sort.SliceStable(l.HandledToday, func(i, j int) bool { return l.HandledToday[i].Handled.After(l.HandledToday[j].Handled) })
return l
}
// itemsOf is an entry's tasks and promises as items, in their numbers' order.
func itemsOf(e Entry) []ListItem {
var out []ListItem
for _, t := range e.Tasks {
out = append(out, ListItem{Entry: e, Item: "task " + strconv.Itoa(t.N), Kind: "task", Line: t.Line, Due: t.Due})
}
for _, p := range e.Promises {
out = append(out, ListItem{Entry: e, Item: "promise " + strconv.Itoa(p.N), Kind: "promise", Line: p.Line, Due: p.Date,
Promise: p.Kind, Who: p.Who})
}
return out
}
// sortByDue orders a section nearest date first, none last, then newest mail first.
func sortByDue(section []ListItem) {
sort.SliceStable(section, func(i, j int) bool {
a, b := section[i], section[j]
if a.Due.IsZero() != b.Due.IsZero() {
return b.Due.IsZero()
}
if !a.Due.Equal(b.Due) {
return a.Due.Before(b.Due)
}
return a.Entry.Date.After(b.Entry.Date)
})
}
// listItemOut is one line as list answers it: never a mail body, only the mail's headers, its summary and the
// model's own lines.
func listItemOut(it ListItem) map[string]any {
e := it.Entry
out := map[string]any{
"id": e.ID, "item": it.Item, "kind": it.Kind, "line": it.Line, "account": e.Account, "folder": e.Folder, "uid": e.UID,
"sender-name": e.SenderName, "sender-address": e.SenderAddress, "subject": e.Subject,
"date": e.Date.UTC().Format(time.RFC3339), "summary": e.Summary, "mark": string(e.Mark), "why": e.Why,
}
if !it.Due.IsZero() {
key := "due-date"
if it.Kind == "promise" {
key = "promise-date"
}
out[key] = it.Due.Format("2006-01-02")
}
if it.Kind == "promise" {
out["promise-kind"] = string(it.Promise)
out["who"] = it.Who
}
if it.Reminded {
out["reminded"] = true
}
if !it.Handled.IsZero() {
out["handled"] = it.Handled.UTC().Format(time.RFC3339)
out["undo"] = true // until the day ends (ADR 0517 §1)
}
return out
}
func sectionOut(items []ListItem, limit int) []map[string]any {
if len(items) > limit {
items = items[:limit]
}
out := make([]map[string]any, len(items))
for i, it := range items {
out[i] = listItemOut(it)
}
return out
}