3962 usability test todos (#3973)

* in new course pages, show images

* in space analytics, if no available languages, pick user's l2

* chore: add cooldown on ping participants

* replace image loading icon with shimmer
This commit is contained in:
ggurdin 2025-09-12 14:03:08 -04:00 committed by GitHub
parent c831d6964d
commit 7348e655f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 97 additions and 66 deletions

View file

@ -57,6 +57,8 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
bool showInstructions = false;
String? _selectedRoleId;
Timer? _pingCooldown;
@override
void initState() {
super.initState();
@ -78,6 +80,12 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
}
}
@override
void dispose() {
_pingCooldown?.cancel();
super.dispose();
}
Room? get room => widget.room;
Room? get parent => widget.parentId != null
@ -152,6 +160,11 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
return parent!.numOpenSessions(widget.activityId) > 0;
}
bool get canPingParticipants {
if (room == null || room?.courseParent == null) return false;
return _pingCooldown == null || !_pingCooldown!.isActive;
}
void toggleInstructions() {
setState(() {
showInstructions = !showInstructions;
@ -276,6 +289,16 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
throw Exception("Activity is not part of a course");
}
if (!canPingParticipants) {
throw Exception("Ping is on cooldown");
}
_pingCooldown?.cancel();
_pingCooldown = Timer(const Duration(minutes: 1), () {
_pingCooldown = null;
if (mounted) setState(() {});
});
await room!.courseParent!.sendEvent(
{
"body": L10n.of(context).pingParticipantsNotification(
@ -286,6 +309,8 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
"pangea.activity.session_room_id": room!.id,
},
);
if (mounted) setState(() {});
}
Future<void> playWithBot() async {

View file

@ -180,11 +180,16 @@ class ActivitySessionStartView extends StatelessWidget {
null)
ElevatedButton(
style: buttonStyle,
onPressed: () =>
showFutureLoadingDialog(
context: context,
future: controller.pingCourse,
),
onPressed: controller
.canPingParticipants
? () {
showFutureLoadingDialog(
context: context,
future: controller
.pingCourse,
);
}
: null,
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:shimmer/shimmer.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
@ -50,8 +51,18 @@ class ImageByUrl extends StatelessWidget {
context,
url,
) =>
const Center(
child: CircularProgressIndicator(),
Shimmer.fromColors(
baseColor:
Theme.of(context).colorScheme.primary.withAlpha(20),
highlightColor:
Theme.of(context).colorScheme.primary.withAlpha(50),
child: Container(
width: width,
height: width,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
),
),
),
errorWidget: (
context,

View file

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart';
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/map_clipper.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
class CoursePlanTile extends StatelessWidget {
@ -41,14 +42,18 @@ class CoursePlanTile extends StatelessWidget {
child: Row(
spacing: 4.0,
children: [
ImageByUrl(
imageUrl: course.imageUrl,
width: 40.0,
replacement: Container(
ClipPath(
clipper: MapClipper(),
child: ImageByUrl(
imageUrl: course.imageUrl,
borderRadius: BorderRadius.circular(0.0),
width: 40.0,
height: 40.0,
decoration: BoxDecoration(
color: theme.colorScheme.secondary,
replacement: Container(
width: 40.0,
height: 40.0,
decoration: BoxDecoration(
color: theme.colorScheme.secondary,
),
),
),
),

View file

@ -2,7 +2,6 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
@ -28,11 +27,7 @@ class SelectedCourseController extends State<SelectedCourse> {
final client = Matrix.of(context).client;
Uint8List? avatar;
Uri? avatarUrl;
final imageUrl = course.imageUrl ??
course.loadedTopics
.lastWhereOrNull((topic) => topic.imageUrl != null)
?.imageUrl;
final imageUrl = course.imageUrl;
if (imageUrl != null) {
try {
final Response response = await http.get(

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/l10n/l10n.dart';
@ -8,6 +7,8 @@ import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart';
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/map_clipper.dart';
import 'package:fluffychat/pangea/course_settings/pin_clipper.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
@ -58,14 +59,18 @@ class SelectedCourseView extends StatelessWidget {
return Column(
spacing: 8.0,
children: [
ImageByUrl(
imageUrl: course.imageUrl,
width: 100.0,
replacement: Container(
ClipPath(
clipper: MapClipper(),
child: ImageByUrl(
imageUrl: course.imageUrl,
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
color: theme.colorScheme.secondary,
borderRadius: BorderRadius.circular(0.0),
replacement: Container(
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
color: theme.colorScheme.secondary,
),
),
),
),
@ -124,39 +129,19 @@ class SelectedCourseView extends StatelessWidget {
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(80),
child: topic.imageUrl != null
? CachedNetworkImage(
width: 40.0,
height: 40.0,
fit: BoxFit.cover,
imageUrl: topic.imageUrl!,
placeholder: (context, url) {
return const Center(
child:
CircularProgressIndicator(),
);
},
errorWidget: (context, url, error) {
return Container(
width: 40.0,
height: 40.0,
decoration: BoxDecoration(
color: theme
.colorScheme.secondary,
),
);
},
)
: Container(
width: 40.0,
height: 40.0,
decoration: BoxDecoration(
color:
theme.colorScheme.secondary,
),
),
ClipPath(
clipper: PinClipper(),
child: ImageByUrl(
imageUrl: topic.imageUrl,
width: 45.0,
replacement: Container(
width: 45.0,
height: 45.0,
decoration: BoxDecoration(
color: theme.colorScheme.secondary,
),
),
),
),
Flexible(
child: Column(

View file

@ -1,3 +1,5 @@
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/course_plans/course_media_repo.dart';
@ -111,7 +113,9 @@ class CoursePlanModel {
List<String> get loadedMediaUrls => CourseMediaRepo.getSync(mediaIds);
Future<List<String>> fetchMediaUrls() => CourseMediaRepo.get(uuid, mediaIds);
String? get imageUrl => loadedMediaUrls.isEmpty
? null
? loadedTopics
.lastWhereOrNull((topic) => topic.imageUrl != null)
?.imageUrl
: "${Environment.cmsApi}${loadedMediaUrls.first}";
Future<void> init() async {

View file

@ -10,7 +10,7 @@ class PinClipper extends CustomClipper<Path> {
path.moveTo(w * 0.1, h * 0.4);
path.arcToPoint(
Offset(w * 0.9, h * 0.4),
radius: const Radius.circular(20),
radius: const Radius.circular(15),
);
path.quadraticBezierTo(w * 0.9, h * 0.75, w / 2, h);
path.quadraticBezierTo(w * 0.1, h * 0.75, w * 0.1, h * 0.4);

View file

@ -228,9 +228,10 @@ class SpaceAnalyticsState extends State<SpaceAnalytics> {
];
await Future.wait(futures);
selectedLanguage = availableLanguages.contains(_userL2)
? _userL2
: availableLanguages.firstOrNull;
selectedLanguage =
availableLanguages.contains(_userL2) || availableLanguages.isEmpty
? _userL2
: availableLanguages.firstOrNull;
await refresh();
if (mounted) {