feat(messages): unread badges + OS notification for private messages

Private messages (NIP-17) arrived in the foreground with no signal: no
unread count, no badge, no OS notification. Hook both into the single
InboxService.ingest choke point.

- UnreadService: per-peer "last read" in the keystore (timestamps+pubkey
  only, no message text), live count via a changes stream, active-peer
  suppression so the open chat never badges or notifies.
- UnreadBadge: reusable, RTL-aware (Flutter Badge) — on the home hamburger,
  the drawer "Chat" item, and per-conversation in the messages list.
- Mark-read on chat open (ChatScreen).
- NotificationService over flutter_local_notifications: generic
  "New message from <name>" (no message text, for privacy), payload = pubkey,
  tap opens /chat/:pubkey. No-op on web/Windows; Android POST_NOTIFICATIONS.
- i18n notifications.newMessageFrom (en/es/pt/ast).

Background/push stays out of scope (foreground-only by design).

Tests: UnreadService, NotificationService (mock plugin), InboxService hooks
(new peer notifies/counts; duplicate, own, and open-chat do not), and a
UnreadBadge widget test. dart analyze clean; commons_core green.
This commit is contained in:
vjrj 2026-07-10 21:12:00 +02:00
parent 63f48db5c2
commit 5fe0f4540e
27 changed files with 1238 additions and 114 deletions

View file

@ -214,8 +214,18 @@
"anyMonth": "Cualquier mes",
"noDate": "Indicar data de coyecha",
"monthNames": [
"xineru", "febreru", "marzu", "abril", "mayu", "xunu",
"xunetu", "agostu", "setiembre", "ochobre", "payares", "avientu"
"xineru",
"febreru",
"marzu",
"abril",
"mayu",
"xunu",
"xunetu",
"agostu",
"setiembre",
"ochobre",
"payares",
"avientu"
]
},
"lotType": {
@ -330,30 +340,102 @@
"some": "dalgunes",
"plenty": "munches",
"pinch": "una migaya",
"handful": { "singular": "puñáu", "plural": "puñaos" },
"teaspoon": { "singular": "cuyaradina", "plural": "cuyaradines" },
"spoon": { "singular": "cuyar", "plural": "cuyares" },
"cup": { "singular": "taza", "plural": "tazes" },
"jar": { "singular": "bote", "plural": "botes" },
"sack": { "singular": "sacu", "plural": "sacos" },
"packet": { "singular": "sobre", "plural": "sobres" },
"cob": { "singular": "panoya", "plural": "panoyes" },
"pod": { "singular": "vaina", "plural": "vaines" },
"ear": { "singular": "espiga", "plural": "espigues" },
"head": { "singular": "cabezuela", "plural": "cabezueles" },
"fruit": { "singular": "frutu", "plural": "frutos" },
"bulb": { "singular": "bulbu", "plural": "bulbos" },
"tuber": { "singular": "tubérculu", "plural": "tubérculos" },
"seedHead": { "singular": "cabeza de simiente", "plural": "cabeces de simiente" },
"bunch": { "singular": "manoyu", "plural": "manoyos" },
"plant": { "singular": "planta", "plural": "plantes" },
"pot": { "singular": "tiestu", "plural": "tiestos" },
"tray": { "singular": "bandexa", "plural": "bandexes" },
"seedling": { "singular": "plántula", "plural": "plántules" },
"tree": { "singular": "árbol", "plural": "árboles" },
"cutting": { "singular": "esqueje", "plural": "esquejes" },
"grams": { "singular": "gramu", "plural": "gramos" },
"count": { "singular": "grana", "plural": "granes" }
"handful": {
"singular": "puñáu",
"plural": "puñaos"
},
"teaspoon": {
"singular": "cuyaradina",
"plural": "cuyaradines"
},
"spoon": {
"singular": "cuyar",
"plural": "cuyares"
},
"cup": {
"singular": "taza",
"plural": "tazes"
},
"jar": {
"singular": "bote",
"plural": "botes"
},
"sack": {
"singular": "sacu",
"plural": "sacos"
},
"packet": {
"singular": "sobre",
"plural": "sobres"
},
"cob": {
"singular": "panoya",
"plural": "panoyes"
},
"pod": {
"singular": "vaina",
"plural": "vaines"
},
"ear": {
"singular": "espiga",
"plural": "espigues"
},
"head": {
"singular": "cabezuela",
"plural": "cabezueles"
},
"fruit": {
"singular": "frutu",
"plural": "frutos"
},
"bulb": {
"singular": "bulbu",
"plural": "bulbos"
},
"tuber": {
"singular": "tubérculu",
"plural": "tubérculos"
},
"seedHead": {
"singular": "cabeza de simiente",
"plural": "cabeces de simiente"
},
"bunch": {
"singular": "manoyu",
"plural": "manoyos"
},
"plant": {
"singular": "planta",
"plural": "plantes"
},
"pot": {
"singular": "tiestu",
"plural": "tiestos"
},
"tray": {
"singular": "bandexa",
"plural": "bandexes"
},
"seedling": {
"singular": "plántula",
"plural": "plántules"
},
"tree": {
"singular": "árbol",
"plural": "árboles"
},
"cutting": {
"singular": "esqueje",
"plural": "esquejes"
},
"grams": {
"singular": "gramu",
"plural": "gramos"
},
"count": {
"singular": "grana",
"plural": "granes"
}
},
"market": {
"title": "Simiente cerca de ti",
@ -436,5 +518,8 @@
"vouch": "Conozo a esta persona",
"vouched": "Avales a esta persona",
"circle": "Nel to círculu"
},
"notifications": {
"newMessageFrom": "Mensaxe nuevu de {name}"
}
}