3822 begin activity design implementation (#3827)

* file reorganization

* added activity summary widget to show in chat view and activity launch view

* more updates to activity sessions start page

* function to ping course

* remove duplicate loading of participants

* nav bar visibility changes

* add generalized image from url widget

* update bottom of screen activity status message and add summaries to chat event list

* scroll to summary on click

* show invited activity sessions in course chats view
This commit is contained in:
ggurdin 2025-09-02 12:37:16 -04:00 committed by GitHub
parent a446229242
commit 677e9ce594
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1383 additions and 980 deletions

View file

@ -578,13 +578,17 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
ChatDetails(
roomId: state.pathParameters['spaceid']!,
),
const EmptyPage(),
),
redirect: loggedOutRedirect,
redirect: (context, state) {
final subroute =
state.fullPath?.split(":spaceid/details").last ??
"";
return "/rooms/spaces/${state.pathParameters['spaceid']}$subroute";
},
routes: roomDetailsRoutes('spaceid'),
),
...roomDetailsRoutes('spaceid'),
GoRoute(
path: ':roomid',
pageBuilder: (context, state) {

View file

@ -5243,5 +5243,26 @@
"myActivitySessions": "My Activity Sessions",
"directMessages": "Direct Messages",
"whatNow": "What now?",
"chooseNextActivity": "Choose your next activity!"
"chooseNextActivity": "Choose your next activity!",
"seeInstructions": "See Instructions",
"hideInstructions": "Hide Instructions",
"letsGo": "Lets go!",
"chooseRole": "Choose a role!",
"chooseRoleToParticipate": "Choose a role to participate!",
"waitingToFillRole": "Waiting to fill {num} roles...",
"@waitingToFillRole": {
"type": "int",
"placeholders": {
"num": {
"type": "int"
}
}
},
"pingParticipants": "Ping course participants",
"playWithBot": "Play with Pangea Bot",
"inviteFriends": "Invite friends",
"waitNotDone": "Wait Im not done!",
"waitingForOthersToFinish": "Waiting for the rest to finish up...",
"saveToCompletedActivities": "Save to completed activities",
"generatingSummary": "Analyzing chat and generating results"
}

View file

@ -29,6 +29,8 @@ import 'package:fluffychat/pages/chat/events/audio_player.dart';
import 'package:fluffychat/pages/chat/recording_dialog.dart';
import 'package:fluffychat/pages/chat_details/chat_details.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_start/activity_session_start_page.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
@ -56,6 +58,7 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/learning_settings/widgets/p_language_dialog.dart';
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/utils/error_reporter.dart';
@ -100,7 +103,6 @@ class ChatPage extends StatelessWidget {
Widget build(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(roomId);
// #Pangea
if (room?.isSpace == true &&
GoRouterState.of(context).fullPath?.endsWith(":roomid") == true) {
ErrorHandler.logError(
@ -788,6 +790,7 @@ class ChatController extends State<ChatPageWithRoom>
_analyticsSubscription?.cancel();
_botAudioSubscription?.cancel();
_router.routeInformationProvider.removeListener(_onRouteChanged);
carouselController.dispose();
//Pangea#
super.dispose();
}
@ -2204,11 +2207,17 @@ class ChatController extends State<ChatPageWithRoom>
);
}
ActivityRoleModel? highlightedRole;
final ScrollController carouselController = ScrollController();
ActivityRoleModel? highlightedRole;
void highlightRole(ActivityRoleModel role) {
if (mounted) setState(() => highlightedRole = role);
}
bool showInstructions = false;
void toggleShowInstructions() {
if (mounted) setState(() => showInstructions = !showInstructions);
}
// Pangea#
late final ValueNotifier<bool> _displayChatDetailsColumn;
@ -2223,43 +2232,59 @@ class ChatController extends State<ChatPageWithRoom>
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
children: [
Expanded(
child: ChatView(this),
),
ValueListenableBuilder(
valueListenable: _displayChatDetailsColumn,
builder: (context, displayChatDetailsColumn, _) =>
!FluffyThemes.isThreeColumnMode(context) ||
room.membership != Membership.join ||
!displayChatDetailsColumn
? const SizedBox(
height: double.infinity,
width: 0,
)
: Container(
width: FluffyThemes.columnWidth,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
border: Border(
left: BorderSide(
width: 1,
color: theme.dividerColor,
// #Pangea
return LoadParticipantsBuilder(
room: room,
builder: (context, participants) {
if (!room.participantListComplete && participants.loading) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
if (room.isActivitySession == true && !room.activityHasStarted) {
return ActivitySessionStartPage(room: room);
}
// Pangea#
final theme = Theme.of(context);
return Row(
children: [
Expanded(
child: ChatView(this),
),
ValueListenableBuilder(
valueListenable: _displayChatDetailsColumn,
builder: (context, displayChatDetailsColumn, _) =>
!FluffyThemes.isThreeColumnMode(context) ||
room.membership != Membership.join ||
!displayChatDetailsColumn
? const SizedBox(
height: double.infinity,
width: 0,
)
: Container(
width: FluffyThemes.columnWidth,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
border: Border(
left: BorderSide(
width: 1,
color: theme.dividerColor,
),
),
),
child: ChatDetails(
roomId: roomId,
embeddedCloseButton: IconButton(
icon: const Icon(Icons.close),
onPressed: toggleDisplayChatDetailsColumn,
),
),
),
),
child: ChatDetails(
roomId: roomId,
embeddedCloseButton: IconButton(
icon: const Icon(Icons.close),
onPressed: toggleDisplayChatDetailsColumn,
),
),
),
),
],
),
],
);
},
);
}
}

View file

