chore: use synapse room_preview to determine which activities users have completed (#4078)
This commit is contained in:
parent
8887efa22d
commit
152a716aa8
8 changed files with 177 additions and 242 deletions
|
|
@ -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/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
|
|
@ -49,14 +48,6 @@ class ActivityFinishedStatusMessage extends StatelessWidget {
|
|||
await controller.room.archiveActivity();
|
||||
await MatrixState.pangeaController.putAnalytics
|
||||
.sendActivityAnalytics(controller.room.id);
|
||||
|
||||
final courseParent = controller.room.courseParent;
|
||||
if (courseParent?.coursePlan == null) return;
|
||||
final activityId = controller.room.activityPlan!.activityId;
|
||||
await courseParent!.finishCourseActivity(
|
||||
activityId,
|
||||
controller.room.id,
|
||||
);
|
||||
}
|
||||
|
||||
ActivitySummaryModel? get summary => controller.room.activitySummary;
|
||||
|
|
|
|||
|
|
@ -71,16 +71,7 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage>
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadActivity();
|
||||
|
||||
if (courseParent != null) {
|
||||
loadRoomSummaries(
|
||||
courseParent!.spaceChildren
|
||||
.map((c) => c.roomId)
|
||||
.whereType<String>()
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
_load();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -94,7 +85,7 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage>
|
|||
}
|
||||
|
||||
if (oldWidget.activityId != widget.activityId) {
|
||||
_loadActivity();
|
||||
_load();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -220,34 +211,52 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage>
|
|||
return (activity?.req.numberOfParticipants ?? 0) - availableParticipants;
|
||||
}
|
||||
|
||||
Future<void> _loadActivity() async {
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
setState(() {
|
||||
loading = true;
|
||||
error = null;
|
||||
});
|
||||
|
||||
if (courseParent?.coursePlan != null) {
|
||||
course = await CoursePlansRepo.get(courseParent!.coursePlan!.uuid);
|
||||
}
|
||||
|
||||
final activities = await CourseActivityRepo.get(
|
||||
widget.activityId,
|
||||
[widget.activityId],
|
||||
);
|
||||
|
||||
if (activities.isEmpty) {
|
||||
throw Exception("Activity not found");
|
||||
}
|
||||
|
||||
if (mounted) setState(() => activity = activities.first);
|
||||
final futures = <Future>[];
|
||||
futures.add(_loadSummary());
|
||||
futures.add(_loadActivity());
|
||||
await Future.wait(futures);
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => error = e);
|
||||
error = e;
|
||||
} finally {
|
||||
if (mounted) setState(() => loading = false);
|
||||
if (mounted) {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadSummary() async {
|
||||
if (courseParent == null) return;
|
||||
await loadRoomSummaries(
|
||||
courseParent!.spaceChildren
|
||||
.map((c) => c.roomId)
|
||||
.whereType<String>()
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadActivity() async {
|
||||
if (courseParent?.coursePlan != null) {
|
||||
course = await CoursePlansRepo.get(courseParent!.coursePlan!.uuid);
|
||||
}
|
||||
|
||||
final activities = await CourseActivityRepo.get(
|
||||
widget.activityId,
|
||||
[widget.activityId],
|
||||
);
|
||||
|
||||
if (activities.isEmpty) {
|
||||
throw Exception("Activity not found");
|
||||
}
|
||||
|
||||
activity = activities.first;
|
||||
}
|
||||
|
||||
Future<void> joinActivity() async {
|
||||
if (state != SessionState.selectedRole) return;
|
||||
if (widget.roomId == null) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: implementation_imports
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class SelectedCourseView extends StatelessWidget {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: ListView.builder(
|
||||
itemCount: course.loadedTopics.length + 2,
|
||||
itemCount: course.loadedTopics.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return Column(
|
||||
|
|
@ -122,11 +122,6 @@ class SelectedCourseView extends StatelessWidget {
|
|||
}
|
||||
|
||||
index--;
|
||||
|
||||
if (index == course.loadedTopics.length) {
|
||||
return const SizedBox(height: 150.0);
|
||||
}
|
||||
|
||||
final topic = course.loadedTopics[index];
|
||||
return Padding(
|
||||
padding:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_topic_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
|
||||
|
|
@ -13,15 +17,9 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
|
|||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final resp =
|
||||
await Matrix.of(context).client.requestRoomSummaries(roomIds);
|
||||
|
||||
if (mounted) {
|
||||
setState(() => roomSummaries = resp.summaries);
|
||||
}
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(e: e, s: s, data: {'roomIds': roomIds});
|
||||
final resp = await Matrix.of(context).client.requestRoomSummaries(roomIds);
|
||||
if (mounted) {
|
||||
setState(() => roomSummaries = resp.summaries);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,4 +46,83 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
|
||||
int numOpenSessions(String activityId) => openSessions(activityId).length;
|
||||
|
||||
Set<String> _completedActivities(String userID) {
|
||||
if (roomSummaries == null || roomSummaries!.isEmpty) return {};
|
||||
return roomSummaries!.values
|
||||
.where(
|
||||
(entry) => entry.activityRoles.roles.values.any(
|
||||
(v) => v.userId == userID && v.isArchived,
|
||||
),
|
||||
)
|
||||
.map((e) => e.activityPlan.activityId)
|
||||
.toSet();
|
||||
}
|
||||
|
||||
bool hasCompletedActivity(
|
||||
String userID,
|
||||
String activityID,
|
||||
) {
|
||||
final completed = _completedActivities(userID);
|
||||
return completed.contains(activityID);
|
||||
}
|
||||
|
||||
bool _hasCompletedTopic(
|
||||
String userID,
|
||||
CourseTopicModel topic,
|
||||
CoursePlanModel course,
|
||||
) {
|
||||
final topicIndex = course.loadedTopics.indexWhere(
|
||||
(t) => t.uuid == topic.uuid,
|
||||
);
|
||||
|
||||
if (topicIndex == -1) {
|
||||
throw Exception('Topic not found');
|
||||
}
|
||||
|
||||
final topicActivities = course.loadedTopics[topicIndex].loadedActivities;
|
||||
final topicActivityIds = topicActivities.map((a) => a.activityId).toSet();
|
||||
final numTwoPersonActivities =
|
||||
topicActivities.where((a) => a.req.numberOfParticipants <= 2).length;
|
||||
|
||||
final completedTopicActivities =
|
||||
_completedActivities(userID).intersection(topicActivityIds);
|
||||
|
||||
return completedTopicActivities.length >= numTwoPersonActivities;
|
||||
}
|
||||
|
||||
int currentTopicIndex(
|
||||
String userID,
|
||||
CoursePlanModel course,
|
||||
) {
|
||||
if (course.loadedTopics.isEmpty) return -1;
|
||||
for (int i = 0; i < course.loadedTopics.length; i++) {
|
||||
if (!_hasCompletedTopic(userID, course.loadedTopics[i], course)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Future<Map<String, List<User>>> topicsToUsers(
|
||||
Room room,
|
||||
CoursePlanModel course,
|
||||
) async {
|
||||
final Map<String, List<User>> topicUserMap = {};
|
||||
final users = await room.requestParticipants(
|
||||
[Membership.join, Membership.invite, Membership.knock],
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
for (final user in users) {
|
||||
if (user.id == BotName.byEnvironment) continue;
|
||||
final topicIndex = currentTopicIndex(user.id, course);
|
||||
if (topicIndex != -1) {
|
||||
final topicID = course.loadedTopics[topicIndex].uuid;
|
||||
topicUserMap.putIfAbsent(topicID, () => []).add(user);
|
||||
}
|
||||
}
|
||||
return topicUserMap;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,9 @@ import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
|||
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_plan_event.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_topic_model.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_user_event.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/join_rule_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
|
|
@ -23,96 +19,6 @@ extension CoursePlanRoomExtension on Room {
|
|||
return CoursePlanEvent.fromJson(event.content);
|
||||
}
|
||||
|
||||
CourseUserState? _courseUserState(String userID) {
|
||||
final event = getState(
|
||||
PangeaEventTypes.courseUser,
|
||||
userID,
|
||||
);
|
||||
if (event == null) return null;
|
||||
|
||||
try {
|
||||
return CourseUserState.fromJson(event.content);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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>>(),
|
||||
);
|
||||
}
|
||||
|
||||
bool hasCompletedActivity(
|
||||
String userID,
|
||||
String activityID,
|
||||
) {
|
||||
final state = _courseUserState(userID);
|
||||
if (state == null) return false;
|
||||
return state.hasCompletedActivity(activityID);
|
||||
}
|
||||
|
||||
bool _hasCompletedTopic(
|
||||
String userID,
|
||||
CourseTopicModel topic,
|
||||
CoursePlanModel course,
|
||||
) {
|
||||
final state = _courseUserState(userID);
|
||||
if (state == null) return false;
|
||||
|
||||
final topicIndex = course.loadedTopics.indexWhere(
|
||||
(t) => t.uuid == topic.uuid,
|
||||
);
|
||||
|
||||
if (topicIndex == -1) {
|
||||
throw Exception('Topic not found');
|
||||
}
|
||||
|
||||
final topicActivities = course.loadedTopics[topicIndex].loadedActivities;
|
||||
final topicActivityIds = topicActivities.map((a) => a.activityId).toSet();
|
||||
final numTwoPersonActivities =
|
||||
topicActivities.where((a) => a.req.numberOfParticipants <= 2).length;
|
||||
|
||||
final completedTopicActivities =
|
||||
state.completedActivities.intersection(topicActivityIds);
|
||||
|
||||
return completedTopicActivities.length >= numTwoPersonActivities;
|
||||
}
|
||||
|
||||
int currentTopicIndex(
|
||||
String userID,
|
||||
CoursePlanModel course,
|
||||
) {
|
||||
if (coursePlan == null) return -1;
|
||||
if (course.loadedTopics.isEmpty) return -1;
|
||||
|
||||
for (int i = 0; i < course.loadedTopics.length; i++) {
|
||||
if (!_hasCompletedTopic(userID, course.loadedTopics[i], course)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ownCurrentTopicIndex(CoursePlanModel course) =>
|
||||
currentTopicIndex(client.userID!, course);
|
||||
|
||||
String? activeActivityRoomId(String activityId) {
|
||||
for (final child in spaceChildren) {
|
||||
if (child.roomId == null) continue;
|
||||
|
|
@ -137,43 +43,6 @@ extension CoursePlanRoomExtension on Room {
|
|||
);
|
||||
}
|
||||
|
||||
Future<Map<String, List<User>>> topicsToUsers(CoursePlanModel course) async {
|
||||
final Map<String, List<User>> topicUserMap = {};
|
||||
final users = await requestParticipants(
|
||||
[Membership.join, Membership.invite, Membership.knock],
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
for (final user in users) {
|
||||
if (user.id == BotName.byEnvironment) continue;
|
||||
final topicIndex = currentTopicIndex(user.id, course);
|
||||
if (topicIndex != -1) {
|
||||
final topicID = course.loadedTopics[topicIndex].uuid;
|
||||
topicUserMap.putIfAbsent(topicID, () => []).add(user);
|
||||
}
|
||||
}
|
||||
return topicUserMap;
|
||||
}
|
||||
|
||||
Future<void> finishCourseActivity(
|
||||
String activityID,
|
||||
String roomID,
|
||||
) async {
|
||||
CourseUserState? state = _ownCourseState;
|
||||
state ??= CourseUserState(
|
||||
userID: client.userID!,
|
||||
completedActivities: {},
|
||||
);
|
||||
state.completeActivity(activityID, roomID);
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.courseUser,
|
||||
client.userID!,
|
||||
state.toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> launchActivityRoom(
|
||||
ActivityPlanModel activity,
|
||||
ActivityRole? role,
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
class CourseUserState {
|
||||
final String userID;
|
||||
|
||||
// Map of activityIds to list of roomIds
|
||||
final Map<String, List<String>> _completedActivities;
|
||||
|
||||
CourseUserState({
|
||||
required this.userID,
|
||||
required Map<String, List<String>> completedActivities,
|
||||
}) : _completedActivities = completedActivities;
|
||||
|
||||
void completeActivity(
|
||||
String activityID,
|
||||
String roomID,
|
||||
) {
|
||||
_completedActivities[activityID] ??= [];
|
||||
_completedActivities[activityID]!.add(roomID);
|
||||
}
|
||||
|
||||
Set<String> get completedActivities => _completedActivities.keys.toSet();
|
||||
|
||||
bool hasCompletedActivity(
|
||||
String activityID,
|
||||
) {
|
||||
return _completedActivities.containsKey(activityID);
|
||||
}
|
||||
|
||||
factory CourseUserState.fromJson(Map<String, dynamic> json) {
|
||||
final activityEntry = json['comp_act_by_topic'];
|
||||
|
||||
final Map<String, List<String>> activityMap = {};
|
||||
if (activityEntry != null) {
|
||||
activityEntry.forEach((key, value) {
|
||||
activityMap[key] = List<String>.from(value);
|
||||
});
|
||||
}
|
||||
|
||||
return CourseUserState(
|
||||
userID: json['user_id'],
|
||||
completedActivities: activityMap,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'user_id': userID,
|
||||
'comp_act_by_topic': _completedActivities,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -14,13 +14,14 @@ import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.
|
|||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/activity_summaries_provider.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_settings/pin_clipper.dart';
|
||||
import 'package:fluffychat/pangea/course_settings/topic_participant_list.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
|
||||
class CourseSettings extends StatelessWidget {
|
||||
class CourseSettings extends StatefulWidget {
|
||||
final Room room;
|
||||
final CoursePlanController controller;
|
||||
const CourseSettings(
|
||||
|
|
@ -29,13 +30,50 @@ class CourseSettings extends StatelessWidget {
|
|||
required this.room,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CourseSettings> createState() => CourseSettingsState();
|
||||
}
|
||||
|
||||
class CourseSettingsState extends State<CourseSettings>
|
||||
with ActivitySummariesProvider {
|
||||
CoursePlanController get controller => widget.controller;
|
||||
Room get room => widget.room;
|
||||
|
||||
bool _loading = true;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSummaries();
|
||||
}
|
||||
|
||||
Future<void> _loadSummaries() async {
|
||||
try {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
await loadRoomSummaries(
|
||||
room.spaceChildren.map((c) => c.roomId).whereType<String>().toList(),
|
||||
);
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.loading) {
|
||||
if (_loading || controller.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.error != null) {
|
||||
if (_error != null || controller.error != null) {
|
||||
return Center(
|
||||
child: ErrorIndicator(message: L10n.of(context).failedToLoadCourseInfo),
|
||||
);
|
||||
|
|
@ -82,20 +120,20 @@ class CourseSettings extends StatelessWidget {
|
|||
final double iconSize = isColumnMode ? 16.0 : 12.0;
|
||||
|
||||
final course = controller.course!;
|
||||
final currentTopicIndex = room.currentTopicIndex(
|
||||
final topicIndex = currentTopicIndex(
|
||||
room.client.userID!,
|
||||
course,
|
||||
);
|
||||
|
||||
return FutureBuilder(
|
||||
future: room.topicsToUsers(course),
|
||||
future: topicsToUsers(room, course),
|
||||
builder: (context, snapshot) {
|
||||
final topicsToUsers = snapshot.data ?? {};
|
||||
return Column(
|
||||
spacing: isColumnMode ? 40.0 : 36.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: course.loadedTopics.mapIndexed((index, topic) {
|
||||
final unlocked = index <= currentTopicIndex;
|
||||
final unlocked = index <= topicIndex;
|
||||
final usersInTopic = topicsToUsers[topic.uuid] ?? [];
|
||||
final activities = topic.loadedActivities;
|
||||
activities.sort(
|
||||
|
|
@ -207,6 +245,7 @@ class CourseSettings extends StatelessWidget {
|
|||
child: TopicActivitiesList(
|
||||
room: room,
|
||||
activities: activities,
|
||||
controller: this,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -223,10 +262,13 @@ class CourseSettings extends StatelessWidget {
|
|||
class TopicActivitiesList extends StatefulWidget {
|
||||
final Room room;
|
||||
final List<ActivityPlanModel> activities;
|
||||
final CourseSettingsState controller;
|
||||
|
||||
const TopicActivitiesList({
|
||||
super.key,
|
||||
required this.room,
|
||||
required this.activities,
|
||||
required this.controller,
|
||||
});
|
||||
@override
|
||||
State<TopicActivitiesList> createState() => TopicActivitiesListState();
|
||||
|
|
@ -255,7 +297,7 @@ class TopicActivitiesListState extends State<TopicActivitiesList> {
|
|||
itemBuilder: (context, index) {
|
||||
final activityId = widget.activities[index].activityId;
|
||||
|
||||
final complete = widget.room.hasCompletedActivity(
|
||||
final complete = widget.controller.hasCompletedActivity(
|
||||
widget.room.client.userID!,
|
||||
activityId,
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue