Activity loading tweaks (#3636)

* Added activity message when generation is partially successful

* Fix problem with missing intl entries

* Fix error message on timeout, add try again button to partial timeout

* Revert _activityItems.isNotEmpty check on _loading

* Make reversion match previous format

* Fix _setActivityItems failure from empty activity_plans

* Set timeout after first timeout, throw timeout exception for empty activities

* Only show Try again buttons when not currently loading

* fix text align

---------

Co-authored-by: ggurdin <ggurdin@gmail.com>
This commit is contained in:
Kelrap 2025-08-07 15:09:06 -04:00 committed by GitHub
parent fe5a5ce66a
commit d09617aa81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 110 additions and 43 deletions

View file

@ -4999,7 +4999,7 @@
"access": "Access",
"addSubspace": "Add subspace",
"botSettings": "Bot settings",
"activitySuggestionTimeoutMessage": "We are working hard to generate activities for you, please check back in a minute",
"activitySuggestionTimeoutMessage": "We are working hard to generate more activities for you, please check back in a minute",
"accessSettingsWarning": "Oops! It looks like you don't have permission to set the Access rules of this room. You should check these to make sure they're what you need and talk to a room admin if you need to change them",
"howSpaceCanBeFound": "How this space can be found",
"private": "Private",
@ -5156,6 +5156,7 @@
"activitySummaryError": "Activity summaries unavailable",
"requestSummaries": "Request summaries",
"loadingActivitySummary": "Loading activity summary...",
"generatingNewActivities": "You're the first user of this language pair! Please wait a moment, we're preparing activities just for you.",
"requestAccessTitle": "Request to analytics view access?",
"requestAccessDesc": "Would you like to request access to view participant analytics?\n\nIf participants agree, admin of this space will be able to view their:\n • total vocabulary\n • total grammar concepts\n • total activity sessions completed\n • the specific grammar concepts used, correctly and incorrectly\n\n • They will not be able to view their:\n • messages in chats outside the space\n • vocabulary list",
"requestAccess": "Request access ({count})",

View file

@ -20,7 +20,8 @@ class ActivitySearchRepo {
static Future<ActivityPlanResponse> get(ActivityPlanRequest request) async {
final cachedJson = _activityPlanStorage.read(request.storageKey);
if (cachedJson != null) {
if (cachedJson != null &&
(cachedJson['activity_plans'] as List).isNotEmpty) {
final cached = ActivityPlanResponse.fromJson(cachedJson);
return cached;

View file

@ -63,7 +63,10 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
super.dispose();
}
// _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;
bool get _isColumnMode => FluffyThemes.isColumnMode(context);
@ -105,10 +108,13 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
}
try {
setState(() {
_activityItems.clear();
_loading = true;
});
if (retries == 0) {
setState(() {
_activityItems.clear();
_loading = true;
_timeout = false;
});
}
final resp = await ActivitySearchRepo.get(_request).timeout(
const Duration(seconds: 5),
@ -116,7 +122,6 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
if (mounted) {
setState(() {
_timeout = true;
_loading = false;
});
}
@ -132,7 +137,23 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
},
);
_activityItems.addAll(resp.activityPlans);
_timeout = false;
// 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);
});
throw TimeoutException(
L10n.of(context).activitySuggestionTimeoutMessage,
);
}
} catch (e, s) {
if (e is! TimeoutException) rethrow;
ErrorHandler.logError(
@ -145,7 +166,15 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
level: SentryLevel.warning,
);
} finally {
if (mounted) setState(() => _loading = false);
// If activities are successfully retrieved, set timeout and loading to false
if (mounted && _activityItems.isNotEmpty) {
setState(
() {
_loading = false;
_timeout = false;
},
);
}
}
}
@ -214,59 +243,95 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
children: [
AnimatedSize(
duration: FluffyThemes.animationDuration,
child: (_timeout || !_loading && cards.isEmpty)
child: _timeout
? Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
spacing: 16.0,
mainAxisSize: MainAxisSize.min,
children: [
ErrorIndicator(
message: _timeout
? L10n.of(context).activitySuggestionTimeoutMessage
: L10n.of(context).errorFetchingActivitiesMessage,
),
ElevatedButton(
onPressed: _setActivityItems,
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primaryContainer,
foregroundColor: theme.colorScheme.onPrimaryContainer,
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: Text(
L10n.of(context).generatingNewActivities,
textAlign: TextAlign.center,
),
child: Text(L10n.of(context).tryAgain),
),
if (_loading) const CircularProgressIndicator(),
if (!_loading)
ElevatedButton(
onPressed: _setActivityItems,
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primaryContainer,
foregroundColor:
theme.colorScheme.onPrimaryContainer,
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
),
),
child: Text(L10n.of(context).tryAgain),
),
],
),
)
: Container(
decoration: const BoxDecoration(),
child: scrollDirection == Axis.horizontal
? Scrollbar(
thumbVisibility: true,
controller: _scrollController,
child: Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: SingleChildScrollView(
child: Column(
children: [
scrollDirection == Axis.horizontal
? Scrollbar(
thumbVisibility: true,
controller: _scrollController,
scrollDirection: Axis.horizontal,
child: Row(
spacing: 8.0,
child: Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: SingleChildScrollView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
child: Row(
spacing: 8.0,
children: cards,
),
),
),
)
: SizedBox(
width: MediaQuery.of(context).size.width,
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
runSpacing: 16.0,
spacing: 4.0,
children: cards,
),
),
),
)
: SizedBox(
width: MediaQuery.of(context).size.width,
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
runSpacing: 16.0,
spacing: 4.0,
children: cards,
if (cards.length < 5)
Padding(
padding: const EdgeInsetsGeometry.all(16.0),
child: ErrorIndicator(
message: L10n.of(context)
.activitySuggestionTimeoutMessage,
),
),
if (cards.length < 5 && _loading)
const CircularProgressIndicator(),
if (cards.length < 5 && !_loading)
Padding(
padding: const EdgeInsetsGeometry.only(bottom: 16),
child: ElevatedButton(
onPressed: _setActivityItems,
style: ElevatedButton.styleFrom(
backgroundColor:
theme.colorScheme.primaryContainer,
foregroundColor:
theme.colorScheme.onPrimaryContainer,
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
),
),
child: Text(L10n.of(context).tryAgain),
),
),
],
),
),
),
],