From a6f5dedcdea40d087abe616990d7cbf9e8139bff Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Tue, 16 Jan 2024 19:24:52 -0500 Subject: [PATCH] Navigate to new post after creation (#1044) --- lib/community/pages/create_post_page.dart | 22 +- lib/feed/view/feed_page.dart | 18 +- lib/feed/widgets/feed_fab.dart | 6 +- lib/l10n/app_en.arb | 4 + lib/post/cubit/create_post_cubit.dart | 10 +- lib/post/pages/post_page.dart | 764 +++++++++++----------- lib/post/pages/post_page_success.dart | 5 + lib/post/widgets/comment_view.dart | 5 + lib/post/widgets/post_view.dart | 11 +- lib/shared/cross_posts.dart | 28 +- lib/thunder/pages/thunder_page.dart | 44 +- lib/utils/navigate_create_post.dart | 2 + 12 files changed, 518 insertions(+), 401 deletions(-) diff --git a/lib/community/pages/create_post_page.dart b/lib/community/pages/create_post_page.dart index d7f0eaeca..75d87a072 100644 --- a/lib/community/pages/create_post_page.dart +++ b/lib/community/pages/create_post_page.dart @@ -35,6 +35,7 @@ import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/utils/debounce.dart'; import 'package:thunder/utils/image.dart'; import 'package:thunder/utils/instance.dart'; +import 'package:thunder/utils/navigate_post.dart'; class CreatePostPage extends StatefulWidget { final int? communityId; @@ -61,6 +62,9 @@ class CreatePostPage extends StatefulWidget { /// Callback function that is triggered whenever the post is successfully created or updated final Function(PostViewMedia postViewMedia)? onPostSuccess; + // The scaffold key for the main Thunder page + final GlobalKey? scaffoldMessengerKey; + const CreatePostPage({ super.key, required this.communityId, @@ -72,6 +76,7 @@ class CreatePostPage extends StatefulWidget { this.prePopulated = false, this.postView, this.onPostSuccess, + this.scaffoldMessengerKey, }); @override @@ -337,9 +342,10 @@ class _CreatePostPageState extends State { child: IconButton( onPressed: isSubmitButtonDisabled ? null - : () { + : () async { draftPost.saveAsDraft = false; - context.read().createOrEditPost( + + final int? postId = await context.read().createOrEditPost( communityId: communityId!, name: _titleTextController.text, body: _bodyTextController.text, @@ -348,6 +354,18 @@ class _CreatePostPageState extends State { postIdBeingEdited: widget.postView?.post.id, languageId: languageId, ); + + if (context.mounted && widget.scaffoldMessengerKey?.currentContext != null && widget.postView?.post.id == null && postId != null) { + showSnackbar( + context, + l10n.postCreatedSuccessfully, + trailingIcon: Icons.remove_red_eye_rounded, + trailingAction: () { + navigateToPost(widget.scaffoldMessengerKey!.currentContext!, postId: postId); + }, + customState: widget.scaffoldMessengerKey?.currentState, + ); + } }, icon: Icon( widget.postView != null ? Icons.edit_rounded : Icons.send_rounded, diff --git a/lib/feed/view/feed_page.dart b/lib/feed/view/feed_page.dart index 223212d6b..a06964223 100644 --- a/lib/feed/view/feed_page.dart +++ b/lib/feed/view/feed_page.dart @@ -52,6 +52,7 @@ class FeedPage extends StatefulWidget { this.userId, this.username, this.scaffoldStateKey, + this.scaffoldMessengerKey, }); /// The type of feed to display. @@ -84,6 +85,9 @@ class FeedPage extends StatefulWidget { /// The scaffold key which holds the drawer final GlobalKey? scaffoldStateKey; + /// The messenger key back to the main Thunder page + final GlobalKey? scaffoldMessengerKey; + @override State createState() => _FeedPageState(); } @@ -127,7 +131,7 @@ class _FeedPageState extends State with AutomaticKeepAliveClientMixin< return BlocProvider.value( value: bloc, - child: FeedView(scaffoldStateKey: widget.scaffoldStateKey), + child: FeedView(scaffoldStateKey: widget.scaffoldStateKey, scaffoldMessengerKey: widget.scaffoldMessengerKey), ); } @@ -143,17 +147,20 @@ class _FeedPageState extends State with AutomaticKeepAliveClientMixin< username: widget.username, reset: true, )), - child: FeedView(scaffoldStateKey: widget.scaffoldStateKey), + child: FeedView(scaffoldStateKey: widget.scaffoldStateKey, scaffoldMessengerKey: widget.scaffoldMessengerKey), ); } } class FeedView extends StatefulWidget { - const FeedView({super.key, this.scaffoldStateKey}); + const FeedView({super.key, this.scaffoldStateKey, this.scaffoldMessengerKey}); /// The scaffold key which holds the drawer final GlobalKey? scaffoldStateKey; + /// The messenger key back to the main Thunder page + final GlobalKey? scaffoldMessengerKey; + @override State createState() => _FeedViewState(); } @@ -426,7 +433,10 @@ class _FeedViewState extends State { curve: Curves.easeIn, child: Container( margin: const EdgeInsets.all(16), - child: FeedFAB(heroTag: state.communityName), + child: FeedFAB( + heroTag: state.communityName, + scaffoldMessengerKey: widget.scaffoldMessengerKey, + ), ), ), ], diff --git a/lib/feed/widgets/feed_fab.dart b/lib/feed/widgets/feed_fab.dart index c710bee5b..e24d81ffd 100644 --- a/lib/feed/widgets/feed_fab.dart +++ b/lib/feed/widgets/feed_fab.dart @@ -20,10 +20,13 @@ import 'package:thunder/shared/sort_picker.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; class FeedFAB extends StatelessWidget { - const FeedFAB({super.key, this.heroTag}); + const FeedFAB({super.key, this.heroTag, this.scaffoldMessengerKey}); final String? heroTag; + /// The messenger key back to the main Thunder page + final GlobalKey? scaffoldMessengerKey; + @override build(BuildContext context) { final ThunderState state = context.watch().state; @@ -287,6 +290,7 @@ class FeedFAB extends StatelessWidget { child: CreatePostPage( communityId: feedBloc.state.communityId, communityView: feedBloc.state.fullCommunityView?.communityView, + scaffoldMessengerKey: scaffoldMessengerKey, ), ); }, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 571b9f810..8cc295edb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -779,6 +779,10 @@ "@postContentFontScale": { "description": "Setting for post content font scale" }, + "postCreatedSuccessfully" :"Post created successfully!", + "@postCreatedSuccessfully": { + "description": "Notifying the user that their post was created successfully" + }, "postLocked": "Post locked. No replies allowed.", "@postLocked": {}, "postNSFW": "Mark as NSFW", diff --git a/lib/post/cubit/create_post_cubit.dart b/lib/post/cubit/create_post_cubit.dart index 67b04f521..3f407f267 100644 --- a/lib/post/cubit/create_post_cubit.dart +++ b/lib/post/cubit/create_post_cubit.dart @@ -39,8 +39,9 @@ class CreatePostCubit extends Cubit { } } - /// Creates or edits a post. When successful, it returns the newly created/updated post in the form of a [PostViewMedia] - Future createOrEditPost({required int communityId, required String name, String? body, String? url, bool? nsfw, int? postIdBeingEdited, int? languageId}) async { + /// Creates or edits a post. When successful, it emits the newly created/updated post in the form of a [PostViewMedia] + /// and returns the newly created post id. + Future createOrEditPost({required int communityId, required String name, String? body, String? url, bool? nsfw, int? postIdBeingEdited, int? languageId}) async { emit(state.copyWith(status: CreatePostStatus.submitting)); try { @@ -58,8 +59,11 @@ class CreatePostCubit extends Cubit { List postViewMedias = await parsePostViews([postView]); emit(state.copyWith(status: CreatePostStatus.success, postViewMedia: postViewMedias.firstOrNull)); + return postViewMedias.firstOrNull?.postView.post.id; } catch (e) { - return emit(state.copyWith(status: CreatePostStatus.error, message: getExceptionErrorMessage(e))); + emit(state.copyWith(status: CreatePostStatus.error, message: getExceptionErrorMessage(e))); } + + return null; } } diff --git a/lib/post/pages/post_page.dart b/lib/post/pages/post_page.dart index 0e89e8ca8..16fee684e 100644 --- a/lib/post/pages/post_page.dart +++ b/lib/post/pages/post_page.dart @@ -70,6 +70,9 @@ class _PostPageState extends State { IconData? sortTypeIcon; String? sortTypeLabel; + /// The messenger key for this post page + final GlobalKey _scaffoldMessengerKey = GlobalKey(); + @override void initState() { super.initState(); @@ -120,420 +123,425 @@ class _PostPageState extends State { _previousIsFabSummoned = isFabSummoned; } - return BlocProvider( - create: (context) => PostBloc(), - child: BlocConsumer( - listenWhen: (previousState, currentState) { - if (previousState.sortType != currentState.sortType) { - setState(() { - sortType = currentState.sortType; - final sortTypeItem = CommentSortPicker.getCommentSortTypeItems(includeVersionSpecificFeature: IncludeVersionSpecificFeature.always) - .firstWhere((sortTypeItem) => sortTypeItem.payload == currentState.sortType); - sortTypeIcon = sortTypeItem.icon; - sortTypeLabel = sortTypeItem.label; - }); - } - return true; - }, - listener: (context, state) {}, - builder: (context, state) { - return Scaffold( - appBar: AppBar( - title: ListTile( - title: Text( - sortTypeLabel?.isNotEmpty == true ? l10n.comments : '', - style: theme.textTheme.titleLarge, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Row( - children: [ - Icon(sortTypeIcon, size: 13), - const SizedBox(width: 4), - Text(sortTypeLabel ?? ''), - ], - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 0), - ), - flexibleSpace: Semantics( - excludeSemantics: true, - child: GestureDetector( - onTap: () { - if (context.read().state.isFabOpen) { - context.read().add(const OnFabToggle(false)); - } - }, - ), - ), - leading: IconButton( - icon: Icon( - Icons.arrow_back_rounded, - semanticLabel: l10n.back, + return ScaffoldMessenger( + key: _scaffoldMessengerKey, + child: BlocProvider( + create: (context) => PostBloc(), + child: BlocConsumer( + listenWhen: (previousState, currentState) { + if (previousState.sortType != currentState.sortType) { + setState(() { + sortType = currentState.sortType; + final sortTypeItem = CommentSortPicker.getCommentSortTypeItems(includeVersionSpecificFeature: IncludeVersionSpecificFeature.always) + .firstWhere((sortTypeItem) => sortTypeItem.payload == currentState.sortType); + sortTypeIcon = sortTypeItem.icon; + sortTypeLabel = sortTypeItem.label; + }); + } + return true; + }, + listener: (context, state) {}, + builder: (context, state) { + return Scaffold( + appBar: AppBar( + title: ListTile( + title: Text( + sortTypeLabel?.isNotEmpty == true ? l10n.comments : '', + style: theme.textTheme.titleLarge, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Row( + children: [ + Icon(sortTypeIcon, size: 13), + const SizedBox(width: 4), + Text(sortTypeLabel ?? ''), + ], + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 0), ), - onPressed: () { - if (context.read().state.isFabOpen) { - context.read().add(const OnFabToggle(false)); - } - Navigator.pop(context); - }, - ), - actions: [ - IconButton( - icon: Icon(Icons.refresh_rounded, semanticLabel: l10n.refresh), - onPressed: () { + flexibleSpace: Semantics( + excludeSemantics: true, + child: GestureDetector( + onTap: () { if (context.read().state.isFabOpen) { context.read().add(const OnFabToggle(false)); } - HapticFeedback.mediumImpact(); - return context - .read() - .add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentId: state.selectedCommentId, selectedCommentPath: state.selectedCommentPath)); - }), - IconButton( + }, + ), + ), + leading: IconButton( icon: Icon( - Icons.sort, - semanticLabel: l10n.sortBy, + Icons.arrow_back_rounded, + semanticLabel: l10n.back, ), - tooltip: l10n.sortBy, onPressed: () { if (context.read().state.isFabOpen) { context.read().add(const OnFabToggle(false)); } - showSortBottomSheet(context, state); + Navigator.pop(context); }, ), - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - onTap: () => createCrossPost( - context, - title: widget.postView?.postView.post.name ?? '', - url: widget.postView?.postView.post.url, - text: widget.postView?.postView.post.body, - postUrl: widget.postView?.postView.post.apId, - ), - child: ListTile( - dense: true, - horizontalTitleGap: 5, - leading: const Icon(Icons.repeat_rounded, size: 20), - title: Text(l10n.createNewCrossPost), - ), + actions: [ + IconButton( + icon: Icon(Icons.refresh_rounded, semanticLabel: l10n.refresh), + onPressed: () { + if (context.read().state.isFabOpen) { + context.read().add(const OnFabToggle(false)); + } + HapticFeedback.mediumImpact(); + return context + .read() + .add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentId: state.selectedCommentId, selectedCommentPath: state.selectedCommentPath)); + }), + IconButton( + icon: Icon( + Icons.sort, + semanticLabel: l10n.sortBy, ), - ], - ), - ], - centerTitle: false, - toolbarHeight: 70.0, - ), - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: Stack( - alignment: Alignment.center, - children: [ - if (enableCommentNavigation) - Positioned.fill( - child: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.bottomCenter, - child: CommentNavigatorFab( - itemPositionsListener: _itemPositionsListener, + tooltip: l10n.sortBy, + onPressed: () { + if (context.read().state.isFabOpen) { + context.read().add(const OnFabToggle(false)); + } + showSortBottomSheet(context, state); + }, + ), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + onTap: () => createCrossPost( + context, + title: widget.postView?.postView.post.name ?? '', + url: widget.postView?.postView.post.url, + text: widget.postView?.postView.post.body, + postUrl: widget.postView?.postView.post.apId, + scaffoldMessengerKey: _scaffoldMessengerKey, + ), + child: ListTile( + dense: true, + horizontalTitleGap: 5, + leading: const Icon(Icons.repeat_rounded, size: 20), + title: Text(l10n.createNewCrossPost), ), ), - ), + ], ), - if (enableFab) - Padding( - padding: EdgeInsets.only( - right: combineNavAndFab ? 0 : 16, - bottom: combineNavAndFab ? 5 : 0, + ], + centerTitle: false, + toolbarHeight: 70.0, + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + floatingActionButton: Stack( + alignment: Alignment.center, + children: [ + if (enableCommentNavigation) + Positioned.fill( + child: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Align( + alignment: Alignment.bottomCenter, + child: CommentNavigatorFab( + itemPositionsListener: _itemPositionsListener, + ), + ), + ), ), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - child: isFabSummoned - ? GestureFab( - centered: combineNavAndFab, - distance: combineNavAndFab ? 45 : 60, - icon: Icon( - state.status == PostStatus.searchInProgress ? Icons.youtube_searched_for_rounded : singlePressAction.getIcon(postLocked: postLocked), - semanticLabel: state.status == PostStatus.searchInProgress ? l10n.search : singlePressAction.getTitle(context, postLocked: postLocked), - size: 35, - ), - onPressed: state.status == PostStatus.searchInProgress - ? () { - context.read().add(const ContinueCommentSearchEvent()); - } - : () => singlePressAction.execute( - context: context, - postView: state.postView, - postId: state.postId, - selectedCommentId: state.selectedCommentId, - selectedCommentPath: state.selectedCommentPath, - override: singlePressAction == PostFabAction.backToTop - ? () => { - _itemScrollController.scrollTo( - index: 0, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ) - } - : singlePressAction == PostFabAction.changeSort - ? () => showSortBottomSheet(context, state) - : singlePressAction == PostFabAction.replyToPost - ? () => replyToPost(context, postLocked: postLocked) - : null), - onLongPress: () => longPressAction.execute( - context: context, - postView: state.postView, - postId: state.postId, - selectedCommentId: state.selectedCommentId, - selectedCommentPath: state.selectedCommentPath, - override: longPressAction == PostFabAction.backToTop - ? () => { - _itemScrollController.scrollTo( - index: 0, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ) - } - : longPressAction == PostFabAction.changeSort - ? () => showSortBottomSheet(context, state) - : longPressAction == PostFabAction.replyToPost - ? () => replyToPost(context, postLocked: postLocked) - : null), - children: [ - if (enableRefresh) - ActionButton( - centered: combineNavAndFab, - onPressed: () { - HapticFeedback.mediumImpact(); - PostFabAction.refresh.execute( + if (enableFab) + Padding( + padding: EdgeInsets.only( + right: combineNavAndFab ? 0 : 16, + bottom: combineNavAndFab ? 5 : 0, + ), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: isFabSummoned + ? GestureFab( + centered: combineNavAndFab, + distance: combineNavAndFab ? 45 : 60, + icon: Icon( + state.status == PostStatus.searchInProgress ? Icons.youtube_searched_for_rounded : singlePressAction.getIcon(postLocked: postLocked), + semanticLabel: state.status == PostStatus.searchInProgress ? l10n.search : singlePressAction.getTitle(context, postLocked: postLocked), + size: 35, + ), + onPressed: state.status == PostStatus.searchInProgress + ? () { + context.read().add(const ContinueCommentSearchEvent()); + } + : () => singlePressAction.execute( context: context, postView: state.postView, postId: state.postId, selectedCommentId: state.selectedCommentId, selectedCommentPath: state.selectedCommentPath, - ); - }, - title: PostFabAction.refresh.getTitle(context), - icon: Icon( - PostFabAction.refresh.getIcon(), + override: singlePressAction == PostFabAction.backToTop + ? () => { + _itemScrollController.scrollTo( + index: 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ) + } + : singlePressAction == PostFabAction.changeSort + ? () => showSortBottomSheet(context, state) + : singlePressAction == PostFabAction.replyToPost + ? () => replyToPost(context, postLocked: postLocked) + : null), + onLongPress: () => longPressAction.execute( + context: context, + postView: state.postView, + postId: state.postId, + selectedCommentId: state.selectedCommentId, + selectedCommentPath: state.selectedCommentPath, + override: longPressAction == PostFabAction.backToTop + ? () => { + _itemScrollController.scrollTo( + index: 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ) + } + : longPressAction == PostFabAction.changeSort + ? () => showSortBottomSheet(context, state) + : longPressAction == PostFabAction.replyToPost + ? () => replyToPost(context, postLocked: postLocked) + : null), + children: [ + if (enableRefresh) + ActionButton( + centered: combineNavAndFab, + onPressed: () { + HapticFeedback.mediumImpact(); + PostFabAction.refresh.execute( + context: context, + postView: state.postView, + postId: state.postId, + selectedCommentId: state.selectedCommentId, + selectedCommentPath: state.selectedCommentPath, + ); + }, + title: PostFabAction.refresh.getTitle(context), + icon: Icon( + PostFabAction.refresh.getIcon(), + ), ), - ), - if (enableReplyToPost) - ActionButton( - centered: combineNavAndFab, - onPressed: () { - HapticFeedback.mediumImpact(); - PostFabAction.replyToPost.execute( - override: () => replyToPost(context, postLocked: postLocked), - ); - }, - title: PostFabAction.replyToPost.getTitle(context), - icon: Icon( - postLocked ? Icons.lock : PostFabAction.replyToPost.getIcon(), + if (enableReplyToPost) + ActionButton( + centered: combineNavAndFab, + onPressed: () { + HapticFeedback.mediumImpact(); + PostFabAction.replyToPost.execute( + override: () => replyToPost(context, postLocked: postLocked), + ); + }, + title: PostFabAction.replyToPost.getTitle(context), + icon: Icon( + postLocked ? Icons.lock : PostFabAction.replyToPost.getIcon(), + ), ), - ), - if (enableChangeSort) - ActionButton( - centered: combineNavAndFab, - onPressed: () { - HapticFeedback.mediumImpact(); - PostFabAction.changeSort.execute( - override: () => showSortBottomSheet(context, state), - ); - }, - title: PostFabAction.changeSort.getTitle(context), - icon: Icon( - PostFabAction.changeSort.getIcon(), + if (enableChangeSort) + ActionButton( + centered: combineNavAndFab, + onPressed: () { + HapticFeedback.mediumImpact(); + PostFabAction.changeSort.execute( + override: () => showSortBottomSheet(context, state), + ); + }, + title: PostFabAction.changeSort.getTitle(context), + icon: Icon( + PostFabAction.changeSort.getIcon(), + ), ), - ), - if (enableBackToTop) - ActionButton( - centered: combineNavAndFab, - onPressed: () { - PostFabAction.backToTop.execute( - override: () => { - _itemScrollController.scrollTo( - index: 0, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ) - }); - }, - title: PostFabAction.backToTop.getTitle(context), - icon: Icon( - PostFabAction.backToTop.getIcon(), + if (enableBackToTop) + ActionButton( + centered: combineNavAndFab, + onPressed: () { + PostFabAction.backToTop.execute( + override: () => { + _itemScrollController.scrollTo( + index: 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ) + }); + }, + title: PostFabAction.backToTop.getTitle(context), + icon: Icon( + PostFabAction.backToTop.getIcon(), + ), ), - ), - if (enableSearch) - ActionButton( - centered: combineNavAndFab, - onPressed: () { - PostFabAction.search.execute(override: () { - if (state.status == PostStatus.searchInProgress) { - context.read().add(const EndCommentSearchEvent()); - } else { - showInputDialog( - context: context, - title: l10n.searchComments, - inputLabel: l10n.searchTerm, - onSubmitted: ({payload, value}) { - Navigator.of(context).pop(); - - List commentMatches = []; - - /// Recursive function which checks if any child of the given [commentViewTrees] contains the query - void findMatches(List commentViewTrees) { - for (CommentViewTree commentViewTree in commentViewTrees) { - if (commentViewTree.commentView?.comment.content.contains(RegExp(value!, caseSensitive: false)) == true) { - commentMatches.add(commentViewTree.commentView!.comment); + if (enableSearch) + ActionButton( + centered: combineNavAndFab, + onPressed: () { + PostFabAction.search.execute(override: () { + if (state.status == PostStatus.searchInProgress) { + context.read().add(const EndCommentSearchEvent()); + } else { + showInputDialog( + context: context, + title: l10n.searchComments, + inputLabel: l10n.searchTerm, + onSubmitted: ({payload, value}) { + Navigator.of(context).pop(); + + List commentMatches = []; + + /// Recursive function which checks if any child of the given [commentViewTrees] contains the query + void findMatches(List commentViewTrees) { + for (CommentViewTree commentViewTree in commentViewTrees) { + if (commentViewTree.commentView?.comment.content.contains(RegExp(value!, caseSensitive: false)) == true) { + commentMatches.add(commentViewTree.commentView!.comment); + } + findMatches(commentViewTree.replies); } - findMatches(commentViewTree.replies); } - } - - // Find all comments which contain the query - findMatches(state.comments); - - if (commentMatches.isEmpty) { - showSnackbar(context, l10n.noResultsFound); - } else { - context.read().add(StartCommentSearchEvent(commentMatches: commentMatches)); - } - - return Future.value(null); - }, - getSuggestions: (_) => Future.value(const Iterable.empty()), - suggestionBuilder: (payload) => Container(), - ); - } - }); - }, - title: state.status == PostStatus.searchInProgress ? l10n.endSearch : PostFabAction.search.getTitle(context), - icon: Icon( - state.status == PostStatus.searchInProgress ? Icons.search_off_rounded : PostFabAction.search.getIcon(), + + // Find all comments which contain the query + findMatches(state.comments); + + if (commentMatches.isEmpty) { + showSnackbar(context, l10n.noResultsFound); + } else { + context.read().add(StartCommentSearchEvent(commentMatches: commentMatches)); + } + + return Future.value(null); + }, + getSuggestions: (_) => Future.value(const Iterable.empty()), + suggestionBuilder: (payload) => Container(), + ); + } + }); + }, + title: state.status == PostStatus.searchInProgress ? l10n.endSearch : PostFabAction.search.getTitle(context), + icon: Icon( + state.status == PostStatus.searchInProgress ? Icons.search_off_rounded : PostFabAction.search.getIcon(), + ), ), - ), - ], - ) - : null, + ], + ) + : null, + ), ), - ), - ], - ), - body: Stack( - alignment: Alignment.bottomRight, - children: [ - SafeArea( - child: BlocConsumer( - listenWhen: (previous, current) { - if ((previous.status != PostStatus.failure && current.status == PostStatus.failure) || (previous.errorMessage != current.errorMessage)) { - setState(() => resetFailureMessage = true); - } - return true; - }, - listener: (context, state) { - if (state.status == PostStatus.success && widget.postView != null && state.postView != null) { - widget.onPostUpdated(state.postView!); - } - }, - builder: (context, state) { - if (state.status == PostStatus.failure && resetFailureMessage == true) { - showSnackbar( - context, - state.errorMessage ?? l10n.missingErrorMessage, - backgroundColor: Theme.of(context).colorScheme.onErrorContainer, - leadingIcon: Icons.warning_rounded, - leadingIconColor: Theme.of(context).colorScheme.errorContainer, - ); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - setState(() => resetFailureMessage = false); - }); - } - switch (state.status) { - case PostStatus.initial: - context - .read() - .add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentPath: widget.selectedCommentPath, selectedCommentId: widget.selectedCommentId)); - return const Center(child: CircularProgressIndicator()); - case PostStatus.loading: - return const Center(child: CircularProgressIndicator()); - case PostStatus.refreshing: - case PostStatus.success: - case PostStatus.failure: - case PostStatus.searchInProgress: - if (state.postView != null) { - return RefreshIndicator( - onRefresh: () async { - HapticFeedback.mediumImpact(); - return context - .read() - .add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentId: state.selectedCommentId, selectedCommentPath: state.selectedCommentPath)); + ], + ), + body: Stack( + alignment: Alignment.bottomRight, + children: [ + SafeArea( + child: BlocConsumer( + listenWhen: (previous, current) { + if ((previous.status != PostStatus.failure && current.status == PostStatus.failure) || (previous.errorMessage != current.errorMessage)) { + setState(() => resetFailureMessage = true); + } + return true; + }, + listener: (context, state) { + if (state.status == PostStatus.success && widget.postView != null && state.postView != null) { + widget.onPostUpdated(state.postView!); + } + }, + builder: (context, state) { + if (state.status == PostStatus.failure && resetFailureMessage == true) { + showSnackbar( + context, + state.errorMessage ?? l10n.missingErrorMessage, + backgroundColor: Theme.of(context).colorScheme.onErrorContainer, + leadingIcon: Icons.warning_rounded, + leadingIconColor: Theme.of(context).colorScheme.errorContainer, + ); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + setState(() => resetFailureMessage = false); + }); + } + switch (state.status) { + case PostStatus.initial: + context + .read() + .add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentPath: widget.selectedCommentPath, selectedCommentId: widget.selectedCommentId)); + return const Center(child: CircularProgressIndicator()); + case PostStatus.loading: + return const Center(child: CircularProgressIndicator()); + case PostStatus.refreshing: + case PostStatus.success: + case PostStatus.failure: + case PostStatus.searchInProgress: + if (state.postView != null) { + return RefreshIndicator( + onRefresh: () async { + HapticFeedback.mediumImpact(); + return context + .read() + .add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentId: state.selectedCommentId, selectedCommentPath: state.selectedCommentPath)); + }, + child: PostPageSuccess( + postView: state.postView!, + comments: state.comments, + selectedCommentId: state.selectedCommentId, + selectedCommentPath: state.selectedCommentPath, + newlyCreatedCommentId: state.newlyCreatedCommentId, + moddingCommentId: state.moddingCommentId, + viewFullCommentsRefreshing: state.viewAllCommentsRefresh, + itemScrollController: _itemScrollController, + itemPositionsListener: _itemPositionsListener, + hasReachedCommentEnd: state.hasReachedCommentEnd, + moderators: state.moderators, + crossPosts: state.crossPosts, + scaffoldMessengerKey: _scaffoldMessengerKey, + ), + ); + } + return ErrorMessage( + message: state.errorMessage, + action: () { + context.read().add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentId: null)); }, - child: PostPageSuccess( - postView: state.postView!, - comments: state.comments, - selectedCommentId: state.selectedCommentId, - selectedCommentPath: state.selectedCommentPath, - newlyCreatedCommentId: state.newlyCreatedCommentId, - moddingCommentId: state.moddingCommentId, - viewFullCommentsRefreshing: state.viewAllCommentsRefresh, - itemScrollController: _itemScrollController, - itemPositionsListener: _itemPositionsListener, - hasReachedCommentEnd: state.hasReachedCommentEnd, - moderators: state.moderators, - crossPosts: state.crossPosts, - ), + actionText: l10n.refreshContent, + ); + case PostStatus.empty: + return ErrorMessage( + message: state.errorMessage, + action: () { + context.read().add(GetPostEvent(postView: widget.postView, postId: widget.postId)); + }, + actionText: l10n.refreshContent, ); - } - return ErrorMessage( - message: state.errorMessage, - action: () { - context.read().add(GetPostEvent(postView: widget.postView, postId: widget.postId, selectedCommentId: null)); - }, - actionText: l10n.refreshContent, - ); - case PostStatus.empty: - return ErrorMessage( - message: state.errorMessage, - action: () { - context.read().add(GetPostEvent(postView: widget.postView, postId: widget.postId)); - }, - actionText: l10n.refreshContent, - ); - } - }, - ), - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: isFabOpen - ? Listener( - onPointerUp: (details) { - context.read().add(const OnFabToggle(false)); - }, - child: Container( - color: theme.colorScheme.background.withOpacity(0.95), - ), - ) - : null, - ), - if (enableFab) - SizedBox( - height: 70, - width: 70, - child: GestureDetector( - onVerticalDragUpdate: (details) { - if (details.delta.dy < -5) { - context.read().add(const OnFabSummonToggle(true)); } }, ), ), - ], - ), - ); - }, + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: isFabOpen + ? Listener( + onPointerUp: (details) { + context.read().add(const OnFabToggle(false)); + }, + child: Container( + color: theme.colorScheme.background.withOpacity(0.95), + ), + ) + : null, + ), + if (enableFab) + SizedBox( + height: 70, + width: 70, + child: GestureDetector( + onVerticalDragUpdate: (details) { + if (details.delta.dy < -5) { + context.read().add(const OnFabSummonToggle(true)); + } + }, + ), + ), + ], + ), + ); + }, + ), ), ); } diff --git a/lib/post/pages/post_page_success.dart b/lib/post/pages/post_page_success.dart index adf036900..cc1de0fd0 100644 --- a/lib/post/pages/post_page_success.dart +++ b/lib/post/pages/post_page_success.dart @@ -40,6 +40,9 @@ class PostPageSuccess extends StatefulWidget { final List? moderators; final List? crossPosts; + /// The messenger key back to the post page + final GlobalKey? scaffoldMessengerKey; + const PostPageSuccess({ super.key, required this.postView, @@ -54,6 +57,7 @@ class PostPageSuccess extends StatefulWidget { this.viewFullCommentsRefreshing = false, required this.moderators, required this.crossPosts, + this.scaffoldMessengerKey, }); @override @@ -163,6 +167,7 @@ class _PostPageSuccessState extends State { }, moderators: widget.moderators, crossPosts: widget.crossPosts, + scaffoldMessengerKey: widget.scaffoldMessengerKey, ), ), ], diff --git a/lib/post/widgets/comment_view.dart b/lib/post/widgets/comment_view.dart index 9c0206882..31c5b199e 100644 --- a/lib/post/widgets/comment_view.dart +++ b/lib/post/widgets/comment_view.dart @@ -38,6 +38,9 @@ class CommentSubview extends StatefulWidget { final List? moderators; final List? crossPosts; + /// The messenger key back to the post page + final GlobalKey? scaffoldMessengerKey; + const CommentSubview({ super.key, required this.comments, @@ -59,6 +62,7 @@ class CommentSubview extends StatefulWidget { required this.now, required this.moderators, required this.crossPosts, + this.scaffoldMessengerKey, }); @override @@ -135,6 +139,7 @@ class _CommentSubviewState extends State with SingleTickerProvid postViewMedia: widget.postViewMedia!, moderators: widget.moderators, crossPosts: widget.crossPosts, + scaffoldMessengerKey: widget.scaffoldMessengerKey, ); } if (widget.hasReachedCommentEnd == false && widget.comments.isEmpty) { diff --git a/lib/post/widgets/post_view.dart b/lib/post/widgets/post_view.dart index 030944c2a..28ec507d6 100644 --- a/lib/post/widgets/post_view.dart +++ b/lib/post/widgets/post_view.dart @@ -54,6 +54,9 @@ class PostSubview extends StatefulWidget { final List? moderators; final List? crossPosts; + /// The messenger key back to the post page + final GlobalKey? scaffoldMessengerKey; + const PostSubview({ super.key, this.selectedCommentId, @@ -61,6 +64,7 @@ class PostSubview extends StatefulWidget { required this.postViewMedia, required this.moderators, required this.crossPosts, + this.scaffoldMessengerKey, }); @override @@ -170,7 +174,12 @@ class _PostSubviewState extends State with SingleTickerProviderStat ), ), ), - if (showCrossPosts && sortedCrossPosts.isNotEmpty) CrossPosts(crossPosts: sortedCrossPosts, originalPost: widget.postViewMedia), + if (showCrossPosts && sortedCrossPosts.isNotEmpty) + CrossPosts( + crossPosts: sortedCrossPosts, + originalPost: widget.postViewMedia, + scaffoldMessengerKey: widget.scaffoldMessengerKey, + ), const SizedBox(height: 16.0), SizedBox( width: MediaQuery.of(context).size.width, diff --git a/lib/shared/cross_posts.dart b/lib/shared/cross_posts.dart index 3b53abb74..bf221ef05 100644 --- a/lib/shared/cross_posts.dart +++ b/lib/shared/cross_posts.dart @@ -14,8 +14,15 @@ class CrossPosts extends StatefulWidget { final List crossPosts; final PostViewMedia? originalPost; final bool? isNewPost; + final GlobalKey? scaffoldMessengerKey; - const CrossPosts({super.key, required this.crossPosts, this.originalPost, this.isNewPost}) : assert(originalPost != null || isNewPost == true); + const CrossPosts({ + super.key, + required this.crossPosts, + this.originalPost, + this.isNewPost, + this.scaffoldMessengerKey, + }) : assert(originalPost != null || isNewPost == true); @override State createState() => _CrossPostsState(); @@ -165,7 +172,12 @@ class _CrossPostsState extends State with SingleTickerProviderStateM ), widget.isNewPost != true ? InkWell( - onTap: () => createCrossPost(context, title: widget.originalPost!.postView.post.name, url: widget.originalPost!.postView.post.url), + onTap: () => createCrossPost( + context, + title: widget.originalPost!.postView.post.name, + url: widget.originalPost!.postView.post.url, + scaffoldMessengerKey: widget.scaffoldMessengerKey, + ), borderRadius: BorderRadius.circular(10), child: Padding( padding: const EdgeInsets.all(5), @@ -192,9 +204,14 @@ class _CrossPostsState extends State with SingleTickerProviderStateM } } -void createCrossPost(BuildContext context, {required String title, String? url, String? text, String? postUrl}) async { - assert((text == null) == (postUrl == null)); - +void createCrossPost( + BuildContext context, { + required String title, + String? url, + String? text, + String? postUrl, + GlobalKey? scaffoldMessengerKey, +}) async { final AppLocalizations l10n = AppLocalizations.of(context)!; if (url?.isNotEmpty == true) { @@ -210,5 +227,6 @@ void createCrossPost(BuildContext context, {required String title, String? url, url: url, text: text, prePopulated: true, + scaffoldMessengerKey: scaffoldMessengerKey, ); } diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index d60e232b7..c9d744612 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -126,14 +126,24 @@ class _ThunderState extends State { // For sharing images from outside the app while the app is closed final initialMedia = await ReceiveSharingIntent.getInitialMedia(); if (initialMedia.isNotEmpty && context.mounted && currentIntent != ANDROID_INTENT_ACTION_VIEW) { - navigateToCreatePostPage(context, image: File(initialMedia.first.path), prePopulated: true); + navigateToCreatePostPage( + context, + image: File(initialMedia.first.path), + prePopulated: true, + scaffoldMessengerKey: scaffoldMessengerKey, + ); } // For sharing images while the app is in the memory mediaIntentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen(( List value, ) { if (context.mounted && currentIntent != ANDROID_INTENT_ACTION_VIEW) { - navigateToCreatePostPage(context, image: File(value.first.path), prePopulated: true); + navigateToCreatePostPage( + context, + image: File(value.first.path), + prePopulated: true, + scaffoldMessengerKey: scaffoldMessengerKey, + ); } }); } @@ -144,9 +154,19 @@ class _ThunderState extends State { if ((initialText?.isNotEmpty ?? false) && context.mounted && currentIntent != ANDROID_INTENT_ACTION_VIEW) { final uri = Uri.tryParse(initialText!); if (uri?.isAbsolute == true) { - navigateToCreatePostPage(context, url: uri.toString(), prePopulated: true); + navigateToCreatePostPage( + context, + url: uri.toString(), + prePopulated: true, + scaffoldMessengerKey: scaffoldMessengerKey, + ); } else { - navigateToCreatePostPage(context, text: initialText, prePopulated: true); + navigateToCreatePostPage( + context, + text: initialText, + prePopulated: true, + scaffoldMessengerKey: scaffoldMessengerKey, + ); } } @@ -157,9 +177,19 @@ class _ThunderState extends State { if ((value?.isNotEmpty ?? false) && context.mounted && currentIntent != ANDROID_INTENT_ACTION_VIEW) { final uri = Uri.tryParse(value!); if (uri?.isAbsolute == true) { - navigateToCreatePostPage(context, url: uri.toString(), prePopulated: true); + navigateToCreatePostPage( + context, + url: uri.toString(), + prePopulated: true, + scaffoldMessengerKey: scaffoldMessengerKey, + ); } else { - navigateToCreatePostPage(context, text: value, prePopulated: true); + navigateToCreatePostPage( + context, + text: value, + prePopulated: true, + scaffoldMessengerKey: scaffoldMessengerKey, + ); } } }); @@ -428,7 +458,7 @@ class _ThunderState extends State { opacity: selectedPageIndex == 0 ? 1.0 : 0.0, duration: const Duration(milliseconds: 150), curve: Curves.easeIn, - child: const FeedFAB(), + child: FeedFAB(scaffoldMessengerKey: scaffoldMessengerKey), ) : null, floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling, diff --git a/lib/utils/navigate_create_post.dart b/lib/utils/navigate_create_post.dart index 7e77b525a..6c8dedeaa 100644 --- a/lib/utils/navigate_create_post.dart +++ b/lib/utils/navigate_create_post.dart @@ -19,6 +19,7 @@ Future navigateToCreatePostPage( File? image, String? url, bool? prePopulated, + GlobalKey? scaffoldMessengerKey, }) async { try { ThunderBloc thunderBloc = context.read(); @@ -47,6 +48,7 @@ Future navigateToCreatePostPage( context.read().add(FeedItemUpdatedEvent(postViewMedia: postViewMedia)); } catch (e) {} }, + scaffoldMessengerKey: scaffoldMessengerKey, ), ); },