Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
@@ -12,8 +12,9 @@ import 'telemetry_device_id.dart';
|
|||||||
|
|
||||||
/// Sends a telemetry heartbeat to MarianumConnect (`POST me/telemetry`) —
|
/// Sends a telemetry heartbeat to MarianumConnect (`POST me/telemetry`) —
|
||||||
/// upserts the stable install id, platform, app version and device info. Sent
|
/// upserts the stable install id, platform, app version and device info. Sent
|
||||||
/// once on app start and again once push registration completes that session
|
/// on app start, again once push registration completes that session (so a
|
||||||
/// (so a fresh registration isn't under-reported until the next launch).
|
/// fresh registration isn't under-reported until the next launch), and on
|
||||||
|
/// resume after the app spent more than 15 minutes in the background.
|
||||||
/// Bearer-authenticated via the shared dio interceptor. Replaces the legacy
|
/// Bearer-authenticated via the shared dio interceptor. Replaces the legacy
|
||||||
/// mhsl.eu `server/userIndex/update` call.
|
/// mhsl.eu `server/userIndex/update` call.
|
||||||
class TelemetryHeartbeat extends MarianumConnectQuery {
|
class TelemetryHeartbeat extends MarianumConnectQuery {
|
||||||
@@ -21,7 +22,8 @@ class TelemetryHeartbeat extends MarianumConnectQuery {
|
|||||||
|
|
||||||
/// Fire-and-forget: schedules a heartbeat and swallows any error, so a failed
|
/// Fire-and-forget: schedules a heartbeat and swallows any error, so a failed
|
||||||
/// send never disrupts app start. Used from the app shell's initState and
|
/// send never disrupts app start. Used from the app shell's initState and
|
||||||
/// re-emitted once push registration completes (see `_MainState._syncPush`).
|
/// lifecycle handler, and re-emitted once push registration completes (see
|
||||||
|
/// `_MainState._syncPush`).
|
||||||
static void report({required bool notificationsEnabled}) {
|
static void report({required bool notificationsEnabled}) {
|
||||||
unawaited(
|
unawaited(
|
||||||
TelemetryHeartbeat()
|
TelemetryHeartbeat()
|
||||||
|
|||||||
+22
-4
@@ -46,9 +46,11 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||||||
int _knownTotalTabs = 1;
|
int _knownTotalTabs = 1;
|
||||||
int _lastTabIndex = 0;
|
int _lastTabIndex = 0;
|
||||||
bool _userOnLastTab = false;
|
bool _userOnLastTab = false;
|
||||||
|
DateTime? _lastTelemetryAt;
|
||||||
|
|
||||||
static const Duration _chatListActiveInterval = Duration(seconds: 15);
|
static const Duration _chatListActiveInterval = Duration(seconds: 15);
|
||||||
static const Duration _chatListIdleInterval = Duration(seconds: 60);
|
static const Duration _chatListIdleInterval = Duration(seconds: 60);
|
||||||
|
static const Duration _telemetryInterval = Duration(minutes: 15);
|
||||||
|
|
||||||
void _onTabControllerChanged() {
|
void _onTabControllerChanged() {
|
||||||
final newIndex = Main.bottomNavigator.index;
|
final newIndex = Main.bottomNavigator.index;
|
||||||
@@ -73,10 +75,29 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||||||
if (talkIsActive) bloc.refresh();
|
if (talkIsActive) bloc.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wall-clock throttle rather than Debouncer.throttle: a Timer does not tick
|
||||||
|
// reliably while the app is suspended, so the window would still be open on
|
||||||
|
// the resume it is supposed to let through.
|
||||||
|
void _reportTelemetry() {
|
||||||
|
if (!mounted) return;
|
||||||
|
final now = DateTime.now();
|
||||||
|
final last = _lastTelemetryAt;
|
||||||
|
if (last != null && now.difference(last) < _telemetryInterval) return;
|
||||||
|
_lastTelemetryAt = now;
|
||||||
|
TelemetryHeartbeat.report(
|
||||||
|
notificationsEnabled: context
|
||||||
|
.read<SettingsCubit>()
|
||||||
|
.val()
|
||||||
|
.notificationSettings
|
||||||
|
.enabled,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
log('AppLifecycle: $state');
|
log('AppLifecycle: $state');
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
_reportTelemetry();
|
||||||
Debouncer.throttle('appLifecycleState', const Duration(seconds: 10), () {
|
Debouncer.throttle('appLifecycleState', const Duration(seconds: 10), () {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
log('Refreshing due to LifecycleChange');
|
log('Refreshing due to LifecycleChange');
|
||||||
@@ -178,10 +199,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
});
|
});
|
||||||
|
|
||||||
TelemetryHeartbeat.report(
|
_reportTelemetry();
|
||||||
notificationsEnabled:
|
|
||||||
context.read<SettingsCubit>().val().notificationSettings.enabled,
|
|
||||||
);
|
|
||||||
|
|
||||||
// A refreshed FCM token invalidates the existing push subscription — the
|
// A refreshed FCM token invalidates the existing push subscription — the
|
||||||
// NC device identifier stays stable, so we simply re-register (NC first,
|
// NC device identifier stays stable, so we simply re-register (NC first,
|
||||||
|
|||||||
@@ -173,7 +173,12 @@ class _AbsenceReportViewState extends State<AbsenceReportView> {
|
|||||||
icon: Icons.error_outline,
|
icon: Icons.error_outline,
|
||||||
text: errorToUserMessage(snapshot.error),
|
text: errorToUserMessage(snapshot.error),
|
||||||
button: ElevatedButton.icon(
|
button: ElevatedButton.icon(
|
||||||
onPressed: () => setState(() => _init = _load()),
|
onPressed: () {
|
||||||
|
final reload = _load();
|
||||||
|
setState(() {
|
||||||
|
_init = reload;
|
||||||
|
});
|
||||||
|
},
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
label: const Text('Erneut versuchen'),
|
label: const Text('Erneut versuchen'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -230,11 +230,15 @@ class _UserAvatarState extends State<UserAvatar> {
|
|||||||
|
|
||||||
final pending = _pendingAvatars.putIfAbsent(url, () {
|
final pending = _pendingAvatars.putIfAbsent(url, () {
|
||||||
final future = _fetch(url);
|
final future = _fetch(url);
|
||||||
future.whenComplete(() {
|
// Cleanup hangs off an error-neutralised copy: whenComplete on `future`
|
||||||
|
// itself returns a second future that forwards the error unawaited.
|
||||||
|
unawaited(
|
||||||
|
future.then<void>((_) {}, onError: (_) {}).whenComplete(() {
|
||||||
if (identical(_pendingAvatars[url], future)) {
|
if (identical(_pendingAvatars[url], future)) {
|
||||||
_pendingAvatars.remove(url);
|
_pendingAvatars.remove(url);
|
||||||
}
|
}
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
return future;
|
return future;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user