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
|
|
@ -23,6 +23,7 @@ import '../services/file_picker_file_service.dart';
|
|||
import '../services/file_service.dart';
|
||||
import '../services/inbox_service.dart';
|
||||
import '../services/locale_store.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../services/ocr/label_text_extractor.dart';
|
||||
import '../services/ocr/ocr_language.dart';
|
||||
import '../services/ocr/tesseract_label_extractor.dart';
|
||||
|
|
@ -36,6 +37,7 @@ import '../services/profile_cache.dart';
|
|||
import '../services/profile_store.dart';
|
||||
import '../services/social_service.dart';
|
||||
import '../services/social_settings.dart';
|
||||
import '../services/unread_service.dart';
|
||||
|
||||
/// The app's service locator. Kept to the composition root — widgets get their
|
||||
/// repositories from here (or via BlocProvider), never by reaching into it deep
|
||||
|
|
@ -169,19 +171,32 @@ Future<void> configureDependencies() async {
|
|||
);
|
||||
}
|
||||
|
||||
// OS notifications for foreground messages. Registered unconditionally — it
|
||||
// self-no-ops on unsupported platforms (web/Windows) and when the social layer
|
||||
// is absent nothing ever calls it. Its tap handler is wired to the router in
|
||||
// `TaneApp`, after the router exists.
|
||||
getIt.registerSingleton<NotificationService>(NotificationService());
|
||||
|
||||
// Optional: absent only if the derivation above failed — then the app runs
|
||||
// inventory-only, by design (market/chat/profile hidden).
|
||||
if (socialService != null) {
|
||||
getIt
|
||||
..registerSingleton<SocialService>(socialService)
|
||||
// Tracks unread private messages so the UI can badge them.
|
||||
..registerSingleton<UnreadService>(
|
||||
UnreadService(getIt<MessageStore>(), secretStore),
|
||||
)
|
||||
// App-wide inbox listener so private messages arrive (and land in the
|
||||
// inbox list) even when the specific chat isn't open. Started in `main`.
|
||||
// It also updates unread counts and fires notifications.
|
||||
..registerSingleton<InboxService>(
|
||||
InboxService(
|
||||
social: socialService,
|
||||
settings: getIt<SocialSettings>(),
|
||||
store: getIt<MessageStore>(),
|
||||
profileCache: getIt<ProfileCache>(),
|
||||
unread: getIt<UnreadService>(),
|
||||
notifications: getIt<NotificationService>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue