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.
212 lines
7.2 KiB
Go
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
|
|
}
|