some activity / invite page tweaks (#3958)

This commit is contained in:
ggurdin 2025-09-12 10:12:12 -04:00 committed by GitHub
parent 7d46892a39
commit 40137c226a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 276 additions and 160 deletions

View file

@ -83,6 +83,10 @@ class ActivityParticipantIndicator extends StatelessWidget {
radius: 30.0,
backgroundColor:
theme.colorScheme.primaryContainer,
child: const Icon(
Icons.question_mark,
size: 30.0,
),
),
Text(
name,

View file

@ -9,10 +9,12 @@ 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';
class ActivityParticipantList extends StatelessWidget {
final ActivityPlanModel activity;
final Room? room;
final Room? course;
final Function(String)? onTap;
final bool Function(String)? canSelect;
@ -23,6 +25,7 @@ class ActivityParticipantList extends StatelessWidget {
super.key,
required this.activity,
this.room,
this.course,
this.onTap,
this.canSelect,
this.isSelected,
@ -50,10 +53,24 @@ class ActivityParticipantList extends StatelessWidget {
spacing: 12.0,
runSpacing: 12.0,
children: availableRoles.values.map((availableRole) {
final assignedRole = assignedRoles[availableRole.id];
final user = participants.participants.firstWhereOrNull(
(u) => u.id == assignedRole?.userId,
);
final selected =
isSelected != null ? isSelected!(availableRole.id) : false;
final assignedRole = assignedRoles[availableRole.id] ??
(selected
? ActivityRoleModel(
id: availableRole.id,
userId: Matrix.of(context).client.userID!,
role: availableRole.name,
)
: null);
final User? user = participants.participants.firstWhereOrNull(
(u) => u.id == assignedRole?.userId,
) ??
course?.getParticipants().firstWhereOrNull(
(u) => u.id == assignedRole?.userId,
);
final selectable =
canSelect != null ? canSelect!(availableRole.id) : true;
@ -67,9 +84,7 @@ class ActivityParticipantList extends StatelessWidget {
onTap: onTap != null && selectable
? () => onTap!(availableRole.id)
: null,
selected: isSelected != null
? isSelected!(availableRole.id)
: false,
selected: selected,
);
}).toList(),
),

View file

@ -16,6 +16,7 @@ import 'package:fluffychat/pangea/course_plans/course_activity_repo.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
@ -287,6 +288,27 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage> {
);
}
Future<void> playWithBot() async {
if (room == null) {
throw Exception("Room is null");
}
if (isBotRoomMember) {
throw Exception("Bot is a member of the room");
}
final future = room!.client.onRoomState.stream
.where(
(state) =>
state.roomId == room!.id &&
state.state.type == PangeaEventTypes.activityRole &&
state.state.senderId == BotName.byEnvironment,
)
.first;
room!.invite(BotName.byEnvironment);
await future.timeout(const Duration(seconds: 30));
}
@override
Widget build(BuildContext context) => ActivitySessionStartView(this);
}

View file

@ -7,9 +7,7 @@ import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_summary_widget.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/common/widgets/share_room_button.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
@ -56,19 +54,6 @@ class ActivitySessionStartView extends StatelessWidget {
),
),
),
actions: [
if (controller.room != null)
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: SizedBox(
width: 40.0,
height: 40.0,
child: Center(
child: ShareRoomButton(room: controller.room!),
),
),
),
],
),
body: SafeArea(
child: controller.loading
@ -94,6 +79,7 @@ class ActivitySessionStartView extends StatelessWidget {
ActivitySummary(
activity: controller.activity!,
room: controller.room,
course: controller.parent,
showInstructions:
controller.showInstructions,
toggleInstructions:
@ -119,138 +105,164 @@ class ActivitySessionStartView extends StatelessWidget {
color: theme.colorScheme.surface,
),
padding: const EdgeInsets.all(24.0),
child: Column(
spacing: 16.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (controller.descriptionText != null)
Text(
controller.descriptionText!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
Flexible(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.maxTimelineWidth,
),
textAlign: TextAlign.center,
),
if (controller.state ==
SessionState.notStarted) ...[
ElevatedButton(
style: buttonStyle,
onPressed: () => context.go(
"/rooms/spaces/${controller.widget.parentId}/activity/${controller.widget.activityId}?new=true",
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
child: Column(
spacing: 16.0,
children: [
Text(
L10n.of(context).startNewSession,
),
],
),
),
ElevatedButton(
style: buttonStyle,
onPressed:
controller.canJoinExistingSession
? () async {
final resp =
await showFutureLoadingDialog(
context: context,
future: controller
.joinExistingSession,
);
if (!resp.isError) {
context.go(
"/rooms/spaces/${controller.widget.parentId}/${resp.result}",
);
}
}
: null,
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
L10n.of(context).joinOpenSession,
),
],
),
),
] else if (controller.state ==
SessionState.confirmedRole) ...[
if (controller.room!.courseParent != null)
ElevatedButton(
style: buttonStyle,
onPressed: () =>
showFutureLoadingDialog(
context: context,
future: controller.pingCourse,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
if (controller.descriptionText !=
null)
Text(
L10n.of(context).pingParticipants,
controller.descriptionText!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
],
),
),
if (controller.room!.isRoomAdmin) ...[
if (!controller.isBotRoomMember)
ElevatedButton(
style: buttonStyle,
onPressed: () =>
showFutureLoadingDialog(
context: context,
future: () => controller.room!
.invite(BotName.byEnvironment),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
L10n.of(context).playWithBot,
if (controller.state ==
SessionState.notStarted) ...[
ElevatedButton(
style: buttonStyle,
onPressed: () => context.go(
"/rooms/spaces/${controller.widget.parentId}/activity/${controller.widget.activityId}?new=true",
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
L10n.of(context)
.startNewSession,
),
],
),
),
ElevatedButton(
style: buttonStyle,
onPressed: controller
.canJoinExistingSession
? () async {
final resp =
await showFutureLoadingDialog(
context: context,
future: controller
.joinExistingSession,
);
if (!resp.isError) {
context.go(
"/rooms/spaces/${controller.widget.parentId}/${resp.result}",
);
}
}
: null,
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
L10n.of(context)
.joinOpenSession,
),
],
),
),
] else if (controller.state ==
SessionState.confirmedRole) ...[
if (controller.room!.courseParent !=
null)
ElevatedButton(
style: buttonStyle,
onPressed: () =>
showFutureLoadingDialog(
context: context,
future: controller.pingCourse,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
L10n.of(context)
.pingParticipants,
),
],
),
),
if (controller
.room!.isRoomAdmin) ...[
if (!controller.isBotRoomMember)
ElevatedButton(
style: buttonStyle,
onPressed: () =>
showFutureLoadingDialog(
context: context,
future:
controller.playWithBot,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment
.center,
children: [
Text(
L10n.of(context)
.playWithBot,
),
],
),
),
ElevatedButton(
style: buttonStyle,
onPressed: () => context.go(
"/rooms/${controller.room!.id}/invite",
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
L10n.of(context)
.inviteFriends,
),
],
),
),
],
),
),
ElevatedButton(
style: buttonStyle,
onPressed: () => context.go(
"/rooms/${controller.room!.id}/invite",
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
L10n.of(context).inviteFriends,
] else
ElevatedButton(
style: buttonStyle,
onPressed:
controller.enableButtons
? controller
.confirmRoleSelection
: null,
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
controller.room
?.isRoomAdmin ??
true
? L10n.of(context).start
: L10n.of(context)
.confirm,
),
],
),
),
],
),
),
],
] else
ElevatedButton(
style: buttonStyle,
onPressed: controller.enableButtons
? controller.confirmRoleSelection
: null,
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
controller.room?.isRoomAdmin ?? true
? L10n.of(context).start
: L10n.of(context).confirm,
),
],
),
),
),
],
),
),

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
@ -15,6 +17,7 @@ import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_en
class ActivitySummary extends StatelessWidget {
final ActivityPlanModel activity;
final Room? room;
final Room? course;
final bool showInstructions;
final VoidCallback toggleInstructions;
@ -34,6 +37,7 @@ class ActivitySummary extends StatelessWidget {
this.isParticipantSelected,
this.getParticipantOpacity,
this.room,
this.course,
});
@override
@ -48,14 +52,22 @@ class ActivitySummary extends StatelessWidget {
child: Column(
spacing: 4.0,
children: [
ImageByUrl(
imageUrl: activity.imageURL,
width: 80.0,
borderRadius: BorderRadius.circular(20),
LayoutBuilder(
builder: (context, constraints) {
return ImageByUrl(
imageUrl: activity.imageURL,
width: min(
constraints.maxWidth,
MediaQuery.sizeOf(context).height * 0.5,
),
borderRadius: BorderRadius.circular(20),
);
},
),
ActivityParticipantList(
activity: activity,
room: room,
course: course,
onTap: onTapParticipant,
canSelect: canSelectParticipant,
isSelected: isParticipantSelected,

View file

@ -17,11 +17,11 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
enum InvitationFilter {
participants,
space,
contacts,
knocking,
invited,
participants,
public;
static InvitationFilter? fromString(String value) {
@ -268,6 +268,9 @@ class PangeaInvitationSelectionController
.toList();
contacts.sort(_sortUsers);
if (_room?.isSpace ?? false) {
contacts.removeWhere((u) => u.id == BotName.byEnvironment);
}
return contacts;
}
@ -341,8 +344,14 @@ class PangeaInvitationSelectionController
} finally {
setState(() => loading = false);
}
final results = response.results;
if (_room?.isSpace ?? false) {
results.removeWhere((profile) => profile.userId == BotName.byEnvironment);
}
setState(() {
foundProfiles = List<Profile>.from(response.results);
foundProfiles = List<Profile>.from(results);
if (text.isValidMatrixId &&
foundProfiles.indexWhere((profile) => text == profile.userId) == -1) {
setState(
@ -359,6 +368,7 @@ class PangeaInvitationSelectionController
[Membership.join, Membership.invite].contains(user.membership),
)
.toList();
foundProfiles.removeWhere(
(profile) =>
participants?.indexWhere((u) => u.id == profile.userId) != -1 &&

View file

@ -130,7 +130,16 @@ class PangeaInvitationSelectionView extends StatelessWidget {
spacing: 12.0,
children: controller.availableFilters.map((filter) {
return FilterChip(
label: Text(controller.filterLabel(filter)),
label: filter == InvitationFilter.participants
? Row(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.group, size: 16.0),
Text(controller.filterLabel(filter)),
],
)
: Text(controller.filterLabel(filter)),
onSelected: (_) => controller.setFilter(filter),
selected: controller.filter == filter,
);

View file

@ -812,12 +812,14 @@ class CourseChatsController extends State<CourseChats> {
final leaveTimeline = leaveUpdate?[widget.roomId]?.timeline?.events;
if (joinTimeline == null && leaveTimeline == null) return false;
final bool hasJoinUpdate = joinTimeline!.any(
(event) => event.type == EventTypes.SpaceChild,
);
final bool hasLeaveUpdate = leaveTimeline!.any(
(event) => event.type == EventTypes.SpaceChild,
);
final bool hasJoinUpdate = joinTimeline?.any(
(event) => event.type == EventTypes.SpaceChild,
) ??
false;
final bool hasLeaveUpdate = leaveTimeline?.any(
(event) => event.type == EventTypes.SpaceChild,
) ??
false;
return hasJoinUpdate || hasLeaveUpdate;
}

View file

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
@ -27,10 +28,15 @@ class SelectedCourseController extends State<SelectedCourse> {
final client = Matrix.of(context).client;
Uint8List? avatar;
Uri? avatarUrl;
if (course.imageUrl != null) {
final imageUrl = course.imageUrl ??
course.loadedTopics
.lastWhereOrNull((topic) => topic.imageUrl != null)
?.imageUrl;
if (imageUrl != null) {
try {
final Response response = await http.get(
Uri.parse(course.imageUrl!),
Uri.parse(imageUrl),
headers: {
'Authorization':
'Bearer ${MatrixState.pangeaController.userController.accessToken}',

View file

@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:matrix/matrix.dart' as sdk;
import 'package:matrix/matrix.dart';
@ -15,6 +18,7 @@ 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';
import 'package:fluffychat/widgets/matrix.dart';
extension CoursePlanRoomExtension on Room {
CoursePlanEvent? get coursePlan {
@ -101,6 +105,7 @@ extension CoursePlanRoomExtension on Room {
}
final activityIds = course.loadedTopics[topicIndex].loadedActivities
.where((a) => a.req.numberOfParticipants <= 2)
.map((a) => a.activityId)
.toList();
return state.completedActivities.toSet().containsAll(activityIds);
@ -214,6 +219,25 @@ extension CoursePlanRoomExtension on Room {
ActivityPlanModel activity,
ActivityRole? role,
) async {
Uri? avatarUrl;
if (activity.imageURL != null) {
try {
final http.Response response = await http.get(
Uri.parse(activity.imageURL!),
headers: {
'Authorization':
'Bearer ${MatrixState.pangeaController.userController.accessToken}',
},
);
if (response.statusCode != 200) {
throw Exception('Failed to load course image');
}
final avatar = response.bodyBytes;
avatarUrl = await client.uploadContent(avatar);
} catch (e) {
debugPrint("Error fetching course image: $e");
}
}
final roomID = await client.createRoom(
creationContent: {
'type': "${PangeaRoomTypes.activitySession}:${activity.activityId}",
@ -225,10 +249,10 @@ extension CoursePlanRoomExtension on Room {
type: PangeaEventTypes.activityPlan,
content: activity.toJson(),
),
if (activity.imageURL != null)
if (avatarUrl != null)
StateEvent(
type: EventTypes.RoomAvatar,
content: {'url': activity.imageURL},
content: {'url': avatarUrl.toString()},
),
if (role != null)
StateEvent(