From 414c01b6161671b4ef64dbed3edfa6d71d2327df Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 23 Jun 2025 17:02:24 -0400 Subject: [PATCH] chore: add fake event to show when activity ended --- lib/pages/chat/chat_event_list.dart | 27 ++ lib/pages/chat/events/message.dart | 4 + .../activities/activity_state_event.dart | 333 +++++++++--------- .../events/constants/pangea_event_types.dart | 1 + .../filtered_timeline_extension.dart | 31 +- 5 files changed, 206 insertions(+), 190 deletions(-) diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 948c0578b..030f81422 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:fluffychat/config/app_config.dart'; @@ -10,7 +11,9 @@ 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/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -40,6 +43,30 @@ class ChatEventList extends StatelessWidget { final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; final events = timeline.events.filterByVisibleInGui(); + // #Pangea + if (timeline.room.activityPlan?.endAt != null && + timeline.room.activityPlan!.endAt!.isBefore(DateTime.now())) { + final eventIndex = events.indexWhere( + (e) => e.originServerTs.isBefore( + timeline.room.activityPlan!.endAt!, + ), + ); + + if (eventIndex != -1) { + events.insert( + eventIndex, + Event( + type: PangeaEventTypes.activityPlanEnd, + eventId: timeline.room.client.generateUniqueTransactionId(), + senderId: timeline.room.client.userID!, + originServerTs: timeline.room.activityPlan!.endAt!, + room: timeline.room, + content: {}, + ), + ); + } + } + // Pangea# final animateInEventIndex = controller.animateInEventIndex; // create a map of eventId --> index to greatly improve performance of diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 0fd7e79bc..30d787343 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -134,6 +134,10 @@ class Message extends StatelessWidget { ? ActivityStateEvent(event: event) : const SizedBox(); } + + if (event.type == PangeaEventTypes.activityPlanEnd) { + return const ActivityFinishedEvent(); + } // Pangea# return StateMessage(event); } diff --git a/lib/pangea/activities/activity_state_event.dart b/lib/pangea/activities/activity_state_event.dart index 1f7a250e5..b59b8c233 100644 --- a/lib/pangea/activities/activity_state_event.dart +++ b/lib/pangea/activities/activity_state_event.dart @@ -57,11 +57,6 @@ class ActivityStateEventState extends State { } } - bool get _activityIsOver { - return activityPlan?.endAt != null && - DateTime.now().isAfter(activityPlan!.endAt!); - } - @override Widget build(BuildContext context) { if (activityPlan == null) { @@ -83,186 +78,151 @@ class ActivityStateEventState extends State { spacing: 12.0, children: [ Container( - padding: EdgeInsets.all(_activityIsOver ? 24.0 : 16.0), + padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( color: theme.colorScheme.primaryContainer, borderRadius: BorderRadius.circular(18), ), child: AnimatedSize( duration: FluffyThemes.animationDuration, - child: _activityIsOver - ? Column( - spacing: 12.0, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - L10n.of(context).activityEnded, - style: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - fontSize: 16.0, - ), - ), - CachedNetworkImage( - width: 120.0, - imageUrl: - "${AppConfig.assetsBaseURL}/${ActivityConstants.activityFinishedAsset}", - fit: BoxFit.cover, - placeholder: (context, url) => const Center( - child: CircularProgressIndicator(), - ), - errorWidget: (context, url, error) => - const SizedBox(), - ), - ], - ) - : Text( - activityPlan!.markdown, - style: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - fontSize: AppConfig.fontSizeFactor * - AppConfig.messageFontSize, - ), - ), + child: Text( + activityPlan!.markdown, + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontSize: + AppConfig.fontSizeFactor * AppConfig.messageFontSize, + ), + ), ), ), AnimatedSize( duration: FluffyThemes.animationDuration, - child: _activityIsOver - ? const SizedBox() - : IntrinsicHeight( - child: Row( - spacing: 12.0, + child: IntrinsicHeight( + child: Row( + spacing: 12.0, + children: [ + Container( + height: imageWidth, + width: imageWidth, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: activityPlan!.imageURL != null + ? activityPlan!.imageURL!.startsWith("mxc") + ? MxcImage( + uri: Uri.parse( + activityPlan!.imageURL!, + ), + width: imageWidth, + height: imageWidth, + cacheKey: activityPlan!.bookmarkId, + fit: BoxFit.cover, + ) + : CachedNetworkImage( + imageUrl: activityPlan!.imageURL!, + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: ( + context, + url, + error, + ) => + const SizedBox(), + ) + : const SizedBox(), + ), + ), + Expanded( + child: Column( + spacing: 9.0, + mainAxisSize: MainAxisSize.max, children: [ - Container( - height: imageWidth, - width: imageWidth, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: activityPlan!.imageURL != null - ? activityPlan!.imageURL!.startsWith("mxc") - ? MxcImage( - uri: Uri.parse( - activityPlan!.imageURL!, - ), - width: imageWidth, - height: imageWidth, - cacheKey: activityPlan!.bookmarkId, - fit: BoxFit.cover, - ) - : CachedNetworkImage( - imageUrl: activityPlan!.imageURL!, - fit: BoxFit.cover, - placeholder: (context, url) => - const Center( - child: CircularProgressIndicator(), - ), - errorWidget: ( - context, - url, - error, - ) => - const SizedBox(), - ) - : const SizedBox(), - ), - ), Expanded( - child: Column( - spacing: 9.0, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: SizedBox.expand( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(20), - ), - backgroundColor: - theme.colorScheme.primaryContainer, - foregroundColor: theme - .colorScheme.onPrimaryContainer, - ), - onPressed: () async { - final Duration? duration = - await showDialog( - context: context, - builder: (context) { - return ActivityDurationPopup( - initialValue: - activityPlan?.duration ?? - const Duration(days: 1), - ); - }, - ); - - if (duration == null) return; - - showFutureLoadingDialog( - context: context, - future: () => widget.event.room - .sendActivityPlan( - activityPlan!.copyWith( - endAt: - DateTime.now().add(duration), - duration: duration, - ), - ), - ); - }, - child: CountDown( - deadline: activityPlan!.endAt, - iconSize: 20.0, - textSize: 16.0, - ), - ), + child: SizedBox.expand( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), ), - ), // Optional spacing between buttons - Expanded( - child: SizedBox.expand( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(20), - ), - backgroundColor: - theme.colorScheme.error, - foregroundColor: - theme.colorScheme.onPrimary, - ), - onPressed: () { - showFutureLoadingDialog( - context: context, - future: () => widget.event.room - .sendActivityPlan( - activityPlan!.copyWith( - endAt: DateTime.now(), - duration: Duration.zero, - ), - ), - ); - }, - child: Text( - L10n.of(context).endNow, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), + backgroundColor: + theme.colorScheme.primaryContainer, + foregroundColor: + theme.colorScheme.onPrimaryContainer, + ), + onPressed: () async { + final Duration? duration = await showDialog( + context: context, + builder: (context) { + return ActivityDurationPopup( + initialValue: activityPlan?.duration ?? + const Duration(days: 1), + ); + }, + ); + + if (duration == null) return; + + showFutureLoadingDialog( + context: context, + future: () => + widget.event.room.sendActivityPlan( + activityPlan!.copyWith( + endAt: DateTime.now().add(duration), + duration: duration, ), ), + ); + }, + child: CountDown( + deadline: activityPlan!.endAt, + iconSize: 20.0, + textSize: 16.0, + ), + ), + ), + ), // Optional spacing between buttons + Expanded( + child: SizedBox.expand( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: theme.colorScheme.error, + foregroundColor: theme.colorScheme.onPrimary, + ), + onPressed: () { + showFutureLoadingDialog( + context: context, + future: () => + widget.event.room.sendActivityPlan( + activityPlan!.copyWith( + endAt: DateTime.now(), + duration: Duration.zero, + ), + ), + ); + }, + child: Text( + L10n.of(context).endNow, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, ), ), - ], + ), ), ), ], ), ), + ], + ), + ), ), ], ), @@ -270,3 +230,50 @@ class ActivityStateEventState extends State { ); } } + +class ActivityFinishedEvent extends StatelessWidget { + const ActivityFinishedEvent({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: 400.0, + ), + margin: const EdgeInsets.all(18.0), + child: Container( + padding: const EdgeInsets.all(24.0), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(18), + ), + child: Column( + spacing: 12.0, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + L10n.of(context).activityEnded, + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontSize: 16.0, + ), + ), + CachedNetworkImage( + width: 120.0, + imageUrl: + "${AppConfig.assetsBaseURL}/${ActivityConstants.activityFinishedAsset}", + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => const SizedBox(), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pangea/events/constants/pangea_event_types.dart b/lib/pangea/events/constants/pangea_event_types.dart index fe2a47e18..a385fe97d 100644 --- a/lib/pangea/events/constants/pangea_event_types.dart +++ b/lib/pangea/events/constants/pangea_event_types.dart @@ -26,6 +26,7 @@ class PangeaEventTypes { static const capacity = "pangea.capacity"; static const activityPlan = "pangea.activity_plan"; + static const activityPlanEnd = "pangea.activity.end"; static const userAge = "pangea.user_age"; diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index f074a43f1..d762a4dcc 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -5,33 +5,9 @@ import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import '../../config/app_config.dart'; extension VisibleInGuiExtension on List { - List filterByVisibleInGui({String? exceptionEventId}) { - final visibleEvents = - where((e) => e.isVisibleInGui || e.eventId == exceptionEventId) - .toList(); - - // Hide creation state events: - if (visibleEvents.isNotEmpty && - visibleEvents.last.type == EventTypes.RoomCreate) { - var i = visibleEvents.length - 2; - while (i > 0) { - final event = visibleEvents[i]; - if (!event.isState) break; - if (event.type == EventTypes.Encryption) { - i--; - continue; - } - if (event.type == EventTypes.RoomMember && - event.roomMemberChangeType == RoomMemberChangeType.acceptInvite) { - i--; - continue; - } - visibleEvents.removeAt(i); - i--; - } - } - return visibleEvents; - } + List filterByVisibleInGui({String? exceptionEventId}) => where( + (event) => event.isVisibleInGui || event.eventId == exceptionEventId, + ).toList(); } extension IsStateExtension on Event { @@ -89,6 +65,7 @@ extension IsStateExtension on Event { EventTypes.RoomTombstone, EventTypes.CallInvite, PangeaEventTypes.activityPlan, + PangeaEventTypes.activityPlanEnd, }; // Pangea# }