From e50e1db16a5686d32ccedbfd9dd8eb02e86d8688 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 16 Jun 2025 11:50:21 -0400 Subject: [PATCH] feat: initial work for add duration to in-chat activities --- lib/l10n/intl_en.arb | 6 +- lib/pages/chat/chat_view.dart | 10 + lib/pages/chat/events/message.dart | 7 + lib/pangea/activities/activity_constants.dart | 3 + .../activities/activity_duration_popup.dart | 280 ++++++++++++++++++ .../activities/activity_state_event.dart | 272 +++++++++++++++++ lib/pangea/activities/countdown.dart | 112 +++++++ .../activities/pinned_activity_message.dart | 95 ++++++ .../activity_planner/activity_plan_model.dart | 51 +++- lib/pangea/common/constants/model_keys.dart | 2 + .../extensions/pangea_room_extension.dart | 1 - .../extensions/room_events_extension.dart | 55 ---- .../filtered_timeline_extension.dart | 9 +- 13 files changed, 840 insertions(+), 63 deletions(-) create mode 100644 lib/pangea/activities/activity_constants.dart create mode 100644 lib/pangea/activities/activity_duration_popup.dart create mode 100644 lib/pangea/activities/activity_state_event.dart create mode 100644 lib/pangea/activities/countdown.dart create mode 100644 lib/pangea/activities/pinned_activity_message.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 50166f59f..2d44b7524 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5016,5 +5016,9 @@ "directMessage": "Direct Message", "newDirectMessage": "New direct message", "speakingExercisesTooltip": "Speaking practice", - "noChatsFoundHereYet": "No chats found here yet" + "noChatsFoundHereYet": "No chats found here yet", + "endNow": "End now", + "setDuration": "Set duration", + "activityEnded": "That’s a wrap for this activity! Big thanks to everyone for chatting, learning, and making this space so lively. Language grows with conversation, and every word exchanged brings us closer to confidence and fluency.\n\nKeep practicing, stay curious, and don’t be shy to keep the conversation going!", + "duration": "Duration" } \ No newline at end of file diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 1436cb00b..8840a2bf4 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -13,9 +13,11 @@ 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/activities/pinned_activity_message.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'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -188,6 +190,11 @@ class ChatView extends StatelessWidget { if (scrollUpBannerEventId != null) { appbarBottomHeight += ChatAppBarListTile.fixedHeight; } + // #Pangea + if (controller.room.activityPlan != null) { + appbarBottomHeight += ChatAppBarListTile.fixedHeight; + } + // Pangea# return Scaffold( appBar: AppBar( actionsIconTheme: IconThemeData( @@ -226,6 +233,9 @@ class ChatView extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ PinnedEvents(controller), + // #Pangea + PinnedActivityMessage(controller), + // Pangea# if (scrollUpBannerEventId != null) ChatAppBarListTile( leading: IconButton( diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index ada6cceb4..e98bb7554 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -9,7 +9,9 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart'; +import 'package:fluffychat/pangea/activities/activity_state_event.dart'; import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/file_description.dart'; @@ -121,6 +123,11 @@ class Message extends StatelessWidget { if (event.type == EventTypes.RoomCreate) { return RoomCreationStateEvent(event: event); } + // #Pangea + if (event.type == PangeaEventTypes.activityPlan) { + return ActivityStateEvent(event: event); + } + // Pangea# return StateMessage(event); } diff --git a/lib/pangea/activities/activity_constants.dart b/lib/pangea/activities/activity_constants.dart new file mode 100644 index 000000000..41858d53a --- /dev/null +++ b/lib/pangea/activities/activity_constants.dart @@ -0,0 +1,3 @@ +class ActivityConstants { + static const String activityFinishedAsset = "EndActivityMsg.png"; +} diff --git a/lib/pangea/activities/activity_duration_popup.dart b/lib/pangea/activities/activity_duration_popup.dart new file mode 100644 index 000000000..4ce8a40a6 --- /dev/null +++ b/lib/pangea/activities/activity_duration_popup.dart @@ -0,0 +1,280 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/l10n/l10n.dart'; + +class ActivityDurationPopup extends StatefulWidget { + final Duration initialValue; + const ActivityDurationPopup({ + super.key, + required this.initialValue, + }); + + @override + State createState() => ActivityDurationPopupState(); +} + +class ActivityDurationPopupState extends State { + final TextEditingController _daysController = TextEditingController(); + final TextEditingController _hoursController = TextEditingController(); + final TextEditingController _minutesController = TextEditingController(); + + String? error; + + final List _durations = [ + const Duration(minutes: 15), + const Duration(minutes: 30), + const Duration(minutes: 45), + const Duration(minutes: 60), + const Duration(hours: 1, minutes: 30), + const Duration(hours: 2), + const Duration(hours: 24), + const Duration(days: 2), + const Duration(days: 7), + ]; + + @override + void initState() { + super.initState(); + _daysController.text = widget.initialValue.inDays.toString(); + _hoursController.text = + widget.initialValue.inHours.remainder(24).toString(); + _minutesController.text = + widget.initialValue.inMinutes.remainder(60).toString(); + + _daysController.addListener(() => setState(() => error = null)); + _hoursController.addListener(() => setState(() => error = null)); + _minutesController.addListener(() => setState(() => error = null)); + } + + @override + void dispose() { + _daysController.dispose(); + _hoursController.dispose(); + _minutesController.dispose(); + super.dispose(); + } + + void _setDuration({int? days, int? hours, int? minutes}) { + setState(() { + if (days != null) _daysController.text = days.toString(); + if (hours != null) _hoursController.text = hours.toString(); + if (minutes != null) _minutesController.text = minutes.toString(); + }); + } + + String _formatDuration(Duration duration) { + final days = duration.inDays; + final hours = duration.inHours.remainder(24); + final minutes = duration.inMinutes.remainder(60); + + final List parts = []; + if (days > 0) parts.add("${days}d"); + if (hours > 0) parts.add("${hours}h"); + if (minutes > 0) parts.add("${minutes}m"); + if (parts.isEmpty) return "0m"; + + return parts.join(" "); + } + + Duration get _duration { + final days = int.tryParse(_daysController.text) ?? 0; + final hours = int.tryParse(_hoursController.text) ?? 0; + final minutes = int.tryParse(_minutesController.text) ?? 0; + return Duration(days: days, hours: hours, minutes: minutes); + } + + void _submit() { + final days = int.tryParse(_daysController.text); + final hours = int.tryParse(_hoursController.text); + final minutes = int.tryParse(_minutesController.text); + + if (days == null || hours == null || minutes == null) { + setState(() { + error = "Invalid duration"; + }); + return; + } + + Navigator.of(context).pop(_duration); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 350.0, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + spacing: 12.0, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + L10n.of(context).setDuration, + style: const TextStyle(fontSize: 20.0, height: 1.2), + ), + Column( + children: [ + Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 2, + color: theme.colorScheme.primary.withAlpha(100), + ), + borderRadius: BorderRadius.circular(20), + ), + ), + padding: const EdgeInsets.only( + top: 12.0, + bottom: 12.0, + right: 24.0, + left: 8.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 12.0, + children: [ + _DatePickerInput( + type: "d", + controller: _daysController, + ), + _DatePickerInput( + type: "h", + controller: _hoursController, + ), + _DatePickerInput( + type: "m", + controller: _minutesController, + ), + ], + ), + const Icon( + Icons.alarm, + size: 24, + ), + ], + ), + ), + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: error != null + ? Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + error!, + style: TextStyle( + color: theme.colorScheme.error, + fontSize: 14.0, + ), + ), + ) + : const SizedBox.shrink(), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 24.0, + ), + child: Wrap( + spacing: 10.0, + runSpacing: 10.0, + children: _durations + .map( + (d) => InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + _setDuration( + days: d.inDays, + hours: d.inHours.remainder(24), + minutes: d.inMinutes.remainder(60), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 0.0, + ), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer + .withAlpha(_duration == d ? 200 : 100), + borderRadius: BorderRadius.circular(12), + ), + child: Text(_formatDuration(d)), + ), + ), + ) + .toList(), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: _submit, + child: Text(L10n.of(context).confirm), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} + +class _DatePickerInput extends StatelessWidget { + final String type; + final TextEditingController controller; + + const _DatePickerInput({ + required this.type, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox( + width: 35.0, + child: TextField( + controller: controller, + textAlign: TextAlign.end, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + contentPadding: const EdgeInsets.all(0.0), + hintText: "0", + hintStyle: TextStyle( + fontSize: 20.0, + color: theme.colorScheme.onSurfaceVariant.withAlpha(100), + ), + ), + style: const TextStyle( + fontSize: 20.0, + ), + keyboardType: TextInputType.number, + ), + ), + Text(type, style: const TextStyle(fontSize: 20.0)), + ], + ); + } +} diff --git a/lib/pangea/activities/activity_state_event.dart b/lib/pangea/activities/activity_state_event.dart new file mode 100644 index 000000000..97715632e --- /dev/null +++ b/lib/pangea/activities/activity_state_event.dart @@ -0,0 +1,272 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/activities/activity_constants.dart'; +import 'package:fluffychat/pangea/activities/activity_duration_popup.dart'; +import 'package:fluffychat/pangea/activities/countdown.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; + +class ActivityStateEvent extends StatefulWidget { + final Event event; + + const ActivityStateEvent({required this.event, super.key}); + + @override + State createState() => ActivityStateEventState(); +} + +class ActivityStateEventState extends State { + late final Timer _timer; + + @override + void initState() { + super.initState(); + final now = DateTime.now(); + final delay = activityPlan?.endAt != null + ? activityPlan!.endAt!.difference(now) + : null; + + if (delay != null && delay > Duration.zero) { + _timer = Timer(delay, () { + setState(() {}); + }); + } + } + + @override + void dispose() { + _timer.cancel(); + super.dispose(); + } + + ActivityPlanModel? get activityPlan { + try { + return ActivityPlanModel.fromJson(widget.event.content); + } catch (e) { + return null; + } + } + + bool get _activityIsOver { + return activityPlan?.endAt != null && + DateTime.now().isAfter(activityPlan!.endAt!); + } + + @override + Widget build(BuildContext context) { + if (activityPlan == null) { + return const SizedBox.shrink(); + } + + final theme = Theme.of(context); + final isColumnMode = FluffyThemes.isColumnMode(context); + + final double imageWidth = isColumnMode ? 240.0 : 175.0; + + return Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: 400.0, + ), + margin: const EdgeInsets.all(18.0), + child: Column( + spacing: 12.0, + children: [ + Container( + padding: EdgeInsets.all(_activityIsOver ? 24.0 : 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, + ), + ), + ), + ), + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: _activityIsOver + ? const SizedBox() + : 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: [ + 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, + ), + ), + ), + ), // 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, + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/activities/countdown.dart b/lib/pangea/activities/countdown.dart new file mode 100644 index 000000000..f0ed5e48d --- /dev/null +++ b/lib/pangea/activities/countdown.dart @@ -0,0 +1,112 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; + +class CountDown extends StatefulWidget { + final DateTime? deadline; + + final double? iconSize; + final double? textSize; + + const CountDown({ + super.key, + required this.deadline, + this.iconSize, + this.textSize, + }); + + @override + State createState() => CountDownState(); +} + +class CountDownState extends State { + late Timer _timer; + + @override + void initState() { + super.initState(); + _timer = Timer.periodic(const Duration(seconds: 1), (_) { + setState(() {}); + }); + + // final now = DateTime.now(); + // final delay = widget.deadline?.difference(now); + + // if (delay != null && delay > Duration.zero) { + // _endTimer = Timer(delay, () { + // setState( + // () => setState(() {}), + // ); + // }); + // } + } + + @override + void dispose() { + _timer.cancel(); + // _endTimer.cancel(); + super.dispose(); + } + + String? _formatDuration(Duration duration) { + final days = duration.inDays; + final hours = duration.inHours.remainder(24); + final minutes = duration.inMinutes.remainder(60); + final seconds = duration.inSeconds.remainder(60); + + final List parts = []; + if (days > 0) parts.add("${days}d"); + if (hours > 0) parts.add("${hours}h"); + if (minutes > 0) parts.add("${minutes}m"); + if (seconds > 0 && minutes <= 0) parts.add("${seconds}s"); + if (parts.isEmpty) return null; + + return parts.join(" "); + } + + Duration? get remainingTime { + if (widget.deadline == null) { + return null; + } + + final now = DateTime.now(); + return widget.deadline!.isAfter(now) + ? widget.deadline!.difference(now) + : Duration.zero; + } + + @override + Widget build(BuildContext context) { + final remainingTime = this.remainingTime; + final durationString = _formatDuration(remainingTime ?? Duration.zero); + + return ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 250.0, + ), + child: Row( + spacing: 4.0, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.timer_outlined, + size: widget.iconSize ?? 28.0, + ), + Flexible( + child: Text( + remainingTime != null && + remainingTime > Duration.zero && + durationString != null + ? durationString + : L10n.of(context).duration, + style: TextStyle(fontSize: widget.textSize ?? 20), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/activities/pinned_activity_message.dart b/lib/pangea/activities/pinned_activity_message.dart new file mode 100644 index 000000000..32554058e --- /dev/null +++ b/lib/pangea/activities/pinned_activity_message.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart'; +import 'package:fluffychat/pangea/activities/activity_duration_popup.dart'; +import 'package:fluffychat/pangea/activities/countdown.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; + +class PinnedActivityMessage extends StatelessWidget { + final ChatController controller; + + const PinnedActivityMessage(this.controller, {super.key}); + + Future _scrollToEvent() async { + final eventId = _activityPlanEvent?.eventId; + if (eventId != null) controller.scrollToEventId(eventId); + } + + Event? get _activityPlanEvent => controller.timeline?.events.firstWhereOrNull( + (event) => event.type == PangeaEventTypes.activityPlan, + ); + + ActivityPlanModel? get _activityPlan => controller.room.activityPlan; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + if (_activityPlan?.endAt == null || + _activityPlan!.endAt!.isBefore(DateTime.now())) { + return const SizedBox.shrink(); + } + + return ChatAppBarListTile( + title: _activityPlan!.title, + leading: IconButton( + splashRadius: 18, + iconSize: 18, + color: theme.colorScheme.onSurfaceVariant, + icon: const Icon(Icons.push_pin), + onPressed: () {}, + ), + trailing: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () 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: () => controller.room.sendActivityPlan( + _activityPlan!.copyWith( + endAt: DateTime.now().add(duration), + duration: duration, + ), + ), + ); + }, + child: Container( + padding: const EdgeInsets.all(4.0), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(20), + ), + child: CountDown( + deadline: _activityPlan!.endAt, + iconSize: 16.0, + textSize: 14.0, + ), + ), + ), + ), + onTap: _scrollToEvent, + ); + } +} diff --git a/lib/pangea/activity_planner/activity_plan_model.dart b/lib/pangea/activity_planner/activity_plan_model.dart index bd37039fb..7ea09a68a 100644 --- a/lib/pangea/activity_planner/activity_plan_model.dart +++ b/lib/pangea/activity_planner/activity_plan_model.dart @@ -11,6 +11,8 @@ class ActivityPlanModel { final String instructions; final List vocab; final String? imageURL; + final DateTime? endAt; + final Duration? duration; ActivityPlanModel({ required this.req, @@ -19,31 +21,70 @@ class ActivityPlanModel { required this.instructions, required this.vocab, this.imageURL, + this.endAt, + this.duration, }) : bookmarkId = "${title.hashCode ^ learningObjective.hashCode ^ instructions.hashCode ^ imageURL.hashCode ^ vocab.map((v) => v.hashCode).reduce((a, b) => a ^ b)}"; + ActivityPlanModel copyWith({ + String? title, + String? learningObjective, + String? instructions, + List? vocab, + String? imageURL, + DateTime? endAt, + Duration? duration, + }) { + return ActivityPlanModel( + req: req, + title: title ?? this.title, + learningObjective: learningObjective ?? this.learningObjective, + instructions: instructions ?? this.instructions, + vocab: vocab ?? this.vocab, + imageURL: imageURL ?? this.imageURL, + endAt: endAt ?? this.endAt, + duration: duration ?? this.duration, + ); + } + factory ActivityPlanModel.fromJson(Map json) { return ActivityPlanModel( + imageURL: json[ModelKey.activityPlanImageURL], + instructions: json[ModelKey.activityPlanInstructions], req: ActivityPlanRequest.fromJson(json[ModelKey.activityPlanRequest]), title: json[ModelKey.activityPlanTitle], learningObjective: json[ModelKey.activityPlanLearningObjective], - instructions: json[ModelKey.activityPlanInstructions], vocab: List.from( json[ModelKey.activityPlanVocab].map((vocab) => Vocab.fromJson(vocab)), ), - imageURL: json[ModelKey.activityPlanImageURL], + endAt: json[ModelKey.activityPlanEndAt] != null + ? DateTime.parse(json[ModelKey.activityPlanEndAt]) + : null, + duration: json[ModelKey.activityPlanDuration] != null + ? Duration( + days: json[ModelKey.activityPlanDuration]['days'] ?? 0, + hours: json[ModelKey.activityPlanDuration]['hours'] ?? 0, + minutes: json[ModelKey.activityPlanDuration]['minutes'] ?? 0, + ) + : null, ); } Map toJson() { return { + ModelKey.activityPlanBookmarkId: bookmarkId, + ModelKey.activityPlanImageURL: imageURL, + ModelKey.activityPlanInstructions: instructions, ModelKey.activityPlanRequest: req.toJson(), ModelKey.activityPlanTitle: title, ModelKey.activityPlanLearningObjective: learningObjective, - ModelKey.activityPlanInstructions: instructions, ModelKey.activityPlanVocab: vocab.map((vocab) => vocab.toJson()).toList(), - ModelKey.activityPlanImageURL: imageURL, - ModelKey.activityPlanBookmarkId: bookmarkId, + ModelKey.activityPlanEndAt: endAt?.toIso8601String(), + ModelKey.activityPlanDuration: { + 'days': duration?.inDays ?? 0, + 'hours': duration?.inHours.remainder(24) ?? 0, + 'minutes': duration?.inMinutes.remainder(60) ?? 0, + }, }; } diff --git a/lib/pangea/common/constants/model_keys.dart b/lib/pangea/common/constants/model_keys.dart index 781667c34..22b360c8d 100644 --- a/lib/pangea/common/constants/model_keys.dart +++ b/lib/pangea/common/constants/model_keys.dart @@ -163,6 +163,8 @@ class ModelKey { static const String activityPlanVocab = "vocab"; static const String activityPlanImageURL = "image_url"; static const String activityPlanBookmarkId = "bookmark_id"; + static const String activityPlanEndAt = "end_at"; + static const String activityPlanDuration = "duration"; static const String activityRequestTopic = "topic"; static const String activityRequestMode = "mode"; diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index 316ef7199..58616de36 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -9,7 +9,6 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:html_unescape/html_unescape.dart'; -import 'package:http/http.dart' as http; import 'package:matrix/matrix.dart'; import 'package:matrix/src/utils/markdown.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; diff --git a/lib/pangea/extensions/room_events_extension.dart b/lib/pangea/extensions/room_events_extension.dart index 3da8c1272..7c5676bc3 100644 --- a/lib/pangea/extensions/room_events_extension.dart +++ b/lib/pangea/extensions/room_events_extension.dart @@ -277,57 +277,6 @@ extension EventsRoomExtension on Room { }) async { BookmarkedActivitiesRepo.save(activity); - String? imageURL = activity.imageURL; - final eventId = await pangeaSendTextEvent( - activity.markdown, - messageTag: ModelKey.messageTagActivityPlan, - ); - - Uint8List? bytes = avatar; - if (imageURL != null && bytes == null) { - try { - final resp = await http - .get(Uri.parse(imageURL)) - .timeout(const Duration(seconds: 5)); - bytes = resp.bodyBytes; - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "avatarURL": imageURL, - }, - ); - } - } - - if (bytes != null && imageURL == null) { - final url = await client.uploadContent( - bytes, - filename: filename, - ); - imageURL = url.toString(); - } - - MatrixFile? file; - if (filename != null && bytes != null) { - file = MatrixFile( - bytes: bytes, - name: filename, - ); - } - - if (file != null) { - final content = { - 'msgtype': file.msgType, - 'body': file.name, - 'filename': file.name, - 'url': imageURL, - ModelKey.messageTags: ModelKey.messageTagActivityPlan, - }; - await sendEvent(content); - } - if (canSendDefaultStates) { await client.setRoomStateWithKey( id, @@ -335,10 +284,6 @@ extension EventsRoomExtension on Room { "", activity.toJson(), ); - - if (eventId != null) { - await setPinnedEvents([eventId]); - } } } diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index fee1b3c62..f074a43f1 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -1,6 +1,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import '../../config/app_config.dart'; extension VisibleInGuiExtension on List { @@ -46,7 +47,12 @@ extension IsStateExtension on Event { // if we enabled to hide all redacted events, don't show those (!AppConfig.hideRedactedEvents || !redacted) && // if we enabled to hide all unknown events, don't show those - (!AppConfig.hideUnknownEvents || isEventTypeKnown) && + // #Pangea + // (!AppConfig.hideUnknownEvents || isEventTypeKnown) && + (!AppConfig.hideUnknownEvents || + isEventTypeKnown || + importantStateEvents.contains(type)) && + // Pangea# // remove state events that we don't want to render (isState || !AppConfig.hideAllStateEvents) && // #Pangea @@ -82,6 +88,7 @@ extension IsStateExtension on Event { EventTypes.RoomMember, EventTypes.RoomTombstone, EventTypes.CallInvite, + PangeaEventTypes.activityPlan, }; // Pangea# }