@ -9,7 +9,8 @@ import 'package:fluffychat/pages/chat/events/message.dart';
import 'package:fluffychat/pages/chat/seen_by_row.dart';
import 'package:fluffychat/pages/chat/typing_indicators.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_message.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_finished_status_message.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_user_summaries_widget.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
@ -94,7 +95,7 @@ class ChatEventList extends StatelessWidget {
// Request history button or progress indicator:
// #Pangea
// if (i == events.length + 1) {
if (i == events.length + 2) {
if (i == events.length + 3) {
// Pangea#
if (timeline.isRequestingHistory) {
return const Center(
@ -127,11 +128,15 @@ class ChatEventList extends StatelessWidget {
if (i == 1) {
return ActivityFinishedStatusMessage(controller: controller);
}
if (i == 2) {
return ActivityUserSummaries(controller: controller);
}
// Pangea#
// #Pangea
// i--;
i = i - 2;
i = i - 3;
// Pangea#
// The message at this index:
@ -209,7 +214,7 @@ class ChatEventList extends StatelessWidget {
},
// #Pangea
// childCount: events.length + 2,
childCount: events.length + 3,
childCount: events.length + 4,
// Pangea#
findChildIndexCallback: (key) =>
controller.findChildIndexCallback(key, thisEventsKeyMap),

View file

@ -13,9 +13,9 @@ import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
import 'package:fluffychat/pages/chat/chat_event_list.dart';
import 'package:fluffychat/pages/chat/pinned_events.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_pinned_message.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_status_message.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_pinned_message.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/load_activity_summary_widget.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar_header.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_view_background.dart';
@ -435,7 +435,8 @@ class ChatView extends StatelessWidget {
height: controller.inputBarHeight,
),
),
ActivityStatusMessage(room: controller.room),
if (controller.room.activityIsFinished)
LoadActivitySummaryWidget(room: controller.room),
// Pangea#
],
),

View file

@ -10,10 +10,9 @@ import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/pangea_message_reactions.dart';
import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_creation_state_event.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_roles_event.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_state_event.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_roles_event_widget.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_summary_widget.dart';
import 'package:fluffychat/pangea/chat/extensions/custom_room_display_extension.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
@ -131,15 +130,22 @@ class Message extends StatelessWidget {
if (event.type == EventTypes.RoomCreate) {
// #Pangea
// return RoomCreationStateEvent(event: event);
return event.room.activityPlan != null
? ActivityCreationStateEvent(event: event)
return event.room.isActivitySession
? const SizedBox(height: 60.0)
: RoomCreationStateEvent(event: event);
// Pangea#
}
// #Pangea
if (event.type == PangeaEventTypes.activityPlan) {
return ActivityStateEvent(event: event);
return ActivitySummary(
room: event.room,
showInstructions: controller.showInstructions,
toggleInstructions: controller.toggleShowInstructions,
getParticipantOpacity: (role) =>
role == null || role.isFinished ? 0.5 : 1.0,
isParticipantSelected: (id) => controller.room.ownRole?.id == id,
);
}
if (event.type == PangeaEventTypes.activityRole) {

View file

@ -12,12 +12,12 @@ import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart';
import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
class ActivityPlanCard extends StatelessWidget {
final VoidCallback regenerate;
@ -177,57 +177,15 @@ class ActivityPlanCard extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
controller.updatedActivity.imageURL != null
? ClipRRect(
borderRadius:
BorderRadius.circular(4.0),
child: controller
.updatedActivity.imageURL!
.startsWith("mxc")
? MxcImage(
uri: Uri.parse(
controller.updatedActivity
.imageURL!,
),
width: 24.0,
height: 24.0,
cacheKey: controller
.updatedActivity.activityId,
fit: BoxFit.cover,
)
: CachedNetworkImage(
imageRenderMethodForWeb:
ImageRenderMethodForWeb
.HttpGet,
imageUrl: controller
.updatedActivity.imageURL!,
httpHeaders: {
'Authorization':
'Bearer ${MatrixState.pangeaController.userController.accessToken}',
},
fit: BoxFit.cover,
width: 24.0,
height: 24.0,
placeholder: (
context,
url,
) =>
const Center(
child:
CircularProgressIndicator(),
),
errorWidget: (
context,
url,
error,
) =>
const SizedBox(),
),
)
: const Icon(
Icons.event_note_outlined,
size: 24.0,
),
ImageByUrl(
imageUrl: controller.updatedActivity.imageURL,
width: 24.0,
borderRadius: BorderRadius.circular(4.0),
replacement: const Icon(
Icons.event_note_outlined,
size: 24.0,
),
),
const SizedBox(width: itemPadding),
Expanded(
child: Text(

View file

@ -10,11 +10,11 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_plan_repo.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/common/utils/error_handler.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/pangea/learning_settings/enums/language_level_type_enum.dart';
@ -348,6 +348,10 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
visibility: Visibility.private,
name: "${updatedActivity.title} ${index + 1}",
initialState: [
StateEvent(
type: PangeaEventTypes.activityPlan,
content: updatedActivity.toJson(),
),
if (imageURL != null)
StateEvent(
type: EventTypes.RoomAvatar,
@ -382,12 +386,6 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
await room.client.waitForRoomInSync(activityRoom.id);
}
await activityRoom.sendActivityPlan(
updatedActivity,
avatar: avatar,
filename: filename,
);
return activityRoom.id;
}

View file

@ -1,40 +0,0 @@
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/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
class ActivityCreationStateEvent extends StatelessWidget {
final Event event;
const ActivityCreationStateEvent({required this.event, super.key});
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
final matrixLocals = MatrixLocals(l10n);
final theme = Theme.of(context);
final roomName = event.room.getLocalizedDisplayname(matrixLocals);
return Center(
child: Container(
padding: const EdgeInsets.only(bottom: 32.0, top: 60.0),
child: Material(
color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Avatar(
mxContent: event.room.avatar,
name: roomName,
size: Avatar.defaultSize * 5,
userId: event.room.directChatMatrixID,
),
),
),
),
);
}
}

View file

@ -1,277 +0,0 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_analytics_chip.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_participant_indicator.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_results_carousel.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/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.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/spaces/utils/load_participants_util.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ActivityFinishedStatusMessage extends StatelessWidget {
final ChatController controller;
const ActivityFinishedStatusMessage({
super.key,
required this.controller,
});
Map<String, ActivityRole> get _roles =>
controller.room.activityPlan?.roles ?? {};
Future<void> _archiveToAnalytics(BuildContext context) async {
await controller.room.archiveActivity();
await MatrixState.pangeaController.putAnalytics
.sendActivityAnalytics(controller.room.id);
final courseParent = controller.room.courseParent;
if (courseParent?.coursePlan == null) return;
final coursePlan = await CoursePlansRepo.get(
courseParent!.coursePlan!.uuid,
);
if (coursePlan == null) {
throw L10n.of(context).noCourseFound;
}
final activityId = controller.room.activityPlan!.activityId;
final topicId = coursePlan.topicID(activityId);
if (topicId == null) {
throw L10n.of(context).activityNotFoundForCourse;
}
await courseParent.finishCourseActivity(activityId, topicId);
}
List<ActivityRoleModel> get _rolesWithSummaries {
if (controller.room.activitySummary?.summary == null) {
return <ActivityRoleModel>[];
}
final roles = controller.room.activityRoles;
return roles?.roles.values.where((role) {
return controller.room.activitySummary!.summary!.participants.any(
(p) => p.participantId == role.userId,
);
}).toList() ??
[];
}
ActivityRoleModel? get _highlightedRole {
if (controller.highlightedRole != null) {
return controller.highlightedRole;
}
return _rolesWithSummaries.firstWhereOrNull(
(r) => r.userId == controller.room.client.userID,
);
}
@override
Widget build(BuildContext context) {
return LoadParticipantsBuilder(
room: controller.room,
builder: (context, participants) {
if (!controller.room.showActivityChatUI ||
!controller.room.activityIsFinished ||
controller.room.ownRole == null) {
return const SizedBox.shrink();
}
final summary = controller.room.activitySummary;
final theme = Theme.of(context);
final user = participants.participants.firstWhereOrNull(
(u) => u.id == _highlightedRole?.userId,
);
final userSummary = controller
.room.activitySummary?.summary?.participants
.firstWhereOrNull(
(p) => p.participantId == _highlightedRole?.userId,
);
return AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Center(
child: Container(
padding: const EdgeInsets.all(16.0),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (summary?.summary != null) ...[
Text(
L10n.of(context).activityFinishedMessage,
style: const TextStyle(fontSize: 18.0),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(
summary!.summary!.summary,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14.0,
),
),
),
if (summary.analytics != null)
Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
ActivityAnalyticsChip(
ConstructTypeEnum.vocab.indicator.icon,
"${summary.analytics!.uniqueConstructCount(ConstructTypeEnum.vocab)}",
),
ActivityAnalyticsChip(
ConstructTypeEnum.morph.indicator.icon,
"${summary.analytics!.uniqueConstructCount(ConstructTypeEnum.morph)}",
),
],
),
const SizedBox(height: 16.0),
if (_highlightedRole != null && userSummary != null)
ClipRRect(
borderRadius: BorderRadius.circular(24.0),
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer,
),
child: Column(
children: [
ActivityResultsCarousel(
userId: _highlightedRole!.userId,
selectedRole: _highlightedRole!,
user: user,
summary: userSummary,
analytics: summary.analytics,
),
Wrap(
alignment: WrapAlignment.center,
children: _rolesWithSummaries.map(
(role) {
final user = participants.participants
.firstWhereOrNull(
(u) => u.id == role.userId,
);
return IntrinsicWidth(
child: ActivityParticipantIndicator(
availableRole: _roles[role.id]!,
avatarUrl: _roles[role.id]?.avatarUrl ??
user?.avatarUrl?.toString(),
onTap: _highlightedRole == role
? null
: () =>
controller.highlightRole(role),
assignedRole: role,
selected: _highlightedRole == role,
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 24.0,
),
borderRadius: BorderRadius.zero,
),
);
},
).toList(),
),
],
),
),
),
const SizedBox(height: 20.0),
] else if (summary?.isLoading ?? false)
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
spacing: 8.0,
children: [
const CircularProgressIndicator.adaptive(),
Text(L10n.of(context).loadingActivitySummary),
],
),
)
else if (summary?.hasError ?? false)
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
spacing: 8.0,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.school_outlined,
size: 24.0,
),
const SizedBox(width: 8),
Flexible(
child: Text(
L10n.of(context).activitySummaryError,
textAlign: TextAlign.center,
),
),
],
),
TextButton(
onPressed: () => controller.room.fetchSummaries(),
child: Text(L10n.of(context).requestSummaries),
),
],
),
),
if (!controller.room.isHiddenActivityRoom)
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 8.0,
),
foregroundColor: theme.colorScheme.onPrimaryContainer,
backgroundColor: theme.colorScheme.primaryContainer,
),
onPressed: () async {
final resp = await showFutureLoadingDialog(
context: context,
future: () => _archiveToAnalytics(context),
);
if (!resp.isError) {
context.go(
"/rooms/analytics?mode=activities",
);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).archiveToAnalytics),
],
),
),
],
),
),
),
);
},
);
}
}

View file

@ -4,16 +4,13 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
class ActivityParticipantIndicator extends StatelessWidget {
final ActivityRole availableRole;
final ActivityRoleModel? assignedRole;
final String name;
final String? userId;
final String? avatarUrl;
final VoidCallback? onTap;
@ -25,9 +22,9 @@ class ActivityParticipantIndicator extends StatelessWidget {
const ActivityParticipantIndicator({
super.key,
required this.availableRole,
required this.name,
this.avatarUrl,
this.assignedRole,
this.userId,
this.selected = false,
this.onTap,
this.opacity = 1.0,
@ -63,13 +60,13 @@ class ActivityParticipantIndicator extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
assignedRole != null
userId != null
? avatarUrl == null || avatarUrl!.startsWith("mxc")
? Avatar(
mxContent: avatarUrl != null
? Uri.parse(avatarUrl!)
: null,
name: assignedRole?.userId.localpart,
name: userId!.localpart,
size: 60.0,
)
: ClipRRect(
@ -87,23 +84,20 @@ class ActivityParticipantIndicator extends StatelessWidget {
theme.colorScheme.primaryContainer,
),
Text(
availableRole.name,
name,
style: const TextStyle(
fontSize: 12.0,
),
),
Text(
assignedRole?.userId.localpart ??
L10n.of(context).openRoleLabel,
userId?.localpart ?? L10n.of(context).openRoleLabel,
style: TextStyle(
fontSize: 12.0,
color:
(Theme.of(context).brightness == Brightness.light
? assignedRole
?.userId.localpart?.lightColorAvatar
: assignedRole
?.userId.localpart?.lightColorText) ??
assignedRole?.role?.lightColorAvatar,
? userId?.localpart?.lightColorAvatar
: userId?.localpart?.lightColorText) ??
name.lightColorAvatar,
),
),
],

View file

@ -56,8 +56,8 @@ class ActivityParticipantList extends StatelessWidget {
canSelect != null ? canSelect!(availableRole.id) : true;
return ActivityParticipantIndicator(
availableRole: availableRole,
assignedRole: assignedRole,
name: availableRole.name,
userId: assignedRole?.userId,
opacity: getOpacity != null ? getOpacity!(assignedRole) : 1.0,
avatarUrl:
availableRole.avatarUrl ?? user?.avatarUrl?.toString(),

View file

@ -36,6 +36,9 @@ class ActivityRolesModel {
static ActivityRolesModel fromJson(Map<String, dynamic> json) {
final roles = (json['roles'] as Map<String, dynamic>)
.map((id, value) => MapEntry(id, ActivityRoleModel.fromJson(value)));
return ActivityRolesModel(roles);
return ActivityRolesModel(
roles,
);
}
}

View file

@ -22,21 +22,6 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import '../activity_summary/activity_summary_repo.dart';
extension ActivityRoomExtension on Room {
Future<void> sendActivityPlan(
ActivityPlanModel activity, {
Uint8List? avatar,
String? filename,
}) async {
if (canChangeStateEvent(PangeaEventTypes.activityPlan)) {
await client.setRoomStateWithKey(
id,
PangeaEventTypes.activityPlan,
"",
activity.toJson(),
);
}
}
Future<void> joinActivity(ActivityRole role) async {
final currentRoles = activityRoles ?? ActivityRolesModel.empty;
final activityRole = ActivityRoleModel(
@ -279,8 +264,8 @@ extension ActivityRoomExtension on Room {
ActivityRoleModel? get ownRole => activityRoles?.role(client.userID!);
int get remainingRoles {
final availableRoles = activityPlan!.roles;
return max(0, availableRoles.length - (assignedRoles?.length ?? 0));
final availableRoles = activityPlan?.roles;
return max(0, (availableRoles?.length ?? 0) - (assignedRoles?.length ?? 0));
}
bool get showActivityChatUI {
@ -289,6 +274,8 @@ extension ActivityRoomExtension on Room {
powerForChangingStateEvent(PangeaEventTypes.activitySummary) == 0;
}
bool get activityHasStarted => remainingRoles == 0;
bool get isActiveInActivity {
if (!showActivityChatUI) return false;
final role = ownRole;
@ -329,11 +316,8 @@ extension ActivityRoomExtension on Room {
(parent) => parent.coursePlan != null,
);
bool get isActivitySession =>
getState(EventTypes.RoomCreate)
?.content
.tryGet<String>('type')
?.startsWith(PangeaRoomTypes.activitySession) ==
true ||
activityPlan != null;
bool get isActivityRoomType =>
roomType?.startsWith(PangeaRoomTypes.activitySession) == true;
bool get isActivitySession => isActivityRoomType || activityPlan != null;
}

View file

@ -0,0 +1,190 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_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/matrix.dart';
class ActivityFinishedStatusMessage extends StatelessWidget {
final ChatController controller;
const ActivityFinishedStatusMessage({
super.key,
required this.controller,
});
Future<void> _archiveToAnalytics(BuildContext context) async {
await controller.room.archiveActivity();
await MatrixState.pangeaController.putAnalytics
.sendActivityAnalytics(controller.room.id);
final courseParent = controller.room.courseParent;
if (courseParent?.coursePlan == null) return;
final coursePlan = await CoursePlansRepo.get(
courseParent!.coursePlan!.uuid,
);
if (coursePlan == null) {
throw L10n.of(context).noCourseFound;
}
final activityId = controller.room.activityPlan!.activityId;
final topicId = coursePlan.topicID(activityId);
if (topicId == null) {
throw L10n.of(context).activityNotFoundForCourse;
}
await courseParent.finishCourseActivity(activityId, topicId);
}
@override
Widget build(BuildContext context) {
if (!controller.room.showActivityChatUI ||
controller.room.ownRole == null ||
!controller.room.hasCompletedActivity) {
return const SizedBox.shrink();
}
final theme = Theme.of(context);
final summary = controller.room.activitySummary;
return AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Container(
margin: const EdgeInsets.only(top: 20.0),
padding: const EdgeInsets.only(
top: 12.0,
left: 12.0,
right: 12.0,
),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: theme.dividerColor),
),
),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
spacing: 12.0,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: controller.room.activityIsFinished
? [
if (summary?.isLoading ?? false) ...[
Text(
L10n.of(context).generatingSummary,
style: const TextStyle(
fontStyle: FontStyle.italic,
),
),
const SizedBox(
height: 36.0,
width: 36.0,
child: CircularProgressIndicator(),
),
] else if (summary?.hasError ?? false) ...[
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.school_outlined,
size: 24.0,
),
const SizedBox(width: 8),
Flexible(
child: Text(
L10n.of(context).activitySummaryError,
textAlign: TextAlign.center,
),
),
],
),
TextButton(
onPressed: () => controller.room.fetchSummaries(),
child: Text(L10n.of(context).requestSummaries),
),
],
if (!controller.room.isHiddenActivityRoom)
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 8.0,
),
foregroundColor:
theme.colorScheme.onPrimaryContainer,
backgroundColor: theme.colorScheme.primaryContainer,
),
onPressed: () async {
final resp = await showFutureLoadingDialog(
context: context,
future: () => _archiveToAnalytics(context),
);
if (!resp.isError) {
context.go(
"/rooms/analytics?mode=activities",
);
}
},
child: Row(
spacing: 12.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.radar, size: 20.0),
Text(
L10n.of(context).saveToCompletedActivities,
style: const TextStyle(fontSize: 12.0),
),
],
),
),
]
: [
Text(
L10n.of(context).waitingForOthersToFinish,
style: const TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 8.0,
),
foregroundColor: theme.colorScheme.onSurface,
backgroundColor: theme.colorScheme.surface,
side: BorderSide(
color: theme.colorScheme.primaryContainer,
),
),
onPressed: controller.room.continueActivity,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
L10n.of(context).waitNotDone,
style: const TextStyle(
fontSize: 12,
),
),
],
),
),
],
),
),
),
),
);
}
}

View file

@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_analytics_chip.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_analytics_chip.dart';
import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart';
import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';

View file

@ -0,0 +1,146 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
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_sessions_start_view.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';
enum SessionState {
notStarted,
notSelectedRole,
selectedRole,
confirmedRole,
}
class ActivitySessionStartPage extends StatefulWidget {
final Room room;
const ActivitySessionStartPage({
super.key,
required this.room,
});
@override
ActivitySessionStartController createState() =>
ActivitySessionStartController();
}
class ActivitySessionStartController extends State<ActivitySessionStartPage> {
bool _started = false;
bool showInstructions = false;
String? _selectedRoleId;
@override
void didUpdateWidget(covariant ActivitySessionStartPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.room.id != widget.room.id) {
setState(() {
_started = false;
_selectedRoleId = null;
showInstructions = false;
});
}
}
Room get room => widget.room;
String get displayname => room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)),
);
SessionState get state {
if (room.ownRole != null) return SessionState.confirmedRole;
if (_selectedRoleId != null) return SessionState.selectedRole;
if (room.isRoomAdmin && !_started) return SessionState.notStarted;
return SessionState.notSelectedRole;
}
String get descriptionText {
switch (state) {
case SessionState.confirmedRole:
return L10n.of(context).waitingToFillRole(room.remainingRoles);
case SessionState.selectedRole:
return room.activityPlan!.learningObjective;
case SessionState.notStarted:
return L10n.of(context).letsGo;
case SessionState.notSelectedRole:
return room.isRoomAdmin
? L10n.of(context).chooseRole
: L10n.of(context).chooseRoleToParticipate;
}
}
String get buttonText => state == SessionState.notStarted
? L10n.of(context).start
: L10n.of(context).confirm;
bool get enableButtons => [
SessionState.notStarted,
SessionState.selectedRole,
].contains(state);
bool canSelectParticipant(String id) {
if (state == SessionState.confirmedRole) return false;
final availableRoles = room.activityPlan!.roles;
final assignedRoles = room.assignedRoles ?? {};
final unassignedIds = availableRoles.keys
.where((id) => !assignedRoles.containsKey(id))
.toList();
return unassignedIds.contains(id);
}
bool isParticipantSelected(String id) {
if (state == SessionState.confirmedRole) {
return room.ownRole?.id == id;
}
return _selectedRoleId == id;
}
void toggleInstructions() {
setState(() {
showInstructions = !showInstructions;
});
}
void selectRole(String id) {
if (state == SessionState.confirmedRole) return;
if (_selectedRoleId == id) return;
if (mounted) setState(() => _selectedRoleId = id);
}
Future<void> onTap() async {
switch (state) {
case SessionState.notStarted:
if (mounted) setState(() => _started = true);
case SessionState.selectedRole:
await showFutureLoadingDialog(
context: context,
future: () => room.joinActivity(
room.activityPlan!.roles[_selectedRoleId!]!,
),
);
if (mounted) setState(() {});
case SessionState.notSelectedRole:
case SessionState.confirmedRole:
break;
}
}
Future<void> pingCourse() async {
if (room.courseParent == null) {
throw Exception("Activity is not part of a course");
}
await room.courseParent!.sendTextEvent("");
}
@override
Widget build(BuildContext context) => ActivitySessionStartView(this);
}

View file

@ -0,0 +1,222 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/themes.dart';
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/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';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
class ActivitySessionStartView extends StatelessWidget {
final ActivitySessionStartController controller;
const ActivitySessionStartView(
this.controller, {
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return StreamBuilder(
stream: controller.room.client.onRoomState.stream
.rateLimit(const Duration(seconds: 1)),
builder: (context, snapshot) {
return Scaffold(
appBar: AppBar(
leadingWidth: 52.0,
title: Text(controller.displayname),
leading: Padding(
padding: const EdgeInsets.only(left: 12.0),
child: Center(
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
),
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: SizedBox(
width: 40.0,
height: 40.0,
child: Center(
child: ShareRoomButton(room: controller.room),
),
),
),
],
),
body: MaxWidthBody(
showBorder: false,
withScrolling: false,
child: Stack(
children: [
Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 12.0,
children: [
ActivitySummary(
room: controller.room,
showInstructions: controller.showInstructions,
toggleInstructions:
controller.toggleInstructions,
onTapParticipant: controller.selectRole,
isParticipantSelected:
controller.isParticipantSelected,
canSelectParticipant:
controller.canSelectParticipant,
),
const SizedBox(height: 250.0),
],
),
),
),
),
],
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: theme.dividerColor),
),
color: theme.colorScheme.surface,
),
padding: const EdgeInsets.all(24.0),
child: Column(
spacing: 16.0,
children: [
Text(
controller.descriptionText,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
if (controller.state ==
SessionState.confirmedRole) ...[
if (controller.room.courseParent != null)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
theme.colorScheme.primaryContainer,
foregroundColor:
theme.colorScheme.onPrimaryContainer,
padding: const EdgeInsets.all(8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
),
onPressed: () => showFutureLoadingDialog(
context: context,
future: controller.pingCourse,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
L10n.of(context).pingParticipants,
),
],
),
),
if (controller.room.isRoomAdmin) ...[
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
theme.colorScheme.primaryContainer,
foregroundColor:
theme.colorScheme.onPrimaryContainer,
padding: const EdgeInsets.all(8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
),
onPressed: () => showFutureLoadingDialog(
context: context,
future: () => controller.room
.invite(BotName.byEnvironment),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).playWithBot),
],
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
theme.colorScheme.primaryContainer,
foregroundColor:
theme.colorScheme.onPrimaryContainer,
padding: const EdgeInsets.all(8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
),
onPressed: () => context.go(
"/rooms/${controller.room.id}/invite",
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).inviteFriends),
],
),
),
],
] else
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
theme.colorScheme.primaryContainer,
foregroundColor:
theme.colorScheme.onPrimaryContainer,
padding: const EdgeInsets.all(8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
),
onPressed: controller.enableButtons
? controller.onTap
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(controller.buttonText),
],
),
),
],
),
),
),
),
],
),
),
);
},
);
}
}

View file

@ -1,53 +0,0 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/events/state_message.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_room_extension.dart';
class ActivityStateEvent extends StatelessWidget {
final Event event;
const ActivityStateEvent({super.key, required this.event});
@override
Widget build(BuildContext context) {
if (event.room.activityPlan == null) {
return const SizedBox();
}
try {
final activity = ActivityPlanModel.fromJson(event.content);
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 16.0,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.maxTimelineWidth,
),
child: Column(
spacing: 12.0,
children: [
Text(
activity.markdown,
style: const TextStyle(fontSize: 14.0),
),
if (event.room.ownRole != null ||
event.room.remainingRoles == 0) ...[
ActivityParticipantList(
room: event.room,
getOpacity: (role) =>
role == null || role.isFinished ? 0.5 : 1.0,
),
],
],
),
);
} catch (e) {
return StateMessage(event);
}
}
}

View file

@ -1,54 +0,0 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_unfinished_status_message.dart';
import 'package:fluffychat/pangea/activity_sessions/load_activity_summary_widget.dart';
class ActivityStatusMessage extends StatelessWidget {
final Room room;
const ActivityStatusMessage({
super.key,
required this.room,
});
@override
Widget build(BuildContext context) {
if (!room.showActivityChatUI) {
return const SizedBox.shrink();
}
if (room.activityIsFinished) {
return LoadActivitySummaryWidget(room: room);
}
final role = room.ownRole;
if (role != null && !role.isFinished) {
return const SizedBox.shrink();
}
return Material(
child: AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Padding(
padding: EdgeInsets.only(
bottom: FluffyThemes.isColumnMode(context) ? 32.0 : 16.0,
left: 16.0,
right: 16.0,
),
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: SingleChildScrollView(
child: ActivityUnfinishedStatusMessage(room: room),
),
),
),
),
);
}
}

View file

@ -0,0 +1,189 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.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_suggestions/activity_suggestion_card_row.dart';
import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
class ActivitySummary extends StatelessWidget {
final Room room;
final bool showInstructions;
final VoidCallback toggleInstructions;
final Function(String)? onTapParticipant;
final bool Function(String)? canSelectParticipant;
final bool Function(String)? isParticipantSelected;
final double Function(ActivityRoleModel?)? getParticipantOpacity;
const ActivitySummary({
super.key,
required this.room,
required this.showInstructions,
required this.toggleInstructions,
this.onTapParticipant,
this.canSelectParticipant,
this.isParticipantSelected,
this.getParticipantOpacity,
});
@override
Widget build(BuildContext context) {
final activity = room.activityPlan;
if (activity == null) {
return const SizedBox();
}
final theme = Theme.of(context);
return Center(
child: Container(
padding: const EdgeInsets.all(12.0),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
),
child: Column(
spacing: 12.0,
children: [
Padding(
padding: const EdgeInsets.all(4.0),
child: ImageByUrl(
imageUrl: activity.imageURL,
width: 80.0,
borderRadius: BorderRadius.circular(20),
),
),
Text(
activity.learningObjective,
style: const TextStyle(fontSize: 12.0),
),
ActivityParticipantList(
room: room,
onTap: onTapParticipant,
canSelect: canSelectParticipant,
isSelected: isParticipantSelected,
getOpacity: getParticipantOpacity,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
child: Column(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 6.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
Text(
activity.req.mode,
style: const TextStyle(fontSize: 12.0),
),
Row(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.school, size: 12.0),
Text(
activity.req.cefrLevel.string,
style: const TextStyle(fontSize: 12.0),
),
],
),
],
),
GestureDetector(
onTap: toggleInstructions,
child: Row(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: [
Text(
showInstructions
? L10n.of(context).hideInstructions
: L10n.of(context).seeInstructions,
style: const TextStyle(fontSize: 12.0),
),
Icon(
showInstructions
? Icons.arrow_drop_up
: Icons.arrow_drop_down,
size: 12.0,
),
],
),
),
],
),
),
if (showInstructions) ...[
ActivitySuggestionCardRow(
icon: Symbols.target,
iconSize: 16.0,
child: Text(
activity.learningObjective,
style: const TextStyle(fontSize: 12.0),
),
),
ActivitySuggestionCardRow(
icon: Symbols.steps,
iconSize: 16.0,
child: Text(
activity.instructions,
style: const TextStyle(fontSize: 12.0),
),
),
ActivitySuggestionCardRow(
icon: Symbols.dictionary,
iconSize: 16.0,
child: Wrap(
spacing: 4.0,
runSpacing: 4.0,
children: activity.vocab
.map(
(vocab) => Container(
padding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 8.0,
),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withAlpha(
20,
),
borderRadius: BorderRadius.circular(
24.0,
),
),
child: Text(
vocab.lemma,
style: const TextStyle(fontSize: 12),
),
),
)
.toList(),
),
),
],
],
),
),
],
),
),
);
}
}

View file

