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:
parent
171daabce3
commit
6cff0d0b11
27 changed files with 1793 additions and 264 deletions
59
apps/app_seeds/lib/ui/peer_avatar.dart
Normal file
59
apps/app_seeds/lib/ui/peer_avatar.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A small round avatar for a chat peer. We have no profile photos yet, so it's
|
||||
/// a deterministic disc coloured from the peer's pubkey, showing the first
|
||||
/// letter of their [name] when known (else a person glyph). Same pubkey → same
|
||||
/// colour on every device, so the peer is visually recognizable.
|
||||
class PeerAvatar extends StatelessWidget {
|
||||
const PeerAvatar({
|
||||
required this.pubkey,
|
||||
this.name,
|
||||
this.radius = 14,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String pubkey;
|
||||
final String? name;
|
||||
final double radius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final letter = _initial(name);
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundColor: peerAvatarColor(pubkey),
|
||||
child: letter == null
|
||||
? Icon(Icons.person_outline, size: radius, color: Colors.white)
|
||||
: Text(
|
||||
letter,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: radius,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String? _initial(String? name) {
|
||||
if (name == null) return null;
|
||||
final trimmed = name.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
// characters.first handles emoji/combining marks safely.
|
||||
return trimmed.characters.first.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
/// A deterministic, readable avatar colour derived from [pubkey]. Hashes the
|
||||
/// key to a hue, then fixes saturation/lightness so white text stays legible on
|
||||
/// top. Pure and stable — exposed for testing.
|
||||
Color peerAvatarColor(String pubkey) {
|
||||
// FNV-1a over the code units — cheap, stable, well-spread across hues.
|
||||
var hash = 0x811c9dc5;
|
||||
for (final unit in pubkey.codeUnits) {
|
||||
hash = (hash ^ unit) * 0x01000193;
|
||||
hash &= 0xffffffff;
|
||||
}
|
||||
final hue = (hash % 360).toDouble();
|
||||
return HSLColor.fromAHSL(1, hue, 0.45, 0.42).toColor();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue