improved reliability and error handling for background notification actions by migrating to a top-level entry point for stable AOT callback resolution; implemented failure notifications to preserve undelivered reply text and surface technical errors; resolved a race condition that could resurrect dismissed notifications during silent re-renders; updated the Android manifest with the required broadcast receiver for notification actions; and refactored internal OCS methods to return detailed success/failure records for improved logging and field debugging.

This commit is contained in:
2026-07-05 23:43:58 +02:00
parent 483fea62ba
commit dab208877f
5 changed files with 214 additions and 37 deletions
+47 -1
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../notification/notification_service.dart';
@@ -146,6 +147,17 @@ class PushRenderer {
bool alert = true,
}) async {
if (messages.isEmpty) return;
// A silent render only ever UPDATES an existing card (delete-push shrunk
// the thread, late avatar arrived, failed reply needs its spinner
// stopped). If the card is verifiably gone meanwhile (reply/mark-read
// cleanup cancelled it, or the user swiped it away), re-posting would
// resurrect it — and Android would re-attach a pending inline reply on
// top. Probe failure (null) still renders: stopping a possible reply
// spinner outweighs a rare resurrection.
if (!alert && await _isChatNotificationActive(chatToken) == false) {
debugPrint('PushRenderer: skip silent re-render, card gone ($chatToken)');
return;
}
final tag = chatNotificationTag(chatToken);
final id = stableChatNotificationId(chatToken);
final latest = messages.last;
@@ -261,12 +273,18 @@ class PushRenderer {
);
}
// Both actions keep cancelNotification: true (the default): the plugin's
// Java receiver then removes the card NATIVELY the moment the action fires.
// The reply action must not rely on our Dart-side cancel instead — MIUI/
// HyperOS ignores an app-issued cancel while an inline reply is pending, so
// the card would stay behind showing the reply as an attached "Ich" row.
// If sending subsequently fails, the error card (with the typed text)
// replaces the lost thread card.
static const _talkActions = [
AndroidNotificationAction(
kTalkReplyActionId,
'Antworten',
showsUserInterface: false,
cancelNotification: false,
inputs: [AndroidNotificationActionInput(label: 'Nachricht')],
),
AndroidNotificationAction(
@@ -276,6 +294,34 @@ class PushRenderer {
),
];
/// Renders a failure card for a Talk notification action (reply/mark-read).
/// Separate tag per chat, so it neither replaces the thread notification
/// nor stacks across repeated failures. Carries no payload — tapping it
/// just opens the app.
Future<void> renderTalkActionError({
required String chatToken,
required String title,
required String body,
}) async {
await _plugin.show(
id: stableChatNotificationId(chatToken),
title: title,
body: body,
notificationDetails: NotificationDetails(
android: AndroidNotificationDetails(
talkChannelId,
talkChannelName,
importance: Importance.high,
priority: Priority.high,
color: _accentColor,
tag: 'talk_error_$chatToken',
styleInformation: BigTextStyleInformation(body),
),
iOS: const DarwinNotificationDetails(),
),
);
}
Future<void> _renderGeneric(PushSubject subject) async {
final nid = subject.nid ?? _fallbackId(subject.subject);
const androidDetails = AndroidNotificationDetails(