chore: simplify activity summary display, add activity summary widget to chat event list

This commit is contained in:
ggurdin 2025-08-12 11:31:07 -04:00 committed by GitHub
parent 25e72f440d
commit 1e3529180b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 286 additions and 391 deletions

View file

@ -5192,5 +5192,6 @@
}
}
},
"noDataFound": "No data found"
"noDataFound": "No data found",
"activityFinishedMessage": "All Finished!"
}

View file

@ -28,6 +28,7 @@ import 'package:fluffychat/pages/chat/event_info_dialog.dart';
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/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';
@ -2180,6 +2181,12 @@ class ChatController extends State<ChatPageWithRoom>
closePrevOverlay: false,
);
}
ActivityRoleModel? highlightedRole;
void highlightRole(ActivityRoleModel role) {
if (mounted) setState(() => highlightedRole = role);
}
// Pangea#
late final ValueNotifier<bool> _displayChatDetailsColumn;

View file

@ -9,6 +9,7 @@ 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/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
@ -91,7 +92,10 @@ class ChatEventList extends StatelessWidget {
}
// Request history button or progress indicator:
if (i == events.length + 1) {
// #Pangea
// if (i == events.length + 1) {
if (i == events.length + 2) {
// Pangea#
if (timeline.isRequestingHistory) {
return const Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
@ -118,7 +122,17 @@ class ChatEventList extends StatelessWidget {
}
return const SizedBox.shrink();
}
i--;
// #Pangea
if (i == 1) {
return ActivityFinishedStatusMessage(controller: controller);
}
// Pangea#
// #Pangea
// i--;
i = i - 2;
// Pangea#
// The message at this index:
final event = events[i];
@ -193,7 +207,10 @@ class ChatEventList extends StatelessWidget {
),
);
},
childCount: events.length + 2,
// #Pangea
// childCount: events.length + 2,
childCount: events.length + 3,
// Pangea#
findChildIndexCallback: (key) =>
controller.findChildIndexCallback(key, thisEventsKeyMap),
),

View file

@ -1,12 +1,11 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.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_participant_indicator.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_results_carousel.dart';
@ -14,301 +13,217 @@ import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
class ActivityFinishedStatusMessage extends StatefulWidget {
final Room room;
class ActivityFinishedStatusMessage extends StatelessWidget {
final ChatController controller;
const ActivityFinishedStatusMessage({
super.key,
required this.room,
required this.controller,
});
@override
ActivityFinishedStatusMessageState createState() =>
ActivityFinishedStatusMessageState();
}
class ActivityFinishedStatusMessageState
extends State<ActivityFinishedStatusMessage> {
ActivityRoleModel? _highlightedRole;
bool _expanded = true;
@override
void initState() {
super.initState();
_setDefaultHighlightedRole();
if (widget.room.activityIsFinished && widget.room.activitySummary == null) {
widget.room.fetchSummaries();
}
}
@override
void didUpdateWidget(ActivityFinishedStatusMessage oldWidget) {
super.didUpdateWidget(oldWidget);
_setDefaultHighlightedRole();
}
Map<String, ActivityRole> get _roles => widget.room.activityPlan?.roles ?? {};
void _setExpanded(bool expanded) {
if (mounted) setState(() => _expanded = expanded);
}
int get _hightlightedRoleIndex {
if (_highlightedRole == null) {
return -1; // No highlighted role
}
return rolesWithSummaries.indexOf(_highlightedRole!);
}
void _setDefaultHighlightedRole() {
if (_hightlightedRoleIndex >= 0) return;
final roles = rolesWithSummaries;
_highlightedRole = roles.firstWhereOrNull(
(r) => r.userId == widget.room.client.userID,
);
if (_highlightedRole == null && roles.isNotEmpty) {
_highlightedRole = roles.first;
}
if (mounted) setState(() {});
}
void _highlightRole(ActivityRoleModel role) {
if (mounted) setState(() => _highlightedRole = role);
}
bool get _canMoveLeft =>
_hightlightedRoleIndex > 0 && _highlightedRole != null;
bool get _canMoveRight =>
_hightlightedRoleIndex < rolesWithSummaries.length - 1 &&
_highlightedRole != null;
void _moveLeft() {
if (_hightlightedRoleIndex > 0) {
_highlightRole(rolesWithSummaries[_hightlightedRoleIndex - 1]);
}
}
void _moveRight() {
if (_hightlightedRoleIndex < rolesWithSummaries.length - 1) {
_highlightRole(rolesWithSummaries[_hightlightedRoleIndex + 1]);
}
}
Map<String, ActivityRole> get _roles =>
controller.room.activityPlan?.roles ?? {};
Future<void> _archiveToAnalytics() async {
await widget.room.archiveActivity();
await controller.room.archiveActivity();
await MatrixState.pangeaController.putAnalytics
.sendActivityAnalytics(widget.room.id);
.sendActivityAnalytics(controller.room.id);
}
List<ActivityRoleModel> get rolesWithSummaries {
if (widget.room.activitySummary?.summary == null) {
List<ActivityRoleModel> get _rolesWithSummaries {
if (controller.room.activitySummary?.summary == null) {
return <ActivityRoleModel>[];
}
final roles = widget.room.activityRoles;
final roles = controller.room.activityRoles;
return roles?.roles.values.where((role) {
return widget.room.activitySummary!.summary!.participants.any(
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) {
final summary = widget.room.activitySummary;
final imageURL = widget.room.activityPlan!.imageURL;
if (!controller.room.showActivityChatUI ||
!controller.room.activityIsFinished) {
return const SizedBox.shrink();
}
final summary = controller.room.activitySummary;
final theme = Theme.of(context);
final isColumnMode = MediaQuery.of(context).size.width < 600;
final user = widget.room.getParticipants().firstWhereOrNull(
final user = controller.room.getParticipants().firstWhereOrNull(
(u) => u.id == _highlightedRole?.userId,
);
final userSummary =
widget.room.activitySummary?.summary?.participants.firstWhereOrNull(
(p) => p.participantId == _highlightedRole!.userId,
controller.room.activitySummary?.summary?.participants.firstWhereOrNull(
(p) => p.participantId == _highlightedRole?.userId,
);
return AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _expanded
? [
if (summary?.summary != null) ...[
IconButton(
icon: Icon(
Icons.expand_more,
color: theme.colorScheme.onSurfaceVariant,
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,
),
onPressed: () => _setExpanded(!_expanded),
),
const SizedBox(height: 8.0),
if (imageURL != null)
ClipRRect(
borderRadius: BorderRadius.circular(100),
child: imageURL.startsWith("mxc")
? MxcImage(
uri: Uri.parse(imageURL),
width: 100.0,
height: 100.0,
cacheKey: widget.room.activityPlan!.bookmarkId,
fit: BoxFit.cover,
)
: CachedNetworkImage(
imageUrl: imageURL,
fit: BoxFit.cover,
width: 100.0,
height: 100.0,
placeholder: (
context,
url,
) =>
const Center(
child: CircularProgressIndicator(),
),
errorWidget: (
context,
url,
error,
) =>
const SizedBox(),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(
summary!.summary!.summary,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: isColumnMode ? 16.0 : 12.0,
),
const SizedBox(height: 16.0),
if (_highlightedRole != null && userSummary != null)
ClipRRect(
borderRadius: BorderRadius.circular(24.0),
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer,
),
),
),
if (_highlightedRole != null && userSummary != null)
ActivityResultsCarousel(
selectedRole: _highlightedRole!,
moveLeft: _canMoveLeft ? _moveLeft : null,
moveRight: _canMoveRight ? _moveRight : null,
user: user,
summary: userSummary,
),
const SizedBox(height: 8.0),
Wrap(
alignment: WrapAlignment.center,
spacing: 12.0,
runSpacing: 12.0,
children: rolesWithSummaries.map(
(role) {
final user =
widget.room.getParticipants().firstWhereOrNull(
(u) => u.id == role.userId,
child: Column(
children: [
ActivityResultsCarousel(
selectedRole: _highlightedRole!,
user: user,
summary: userSummary,
),
Wrap(
alignment: WrapAlignment.center,
children: _rolesWithSummaries.map(
(role) {
final user = controller.room
.getParticipants()
.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,
),
);
return ActivityParticipantIndicator(
availableRole: _roles[role.id]!,
avatarUrl: _roles[role.id]?.avatarUrl ??
user?.avatarUrl?.toString(),
onTap: _highlightedRole == role
? null
: () => _highlightRole(role),
assignedRole: role,
selected: _highlightedRole == role,
);
},
).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: () => widget.room.fetchSummaries(),
child: Text(L10n.of(context).requestSummaries),
),
],
),
),
if (!widget.room.isHiddenActivityRoom)
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 8.0,
},
).toList(),
),
],
),
foregroundColor: theme.colorScheme.onPrimaryContainer,
backgroundColor: theme.colorScheme.primaryContainer,
),
onPressed: () async {
final resp = await showFutureLoadingDialog(
context: context,
future: _archiveToAnalytics,
);
),
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,
);
if (!resp.isError) {
context.go(
"/rooms/analytics?mode=activities",
);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).archiveToAnalytics),
],
),
if (!resp.isError) {
context.go(
"/rooms/analytics?mode=activities",
);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).archiveToAnalytics),
],
),
]
: [
if (summary != null)
IconButton(
icon: Icon(
Icons.expand_less,
color: theme.colorScheme.onSurfaceVariant,
),
onPressed: () => _setExpanded(!_expanded),
),
],
),
],
),
),
),
);
}

View file

