fix(app): say it out loud when wake-ups do not work
Registration failures from the distributor were swallowed, so push looked enabled while nothing arrived. Without wake-ups Android suspends the app and messages only show up once the user opens it — which looks like broken delivery even though the relay has them queued. The failure is now remembered and surfaced in Settings, and the chat list carries a dismissible line explaining the state.
This commit is contained in:
@@ -45,6 +45,7 @@ class PushBootstrap {
|
|||||||
|
|
||||||
static const _pendingEndpointKey = 'pending_push_endpoint';
|
static const _pendingEndpointKey = 'pending_push_endpoint';
|
||||||
static const _modeKey = 'push_mode'; // 'off' | 'up' | 'fcm'
|
static const _modeKey = 'push_mode'; // 'off' | 'up' | 'fcm'
|
||||||
|
static const _registrationFailedKey = 'push_registration_failed';
|
||||||
static const _store = FlutterSecureStorage(
|
static const _store = FlutterSecureStorage(
|
||||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||||
);
|
);
|
||||||
@@ -57,6 +58,16 @@ class PushBootstrap {
|
|||||||
static Future<void> writeMode(String mode) =>
|
static Future<void> writeMode(String mode) =>
|
||||||
_store.write(key: _modeKey, value: 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<bool> registrationFailed() async =>
|
||||||
|
await _store.read(key: _registrationFailedKey) == '1';
|
||||||
|
|
||||||
|
static Future<void> _setRegistrationFailed(bool failed) => failed
|
||||||
|
? _store.write(key: _registrationFailedKey, value: '1')
|
||||||
|
: _store.delete(key: _registrationFailedKey);
|
||||||
|
|
||||||
static Future<void> init(List<String> args) async {
|
static Future<void> init(List<String> args) async {
|
||||||
backgroundStart = args.contains('--unifiedpush-bg');
|
backgroundStart = args.contains('--unifiedpush-bg');
|
||||||
|
|
||||||
@@ -69,7 +80,8 @@ class PushBootstrap {
|
|||||||
} else {
|
} else {
|
||||||
await UnifiedPush.initialize(
|
await UnifiedPush.initialize(
|
||||||
onNewEndpoint: _onNewEndpoint,
|
onNewEndpoint: _onNewEndpoint,
|
||||||
onRegistrationFailed: (_, _) {},
|
onRegistrationFailed: (_, _) =>
|
||||||
|
unawaited(_setRegistrationFailed(true)),
|
||||||
onUnregistered: _onUnregistered,
|
onUnregistered: _onUnregistered,
|
||||||
onMessage: _onMessage,
|
onMessage: _onMessage,
|
||||||
);
|
);
|
||||||
@@ -114,6 +126,7 @@ class PushBootstrap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void _onNewEndpoint(PushEndpoint endpoint, String instance) {
|
static void _onNewEndpoint(PushEndpoint endpoint, String instance) {
|
||||||
|
unawaited(_setRegistrationFailed(false)); // registrace se povedla
|
||||||
final s = _service;
|
final s = _service;
|
||||||
if (s != null) {
|
if (s != null) {
|
||||||
s.handleNewEndpoint(endpoint.url);
|
s.handleNewEndpoint(endpoint.url);
|
||||||
|
|||||||
@@ -144,6 +144,10 @@ class PushService {
|
|||||||
return (await UnifiedPush.getDistributor()) != null ? 'up' : 'off';
|
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<bool> registrationFailed() => PushBootstrap.registrationFailed();
|
||||||
|
|
||||||
Future<void> setModeOff() async {
|
Future<void> setModeOff() async {
|
||||||
await _teardownFcm(deleteToken: true);
|
await _teardownFcm(deleteToken: true);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:sepot/core/l10n/error_messages.dart';
|
|||||||
import 'package:sepot/core/l10n/l10n.dart';
|
import 'package:sepot/core/l10n/l10n.dart';
|
||||||
import 'package:sepot/core/theme/app_theme.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/chat_list_tile.dart';
|
||||||
|
import 'package:sepot/features/chat_list/push_hint_banner.dart';
|
||||||
import 'package:sepot/models/conversation.dart';
|
import 'package:sepot/models/conversation.dart';
|
||||||
import 'package:sepot/providers/contacts_provider.dart';
|
import 'package:sepot/providers/contacts_provider.dart';
|
||||||
import 'package:sepot/providers/conversations_provider.dart';
|
import 'package:sepot/providers/conversations_provider.dart';
|
||||||
@@ -62,7 +63,13 @@ class ChatListScreen extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
final hasArchive = archived.isNotEmpty;
|
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),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
itemCount: active.length + (hasArchive ? 1 : 0),
|
itemCount: active.length + (hasArchive ? 1 : 0),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -96,6 +103,9 @@ class ChatListScreen extends ConsumerWidget {
|
|||||||
onLongPress: () => _showChatActions(context, ref, conv, name),
|
onLongPress: () => _showChatActions(context, ref, conv, name),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ class _PushSettingsTileState extends ConsumerState<PushSettingsTile> {
|
|||||||
String _mode = 'off';
|
String _mode = 'off';
|
||||||
String? _distributor;
|
String? _distributor;
|
||||||
bool _busy = false;
|
bool _busy = false;
|
||||||
|
bool _registrationFailed = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -31,10 +32,12 @@ class _PushSettingsTileState extends ConsumerState<PushSettingsTile> {
|
|||||||
final push = ref.read(pushServiceProvider);
|
final push = ref.read(pushServiceProvider);
|
||||||
final mode = await push.currentMode();
|
final mode = await push.currentMode();
|
||||||
final d = await push.currentDistributor();
|
final d = await push.currentDistributor();
|
||||||
|
final failed = await push.registrationFailed();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_mode = mode;
|
_mode = mode;
|
||||||
_distributor = d;
|
_distributor = d;
|
||||||
|
_registrationFailed = failed;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,7 +163,12 @@ class _PushSettingsTileState extends ConsumerState<PushSettingsTile> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final subtitle = switch (_mode) {
|
// 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(
|
'up' => context.l10n.pushSubtitleUnifiedPush(
|
||||||
_distributor ?? context.l10n.pushNoDistributorFallback),
|
_distributor ?? context.l10n.pushNoDistributorFallback),
|
||||||
'fcm' => context.l10n.pushSubtitleFcm,
|
'fcm' => context.l10n.pushSubtitleFcm,
|
||||||
@@ -184,7 +192,10 @@ class _PushSettingsTileState extends ConsumerState<PushSettingsTile> {
|
|||||||
style: const TextStyle(fontSize: 15)),
|
style: const TextStyle(fontSize: 15)),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
style: const TextStyle(fontSize: 13, color: SepotColors.textMuted),
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: failed ? SepotColors.ember : SepotColors.textMuted,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
trailing: _busy
|
trailing: _busy
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
|
|||||||
@@ -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": "Žádný distributor",
|
||||||
"@pushNoDistributorTitle": {
|
"@pushNoDistributorTitle": {
|
||||||
"description": "Titulek AlertDialog, když v systému není nainstalovaný žádný UnifiedPush distributor"
|
"description": "Titulek AlertDialog, když v systému není nainstalovaný žádný UnifiedPush distributor"
|
||||||
|
|||||||
@@ -271,6 +271,9 @@
|
|||||||
"pushModeFcm": "Google (FCM)",
|
"pushModeFcm": "Google (FCM)",
|
||||||
"pushModeFcmDesc": "Zuverlässig ohne zusätzliche App. Der Push-Token des Geräts geht an Google; der Inhalt der Nachrichten nie.",
|
"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}",
|
"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",
|
"pushNoDistributorTitle": "Kein Distributor",
|
||||||
"pushNoDistributorBody": "Für Google-freie Benachrichtigungen installiere einen UnifiedPush-Distributor, z. B. ntfy (F-Droid oder Play). Aktiviere Push danach erneut.",
|
"pushNoDistributorBody": "Für Google-freie Benachrichtigungen installiere einen UnifiedPush-Distributor, z. B. ntfy (F-Droid oder Play). Aktiviere Push danach erneut.",
|
||||||
"commonOk": "OK",
|
"commonOk": "OK",
|
||||||
|
|||||||
@@ -271,6 +271,9 @@
|
|||||||
"pushModeFcm": "Google (FCM)",
|
"pushModeFcm": "Google (FCM)",
|
||||||
"pushModeFcmDesc": "Reliable without an extra app. The device push token goes to Google; message content never does.",
|
"pushModeFcmDesc": "Reliable without an extra app. The device push token goes to Google; message content never does.",
|
||||||
"pushSetupFailed": "Setting up push failed: {error}",
|
"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",
|
"pushNoDistributorTitle": "No distributor",
|
||||||
"pushNoDistributorBody": "For de-Googled notifications, install a UnifiedPush distributor such as ntfy (F-Droid or Play). Then turn push on again.",
|
"pushNoDistributorBody": "For de-Googled notifications, install a UnifiedPush distributor such as ntfy (F-Droid or Play). Then turn push on again.",
|
||||||
"commonOk": "OK",
|
"commonOk": "OK",
|
||||||
|
|||||||
@@ -271,6 +271,9 @@
|
|||||||
"pushModeFcm": "Google (FCM)",
|
"pushModeFcm": "Google (FCM)",
|
||||||
"pushModeFcmDesc": "Affidabile senza altre app. Il token push del dispositivo va a Google; il contenuto dei messaggi mai.",
|
"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}",
|
"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",
|
"pushNoDistributorTitle": "Nessun distributore",
|
||||||
"pushNoDistributorBody": "Per notifiche senza Google installa un distributore UnifiedPush, ad es. ntfy (F-Droid o Play). Poi riattiva le notifiche push.",
|
"pushNoDistributorBody": "Per notifiche senza Google installa un distributore UnifiedPush, ad es. ntfy (F-Droid o Play). Poi riattiva le notifiche push.",
|
||||||
"commonOk": "OK",
|
"commonOk": "OK",
|
||||||
|
|||||||
@@ -271,6 +271,9 @@
|
|||||||
"pushModeFcm": "Google (FCM)",
|
"pushModeFcm": "Google (FCM)",
|
||||||
"pushModeFcmDesc": "Надёжно и без дополнительных приложений. Push-токен устройства передаётся Google; содержимое сообщений — никогда.",
|
"pushModeFcmDesc": "Надёжно и без дополнительных приложений. Push-токен устройства передаётся Google; содержимое сообщений — никогда.",
|
||||||
"pushSetupFailed": "Не удалось настроить push: {error}",
|
"pushSetupFailed": "Не удалось настроить push: {error}",
|
||||||
|
"pushHintOff": "Сообщения придут, только когда откроешь Šepot — сигналы не настроены.",
|
||||||
|
"pushHintFailed": "Сигналы не работают: сообщения увидишь только после открытия Šepot.",
|
||||||
|
"pushRegistrationFailed": "Сигналы не приходят — дистрибьютор отклонил регистрацию. Попробуй выключить и снова включить push.",
|
||||||
"pushNoDistributorTitle": "Нет дистрибьютора",
|
"pushNoDistributorTitle": "Нет дистрибьютора",
|
||||||
"pushNoDistributorBody": "Для уведомлений без сервисов Google установите дистрибьютор UnifiedPush, например ntfy (F-Droid или Play). Затем включите push снова.",
|
"pushNoDistributorBody": "Для уведомлений без сервисов Google установите дистрибьютор UnifiedPush, например ntfy (F-Droid или Play). Затем включите push снова.",
|
||||||
"commonOk": "OK",
|
"commonOk": "OK",
|
||||||
|
|||||||
@@ -1730,6 +1730,24 @@ abstract class AppLocalizations {
|
|||||||
/// **'Nastavení pushů selhalo: {error}'**
|
/// **'Nastavení pushů selhalo: {error}'**
|
||||||
String pushSetupFailed(String 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
|
/// Titulek AlertDialog, když v systému není nainstalovaný žádný UnifiedPush distributor
|
||||||
///
|
///
|
||||||
/// In cs, this message translates to:
|
/// In cs, this message translates to:
|
||||||
|
|||||||
@@ -961,6 +961,18 @@ class AppLocalizationsCs extends AppLocalizations {
|
|||||||
return 'Nastavení pushů selhalo: $error';
|
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
|
@override
|
||||||
String get pushNoDistributorTitle => 'Žádný distributor';
|
String get pushNoDistributorTitle => 'Žádný distributor';
|
||||||
|
|
||||||
|
|||||||
@@ -980,6 +980,18 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
return 'Einrichtung der Push-Benachrichtigungen fehlgeschlagen: $error';
|
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
|
@override
|
||||||
String get pushNoDistributorTitle => 'Kein Distributor';
|
String get pushNoDistributorTitle => 'Kein Distributor';
|
||||||
|
|
||||||
|
|||||||
@@ -959,6 +959,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
return 'Setting up push failed: $error';
|
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
|
@override
|
||||||
String get pushNoDistributorTitle => 'No distributor';
|
String get pushNoDistributorTitle => 'No distributor';
|
||||||
|
|
||||||
|
|||||||
@@ -969,6 +969,18 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
return 'Configurazione delle notifiche push non riuscita: $error';
|
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
|
@override
|
||||||
String get pushNoDistributorTitle => 'Nessun distributore';
|
String get pushNoDistributorTitle => 'Nessun distributore';
|
||||||
|
|
||||||
|
|||||||
@@ -976,6 +976,18 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'Не удалось настроить push: $error';
|
return 'Не удалось настроить push: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pushHintOff =>
|
||||||
|
'Сообщения придут, только когда откроешь Šepot — сигналы не настроены.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pushHintFailed =>
|
||||||
|
'Сигналы не работают: сообщения увидишь только после открытия Šepot.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pushRegistrationFailed =>
|
||||||
|
'Сигналы не приходят — дистрибьютор отклонил регистрацию. Попробуй выключить и снова включить push.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushNoDistributorTitle => 'Нет дистрибьютора';
|
String get pushNoDistributorTitle => 'Нет дистрибьютора';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:sepot/core/push/push_service.dart';
|
import 'package:sepot/core/push/push_service.dart';
|
||||||
import 'package:sepot/providers/messaging_provider.dart';
|
import 'package:sepot/providers/messaging_provider.dart';
|
||||||
|
import 'package:sepot/providers/storage_provider.dart';
|
||||||
|
|
||||||
/// Push vrstva nad messagingem.
|
/// Push vrstva nad messagingem.
|
||||||
final pushServiceProvider = Provider<PushService>((ref) {
|
final pushServiceProvider = Provider<PushService>((ref) {
|
||||||
@@ -18,3 +19,31 @@ final pushInitProvider = FutureProvider<void>((ref) async {
|
|||||||
// re-registruje u relaye po každém startu.
|
// re-registruje u relaye po každém startu.
|
||||||
await push.restoreMode();
|
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<PushHealth>((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<bool>((ref) async {
|
||||||
|
final storage = ref.watch(storageProvider);
|
||||||
|
return await storage.getSetting<bool>('push_hint_dismissed') ?? false;
|
||||||
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user