diff --git a/app/lib/core/push/push_bootstrap.dart b/app/lib/core/push/push_bootstrap.dart index 20beae6..b7e816a 100644 --- a/app/lib/core/push/push_bootstrap.dart +++ b/app/lib/core/push/push_bootstrap.dart @@ -45,6 +45,7 @@ class PushBootstrap { static const _pendingEndpointKey = 'pending_push_endpoint'; static const _modeKey = 'push_mode'; // 'off' | 'up' | 'fcm' + static const _registrationFailedKey = 'push_registration_failed'; static const _store = FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), ); @@ -57,6 +58,16 @@ class PushBootstrap { static Future writeMode(String mode) => _store.write(key: _modeKey, value: mode); + /// Odmítl distributor registraci? Selhání se dřív jen spolklo, takže + /// uživatel měl v Nastavení „push zapnutý", ale budíčky nechodily a + /// zprávy čekaly, než appku otevře. Nastavení si to přečte a řekne to. + static Future registrationFailed() async => + await _store.read(key: _registrationFailedKey) == '1'; + + static Future _setRegistrationFailed(bool failed) => failed + ? _store.write(key: _registrationFailedKey, value: '1') + : _store.delete(key: _registrationFailedKey); + static Future init(List args) async { backgroundStart = args.contains('--unifiedpush-bg'); @@ -69,7 +80,8 @@ class PushBootstrap { } else { await UnifiedPush.initialize( onNewEndpoint: _onNewEndpoint, - onRegistrationFailed: (_, _) {}, + onRegistrationFailed: (_, _) => + unawaited(_setRegistrationFailed(true)), onUnregistered: _onUnregistered, onMessage: _onMessage, ); @@ -114,6 +126,7 @@ class PushBootstrap { } static void _onNewEndpoint(PushEndpoint endpoint, String instance) { + unawaited(_setRegistrationFailed(false)); // registrace se povedla final s = _service; if (s != null) { s.handleNewEndpoint(endpoint.url); diff --git a/app/lib/core/push/push_service.dart b/app/lib/core/push/push_service.dart index 5b8f0ab..9032d39 100644 --- a/app/lib/core/push/push_service.dart +++ b/app/lib/core/push/push_service.dart @@ -144,6 +144,10 @@ class PushService { return (await UnifiedPush.getDistributor()) != null ? 'up' : 'off'; } + /// Odmítl distributor poslední registraci? Nastavení to ukáže — jinak by + /// push vypadal zapnutý, ale budíčky by nechodily. + Future registrationFailed() => PushBootstrap.registrationFailed(); + Future setModeOff() async { await _teardownFcm(deleteToken: true); try { diff --git a/app/lib/features/chat_list/chat_list_screen.dart b/app/lib/features/chat_list/chat_list_screen.dart index 4b41396..bca9fa1 100644 --- a/app/lib/features/chat_list/chat_list_screen.dart +++ b/app/lib/features/chat_list/chat_list_screen.dart @@ -5,6 +5,7 @@ import 'package:sepot/core/l10n/error_messages.dart'; import 'package:sepot/core/l10n/l10n.dart'; import 'package:sepot/core/theme/app_theme.dart'; import 'package:sepot/features/chat_list/chat_list_tile.dart'; +import 'package:sepot/features/chat_list/push_hint_banner.dart'; import 'package:sepot/models/conversation.dart'; import 'package:sepot/providers/contacts_provider.dart'; import 'package:sepot/providers/conversations_provider.dart'; @@ -62,7 +63,13 @@ class ChatListScreen extends ConsumerWidget { } final hasArchive = archived.isNotEmpty; - return ListView.builder( + return Column( + children: [ + // Bez budíčků appka na pozadí spí — ať uživatel ví, proč + // mu zprávy chodí, až když Šepot otevře. + const PushHintBanner(), + Expanded( + child: ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: active.length + (hasArchive ? 1 : 0), itemBuilder: (context, index) { @@ -95,7 +102,10 @@ class ChatListScreen extends ConsumerWidget { onTap: () => context.push('/conversation/${conv.contactId}'), onLongPress: () => _showChatActions(context, ref, conv, name), ); - }, + }, + ), + ), + ], ); }, loading: () => const Center( diff --git a/app/lib/features/chat_list/push_hint_banner.dart b/app/lib/features/chat_list/push_hint_banner.dart new file mode 100644 index 0000000..4c08df2 --- /dev/null +++ b/app/lib/features/chat_list/push_hint_banner.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:sepot/core/l10n/l10n.dart'; +import 'package:sepot/core/theme/app_theme.dart'; +import 'package:sepot/providers/push_provider.dart'; +import 'package:sepot/providers/storage_provider.dart'; + +/// Upozornění nad seznamem chatů, když budíčky nefungují. +/// +/// Bez pushe Android appku na pozadí uspí a zprávy dorazí, až ji uživatel +/// sám otevře — vypadá to jako výpadek doručování, i když relay má všechno +/// připravené ve frontě. Proužek je proto informace o stavu, ne reklama: +/// zmizí, jakmile si push nastaví, a jde odmítnout natrvalo. +class PushHintBanner extends ConsumerWidget { + const PushHintBanner({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final health = ref.watch(pushHealthProvider).valueOrNull; + final dismissed = ref.watch(pushHintDismissedProvider).valueOrNull ?? true; + if (dismissed || health == null || health == PushHealth.ok) { + return const SizedBox.shrink(); + } + + final text = health == PushHealth.failed + ? context.l10n.pushHintFailed + : context.l10n.pushHintOff; + + return Material( + color: SepotColors.surfaceVariant, + child: InkWell( + onTap: () => context.push('/settings'), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 10, 8, 10), + child: Row( + children: [ + Icon( + Icons.notifications_off_outlined, + size: 18, + color: health == PushHealth.failed + ? SepotColors.ember + : SepotColors.textMuted, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: const TextStyle( + fontFamily: kFontSans, + fontSize: 13, + color: SepotColors.textSecondary, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close, size: 18), + color: SepotColors.textMuted, + tooltip: context.l10n.commonClose, + onPressed: () async { + await ref + .read(storageProvider) + .setSetting('push_hint_dismissed', true); + ref.invalidate(pushHintDismissedProvider); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/features/settings/push_settings_tile.dart b/app/lib/features/settings/push_settings_tile.dart index c014e7a..fa48c07 100644 --- a/app/lib/features/settings/push_settings_tile.dart +++ b/app/lib/features/settings/push_settings_tile.dart @@ -20,6 +20,7 @@ class _PushSettingsTileState extends ConsumerState { String _mode = 'off'; String? _distributor; bool _busy = false; + bool _registrationFailed = false; @override void initState() { @@ -31,10 +32,12 @@ class _PushSettingsTileState extends ConsumerState { final push = ref.read(pushServiceProvider); final mode = await push.currentMode(); final d = await push.currentDistributor(); + final failed = await push.registrationFailed(); if (mounted) { setState(() { _mode = mode; _distributor = d; + _registrationFailed = failed; }); } } @@ -160,12 +163,17 @@ class _PushSettingsTileState extends ConsumerState { @override Widget build(BuildContext context) { - final subtitle = switch (_mode) { - 'up' => context.l10n.pushSubtitleUnifiedPush( - _distributor ?? context.l10n.pushNoDistributorFallback), - 'fcm' => context.l10n.pushSubtitleFcm, - _ => context.l10n.commonOff, - }; + // Selhaná registrace přebíjí popis režimu — „zapnuto" bez budíčků je + // horší než vypnuto, protože uživatel čeká zprávy, které nepřijdou. + final failed = _registrationFailed && _mode != 'off'; + final subtitle = failed + ? context.l10n.pushRegistrationFailed + : switch (_mode) { + 'up' => context.l10n.pushSubtitleUnifiedPush( + _distributor ?? context.l10n.pushNoDistributorFallback), + 'fcm' => context.l10n.pushSubtitleFcm, + _ => context.l10n.commonOff, + }; return ListTile( leading: Container( width: 40, @@ -184,7 +192,10 @@ class _PushSettingsTileState extends ConsumerState { style: const TextStyle(fontSize: 15)), subtitle: Text( subtitle, - style: const TextStyle(fontSize: 13, color: SepotColors.textMuted), + style: TextStyle( + fontSize: 13, + color: failed ? SepotColors.ember : SepotColors.textMuted, + ), ), trailing: _busy ? const SizedBox( diff --git a/app/lib/l10n/app_cs.arb b/app/lib/l10n/app_cs.arb index a86e083..43d6c0e 100644 --- a/app/lib/l10n/app_cs.arb +++ b/app/lib/l10n/app_cs.arb @@ -1295,6 +1295,18 @@ } } }, + "pushHintOff": "Zprávy ti přijdou, až Šepot otevřeš — budíčky nejsou nastavené.", + "@pushHintOff": { + "description": "Proužek nad seznamem chatů, když push není vůbec zapnutý." + }, + "pushHintFailed": "Budíčky nefungují, zprávy uvidíš až po otevření Šepotu.", + "@pushHintFailed": { + "description": "Proužek nad seznamem chatů, když distributor registraci odmítl." + }, + "pushRegistrationFailed": "Budíčky nechodí — distributor registraci odmítl. Zkus push vypnout a znovu zapnout.", + "@pushRegistrationFailed": { + "description": "Podtitulek dlaždice Push v Nastavení, když distributor odmítl registraci. Bez téhle hlášky uživatel vidí push jako zapnutý, ale zprávy mu chodí až po otevření appky." + }, "pushNoDistributorTitle": "Žádný distributor", "@pushNoDistributorTitle": { "description": "Titulek AlertDialog, když v systému není nainstalovaný žádný UnifiedPush distributor" diff --git a/app/lib/l10n/app_de.arb b/app/lib/l10n/app_de.arb index 39c5b3c..9b98b41 100644 --- a/app/lib/l10n/app_de.arb +++ b/app/lib/l10n/app_de.arb @@ -271,6 +271,9 @@ "pushModeFcm": "Google (FCM)", "pushModeFcmDesc": "Zuverlässig ohne zusätzliche App. Der Push-Token des Geräts geht an Google; der Inhalt der Nachrichten nie.", "pushSetupFailed": "Einrichtung der Push-Benachrichtigungen fehlgeschlagen: {error}", + "pushHintOff": "Nachrichten kommen erst an, wenn du Šepot öffnest — keine Weckrufe eingerichtet.", + "pushHintFailed": "Weckrufe funktionieren nicht; Nachrichten siehst du erst beim Öffnen von Šepot.", + "pushRegistrationFailed": "Es kommen keine Weckrufe an — der Distributor hat die Registrierung abgelehnt. Schalte Push aus und wieder ein.", "pushNoDistributorTitle": "Kein Distributor", "pushNoDistributorBody": "Für Google-freie Benachrichtigungen installiere einen UnifiedPush-Distributor, z. B. ntfy (F-Droid oder Play). Aktiviere Push danach erneut.", "commonOk": "OK", diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 0187e96..da2ff93 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -271,6 +271,9 @@ "pushModeFcm": "Google (FCM)", "pushModeFcmDesc": "Reliable without an extra app. The device push token goes to Google; message content never does.", "pushSetupFailed": "Setting up push failed: {error}", + "pushHintOff": "Messages arrive only when you open Šepot — wake-ups aren’t set up.", + "pushHintFailed": "Wake-ups aren’t working; you’ll see messages only after opening Šepot.", + "pushRegistrationFailed": "Wake-ups aren’t arriving — the distributor refused registration. Try turning push off and on again.", "pushNoDistributorTitle": "No distributor", "pushNoDistributorBody": "For de-Googled notifications, install a UnifiedPush distributor such as ntfy (F-Droid or Play). Then turn push on again.", "commonOk": "OK", diff --git a/app/lib/l10n/app_it.arb b/app/lib/l10n/app_it.arb index 1f97c97..4fc0143 100644 --- a/app/lib/l10n/app_it.arb +++ b/app/lib/l10n/app_it.arb @@ -271,6 +271,9 @@ "pushModeFcm": "Google (FCM)", "pushModeFcmDesc": "Affidabile senza altre app. Il token push del dispositivo va a Google; il contenuto dei messaggi mai.", "pushSetupFailed": "Configurazione delle notifiche push non riuscita: {error}", + "pushHintOff": "I messaggi arrivano solo quando apri Šepot — le notifiche non sono configurate.", + "pushHintFailed": "Le notifiche non funzionano: vedrai i messaggi solo aprendo Šepot.", + "pushRegistrationFailed": "Le notifiche non arrivano — il distributore ha rifiutato la registrazione. Prova a disattivare e riattivare il push.", "pushNoDistributorTitle": "Nessun distributore", "pushNoDistributorBody": "Per notifiche senza Google installa un distributore UnifiedPush, ad es. ntfy (F-Droid o Play). Poi riattiva le notifiche push.", "commonOk": "OK", diff --git a/app/lib/l10n/app_ru.arb b/app/lib/l10n/app_ru.arb index 73743c9..5d05175 100644 --- a/app/lib/l10n/app_ru.arb +++ b/app/lib/l10n/app_ru.arb @@ -271,6 +271,9 @@ "pushModeFcm": "Google (FCM)", "pushModeFcmDesc": "Надёжно и без дополнительных приложений. Push-токен устройства передаётся Google; содержимое сообщений — никогда.", "pushSetupFailed": "Не удалось настроить push: {error}", + "pushHintOff": "Сообщения придут, только когда откроешь Šepot — сигналы не настроены.", + "pushHintFailed": "Сигналы не работают: сообщения увидишь только после открытия Šepot.", + "pushRegistrationFailed": "Сигналы не приходят — дистрибьютор отклонил регистрацию. Попробуй выключить и снова включить push.", "pushNoDistributorTitle": "Нет дистрибьютора", "pushNoDistributorBody": "Для уведомлений без сервисов Google установите дистрибьютор UnifiedPush, например ntfy (F-Droid или Play). Затем включите push снова.", "commonOk": "OK", diff --git a/app/lib/l10n/generated/app_localizations.dart b/app/lib/l10n/generated/app_localizations.dart index 3bad78a..efe465e 100644 --- a/app/lib/l10n/generated/app_localizations.dart +++ b/app/lib/l10n/generated/app_localizations.dart @@ -1730,6 +1730,24 @@ abstract class AppLocalizations { /// **'Nastavení pushů selhalo: {error}'** String pushSetupFailed(String error); + /// Proužek nad seznamem chatů, když push není vůbec zapnutý. + /// + /// In cs, this message translates to: + /// **'Zprávy ti přijdou, až Šepot otevřeš — budíčky nejsou nastavené.'** + String get pushHintOff; + + /// Proužek nad seznamem chatů, když distributor registraci odmítl. + /// + /// In cs, this message translates to: + /// **'Budíčky nefungují, zprávy uvidíš až po otevření Šepotu.'** + String get pushHintFailed; + + /// Podtitulek dlaždice Push v Nastavení, když distributor odmítl registraci. Bez téhle hlášky uživatel vidí push jako zapnutý, ale zprávy mu chodí až po otevření appky. + /// + /// In cs, this message translates to: + /// **'Budíčky nechodí — distributor registraci odmítl. Zkus push vypnout a znovu zapnout.'** + String get pushRegistrationFailed; + /// Titulek AlertDialog, když v systému není nainstalovaný žádný UnifiedPush distributor /// /// In cs, this message translates to: diff --git a/app/lib/l10n/generated/app_localizations_cs.dart b/app/lib/l10n/generated/app_localizations_cs.dart index 994e3a1..468b961 100644 --- a/app/lib/l10n/generated/app_localizations_cs.dart +++ b/app/lib/l10n/generated/app_localizations_cs.dart @@ -961,6 +961,18 @@ class AppLocalizationsCs extends AppLocalizations { return 'Nastavení pushů selhalo: $error'; } + @override + String get pushHintOff => + 'Zprávy ti přijdou, až Šepot otevřeš — budíčky nejsou nastavené.'; + + @override + String get pushHintFailed => + 'Budíčky nefungují, zprávy uvidíš až po otevření Šepotu.'; + + @override + String get pushRegistrationFailed => + 'Budíčky nechodí — distributor registraci odmítl. Zkus push vypnout a znovu zapnout.'; + @override String get pushNoDistributorTitle => 'Žádný distributor'; diff --git a/app/lib/l10n/generated/app_localizations_de.dart b/app/lib/l10n/generated/app_localizations_de.dart index ba40bee..3dfad4f 100644 --- a/app/lib/l10n/generated/app_localizations_de.dart +++ b/app/lib/l10n/generated/app_localizations_de.dart @@ -980,6 +980,18 @@ class AppLocalizationsDe extends AppLocalizations { return 'Einrichtung der Push-Benachrichtigungen fehlgeschlagen: $error'; } + @override + String get pushHintOff => + 'Nachrichten kommen erst an, wenn du Šepot öffnest — keine Weckrufe eingerichtet.'; + + @override + String get pushHintFailed => + 'Weckrufe funktionieren nicht; Nachrichten siehst du erst beim Öffnen von Šepot.'; + + @override + String get pushRegistrationFailed => + 'Es kommen keine Weckrufe an — der Distributor hat die Registrierung abgelehnt. Schalte Push aus und wieder ein.'; + @override String get pushNoDistributorTitle => 'Kein Distributor'; diff --git a/app/lib/l10n/generated/app_localizations_en.dart b/app/lib/l10n/generated/app_localizations_en.dart index 1c60967..494658e 100644 --- a/app/lib/l10n/generated/app_localizations_en.dart +++ b/app/lib/l10n/generated/app_localizations_en.dart @@ -959,6 +959,18 @@ class AppLocalizationsEn extends AppLocalizations { return 'Setting up push failed: $error'; } + @override + String get pushHintOff => + 'Messages arrive only when you open Šepot — wake-ups aren’t set up.'; + + @override + String get pushHintFailed => + 'Wake-ups aren’t working; you’ll see messages only after opening Šepot.'; + + @override + String get pushRegistrationFailed => + 'Wake-ups aren’t arriving — the distributor refused registration. Try turning push off and on again.'; + @override String get pushNoDistributorTitle => 'No distributor'; diff --git a/app/lib/l10n/generated/app_localizations_it.dart b/app/lib/l10n/generated/app_localizations_it.dart index 2462987..7123eb4 100644 --- a/app/lib/l10n/generated/app_localizations_it.dart +++ b/app/lib/l10n/generated/app_localizations_it.dart @@ -969,6 +969,18 @@ class AppLocalizationsIt extends AppLocalizations { return 'Configurazione delle notifiche push non riuscita: $error'; } + @override + String get pushHintOff => + 'I messaggi arrivano solo quando apri Šepot — le notifiche non sono configurate.'; + + @override + String get pushHintFailed => + 'Le notifiche non funzionano: vedrai i messaggi solo aprendo Šepot.'; + + @override + String get pushRegistrationFailed => + 'Le notifiche non arrivano — il distributore ha rifiutato la registrazione. Prova a disattivare e riattivare il push.'; + @override String get pushNoDistributorTitle => 'Nessun distributore'; diff --git a/app/lib/l10n/generated/app_localizations_ru.dart b/app/lib/l10n/generated/app_localizations_ru.dart index ddcde31..1352b85 100644 --- a/app/lib/l10n/generated/app_localizations_ru.dart +++ b/app/lib/l10n/generated/app_localizations_ru.dart @@ -976,6 +976,18 @@ class AppLocalizationsRu extends AppLocalizations { return 'Не удалось настроить push: $error'; } + @override + String get pushHintOff => + 'Сообщения придут, только когда откроешь Šepot — сигналы не настроены.'; + + @override + String get pushHintFailed => + 'Сигналы не работают: сообщения увидишь только после открытия Šepot.'; + + @override + String get pushRegistrationFailed => + 'Сигналы не приходят — дистрибьютор отклонил регистрацию. Попробуй выключить и снова включить push.'; + @override String get pushNoDistributorTitle => 'Нет дистрибьютора'; diff --git a/app/lib/providers/push_provider.dart b/app/lib/providers/push_provider.dart index f7597e8..387f5e0 100644 --- a/app/lib/providers/push_provider.dart +++ b/app/lib/providers/push_provider.dart @@ -1,6 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sepot/core/push/push_service.dart'; import 'package:sepot/providers/messaging_provider.dart'; +import 'package:sepot/providers/storage_provider.dart'; /// Push vrstva nad messagingem. final pushServiceProvider = Provider((ref) { @@ -18,3 +19,31 @@ final pushInitProvider = FutureProvider((ref) async { // re-registruje u relaye po každém startu. await push.restoreMode(); }); + +/// Stav budíčků pro chat list. Bez funkčního pushe appka na pozadí spí a +/// zprávy dorazí, až ji uživatel otevře — o tom má vědět dřív, než mu +/// někdo vyčte, že neodpovídá. +enum PushHealth { + /// Budíčky nastavené a bez chyby. + ok, + + /// Push vůbec nezapnutý (výchozí stav po instalaci). + off, + + /// Distributor registraci odmítl — zapnuto, ale nefunguje. + failed, +} + +final pushHealthProvider = FutureProvider((ref) async { + await ref.watch(pushInitProvider.future); + final push = ref.read(pushServiceProvider); + if (await push.registrationFailed()) return PushHealth.failed; + return await push.currentMode() == 'off' ? PushHealth.off : PushHealth.ok; +}); + +/// Skryl si uživatel upozornění na chat listu? Rozhodnutí se respektuje — +/// připomínat donekonečna by bylo otravné. +final pushHintDismissedProvider = FutureProvider((ref) async { + final storage = ref.watch(storageProvider); + return await storage.getSetting('push_hint_dismissed') ?? false; +}); diff --git a/app/test/widget/push_hint_banner_test.dart b/app/test/widget/push_hint_banner_test.dart new file mode 100644 index 0000000..663175f --- /dev/null +++ b/app/test/widget/push_hint_banner_test.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sepot/features/chat_list/push_hint_banner.dart'; +import 'package:sepot/l10n/generated/app_localizations.dart'; +import 'package:sepot/providers/push_provider.dart'; + +/// Proužek říká uživateli, proč mu zprávy chodí až po otevření appky. +/// Musí se ukázat jen tehdy, když budíčky opravdu nefungují — jinak by +/// z informace o stavu byla otravná reklama. +Widget _app({required PushHealth health, required bool dismissed}) => + ProviderScope( + overrides: [ + pushHealthProvider.overrideWith((ref) async => health), + pushHintDismissedProvider.overrideWith((ref) async => dismissed), + ], + child: const MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: Locale('cs'), + home: Scaffold(body: PushHintBanner()), + ), + ); + +void main() { + testWidgets('push vypnutý → proužek se ukáže', (tester) async { + await tester.pumpWidget(_app(health: PushHealth.off, dismissed: false)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.notifications_off_outlined), findsOneWidget); + }); + + testWidgets('registrace selhala → proužek se ukáže', (tester) async { + await tester.pumpWidget(_app(health: PushHealth.failed, dismissed: false)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.notifications_off_outlined), findsOneWidget); + }); + + testWidgets('budíčky fungují → proužek mlčí', (tester) async { + await tester.pumpWidget(_app(health: PushHealth.ok, dismissed: false)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.notifications_off_outlined), findsNothing); + }); + + testWidgets('odmítnutý proužek se znovu neukáže', (tester) async { + await tester.pumpWidget(_app(health: PushHealth.off, dismissed: true)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.notifications_off_outlined), findsNothing); + }); +}