@ -1,112 +0,0 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_participant_list.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
class ActivityUnfinishedStatusMessage extends StatefulWidget {
final Room room;
const ActivityUnfinishedStatusMessage({
super.key,
required this.room,
});
@override
ActivityUnfinishedStatusMessageState createState() =>
ActivityUnfinishedStatusMessageState();
}
class ActivityUnfinishedStatusMessageState
extends State<ActivityUnfinishedStatusMessage> {
String? _selectedRoleId;
void _selectRole(String id) {
if (_selectedRoleId == id) return;
if (mounted) setState(() => _selectedRoleId = id);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isColumnMode = FluffyThemes.isColumnMode(context);
final completed = widget.room.hasCompletedActivity;
final availableRoles = widget.room.activityPlan!.roles;
final assignedRoles = widget.room.assignedRoles ?? {};
final remainingRoles = availableRoles.length - assignedRoles.length;
final unassignedIds = availableRoles.keys
.where((id) => !assignedRoles.containsKey(id))
.toList();
return Column(
children: [
if (!completed) ...[
if (unassignedIds.isNotEmpty)
ActivityParticipantList(
room: widget.room,
onTap: _selectRole,
isSelected: (id) => _selectedRoleId == id,
canSelect: (id) => unassignedIds.contains(id),
),
const SizedBox(height: 16.0),
Text(
remainingRoles > 0
? L10n.of(context).unjoinedActivityMessage
: L10n.of(context).fullActivityMessage,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: isColumnMode ? 16.0 : 12.0,
),
),
const SizedBox(height: 16.0),
],
if (completed || remainingRoles > 0)
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 8.0,
),
foregroundColor: theme.colorScheme.onPrimaryContainer,
backgroundColor: theme.colorScheme.primaryContainer,
),
onPressed: completed
? () {
showFutureLoadingDialog(
context: context,
future: widget.room.continueActivity,
);
}
: _selectedRoleId != null
? () {
showFutureLoadingDialog(
context: context,
future: () => widget.room.joinActivity(
availableRoles[_selectedRoleId!]!,
),
);
}
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
completed
? L10n.of(context).continueText
: L10n.of(context).confirmRole,
style: TextStyle(
fontSize: isColumnMode ? 16.0 : 12.0,
),
),
],
),
),
],
);
}
}

View file

@ -0,0 +1,223 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_participant_indicator.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart';
import 'package:fluffychat/widgets/avatar.dart';
class ActivityUserSummaries extends StatelessWidget {
final ChatController controller;
const ActivityUserSummaries({
super.key,
required this.controller,
});
Room get room => controller.room;
@override
Widget build(BuildContext context) {
final summary = room.activitySummary?.summary;
if (summary == null) return const SizedBox();
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: [
Text(
L10n.of(context).activityFinishedMessage,
),
Text(
summary.summary,
textAlign: TextAlign.center,
),
const Padding(
padding: EdgeInsets.symmetric(
vertical: 8.0,
),
),
ButtonControlledCarouselView(
summary: summary,
controller: controller,
),
// Row(
// mainAxisSize: MainAxisSize.min,
// children: userSummaries.map((p) {
// final user = room.getParticipants().firstWhereOrNull(
// (u) => u.id == p.participantId,
// );
// final userRole = assignedRoles.values.firstWhere(
// (role) => role.userId == p.participantId,
// );
// final userRoleInfo = availableRoles[userRole.id]!;
// return ActivityParticipantIndicator(
// availableRole: userRoleInfo,
// assignedRole: userRole,
// avatarUrl:
// userRoleInfo.avatarUrl ?? user?.avatarUrl?.toString(),
// borderRadius: BorderRadius.circular(4),
// selected: controller.highlightedRole?.id == userRole.id,
// onTap: () => controller.highlightRole(userRole),
// );
// }).toList(),
// ),
],
),
);
}
}
class ButtonControlledCarouselView extends StatelessWidget {
final ActivitySummaryResponseModel summary;
final ChatController controller;
const ButtonControlledCarouselView({
super.key,
required this.summary,
required this.controller,
});
@override
Widget build(BuildContext context) {
final room = controller.room;
final availableRoles = room.activityPlan!.roles;
final assignedRoles = room.assignedRoles ?? {};
final userSummaries = summary.participants.where(
(p) => assignedRoles.values.any(
(role) => role.userId == p.participantId,
),
);
return Column(
children: [
SizedBox(
height: 175.0,
child: ListView(
controller: controller.carouselController,
itemExtent: 250,
scrollDirection: Axis.horizontal,
children: userSummaries.mapIndexed((i, p) {
final user = room.getParticipants().firstWhereOrNull(
(u) => u.id == p.participantId,
);
final userRole = assignedRoles.values.firstWhere(
(role) => role.userId == p.participantId,
);
return Container(
width: 250.0,
margin: const EdgeInsets.only(right: 5.0),
padding: const EdgeInsets.all(12.0),
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: BorderSide(
width: 0.10,
color: Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(12),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 10.0,
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
name: p.participantId.localpart,
mxContent: user?.avatarUrl,
size: 40,
),
Flexible(
child: Text(
"${userRole.role ?? L10n.of(context).participant} | ${user?.calcDisplayname() ?? p.participantId.localpart}",
style: const TextStyle(
fontSize: 12.0,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
Text(
p.feedback,
style: const TextStyle(fontSize: 8.0),
),
Row(
spacing: 14.0,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.school,
size: 12.0,
color: AppConfig.yellowDark,
),
Text(
p.cefrLevel,
style: const TextStyle(
color: AppConfig.yellowDark,
fontSize: 12.0,
),
),
],
),
),
if (p.superlatives.isNotEmpty)
Padding(
padding: const EdgeInsets.all(4.0),
child: Text(
p.superlatives.first,
style: const TextStyle(fontSize: 12.0),
),
),
],
),
],
),
);
}).toList(),
),
),
const SizedBox(height: 12),
Row(
mainAxisSize: MainAxisSize.min,
children: userSummaries.mapIndexed((i, p) {
final user = room.getParticipants().firstWhereOrNull(
(u) => u.id == p.participantId,
);
final userRole = assignedRoles.values.firstWhere(
(role) => role.userId == p.participantId,
);
final userRoleInfo = availableRoles[userRole.id]!;
return ActivityParticipantIndicator(
name: userRoleInfo.name,
userId: p.participantId,
avatarUrl: userRoleInfo.avatarUrl ?? user?.avatarUrl?.toString(),
borderRadius: BorderRadius.circular(4),
selected: controller.highlightedRole?.id == userRole.id,
onTap: () {
controller.highlightRole(userRole);
controller.carouselController.jumpTo(i * 250.0);
},
);
}).toList(),
),
],
);
}
}

View file

@ -2,13 +2,9 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart';
class ActivitySuggestionCard extends StatelessWidget {
final ActivityPlannerBuilderState controller;
@ -53,36 +49,11 @@ class ActivitySuggestionCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
height: width,
ImageByUrl(
imageUrl: activity.imageURL,
width: width,
child: activity.imageURL != null
? activity.imageURL!.startsWith("mxc")
? MxcImage(
uri: Uri.parse(activity.imageURL!),
width: width,
height: width,
cacheKey: activity.activityId,
fit: BoxFit.cover,
)
: CachedNetworkImage(
imageUrl: activity.imageURL!,
imageRenderMethodForWeb:
ImageRenderMethodForWeb.HttpGet,
httpHeaders: {
'Authorization':
'Bearer ${MatrixState.pangeaController.userController.accessToken}',
},
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) =>
const SizedBox(),
fit: BoxFit.cover,
width: width,
height: width,
)
: const SizedBox(),
borderRadius: const BorderRadius.all(Radius.zero),
replacement: SizedBox(height: width),
),
Expanded(
child: Padding(

View file

@ -4,11 +4,13 @@ class ActivitySuggestionCardRow extends StatelessWidget {
final IconData? icon;
final Widget? leading;
final Widget child;
final double? iconSize;
const ActivitySuggestionCardRow({
required this.child,
this.icon,
this.leading,
this.iconSize,
super.key,
});
@ -23,7 +25,7 @@ class ActivitySuggestionCardRow extends StatelessWidget {
if (icon != null)
Icon(
icon,
size: 24.0,
size: iconSize ?? 24.0,
),
Expanded(child: child),
],

View file

@ -2,8 +2,6 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:collection/collection.dart';
import 'package:material_symbols_icons/symbols.dart';
@ -12,11 +10,10 @@ import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card_row.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart';
import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
class ActivitySuggestionDialogContent extends StatelessWidget {
final ActivitySuggestionDialogState controller;
@ -50,52 +47,23 @@ class _ActivitySuggestionDialogImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final imageWidth = (width / 2) + 42.0;
return Container(
padding: const EdgeInsets.all(24.0),
width: (width / 2) + 42.0,
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: activityController.avatar != null
? Image.memory(
width: imageWidth,
child: activityController.avatar != null
? ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: Image.memory(
activityController.avatar!,
fit: BoxFit.cover,
)
: activityController.updatedActivity.imageURL != null
? activityController.updatedActivity.imageURL!.startsWith("mxc")
? MxcImage(
uri: Uri.parse(
activityController.updatedActivity.imageURL!,
),
width: width / 2,
height: 200,
cacheKey: activityController.updatedActivity.activityId,
fit: BoxFit.cover,
)
: CachedNetworkImage(
imageRenderMethodForWeb:
ImageRenderMethodForWeb.HttpGet,
httpHeaders: {
'Authorization':
'Bearer ${MatrixState.pangeaController.userController.accessToken}',
},
imageUrl: activityController.updatedActivity.imageURL!,
fit: BoxFit.cover,
placeholder: (
context,
url,
) =>
const Center(
child: CircularProgressIndicator(),
),
errorWidget: (
context,
url,
error,
) =>
const SizedBox(),
)
: null,
),
),
)
: ImageByUrl(
imageUrl: activityController.updatedActivity.imageURL,
width: imageWidth,
borderRadius: BorderRadius.circular(20.0),
),
);
}
}
@ -623,50 +591,15 @@ class _ActivitySuggestionLaunchContent extends StatelessWidget {
),
),
ActivitySuggestionCardRow(
leading: activityController.updatedActivity.imageURL != null
? ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: activityController.updatedActivity.imageURL!
.startsWith("mxc")
? MxcImage(
uri: Uri.parse(
activityController.updatedActivity.imageURL!,
),
width: 24.0,
height: 24.0,
cacheKey: activityController.updatedActivity.activityId,
fit: BoxFit.cover,
)
: CachedNetworkImage(
imageRenderMethodForWeb:
ImageRenderMethodForWeb.HttpGet,
httpHeaders: {
'Authorization':
'Bearer ${MatrixState.pangeaController.userController.accessToken}',
},
imageUrl: activityController.updatedActivity.imageURL!,
fit: BoxFit.cover,
width: 24.0,
height: 24.0,
placeholder: (
context,
url,
) =>
const Center(
child: CircularProgressIndicator(),
),
errorWidget: (
context,
url,
error,
) =>
const SizedBox(),
),
)
: const Icon(
Icons.event_note_outlined,
size: 24.0,
),
leading: ImageByUrl(
imageUrl: activityController.updatedActivity.imageURL,
width: 24.0,
borderRadius: BorderRadius.circular(4.0),
replacement: const Icon(
Icons.event_note_outlined,
size: 24.0,
),
),
child: Text(
activityController.updatedActivity.title,
style: const TextStyle(

View file

@ -1,11 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:matrix/matrix.dart';
import 'package:universal_html/html.dart' as html;
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
@ -14,7 +11,7 @@ import 'package:fluffychat/pangea/chat_settings/pages/room_details_buttons.dart'
import 'package:fluffychat/pangea/chat_settings/pages/room_participants_widget.dart';
import 'package:fluffychat/pangea/chat_settings/pages/space_details_button_row.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/widgets/share_room_button.dart';
import 'package:fluffychat/pangea/course_chats/course_chats_page.dart';
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart';
@ -23,7 +20,6 @@ import 'package:fluffychat/pangea/course_settings/course_settings.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/space_analytics/space_analytics.dart';
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/avatar.dart';
@ -278,51 +274,7 @@ class SpaceDetailsContentState extends State<SpaceDetailsContent> {
if (widget.room.classCode != null)
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: PopupMenuButton(
child: const Icon(Symbols.upload),
onSelected: (value) async {
final spaceCode = widget.room.classCode!;
String toCopy = spaceCode;
if (value == 0) {
final String initialUrl = kIsWeb
? html.window.origin!
: Environment.frontendURL;
toCopy =
"$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${widget.room.classCode}";
}
await Clipboard.setData(ClipboardData(text: toCopy));
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(
L10n.of(context).copiedToClipboard,
),
),
);
},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<int>>[
PopupMenuItem<int>(
value: 0,
child: ListTile(
title: Text(L10n.of(context).shareSpaceLink),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<int>(
value: 1,
child: ListTile(
title: Text(
L10n.of(context)
.shareInviteCode(widget.room.classCode!),
),
contentPadding: const EdgeInsets.all(0),
),
),
],
),
child: ShareRoomButton(room: widget.room),
),
],
),

View file

@ -0,0 +1,70 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:matrix/matrix.dart';
import 'package:universal_html/html.dart' as html;
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
class ShareRoomButton extends StatelessWidget {
final Room room;
const ShareRoomButton({
super.key,
required this.room,
});
@override
Widget build(BuildContext context) {
if (room.classCode == null) {
return const SizedBox.shrink();
}
return PopupMenuButton(
child: const Icon(Symbols.upload),
onSelected: (value) async {
final spaceCode = room.classCode!;
String toCopy = spaceCode;
if (value == 0) {
final String initialUrl =
kIsWeb ? html.window.origin! : Environment.frontendURL;
toCopy =
"$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${room.classCode}";
}
await Clipboard.setData(ClipboardData(text: toCopy));
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(
L10n.of(context).copiedToClipboard,
),
),
);
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<int>>[
PopupMenuItem<int>(
value: 0,
child: ListTile(
title: Text(L10n.of(context).shareSpaceLink),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<int>(
value: 1,
child: ListTile(
title: Text(
L10n.of(context).shareInviteCode(room.classCode!),
),
contentPadding: const EdgeInsets.all(0),
),
),
],
);
}
}

View file

@ -0,0 +1,71 @@
// ignore_for_file: depend_on_referenced_packages
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
class ImageByUrl extends StatelessWidget {
final String? imageUrl;
final double width;
final BorderRadius borderRadius;
final Widget? replacement;
const ImageByUrl({
super.key,
required this.imageUrl,
required this.width,
this.replacement,
this.borderRadius = const BorderRadius.all(Radius.circular(20.0)),
});
@override
Widget build(BuildContext context) {
if (imageUrl == null) {
return replacement ?? const SizedBox();
}
return SizedBox(
width: width,
height: width,
child: ClipRRect(
borderRadius: borderRadius,
child: imageUrl!.startsWith("mxc")
? MxcImage(
uri: Uri.parse(imageUrl!),
width: width,
height: width,
cacheKey: imageUrl,
fit: BoxFit.cover,
)
: CachedNetworkImage(
width: width,
height: width,
fit: BoxFit.cover,
imageUrl: imageUrl!,
placeholder: (
context,
url,
) =>
const Center(
child: CircularProgressIndicator(),
),
errorWidget: (
context,
url,
error,
) =>
replacement ?? const SizedBox(),
httpHeaders: {
'Authorization':
'Bearer ${MatrixState.pangeaController.userController.accessToken}',
},
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
),
),
);
}
}

View file

@ -50,50 +50,56 @@ class CourseChatsView extends StatelessWidget {
final Topic? topic = controller.selectedTopic;
final List<String> activityIds = topic?.activityIds ?? [];
final childrenIds =
room.spaceChildren.map((c) => c.roomId).whereType<String>().toSet();
final joinedChats = [];
final joinedSessions = [];
final joinedRooms = room.client.rooms
.where((room) => childrenIds.remove(room.id))
.where((room) => !room.isHiddenRoom)
.toList();
for (final joinedRoom in joinedRooms) {
if (joinedRoom.isActivitySession) {
if (topic == null ||
activityIds.contains(joinedRoom.activityPlan?.activityId)) {
joinedSessions.add(joinedRoom);
}
} else {
joinedChats.add(joinedRoom);
}
}
final discoveredGroupChats = [];
final discoveredSessions = [];
final discoveredChildren =
controller.discoveredChildren ?? <SpaceRoomsChunk>[];
for (final child in discoveredChildren) {
if (child.roomType?.startsWith(PangeaRoomTypes.activitySession) ==
true) {
if (activityIds.contains(child.roomType!.split(":").last)) {
discoveredSessions.add(child);
}
} else {
discoveredGroupChats.add(child);
}
}
final isColumnMode = FluffyThemes.isColumnMode(context);
return StreamBuilder(
stream: room.client.onSync.stream
.where((s) => s.hasRoomUpdate)
.rateLimit(const Duration(seconds: 1)),
builder: (context, snapshot) {
final childrenIds = room.spaceChildren
.map((c) => c.roomId)
.whereType<String>()
.toSet();
final joinedChats = [];
final joinedSessions = [];
final joinedRooms = room.client.rooms
.where((room) => childrenIds.remove(room.id))
.where((room) => !room.isHiddenRoom)
.toList();
for (final joinedRoom in joinedRooms) {
if (joinedRoom.isActivitySession) {
String? activityId = joinedRoom.activityPlan?.activityId;
if (activityId == null && joinedRoom.isActivityRoomType) {
activityId = joinedRoom.roomType!.split(":").last;
}
if (topic == null || activityIds.contains(activityId)) {
joinedSessions.add(joinedRoom);
}
} else {
joinedChats.add(joinedRoom);
}
}
final discoveredGroupChats = [];
final discoveredSessions = [];
final discoveredChildren =
controller.discoveredChildren ?? <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);
return Padding(
padding: isColumnMode
? const EdgeInsets.symmetric(

View file

@ -1,51 +0,0 @@
// ignore_for_file: depend_on_referenced_packages
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:fluffychat/widgets/matrix.dart';
class CourseImage extends StatelessWidget {
final String? imageUrl;
final double width;
final Widget? replacement;
final BorderRadius borderRadius;
const CourseImage({
super.key,
required this.imageUrl,
required this.width,
this.replacement,
this.borderRadius = const BorderRadius.all(Radius.circular(20.0)),
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: borderRadius,
child: imageUrl != null
? CachedNetworkImage(
width: width,
height: width,
fit: BoxFit.cover,
imageUrl: imageUrl!,
httpHeaders: {
'Authorization':
'Bearer ${MatrixState.pangeaController.userController.accessToken}',
},
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
placeholder: (context, url) {
return const Center(
child: CircularProgressIndicator(),
);
},
errorWidget: (context, url, error) {
return replacement ?? const SizedBox();
},
)
: replacement ?? const SizedBox(),
);
}
}

View file

@ -2,7 +2,7 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/course_creation/course_image_widget.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/course_plan_model.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
@ -41,7 +41,7 @@ class CoursePlanTile extends StatelessWidget {
child: Row(
spacing: 4.0,
children: [
CourseImage(
ImageByUrl(
imageUrl: course.imageUrl,
width: 40.0,
replacement: Container(

View file

@ -4,7 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/course_creation/course_image_widget.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/course_plan_builder.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
@ -58,7 +58,7 @@ class SelectedCourseView extends StatelessWidget {
return Column(
spacing: 8.0,
children: [
CourseImage(
ImageByUrl(
imageUrl: course.imageUrl,
width: 100.0,
replacement: Container(

View file

@ -195,6 +195,16 @@ class CoursePlanModel {
}
}
final Map<String, ActivityRole> roles = {};
for (final role in activity.roles) {
roles[role.id] = ActivityRole(
id: role.id,
name: role.name,
avatarUrl: role.avatarUrl,
goal: role.goal,
);
}
activityPlans ??= [];
activityPlans.add(
ActivityPlanModel(
@ -216,17 +226,7 @@ class CoursePlanModel {
vocab: activity.vocabs
.map((v) => Vocab(lemma: v.lemma, pos: v.pos))
.toList(),
roles: activity.roles.asMap().map(
(index, v) => MapEntry(
index.toString(),
ActivityRole(
id: v.id,
name: v.name,
avatarUrl: v.avatarUrl,
goal: v.goal,
),
),
),
roles: roles,
imageURL: activityMedias != null && activityMedias.isNotEmpty
? '${Environment.cmsApi}${activityMedias.first.url}'
: null,

View file

@ -28,12 +28,13 @@ extension RoomInformationRoomExtension on Room {
return botOptions?.mode == BotMode.directChat && await botIsInRoom;
}
String? get roomType =>
getState(EventTypes.RoomCreate)?.content.tryGet<String>('type');
bool isAnalyticsRoomOfUser(String userId) =>
isAnalyticsRoom && isMadeByUser(userId);
bool get isAnalyticsRoom =>
getState(EventTypes.RoomCreate)?.content.tryGet<String>('type') ==
PangeaRoomTypes.analytics;
bool get isAnalyticsRoom => roomType == PangeaRoomTypes.analytics;
bool get isHiddenRoom => isAnalyticsRoom || isHiddenActivityRoom;
}

View file

@ -30,13 +30,28 @@ class TwoColumnLayout extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
// #Pangea
bool showNavRail = FluffyThemes.isColumnMode(context);
if (!showNavRail) {
final roomID = state.pathParameters['roomid'];
final spaceID = state.pathParameters['spaceid'];
if (roomID == null && spaceID == null) {
showNavRail = !["newcourse"].any(
(p) => state.fullPath?.contains(p) ?? false,
);
} else if (roomID == null) {
showNavRail = state.fullPath?.endsWith(':spaceid') == true;
}
}
// Pangea#
return ScaffoldMessenger(
child: Scaffold(
body: Row(
children: [
// #Pangea
if (FluffyThemes.isColumnMode(context) ||
!(state.fullPath?.endsWith(":roomid") ?? false)) ...[
if (showNavRail) ...[
SpacesNavigationRail(
activeSpaceId: state.pathParameters['spaceid'],
path: state.fullPath,