3915 course chat view updates (#3919)
This commit is contained in:
parent
08eb8fe19f
commit
c04466fdca
21 changed files with 805 additions and 283 deletions
|
|
@ -5225,5 +5225,9 @@
|
||||||
"startNewSession": "Start new session",
|
"startNewSession": "Start new session",
|
||||||
"joinOpenSession": "Join open session",
|
"joinOpenSession": "Join open session",
|
||||||
"less": "less",
|
"less": "less",
|
||||||
"activityNotFound": "Activity not found"
|
"activityNotFound": "Activity not found",
|
||||||
|
"myActivities": "My activities",
|
||||||
|
"openToJoin": "Open to join",
|
||||||
|
"results": "Results",
|
||||||
|
"activityDone": "Activity Done!"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2247,13 +2247,11 @@ class ChatController extends State<ChatPageWithRoom>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.isActivitySession == true &&
|
if (room.isActivitySession == true && !room.activityHasStarted) {
|
||||||
!room.activityHasStarted &&
|
|
||||||
room.courseParent != null) {
|
|
||||||
return ActivitySessionStartPage(
|
return ActivitySessionStartPage(
|
||||||
activityId: room.activityId!,
|
activityId: room.activityId!,
|
||||||
room: room,
|
room: room,
|
||||||
parentId: room.courseParent!.id,
|
parentId: room.courseParent?.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Pangea#
|
// Pangea#
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ class ChatListItem extends StatelessWidget {
|
||||||
final void Function()? onForget;
|
final void Function()? onForget;
|
||||||
final void Function() onTap;
|
final void Function() onTap;
|
||||||
final String? filter;
|
final String? filter;
|
||||||
|
// #Pangea
|
||||||
|
final BorderRadius? borderRadius;
|
||||||
|
final Widget? trailing;
|
||||||
|
// Pangea#
|
||||||
|
|
||||||
const ChatListItem(
|
const ChatListItem(
|
||||||
this.room, {
|
this.room, {
|
||||||
|
|
@ -35,6 +39,10 @@ class ChatListItem extends StatelessWidget {
|
||||||
this.filter,
|
this.filter,
|
||||||
this.space,
|
this.space,
|
||||||
super.key,
|
super.key,
|
||||||
|
// #Pangea
|
||||||
|
this.borderRadius,
|
||||||
|
this.trailing,
|
||||||
|
// Pangea#
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<bool> archiveAction(BuildContext context) async {
|
Future<bool> archiveAction(BuildContext context) async {
|
||||||
|
|
@ -174,11 +182,19 @@ class ChatListItem extends StatelessWidget {
|
||||||
color: backgroundColor ??
|
color: backgroundColor ??
|
||||||
theme.colorScheme.surface,
|
theme.colorScheme.surface,
|
||||||
),
|
),
|
||||||
borderRadius: room.isSpace
|
// #Pangea
|
||||||
? BorderRadius.circular(
|
// borderRadius: room.isSpace
|
||||||
AppConfig.borderRadius / 4,
|
// ? BorderRadius.circular(
|
||||||
)
|
// AppConfig.borderRadius / 4,
|
||||||
: null,
|
// )
|
||||||
|
// : null,
|
||||||
|
borderRadius: borderRadius ??
|
||||||
|
(room.isSpace
|
||||||
|
? BorderRadius.circular(
|
||||||
|
AppConfig.borderRadius / 4,
|
||||||
|
)
|
||||||
|
: null),
|
||||||
|
// Pangea#
|
||||||
mxContent: room.avatar,
|
mxContent: room.avatar,
|
||||||
size: space != null
|
size: space != null
|
||||||
? Avatar.defaultSize * 0.75
|
? Avatar.defaultSize * 0.75
|
||||||
|
|
@ -428,12 +444,21 @@ class ChatListItem extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
trailing: onForget == null
|
// #Pangea
|
||||||
? null
|
// trailing: onForget == null
|
||||||
: IconButton(
|
// ? null
|
||||||
icon: const Icon(Icons.delete_outlined),
|
// : IconButton(
|
||||||
onPressed: onForget,
|
// icon: const Icon(Icons.delete_outlined),
|
||||||
),
|
// onPressed: onForget,
|
||||||
|
// ),
|
||||||
|
trailing: trailing ??
|
||||||
|
(onForget == null
|
||||||
|
? null
|
||||||
|
: IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outlined),
|
||||||
|
onPressed: onForget,
|
||||||
|
)),
|
||||||
|
// Pangea#
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ class NaviRailItem extends StatelessWidget {
|
||||||
final bool Function(Room)? unreadBadgeFilter;
|
final bool Function(Room)? unreadBadgeFilter;
|
||||||
// #Pangea
|
// #Pangea
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
final BorderRadius? borderRadius;
|
||||||
// Pangea#
|
// Pangea#
|
||||||
|
|
||||||
const NaviRailItem({
|
const NaviRailItem({
|
||||||
|
|
@ -27,6 +28,7 @@ class NaviRailItem extends StatelessWidget {
|
||||||
this.unreadBadgeFilter,
|
this.unreadBadgeFilter,
|
||||||
// #Pangea
|
// #Pangea
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
|
this.borderRadius,
|
||||||
// Pangea#
|
// Pangea#
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
@ -36,7 +38,7 @@ class NaviRailItem extends StatelessWidget {
|
||||||
|
|
||||||
// #Pangea
|
// #Pangea
|
||||||
// final borderRadius = BorderRadius.circular(AppConfig.borderRadius);
|
// 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 isColumnMode = FluffyThemes.isColumnMode(context);
|
||||||
final width = isColumnMode
|
final width = isColumnMode
|
||||||
|
|
|
||||||
|
|
@ -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_sessions/activity_session_chat/saved_activity_analytics_dialog.dart';
|
||||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_model.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_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/future_loading_dialog.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
||||||
|
|
@ -52,17 +51,11 @@ class ActivityFinishedStatusMessage extends StatelessWidget {
|
||||||
|
|
||||||
final courseParent = controller.room.courseParent;
|
final courseParent = controller.room.courseParent;
|
||||||
if (courseParent?.coursePlan == null) return;
|
if (courseParent?.coursePlan == null) return;
|
||||||
final coursePlan = await CoursePlansRepo.get(
|
|
||||||
courseParent!.coursePlan!.uuid,
|
|
||||||
);
|
|
||||||
|
|
||||||
final activityId = controller.room.activityPlan!.activityId;
|
final activityId = controller.room.activityPlan!.activityId;
|
||||||
final topicId = coursePlan.topicID(activityId);
|
await courseParent!.finishCourseActivity(
|
||||||
if (topicId == null) {
|
activityId,
|
||||||
throw L10n.of(context).activityNotFoundForCourse;
|
controller.room.id,
|
||||||
}
|
);
|
||||||
|
|
||||||
await courseParent.finishCourseActivity(activityId, topicId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ActivitySummaryModel? get summary => controller.room.activitySummary;
|
ActivitySummaryModel? get summary => controller.room.activitySummary;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:matrix/matrix.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_room_extension.dart';
|
||||||
import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.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/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_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_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/pangea/extensions/pangea_room_extension.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||||
|
|
@ -28,7 +32,7 @@ class ActivitySessionStartPage extends StatefulWidget {
|
||||||
final String activityId;
|
final String activityId;
|
||||||
final bool isNew;
|
final bool isNew;
|
||||||
final Room? room;
|
final Room? room;
|
||||||
final String parentId;
|
final String? parentId;
|
||||||
const ActivitySessionStartPage({
|
const ActivitySessionStartPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.activityId,
|
required this.activityId,
|
||||||
|
|
@ -44,6 +48,8 @@ class ActivitySessionStartPage extends StatefulWidget {
|
||||||
|
|
||||||
class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
||||||
ActivityPlanModel? activity;
|
ActivityPlanModel? activity;
|
||||||
|
CoursePlanModel? course;
|
||||||
|
|
||||||
bool loading = true;
|
bool loading = true;
|
||||||
Object? error;
|
Object? error;
|
||||||
|
|
||||||
|
|
@ -73,9 +79,11 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
||||||
|
|
||||||
Room? get room => widget.room;
|
Room? get room => widget.room;
|
||||||
|
|
||||||
Room? get parent => Matrix.of(context).client.getRoomById(
|
Room? get parent => widget.parentId != null
|
||||||
widget.parentId,
|
? Matrix.of(context).client.getRoomById(
|
||||||
);
|
widget.parentId!,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
bool get isBotRoomMember =>
|
bool get isBotRoomMember =>
|
||||||
room?.getParticipants().any(
|
room?.getParticipants().any(
|
||||||
|
|
@ -136,6 +144,13 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
||||||
return _selectedRoleId == id;
|
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() {
|
void toggleInstructions() {
|
||||||
setState(() {
|
setState(() {
|
||||||
showInstructions = !showInstructions;
|
showInstructions = !showInstructions;
|
||||||
|
|
@ -155,6 +170,10 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
||||||
error = null;
|
error = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (parent?.coursePlan != null) {
|
||||||
|
course = await CoursePlansRepo.get(parent!.coursePlan!.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
final activities = await CourseActivityRepo.get(
|
final activities = await CourseActivityRepo.get(
|
||||||
widget.activityId,
|
widget.activityId,
|
||||||
[widget.activityId],
|
[widget.activityId],
|
||||||
|
|
@ -172,14 +191,32 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onTap() async {
|
Future<void> confirmRoleSelection() async {
|
||||||
if (state != SessionState.selectedRole) return;
|
if (state != SessionState.selectedRole) return;
|
||||||
if (room != null) {
|
if (room != null) {
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () => room!.joinActivity(
|
future: () async {
|
||||||
activity!.roles[_selectedRoleId!]!,
|
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 {
|
} else {
|
||||||
final resp = await showFutureLoadingDialog(
|
final resp = await showFutureLoadingDialog(
|
||||||
|
|
@ -196,6 +233,43 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> 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<void> pingCourse() async {
|
Future<void> pingCourse() async {
|
||||||
if (room?.courseParent == null) {
|
if (room?.courseParent == null) {
|
||||||
throw Exception("Activity is not part of a course");
|
throw Exception("Activity is not part of a course");
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,23 @@ class ActivitySessionStartView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: buttonStyle,
|
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(
|
child: Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment.center,
|
MainAxisAlignment.center,
|
||||||
|
|
@ -221,7 +237,7 @@ class ActivitySessionStartView extends StatelessWidget {
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: buttonStyle,
|
style: buttonStyle,
|
||||||
onPressed: controller.enableButtons
|
onPressed: controller.enableButtons
|
||||||
? controller.onTap
|
? controller.confirmRoleSelection
|
||||||
: null,
|
: null,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ class ActivitySuggestionCard extends StatelessWidget {
|
||||||
final double? fontSizeSmall;
|
final double? fontSizeSmall;
|
||||||
final double? iconSize;
|
final double? iconSize;
|
||||||
|
|
||||||
|
final int? openSessions;
|
||||||
|
|
||||||
const ActivitySuggestionCard({
|
const ActivitySuggestionCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.activity,
|
required this.activity,
|
||||||
|
|
@ -22,6 +24,7 @@ class ActivitySuggestionCard extends StatelessWidget {
|
||||||
this.fontSize,
|
this.fontSize,
|
||||||
this.fontSizeSmall,
|
this.fontSizeSmall,
|
||||||
this.iconSize,
|
this.iconSize,
|
||||||
|
this.openSessions,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -66,14 +69,16 @@ class ActivitySuggestionCard extends StatelessWidget {
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
if (activity.req.mode.isNotEmpty)
|
if (activity.req.mode.isNotEmpty)
|
||||||
Padding(
|
Expanded(
|
||||||
padding: const EdgeInsets.all(4.0),
|
child: Padding(
|
||||||
child: Text(
|
padding: const EdgeInsets.all(4.0),
|
||||||
activity.req.mode,
|
child: Text(
|
||||||
style: fontSizeSmall != null
|
activity.req.mode,
|
||||||
? TextStyle(fontSize: fontSizeSmall)
|
style: fontSizeSmall != null
|
||||||
: theme.textTheme.labelSmall,
|
? TextStyle(fontSize: fontSizeSmall)
|
||||||
overflow: TextOverflow.ellipsis,
|
: theme.textTheme.labelSmall,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:matrix/matrix.dart';
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/l10n/l10n.dart';
|
import 'package:fluffychat/l10n/l10n.dart';
|
||||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.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/event_wrappers/pangea_message_event.dart';
|
||||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
@ -56,12 +57,29 @@ class ChatListItemSubtitle extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (room.showActivityChatUI) {
|
if (room.showActivityChatUI) {
|
||||||
return Text(
|
if (room.isHiddenActivityRoom) {
|
||||||
room.activityPlan!.learningObjective,
|
return Text(
|
||||||
style: style,
|
room.activityPlan!.learningObjective,
|
||||||
maxLines: 2,
|
style: style,
|
||||||
overflow: TextOverflow.ellipsis,
|
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;
|
final event = room.lastEvent;
|
||||||
|
|
|
||||||
129
lib/pangea/course_chats/activity_template_chat_list_item.dart
Normal file
129
lib/pangea/course_chats/activity_template_chat_list_item.dart
Normal file
|
|
@ -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<ExtendedSpaceRoomsChunk> 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,14 +9,16 @@ import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/l10n/l10n.dart';
|
import 'package:fluffychat/l10n/l10n.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/chat_list.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/constants/pangea_room_types.dart';
|
||||||
import 'package:fluffychat/pangea/chat_settings/utils/delete_room.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/chat_settings/widgets/delete_space_dialog.dart';
|
||||||
import 'package:fluffychat/pangea/common/utils/error_handler.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/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_model.dart';
|
||||||
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.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/extensions/pangea_room_extension.dart';
|
||||||
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
|
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
|
||||||
import 'package:fluffychat/pangea/spaces/constants/space_constants.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/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||||
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
||||||
class CourseChats extends StatefulWidget {
|
class CourseChats extends StatefulWidget {
|
||||||
final Client client;
|
final Client client;
|
||||||
|
|
@ -54,7 +57,6 @@ class CourseChatsController extends State<CourseChats> {
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
|
|
||||||
CoursePlanModel? course;
|
CoursePlanModel? course;
|
||||||
String? selectedTopicId;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -62,7 +64,8 @@ class CourseChatsController extends State<CourseChats> {
|
||||||
|
|
||||||
// Listen for changes to the activeSpace's hierarchy,
|
// Listen for changes to the activeSpace's hierarchy,
|
||||||
// and reload the hierarchy when they come through
|
// and reload the hierarchy when they come through
|
||||||
_roomSubscription ??= widget.client.onSync.stream
|
_roomSubscription?.cancel();
|
||||||
|
_roomSubscription = widget.client.onSync.stream
|
||||||
.where(_hasHierarchyUpdate)
|
.where(_hasHierarchyUpdate)
|
||||||
.listen((update) => loadHierarchy(reload: true));
|
.listen((update) => loadHierarchy(reload: true));
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -74,6 +77,11 @@ class CourseChatsController extends State<CourseChats> {
|
||||||
// via the navigation rail, so this accounts for that
|
// via the navigation rail, so this accounts for that
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.roomId != widget.roomId) {
|
if (oldWidget.roomId != widget.roomId) {
|
||||||
|
_roomSubscription?.cancel();
|
||||||
|
_roomSubscription = widget.client.onSync.stream
|
||||||
|
.where(_hasHierarchyUpdate)
|
||||||
|
.listen((update) => loadHierarchy(reload: true));
|
||||||
|
|
||||||
discoveredChildren = null;
|
discoveredChildren = null;
|
||||||
_nextBatch = null;
|
_nextBatch = null;
|
||||||
noMoreRooms = false;
|
noMoreRooms = false;
|
||||||
|
|
@ -94,38 +102,77 @@ class CourseChatsController extends State<CourseChats> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSelectedTopicId(String topicID) {
|
Set<String> get childrenIds =>
|
||||||
setState(() {
|
room?.spaceChildren.map((c) => c.roomId).whereType<String>().toSet() ??
|
||||||
selectedTopicId = topicID;
|
{};
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
int get _selectedTopicIndex =>
|
List<Room> get joinedRooms => Matrix.of(context)
|
||||||
course?.loadedTopics.indexWhere((t) => t.uuid == selectedTopicId) ?? -1;
|
.client
|
||||||
|
.rooms
|
||||||
|
.where((room) => childrenIds.contains(room.id))
|
||||||
|
.where((room) => !room.isHiddenRoom)
|
||||||
|
.toList();
|
||||||
|
|
||||||
bool get canMoveLeft => _selectedTopicIndex > 0;
|
List<Room> joinedActivities() =>
|
||||||
bool get canMoveRight {
|
joinedRooms.where((r) => r.isActivitySession).toList();
|
||||||
if (course == null) return false;
|
|
||||||
final endIndex = room?.ownCurrentTopicIndex(course!) ??
|
|
||||||
(course!.loadedTopics.length - 1);
|
|
||||||
return _selectedTopicIndex < endIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
void moveLeft() {
|
List<SpaceRoomsChunk> get discoveredGroupChats => (discoveredChildren ?? [])
|
||||||
if (canMoveLeft) {
|
.where(
|
||||||
setSelectedTopicId(course!.loadedTopics[_selectedTopicIndex - 1].uuid);
|
(chunk) =>
|
||||||
|
chunk.roomType == null ||
|
||||||
|
!chunk.roomType!.startsWith(PangeaRoomTypes.activitySession),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Map<ActivityPlanModel, List<ExtendedSpaceRoomsChunk>> discoveredActivities() {
|
||||||
|
if (discoveredChildren == null) return {};
|
||||||
|
|
||||||
|
final courseStates = room?.allCourseUserStates ?? {};
|
||||||
|
final Map<String, List<String>> 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() {
|
final Map<ActivityPlanModel, List<ExtendedSpaceRoomsChunk>> sessionsMap =
|
||||||
if (canMoveRight) {
|
{};
|
||||||
setSelectedTopicId(course!.loadedTopics[_selectedTopicIndex + 1].uuid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CourseTopicModel? get selectedTopic => course?.loadedTopics.firstWhereOrNull(
|
for (final chunk in discoveredChildren!) {
|
||||||
(topic) => topic.uuid == selectedTopicId,
|
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<Room> get joinedChats =>
|
||||||
|
joinedRooms.where((room) => !room.isActivitySession).toList();
|
||||||
|
|
||||||
Future<void> _joinDefaultChats() async {
|
Future<void> _joinDefaultChats() async {
|
||||||
if (discoveredChildren == null) return;
|
if (discoveredChildren == null) return;
|
||||||
|
|
@ -623,7 +670,7 @@ class CourseChatsController extends State<CourseChats> {
|
||||||
future: room.isSpace ? room.leaveSpace : room.leave,
|
future: room.isSpace ? room.leaveSpace : room.leave,
|
||||||
);
|
);
|
||||||
if (mounted && !resp.isError) {
|
if (mounted && !resp.isError) {
|
||||||
context.go("/rooms");
|
context.go("/rooms/spaces/${widget.roomId}/details");
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
@ -711,17 +758,56 @@ class CourseChatsController extends State<CourseChats> {
|
||||||
/// Used to filter out sync updates with hierarchy updates for the active
|
/// 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
|
/// space so that the view can be auto-reloaded in the room subscription
|
||||||
bool _hasHierarchyUpdate(SyncUpdate update) {
|
bool _hasHierarchyUpdate(SyncUpdate update) {
|
||||||
final joinTimeline = update.rooms?.join?[widget.roomId]?.timeline;
|
final joinUpdate = update.rooms?.join;
|
||||||
final leaveTimeline = update.rooms?.leave?[widget.roomId]?.timeline;
|
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<List<MatrixEvent>>();
|
||||||
|
|
||||||
|
final leftRooms = leaveUpdate?.entries
|
||||||
|
.where(
|
||||||
|
(e) => childrenIds.contains(e.key),
|
||||||
|
)
|
||||||
|
.map((e) => e.value.timeline?.events)
|
||||||
|
.whereType<List<MatrixEvent>>();
|
||||||
|
|
||||||
|
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;
|
if (joinTimeline == null && leaveTimeline == null) return false;
|
||||||
final bool hasJoinUpdate = joinTimeline?.events?.any(
|
|
||||||
(event) => event.type == EventTypes.SpaceChild,
|
final bool hasJoinUpdate = joinTimeline!.any(
|
||||||
) ??
|
(event) => event.type == EventTypes.SpaceChild,
|
||||||
false;
|
);
|
||||||
final bool hasLeaveUpdate = leaveTimeline?.events?.any(
|
final bool hasLeaveUpdate = leaveTimeline!.any(
|
||||||
(event) => event.type == EventTypes.SpaceChild,
|
(event) => event.type == EventTypes.SpaceChild,
|
||||||
) ??
|
);
|
||||||
false;
|
|
||||||
return hasJoinUpdate || hasLeaveUpdate;
|
return hasJoinUpdate || hasLeaveUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,11 @@ import 'package:fluffychat/config/themes.dart';
|
||||||
import 'package:fluffychat/l10n/l10n.dart';
|
import 'package:fluffychat/l10n/l10n.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/chat_list_item.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/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/course_chats_page.dart';
|
||||||
import 'package:fluffychat/pangea/course_chats/unjoined_chat_list_item.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_builder.dart';
|
||||||
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.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/space_analytics/analytics_request_indicator.dart';
|
||||||
import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart';
|
import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart';
|
||||||
import 'package:fluffychat/utils/stream_extension.dart';
|
import 'package:fluffychat/utils/stream_extension.dart';
|
||||||
|
|
@ -41,63 +39,19 @@ class CourseChatsView extends StatelessWidget {
|
||||||
|
|
||||||
return CoursePlanBuilder(
|
return CoursePlanBuilder(
|
||||||
courseId: room.coursePlan?.uuid,
|
courseId: room.coursePlan?.uuid,
|
||||||
onLoaded: (course) {
|
onLoaded: controller.setCourse,
|
||||||
controller.setCourse(course);
|
|
||||||
final topic = room.ownCurrentTopic(course);
|
|
||||||
if (topic != null) controller.setSelectedTopicId(topic.uuid);
|
|
||||||
},
|
|
||||||
builder: (context, courseController) {
|
builder: (context, courseController) {
|
||||||
final CourseTopicModel? topic = controller.selectedTopic;
|
|
||||||
final List<String> activityIds = topic?.activityIds ?? [];
|
|
||||||
|
|
||||||
return StreamBuilder(
|
return StreamBuilder(
|
||||||
stream: room.client.onSync.stream
|
stream: room.client.onSync.stream
|
||||||
.where((s) => s.hasRoomUpdate)
|
.where((s) => s.hasRoomUpdate)
|
||||||
.rateLimit(const Duration(seconds: 1)),
|
.rateLimit(const Duration(seconds: 1)),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final childrenIds = room.spaceChildren
|
final joinedChats = controller.joinedChats;
|
||||||
.map((c) => c.roomId)
|
final joinedSessions = controller.joinedActivities();
|
||||||
.whereType<String>()
|
|
||||||
.toSet();
|
|
||||||
|
|
||||||
final joinedChats = [];
|
final discoveredGroupChats = controller.discoveredGroupChats;
|
||||||
final joinedSessions = [];
|
final discoveredSessions =
|
||||||
final joinedRooms = room.client.rooms
|
controller.discoveredActivities().entries.toList();
|
||||||
.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 ?? <SpaceRoomsChunk>[];
|
|
||||||
|
|
||||||
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 isColumnMode = FluffyThemes.isColumnMode(context);
|
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|
@ -113,7 +67,7 @@ class CourseChatsView extends StatelessWidget {
|
||||||
joinedSessions.length +
|
joinedSessions.length +
|
||||||
discoveredGroupChats.length +
|
discoveredGroupChats.length +
|
||||||
discoveredSessions.length +
|
discoveredSessions.length +
|
||||||
6,
|
7,
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
// courses chats title
|
// courses chats title
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
|
|
@ -178,92 +132,6 @@ class CourseChatsView extends StatelessWidget {
|
||||||
}
|
}
|
||||||
i -= discoveredGroupChats.length;
|
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) {
|
if (i == 0) {
|
||||||
return joinedSessions.isEmpty && discoveredSessions.isEmpty
|
return joinedSessions.isEmpty && discoveredSessions.isEmpty
|
||||||
? ListTile(
|
? ListTile(
|
||||||
|
|
@ -279,6 +147,23 @@ class CourseChatsView extends StatelessWidget {
|
||||||
}
|
}
|
||||||
i--;
|
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
|
// joined activity sessions
|
||||||
if (i < joinedSessions.length) {
|
if (i < joinedSessions.length) {
|
||||||
final joinedRoom = joinedSessions[i];
|
final joinedRoom = joinedSessions[i];
|
||||||
|
|
@ -290,17 +175,58 @@ class CourseChatsView extends StatelessWidget {
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
activeChat: controller.widget.activeChat == joinedRoom.id,
|
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;
|
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
|
// unjoined activity sessions
|
||||||
if (i < discoveredSessions.length) {
|
if (i < discoveredSessions.length) {
|
||||||
return UnjoinedChatListItem(
|
final activity = discoveredSessions[i].key;
|
||||||
chunk: discoveredSessions[i],
|
final sessions = discoveredSessions[i].value;
|
||||||
onTap: () => controller.joinChildRoom(
|
return ActivityTemplateChatListItem(
|
||||||
discoveredSessions[i],
|
space: room,
|
||||||
),
|
joinActivity: controller.joinChildRoom,
|
||||||
|
activity: activity,
|
||||||
|
sessions: sessions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
i -= discoveredSessions.length;
|
i -= discoveredSessions.length;
|
||||||
|
|
|
||||||
13
lib/pangea/course_chats/extended_space_rooms_chunk.dart
Normal file
13
lib/pangea/course_chats/extended_space_rooms_chunk.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
class ExtendedSpaceRoomsChunk {
|
||||||
|
final SpaceRoomsChunk chunk;
|
||||||
|
final String activityId;
|
||||||
|
final List<String> userIds;
|
||||||
|
|
||||||
|
ExtendedSpaceRoomsChunk({
|
||||||
|
required this.chunk,
|
||||||
|
required this.activityId,
|
||||||
|
required this.userIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
62
lib/pangea/course_chats/open_roles_indicator.dart
Normal file
62
lib/pangea/course_chats/open_roles_indicator.dart
Normal file
|
|
@ -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<String> 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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/common/config/environment.dart';
|
||||||
import 'package:fluffychat/pangea/course_plans/course_media_repo.dart';
|
import 'package:fluffychat/pangea/course_plans/course_media_repo.dart';
|
||||||
import 'package:fluffychat/pangea/course_plans/course_topic_model.dart';
|
import 'package:fluffychat/pangea/course_plans/course_topic_model.dart';
|
||||||
|
|
@ -57,6 +58,16 @@ class CoursePlanModel {
|
||||||
int get totalActivities =>
|
int get totalActivities =>
|
||||||
loadedTopics.fold(0, (sum, topic) => sum + topic.activityIds.length);
|
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
|
/// Deserialize from JSON
|
||||||
factory CoursePlanModel.fromJson(Map<String, dynamic> json) {
|
factory CoursePlanModel.fromJson(Map<String, dynamic> json) {
|
||||||
return CoursePlanModel(
|
return CoursePlanModel(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:matrix/matrix.dart' as sdk;
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||||
|
|
@ -28,11 +29,52 @@ extension CoursePlanRoomExtension on Room {
|
||||||
userID,
|
userID,
|
||||||
);
|
);
|
||||||
if (event == null) return null;
|
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!);
|
CourseUserState? get _ownCourseState => _courseUserState(client.userID!);
|
||||||
|
|
||||||
|
Map<String, CourseUserState> get allCourseUserStates {
|
||||||
|
final content = states[PangeaEventTypes.courseUser];
|
||||||
|
if (content == null || content.isEmpty) return {};
|
||||||
|
return Map<String, CourseUserState>.fromEntries(
|
||||||
|
content.entries.map(
|
||||||
|
(e) {
|
||||||
|
try {
|
||||||
|
return MapEntry(
|
||||||
|
e.key,
|
||||||
|
CourseUserState.fromJson(e.value.content),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).whereType<MapEntry<String, CourseUserState>>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> openSessions(String activityId) {
|
||||||
|
final Set<String> sessions = {};
|
||||||
|
final Set<String> childIds =
|
||||||
|
spaceChildren.map((child) => child.roomId).whereType<String>().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(
|
bool hasCompletedActivity(
|
||||||
String userID,
|
String userID,
|
||||||
String activityID,
|
String activityID,
|
||||||
|
|
@ -44,14 +86,14 @@ extension CoursePlanRoomExtension on Room {
|
||||||
|
|
||||||
bool _hasCompletedTopic(
|
bool _hasCompletedTopic(
|
||||||
String userID,
|
String userID,
|
||||||
String topicID,
|
CourseTopicModel topic,
|
||||||
CoursePlanModel course,
|
CoursePlanModel course,
|
||||||
) {
|
) {
|
||||||
final state = _courseUserState(userID);
|
final state = _courseUserState(userID);
|
||||||
if (state == null) return false;
|
if (state == null) return false;
|
||||||
|
|
||||||
final topicIndex = course.loadedTopics.indexWhere(
|
final topicIndex = course.loadedTopics.indexWhere(
|
||||||
(t) => t.uuid == topicID,
|
(t) => t.uuid == topic.uuid,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (topicIndex == -1) {
|
if (topicIndex == -1) {
|
||||||
|
|
@ -61,7 +103,7 @@ extension CoursePlanRoomExtension on Room {
|
||||||
final activityIds = course.loadedTopics[topicIndex].loadedActivities
|
final activityIds = course.loadedTopics[topicIndex].loadedActivities
|
||||||
.map((a) => a.activityId)
|
.map((a) => a.activityId)
|
||||||
.toList();
|
.toList();
|
||||||
return state.completedActivities(topicID).toSet().containsAll(activityIds);
|
return state.completedActivities.toSet().containsAll(activityIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
CourseTopicModel? currentTopic(
|
CourseTopicModel? currentTopic(
|
||||||
|
|
@ -69,10 +111,9 @@ extension CoursePlanRoomExtension on Room {
|
||||||
CoursePlanModel course,
|
CoursePlanModel course,
|
||||||
) {
|
) {
|
||||||
if (coursePlan == null) return null;
|
if (coursePlan == null) return null;
|
||||||
final topicIDs = course.loadedTopics.map((t) => t.uuid).toList();
|
if (course.loadedTopics.isEmpty) return null;
|
||||||
if (topicIDs.isEmpty) return null;
|
|
||||||
|
|
||||||
final index = topicIDs.indexWhere(
|
final index = course.loadedTopics.indexWhere(
|
||||||
(t) => !_hasCompletedTopic(userID, t, course),
|
(t) => !_hasCompletedTopic(userID, t, course),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -87,10 +128,9 @@ extension CoursePlanRoomExtension on Room {
|
||||||
CoursePlanModel course,
|
CoursePlanModel course,
|
||||||
) {
|
) {
|
||||||
if (coursePlan == null) return -1;
|
if (coursePlan == null) return -1;
|
||||||
final topicIDs = course.loadedTopics.map((t) => t.uuid).toList();
|
if (course.loadedTopics.isEmpty) return -1;
|
||||||
if (topicIDs.isEmpty) return -1;
|
|
||||||
|
|
||||||
final index = topicIDs.indexWhere(
|
final index = course.loadedTopics.indexWhere(
|
||||||
(t) => !_hasCompletedTopic(userID, t, course),
|
(t) => !_hasCompletedTopic(userID, t, course),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -104,7 +144,9 @@ extension CoursePlanRoomExtension on Room {
|
||||||
for (final child in spaceChildren) {
|
for (final child in spaceChildren) {
|
||||||
if (child.roomId == null) continue;
|
if (child.roomId == null) continue;
|
||||||
final room = client.getRoomById(child.roomId!);
|
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;
|
return room.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,16 +172,36 @@ extension CoursePlanRoomExtension on Room {
|
||||||
return topicUserMap;
|
return topicUserMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> finishCourseActivity(
|
Future<void> joinCourseActivity(
|
||||||
String activityID,
|
String activityID,
|
||||||
String topicID,
|
String roomID,
|
||||||
) async {
|
) async {
|
||||||
CourseUserState? state = _ownCourseState;
|
CourseUserState? state = _ownCourseState;
|
||||||
state ??= CourseUserState(
|
state ??= CourseUserState(
|
||||||
userID: client.userID!,
|
userID: client.userID!,
|
||||||
completedActivities: {},
|
completedActivities: {},
|
||||||
|
joinActivities: {},
|
||||||
);
|
);
|
||||||
state.completeActivity(activityID, topicID);
|
state.joinActivity(activityID, roomID);
|
||||||
|
await client.setRoomStateWithKey(
|
||||||
|
id,
|
||||||
|
PangeaEventTypes.courseUser,
|
||||||
|
client.userID!,
|
||||||
|
state.toJson(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> finishCourseActivity(
|
||||||
|
String activityID,
|
||||||
|
String roomID,
|
||||||
|
) async {
|
||||||
|
CourseUserState? state = _ownCourseState;
|
||||||
|
state ??= CourseUserState(
|
||||||
|
userID: client.userID!,
|
||||||
|
completedActivities: {},
|
||||||
|
joinActivities: {},
|
||||||
|
);
|
||||||
|
state.completeActivity(activityID, roomID);
|
||||||
await client.setRoomStateWithKey(
|
await client.setRoomStateWithKey(
|
||||||
id,
|
id,
|
||||||
PangeaEventTypes.courseUser,
|
PangeaEventTypes.courseUser,
|
||||||
|
|
@ -156,7 +218,7 @@ extension CoursePlanRoomExtension on Room {
|
||||||
creationContent: {
|
creationContent: {
|
||||||
'type': "${PangeaRoomTypes.activitySession}:${activity.activityId}",
|
'type': "${PangeaRoomTypes.activitySession}:${activity.activityId}",
|
||||||
},
|
},
|
||||||
visibility: Visibility.private,
|
visibility: sdk.Visibility.private,
|
||||||
name: activity.title,
|
name: activity.title,
|
||||||
initialState: [
|
initialState: [
|
||||||
StateEvent(
|
StateEvent(
|
||||||
|
|
@ -198,6 +260,11 @@ extension CoursePlanRoomExtension on Room {
|
||||||
if (pangeaSpaceParents.isEmpty) {
|
if (pangeaSpaceParents.isEmpty) {
|
||||||
await client.waitForRoomInSync(roomID);
|
await client.waitForRoomInSync(roomID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await joinCourseActivity(
|
||||||
|
activity.activityId,
|
||||||
|
roomID,
|
||||||
|
);
|
||||||
return roomID;
|
return roomID;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||||
import 'package:fluffychat/pangea/course_plans/course_activity_repo.dart';
|
import 'package:fluffychat/pangea/course_plans/course_activity_repo.dart';
|
||||||
|
|
@ -56,6 +58,11 @@ class CourseTopicModel {
|
||||||
Future<List<ActivityPlanModel>> fetchActivities() =>
|
Future<List<ActivityPlanModel>> fetchActivities() =>
|
||||||
CourseActivityRepo.get(uuid, activityIds);
|
CourseActivityRepo.get(uuid, activityIds);
|
||||||
|
|
||||||
|
ActivityPlanModel? activityById(String activityId) =>
|
||||||
|
loadedActivities.firstWhereOrNull(
|
||||||
|
(activity) => activity.activityId == activityId,
|
||||||
|
);
|
||||||
|
|
||||||
/// Deserialize from JSON
|
/// Deserialize from JSON
|
||||||
factory CourseTopicModel.fromJson(Map<String, dynamic> json) {
|
factory CourseTopicModel.fromJson(Map<String, dynamic> json) {
|
||||||
return CourseTopicModel(
|
return CourseTopicModel(
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,67 @@
|
||||||
class CourseUserState {
|
class CourseUserState {
|
||||||
final String userID;
|
final String userID;
|
||||||
|
|
||||||
|
// Map of activityIds to list of roomIds
|
||||||
final Map<String, List<String>> _completedActivities;
|
final Map<String, List<String>> _completedActivities;
|
||||||
|
final Map<String, List<String>> _joinedActivities;
|
||||||
|
|
||||||
CourseUserState({
|
CourseUserState({
|
||||||
required this.userID,
|
required this.userID,
|
||||||
required Map<String, List<String>> completedActivities,
|
required Map<String, List<String>> completedActivities,
|
||||||
}) : _completedActivities = completedActivities;
|
required Map<String, List<String>> joinActivities,
|
||||||
|
}) : _completedActivities = completedActivities,
|
||||||
|
_joinedActivities = joinActivities;
|
||||||
|
|
||||||
|
void joinActivity(
|
||||||
|
String activityID,
|
||||||
|
String roomID,
|
||||||
|
) {
|
||||||
|
_joinedActivities[activityID] ??= [];
|
||||||
|
_joinedActivities[activityID]!.add(roomID);
|
||||||
|
}
|
||||||
|
|
||||||
void completeActivity(
|
void completeActivity(
|
||||||
String activityID,
|
String activityID,
|
||||||
String topicID,
|
String roomID,
|
||||||
) {
|
) {
|
||||||
_completedActivities[topicID] ??= [];
|
_completedActivities[activityID] ??= [];
|
||||||
if (!_completedActivities[topicID]!.contains(activityID)) {
|
_completedActivities[activityID]!.add(roomID);
|
||||||
_completedActivities[topicID]!.add(activityID);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> completedActivities(String topicID) {
|
Map<String, List<String>> get joinedActivities => _joinedActivities;
|
||||||
return _completedActivities[topicID] ?? [];
|
|
||||||
}
|
List<String> get completedActivities => _completedActivities.keys.toList();
|
||||||
|
List<String> get joinedActivityRooms =>
|
||||||
|
_joinedActivities.values.expand((e) => e).toList();
|
||||||
|
|
||||||
bool hasCompletedActivity(
|
bool hasCompletedActivity(
|
||||||
String activityID,
|
String activityID,
|
||||||
) {
|
) {
|
||||||
return _completedActivities.values.any(
|
return _completedActivities.containsKey(activityID);
|
||||||
(activities) => activities.contains(activityID),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
factory CourseUserState.fromJson(Map<String, dynamic> json) {
|
factory CourseUserState.fromJson(Map<String, dynamic> json) {
|
||||||
final Map<String, List<String>> activities = {};
|
final activityEntry = json['comp_act_by_topic'];
|
||||||
final activityEntry =
|
final joinEntry = json['join_act_by_topic'];
|
||||||
(json['comp_act_by_topic'] as Map<String, dynamic>?) ?? {};
|
|
||||||
|
|
||||||
for (final entry in activityEntry.entries) {
|
final Map<String, List<String>> activityMap = {};
|
||||||
activities[entry.key] = List<String>.from(entry.value);
|
if (activityEntry != null) {
|
||||||
|
activityEntry.forEach((key, value) {
|
||||||
|
activityMap[key] = List<String>.from(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, List<String>> joinMap = {};
|
||||||
|
if (joinEntry != null) {
|
||||||
|
joinEntry.forEach((key, value) {
|
||||||
|
joinMap[key] = List<String>.from(value);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return CourseUserState(
|
return CourseUserState(
|
||||||
userID: json['user_id'],
|
userID: json['user_id'],
|
||||||
completedActivities: activities,
|
completedActivities: activityMap,
|
||||||
|
joinActivities: joinMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,6 +69,7 @@ class CourseUserState {
|
||||||
return {
|
return {
|
||||||
'user_id': userID,
|
'user_id': userID,
|
||||||
'comp_act_by_topic': _completedActivities,
|
'comp_act_by_topic': _completedActivities,
|
||||||
|
'join_act_by_topic': _joinedActivities,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
lib/pangea/course_plans/map_clipper.dart
Normal file
26
lib/pangea/course_plans/map_clipper.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MapClipper extends CustomClipper<Path> {
|
||||||
|
@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<Path> oldClipper) => false;
|
||||||
|
}
|
||||||
|
|
@ -182,6 +182,8 @@ class CourseSettings extends StatelessWidget {
|
||||||
activityId,
|
activityId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final activity = topic.loadedActivities[index];
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 24.0),
|
padding: const EdgeInsets.only(right: 24.0),
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
|
|
@ -195,13 +197,15 @@ class CourseSettings extends StatelessWidget {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
ActivitySuggestionCard(
|
ActivitySuggestionCard(
|
||||||
activity: topic.loadedActivities[index],
|
activity: activity,
|
||||||
width: isColumnMode ? 160.0 : 120.0,
|
width: isColumnMode ? 160.0 : 120.0,
|
||||||
height: isColumnMode ? 280.0 : 200.0,
|
height: isColumnMode ? 280.0 : 200.0,
|
||||||
fontSize: isColumnMode ? 20.0 : 12.0,
|
fontSize: isColumnMode ? 20.0 : 12.0,
|
||||||
fontSizeSmall:
|
fontSizeSmall:
|
||||||
isColumnMode ? 12.0 : 8.0,
|
isColumnMode ? 12.0 : 8.0,
|
||||||
iconSize: isColumnMode ? 12.0 : 8.0,
|
iconSize: isColumnMode ? 12.0 : 8.0,
|
||||||
|
openSessions:
|
||||||
|
room.numOpenSessions(activityId),
|
||||||
),
|
),
|
||||||
if (complete)
|
if (complete)
|
||||||
Container(
|
Container(
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
|
||||||
import 'package:fluffychat/config/themes.dart';
|
import 'package:fluffychat/config/themes.dart';
|
||||||
import 'package:fluffychat/l10n/l10n.dart';
|
import 'package:fluffychat/l10n/l10n.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/navi_rail_item.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/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/pangea/extensions/pangea_room_extension.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
import 'package:fluffychat/utils/stream_extension.dart';
|
import 'package:fluffychat/utils/stream_extension.dart';
|
||||||
|
|
@ -177,6 +177,8 @@ class SpacesNavigationRail extends StatelessWidget {
|
||||||
toolTip: displayname,
|
toolTip: displayname,
|
||||||
isSelected: activeSpaceId == space.id,
|
isSelected: activeSpaceId == space.id,
|
||||||
// #Pangea
|
// #Pangea
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(0),
|
||||||
// onTap: () => onGoToSpaceId(rootSpaces[i].id),
|
// onTap: () => onGoToSpaceId(rootSpaces[i].id),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final room = client.getRoomById(rootSpaces[i].id);
|
final room = client.getRoomById(rootSpaces[i].id);
|
||||||
|
|
@ -194,20 +196,32 @@ class SpacesNavigationRail extends StatelessWidget {
|
||||||
// Pangea#
|
// Pangea#
|
||||||
unreadBadgeFilter: (room) =>
|
unreadBadgeFilter: (room) =>
|
||||||
spaceChildrenIds.contains(room.id),
|
spaceChildrenIds.contains(room.id),
|
||||||
icon: Avatar(
|
// #Pangea
|
||||||
mxContent: rootSpaces[i].avatar,
|
// icon: Avatar(
|
||||||
name: displayname,
|
// mxContent: rootSpaces[i].avatar,
|
||||||
border: BorderSide(
|
// name: displayname,
|
||||||
width: 1,
|
// border: BorderSide(
|
||||||
color: Theme.of(context).dividerColor,
|
// 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#
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue