Use status codes to decide activity search behavior (#3718)
* Add status codes to activity search * Fix strange request error handling * Make search react appropriately to 400+ status codes * Small readability fixes * Use enum for activity search status * Fix switched logic in hideCards
This commit is contained in:
parent
a43c5ffac5
commit
7ca87a9179
4 changed files with 168 additions and 90 deletions
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
|
|
@ -14,22 +16,27 @@ class ActivitySearchRepo {
|
|||
static final GetStorage _activityPlanStorage =
|
||||
GetStorage('activity_plan_search_storage');
|
||||
|
||||
static void set(ActivityPlanRequest request, ActivityPlanResponse response) {
|
||||
_activityPlanStorage.write(request.storageKey, response.toJson());
|
||||
static void set(
|
||||
String storageKey,
|
||||
ActivityPlanResponseWrapper wrappedResponse,
|
||||
) {
|
||||
_activityPlanStorage.write(storageKey, wrappedResponse.toJson());
|
||||
}
|
||||
|
||||
static Future<ActivityPlanResponse> get(ActivityPlanRequest request) async {
|
||||
final cachedJson = _activityPlanStorage.read(request.storageKey);
|
||||
if (cachedJson != null &&
|
||||
(cachedJson['activity_plans'] as List).isNotEmpty) {
|
||||
ActivityPlanResponse? cached;
|
||||
static Future<ActivityPlanResponseWrapper> get(
|
||||
ActivityPlanRequest request,
|
||||
) async {
|
||||
final storageKey = "${request.storageKey}_wrapper";
|
||||
final cachedJson = _activityPlanStorage.read(storageKey);
|
||||
if (cachedJson != null) {
|
||||
ActivityPlanResponseWrapper? cached;
|
||||
try {
|
||||
cached = ActivityPlanResponse.fromJson(cachedJson);
|
||||
cached = ActivityPlanResponseWrapper.fromJson(cachedJson);
|
||||
} catch (e) {
|
||||
_activityPlanStorage.remove(request.storageKey);
|
||||
_activityPlanStorage.remove(storageKey);
|
||||
}
|
||||
|
||||
if (cached != null) {
|
||||
if (cached is ActivityPlanResponseWrapper) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
|
@ -39,16 +46,57 @@ class ActivitySearchRepo {
|
|||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.activityPlanSearch,
|
||||
body: request.toJson(),
|
||||
Response? res;
|
||||
try {
|
||||
res = await req.post(
|
||||
url: PApiUrls.activityPlanSearch,
|
||||
body: request.toJson(),
|
||||
);
|
||||
} catch (err) {
|
||||
debugPrint("err: $err, err is http response: ${err is Response}");
|
||||
if (err is Response) {
|
||||
return ActivityPlanResponseWrapper(
|
||||
response: ActivityPlanResponse(activityPlans: []),
|
||||
statusCode: err.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res!.bodyBytes));
|
||||
final response = ActivityPlanResponse.fromJson(decodedBody);
|
||||
final wrappedResponse = ActivityPlanResponseWrapper(
|
||||
response: response,
|
||||
statusCode: res.statusCode,
|
||||
);
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
final response = ActivityPlanResponse.fromJson(decodedBody);
|
||||
if (res.statusCode == 200) {
|
||||
set(storageKey, wrappedResponse);
|
||||
}
|
||||
|
||||
set(request, response);
|
||||
|
||||
return response;
|
||||
return wrappedResponse;
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityPlanResponseWrapper {
|
||||
final ActivityPlanResponse response;
|
||||
final int statusCode;
|
||||
|
||||
ActivityPlanResponseWrapper({
|
||||
required this.response,
|
||||
required this.statusCode,
|
||||
});
|
||||
|
||||
factory ActivityPlanResponseWrapper.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityPlanResponseWrapper(
|
||||
response: json['activity_plan_response'].fromJson,
|
||||
statusCode: json['activity_response_status'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'activity_plan_response': response.toJson(),
|
||||
'activity_response_status': statusCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
52
lib/pangea/activity_suggestions/activity_search_enum.dart
Normal file
52
lib/pangea/activity_suggestions/activity_search_enum.dart
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
|
||||
/// 200: All activities successfully retrieved
|
||||
/// 202: Waiting for activities to load
|
||||
/// 504: Timeout
|
||||
/// Other: Error
|
||||
enum ActivitySearchEnum {
|
||||
complete,
|
||||
waiting,
|
||||
timeout,
|
||||
error,
|
||||
}
|
||||
|
||||
extension ActivitySearchExtension on ActivitySearchEnum {
|
||||
ActivitySearchEnum fromCode(int statusCode) {
|
||||
switch (statusCode) {
|
||||
case 200:
|
||||
return ActivitySearchEnum.complete;
|
||||
case 202:
|
||||
return ActivitySearchEnum.waiting;
|
||||
case 504:
|
||||
return ActivitySearchEnum.timeout;
|
||||
default:
|
||||
return ActivitySearchEnum.error;
|
||||
}
|
||||
}
|
||||
|
||||
bool get hideCards {
|
||||
switch (this) {
|
||||
case ActivitySearchEnum.complete:
|
||||
case ActivitySearchEnum.waiting:
|
||||
return false;
|
||||
case ActivitySearchEnum.timeout:
|
||||
case ActivitySearchEnum.error:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
String message(L10n l10n) {
|
||||
switch (this) {
|
||||
case ActivitySearchEnum.waiting:
|
||||
l10n.activitySuggestionTimeoutMessage;
|
||||
case ActivitySearchEnum.timeout:
|
||||
return l10n.generatingNewActivities;
|
||||
case ActivitySearchEnum.error:
|
||||
return l10n.errorFetchingActivitiesMessage;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
@ -15,9 +15,9 @@ import 'package:fluffychat/l10n/l10n.dart';
|
|||
import 'package:fluffychat/pangea/activity_generator/media_enum.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_response.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_plan_search_repo.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_search_enum.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
|
@ -65,9 +65,8 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
|
||||
// _loading is true when _setActivityItems is currently requesting activities
|
||||
bool _loading = true;
|
||||
// _timeout is true if 1+ round of _setActivityItems
|
||||
// has occurred and no activities retrieved
|
||||
bool _timeout = false;
|
||||
ActivitySearchEnum _status = ActivitySearchEnum.waiting;
|
||||
|
||||
bool get _isColumnMode => FluffyThemes.isColumnMode(context);
|
||||
|
||||
final List<ActivityPlanModel> _activityItems = [];
|
||||
|
|
@ -100,7 +99,7 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
if (retries > 3) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_timeout = true;
|
||||
_status = ActivitySearchEnum.timeout;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
|
@ -108,54 +107,46 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
}
|
||||
|
||||
try {
|
||||
if (retries == 0) {
|
||||
if (retries == 0 && mounted) {
|
||||
setState(() {
|
||||
_activityItems.clear();
|
||||
_loading = true;
|
||||
_timeout = false;
|
||||
_status = ActivitySearchEnum.waiting;
|
||||
});
|
||||
}
|
||||
|
||||
final resp = await ActivitySearchRepo.get(_request).timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_timeout = true;
|
||||
});
|
||||
}
|
||||
if (mounted) setState(() => _status = ActivitySearchEnum.timeout);
|
||||
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
if (mounted) _setActivityItems(retries: retries + 1);
|
||||
});
|
||||
|
||||
return Future<ActivityPlanResponse>.error(
|
||||
return Future<ActivityPlanResponseWrapper>.error(
|
||||
TimeoutException(
|
||||
L10n.of(context).activitySuggestionTimeoutMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
_activityItems.addAll(resp.activityPlans);
|
||||
_activityItems.addAll(resp.response.activityPlans);
|
||||
_status = _status.fromCode(resp.statusCode);
|
||||
if (_status != ActivitySearchEnum.error) {
|
||||
if (_activityItems.isEmpty) {
|
||||
if (mounted) setState(() => _status = ActivitySearchEnum.timeout);
|
||||
|
||||
// If activities are not successfully retrieved, try again
|
||||
if (_activityItems.isEmpty) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_timeout = true;
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
if (mounted) _setActivityItems(retries: retries + 1);
|
||||
});
|
||||
} else {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
if (mounted) _setActivityItems(retries: retries + 1);
|
||||
});
|
||||
|
||||
throw TimeoutException(
|
||||
L10n.of(context).activitySuggestionTimeoutMessage,
|
||||
);
|
||||
}
|
||||
} on TimeoutException {
|
||||
rethrow;
|
||||
} catch (e, s) {
|
||||
if (e is! TimeoutException) rethrow;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
|
|
@ -165,16 +156,6 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
},
|
||||
level: SentryLevel.warning,
|
||||
);
|
||||
} finally {
|
||||
// If activities are successfully retrieved, set timeout and loading to false
|
||||
if (mounted && _activityItems.isNotEmpty) {
|
||||
setState(
|
||||
() {
|
||||
_loading = false;
|
||||
_timeout = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +167,8 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final List<Widget> cards = _loading
|
||||
// Show all loaded activities, or a loading view if there are none
|
||||
final List<Widget> cards = _activityItems.isEmpty
|
||||
? List.generate(5, (i) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: theme.colorScheme.primary.withAlpha(20),
|
||||
|
|
@ -243,7 +225,7 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
children: [
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: _timeout
|
||||
child: _status.hideCards
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
|
|
@ -253,12 +235,13 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
child: Text(
|
||||
L10n.of(context).generatingNewActivities,
|
||||
_status.message(L10n.of(context)),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
if (_loading) const CircularProgressIndicator(),
|
||||
if (!_loading)
|
||||
if (_loading && _status == ActivitySearchEnum.timeout)
|
||||
const CircularProgressIndicator(),
|
||||
if (!_loading && _status == ActivitySearchEnum.timeout)
|
||||
ElevatedButton(
|
||||
onPressed: _setActivityItems,
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
|
@ -307,8 +290,7 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
Padding(
|
||||
padding: const EdgeInsetsGeometry.all(16.0),
|
||||
child: ErrorIndicator(
|
||||
message: L10n.of(context)
|
||||
.activitySuggestionTimeoutMessage,
|
||||
message: _status.message(L10n.of(context)),
|
||||
),
|
||||
),
|
||||
if (cards.length < 5 && _loading)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,26 @@ class Requests {
|
|||
Uri _uriBuilder(url) =>
|
||||
baseUrl != null ? Uri.parse(baseUrl! + url) : Uri.parse(url);
|
||||
|
||||
void addBreadcrumb(
|
||||
http.Response response, {
|
||||
Map<dynamic, dynamic>? body,
|
||||
String? objectId,
|
||||
}) {
|
||||
debugPrint("Error - code: ${response.statusCode}");
|
||||
debugPrint("api: ${response.request?.url}");
|
||||
debugPrint("request body: ${body ?? objectId}");
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb.http(
|
||||
url: response.request?.url ?? Uri(path: "not available"),
|
||||
method: response.request?.method ?? "not available",
|
||||
statusCode: response.statusCode,
|
||||
),
|
||||
);
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(data: {"body": body, "objectId": objectId}),
|
||||
);
|
||||
}
|
||||
|
||||
void handleError(
|
||||
http.Response response, {
|
||||
Map<dynamic, dynamic>? body,
|
||||
|
|
@ -86,33 +106,9 @@ class Requests {
|
|||
//PTODO - handle 401 error - unauthorized call
|
||||
//kick them back to login?
|
||||
|
||||
addBreadcrumb() {
|
||||
debugPrint("Error - code: ${response.statusCode}");
|
||||
debugPrint("api: ${response.request?.url}");
|
||||
debugPrint("request body: ${body ?? objectId}");
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb.http(
|
||||
url: response.request?.url ?? Uri(path: "not available"),
|
||||
method: response.request?.method ?? "not available",
|
||||
statusCode: response.statusCode,
|
||||
),
|
||||
);
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(data: {"body": body, "objectId": objectId}),
|
||||
);
|
||||
}
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
case 201:
|
||||
break;
|
||||
case 502:
|
||||
case 504:
|
||||
addBreadcrumb();
|
||||
throw response;
|
||||
default:
|
||||
addBreadcrumb();
|
||||
throw response;
|
||||
if (response.statusCode >= 400) {
|
||||
addBreadcrumb(response, body: body, objectId: objectId);
|
||||
throw response;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue