fixed stale state after logout and replayed or lost share intents
This commit is contained in:
+45
-19
@@ -1,8 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
|
||||
import '../../../../../api/errors/error_mapper.dart';
|
||||
import '../../../../../api/errors/stale_session_exception.dart';
|
||||
import '../../../../../model/account_data.dart';
|
||||
import '../../loadable_state/loadable_state.dart';
|
||||
import '../../loadable_state/loading_error.dart';
|
||||
import '../../repository/repository.dart';
|
||||
@@ -114,6 +117,23 @@ abstract class LoadableHydratedBloc<
|
||||
add(Reset<TState>());
|
||||
}
|
||||
|
||||
static const _sessionKey = #loadableSessionEpoch;
|
||||
|
||||
/// Runs [body] tagged with the current session: events it adds, also from
|
||||
/// its async continuations, are dropped once the account signed out, so a
|
||||
/// late response of the previous account cannot refill the reset bloc.
|
||||
R runInSession<R>(R Function() body) => runZoned(
|
||||
body,
|
||||
zoneValues: {_sessionKey: AccountData().sessionEpoch},
|
||||
);
|
||||
|
||||
@override
|
||||
void add(LoadableHydratedBlocEvent<TState> event) {
|
||||
final epoch = Zone.current[_sessionKey];
|
||||
if (epoch is int && !AccountData().isCurrentSession(epoch)) return;
|
||||
super.add(event);
|
||||
}
|
||||
|
||||
TState? get innerState => state.data;
|
||||
TRepository get repo => _repository;
|
||||
|
||||
@@ -126,29 +146,35 @@ abstract class LoadableHydratedBloc<
|
||||
/// Maps [e] through the shared error mapper and emits it as an [Error] event.
|
||||
/// Does not guard [isClosed] — callers decide whether a late error still
|
||||
/// applies.
|
||||
void addLoadingError(Object e) => add(
|
||||
Error(
|
||||
LoadingError(
|
||||
message: errorToUserMessage(e),
|
||||
technicalDetails: errorToTechnicalDetails(e),
|
||||
allowRetry: errorAllowsRetry(e),
|
||||
void addLoadingError(Object e) {
|
||||
// Belongs to a signed-out account; the current one loads on its own.
|
||||
if (e is StaleSessionException) return;
|
||||
add(
|
||||
Error(
|
||||
LoadingError(
|
||||
message: errorToUserMessage(e),
|
||||
technicalDetails: errorToTechnicalDetails(e),
|
||||
allowRetry: errorAllowsRetry(e),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
void fetch() {
|
||||
log('Fetching data for ${TState.toString()}');
|
||||
gatherData()
|
||||
.catchError((Object e) {
|
||||
log('Error while fetching ${TState.toString()}: ${e.toString()}');
|
||||
// The bloc may have been closed before this async error landed;
|
||||
// adding to a closed bloc throws, so swallow that case.
|
||||
if (isClosed) return;
|
||||
addLoadingError(e);
|
||||
})
|
||||
.then((value) {
|
||||
log('Fetch for ${TState.toString()} completed!');
|
||||
});
|
||||
runInSession(
|
||||
() => gatherData()
|
||||
.catchError((Object e) {
|
||||
log('Error while fetching ${TState.toString()}: ${e.toString()}');
|
||||
// The bloc may have been closed before this async error landed;
|
||||
// adding to a closed bloc throws, so swallow that case.
|
||||
if (isClosed) return;
|
||||
addLoadingError(e);
|
||||
})
|
||||
.then((value) {
|
||||
log('Fetch for ${TState.toString()} completed!');
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import '../../../../../api/demo/data/demo_capabilities.dart';
|
||||
import '../../../../../api/demo/demo_mode.dart';
|
||||
import '../../../../../api/marianumconnect/queries/get_capabilities/get_capabilities.dart';
|
||||
import '../../../../../model/account_data.dart';
|
||||
import 'capabilities_state.dart';
|
||||
|
||||
/// Holds the current user's mobile capability flags. Hydrated so the last
|
||||
@@ -34,8 +35,12 @@ class CapabilitiesCubit extends HydratedCubit<CapabilitiesState> {
|
||||
emit(DemoCapabilities.state());
|
||||
return;
|
||||
}
|
||||
final epoch = AccountData().sessionEpoch;
|
||||
try {
|
||||
final response = await GetCapabilities().run();
|
||||
// A slow answer for the previous account must not decide the modules
|
||||
// of the next one.
|
||||
if (!AccountData().isCurrentSession(epoch)) return;
|
||||
emit(
|
||||
CapabilitiesState(
|
||||
viewForeignTimetables: response.viewForeignTimetables,
|
||||
|
||||
@@ -123,6 +123,15 @@ class ChatBloc
|
||||
/// No-op when the bloc has already moved on to a different token: when
|
||||
/// popping a stacked chat (B over A), A's didPopNext runs setToken(A)
|
||||
/// before B's dispose fires.
|
||||
/// The chat view may still be popping when the sign-out resets this bloc,
|
||||
/// so leaveChat would find no token and the long-poll would keep running.
|
||||
@override
|
||||
Future<void> reset() {
|
||||
_chatViewActive = false;
|
||||
_stopLongPoll();
|
||||
return super.reset();
|
||||
}
|
||||
|
||||
void leaveChat(String fromToken) {
|
||||
if ((innerState?.currentToken ?? '') != fromToken) return;
|
||||
_chatViewActive = false;
|
||||
|
||||
@@ -30,6 +30,13 @@ class ChatListBloc
|
||||
return super.close();
|
||||
}
|
||||
|
||||
// The timer outlives the app shell; the next shell re-arms it after login.
|
||||
@override
|
||||
Future<void> reset() {
|
||||
setAutoRefreshInterval(null);
|
||||
return super.reset();
|
||||
}
|
||||
|
||||
/// Silent refresh — explicit pull-to-refresh and tab-activation are non-silent.
|
||||
void setAutoRefreshInterval(Duration? interval) {
|
||||
if (interval == _autoRefreshInterval) return;
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import '../../../../../api/demo/data/demo_capabilities.dart';
|
||||
import '../../../../../api/demo/demo_mode.dart';
|
||||
import '../../../../../api/marianumcloud/capabilities/get_nextcloud_capabilities.dart';
|
||||
import '../../../../../model/account_data.dart';
|
||||
import 'nextcloud_capabilities_state.dart';
|
||||
|
||||
/// Holds the current user's Nextcloud `files_sharing` capabilities. Hydrated so
|
||||
@@ -63,8 +64,12 @@ class NextcloudCapabilitiesCubit
|
||||
emit(DemoNextcloudCapabilities.state());
|
||||
return;
|
||||
}
|
||||
final epoch = AccountData().sessionEpoch;
|
||||
try {
|
||||
final caps = await GetNextcloudCapabilities().run();
|
||||
// A slow answer for the previous account must not decide the modules
|
||||
// of the next one.
|
||||
if (!AccountData().isCurrentSession(epoch)) return;
|
||||
emit(
|
||||
NextcloudCapabilitiesState(
|
||||
apiEnabled: caps.apiEnabled,
|
||||
|
||||
@@ -91,8 +91,10 @@ class TimetableBloc
|
||||
final current = innerState ?? fromNothing();
|
||||
if (current.startDate == startDate && current.endDate == endDate) return;
|
||||
add(Emit((s) => s.copyWith(startDate: startDate, endDate: endDate)));
|
||||
_loadCurrentWeek(startDate, endDate);
|
||||
_prefetchAdjacentWeeks(startDate, endDate);
|
||||
runInSession(() {
|
||||
_loadCurrentWeek(startDate, endDate);
|
||||
_prefetchAdjacentWeeks(startDate, endDate);
|
||||
});
|
||||
}
|
||||
|
||||
void resetWeek() {
|
||||
|
||||
Reference in New Issue
Block a user