From c04466fdca74cfaa68754d9cbbc5eb7bcc87df2f Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:10:13 -0400 Subject: [PATCH] 3915 course chat view updates (#3919) --- lib/l10n/intl_en.arb | 6 +- lib/pages/chat/chat.dart | 6 +- lib/pages/chat_list/chat_list_item.dart | 47 +++- lib/pages/chat_list/navi_rail_item.dart | 4 +- .../activity_finished_status_message.dart | 15 +- .../activity_session_start_page.dart | 90 +++++++- .../activity_sessions_start_view.dart | 20 +- .../activity_suggestion_card.dart | 41 +++- .../utils/get_chat_list_item_subtitle.dart | 30 ++- .../activity_template_chat_list_item.dart | 129 +++++++++++ .../course_chats/course_chats_page.dart | 164 +++++++++---- .../course_chats/course_chats_view.dart | 216 ++++++------------ .../extended_space_rooms_chunk.dart | 13 ++ .../course_chats/open_roles_indicator.dart | 62 +++++ .../course_plans/course_plan_model.dart | 11 + .../course_plan_room_extension.dart | 97 ++++++-- .../course_plans/course_topic_model.dart | 7 + .../course_plans/course_user_event.dart | 58 +++-- lib/pangea/course_plans/map_clipper.dart | 26 +++ .../course_settings/course_settings.dart | 6 +- lib/widgets/navigation_rail.dart | 40 ++-- 21 files changed, 805 insertions(+), 283 deletions(-) create mode 100644 lib/pangea/course_chats/activity_template_chat_list_item.dart create mode 100644 lib/pangea/course_chats/extended_space_rooms_chunk.dart create mode 100644 lib/pangea/course_chats/open_roles_indicator.dart create mode 100644 lib/pangea/course_plans/map_clipper.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index b09fe0539..26872e065 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5225,5 +5225,9 @@ "startNewSession": "Start new session", "joinOpenSession": "Join open session", "less": "less", - "activityNotFound": "Activity not found" + "activityNotFound": "Activity not found", + "myActivities": "My activities", + "openToJoin": "Open to join", + "results": "Results", + "activityDone": "Activity Done!" } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 9ec402971..48f5bdfd8 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -2247,13 +2247,11 @@ class ChatController extends State ); } - if (room.isActivitySession == true && - !room.activityHasStarted && - room.courseParent != null) { + if (room.isActivitySession == true && !room.activityHasStarted) { return ActivitySessionStartPage( activityId: room.activityId!, room: room, - parentId: room.courseParent!.id, + parentId: room.courseParent?.id, ); } // Pangea# diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index c3e6c3256..875938356 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -25,6 +25,10 @@ class ChatListItem extends StatelessWidget { final void Function()? onForget; final void Function() onTap; final String? filter; + // #Pangea + final BorderRadius? borderRadius; + final Widget? trailing; + // Pangea# const ChatListItem( this.room, { @@ -35,6 +39,10 @@ class ChatListItem extends StatelessWidget { this.filter, this.space, super.key, + // #Pangea + this.borderRadius, + this.trailing, + // Pangea# }); Future archiveAction(BuildContext context) async { @@ -174,11 +182,19 @@ class ChatListItem extends StatelessWidget { color: backgroundColor ?? theme.colorScheme.surface, ), - borderRadius: room.isSpace - ? BorderRadius.circular( - AppConfig.borderRadius / 4, - ) - : null, + // #Pangea + // borderRadius: room.isSpace + // ? BorderRadius.circular( + // AppConfig.borderRadius / 4, + // ) + // : null, + borderRadius: borderRadius ?? + (room.isSpace + ? BorderRadius.circular( + AppConfig.borderRadius / 4, + ) + : null), + // Pangea# mxContent: room.avatar, size: space != null ? Avatar.defaultSize * 0.75 @@ -428,12 +444,21 @@ class ChatListItem extends StatelessWidget { ], ), onTap: onTap, - trailing: onForget == null - ? null - : IconButton( - icon: const Icon(Icons.delete_outlined), - onPressed: onForget, - ), + // #Pangea + // trailing: onForget == null + // ? null + // : IconButton( + // icon: const Icon(Icons.delete_outlined), + // onPressed: onForget, + // ), + trailing: trailing ?? + (onForget == null + ? null + : IconButton( + icon: const Icon(Icons.delete_outlined), + onPressed: onForget, + )), + // Pangea# ), ), ), diff --git a/lib/pages/chat_list/navi_rail_item.dart b/lib/pages/chat_list/navi_rail_item.dart index 492804381..dbc3c29bc 100644 --- a/lib/pages/chat_list/navi_rail_item.dart +++ b/lib/pages/chat_list/navi_rail_item.dart @@ -16,6 +16,7 @@ class NaviRailItem extends StatelessWidget { final bool Function(Room)? unreadBadgeFilter; // #Pangea final Color? backgroundColor; + final BorderRadius? borderRadius; // Pangea# const NaviRailItem({ @@ -27,6 +28,7 @@ class NaviRailItem extends StatelessWidget { this.unreadBadgeFilter, // #Pangea this.backgroundColor, + this.borderRadius, // Pangea# super.key, }); @@ -36,7 +38,7 @@ class NaviRailItem extends StatelessWidget { // #Pangea // final borderRadius = BorderRadius.circular(AppConfig.borderRadius); - final borderRadius = BorderRadius.circular(10.0); + final borderRadius = this.borderRadius ?? BorderRadius.circular(10.0); final isColumnMode = FluffyThemes.isColumnMode(context); final width = isColumnMode diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart index 4f0696cd1..bad850fe6 100644 --- a/lib/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart @@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/saved_activity_analytics_dialog.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_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/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -52,17 +51,11 @@ class ActivityFinishedStatusMessage extends StatelessWidget { final courseParent = controller.room.courseParent; if (courseParent?.coursePlan == null) return; - final coursePlan = await CoursePlansRepo.get( - courseParent!.coursePlan!.uuid, - ); - final activityId = controller.room.activityPlan!.activityId; - final topicId = coursePlan.topicID(activityId); - if (topicId == null) { - throw L10n.of(context).activityNotFoundForCourse; - } - - await courseParent.finishCourseActivity(activityId, topicId); + await courseParent!.finishCourseActivity( + activityId, + controller.room.id, + ); } ActivitySummaryModel? get summary => controller.room.activitySummary; 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 9f1df636e..e82a5715c 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 @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; @@ -10,8 +11,11 @@ import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; 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/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -28,7 +32,7 @@ class ActivitySessionStartPage extends StatefulWidget { final String activityId; final bool isNew; final Room? room; - final String parentId; + final String? parentId; const ActivitySessionStartPage({ super.key, required this.activityId, @@ -44,6 +48,8 @@ class ActivitySessionStartPage extends StatefulWidget { class ActivitySessionStartController extends State { ActivityPlanModel? activity; + CoursePlanModel? course; + bool loading = true; Object? error; @@ -73,9 +79,11 @@ class ActivitySessionStartController extends State { Room? get room => widget.room; - Room? get parent => Matrix.of(context).client.getRoomById( - widget.parentId, - ); + Room? get parent => widget.parentId != null + ? Matrix.of(context).client.getRoomById( + widget.parentId!, + ) + : null; bool get isBotRoomMember => room?.getParticipants().any( @@ -136,6 +144,13 @@ class ActivitySessionStartController extends State { return _selectedRoleId == id; } + bool get canJoinExistingSession { + // if the activity session already exists, if there's no parent course, or if the parent course doesn't + // have the event where existing sessions are stored, joining an existing session is not possible + if (room != null || parent?.allCourseUserStates == null) return false; + return parent!.numOpenSessions(widget.activityId) > 0; + } + void toggleInstructions() { setState(() { showInstructions = !showInstructions; @@ -155,6 +170,10 @@ class ActivitySessionStartController extends State { error = null; }); + if (parent?.coursePlan != null) { + course = await CoursePlansRepo.get(parent!.coursePlan!.uuid); + } + final activities = await CourseActivityRepo.get( widget.activityId, [widget.activityId], @@ -172,14 +191,32 @@ class ActivitySessionStartController extends State { } } - Future onTap() async { + Future confirmRoleSelection() async { if (state != SessionState.selectedRole) return; if (room != null) { await showFutureLoadingDialog( context: context, - future: () => room!.joinActivity( - activity!.roles[_selectedRoleId!]!, - ), + future: () async { + await room!.joinActivity( + activity!.roles[_selectedRoleId!]!, + ); + + try { + await parent!.joinCourseActivity( + widget.activityId, + room!.id, + ); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + "activityId": widget.activityId, + "parentId": widget.parentId, + }, + ); + } + }, ); } else { final resp = await showFutureLoadingDialog( @@ -196,6 +233,43 @@ class ActivitySessionStartController extends State { } } + Future joinExistingSession() async { + if (!canJoinExistingSession) { + throw Exception("No existing session to join"); + } + + final sessionIds = parent!.openSessions(widget.activityId); + String? joinedSessionId; + for (final sessionId in sessionIds) { + try { + await parent!.client.joinRoom( + sessionId, + via: parent?.spaceChildren + .firstWhereOrNull( + (child) => child.roomId == sessionId, + ) + ?.via, + ); + joinedSessionId = sessionId; + break; + } catch (_) { + // try next session + continue; + } + } + + if (joinedSessionId == null) { + throw Exception("Failed to join any existing session"); + } + + final room = parent!.client.getRoomById(joinedSessionId); + if (room == null || room.membership != Membership.join) { + await parent!.client.waitForRoomInSync(joinedSessionId, join: true); + } + + return joinedSessionId; + } + Future pingCourse() async { if (room?.courseParent == null) { throw Exception("Activity is not part of a course"); 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 d39e1e61b..2cec592b4 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 @@ -150,7 +150,23 @@ class ActivitySessionStartView extends StatelessWidget { ), ElevatedButton( style: buttonStyle, - onPressed: null, + 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, @@ -221,7 +237,7 @@ class ActivitySessionStartView extends StatelessWidget { ElevatedButton( style: buttonStyle, onPressed: controller.enableButtons - ? controller.onTap + ? controller.confirmRoleSelection : null, child: Row( mainAxisAlignment: diff --git a/lib/pangea/activity_suggestions/activity_suggestion_card.dart b/lib/pangea/activity_suggestions/activity_suggestion_card.dart index bb97b58b4..46879ec81 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_card.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_card.dart @@ -14,6 +14,8 @@ class ActivitySuggestionCard extends StatelessWidget { final double? fontSizeSmall; final double? iconSize; + final int? openSessions; + const ActivitySuggestionCard({ super.key, required this.activity, @@ -22,6 +24,7 @@ class ActivitySuggestionCard extends StatelessWidget { this.fontSize, this.fontSizeSmall, this.iconSize, + this.openSessions, }); @override @@ -66,14 +69,16 @@ class ActivitySuggestionCard extends StatelessWidget { spacing: 8.0, children: [ if (activity.req.mode.isNotEmpty) - Padding( - padding: const EdgeInsets.all(4.0), - child: Text( - activity.req.mode, - style: fontSizeSmall != null - ? TextStyle(fontSize: fontSizeSmall) - : theme.textTheme.labelSmall, - overflow: TextOverflow.ellipsis, + Expanded( + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + activity.req.mode, + style: fontSizeSmall != null + ? TextStyle(fontSize: fontSizeSmall) + : theme.textTheme.labelSmall, + overflow: TextOverflow.ellipsis, + ), ), ), Padding( @@ -95,6 +100,26 @@ class ActivitySuggestionCard extends StatelessWidget { ], ), ), + if (openSessions != null && openSessions! > 0) + Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 4.0, + children: [ + Icon( + Icons.chat_bubble_outline, + size: iconSize ?? 12.0, + ), + Text( + "$openSessions", + style: fontSizeSmall != null + ? TextStyle(fontSize: fontSizeSmall) + : theme.textTheme.labelSmall, + ), + ], + ), + ), ], ), ], diff --git a/lib/pangea/chat_list/utils/get_chat_list_item_subtitle.dart b/lib/pangea/chat_list/utils/get_chat_list_item_subtitle.dart index cc115f790..e610fc91e 100644 --- a/lib/pangea/chat_list/utils/get_chat_list_item_subtitle.dart +++ b/lib/pangea/chat_list/utils/get_chat_list_item_subtitle.dart @@ -5,6 +5,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; +import 'package:fluffychat/pangea/course_chats/open_roles_indicator.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -56,12 +57,29 @@ class ChatListItemSubtitle extends StatelessWidget { @override Widget build(BuildContext context) { if (room.showActivityChatUI) { - return Text( - room.activityPlan!.learningObjective, - style: style, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ); + if (room.isHiddenActivityRoom) { + return Text( + room.activityPlan!.learningObjective, + style: style, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ); + } else if (!room.activityHasStarted) { + return OpenRolesIndicator( + totalSlots: room.activityPlan!.req.numberOfParticipants, + userIds: + room.activityRoles?.roles.values.map((r) => r.userId).toList() ?? + [], + space: room.courseParent, + ); + } else if (room.activityIsFinished) { + return Text( + L10n.of(context).activityDone, + style: style, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ); + } } final event = room.lastEvent; diff --git a/lib/pangea/course_chats/activity_template_chat_list_item.dart b/lib/pangea/course_chats/activity_template_chat_list_item.dart new file mode 100644 index 000000000..ced9accee --- /dev/null +++ b/lib/pangea/course_chats/activity_template_chat_list_item.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; +import 'package:fluffychat/pangea/course_chats/extended_space_rooms_chunk.dart'; +import 'package:fluffychat/pangea/course_chats/open_roles_indicator.dart'; +import 'package:fluffychat/widgets/avatar.dart'; + +class ActivityTemplateChatListItem extends StatelessWidget { + final Room space; + final Function(SpaceRoomsChunk) joinActivity; + final ActivityPlanModel activity; + final List sessions; + + const ActivityTemplateChatListItem({ + super.key, + required this.space, + required this.joinActivity, + required this.activity, + required this.sessions, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 1, + ), + child: Column( + spacing: 10.0, + mainAxisSize: MainAxisSize.min, + children: [ + Material( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + child: ListTile( + visualDensity: const VisualDensity(vertical: -0.5), + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + leading: ImageByUrl( + imageUrl: activity.imageURL, + width: Avatar.defaultSize, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 2, + ), + replacement: Avatar( + name: activity.title, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 2, + ), + ), + ), + trailing: Row( + spacing: 2.0, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.chat_bubble_outline, + size: 8.0, + ), + Text( + "${sessions.length}", + style: const TextStyle( + fontSize: 8.0, + ), + ), + ], + ), + title: Row( + children: [ + Expanded( + child: Text( + activity.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ...sessions.map( + (e) { + return Padding( + padding: const EdgeInsets.only( + top: 4.0, + bottom: 4.0, + right: 4.0, + left: 14.0, + ), + child: Row( + children: [ + Expanded( + child: OpenRolesIndicator( + totalSlots: activity.req.numberOfParticipants, + userIds: e.userIds, + space: space, + ), + ), + SizedBox( + height: 24.0, + width: 40.0, + child: ElevatedButton( + onPressed: () => joinActivity(e.chunk), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(0), + ), + child: Text( + L10n.of(context).join, + style: const TextStyle( + fontSize: 12, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pangea/course_chats/course_chats_page.dart b/lib/pangea/course_chats/course_chats_page.dart index 530dbd4ad..5c04452a5 100644 --- a/lib/pangea/course_chats/course_chats_page.dart +++ b/lib/pangea/course_chats/course_chats_page.dart @@ -9,14 +9,16 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart'; import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/course_chats/course_chats_view.dart'; +import 'package:fluffychat/pangea/course_chats/extended_space_rooms_chunk.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_topic_model.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart'; import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; @@ -26,6 +28,7 @@ import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart' import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/matrix.dart'; class CourseChats extends StatefulWidget { final Client client; @@ -54,7 +57,6 @@ class CourseChatsController extends State { bool isLoading = false; CoursePlanModel? course; - String? selectedTopicId; @override void initState() { @@ -62,7 +64,8 @@ class CourseChatsController extends State { // Listen for changes to the activeSpace's hierarchy, // and reload the hierarchy when they come through - _roomSubscription ??= widget.client.onSync.stream + _roomSubscription?.cancel(); + _roomSubscription = widget.client.onSync.stream .where(_hasHierarchyUpdate) .listen((update) => loadHierarchy(reload: true)); super.initState(); @@ -74,6 +77,11 @@ class CourseChatsController extends State { // via the navigation rail, so this accounts for that super.didUpdateWidget(oldWidget); if (oldWidget.roomId != widget.roomId) { + _roomSubscription?.cancel(); + _roomSubscription = widget.client.onSync.stream + .where(_hasHierarchyUpdate) + .listen((update) => loadHierarchy(reload: true)); + discoveredChildren = null; _nextBatch = null; noMoreRooms = false; @@ -94,38 +102,77 @@ class CourseChatsController extends State { }); } - void setSelectedTopicId(String topicID) { - setState(() { - selectedTopicId = topicID; - }); - } + Set get childrenIds => + room?.spaceChildren.map((c) => c.roomId).whereType().toSet() ?? + {}; - int get _selectedTopicIndex => - course?.loadedTopics.indexWhere((t) => t.uuid == selectedTopicId) ?? -1; + List get joinedRooms => Matrix.of(context) + .client + .rooms + .where((room) => childrenIds.contains(room.id)) + .where((room) => !room.isHiddenRoom) + .toList(); - bool get canMoveLeft => _selectedTopicIndex > 0; - bool get canMoveRight { - if (course == null) return false; - final endIndex = room?.ownCurrentTopicIndex(course!) ?? - (course!.loadedTopics.length - 1); - return _selectedTopicIndex < endIndex; - } + List joinedActivities() => + joinedRooms.where((r) => r.isActivitySession).toList(); - void moveLeft() { - if (canMoveLeft) { - setSelectedTopicId(course!.loadedTopics[_selectedTopicIndex - 1].uuid); + List get discoveredGroupChats => (discoveredChildren ?? []) + .where( + (chunk) => + chunk.roomType == null || + !chunk.roomType!.startsWith(PangeaRoomTypes.activitySession), + ) + .toList(); + + Map> discoveredActivities() { + if (discoveredChildren == null) return {}; + + final courseStates = room?.allCourseUserStates ?? {}; + final Map> roomsToUsers = {}; + if (courseStates.isNotEmpty) { + for (final state in courseStates.values) { + final userID = state.userID; + for (final roomId in state.joinedActivityRooms) { + roomsToUsers[roomId] ??= []; + roomsToUsers[roomId]!.add(userID); + } + } } - } - void moveRight() { - if (canMoveRight) { - setSelectedTopicId(course!.loadedTopics[_selectedTopicIndex + 1].uuid); - } - } + final Map> sessionsMap = + {}; - CourseTopicModel? get selectedTopic => course?.loadedTopics.firstWhereOrNull( - (topic) => topic.uuid == selectedTopicId, + for (final chunk in discoveredChildren!) { + if (chunk.roomType?.startsWith(PangeaRoomTypes.activitySession) != true) { + continue; + } + final activityId = chunk.roomType!.split(":").last; + final activity = course?.activityById(activityId); + if (activity == null) { + continue; + } + + final users = roomsToUsers[chunk.roomId]; + if (users != null && activity.req.numberOfParticipants <= users.length) { + // Don't show full activities + continue; + } + + sessionsMap[activity] ??= []; + sessionsMap[activity]!.add( + ExtendedSpaceRoomsChunk( + chunk: chunk, + activityId: activityId, + userIds: users ?? [], + ), ); + } + + return sessionsMap; + } + + List get joinedChats => + joinedRooms.where((room) => !room.isActivitySession).toList(); Future _joinDefaultChats() async { if (discoveredChildren == null) return; @@ -623,7 +670,7 @@ class CourseChatsController extends State { future: room.isSpace ? room.leaveSpace : room.leave, ); if (mounted && !resp.isError) { - context.go("/rooms"); + context.go("/rooms/spaces/${widget.roomId}/details"); } return; @@ -711,17 +758,56 @@ class CourseChatsController extends State { /// Used to filter out sync updates with hierarchy updates for the active /// space so that the view can be auto-reloaded in the room subscription bool _hasHierarchyUpdate(SyncUpdate update) { - final joinTimeline = update.rooms?.join?[widget.roomId]?.timeline; - final leaveTimeline = update.rooms?.leave?[widget.roomId]?.timeline; + final joinUpdate = update.rooms?.join; + final leaveUpdate = update.rooms?.leave; + if (joinUpdate == null && leaveUpdate == null) return false; + + final joinedRooms = joinUpdate?.entries + .where( + (e) => childrenIds.contains(e.key), + ) + .map((e) => e.value.timeline?.events) + .whereType>(); + + final leftRooms = leaveUpdate?.entries + .where( + (e) => childrenIds.contains(e.key), + ) + .map((e) => e.value.timeline?.events) + .whereType>(); + + final bool hasJoinedRoom = joinedRooms?.any( + (events) => events.any( + (e) => + e.senderId == widget.client.userID && + e.type == EventTypes.RoomMember, + ), + ) ?? + false; + + final bool hasLeftRoom = leftRooms?.any( + (events) => events.any( + (e) => + e.senderId == widget.client.userID && + e.type == EventTypes.RoomMember, + ), + ) ?? + false; + + if (hasJoinedRoom || hasLeftRoom) { + return true; + } + + final joinTimeline = joinUpdate?[widget.roomId]?.timeline?.events; + final leaveTimeline = leaveUpdate?[widget.roomId]?.timeline?.events; if (joinTimeline == null && leaveTimeline == null) return false; - final bool hasJoinUpdate = joinTimeline?.events?.any( - (event) => event.type == EventTypes.SpaceChild, - ) ?? - false; - final bool hasLeaveUpdate = leaveTimeline?.events?.any( - (event) => event.type == EventTypes.SpaceChild, - ) ?? - false; + + final bool hasJoinUpdate = joinTimeline!.any( + (event) => event.type == EventTypes.SpaceChild, + ); + final bool hasLeaveUpdate = leaveTimeline!.any( + (event) => event.type == EventTypes.SpaceChild, + ); return hasJoinUpdate || hasLeaveUpdate; } diff --git a/lib/pangea/course_chats/course_chats_view.dart b/lib/pangea/course_chats/course_chats_view.dart index 67833202b..52283daa2 100644 --- a/lib/pangea/course_chats/course_chats_view.dart +++ b/lib/pangea/course_chats/course_chats_view.dart @@ -9,13 +9,11 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; -import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart'; +import 'package:fluffychat/pangea/course_chats/activity_template_chat_list_item.dart'; import 'package:fluffychat/pangea/course_chats/course_chats_page.dart'; import 'package:fluffychat/pangea/course_chats/unjoined_chat_list_item.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; -import 'package:fluffychat/pangea/course_plans/course_topic_model.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/space_analytics/analytics_request_indicator.dart'; import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart'; import 'package:fluffychat/utils/stream_extension.dart'; @@ -41,63 +39,19 @@ class CourseChatsView extends StatelessWidget { return CoursePlanBuilder( courseId: room.coursePlan?.uuid, - onLoaded: (course) { - controller.setCourse(course); - final topic = room.ownCurrentTopic(course); - if (topic != null) controller.setSelectedTopicId(topic.uuid); - }, + onLoaded: controller.setCourse, builder: (context, courseController) { - final CourseTopicModel? topic = controller.selectedTopic; - final List activityIds = topic?.activityIds ?? []; - return StreamBuilder( stream: room.client.onSync.stream .where((s) => s.hasRoomUpdate) .rateLimit(const Duration(seconds: 1)), builder: (context, snapshot) { - final childrenIds = room.spaceChildren - .map((c) => c.roomId) - .whereType() - .toSet(); + final joinedChats = controller.joinedChats; + final joinedSessions = controller.joinedActivities(); - final joinedChats = []; - final joinedSessions = []; - final joinedRooms = room.client.rooms - .where((room) => childrenIds.remove(room.id)) - .where((room) => !room.isHiddenRoom) - .toList(); - - for (final joinedRoom in joinedRooms) { - if (joinedRoom.isActivitySession) { - String? activityId = joinedRoom.activityPlan?.activityId; - if (activityId == null && joinedRoom.isActivityRoomType) { - activityId = joinedRoom.roomType!.split(":").last; - } - - if (topic == null || activityIds.contains(activityId)) { - joinedSessions.add(joinedRoom); - } - } else { - joinedChats.add(joinedRoom); - } - } - - final discoveredGroupChats = []; - final discoveredSessions = []; - final discoveredChildren = - controller.discoveredChildren ?? []; - - for (final child in discoveredChildren) { - final roomType = child.roomType; - if (roomType?.startsWith(PangeaRoomTypes.activitySession) == - true) { - if (activityIds.contains(roomType!.split(":").last)) { - discoveredSessions.add(child); - } - } else { - discoveredGroupChats.add(child); - } - } + final discoveredGroupChats = controller.discoveredGroupChats; + final discoveredSessions = + controller.discoveredActivities().entries.toList(); final isColumnMode = FluffyThemes.isColumnMode(context); return Padding( @@ -113,7 +67,7 @@ class CourseChatsView extends StatelessWidget { joinedSessions.length + discoveredGroupChats.length + discoveredSessions.length + - 6, + 7, itemBuilder: (context, i) { // courses chats title if (i == 0) { @@ -178,92 +132,6 @@ class CourseChatsView extends StatelessWidget { } i -= discoveredGroupChats.length; - if (i == 0) { - if (room.coursePlan == null || - (courseController.course == null && - !courseController.loading)) { - return const SizedBox(); - } - - return Padding( - padding: const EdgeInsets.only( - top: 20.0, - bottom: 12.0, - ), - child: courseController.loading - ? LinearProgressIndicator( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - ) - : topic != null - ? Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - MouseRegion( - cursor: controller.canMoveLeft - ? SystemMouseCursors.click - : SystemMouseCursors.basic, - child: GestureDetector( - onTap: controller.canMoveLeft - ? controller.moveLeft - : null, - child: Opacity( - opacity: controller.canMoveLeft - ? 1.0 - : 0.3, - child: const Icon( - Icons.arrow_left, - size: 24.0, - ), - ), - ), - ), - if (topic.location != null) - Row( - spacing: 6.0, - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.location_on, - size: 24.0, - ), - Text( - topic.location!, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - MouseRegion( - cursor: controller.canMoveRight - ? SystemMouseCursors.click - : SystemMouseCursors.basic, - child: GestureDetector( - onTap: controller.canMoveRight - ? controller.moveRight - : null, - child: Opacity( - opacity: controller.canMoveRight - ? 1.0 - : 0.3, - child: const Icon( - Icons.arrow_right, - size: 24.0, - ), - ), - ), - ), - ], - ) - : const SizedBox(), - ); - } - i--; - if (i == 0) { return joinedSessions.isEmpty && discoveredSessions.isEmpty ? ListTile( @@ -279,6 +147,23 @@ class CourseChatsView extends StatelessWidget { } i--; + if (i == 0) { + return joinedSessions.isEmpty + ? const SizedBox() + : Padding( + padding: const EdgeInsets.only( + top: 20.0, + bottom: 4.0, + ), + child: Text( + L10n.of(context).myActivities, + style: const TextStyle(fontSize: 12.0), + textAlign: TextAlign.center, + ), + ); + } + i--; + // joined activity sessions if (i < joinedSessions.length) { final joinedRoom = joinedSessions[i]; @@ -290,17 +175,58 @@ class CourseChatsView extends StatelessWidget { context, ), activeChat: controller.widget.activeChat == joinedRoom.id, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 2, + ), + trailing: joinedRoom.activityIsFinished + ? SizedBox( + height: 24.0, + width: 54.0, + child: ElevatedButton( + onPressed: () => + controller.onChatTap(joinedRoom), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(0), + ), + child: Text( + L10n.of(context).results, + style: const TextStyle( + fontSize: 12, + ), + ), + ), + ) + : null, ); } i -= joinedSessions.length; + if (i == 0) { + return discoveredSessions.isEmpty + ? const SizedBox() + : Padding( + padding: const EdgeInsets.only( + top: 20.0, + bottom: 4.0, + ), + child: Text( + L10n.of(context).openToJoin, + style: const TextStyle(fontSize: 12.0), + textAlign: TextAlign.center, + ), + ); + } + i--; + // unjoined activity sessions if (i < discoveredSessions.length) { - return UnjoinedChatListItem( - chunk: discoveredSessions[i], - onTap: () => controller.joinChildRoom( - discoveredSessions[i], - ), + final activity = discoveredSessions[i].key; + final sessions = discoveredSessions[i].value; + return ActivityTemplateChatListItem( + space: room, + joinActivity: controller.joinChildRoom, + activity: activity, + sessions: sessions, ); } i -= discoveredSessions.length; diff --git a/lib/pangea/course_chats/extended_space_rooms_chunk.dart b/lib/pangea/course_chats/extended_space_rooms_chunk.dart new file mode 100644 index 000000000..8006f4f3f --- /dev/null +++ b/lib/pangea/course_chats/extended_space_rooms_chunk.dart @@ -0,0 +1,13 @@ +import 'package:matrix/matrix.dart'; + +class ExtendedSpaceRoomsChunk { + final SpaceRoomsChunk chunk; + final String activityId; + final List userIds; + + ExtendedSpaceRoomsChunk({ + required this.chunk, + required this.activityId, + required this.userIds, + }); +} diff --git a/lib/pangea/course_chats/open_roles_indicator.dart b/lib/pangea/course_chats/open_roles_indicator.dart new file mode 100644 index 000000000..e7cf25e9a --- /dev/null +++ b/lib/pangea/course_chats/open_roles_indicator.dart @@ -0,0 +1,62 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/widgets/avatar.dart'; + +class OpenRolesIndicator extends StatelessWidget { + final int totalSlots; + final List userIds; + final Room? space; + + const OpenRolesIndicator({ + super.key, + required this.totalSlots, + required this.userIds, + this.space, + }); + + @override + Widget build(BuildContext context) { + final remainingSlots = max( + 0, + totalSlots - userIds.length, + ); + + final spaceParticipants = space?.getParticipants() ?? []; + + return Row( + spacing: 2.0, + children: [ + ...userIds.map((u) { + final user = spaceParticipants.firstWhereOrNull( + (p) => p.id == u, + ); + return Avatar( + mxContent: user?.avatarUrl, + userId: user?.calcDisplayname() ?? u.localpart ?? u, + size: 16, + ); + }), + ...List.generate(remainingSlots, (_) { + return Stack( + alignment: Alignment.center, + children: [ + CircleAvatar( + radius: 8, + backgroundColor: Theme.of(context).colorScheme.primary, + ), + CircleAvatar( + radius: 7, + backgroundColor: Theme.of(context).colorScheme.surface, + ), + ], + ); + }), + ], + ); + } +} diff --git a/lib/pangea/course_plans/course_plan_model.dart b/lib/pangea/course_plans/course_plan_model.dart index 77cff337f..de47367bf 100644 --- a/lib/pangea/course_plans/course_plan_model.dart +++ b/lib/pangea/course_plans/course_plan_model.dart @@ -1,3 +1,4 @@ +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'; import 'package:fluffychat/pangea/course_plans/course_topic_model.dart'; @@ -57,6 +58,16 @@ class CoursePlanModel { int get totalActivities => loadedTopics.fold(0, (sum, topic) => sum + topic.activityIds.length); + ActivityPlanModel? activityById(String activityID) { + for (final topic in loadedTopics) { + final activity = topic.activityById(activityID); + if (activity != null) { + return activity; + } + } + return null; + } + /// Deserialize from JSON factory CoursePlanModel.fromJson(Map json) { return CoursePlanModel( diff --git a/lib/pangea/course_plans/course_plan_room_extension.dart b/lib/pangea/course_plans/course_plan_room_extension.dart index ab4a3649a..acd2645e6 100644 --- a/lib/pangea/course_plans/course_plan_room_extension.dart +++ b/lib/pangea/course_plans/course_plan_room_extension.dart @@ -1,3 +1,4 @@ +import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; @@ -28,11 +29,52 @@ extension CoursePlanRoomExtension on Room { userID, ); if (event == null) return null; - return CourseUserState.fromJson(event.content); + + try { + return CourseUserState.fromJson(event.content); + } catch (e) { + return null; + } } CourseUserState? get _ownCourseState => _courseUserState(client.userID!); + Map get allCourseUserStates { + final content = states[PangeaEventTypes.courseUser]; + if (content == null || content.isEmpty) return {}; + return Map.fromEntries( + content.entries.map( + (e) { + try { + return MapEntry( + e.key, + CourseUserState.fromJson(e.value.content), + ); + } catch (e) { + return null; + } + }, + ).whereType>(), + ); + } + + Set openSessions(String activityId) { + final Set sessions = {}; + final Set childIds = + spaceChildren.map((child) => child.roomId).whereType().toSet(); + + for (final userState in allCourseUserStates.values) { + final activitySessions = userState.joinedActivities[activityId]?.toSet(); + if (activitySessions == null) continue; + sessions.addAll( + activitySessions.intersection(childIds), + ); + } + return sessions; + } + + int numOpenSessions(String activityId) => openSessions(activityId).length; + bool hasCompletedActivity( String userID, String activityID, @@ -44,14 +86,14 @@ extension CoursePlanRoomExtension on Room { bool _hasCompletedTopic( String userID, - String topicID, + CourseTopicModel topic, CoursePlanModel course, ) { final state = _courseUserState(userID); if (state == null) return false; final topicIndex = course.loadedTopics.indexWhere( - (t) => t.uuid == topicID, + (t) => t.uuid == topic.uuid, ); if (topicIndex == -1) { @@ -61,7 +103,7 @@ extension CoursePlanRoomExtension on Room { final activityIds = course.loadedTopics[topicIndex].loadedActivities .map((a) => a.activityId) .toList(); - return state.completedActivities(topicID).toSet().containsAll(activityIds); + return state.completedActivities.toSet().containsAll(activityIds); } CourseTopicModel? currentTopic( @@ -69,10 +111,9 @@ extension CoursePlanRoomExtension on Room { CoursePlanModel course, ) { if (coursePlan == null) return null; - final topicIDs = course.loadedTopics.map((t) => t.uuid).toList(); - if (topicIDs.isEmpty) return null; + if (course.loadedTopics.isEmpty) return null; - final index = topicIDs.indexWhere( + final index = course.loadedTopics.indexWhere( (t) => !_hasCompletedTopic(userID, t, course), ); @@ -87,10 +128,9 @@ extension CoursePlanRoomExtension on Room { CoursePlanModel course, ) { if (coursePlan == null) return -1; - final topicIDs = course.loadedTopics.map((t) => t.uuid).toList(); - if (topicIDs.isEmpty) return -1; + if (course.loadedTopics.isEmpty) return -1; - final index = topicIDs.indexWhere( + final index = course.loadedTopics.indexWhere( (t) => !_hasCompletedTopic(userID, t, course), ); @@ -104,7 +144,9 @@ extension CoursePlanRoomExtension on Room { for (final child in spaceChildren) { if (child.roomId == null) continue; final room = client.getRoomById(child.roomId!); - if (room?.activityId == activityId && !room!.isHiddenActivityRoom) { + if (room?.membership == Membership.join && + room?.activityId == activityId && + !room!.isHiddenActivityRoom) { return room.id; } } @@ -130,16 +172,36 @@ extension CoursePlanRoomExtension on Room { return topicUserMap; } - Future finishCourseActivity( + Future joinCourseActivity( String activityID, - String topicID, + String roomID, ) async { CourseUserState? state = _ownCourseState; state ??= CourseUserState( userID: client.userID!, completedActivities: {}, + joinActivities: {}, ); - state.completeActivity(activityID, topicID); + state.joinActivity(activityID, roomID); + await client.setRoomStateWithKey( + id, + PangeaEventTypes.courseUser, + client.userID!, + state.toJson(), + ); + } + + Future finishCourseActivity( + String activityID, + String roomID, + ) async { + CourseUserState? state = _ownCourseState; + state ??= CourseUserState( + userID: client.userID!, + completedActivities: {}, + joinActivities: {}, + ); + state.completeActivity(activityID, roomID); await client.setRoomStateWithKey( id, PangeaEventTypes.courseUser, @@ -156,7 +218,7 @@ extension CoursePlanRoomExtension on Room { creationContent: { 'type': "${PangeaRoomTypes.activitySession}:${activity.activityId}", }, - visibility: Visibility.private, + visibility: sdk.Visibility.private, name: activity.title, initialState: [ StateEvent( @@ -198,6 +260,11 @@ extension CoursePlanRoomExtension on Room { if (pangeaSpaceParents.isEmpty) { await client.waitForRoomInSync(roomID); } + + await joinCourseActivity( + activity.activityId, + roomID, + ); return roomID; } } diff --git a/lib/pangea/course_plans/course_topic_model.dart b/lib/pangea/course_plans/course_topic_model.dart index cc9dd3467..45c5de8c9 100644 --- a/lib/pangea/course_plans/course_topic_model.dart +++ b/lib/pangea/course_plans/course_topic_model.dart @@ -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_activity_repo.dart'; @@ -56,6 +58,11 @@ class CourseTopicModel { Future> fetchActivities() => CourseActivityRepo.get(uuid, activityIds); + ActivityPlanModel? activityById(String activityId) => + loadedActivities.firstWhereOrNull( + (activity) => activity.activityId == activityId, + ); + /// Deserialize from JSON factory CourseTopicModel.fromJson(Map json) { return CourseTopicModel( diff --git a/lib/pangea/course_plans/course_user_event.dart b/lib/pangea/course_plans/course_user_event.dart index 4a4c57a6b..ecc1e633a 100644 --- a/lib/pangea/course_plans/course_user_event.dart +++ b/lib/pangea/course_plans/course_user_event.dart @@ -1,46 +1,67 @@ class CourseUserState { final String userID; + + // Map of activityIds to list of roomIds final Map> _completedActivities; + final Map> _joinedActivities; CourseUserState({ required this.userID, required Map> completedActivities, - }) : _completedActivities = completedActivities; + required Map> joinActivities, + }) : _completedActivities = completedActivities, + _joinedActivities = joinActivities; + + void joinActivity( + String activityID, + String roomID, + ) { + _joinedActivities[activityID] ??= []; + _joinedActivities[activityID]!.add(roomID); + } void completeActivity( String activityID, - String topicID, + String roomID, ) { - _completedActivities[topicID] ??= []; - if (!_completedActivities[topicID]!.contains(activityID)) { - _completedActivities[topicID]!.add(activityID); - } + _completedActivities[activityID] ??= []; + _completedActivities[activityID]!.add(roomID); } - List completedActivities(String topicID) { - return _completedActivities[topicID] ?? []; - } + Map> get joinedActivities => _joinedActivities; + + List get completedActivities => _completedActivities.keys.toList(); + List get joinedActivityRooms => + _joinedActivities.values.expand((e) => e).toList(); bool hasCompletedActivity( String activityID, ) { - return _completedActivities.values.any( - (activities) => activities.contains(activityID), - ); + return _completedActivities.containsKey(activityID); } factory CourseUserState.fromJson(Map json) { - final Map> activities = {}; - final activityEntry = - (json['comp_act_by_topic'] as Map?) ?? {}; + final activityEntry = json['comp_act_by_topic']; + final joinEntry = json['join_act_by_topic']; - for (final entry in activityEntry.entries) { - activities[entry.key] = List.from(entry.value); + final Map> activityMap = {}; + if (activityEntry != null) { + activityEntry.forEach((key, value) { + activityMap[key] = List.from(value); + }); + } + + final Map> joinMap = {}; + if (joinEntry != null) { + joinEntry.forEach((key, value) { + joinMap[key] = List.from(value); + }); } return CourseUserState( userID: json['user_id'], - completedActivities: activities, + completedActivities: activityMap, + joinActivities: joinMap, ); } @@ -48,6 +69,7 @@ class CourseUserState { return { 'user_id': userID, 'comp_act_by_topic': _completedActivities, + 'join_act_by_topic': _joinedActivities, }; } } diff --git a/lib/pangea/course_plans/map_clipper.dart b/lib/pangea/course_plans/map_clipper.dart new file mode 100644 index 000000000..a041e8043 --- /dev/null +++ b/lib/pangea/course_plans/map_clipper.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class MapClipper extends CustomClipper { + @override + Path getClip(Size size) { + final double w = size.width; + final double h = size.height; + + final Path path = Path(); + path.moveTo(0, 0); + path.lineTo(0, h * 0.15); + path.lineTo(w * 0.33, 0); + path.lineTo(w * 0.66, h * 0.15); + path.lineTo(w, 0); + path.lineTo(w, h * 0.85); + path.lineTo(w * 0.66, h); + path.lineTo(w * 0.33, h * 0.85); + path.lineTo(0, h); + + path.close(); + return path; + } + + @override + bool shouldReclip(CustomClipper oldClipper) => false; +} diff --git a/lib/pangea/course_settings/course_settings.dart b/lib/pangea/course_settings/course_settings.dart index 2cc0b27ec..4e3c8ac78 100644 --- a/lib/pangea/course_settings/course_settings.dart +++ b/lib/pangea/course_settings/course_settings.dart @@ -182,6 +182,8 @@ class CourseSettings extends StatelessWidget { activityId, ); + final activity = topic.loadedActivities[index]; + return Padding( padding: const EdgeInsets.only(right: 24.0), child: MouseRegion( @@ -195,13 +197,15 @@ class CourseSettings extends StatelessWidget { child: Stack( children: [ ActivitySuggestionCard( - activity: topic.loadedActivities[index], + activity: activity, width: isColumnMode ? 160.0 : 120.0, height: isColumnMode ? 280.0 : 200.0, fontSize: isColumnMode ? 20.0 : 12.0, fontSizeSmall: isColumnMode ? 12.0 : 8.0, iconSize: isColumnMode ? 12.0 : 8.0, + openSessions: + room.numOpenSessions(activityId), ), if (complete) Container( diff --git a/lib/widgets/navigation_rail.dart b/lib/widgets/navigation_rail.dart index 322d83ec5..8374a15ee 100644 --- a/lib/widgets/navigation_rail.dart +++ b/lib/widgets/navigation_rail.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat_list/navi_rail_item.dart'; import 'package:fluffychat/pangea/chat_list/utils/chat_list_handle_space_tap.dart'; +import 'package:fluffychat/pangea/course_plans/map_clipper.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/stream_extension.dart'; @@ -177,6 +177,8 @@ class SpacesNavigationRail extends StatelessWidget { toolTip: displayname, isSelected: activeSpaceId == space.id, // #Pangea + backgroundColor: Colors.transparent, + borderRadius: BorderRadius.circular(0), // onTap: () => onGoToSpaceId(rootSpaces[i].id), onTap: () { final room = client.getRoomById(rootSpaces[i].id); @@ -194,20 +196,32 @@ class SpacesNavigationRail extends StatelessWidget { // Pangea# unreadBadgeFilter: (room) => spaceChildrenIds.contains(room.id), - icon: Avatar( - mxContent: rootSpaces[i].avatar, - name: displayname, - border: BorderSide( - width: 1, - color: Theme.of(context).dividerColor, + // #Pangea + // icon: Avatar( + // mxContent: rootSpaces[i].avatar, + // name: displayname, + // border: BorderSide( + // width: 1, + // color: Theme.of(context).dividerColor, + // ), + // borderRadius: BorderRadius.circular( + // AppConfig.borderRadius / 2, + // ), + // ), + icon: ClipPath( + clipper: MapClipper(), + child: Avatar( + mxContent: rootSpaces[i].avatar, + name: displayname, + border: BorderSide( + width: 1, + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.circular(0), + size: width - (isColumnMode ? 32.0 : 24.0), ), - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 2, - ), - // #Pangea - size: width - (isColumnMode ? 32.0 : 24.0), - // Pangea# ), + // Pangea# ); }, ),