implemented dual Nextcloud push registration with separate general and talk apptypes to ensure reliable Talk notification delivery; introduced stacked MessagingStyle notifications for chat threads with support for circular conversation avatars and disk caching
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../api/errors/error_mapper.dart';
|
||||
import '../../../../api/marianumconnect/queries/push_device_test/push_device_test.dart';
|
||||
import '../../../../extensions/date_time.dart';
|
||||
import '../../../../push/push_registration.dart';
|
||||
import '../../../../push/push_status.dart';
|
||||
import '../../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../../widget/app_progress_indicator.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
|
||||
/// Opens the push status checklist: one row per link in the push chain with
|
||||
/// an explanation where it is broken, the last registration attempt (incl.
|
||||
/// verbatim error), a manual re-register action and — once the chain is
|
||||
/// operational — a test notification. Loads once on open; the refresh action
|
||||
/// re-collects on demand (no polling).
|
||||
void showPushStatusSheet(BuildContext context) {
|
||||
// Captured here: the sheet outlives this build context's element tree.
|
||||
final settings = context.read<SettingsCubit>();
|
||||
final capabilities = context.read<CapabilitiesCubit>();
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
header: const ListTile(
|
||||
leading: Icon(Icons.monitor_heart_outlined),
|
||||
title: Text('Status der Push-Benachrichtigungen'),
|
||||
),
|
||||
children: (sheetCtx) => [
|
||||
_PushStatusBody(settings: settings, capabilities: capabilities),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _PushStatusBody extends StatefulWidget {
|
||||
final SettingsCubit settings;
|
||||
final CapabilitiesCubit capabilities;
|
||||
|
||||
const _PushStatusBody({required this.settings, required this.capabilities});
|
||||
|
||||
@override
|
||||
State<_PushStatusBody> createState() => _PushStatusBodyState();
|
||||
}
|
||||
|
||||
class _PushStatusBodyState extends State<_PushStatusBody> {
|
||||
PushStatusReport? _report;
|
||||
bool _busy = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final capabilitiesState = widget.capabilities.state;
|
||||
final report = await collectPushStatus(
|
||||
settingEnabled: widget.settings.val().notificationSettings.enabled,
|
||||
capabilityPush: capabilitiesState.pushNotifications,
|
||||
capabilitiesLoaded: capabilitiesState.loaded,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() => _report = report);
|
||||
}
|
||||
|
||||
Future<void> _reRegister() async {
|
||||
if (_busy) return;
|
||||
setState(() => _busy = true);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final ok = await PushRegistration().register();
|
||||
if (!mounted) return;
|
||||
setState(() => _busy = false);
|
||||
await _load();
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
ok
|
||||
? 'Registrierung erfolgreich abgeschlossen'
|
||||
: 'Registrierung fehlgeschlagen — Details in der Statusübersicht',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _sendTest() async {
|
||||
if (_busy) return;
|
||||
setState(() => _busy = true);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
String message;
|
||||
try {
|
||||
final devices = await PushDeviceTest().run();
|
||||
message = switch (devices) {
|
||||
0 => 'Es ist kein Gerät registriert — bitte erneut registrieren',
|
||||
1 => 'Testbenachrichtigung an 1 Gerät gesendet',
|
||||
_ => 'Testbenachrichtigung an $devices Geräte gesendet',
|
||||
};
|
||||
} on Object catch (e) {
|
||||
message = errorToUserMessage(e);
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() => _busy = false);
|
||||
messenger.showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final report = _report;
|
||||
if (report == null) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Center(child: AppProgressIndicator.medium()),
|
||||
);
|
||||
}
|
||||
final theme = Theme.of(context);
|
||||
final rows = buildPushStatusRows(report);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
...rows.map(
|
||||
(row) => ListTile(
|
||||
dense: true,
|
||||
leading: _stateIcon(row.state, theme),
|
||||
title: Text(row.label),
|
||||
subtitle: row.detail == null ? null : Text(row.detail!),
|
||||
),
|
||||
),
|
||||
if (report.general.lastRegistrationAt != null ||
|
||||
report.talk.lastRegistrationAt != null)
|
||||
const Divider(height: 1),
|
||||
..._lastAttempt(theme, 'Allgemein', report.general),
|
||||
..._lastAttempt(theme, 'Talk', report.talk),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: _actions(report),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Last-attempt line (+ verbatim error) for one registration type.
|
||||
List<Widget> _lastAttempt(
|
||||
ThemeData theme,
|
||||
String label,
|
||||
PushTypeStatus status,
|
||||
) {
|
||||
final at = status.lastRegistrationAt;
|
||||
if (at == null) return const [];
|
||||
final error = status.lastRegistrationError;
|
||||
return [
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: Icon(
|
||||
error == null ? Icons.history : Icons.error_outline,
|
||||
color: error == null ? null : theme.colorScheme.error,
|
||||
),
|
||||
title: Text(
|
||||
'Letzte Registrierung ($label): ${at.formatDateTime()} — '
|
||||
'${error == null ? 'erfolgreich' : 'fehlgeschlagen'}',
|
||||
),
|
||||
),
|
||||
if (error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
error,
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Action hierarchy: refresh stays a secondary icon action; the primary
|
||||
/// (filled) button is the test notification once the chain is operational,
|
||||
/// otherwise re-registering IS the primary next step and the test action is
|
||||
/// omitted (it could not succeed and the checklist explains why).
|
||||
Widget _actions(PushStatusReport report) {
|
||||
const spinner = SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
);
|
||||
final ready = report.readyForTestNotification;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _busy ? null : _load,
|
||||
tooltip: 'Aktualisieren',
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
// OverflowBar stacks the buttons vertically on narrow screens instead
|
||||
// of overflowing the row.
|
||||
Expanded(
|
||||
child: OverflowBar(
|
||||
alignment: MainAxisAlignment.end,
|
||||
overflowAlignment: OverflowBarAlignment.end,
|
||||
spacing: 8,
|
||||
overflowSpacing: 4,
|
||||
children: [
|
||||
if (ready) ...[
|
||||
TextButton(
|
||||
onPressed: _busy ? null : _reRegister,
|
||||
child: const Text('Erneut registrieren'),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: _busy ? null : _sendTest,
|
||||
icon: _busy ? spinner : const Icon(Icons.send_outlined),
|
||||
label: const Text('Testbenachrichtigung'),
|
||||
),
|
||||
] else
|
||||
FilledButton.icon(
|
||||
onPressed: _busy ? null : _reRegister,
|
||||
icon: _busy ? spinner : const Icon(Icons.sync),
|
||||
label: const Text('Erneut registrieren'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _stateIcon(PushCheck state, ThemeData theme) {
|
||||
switch (state) {
|
||||
case PushCheck.ok:
|
||||
return const Icon(Icons.check_circle_outline, color: Colors.green);
|
||||
case PushCheck.fail:
|
||||
return Icon(Icons.cancel_outlined, color: theme.colorScheme.error);
|
||||
case PushCheck.unknown:
|
||||
return Icon(
|
||||
Icons.remove_circle_outline,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user