feat: integrate room preview endpoint (#4014)
* feat: integrate room preview endpoint * initial work for intermediary activity page * Update lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/pangea/chat_settings/utils/room_summary_extension.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * formatting --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
007621d808
commit
1b353afbac
14 changed files with 337 additions and 177 deletions
|
|
@ -713,8 +713,9 @@ abstract class AppRoutes {
|
|||
state,
|
||||
ActivitySessionStartPage(
|
||||
activityId: state.pathParameters['activityid']!,
|
||||
isNew: state.uri.queryParameters['new'] == 'true',
|
||||
roomId: state.uri.queryParameters['roomid'],
|
||||
parentId: state.pathParameters['spaceid']!,
|
||||
launch: state.uri.queryParameters['launch'] == 'true',
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
|
|||
|
|
@ -2241,7 +2241,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
if (room.isActivitySession == true && !room.activityHasStarted) {
|
||||
return ActivitySessionStartPage(
|
||||
activityId: room.activityId!,
|
||||
room: room,
|
||||
roomId: room.id,
|
||||
parentId: room.courseParent?.id,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_participant_indicator.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -17,6 +16,7 @@ class ActivityParticipantList extends StatelessWidget {
|
|||
final Room? room;
|
||||
final Room? course;
|
||||
final Function(String)? onTap;
|
||||
final Map<String, ActivityRoleModel> assignedRoles;
|
||||
|
||||
final bool Function(String)? canSelect;
|
||||
final bool Function(String)? isSelected;
|
||||
|
|
@ -25,7 +25,8 @@ class ActivityParticipantList extends StatelessWidget {
|
|||
const ActivityParticipantList({
|
||||
super.key,
|
||||
required this.activity,
|
||||
this.room,
|
||||
required this.assignedRoles,
|
||||
required this.room,
|
||||
this.course,
|
||||
this.onTap,
|
||||
this.canSelect,
|
||||
|
|
@ -40,7 +41,6 @@ class ActivityParticipantList extends StatelessWidget {
|
|||
builder: (context, participants) {
|
||||
final theme = Theme.of(context);
|
||||
final availableRoles = activity.roles;
|
||||
final assignedRoles = room?.assignedRoles ?? {};
|
||||
|
||||
final remainingMembers = participants.participants.where(
|
||||
(p) => !assignedRoles.values.any((r) => r.userId == p.id),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ 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/activity_summaries_provider.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';
|
||||
|
|
@ -23,23 +23,31 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
|||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
enum SessionState {
|
||||
/// The room hasn't been created yet
|
||||
notStarted,
|
||||
|
||||
/// The room has been created but the user hasn't selected a role yet. Non-admins haven't joined yet.
|
||||
notSelectedRole,
|
||||
|
||||
/// The user has selected a role but hasn't confirmed yet. Non-admins haven't joined yet.
|
||||
selectedRole,
|
||||
|
||||
/// The user has confirmed their role and is waiting for others to join. Non-admins have joined.
|
||||
confirmedRole,
|
||||
}
|
||||
|
||||
class ActivitySessionStartPage extends StatefulWidget {
|
||||
final String activityId;
|
||||
final bool isNew;
|
||||
final Room? room;
|
||||
final String? roomId;
|
||||
final String? parentId;
|
||||
final bool launch;
|
||||
|
||||
const ActivitySessionStartPage({
|
||||
super.key,
|
||||
required this.activityId,
|
||||
this.isNew = false,
|
||||
this.room,
|
||||
required this.parentId,
|
||||
this.roomId,
|
||||
this.launch = false,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -47,7 +55,8 @@ class ActivitySessionStartPage extends StatefulWidget {
|
|||
ActivitySessionStartController();
|
||||
}
|
||||
|
||||
class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
||||
class ActivitySessionStartController extends State<ActivitySessionStartPage>
|
||||
with ActivitySummariesProvider {
|
||||
ActivityPlanModel? activity;
|
||||
CoursePlanModel? course;
|
||||
|
||||
|
|
@ -63,12 +72,21 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
_loadActivity();
|
||||
|
||||
if (courseParent != null) {
|
||||
loadRoomSummaries(
|
||||
courseParent!.spaceChildren
|
||||
.map((c) => c.roomId)
|
||||
.whereType<String>()
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ActivitySessionStartPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.room?.id != widget.room?.id) {
|
||||
if (oldWidget.roomId != widget.roomId) {
|
||||
setState(() {
|
||||
_selectedRoleId = null;
|
||||
showInstructions = false;
|
||||
|
|
@ -86,25 +104,29 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Room? get room => widget.room;
|
||||
Room? get activityRoom => widget.roomId != null
|
||||
? Matrix.of(context).client.getRoomById(
|
||||
widget.roomId!,
|
||||
)
|
||||
: null;
|
||||
|
||||
Room? get parent => widget.parentId != null
|
||||
Room? get courseParent => widget.parentId != null
|
||||
? Matrix.of(context).client.getRoomById(
|
||||
widget.parentId!,
|
||||
)
|
||||
: null;
|
||||
|
||||
bool get isBotRoomMember =>
|
||||
room?.getParticipants().any(
|
||||
activityRoom?.getParticipants().any(
|
||||
(p) => p.id == BotName.byEnvironment,
|
||||
) ??
|
||||
false;
|
||||
|
||||
SessionState get state {
|
||||
if (room?.ownRoleState != null) return SessionState.confirmedRole;
|
||||
if (activityRoom?.ownRoleState != null) return SessionState.confirmedRole;
|
||||
if (_selectedRoleId != null) return SessionState.selectedRole;
|
||||
if (room == null) {
|
||||
return widget.isNew
|
||||
if (activityRoom == null) {
|
||||
return widget.roomId != null || widget.launch
|
||||
? SessionState.notSelectedRole
|
||||
: SessionState.notStarted;
|
||||
}
|
||||
|
|
@ -114,14 +136,14 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
String? get descriptionText {
|
||||
switch (state) {
|
||||
case SessionState.confirmedRole:
|
||||
return L10n.of(context).waitingToFillRole(room!.remainingRoles);
|
||||
return L10n.of(context).waitingToFillRole(activityRoom!.remainingRoles);
|
||||
case SessionState.selectedRole:
|
||||
return activity!.roles[_selectedRoleId!]!.goal;
|
||||
case SessionState.notStarted:
|
||||
return null;
|
||||
|
||||
case SessionState.notSelectedRole:
|
||||
return room?.isRoomAdmin ?? false
|
||||
return activityRoom?.isRoomAdmin ?? false
|
||||
? L10n.of(context).chooseRole
|
||||
: L10n.of(context).chooseRoleToParticipate;
|
||||
}
|
||||
|
|
@ -139,7 +161,7 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
}
|
||||
|
||||
final availableRoles = activity!.roles;
|
||||
final assignedRoles = room?.assignedRoles ?? {};
|
||||
final assignedRoles = activityRoom?.assignedRoles ?? {};
|
||||
final unassignedIds = availableRoles.keys
|
||||
.where((id) => !assignedRoles.containsKey(id))
|
||||
.toList();
|
||||
|
|
@ -148,20 +170,18 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
|
||||
bool isParticipantSelected(String id) {
|
||||
if (state == SessionState.confirmedRole) {
|
||||
return room?.ownRoleState?.id == id;
|
||||
return activityRoom?.ownRoleState?.id == 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;
|
||||
if (activityRoom != null) return false;
|
||||
return numOpenSessions(widget.activityId) > 0;
|
||||
}
|
||||
|
||||
bool get canPingParticipants {
|
||||
if (room == null || room?.courseParent == null) return false;
|
||||
if (activityRoom == null || courseParent == null) return false;
|
||||
return _pingCooldown == null || !_pingCooldown!.isActive;
|
||||
}
|
||||
|
||||
|
|
@ -178,22 +198,18 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
}
|
||||
|
||||
Future<bool> courseHasEnoughParticipants() async {
|
||||
final roomParticipants = widget.room?.getParticipants() ?? [];
|
||||
final courseParticipants = await parent?.requestParticipants(
|
||||
final courseParticipants = await courseParent?.requestParticipants(
|
||||
[Membership.join, Membership.invite, Membership.knock],
|
||||
false,
|
||||
true,
|
||||
) ??
|
||||
[];
|
||||
|
||||
final botInRoom = roomParticipants.any(
|
||||
(p) => p.id == BotName.byEnvironment,
|
||||
);
|
||||
final botInCourse = courseParticipants.any(
|
||||
(p) => p.id == BotName.byEnvironment,
|
||||
);
|
||||
|
||||
final addBotToAvailableUsers = !botInCourse && !botInRoom;
|
||||
final addBotToAvailableUsers = !botInCourse && !isBotRoomMember;
|
||||
final availableParticipants =
|
||||
courseParticipants.length + (addBotToAvailableUsers ? 1 : 0);
|
||||
return availableParticipants >= (activity?.req.numberOfParticipants ?? 0);
|
||||
|
|
@ -206,8 +222,8 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
error = null;
|
||||
});
|
||||
|
||||
if (parent?.coursePlan != null) {
|
||||
course = await CoursePlansRepo.get(parent!.coursePlan!.uuid);
|
||||
if (courseParent?.coursePlan != null) {
|
||||
course = await CoursePlansRepo.get(courseParent!.coursePlan!.uuid);
|
||||
}
|
||||
|
||||
final activities = await CourseActivityRepo.get(
|
||||
|
|
@ -227,37 +243,61 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> joinActivity() async {
|
||||
if (state != SessionState.selectedRole) return;
|
||||
if (widget.roomId == null) {
|
||||
throw Exception(
|
||||
"Cannot join activity: room ID is required but not provided",
|
||||
);
|
||||
}
|
||||
|
||||
final client = Matrix.of(context).client;
|
||||
if (activityRoom?.membership != Membership.join) {
|
||||
await client.joinRoom(
|
||||
widget.roomId!,
|
||||
serverName: courseParent?.spaceChildren
|
||||
.firstWhereOrNull(
|
||||
(child) => child.roomId == widget.roomId,
|
||||
)
|
||||
?.via,
|
||||
);
|
||||
|
||||
if (activityRoom == null || activityRoom!.membership != Membership.join) {
|
||||
await client.waitForRoomInSync(widget.roomId!, join: true);
|
||||
}
|
||||
|
||||
if (activityRoom == null || activityRoom!.membership != Membership.join) {
|
||||
throw Exception("Failed to join activity room. "
|
||||
"Room ID: ${widget.roomId}, "
|
||||
"Membership status: ${activityRoom?.membership}");
|
||||
}
|
||||
}
|
||||
|
||||
await activityRoom!.joinActivity(
|
||||
activity!.roles[_selectedRoleId!]!,
|
||||
);
|
||||
|
||||
context.go("/rooms/spaces/${widget.parentId}/${widget.roomId}");
|
||||
}
|
||||
|
||||
Future<void> confirmRoleSelection() async {
|
||||
if (state != SessionState.selectedRole) return;
|
||||
if (room != null) {
|
||||
if (activityRoom != null) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
future: () => activityRoom!.joinActivity(
|
||||
activity!.roles[_selectedRoleId!]!,
|
||||
),
|
||||
);
|
||||
} else if (widget.roomId != null) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: joinActivity,
|
||||
);
|
||||
} else {
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => parent!.launchActivityRoom(
|
||||
future: () => courseParent!.launchActivityRoom(
|
||||
activity!,
|
||||
activity!.roles[_selectedRoleId!],
|
||||
),
|
||||
|
|
@ -274,13 +314,13 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
throw Exception("No existing session to join");
|
||||
}
|
||||
|
||||
final sessionIds = parent!.openSessions(widget.activityId);
|
||||
final sessionIds = openSessions(widget.activityId);
|
||||
String? joinedSessionId;
|
||||
for (final sessionId in sessionIds) {
|
||||
try {
|
||||
await parent!.client.joinRoom(
|
||||
await courseParent!.client.joinRoom(
|
||||
sessionId,
|
||||
via: parent?.spaceChildren
|
||||
via: courseParent?.spaceChildren
|
||||
.firstWhereOrNull(
|
||||
(child) => child.roomId == sessionId,
|
||||
)
|
||||
|
|
@ -298,16 +338,16 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
throw Exception("Failed to join any existing session");
|
||||
}
|
||||
|
||||
final room = parent!.client.getRoomById(joinedSessionId);
|
||||
final room = courseParent!.client.getRoomById(joinedSessionId);
|
||||
if (room == null || room.membership != Membership.join) {
|
||||
await parent!.client.waitForRoomInSync(joinedSessionId, join: true);
|
||||
await courseParent!.client.waitForRoomInSync(joinedSessionId, join: true);
|
||||
}
|
||||
|
||||
return joinedSessionId;
|
||||
}
|
||||
|
||||
Future<void> pingCourse() async {
|
||||
if (room?.courseParent == null) {
|
||||
if (activityRoom?.courseParent == null) {
|
||||
throw Exception("Activity is not part of a course");
|
||||
}
|
||||
|
||||
|
|
@ -321,14 +361,15 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
if (mounted) setState(() {});
|
||||
});
|
||||
|
||||
await room!.courseParent!.sendEvent(
|
||||
await activityRoom!.courseParent!.sendEvent(
|
||||
{
|
||||
"body": L10n.of(context).pingParticipantsNotification(
|
||||
room!.client.userID!.localpart ?? room!.client.userID!,
|
||||
room!.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
|
||||
activityRoom!.client.userID!.localpart ??
|
||||
activityRoom!.client.userID!,
|
||||
activityRoom!.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
|
||||
),
|
||||
"msgtype": "m.text",
|
||||
"pangea.activity.session_room_id": room!.id,
|
||||
"pangea.activity.session_room_id": activityRoom!.id,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -347,7 +388,7 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
}
|
||||
|
||||
Future<void> playWithBot() async {
|
||||
if (room == null) {
|
||||
if (activityRoom == null) {
|
||||
throw Exception("Room is null");
|
||||
}
|
||||
|
||||
|
|
@ -355,15 +396,15 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
|
|||
throw Exception("Bot is a member of the room");
|
||||
}
|
||||
|
||||
final future = room!.client.onRoomState.stream
|
||||
final future = activityRoom!.client.onRoomState.stream
|
||||
.where(
|
||||
(state) =>
|
||||
state.roomId == room!.id &&
|
||||
state.roomId == activityRoom!.id &&
|
||||
state.state.type == PangeaEventTypes.activityRole &&
|
||||
state.state.senderId == BotName.byEnvironment,
|
||||
)
|
||||
.first;
|
||||
room!.invite(BotName.byEnvironment);
|
||||
activityRoom!.invite(BotName.byEnvironment);
|
||||
await future.timeout(const Duration(seconds: 30));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,8 +78,8 @@ class ActivitySessionStartView extends StatelessWidget {
|
|||
children: [
|
||||
ActivitySummary(
|
||||
activity: controller.activity!,
|
||||
room: controller.room,
|
||||
course: controller.parent,
|
||||
room: controller.activityRoom,
|
||||
course: controller.courseParent,
|
||||
showInstructions:
|
||||
controller.showInstructions,
|
||||
toggleInstructions:
|
||||
|
|
@ -89,6 +89,12 @@ class ActivitySessionStartView extends StatelessWidget {
|
|||
controller.isParticipantSelected,
|
||||
canSelectParticipant:
|
||||
controller.canSelectParticipant,
|
||||
assignedRoles: controller
|
||||
.roomSummaries?[
|
||||
controller.widget.roomId]
|
||||
?.activityRoles
|
||||
.roles ??
|
||||
{},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -134,7 +140,8 @@ class ActivitySessionStartView extends StatelessWidget {
|
|||
)
|
||||
else if (controller.state ==
|
||||
SessionState.confirmedRole) ...[
|
||||
if (controller.room!.courseParent !=
|
||||
if (controller.courseParent!
|
||||
.courseParent !=
|
||||
null)
|
||||
ElevatedButton(
|
||||
style: buttonStyle,
|
||||
|
|
@ -160,7 +167,7 @@ class ActivitySessionStartView extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
if (controller
|
||||
.room!.isRoomAdmin) ...[
|
||||
.courseParent!.isRoomAdmin) ...[
|
||||
if (!controller.isBotRoomMember)
|
||||
ElevatedButton(
|
||||
style: buttonStyle,
|
||||
|
|
@ -185,7 +192,7 @@ class ActivitySessionStartView extends StatelessWidget {
|
|||
ElevatedButton(
|
||||
style: buttonStyle,
|
||||
onPressed: () => context.go(
|
||||
"/rooms/${controller.room!.id}/invite",
|
||||
"/rooms/${controller.courseParent!.id}/invite",
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
|
|
@ -212,7 +219,7 @@ class ActivitySessionStartView extends StatelessWidget {
|
|||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
controller.room
|
||||
controller.courseParent
|
||||
?.isRoomAdmin ??
|
||||
true
|
||||
? L10n.of(context).start
|
||||
|
|
@ -268,11 +275,11 @@ class _ActivityStartButtons extends StatelessWidget {
|
|||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (controller.parent?.canInvite ?? false)
|
||||
if (controller.courseParent?.canInvite ?? false)
|
||||
ElevatedButton(
|
||||
style: buttonStyle,
|
||||
onPressed: () => context.go(
|
||||
"/rooms/spaces/${controller.parent!.id}/invite",
|
||||
"/rooms/spaces/${controller.courseParent!.id}/invite",
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
@ -287,7 +294,7 @@ class _ActivityStartButtons extends StatelessWidget {
|
|||
ElevatedButton(
|
||||
style: buttonStyle,
|
||||
onPressed: () => context.go(
|
||||
"/rooms/spaces/${controller.widget.parentId}/activity/${controller.widget.activityId}?new=true",
|
||||
"/rooms/spaces/${controller.widget.parentId}/activity/${controller.widget.activityId}?launch=true",
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:fluffychat/l10n/l10n.dart';
|
|||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_participant_list.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_session_details_row.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
|
|
@ -18,6 +19,7 @@ class ActivitySummary extends StatelessWidget {
|
|||
final ActivityPlanModel activity;
|
||||
final Room? room;
|
||||
final Room? course;
|
||||
final Map<String, ActivityRoleModel>? assignedRoles;
|
||||
|
||||
final bool showInstructions;
|
||||
final VoidCallback toggleInstructions;
|
||||
|
|
@ -32,6 +34,7 @@ class ActivitySummary extends StatelessWidget {
|
|||
required this.activity,
|
||||
required this.showInstructions,
|
||||
required this.toggleInstructions,
|
||||
this.assignedRoles,
|
||||
this.onTapParticipant,
|
||||
this.canSelectParticipant,
|
||||
this.isParticipantSelected,
|
||||
|
|
@ -67,6 +70,7 @@ class ActivitySummary extends StatelessWidget {
|
|||
ActivityParticipantList(
|
||||
activity: activity,
|
||||
room: room,
|
||||
assignedRoles: room?.assignedRoles ?? assignedRoles ?? {},
|
||||
course: course,
|
||||
onTap: onTapParticipant,
|
||||
canSelect: canSelectParticipant,
|
||||
|
|
|
|||
90
lib/pangea/chat_settings/utils/room_summary_extension.dart
Normal file
90
lib/pangea/chat_settings/utils/room_summary_extension.dart
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' hide Client;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/matrix_api_lite/generated/api.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
|
||||
extension RoomSummaryExtension on Api {
|
||||
Future<RoomSummariesResponse> getRoomSummaries(List<String> roomIds) async {
|
||||
final requestUri = Uri(
|
||||
path: '/_synapse/client/unstable/org.pangea/room_preview',
|
||||
queryParameters: {
|
||||
'rooms': roomIds.join(","),
|
||||
},
|
||||
);
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
request.headers['content-type'] = 'application/json';
|
||||
request.headers['authorization'] = 'Bearer ${bearerToken!}';
|
||||
final response = await httpClient.send(request);
|
||||
final responseBody = await response.stream.toBytes();
|
||||
final responseString = utf8.decode(responseBody);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(
|
||||
'HTTP error response: statusCode=${response.statusCode}, body=$responseString',
|
||||
);
|
||||
}
|
||||
final json = jsonDecode(responseString);
|
||||
return RoomSummariesResponse.fromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomSummaryRequest on Client {
|
||||
Future<RoomSummariesResponse> requestRoomSummaries(List<String> roomIds) =>
|
||||
getRoomSummaries(roomIds);
|
||||
}
|
||||
|
||||
class RoomSummariesResponse {
|
||||
Map<String, RoomSummaryResponse> summaries;
|
||||
|
||||
RoomSummariesResponse({required this.summaries});
|
||||
|
||||
factory RoomSummariesResponse.fromJson(Map<String, dynamic> json) {
|
||||
final summaries = <String, RoomSummaryResponse>{};
|
||||
json["rooms"].forEach((key, value) {
|
||||
if (value.isNotEmpty) {
|
||||
summaries[key] = RoomSummaryResponse.fromJson(value);
|
||||
}
|
||||
});
|
||||
return RoomSummariesResponse(summaries: summaries);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
summaries.forEach((key, value) {
|
||||
json[key] = value.toJson();
|
||||
});
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomSummaryResponse {
|
||||
final ActivityPlanModel activityPlan;
|
||||
final ActivityRolesModel activityRoles;
|
||||
|
||||
RoomSummaryResponse({
|
||||
required this.activityPlan,
|
||||
required this.activityRoles,
|
||||
});
|
||||
|
||||
factory RoomSummaryResponse.fromJson(Map<String, dynamic> json) {
|
||||
return RoomSummaryResponse(
|
||||
activityPlan: ActivityPlanModel.fromJson(
|
||||
json[PangeaEventTypes.activityPlan]?["default"]?["content"] ?? {},
|
||||
),
|
||||
activityRoles: ActivityRolesModel.fromJson(
|
||||
json[PangeaEventTypes.activityRole]?["default"]?["content"] ?? {},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
PangeaEventTypes.activityPlan: activityPlan.toJson(),
|
||||
PangeaEventTypes.activityRole: activityRoles.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -9,19 +9,20 @@ 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';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
class ActivityTemplateChatListItem extends StatelessWidget {
|
||||
final Room space;
|
||||
final Function(SpaceRoomsChunk) joinActivity;
|
||||
final ActivityPlanModel activity;
|
||||
final List<ExtendedSpaceRoomsChunk> sessions;
|
||||
final Function(ExtendedSpaceRoomsChunk) joinActivity;
|
||||
|
||||
const ActivityTemplateChatListItem({
|
||||
super.key,
|
||||
required this.space,
|
||||
required this.joinActivity,
|
||||
required this.activity,
|
||||
required this.sessions,
|
||||
required this.joinActivity,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -105,7 +106,10 @@ class ActivityTemplateChatListItem extends StatelessWidget {
|
|||
height: 24.0,
|
||||
width: 40.0,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => joinActivity(e.chunk),
|
||||
onPressed: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => joinActivity(e),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.all(0),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ 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/activity_summaries_provider.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/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';
|
||||
|
|
@ -46,7 +46,8 @@ class CourseChats extends StatefulWidget {
|
|||
State<CourseChats> createState() => CourseChatsController();
|
||||
}
|
||||
|
||||
class CourseChatsController extends State<CourseChats> {
|
||||
class CourseChatsController extends State<CourseChats>
|
||||
with ActivitySummariesProvider {
|
||||
String get roomId => widget.roomId;
|
||||
Room? get room => widget.client.getRoomById(widget.roomId);
|
||||
|
||||
|
|
@ -125,20 +126,7 @@ class CourseChatsController extends State<CourseChats> {
|
|||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (discoveredChildren == null || roomSummaries == null) return {};
|
||||
final Map<ActivityPlanModel, List<ExtendedSpaceRoomsChunk>> sessionsMap =
|
||||
{};
|
||||
|
||||
|
|
@ -146,14 +134,16 @@ class CourseChatsController extends State<CourseChats> {
|
|||
if (chunk.roomType?.startsWith(PangeaRoomTypes.activitySession) != true) {
|
||||
continue;
|
||||
}
|
||||
final activityId = chunk.roomType!.split(":").last;
|
||||
final activity = course?.activityById(activityId);
|
||||
if (activity == null) {
|
||||
|
||||
final summary = roomSummaries?[chunk.roomId];
|
||||
if (summary == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final users = roomsToUsers[chunk.roomId];
|
||||
if (users != null && activity.req.numberOfParticipants <= users.length) {
|
||||
final activity = summary.activityPlan;
|
||||
final users =
|
||||
summary.activityRoles.roles.values.map((r) => r.userId).toList();
|
||||
if (activity.req.numberOfParticipants <= users.length) {
|
||||
// Don't show full activities
|
||||
continue;
|
||||
}
|
||||
|
|
@ -162,8 +152,8 @@ class CourseChatsController extends State<CourseChats> {
|
|||
sessionsMap[activity]!.add(
|
||||
ExtendedSpaceRoomsChunk(
|
||||
chunk: chunk,
|
||||
activityId: activityId,
|
||||
userIds: users ?? [],
|
||||
activityId: activity.activityId,
|
||||
userIds: users,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -221,6 +211,9 @@ class CourseChatsController extends State<CourseChats> {
|
|||
try {
|
||||
await _loadHierarchy(activeSpace: room, reload: reload);
|
||||
await _joinDefaultChats();
|
||||
await loadRoomSummaries(
|
||||
room.spaceChildren.map((c) => c.roomId).whereType<String>().toList(),
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logs().w('Unable to load hierarchy', e, s);
|
||||
if (mounted) {
|
||||
|
|
@ -443,6 +436,42 @@ class CourseChatsController extends State<CourseChats> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> joinActivity(
|
||||
String activityId,
|
||||
ExtendedSpaceRoomsChunk chunk,
|
||||
) async {
|
||||
final hasRole = chunk.userIds.contains(widget.client.userID);
|
||||
final roomId = chunk.chunk.roomId;
|
||||
if (!hasRole) {
|
||||
context.go(
|
||||
"/rooms/spaces/${widget.roomId}/activity/$activityId?roomid=$roomId",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await widget.client.joinRoom(
|
||||
roomId,
|
||||
via: widget.client
|
||||
.getRoomById(widget.roomId)
|
||||
?.spaceChildren
|
||||
.firstWhereOrNull(
|
||||
(child) => child.roomId == roomId,
|
||||
)
|
||||
?.via,
|
||||
);
|
||||
|
||||
final room = widget.client.getRoomById(roomId);
|
||||
if (room == null || room.membership != Membership.join) {
|
||||
await widget.client.waitForRoomInSync(roomId, join: true);
|
||||
}
|
||||
|
||||
if (widget.client.getRoomById(roomId) == null) {
|
||||
throw Exception("Failed to join room");
|
||||
}
|
||||
|
||||
context.go("/rooms/spaces/${widget.roomId}/$roomId");
|
||||
}
|
||||
|
||||
void chatContextAction(
|
||||
Room room,
|
||||
BuildContext posContext, [
|
||||
|
|
|
|||
|
|
@ -204,9 +204,10 @@ class CourseChatsView extends StatelessWidget {
|
|||
final sessions = discoveredSessions[i].value;
|
||||
return ActivityTemplateChatListItem(
|
||||
space: room,
|
||||
joinActivity: controller.joinChildRoom,
|
||||
activity: activity,
|
||||
sessions: sessions,
|
||||
joinActivity: (e) =>
|
||||
controller.joinActivity(activity.activityId, e),
|
||||
);
|
||||
}
|
||||
i -= discoveredSessions.length;
|
||||
|
|
|
|||
51
lib/pangea/course_plans/activity_summaries_provider.dart
Normal file
51
lib/pangea/course_plans/activity_summaries_provider.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
|
||||
Map<String, RoomSummaryResponse>? roomSummaries;
|
||||
|
||||
Future<void> loadRoomSummaries(List<String> roomIds) async {
|
||||
if (roomIds.isEmpty) {
|
||||
roomSummaries = {};
|
||||
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});
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> openSessions(String activityId) {
|
||||
if (roomSummaries == null || roomSummaries!.isEmpty) return {};
|
||||
final Set<String> sessions = {};
|
||||
|
||||
for (final entry in roomSummaries!.entries) {
|
||||
final summary = entry.value;
|
||||
final roomId = entry.key;
|
||||
|
||||
if (summary.activityPlan.activityId != activityId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final isOpen = summary.activityRoles.roles.length <
|
||||
summary.activityPlan.req.numberOfParticipants;
|
||||
|
||||
if (isOpen) {
|
||||
sessions.add(roomId);
|
||||
}
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
int numOpenSessions(String activityId) => openSessions(activityId).length;
|
||||
}
|
||||
|
|
@ -62,23 +62,6 @@ extension CoursePlanRoomExtension on Room {
|
|||
);
|
||||
}
|
||||
|
||||
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(
|
||||
String userID,
|
||||
String activityID,
|
||||
|
|
@ -177,25 +160,6 @@ extension CoursePlanRoomExtension on Room {
|
|||
return topicUserMap;
|
||||
}
|
||||
|
||||
Future<void> joinCourseActivity(
|
||||
String activityID,
|
||||
String roomID,
|
||||
) async {
|
||||
CourseUserState? state = _ownCourseState;
|
||||
state ??= CourseUserState(
|
||||
userID: client.userID!,
|
||||
completedActivities: {},
|
||||
joinActivities: {},
|
||||
);
|
||||
state.joinActivity(activityID, roomID);
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.courseUser,
|
||||
client.userID!,
|
||||
state.toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> finishCourseActivity(
|
||||
String activityID,
|
||||
String roomID,
|
||||
|
|
@ -204,7 +168,6 @@ extension CoursePlanRoomExtension on Room {
|
|||
state ??= CourseUserState(
|
||||
userID: client.userID!,
|
||||
completedActivities: {},
|
||||
joinActivities: {},
|
||||
);
|
||||
state.completeActivity(activityID, roomID);
|
||||
await client.setRoomStateWithKey(
|
||||
|
|
@ -285,11 +248,6 @@ extension CoursePlanRoomExtension on Room {
|
|||
if (pangeaSpaceParents.isEmpty) {
|
||||
await client.waitForRoomInSync(roomID);
|
||||
}
|
||||
|
||||
await joinCourseActivity(
|
||||
activity.activityId,
|
||||
roomID,
|
||||
);
|
||||
return roomID;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,22 +3,11 @@ class CourseUserState {
|
|||
|
||||
// Map of activityIds to list of roomIds
|
||||
final Map<String, List<String>> _completedActivities;
|
||||
final Map<String, List<String>> _joinedActivities;
|
||||
|
||||
CourseUserState({
|
||||
required this.userID,
|
||||
required Map<String, List<String>> completedActivities,
|
||||
required Map<String, List<String>> joinActivities,
|
||||
}) : _completedActivities = completedActivities,
|
||||
_joinedActivities = joinActivities;
|
||||
|
||||
void joinActivity(
|
||||
String activityID,
|
||||
String roomID,
|
||||
) {
|
||||
_joinedActivities[activityID] ??= [];
|
||||
_joinedActivities[activityID]!.add(roomID);
|
||||
}
|
||||
}) : _completedActivities = completedActivities;
|
||||
|
||||
void completeActivity(
|
||||
String activityID,
|
||||
|
|
@ -28,11 +17,7 @@ class CourseUserState {
|
|||
_completedActivities[activityID]!.add(roomID);
|
||||
}
|
||||
|
||||
Map<String, List<String>> get joinedActivities => _joinedActivities;
|
||||
|
||||
Set<String> get completedActivities => _completedActivities.keys.toSet();
|
||||
Set<String> get joinedActivityRooms =>
|
||||
_joinedActivities.values.expand((e) => e).toSet();
|
||||
|
||||
bool hasCompletedActivity(
|
||||
String activityID,
|
||||
|
|
@ -42,7 +27,6 @@ class CourseUserState {
|
|||
|
||||
factory CourseUserState.fromJson(Map<String, dynamic> json) {
|
||||
final activityEntry = json['comp_act_by_topic'];
|
||||
final joinEntry = json['join_act_by_topic'];
|
||||
|
||||
final Map<String, List<String>> activityMap = {};
|
||||
if (activityEntry != null) {
|
||||
|
|
@ -51,17 +35,9 @@ class CourseUserState {
|
|||
});
|
||||
}
|
||||
|
||||
final Map<String, List<String>> joinMap = {};
|
||||
if (joinEntry != null) {
|
||||
joinEntry.forEach((key, value) {
|
||||
joinMap[key] = List<String>.from(value);
|
||||
});
|
||||
}
|
||||
|
||||
return CourseUserState(
|
||||
userID: json['user_id'],
|
||||
completedActivities: activityMap,
|
||||
joinActivities: joinMap,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +45,6 @@ class CourseUserState {
|
|||
return {
|
||||
'user_id': userID,
|
||||
'comp_act_by_topic': _completedActivities,
|
||||
'join_act_by_topic': _joinedActivities,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -284,7 +284,6 @@ class TopicActivitiesListState extends State<TopicActivitiesList> {
|
|||
fontSize: isColumnMode ? 20.0 : 12.0,
|
||||
fontSizeSmall: isColumnMode ? 12.0 : 8.0,
|
||||
iconSize: isColumnMode ? 12.0 : 8.0,
|
||||
openSessions: widget.room.numOpenSessions(activityId),
|
||||
),
|
||||
if (complete)
|
||||
Container(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue