From d111b117832e63b09a4acf5c2348c3c5f674f542 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Mon, 14 Apr 2025 10:56:19 -0400 Subject: [PATCH] chore: fixes for editting / bookmarking of activities (#2436) --- .../activity_generator.dart | 14 ++- .../activity_generator_view.dart | 2 +- .../activity_planner/activity_plan_card.dart | 66 ++++++---- .../activity_planner/activity_plan_model.dart | 13 +- .../bookmarked_activity_list.dart | 17 ++- .../activity_suggestion_carousel.dart | 18 ++- .../activity_suggestion_dialog.dart | 113 ++++++++++-------- .../activity_suggestions_area.dart | 2 +- 8 files changed, 159 insertions(+), 86 deletions(-) diff --git a/lib/pangea/activity_generator/activity_generator.dart b/lib/pangea/activity_generator/activity_generator.dart index 3c8813854..1c0c798bf 100644 --- a/lib/pangea/activity_generator/activity_generator.dart +++ b/lib/pangea/activity_generator/activity_generator.dart @@ -189,10 +189,16 @@ class ActivityGeneratorState extends State { final imageUrl = "${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.modeImageFileStart}$modeName.jpg"; setState(() { - filename = - "${ActivitySuggestionsConstants.modeImageFileStart}$modeName.jpg"; - for (final activity in activities!) { - activity.imageURL = imageUrl; + filename = imageUrl; + for (ActivityPlanModel activity in activities!) { + activity = ActivityPlanModel( + req: activity.req, + title: activity.title, + learningObjective: activity.learningObjective, + instructions: activity.instructions, + vocab: activity.vocab, + imageURL: imageUrl, + ); } }); } diff --git a/lib/pangea/activity_generator/activity_generator_view.dart b/lib/pangea/activity_generator/activity_generator_view.dart index f7840b43d..c197e6f88 100644 --- a/lib/pangea/activity_generator/activity_generator_view.dart +++ b/lib/pangea/activity_generator/activity_generator_view.dart @@ -59,7 +59,7 @@ class ActivityGeneratorView extends StatelessWidget { onEdit: (updatedActivity) => controller.onEdit(index, updatedActivity), onChange: controller.update, - initialFilename: controller.filename, + initialImageURL: controller.filename, ); }, ); diff --git a/lib/pangea/activity_planner/activity_plan_card.dart b/lib/pangea/activity_planner/activity_plan_card.dart index b5ed335d4..0d721e11e 100644 --- a/lib/pangea/activity_planner/activity_plan_card.dart +++ b/lib/pangea/activity_planner/activity_plan_card.dart @@ -27,7 +27,7 @@ class ActivityPlanCard extends StatefulWidget { final VoidCallback onChange; final ValueChanged onEdit; final double maxWidth; - final String? initialFilename; + final String? initialImageURL; const ActivityPlanCard({ super.key, @@ -36,7 +36,7 @@ class ActivityPlanCard extends StatefulWidget { required this.onChange, required this.onEdit, this.maxWidth = 400, - this.initialFilename, + this.initialImageURL, }); @override @@ -54,6 +54,7 @@ class ActivityPlanCardState extends State { Uint8List? _avatar; String? _filename; + String? _imageURL; @override void initState() { @@ -64,7 +65,8 @@ class ActivityPlanCardState extends State { TextEditingController(text: _tempActivity.learningObjective); _instructionsController = TextEditingController(text: _tempActivity.instructions); - _filename = widget.initialFilename; + _filename = widget.initialImageURL?.split("/").last; + _imageURL = widget.activity.imageURL ?? widget.initialImageURL; } static const double itemPadding = 12; @@ -153,19 +155,39 @@ class ActivityPlanCardState extends State { _avatar = bytes; _filename = photo.name; }); + + final url = await Matrix.of(context).client.uploadContent( + bytes, + filename: photo.name, + ); + + final updatedActivity = ActivityPlanModel( + req: _tempActivity.req, + title: _tempActivity.title, + learningObjective: _tempActivity.learningObjective, + instructions: _tempActivity.instructions, + vocab: _tempActivity.vocab, + imageURL: url.toString(), + ); + + widget.onEdit(updatedActivity); + } + + Future _setAvatarByImageURL() async { + if (_avatar != null || _imageURL == null) return; + final resp = await http + .get(Uri.parse(_imageURL!)) + .timeout(const Duration(seconds: 5)); + if (mounted) { + setState(() => _avatar = resp.bodyBytes); + } } Future _onLaunch() async { + await _setAvatarByImageURL(); await showFutureLoadingDialog( context: context, future: () async { - if (_avatar == null && widget.activity.imageURL != null) { - final resp = await http - .get(Uri.parse(widget.activity.imageURL!)) - .timeout(const Duration(seconds: 5)); - _avatar = resp.bodyBytes; - } - String? avatarUrl; if (_avatar != null) { final client = Matrix.of(context).client; @@ -253,12 +275,12 @@ class ActivityPlanCardState extends State { ), clipBehavior: Clip.hardEdge, alignment: Alignment.center, - child: widget.activity.imageURL != null || _avatar != null + child: _imageURL != null || _avatar != null ? ClipRRect( child: _avatar == null ? CachedNetworkImage( fit: BoxFit.cover, - imageUrl: widget.activity.imageURL!, + imageUrl: _imageURL!, placeholder: (context, url) { return const Center( child: CircularProgressIndicator(), @@ -279,16 +301,18 @@ class ActivityPlanCardState extends State { padding: EdgeInsets.all(28.0), ), ), - Positioned( - top: 10.0, - right: 10.0, - child: IconButton( - icon: const Icon(Icons.upload_outlined), - onPressed: selectPhoto, - style: - IconButton.styleFrom(backgroundColor: Colors.black), + if (_isEditing) + Positioned( + top: 10.0, + right: 10.0, + child: IconButton( + icon: const Icon(Icons.upload_outlined), + onPressed: selectPhoto, + style: IconButton.styleFrom( + backgroundColor: Colors.black, + ), + ), ), - ), ], ), ), diff --git a/lib/pangea/activity_planner/activity_plan_model.dart b/lib/pangea/activity_planner/activity_plan_model.dart index e99bf4f95..bd37039fb 100644 --- a/lib/pangea/activity_planner/activity_plan_model.dart +++ b/lib/pangea/activity_planner/activity_plan_model.dart @@ -6,11 +6,11 @@ import 'package:fluffychat/pangea/common/constants/model_keys.dart'; class ActivityPlanModel { final String bookmarkId; final ActivityPlanRequest req; - String title; - String learningObjective; - String instructions; - List vocab; - String? imageURL; + final String title; + final String learningObjective; + final String instructions; + final List vocab; + final String? imageURL; ActivityPlanModel({ required this.req, @@ -19,7 +19,8 @@ class ActivityPlanModel { required this.instructions, required this.vocab, this.imageURL, - }) : bookmarkId = req.hashCode.toString(); + }) : bookmarkId = + "${title.hashCode ^ learningObjective.hashCode ^ instructions.hashCode ^ imageURL.hashCode ^ vocab.map((v) => v.hashCode).reduce((a, b) => a ^ b)}"; factory ActivityPlanModel.fromJson(Map json) { return ActivityPlanModel( diff --git a/lib/pangea/activity_planner/bookmarked_activity_list.dart b/lib/pangea/activity_planner/bookmarked_activity_list.dart index b27dca4e3..a94d5f279 100644 --- a/lib/pangea/activity_planner/bookmarked_activity_list.dart +++ b/lib/pangea/activity_planner/bookmarked_activity_list.dart @@ -39,6 +39,7 @@ class BookmarkedActivitiesListState extends State { double get cardWidth => _isColumnMode ? 225.0 : 150.0; Future _onEdit( + String activityId, ActivityPlanModel activity, Uint8List? avatar, String? filename, @@ -48,10 +49,20 @@ class BookmarkedActivitiesListState extends State { avatar, filename: filename, ); - activity.imageURL = url.toString(); + if (!mounted) return; + setState(() { + activity = ActivityPlanModel( + req: activity.req, + title: activity.title, + learningObjective: activity.learningObjective, + instructions: activity.instructions, + vocab: activity.vocab, + imageURL: url.toString(), + ); + }); } - await BookmarkedActivitiesRepo.remove(activity.bookmarkId); + await BookmarkedActivitiesRepo.remove(activityId); await BookmarkedActivitiesRepo.save(activity); if (mounted) setState(() {}); } @@ -87,7 +98,7 @@ class BookmarkedActivitiesListState extends State { context: context, builder: (context) { return ActivitySuggestionDialog( - activity: activity, + initialActivity: activity, buttonText: L10n.of(context).inviteAndLaunch, room: widget.room, onEdit: _onEdit, diff --git a/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart b/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart index 2250f811e..3a31c89ac 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart @@ -60,6 +60,22 @@ class ActivitySuggestionCarouselState _setActivityItems(); } + @override + void didUpdateWidget(covariant ActivitySuggestionCarousel oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedActivity != oldWidget.selectedActivity && + _currentIndex != null && + widget.selectedActivity != null) { + final prevIndex = _currentIndex!; + setState( + () { + _activityItems[prevIndex] = widget.selectedActivity!; + _currentActivity = widget.selectedActivity; + }, + ); + } + } + Future _setActivityItems() async { try { final ActivityPlanRequest request = ActivityPlanRequest( @@ -138,7 +154,7 @@ class ActivitySuggestionCarouselState context: context, builder: (context) { return ActivitySuggestionDialog( - activity: _currentActivity!, + initialActivity: _currentActivity!, buttonText: L10n.of(context).selectActivity, onLaunch: widget.onActivitySelected, ); diff --git a/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart b/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart index e26c9a455..a4121765b 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart @@ -26,7 +26,7 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; class ActivitySuggestionDialog extends StatefulWidget { - final ActivityPlanModel activity; + final ActivityPlanModel initialActivity; final String buttonText; final Room? room; @@ -37,13 +37,14 @@ class ActivitySuggestionDialog extends StatefulWidget { )? onLaunch; final Future Function( + String, ActivityPlanModel, Uint8List?, String?, )? onEdit; const ActivitySuggestionDialog({ - required this.activity, + required this.initialActivity, required this.buttonText, this.onLaunch, this.onEdit, @@ -59,6 +60,7 @@ class ActivitySuggestionDialog extends StatefulWidget { class ActivitySuggestionDialogState extends State { bool _isEditing = false; Uint8List? _avatar; + String? _imageURL; String? _filename; final TextEditingController _titleController = TextEditingController(); @@ -77,12 +79,14 @@ class ActivitySuggestionDialogState extends State { @override void initState() { super.initState(); - _titleController.text = widget.activity.title; - _learningObjectivesController.text = widget.activity.learningObjective; - _instructionsController.text = widget.activity.instructions; + _titleController.text = widget.initialActivity.title; + _learningObjectivesController.text = + widget.initialActivity.learningObjective; + _instructionsController.text = widget.initialActivity.instructions; _participantsController.text = - widget.activity.req.numberOfParticipants.toString(); - _vocab.addAll(widget.activity.vocab); + widget.initialActivity.req.numberOfParticipants.toString(); + _vocab.addAll(widget.initialActivity.vocab); + _imageURL = widget.initialActivity.imageURL; _setAvatarByURL(); } @@ -117,12 +121,12 @@ class ActivitySuggestionDialogState extends State { } Future _setAvatarByURL() async { - if (widget.activity.imageURL == null) return; + if (widget.initialActivity.imageURL == null) return; try { if (_avatar == null) { - if (widget.activity.imageURL!.startsWith("mxc")) { + if (widget.initialActivity.imageURL!.startsWith("mxc")) { final client = Matrix.of(context).client; - final mxcUri = Uri.parse(widget.activity.imageURL!); + final mxcUri = Uri.parse(widget.initialActivity.imageURL!); final data = await client.downloadMxcCached(mxcUri); _avatar = data; _filename = Uri.encodeComponent( @@ -130,10 +134,10 @@ class ActivitySuggestionDialogState extends State { ); } else { final Response response = - await http.get(Uri.parse(widget.activity.imageURL!)); + await http.get(Uri.parse(widget.initialActivity.imageURL!)); _avatar = response.bodyBytes; _filename = Uri.encodeComponent( - Uri.parse(widget.activity.imageURL!).pathSegments.last, + Uri.parse(widget.initialActivity.imageURL!).pathSegments.last, ); } } @@ -142,7 +146,7 @@ class ActivitySuggestionDialogState extends State { e: err, s: s, data: { - "imageURL": widget.activity.imageURL, + "imageURL": widget.initialActivity.imageURL, }, ); } @@ -153,17 +157,29 @@ class ActivitySuggestionDialogState extends State { _filename = null; _setAvatarByURL(); _vocab.clear(); - _vocab.addAll(widget.activity.vocab); + _vocab.addAll(widget.initialActivity.vocab); if (mounted) setState(() {}); } - Future _updateTextFields() async { - widget.activity.title = _titleController.text; - widget.activity.learningObjective = _learningObjectivesController.text; - widget.activity.instructions = _instructionsController.text; - widget.activity.req.numberOfParticipants = - int.tryParse(_participantsController.text) ?? 3; - widget.activity.vocab = _vocab; + ActivityPlanModel get _updatedActivity => ActivityPlanModel( + req: widget.initialActivity.req, + title: _titleController.text, + learningObjective: _learningObjectivesController.text, + instructions: _instructionsController.text, + vocab: _vocab, + imageURL: _imageURL, + ); + + Future _updateImageURL() async { + if (_avatar == null) return; + final url = await Matrix.of(context).client.uploadContent( + _avatar!, + filename: _filename, + ); + if (!mounted) return; + setState(() { + _imageURL = url.toString(); + }); } void _addVocab() { @@ -184,9 +200,11 @@ class ActivitySuggestionDialogState extends State { } Future _launchActivity() async { + await _updateImageURL(); + if (widget.room != null) { await widget.room!.sendActivityPlan( - widget.activity, + _updatedActivity, avatar: _avatar, filename: _filename, ); @@ -194,27 +212,18 @@ class ActivitySuggestionDialogState extends State { return; } - String? avatarUrl; - if (_avatar != null) { - final url = await Matrix.of(context).client.uploadContent( - _avatar!, - filename: _filename, - ); - avatarUrl = url.toString(); - } - final client = Matrix.of(context).client; final roomId = await client.createGroupChat( preset: CreateRoomPreset.publicChat, visibility: sdk.Visibility.private, - groupName: widget.activity.title, + groupName: _updatedActivity.title, initialState: [ - if (avatarUrl != null) + if (_updatedActivity.imageURL != null) StateEvent( type: EventTypes.RoomAvatar, stateKey: '', content: { - "url": avatarUrl, + "url": _updatedActivity.imageURL, }, ), StateEvent( @@ -234,7 +243,7 @@ class ActivitySuggestionDialogState extends State { } await room.sendActivityPlan( - widget.activity, + _updatedActivity, avatar: _avatar, filename: _filename, ); @@ -244,11 +253,12 @@ class ActivitySuggestionDialogState extends State { Future _saveEdits() async { if (!_formKey.currentState!.validate()) return; - await _updateTextFields(); + await _updateImageURL(); _setEditing(false); if (widget.onEdit != null) { await widget.onEdit!( - widget.activity, + widget.initialActivity.bookmarkId, + _updatedActivity, _avatar, _filename, ); @@ -289,17 +299,19 @@ class ActivitySuggestionDialogState extends State { borderRadius: BorderRadius.circular(24.0), child: _avatar != null ? Image.memory(_avatar!, fit: BoxFit.cover) - : widget.activity.imageURL != null - ? widget.activity.imageURL!.startsWith("mxc") + : _updatedActivity.imageURL != null + ? _updatedActivity.imageURL!.startsWith("mxc") ? MxcImage( - uri: Uri.parse(widget.activity.imageURL!), + uri: Uri.parse( + _updatedActivity.imageURL!, + ), width: width, height: 200, - cacheKey: widget.activity.bookmarkId, + cacheKey: _updatedActivity.bookmarkId, fit: BoxFit.cover, ) : CachedNetworkImage( - imageUrl: widget.activity.imageURL!, + imageUrl: _updatedActivity.imageURL!, fit: BoxFit.cover, placeholder: (context, url) => const Center( @@ -352,7 +364,7 @@ class ActivitySuggestionDialogState extends State { ActivitySuggestionCardRow( icon: Icons.event_note_outlined, child: Text( - widget.activity.title, + _updatedActivity.title, style: theme.textTheme.titleLarge ?.copyWith(fontWeight: FontWeight.bold), maxLines: 6, @@ -376,7 +388,7 @@ class ActivitySuggestionDialogState extends State { ActivitySuggestionCardRow( icon: Symbols.target, child: Text( - widget.activity.learningObjective, + _updatedActivity.learningObjective, maxLines: 6, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyLarge, @@ -398,7 +410,7 @@ class ActivitySuggestionDialogState extends State { ActivitySuggestionCardRow( icon: Symbols.steps, child: Text( - widget.activity.instructions, + _updatedActivity.instructions, maxLines: 8, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyLarge, @@ -436,7 +448,7 @@ class ActivitySuggestionDialogState extends State { icon: Icons.group_outlined, child: Text( L10n.of(context).countParticipants( - widget.activity.req.numberOfParticipants, + _updatedActivity.req.numberOfParticipants, ), style: theme.textTheme.bodyLarge, ), @@ -598,13 +610,16 @@ class ActivitySuggestionDialogState extends State { context: context, future: () async { if (widget.onLaunch != null) { - return widget.onLaunch?.call( - widget.activity, + await _updateImageURL(); + + widget.onLaunch!.call( + _updatedActivity, _avatar, _filename, ); + } else { + await _launchActivity(); } - return _launchActivity(); }, ); diff --git a/lib/pangea/activity_suggestions/activity_suggestions_area.dart b/lib/pangea/activity_suggestions/activity_suggestions_area.dart index bd99a7021..3e8e54799 100644 --- a/lib/pangea/activity_suggestions/activity_suggestions_area.dart +++ b/lib/pangea/activity_suggestions/activity_suggestions_area.dart @@ -136,7 +136,7 @@ class ActivitySuggestionsAreaState extends State { context: context, builder: (context) { return ActivitySuggestionDialog( - activity: activity, + initialActivity: activity, buttonText: L10n.of(context).inviteAndLaunch, room: widget.room, );