From 40137c226a4494705972447199b408e7f603f12c Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Fri, 12 Sep 2025 10:12:12 -0400 Subject: [PATCH] some activity / invite page tweaks (#3958) --- .../activity_participant_indicator.dart | 4 + .../activity_participant_list.dart | 29 +- .../activity_session_start_page.dart | 22 ++ .../activity_sessions_start_view.dart | 284 +++++++++--------- .../activity_summary_widget.dart | 20 +- .../pages/pangea_invitation_selection.dart | 14 +- .../pangea_invitation_selection_view.dart | 11 +- .../course_chats/course_chats_page.dart | 14 +- .../course_creation/selected_course_page.dart | 10 +- .../course_plan_room_extension.dart | 28 +- 10 files changed, 276 insertions(+), 160 deletions(-) diff --git a/lib/pangea/activity_sessions/activity_participant_indicator.dart b/lib/pangea/activity_sessions/activity_participant_indicator.dart index 693363573..841ed3a04 100644 --- a/lib/pangea/activity_sessions/activity_participant_indicator.dart +++ b/lib/pangea/activity_sessions/activity_participant_indicator.dart @@ -83,6 +83,10 @@ class ActivityParticipantIndicator extends StatelessWidget { radius: 30.0, backgroundColor: theme.colorScheme.primaryContainer, + child: const Icon( + Icons.question_mark, + size: 30.0, + ), ), Text( name, diff --git a/lib/pangea/activity_sessions/activity_participant_list.dart b/lib/pangea/activity_sessions/activity_participant_list.dart index 7705990aa..db49014fe 100644 --- a/lib/pangea/activity_sessions/activity_participant_list.dart +++ b/lib/pangea/activity_sessions/activity_participant_list.dart @@ -9,10 +9,12 @@ import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; class ActivityParticipantList extends StatelessWidget { final ActivityPlanModel activity; final Room? room; + final Room? course; final Function(String)? onTap; final bool Function(String)? canSelect; @@ -23,6 +25,7 @@ class ActivityParticipantList extends StatelessWidget { super.key, required this.activity, this.room, + this.course, this.onTap, this.canSelect, this.isSelected, @@ -50,10 +53,24 @@ class ActivityParticipantList extends StatelessWidget { spacing: 12.0, runSpacing: 12.0, children: availableRoles.values.map((availableRole) { - final assignedRole = assignedRoles[availableRole.id]; - final user = participants.participants.firstWhereOrNull( - (u) => u.id == assignedRole?.userId, - ); + final selected = + isSelected != null ? isSelected!(availableRole.id) : false; + + final assignedRole = assignedRoles[availableRole.id] ?? + (selected + ? ActivityRoleModel( + id: availableRole.id, + userId: Matrix.of(context).client.userID!, + role: availableRole.name, + ) + : null); + + final User? user = participants.participants.firstWhereOrNull( + (u) => u.id == assignedRole?.userId, + ) ?? + course?.getParticipants().firstWhereOrNull( + (u) => u.id == assignedRole?.userId, + ); final selectable = canSelect != null ? canSelect!(availableRole.id) : true; @@ -67,9 +84,7 @@ class ActivityParticipantList extends StatelessWidget { onTap: onTap != null && selectable ? () => onTap!(availableRole.id) : null, - selected: isSelected != null - ? isSelected!(availableRole.id) - : false, + selected: selected, ); }).toList(), ), diff --git a/lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart b/lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart index 4bc296bc2..e4caea598 100644 --- a/lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart +++ b/lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart @@ -16,6 +16,7 @@ import 'package:fluffychat/pangea/course_plans/course_activity_repo.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -287,6 +288,27 @@ class ActivitySessionStartController extends State { ); } + Future playWithBot() async { + if (room == null) { + throw Exception("Room is null"); + } + + if (isBotRoomMember) { + throw Exception("Bot is a member of the room"); + } + + final future = room!.client.onRoomState.stream + .where( + (state) => + state.roomId == room!.id && + state.state.type == PangeaEventTypes.activityRole && + state.state.senderId == BotName.byEnvironment, + ) + .first; + room!.invite(BotName.byEnvironment); + await future.timeout(const Duration(seconds: 30)); + } + @override Widget build(BuildContext context) => ActivitySessionStartView(this); } diff --git a/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart b/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart index 2cec592b4..f6c0d1be2 100644 --- a/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart +++ b/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart @@ -7,9 +7,7 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_summary_widget.dart'; -import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; -import 'package:fluffychat/pangea/common/widgets/share_room_button.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -56,19 +54,6 @@ class ActivitySessionStartView extends StatelessWidget { ), ), ), - actions: [ - if (controller.room != null) - Padding( - padding: const EdgeInsets.only(right: 12.0), - child: SizedBox( - width: 40.0, - height: 40.0, - child: Center( - child: ShareRoomButton(room: controller.room!), - ), - ), - ), - ], ), body: SafeArea( child: controller.loading @@ -94,6 +79,7 @@ class ActivitySessionStartView extends StatelessWidget { ActivitySummary( activity: controller.activity!, room: controller.room, + course: controller.parent, showInstructions: controller.showInstructions, toggleInstructions: @@ -119,138 +105,164 @@ class ActivitySessionStartView extends StatelessWidget { color: theme.colorScheme.surface, ), padding: const EdgeInsets.all(24.0), - child: Column( - spacing: 16.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - if (controller.descriptionText != null) - Text( - controller.descriptionText!, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, + Flexible( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: FluffyThemes.maxTimelineWidth, ), - textAlign: TextAlign.center, - ), - if (controller.state == - SessionState.notStarted) ...[ - ElevatedButton( - style: buttonStyle, - onPressed: () => context.go( - "/rooms/spaces/${controller.widget.parentId}/activity/${controller.widget.activityId}?new=true", - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, + child: Column( + spacing: 16.0, children: [ - Text( - L10n.of(context).startNewSession, - ), - ], - ), - ), - ElevatedButton( - style: buttonStyle, - onPressed: - controller.canJoinExistingSession - ? () async { - final resp = - await showFutureLoadingDialog( - context: context, - future: controller - .joinExistingSession, - ); - - if (!resp.isError) { - context.go( - "/rooms/spaces/${controller.widget.parentId}/${resp.result}", - ); - } - } - : null, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Text( - L10n.of(context).joinOpenSession, - ), - ], - ), - ), - ] else if (controller.state == - SessionState.confirmedRole) ...[ - if (controller.room!.courseParent != null) - ElevatedButton( - style: buttonStyle, - onPressed: () => - showFutureLoadingDialog( - context: context, - future: controller.pingCourse, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ + if (controller.descriptionText != + null) Text( - L10n.of(context).pingParticipants, + controller.descriptionText!, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, ), - ], - ), - ), - if (controller.room!.isRoomAdmin) ...[ - if (!controller.isBotRoomMember) - ElevatedButton( - style: buttonStyle, - onPressed: () => - showFutureLoadingDialog( - context: context, - future: () => controller.room! - .invite(BotName.byEnvironment), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Text( - L10n.of(context).playWithBot, + if (controller.state == + SessionState.notStarted) ...[ + ElevatedButton( + style: buttonStyle, + onPressed: () => context.go( + "/rooms/spaces/${controller.widget.parentId}/activity/${controller.widget.activityId}?new=true", + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + L10n.of(context) + .startNewSession, + ), + ], + ), + ), + ElevatedButton( + style: buttonStyle, + onPressed: controller + .canJoinExistingSession + ? () async { + final resp = + await showFutureLoadingDialog( + context: context, + future: controller + .joinExistingSession, + ); + + if (!resp.isError) { + context.go( + "/rooms/spaces/${controller.widget.parentId}/${resp.result}", + ); + } + } + : null, + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + L10n.of(context) + .joinOpenSession, + ), + ], + ), + ), + ] else if (controller.state == + SessionState.confirmedRole) ...[ + if (controller.room!.courseParent != + null) + ElevatedButton( + style: buttonStyle, + onPressed: () => + showFutureLoadingDialog( + context: context, + future: controller.pingCourse, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + L10n.of(context) + .pingParticipants, + ), + ], + ), + ), + if (controller + .room!.isRoomAdmin) ...[ + if (!controller.isBotRoomMember) + ElevatedButton( + style: buttonStyle, + onPressed: () => + showFutureLoadingDialog( + context: context, + future: + controller.playWithBot, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .center, + children: [ + Text( + L10n.of(context) + .playWithBot, + ), + ], + ), + ), + ElevatedButton( + style: buttonStyle, + onPressed: () => context.go( + "/rooms/${controller.room!.id}/invite", + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + L10n.of(context) + .inviteFriends, + ), + ], + ), ), ], - ), - ), - ElevatedButton( - style: buttonStyle, - onPressed: () => context.go( - "/rooms/${controller.room!.id}/invite", - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Text( - L10n.of(context).inviteFriends, + ] else + ElevatedButton( + style: buttonStyle, + onPressed: + controller.enableButtons + ? controller + .confirmRoleSelection + : null, + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + controller.room + ?.isRoomAdmin ?? + true + ? L10n.of(context).start + : L10n.of(context) + .confirm, + ), + ], + ), ), - ], - ), - ), - ], - ] else - ElevatedButton( - style: buttonStyle, - onPressed: controller.enableButtons - ? controller.confirmRoleSelection - : null, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Text( - controller.room?.isRoomAdmin ?? true - ? L10n.of(context).start - : L10n.of(context).confirm, - ), ], ), ), + ), ], ), ), diff --git a/lib/pangea/activity_sessions/activity_summary_widget.dart b/lib/pangea/activity_sessions/activity_summary_widget.dart index ae78e6bed..a1bfc7acf 100644 --- a/lib/pangea/activity_sessions/activity_summary_widget.dart +++ b/lib/pangea/activity_sessions/activity_summary_widget.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -15,6 +17,7 @@ import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_en class ActivitySummary extends StatelessWidget { final ActivityPlanModel activity; final Room? room; + final Room? course; final bool showInstructions; final VoidCallback toggleInstructions; @@ -34,6 +37,7 @@ class ActivitySummary extends StatelessWidget { this.isParticipantSelected, this.getParticipantOpacity, this.room, + this.course, }); @override @@ -48,14 +52,22 @@ class ActivitySummary extends StatelessWidget { child: Column( spacing: 4.0, children: [ - ImageByUrl( - imageUrl: activity.imageURL, - width: 80.0, - borderRadius: BorderRadius.circular(20), + LayoutBuilder( + builder: (context, constraints) { + return ImageByUrl( + imageUrl: activity.imageURL, + width: min( + constraints.maxWidth, + MediaQuery.sizeOf(context).height * 0.5, + ), + borderRadius: BorderRadius.circular(20), + ); + }, ), ActivityParticipantList( activity: activity, room: room, + course: course, onTap: onTapParticipant, canSelect: canSelectParticipant, isSelected: isParticipantSelected, diff --git a/lib/pangea/chat_settings/pages/pangea_invitation_selection.dart b/lib/pangea/chat_settings/pages/pangea_invitation_selection.dart index 009524172..7e4d0234d 100644 --- a/lib/pangea/chat_settings/pages/pangea_invitation_selection.dart +++ b/lib/pangea/chat_settings/pages/pangea_invitation_selection.dart @@ -17,11 +17,11 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; enum InvitationFilter { + participants, space, contacts, knocking, invited, - participants, public; static InvitationFilter? fromString(String value) { @@ -268,6 +268,9 @@ class PangeaInvitationSelectionController .toList(); contacts.sort(_sortUsers); + if (_room?.isSpace ?? false) { + contacts.removeWhere((u) => u.id == BotName.byEnvironment); + } return contacts; } @@ -341,8 +344,14 @@ class PangeaInvitationSelectionController } finally { setState(() => loading = false); } + + final results = response.results; + if (_room?.isSpace ?? false) { + results.removeWhere((profile) => profile.userId == BotName.byEnvironment); + } + setState(() { - foundProfiles = List.from(response.results); + foundProfiles = List.from(results); if (text.isValidMatrixId && foundProfiles.indexWhere((profile) => text == profile.userId) == -1) { setState( @@ -359,6 +368,7 @@ class PangeaInvitationSelectionController [Membership.join, Membership.invite].contains(user.membership), ) .toList(); + foundProfiles.removeWhere( (profile) => participants?.indexWhere((u) => u.id == profile.userId) != -1 && diff --git a/lib/pangea/chat_settings/pages/pangea_invitation_selection_view.dart b/lib/pangea/chat_settings/pages/pangea_invitation_selection_view.dart index b7bfc8266..c2a90a085 100644 --- a/lib/pangea/chat_settings/pages/pangea_invitation_selection_view.dart +++ b/lib/pangea/chat_settings/pages/pangea_invitation_selection_view.dart @@ -130,7 +130,16 @@ class PangeaInvitationSelectionView extends StatelessWidget { spacing: 12.0, children: controller.availableFilters.map((filter) { return FilterChip( - label: Text(controller.filterLabel(filter)), + label: filter == InvitationFilter.participants + ? Row( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.group, size: 16.0), + Text(controller.filterLabel(filter)), + ], + ) + : Text(controller.filterLabel(filter)), onSelected: (_) => controller.setFilter(filter), selected: controller.filter == filter, ); diff --git a/lib/pangea/course_chats/course_chats_page.dart b/lib/pangea/course_chats/course_chats_page.dart index 17d4aa92c..865ccaba6 100644 --- a/lib/pangea/course_chats/course_chats_page.dart +++ b/lib/pangea/course_chats/course_chats_page.dart @@ -812,12 +812,14 @@ class CourseChatsController extends State { final leaveTimeline = leaveUpdate?[widget.roomId]?.timeline?.events; if (joinTimeline == null && leaveTimeline == null) return false; - final bool hasJoinUpdate = joinTimeline!.any( - (event) => event.type == EventTypes.SpaceChild, - ); - final bool hasLeaveUpdate = leaveTimeline!.any( - (event) => event.type == EventTypes.SpaceChild, - ); + final bool hasJoinUpdate = joinTimeline?.any( + (event) => event.type == EventTypes.SpaceChild, + ) ?? + false; + final bool hasLeaveUpdate = leaveTimeline?.any( + (event) => event.type == EventTypes.SpaceChild, + ) ?? + false; return hasJoinUpdate || hasLeaveUpdate; } diff --git a/lib/pangea/course_creation/selected_course_page.dart b/lib/pangea/course_creation/selected_course_page.dart index acf17c858..dba6d780b 100644 --- a/lib/pangea/course_creation/selected_course_page.dart +++ b/lib/pangea/course_creation/selected_course_page.dart @@ -2,6 +2,7 @@ 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'; @@ -27,10 +28,15 @@ class SelectedCourseController extends State { final client = Matrix.of(context).client; Uint8List? avatar; Uri? avatarUrl; - if (course.imageUrl != null) { + final imageUrl = course.imageUrl ?? + course.loadedTopics + .lastWhereOrNull((topic) => topic.imageUrl != null) + ?.imageUrl; + + if (imageUrl != null) { try { final Response response = await http.get( - Uri.parse(course.imageUrl!), + Uri.parse(imageUrl), headers: { 'Authorization': 'Bearer ${MatrixState.pangeaController.userController.accessToken}', diff --git a/lib/pangea/course_plans/course_plan_room_extension.dart b/lib/pangea/course_plans/course_plan_room_extension.dart index acd2645e6..c120c646b 100644 --- a/lib/pangea/course_plans/course_plan_room_extension.dart +++ b/lib/pangea/course_plans/course_plan_room_extension.dart @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart'; + +import 'package:http/http.dart' as http; import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; @@ -15,6 +18,7 @@ import 'package:fluffychat/pangea/course_plans/course_user_event.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/join_rule_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/widgets/matrix.dart'; extension CoursePlanRoomExtension on Room { CoursePlanEvent? get coursePlan { @@ -101,6 +105,7 @@ extension CoursePlanRoomExtension on Room { } final activityIds = course.loadedTopics[topicIndex].loadedActivities + .where((a) => a.req.numberOfParticipants <= 2) .map((a) => a.activityId) .toList(); return state.completedActivities.toSet().containsAll(activityIds); @@ -214,6 +219,25 @@ extension CoursePlanRoomExtension on Room { ActivityPlanModel activity, ActivityRole? role, ) async { + Uri? avatarUrl; + if (activity.imageURL != null) { + try { + final http.Response response = await http.get( + Uri.parse(activity.imageURL!), + headers: { + 'Authorization': + 'Bearer ${MatrixState.pangeaController.userController.accessToken}', + }, + ); + if (response.statusCode != 200) { + throw Exception('Failed to load course image'); + } + final avatar = response.bodyBytes; + avatarUrl = await client.uploadContent(avatar); + } catch (e) { + debugPrint("Error fetching course image: $e"); + } + } final roomID = await client.createRoom( creationContent: { 'type': "${PangeaRoomTypes.activitySession}:${activity.activityId}", @@ -225,10 +249,10 @@ extension CoursePlanRoomExtension on Room { type: PangeaEventTypes.activityPlan, content: activity.toJson(), ), - if (activity.imageURL != null) + if (avatarUrl != null) StateEvent( type: EventTypes.RoomAvatar, - content: {'url': activity.imageURL}, + content: {'url': avatarUrl.toString()}, ), if (role != null) StateEvent(