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:
parent
4d110e8082
commit
d026376993
27 changed files with 1238 additions and 114 deletions
|
|
@ -5,11 +5,13 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../di/injector.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../services/message_store.dart';
|
||||
import '../services/profile_cache.dart';
|
||||
import '../services/social_service.dart';
|
||||
import '../services/social_settings.dart';
|
||||
import '../services/unread_service.dart';
|
||||
import '../state/messages_cubit.dart';
|
||||
import '../state/trust_cubit.dart';
|
||||
import 'theme.dart';
|
||||
|
|
@ -50,9 +52,20 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
String? _peerG1;
|
||||
final _input = TextEditingController();
|
||||
|
||||
/// App-wide unread tracker (absent in tests / inventory-only builds). While
|
||||
/// this chat is open it is the "active peer", so incoming messages for it are
|
||||
/// read on arrival and never badge or notify.
|
||||
UnreadService? _unread;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_unread =
|
||||
getIt.isRegistered<UnreadService>() ? getIt<UnreadService>() : null;
|
||||
// Set synchronously (before any await) so a message landing during _init is
|
||||
// already suppressed.
|
||||
_unread?.activePeer = widget.peerPubkey;
|
||||
unawaited(_unread?.markRead(widget.peerPubkey));
|
||||
_init();
|
||||
}
|
||||
|
||||
|
|
@ -148,6 +161,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_unread?.activePeer == widget.peerPubkey) _unread!.activePeer = null;
|
||||
// Mark read once more on the way out, so anything that arrived while the
|
||||
// chat was open (and was auto-read) stays cleared.
|
||||
unawaited(_unread?.markRead(widget.peerPubkey));
|
||||
_messages?.close();
|
||||
_trust?.close();
|
||||
_session?.close();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue