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
|
|
@ -4,10 +4,13 @@ import 'package:commons_core/commons_core.dart';
|
|||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../i18n/strings.g.dart';
|
||||
import 'message_store.dart';
|
||||
import 'notification_service.dart';
|
||||
import 'profile_cache.dart';
|
||||
import 'social_service.dart';
|
||||
import 'social_settings.dart';
|
||||
import 'unread_service.dart';
|
||||
|
||||
/// App-wide inbox listener for private messages (NIP-17).
|
||||
///
|
||||
|
|
@ -26,15 +29,21 @@ class InboxService {
|
|||
required SocialSettings settings,
|
||||
required MessageStore store,
|
||||
ProfileCache? profileCache,
|
||||
UnreadService? unread,
|
||||
NotificationService? notifications,
|
||||
}) : _social = social,
|
||||
_settings = settings,
|
||||
_store = store,
|
||||
_profileCache = profileCache;
|
||||
_profileCache = profileCache,
|
||||
_unread = unread,
|
||||
_notifications = notifications;
|
||||
|
||||
final SocialService _social;
|
||||
final SocialSettings _settings;
|
||||
final MessageStore _store;
|
||||
final ProfileCache? _profileCache;
|
||||
final UnreadService? _unread;
|
||||
final NotificationService? _notifications;
|
||||
|
||||
final _changes = StreamController<void>.broadcast();
|
||||
SocialSession? _session;
|
||||
|
|
@ -101,7 +110,29 @@ class InboxService {
|
|||
final stored = await _store.append(message.fromPubkey, message);
|
||||
if (!stored) return; // a re-delivered duplicate
|
||||
if (!_changes.isClosed) _changes.add(null);
|
||||
await _cacheName(message.fromPubkey);
|
||||
|
||||
final peer = message.fromPubkey;
|
||||
// Defensive: never badge or notify for your own message. NIP-17 doesn't
|
||||
// loop a sent gift wrap back to its author, so this shouldn't happen — the
|
||||
// guard just keeps the seam honest.
|
||||
if (peer != _social.publicKeyHex) {
|
||||
await _unread?.onMessageReceived(peer, message);
|
||||
await _maybeNotify(peer);
|
||||
}
|
||||
await _cacheName(peer);
|
||||
}
|
||||
|
||||
/// Fires a text-free OS notification for a new message from [peer], unless its
|
||||
/// chat is the one already on screen (then the message is visible anyway).
|
||||
Future<void> _maybeNotify(String peer) async {
|
||||
final notifications = _notifications;
|
||||
if (notifications == null) return;
|
||||
if (_unread?.activePeer == peer) return;
|
||||
final name = await _profileCache?.name(peer) ?? shortPubkey(peer);
|
||||
await notifications.showMessage(
|
||||
peerPubkey: peer,
|
||||
title: t.notifications.newMessageFrom(name: name),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _cacheName(String peerPubkey) async {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue