diff --git a/lib/pangea/activity_suggestions/activity_plan_search_repo.dart b/lib/pangea/activity_suggestions/activity_plan_search_repo.dart index 36061347d..fdd7dcbf7 100644 --- a/lib/pangea/activity_suggestions/activity_plan_search_repo.dart +++ b/lib/pangea/activity_suggestions/activity_plan_search_repo.dart @@ -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 get(ActivityPlanRequest request) async { - final cachedJson = _activityPlanStorage.read(request.storageKey); - if (cachedJson != null && - (cachedJson['activity_plans'] as List).isNotEmpty) { - ActivityPlanResponse? cached; + static Future 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 json) { + return ActivityPlanResponseWrapper( + response: json['activity_plan_response'].fromJson, + statusCode: json['activity_response_status'], + ); + } + + Map toJson() { + return { + 'activity_plan_response': response.toJson(), + 'activity_response_status': statusCode, + }; } } diff --git a/lib/pangea/activity_suggestions/activity_search_enum.dart b/lib/pangea/activity_suggestions/activity_search_enum.dart new file mode 100644 index 000000000..35adb9cae --- /dev/null +++ b/lib/pangea/activity_suggestions/activity_search_enum.dart @@ -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 ''; + } +} diff --git a/lib/pangea/activity_suggestions/activity_suggestions_area.dart b/lib/pangea/activity_suggestions/activity_suggestions_area.dart index 9064b765b..e7a537592 100644 --- a/lib/pangea/activity_suggestions/activity_suggestions_area.dart +++ b/lib/pangea/activity_suggestions/activity_suggestions_area.dart @@ -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 { // _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 _activityItems = []; @@ -100,7 +99,7 @@ class ActivitySuggestionsAreaState extends State { if (retries > 3) { if (mounted) { setState(() { - _timeout = true; + _status = ActivitySearchEnum.timeout; _loading = false; }); } @@ -108,54 +107,46 @@ class ActivitySuggestionsAreaState extends State { } 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.error( + return Future.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 { }, 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 { Widget build(BuildContext context) { final theme = Theme.of(context); - final List cards = _loading + // Show all loaded activities, or a loading view if there are none + final List cards = _activityItems.isEmpty ? List.generate(5, (i) { return Shimmer.fromColors( baseColor: theme.colorScheme.primary.withAlpha(20), @@ -243,7 +225,7 @@ class ActivitySuggestionsAreaState extends State { 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 { 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 { 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) diff --git a/lib/pangea/common/network/requests.dart b/lib/pangea/common/network/requests.dart index 444b7fa9a..8da609734 100644 --- a/lib/pangea/common/network/requests.dart +++ b/lib/pangea/common/network/requests.dart @@ -78,6 +78,26 @@ class Requests { Uri _uriBuilder(url) => baseUrl != null ? Uri.parse(baseUrl! + url) : Uri.parse(url); + void addBreadcrumb( + http.Response response, { + Map? 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? 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; } }