feat(chat): usable 1:1 chat — bottom-anchored, Drift-backed, dated

- Anchor the message list to the bottom (`reverse: true`) so new messages
  stay in view instead of landing below the fold.
- Move chat history from the OS keystore (O(n²) JSON blob, silent 200-msg
  cap) to a separate encrypted Drift/SQLCipher DB (`ChatDatabase`):
  indexed append, uncapped history, dedup as a unique-key invariant. It's
  an ephemeral per-device cache, isolated from the inventory schema, its
  migrations, and its sync. No data migration (pre-release).
- Add day separators (Today/Yesterday/locale date) and a per-bubble time,
  all via ICU (12/24h per locale; Localizations locale maps Asturian →
  Spanish for intl date symbols).
- Add peer avatars (deterministic colour from the pubkey + name initial),
  surface send failures that were previously silent, and make bubble text
  selectable (addresses, links).
- New i18n keys in en/es/pt/ast; tests for grouping, formatting, avatars,
  scroll anchoring, storage and send errors.

Docs: docs/design/chat-storage.md + open-decisions.md.
This commit is contained in:
vjrj 2026-07-11 06:39:39 +02:00
parent 171daabce3
commit 6cff0d0b11
27 changed files with 1793 additions and 264 deletions

View file

@ -528,7 +528,10 @@
"empty": "Entá nun hai mensaxes — saluda",
"offline": "Configura'l compartir pa unviar mensaxes",
"payG1": "Pagar en Ğ1",
"g1Copied": "Direición Ğ1 copiada — apégala na to cartera"
"g1Copied": "Direición Ğ1 copiada — apégala na to cartera",
"today": "Güei",
"yesterday": "Ayeri",
"sendError": "Nun se pudo unviar — comprueba la conexón"
},
"trust": {
"none": "Naide los avala entá",

View file

@ -531,7 +531,10 @@
"empty": "No messages yet — say hello",
"offline": "Set up sharing to send messages",
"payG1": "Pay in Ğ1",
"g1Copied": "Ğ1 address copied — paste it in your wallet"
"g1Copied": "Ğ1 address copied — paste it in your wallet",
"today": "Today",
"yesterday": "Yesterday",
"sendError": "Couldn't send — check your connection"
},
"trust": {
"none": "No one vouches for them yet",

View file

@ -530,7 +530,10 @@
"empty": "Aún no hay mensajes — saluda",
"offline": "Configura el compartir para enviar mensajes",
"payG1": "Pagar en Ğ1",
"g1Copied": "Dirección Ğ1 copiada — pégala en tu cartera"
"g1Copied": "Dirección Ğ1 copiada — pégala en tu cartera",
"today": "Hoy",
"yesterday": "Ayer",
"sendError": "No se pudo enviar — revisa tu conexión"
},
"trust": {
"none": "Nadie los avala aún",

View file

@ -527,7 +527,10 @@
"empty": "Ainda não há mensagens — diz olá",
"offline": "Configura a partilha para enviar mensagens",
"payG1": "Pagar em Ğ1",
"g1Copied": "Endereço Ğ1 copiado — cola-o na tua carteira"
"g1Copied": "Endereço Ğ1 copiado — cola-o na tua carteira",
"today": "Hoje",
"yesterday": "Ontem",
"sendError": "Não foi possível enviar — verifica a tua ligação"
},
"trust": {
"none": "Ainda ninguém os avaliza",

View file

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 4
/// Strings: 1852 (463 per locale)
/// Strings: 1864 (466 per locale)
///
/// Built on 2026-07-11 at 04:24 UTC
/// Built on 2026-07-11 at 04:40 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import

View file

@ -770,6 +770,9 @@ class _Translations$chat$ast extends Translations$chat$en {
@override String get offline => 'Configura\'l compartir pa unviar mensaxes';
@override String get payG1 => 'Pagar en Ğ1';
@override String get g1Copied => 'Direición Ğ1 copiada — apégala na to cartera';
@override String get today => 'Güei';
@override String get yesterday => 'Ayeri';
@override String get sendError => 'Nun se pudo unviar — comprueba la conexón';
}
// Path: trust
@ -1620,6 +1623,9 @@ extension on TranslationsAst {
'chat.offline' => 'Configura\'l compartir pa unviar mensaxes',
'chat.payG1' => 'Pagar en Ğ1',
'chat.g1Copied' => 'Direición Ğ1 copiada — apégala na to cartera',
'chat.today' => 'Güei',
'chat.yesterday' => 'Ayeri',
'chat.sendError' => 'Nun se pudo unviar — comprueba la conexón',
'trust.none' => 'Naide los avala entá',
'trust.count' => ({required Object n}) => 'Avalada por ${n}',
'trust.vouch' => 'Conozo a esta persona',

View file

@ -1437,6 +1437,15 @@ class Translations$chat$en {
/// en: 'Ğ1 address copied — paste it in your wallet'
String get g1Copied => 'Ğ1 address copied — paste it in your wallet';
/// en: 'Today'
String get today => 'Today';
/// en: 'Yesterday'
String get yesterday => 'Yesterday';
/// en: 'Couldn't send check your connection'
String get sendError => 'Couldn\'t send — check your connection';
}
// Path: trust
@ -2536,6 +2545,9 @@ extension on Translations {
'chat.offline' => 'Set up sharing to send messages',
'chat.payG1' => 'Pay in Ğ1',
'chat.g1Copied' => 'Ğ1 address copied — paste it in your wallet',
'chat.today' => 'Today',
'chat.yesterday' => 'Yesterday',
'chat.sendError' => 'Couldn\'t send — check your connection',
'trust.none' => 'No one vouches for them yet',
'trust.count' => ({required Object n}) => 'Vouched for by ${n}',
'trust.vouch' => 'I know this person',

View file

@ -772,6 +772,9 @@ class _Translations$chat$es extends Translations$chat$en {
@override String get offline => 'Configura el compartir para enviar mensajes';
@override String get payG1 => 'Pagar en Ğ1';
@override String get g1Copied => 'Dirección Ğ1 copiada — pégala en tu cartera';
@override String get today => 'Hoy';
@override String get yesterday => 'Ayer';
@override String get sendError => 'No se pudo enviar — revisa tu conexión';
}
// Path: trust
@ -1624,6 +1627,9 @@ extension on TranslationsEs {
'chat.offline' => 'Configura el compartir para enviar mensajes',
'chat.payG1' => 'Pagar en Ğ1',
'chat.g1Copied' => 'Dirección Ğ1 copiada — pégala en tu cartera',
'chat.today' => 'Hoy',
'chat.yesterday' => 'Ayer',
'chat.sendError' => 'No se pudo enviar — revisa tu conexión',
'trust.none' => 'Nadie los avala aún',
'trust.count' => ({required Object n}) => 'Avalada por ${n}',
'trust.vouch' => 'Conozco a esta persona',

View file

@ -769,6 +769,9 @@ class _Translations$chat$pt extends Translations$chat$en {
@override String get offline => 'Configura a partilha para enviar mensagens';
@override String get payG1 => 'Pagar em Ğ1';
@override String get g1Copied => 'Endereço Ğ1 copiado — cola-o na tua carteira';
@override String get today => 'Hoje';
@override String get yesterday => 'Ontem';
@override String get sendError => 'Não foi possível enviar — verifica a tua ligação';
}
// Path: trust
@ -1618,6 +1621,9 @@ extension on TranslationsPt {
'chat.offline' => 'Configura a partilha para enviar mensagens',
'chat.payG1' => 'Pagar em Ğ1',
'chat.g1Copied' => 'Endereço Ğ1 copiado — cola-o na tua carteira',
'chat.today' => 'Hoje',
'chat.yesterday' => 'Ontem',
'chat.sendError' => 'Não foi possível enviar — verifica a tua ligação',
'trust.none' => 'Ainda ninguém os avaliza',
'trust.count' => ({required Object n}) => 'Avalizada por ${n}',
'trust.vouch' => 'Conheço esta pessoa',