@ -20,6 +20,9 @@ class ActivityParticipantIndicator extends StatelessWidget {
final bool selected;
final double opacity;
final EdgeInsetsGeometry? padding;
final BorderRadius? borderRadius;
const ActivityParticipantIndicator({
super.key,
required this.availableRole,
@ -28,6 +31,8 @@ class ActivityParticipantIndicator extends StatelessWidget {
this.selected = false,
this.onTap,
this.opacity = 1.0,
this.padding,
this.borderRadius,
});
@override
@ -44,16 +49,15 @@ class ActivityParticipantIndicator extends StatelessWidget {
return Opacity(
opacity: opacity,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 8.0,
),
padding: padding ??
const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 8.0,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
borderRadius: borderRadius ?? BorderRadius.circular(8.0),
color: hovered || selected
? theme.colorScheme.primaryContainer.withAlpha(
selected ? 100 : 50,
)
? theme.colorScheme.surfaceContainerHighest
: Colors.transparent,
),
child: Column(

View file

@ -5,23 +5,16 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
import 'package:fluffychat/widgets/avatar.dart';
class ActivityResultsCarousel extends StatelessWidget {
final ActivityRoleModel selectedRole;
final ParticipantSummaryModel summary;
final VoidCallback? moveLeft;
final VoidCallback? moveRight;
final User? user;
const ActivityResultsCarousel({
super.key,
required this.selectedRole,
required this.moveLeft,
required this.moveRight,
required this.summary,
this.user,
});
@ -31,111 +24,69 @@ class ActivityResultsCarousel extends StatelessWidget {
final theme = Theme.of(context);
final isColumnMode = FluffyThemes.isColumnMode(context);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new),
onPressed: moveLeft,
),
Padding(
padding: const EdgeInsets.all(12.0),
child: PressableButton(
onPressed: null,
borderRadius: BorderRadius.circular(24.0),
color: theme.brightness == Brightness.dark
? theme.colorScheme.primary
: theme.colorScheme.surfaceContainerHighest,
colorFactor: theme.brightness == Brightness.dark ? 0.6 : 0.2,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24.0),
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
),
padding: const EdgeInsets.all(12.0),
alignment: Alignment.centerLeft,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: isColumnMode ? 80.0 : 125.0,
child: SingleChildScrollView(
child: Text(
summary.feedback,
style: const TextStyle(fontSize: 12.0),
),
width: isColumnMode ? 225.0 : 175.0,
child: Container(
),
),
const SizedBox(height: 10.0),
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
Container(
padding: const EdgeInsets.all(4.0),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(24.0),
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: [
Align(
alignment: Alignment.center,
child: Avatar(
size: isColumnMode ? 60.0 : 40.0,
mxContent: user?.avatarUrl,
name: user?.calcDisplayname() ??
summary.participantId.localpart,
userId: selectedRole.userId,
const Icon(Icons.school, size: 12.0),
Text(
summary.cefrLevel,
style: const TextStyle(
fontSize: 12.0,
),
),
const SizedBox(height: 4.0),
Text(
selectedRole.role != null
? "${selectedRole.role!} | ${selectedRole.userId.localpart}"
: "${selectedRole.userId.localpart}",
style: TextStyle(fontSize: isColumnMode ? 16.0 : 12.0),
),
const SizedBox(height: 10.0),
Text(
summary.feedback,
style: TextStyle(fontSize: isColumnMode ? 12.0 : 8.0),
),
const SizedBox(height: 10.0),
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
Container(
padding: const EdgeInsets.all(4.0),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(20),
),
child: Row(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.school, size: 12.0),
Text(
summary.cefrLevel,
style: TextStyle(
fontSize: isColumnMode ? 12.0 : 8.0,
),
),
],
),
),
...summary.superlatives.map(
(sup) => Container(
padding: const EdgeInsets.all(4.0),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(20),
),
child: Text(
sup,
style: TextStyle(
fontSize: isColumnMode ? 12.0 : 8.0,
),
),
),
),
],
),
],
),
),
),
...summary.superlatives.map(
(sup) => Container(
padding: const EdgeInsets.all(4.0),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(20),
),
child: Text(
sup,
style: const TextStyle(
fontSize: 12.0,
),
),
),
),
],
),
),
IconButton(
icon: const Icon(Icons.arrow_forward_ios),
onPressed: moveRight,
),
],
],
),
);
}
}

View file

@ -63,7 +63,7 @@ class ActivityStateEvent extends StatelessWidget {
? 0.5
: 1.0,
avatarUrl:
availableRole.avatarUrl ?? user?.avatarUrl.toString(),
availableRole.avatarUrl ?? user?.avatarUrl?.toString(),
);
}).toList(),
),

View file

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_finished_status_message.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_unfinished_status_message.dart';
@ -17,32 +16,33 @@ class ActivityStatusMessage extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (!room.showActivityChatUI) {
if (!room.showActivityChatUI || room.activityIsFinished) {
return const SizedBox.shrink();
}
final role = room.activityRoles?.role(room.client.userID!);
if (role != null && !role.isFinished) {
return const SizedBox.shrink();
}
return Material(
child: AnimatedSize(
duration: FluffyThemes.animationDuration,
child: room.isInactiveInActivity
? 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: room.activityIsFinished
? ActivityFinishedStatusMessage(room: room)
: ActivityUnfinishedStatusMessage(room: room),
),
),
)
: const SizedBox.shrink(),
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),
),
),
),
),
);
}