diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 953390a52..b9b58ede7 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5353,5 +5353,8 @@ "denyKnockChat": "User will be rejected from chat, however they may attempt to rejoin any time later.", "banFromSpace": "Ban from course", "unbanFromSpace": "Unban from course", - "cannotJoinBannedRoom": "Banned. Unable to join." + "cannotJoinBannedRoom": "Banned. Unable to join.", + "sessionFull": "Too late! This activity is full.", + "returnToCourse": "Return to course", + "returnHome": "Return home" } diff --git a/lib/pangea/activity_sessions/activity_session_button_widget.dart b/lib/pangea/activity_sessions/activity_session_button_widget.dart new file mode 100644 index 000000000..3f2a3ebee --- /dev/null +++ b/lib/pangea/activity_sessions/activity_session_button_widget.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; + +class ActivitySessionButtonWidget extends StatelessWidget { + final ActivitySessionStartController controller; + + const ActivitySessionButtonWidget({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return AnimatedSize( + duration: FluffyThemes.animationDuration, + child: Container( + decoration: BoxDecoration( + border: Border(top: BorderSide(color: theme.dividerColor)), + color: theme.colorScheme.surface, + ), + padding: const EdgeInsets.all(24.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: FluffyThemes.maxTimelineWidth, + ), + child: Column( + spacing: 16.0, + children: [ + if (controller.descriptionText != null) + Text( + controller.descriptionText!, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + switch (controller.state) { + SessionState.notStarted => _ActivityStartButtons( + controller, + ), + SessionState.confirmedRole => + _ActivityRoleConfirmedButtons(controller: controller), + SessionState.selectedSessionFull => _CTAButton( + controller.courseParent != null + ? L10n.of(context).returnToCourse + : L10n.of(context).returnHome, + controller.returnFromFullSession, + ), + _ => _CTAButton( + controller.activityRoom?.isRoomAdmin ?? true + ? L10n.of(context).start + : L10n.of(context).confirm, + controller.confirmRoleSelection, + ), + }, + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class _ActivityStartButtons extends StatelessWidget { + final ActivitySessionStartController controller; + const _ActivityStartButtons(this.controller); + + @override + Widget build(BuildContext context) { + final hasActiveSession = controller.canJoinExistingSession; + + return FutureBuilder( + future: controller.neededCourseParticipants(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const LinearProgressIndicator(); + } + + final int neededParticipants = snapshot.data ?? 0; + final bool hasEnoughParticipants = neededParticipants <= 0; + return Column( + spacing: 16.0, + children: [ + if (!hasEnoughParticipants) ...[ + Text( + neededParticipants > 1 + ? L10n.of(context).activityNeedsMembers(neededParticipants) + : L10n.of(context).activityNeedsOneMember, + textAlign: TextAlign.center, + ), + _CTAButton( + L10n.of(context).inviteFriendsToCourse, + controller.inviteToCourse, + ), + _CTAButton( + L10n.of(context).pickDifferentActivity, + controller.goToCourse, + ), + ] else if (controller.joinedActivityRoomId != null) ...[ + _CTAButton( + L10n.of(context).continueText, + controller.goToJoinedActivity, + ), + ] else ...[ + _CTAButton( + hasActiveSession + ? L10n.of(context).startNewSession + : L10n.of(context).start, + controller.startNewActivity, + ), + if (hasActiveSession) + _CTAButton( + L10n.of(context).joinOpenSession, + controller.joinExistingSession, + ), + ], + ], + ); + }, + ); + } +} + +class _ActivityRoleConfirmedButtons extends StatelessWidget { + final ActivitySessionStartController controller; + const _ActivityRoleConfirmedButtons({required this.controller}); + + @override + Widget build(BuildContext context) { + final showPingCourse = controller.courseParent != null; + final canPingCourse = controller.canPingParticipants; + + final showInviteOptions = controller.activityRoom?.isRoomAdmin == true; + final showPlayWithBot = !controller.isBotRoomMember; + + return Column( + mainAxisSize: .min, + children: [ + if (showPingCourse) + _CTAButton( + L10n.of(context).pingParticipants, + canPingCourse ? controller.pingCourse : null, + ), + if (showInviteOptions && showPlayWithBot) + _CTAButton(L10n.of(context).playWithBot, controller.playWithBot), + if (showInviteOptions) + _CTAButton(L10n.of(context).inviteFriends, controller.inviteFriends), + ], + ); + } +} + +class _CTAButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + + const _CTAButton(this.text, this.onPressed); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primaryContainer, + foregroundColor: theme.colorScheme.onPrimaryContainer, + padding: const EdgeInsets.all(8.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + ), + onPressed: onPressed, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [Flexible(child: Text(text, textAlign: TextAlign.center))], + ), + ); + } +} 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 070d8d521..5722349fe 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 @@ -33,6 +33,9 @@ enum SessionState { /// The room has been created but the user hasn't selected a role yet. Non-admins haven't joined yet. notSelectedRole, + /// The room has been created and all roles are full. The user cannot get a role in this session. + selectedSessionFull, + /// The user has selected a role but hasn't confirmed yet. Non-admins haven't joined yet. selectedRole, @@ -86,9 +89,7 @@ class ActivitySessionStartController extends State _selectedRoleId = null; showInstructions = false; }); - } - if (oldWidget.activityId != widget.activityId) { _load(); } } @@ -115,22 +116,31 @@ class ActivitySessionStartController extends State false; SessionState get state { + // the room exists and user has set their role if (activityRoom?.membership == Membership.join && activityRoom?.hasPickedRole == true) { return SessionState.confirmedRole; } + // the user has selected a role but hasn't confirmed yet if (_selectedRoleId != null) { return SessionState.selectedRole; } + // the room either doesn't exist or the user hasn't joined it yet if (activityRoom == null || activityRoom!.membership != Membership.join) { + // If the room does exist or is being created, then user needs to select a role. + // Else (room doesn't exist and user hasn't started creating it), then not started. return widget.roomId != null || widget.launch - ? SessionState.notSelectedRole + ? canSelectRole + ? SessionState.notSelectedRole + : SessionState.selectedSessionFull : SessionState.notStarted; } - return SessionState.notSelectedRole; + return canSelectRole + ? SessionState.notSelectedRole + : SessionState.selectedSessionFull; } String? get descriptionText { @@ -151,12 +161,11 @@ class ActivitySessionStartController extends State return activityRoom?.isRoomAdmin ?? false ? L10n.of(context).chooseRole : L10n.of(context).chooseRoleToParticipate; + case SessionState.selectedSessionFull: + return L10n.of(context).sessionFull; } } - bool get enableButtons => - [SessionState.notStarted, SessionState.selectedRole].contains(state); - Map get assignedRoles { if (activityRoom != null && activityRoom!.membership == Membership.join) { return activityRoom!.assignedRoles ?? {}; @@ -164,6 +173,12 @@ class ActivitySessionStartController extends State return roomSummaries?[widget.roomId]?.joinedUsersWithRoles ?? {}; } + bool get canSelectRole { + final assigned = assignedRoles.length; + final total = activity?.roles.length ?? 0; + return assigned < total; + } + bool canSelectParticipant(String id) { if (state == SessionState.confirmedRole || state == SessionState.notStarted) { @@ -311,13 +326,19 @@ class ActivitySessionStartController extends State } Future _loadSummary() async { - if (courseParent == null) return; - await loadRoomSummaries( - courseParent!.spaceChildren - .map((c) => c.roomId) - .whereType() - .toList(), - ); + final Set roomIds = {}; + if (widget.roomId != null) { + roomIds.add(widget.roomId!); + } + + if (courseParent != null) { + roomIds.addAll( + courseParent!.spaceChildren.map((c) => c.roomId).whereType(), + ); + } + + if (roomIds.isEmpty) return; + await loadRoomSummaries(roomIds.toList()); } Future _loadActivity() async { @@ -412,7 +433,18 @@ class ActivitySessionStartController extends State } } - Future joinExistingSession() async { + Future joinExistingSession() async { + final resp = await showFutureLoadingDialog( + context: context, + future: _joinExistingSession, + ); + + if (!resp.isError) { + NavigationUtil.goToSpaceRoute(resp.result, [], context); + } + } + + Future _joinExistingSession() async { if (!canJoinExistingSession) { throw Exception("No existing session to join"); } @@ -476,7 +508,10 @@ class ActivitySessionStartController extends State } } - Future pingCourse() async { + Future pingCourse() => + showFutureLoadingDialog(context: context, future: _pingCourse); + + Future _pingCourse() async { if (activityRoom?.courseParent == null) { throw Exception("Activity is not part of a course"); } @@ -546,6 +581,34 @@ class ActivitySessionStartController extends State await future.timeout(const Duration(seconds: 5)); } + void inviteFriends() { + if (activityRoom == null) return; + NavigationUtil.goToSpaceRoute(activityRoom!.id, ['invite'], context); + } + + void inviteToCourse() { + if (courseParent == null) return; + context.push("/rooms/spaces/${courseParent!.id}/invite"); + } + + void goToCourse() { + if (courseParent == null) return; + context.push("/rooms/spaces/${courseParent!.id}/details?tab=course"); + } + + void goToJoinedActivity() { + if (joinedActivityRoomId == null) return; + NavigationUtil.goToSpaceRoute(joinedActivityRoomId!, [], context); + } + + void returnFromFullSession() { + if (courseParent != null) { + goToCourse(); + } else { + NavigationUtil.goToSpaceRoute(null, [], context); + } + } + @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 5003a8379..80c0cf816 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 @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; -import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; @@ -10,6 +9,7 @@ import 'package:fluffychat/pangea/activity_feedback/activity_feedback_repo.dart' import 'package:fluffychat/pangea/activity_feedback/activity_feedback_request.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_session_button_widget.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/chat_settings/utils/room_summary_extension.dart'; @@ -20,7 +20,6 @@ import 'package:fluffychat/pangea/course_chats/open_roles_indicator.dart'; import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart'; import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_repo.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/navigation/navigation_util.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -30,16 +29,60 @@ class ActivitySessionStartView extends StatelessWidget { final ActivitySessionStartController controller; const ActivitySessionStartView(this.controller, {super.key}); - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final buttonStyle = ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.primaryContainer, - foregroundColor: theme.colorScheme.onPrimaryContainer, - padding: const EdgeInsets.all(8.0), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)), + Future _submitActivityFeedback(BuildContext context) async { + final feedback = await showDialog( + context: context, + builder: (context) { + return FeedbackDialog( + title: L10n.of(context).feedbackTitle, + onSubmit: (feedback) { + Navigator.of(context).pop(feedback); + }, + scrollable: false, + ); + }, ); + if (feedback == null || feedback.isEmpty) { + return; + } + + final resp = await showFutureLoadingDialog( + context: context, + future: () => ActivityFeedbackRepo.submitFeedback( + ActivityFeedbackRequest( + activityId: controller.widget.activityId, + feedbackText: feedback, + userId: Matrix.of(context).client.userID!, + userL1: MatrixState.pangeaController.userController.userL1Code!, + userL2: MatrixState.pangeaController.userController.userL2Code!, + ), + ), + ); + + if (resp.isError) { + return; + } + + CourseActivityRepo.setSentFeedback( + controller.widget.activityId, + MatrixState.pangeaController.userController.userL1Code!, + ); + + await showDialog( + context: context, + builder: (context) { + return FeedbackResponseDialog( + title: L10n.of(context).feedbackTitle, + feedback: resp.result!.userFriendlyResponse, + description: L10n.of(context).feedbackRespDesc, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { return StreamBuilder( stream: Matrix.of( context, @@ -73,63 +116,7 @@ class ActivitySessionStartView extends StatelessWidget { actions: [ IconButton( icon: const Icon(Icons.flag_outlined), - onPressed: () async { - final feedback = await showDialog( - context: context, - builder: (context) { - return FeedbackDialog( - title: L10n.of(context).feedbackTitle, - onSubmit: (feedback) { - Navigator.of(context).pop(feedback); - }, - scrollable: false, - ); - }, - ); - - if (feedback == null || feedback.isEmpty) { - return; - } - - final resp = await showFutureLoadingDialog( - context: context, - future: () => ActivityFeedbackRepo.submitFeedback( - ActivityFeedbackRequest( - activityId: controller.widget.activityId, - feedbackText: feedback, - userId: Matrix.of(context).client.userID!, - userL1: MatrixState - .pangeaController - .userController - .userL1Code!, - userL2: MatrixState - .pangeaController - .userController - .userL2Code!, - ), - ), - ); - - if (resp.isError) { - return; - } - - CourseActivityRepo.setSentFeedback( - controller.widget.activityId, - MatrixState.pangeaController.userController.userL1Code!, - ); - - await showDialog( - context: context, - builder: (context) { - return FeedbackResponseDialog( - title: L10n.of(context).feedbackTitle, - feedback: resp.result!.userFriendlyResponse, - description: L10n.of(context).feedbackRespDesc, - ); - }, - ); - }, + onPressed: () => _submitActivityFeedback(context), ), ], ), @@ -180,142 +167,7 @@ class ActivitySessionStartView extends StatelessWidget { ), ), ), - AnimatedSize( - duration: FluffyThemes.animationDuration, - child: Container( - decoration: BoxDecoration( - border: Border( - top: BorderSide(color: theme.dividerColor), - ), - color: theme.colorScheme.surface, - ), - padding: const EdgeInsets.all(24.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: FluffyThemes.maxTimelineWidth, - ), - child: Column( - spacing: 16.0, - children: [ - if (controller.descriptionText != null) - Text( - controller.descriptionText!, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - if (controller.state == - SessionState.notStarted) - _ActivityStartButtons( - controller, - buttonStyle, - ) - else if (controller.state == - SessionState.confirmedRole) ...[ - if (controller.courseParent != null) - ElevatedButton( - style: buttonStyle, - onPressed: - controller.canPingParticipants - ? () { - showFutureLoadingDialog( - context: context, - future: - controller.pingCourse, - ); - } - : null, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Flexible( - child: Text( - L10n.of( - context, - ).pingParticipants, - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - if (controller - .activityRoom! - .isRoomAdmin) ...[ - if (!controller.isBotRoomMember) - ElevatedButton( - style: buttonStyle, - onPressed: controller.playWithBot, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Text( - L10n.of( - context, - ).playWithBot, - ), - ], - ), - ), - ElevatedButton( - style: buttonStyle, - onPressed: () { - NavigationUtil.goToSpaceRoute( - controller.activityRoom!.id, - ['invite'], - context, - ); - }, - 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 - .activityRoom - ?.isRoomAdmin ?? - true - ? L10n.of(context).start - : L10n.of(context).confirm, - ), - ], - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), + ActivitySessionButtonWidget(controller: controller), ], ), ), @@ -325,113 +177,6 @@ class ActivitySessionStartView extends StatelessWidget { } } -class _ActivityStartButtons extends StatelessWidget { - final ActivitySessionStartController controller; - final ButtonStyle buttonStyle; - const _ActivityStartButtons(this.controller, this.buttonStyle); - - @override - Widget build(BuildContext context) { - final hasActiveSession = controller.canJoinExistingSession; - final joinedActivityRoom = controller.joinedActivityRoomId; - - return FutureBuilder( - future: controller.neededCourseParticipants(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const LinearProgressIndicator(); - } - - final int neededParticipants = snapshot.data ?? 0; - final bool hasEnoughParticipants = neededParticipants <= 0; - return Column( - spacing: 16.0, - children: [ - if (!hasEnoughParticipants) ...[ - Text( - neededParticipants > 1 - ? L10n.of(context).activityNeedsMembers(neededParticipants) - : L10n.of(context).activityNeedsOneMember, - textAlign: TextAlign.center, - ), - ElevatedButton( - style: buttonStyle, - onPressed: controller.courseParent?.canInvite ?? false - ? () => context.push( - "/rooms/spaces/${controller.courseParent!.id}/invite", - ) - : null, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [Text(L10n.of(context).inviteFriendsToCourse)], - ), - ), - ElevatedButton( - style: buttonStyle, - onPressed: () => context.push( - "/rooms/spaces/${controller.courseParent!.id}/details?tab=course", - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [Text(L10n.of(context).pickDifferentActivity)], - ), - ), - ] else if (joinedActivityRoom != null) ...[ - ElevatedButton( - style: buttonStyle, - onPressed: () { - NavigationUtil.goToSpaceRoute( - joinedActivityRoom, - [], - context, - ); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [Text(L10n.of(context).continueText)], - ), - ), - ] else ...[ - ElevatedButton( - style: buttonStyle, - onPressed: controller.startNewActivity, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - hasActiveSession - ? L10n.of(context).startNewSession - : L10n.of(context).start, - ), - ], - ), - ), - if (hasActiveSession) - ElevatedButton( - style: buttonStyle, - onPressed: () async { - final resp = await showFutureLoadingDialog( - context: context, - future: controller.joinExistingSession, - ); - - if (!resp.isError) { - NavigationUtil.goToSpaceRoute(resp.result, [], context); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [Text(L10n.of(context).joinOpenSession)], - ), - ), - ], - ], - ); - }, - ); - } -} - class _ActivityStatuses extends StatelessWidget { final Map> statuses; final Room space; diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 7d83f1275..cd00e9583 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -88,6 +88,7 @@ class BackgroundPush { // Handle notifications when app is opened from terminated/background state FirebaseMessaging.instance.getInitialMessage().then(_onOpenNotification); FirebaseMessaging.onMessageOpenedApp.listen(_onOpenNotification); + FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); // Pangea# mainIsolateReceivePort?.listen((message) async { try { @@ -631,3 +632,15 @@ class UPFunctions extends UnifiedPushFunctions { await UnifiedPush.saveDistributor(distributor); } } + +// #Pangea +@pragma('vm:entry-point') +Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { + // Required for background isolate + WidgetsFlutterBinding.ensureInitialized(); + final instance = BackgroundPush._instance; + if (instance == null) return; + await instance._onOpenNotification(message); +} + +// Pangea#