import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../push/push_registration.dart'; import '../../../../push/push_status.dart'; import '../../../../routing/app_routes.dart'; import '../../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../../../widget/centered_leading.dart'; import '../widgets/push_status_sheet.dart'; import '../widgets/settings_checkbox_tile.dart'; class TalkSection extends StatelessWidget { const TalkSection({super.key}); @override Widget build(BuildContext context) { final settings = context.watch(); final talkSettings = settings.val().talkSettings; return Column( children: [ SettingsCheckboxTile( icon: Icons.star_border, title: 'Favoriten im Talk nach oben sortieren', value: talkSettings.sortFavoritesToTop, onChanged: (e) => settings.val(write: true).talkSettings.sortFavoritesToTop = e, ), SettingsCheckboxTile( icon: Icons.mark_email_unread_outlined, title: 'Ungelesene Chats nach oben sortieren', value: talkSettings.sortUnreadToTop, onChanged: (e) => settings.val(write: true).talkSettings.sortUnreadToTop = e, ), ListTile( leading: const Icon(Icons.wallpaper_outlined), title: const Text('Chat-Hintergrund'), subtitle: const Text('Bild, Farbe und Darstellung anpassen'), trailing: const Icon(Icons.arrow_right), onTap: () => AppRoutes.openChatBackgroundSettings(context), ), _PushSettings( settings: settings, capabilities: context.read(), enabled: settings.val().notificationSettings.enabled, devMode: settings.val().devToolsEnabled, ), ], ); } } /// The push area: the enable switch carries an at-a-glance health icon (green /// check / red X) right before the checkbox, and the detailed status checklist /// is hidden — it only surfaces when the chain is broken or the developer mode /// is on. Owns the [PushStatusReport] loading so the inline icon and the detail /// entry share one source of truth. class _PushSettings extends StatefulWidget { final SettingsCubit settings; final CapabilitiesCubit capabilities; final bool enabled; final bool devMode; const _PushSettings({ required this.settings, required this.capabilities, required this.enabled, required this.devMode, }); @override State<_PushSettings> createState() => _PushSettingsState(); } class _PushSettingsState extends State<_PushSettings> with WidgetsBindingObserver { PushStatusReport? _report; /// True while a (de)registration triggered by the switch is in flight. The /// report collected in that window still reflects the pre-registration state, /// so the status is shown as "loading" instead of briefly flashing red. bool _busy = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); unawaited(_load()); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didUpdateWidget(covariant _PushSettings oldWidget) { super.didUpdateWidget(oldWidget); // Toggling the setting changes several links at once — re-collect. if (oldWidget.enabled != widget.enabled) unawaited(_load()); } @override void didChangeAppLifecycleState(AppLifecycleState state) { // The OS permission can change while the app is backgrounded. if (state == AppLifecycleState.resumed) unawaited(_load()); } Future _load() async { final caps = widget.capabilities.state; final report = await collectPushStatus( settingEnabled: widget.settings.val().notificationSettings.enabled, capabilityPush: caps.pushNotifications, capabilitiesLoaded: caps.loaded, ); if (!mounted) return; setState(() => _report = report); } void _onToggle(bool enabled) { widget.settings.val(write: true).notificationSettings.enabled = enabled; // Turning off does NOT unregister: the device stays subscribed so silent // sync pushes keep arriving; the message handler and iOS NSE suppress only // the visible notification (via the mirrored flag). Enabling (re-)registers // and ensures the OS permission. if (!enabled) return; setState(() => _busy = true); final messenger = ScaffoldMessenger.of(context); unawaited(() async { try { // Only register when the OS permission isn't explicitly denied — // otherwise NC + proxy would push into the void. if (await PushRegistration.requestOsPermission()) { await PushRegistration().register(); } else { messenger.showSnackBar( const SnackBar( content: Text( 'Die Benachrichtigungsberechtigung wurde in den ' 'Systemeinstellungen deaktiviert. Bitte aktiviere sie dort, um ' 'Push-Benachrichtigungen zu erhalten.', ), ), ); } } finally { if (mounted) await _load(); if (mounted) setState(() => _busy = false); } }()); } @override Widget build(BuildContext context) { final report = _report; final broken = widget.enabled && !_busy && report != null && !report.chainHealthy; return Column( children: [ SettingsCheckboxTile( icon: Icons.notifications_active_outlined, title: 'Push-Benachrichtigungen', subtitle: 'Benachrichtigungen bei neuen Talk-Nachrichten erhalten', value: widget.enabled, beforeCheckbox: _inlineStatusIcon(report), onChanged: _onToggle, ), // Detail entry only when there is a problem to fix or for developers. if (broken || widget.devMode) _detailTile(error: broken), ], ); } /// Health icon shown before the checkbox — a spinner while a registration is /// in flight, otherwise the green/red verdict once the report has loaded. Widget? _inlineStatusIcon(PushStatusReport? report) { if (_busy) { return const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ); } if (!widget.enabled || report == null) return null; final healthy = report.chainHealthy; return Icon( healthy ? Icons.check_circle : Icons.cancel, color: healthy ? Colors.green : Theme.of(context).colorScheme.error, semanticLabel: healthy ? 'Push in Ordnung' : 'Push funktioniert nicht', ); } /// The full status checklist entry — same list-tile footprint whether broken /// or not; a problem is signalled only through the error-colored icon/text. Widget _detailTile({required bool error}) { final color = error ? Theme.of(context).colorScheme.error : null; final textStyle = color == null ? null : TextStyle(color: color); return ListTile( leading: CenteredLeading( Icon(Icons.monitor_heart_outlined, color: color), ), title: Text('Push-Status', style: textStyle), subtitle: Text( error ? 'Ein Schritt in der Zustellkette ist unterbrochen' : 'Registrierung und Zustellung im Detail', style: textStyle, ), trailing: Icon(Icons.arrow_right, color: color), // The sheet can re-register; re-collect on close so the dot reflects it. onTap: () async { await showPushStatusSheet(context); if (mounted) await _load(); }, ); } }