Files
Client/lib/view/pages/settings/sections/talk_section.dart
T

222 lines
7.7 KiB
Dart

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<SettingsCubit>();
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<CapabilitiesCubit>(),
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<void> _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();
},
);
}
}