diff --git a/CHANGELOG.md b/CHANGELOG.md index d331f6183..e8d71445a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Added support for receiving share intents. Android only. - contribution from @ggichure - Added ability to block instances from long-press menu and user settings - Add more search options - contribution from @micahmo +- Added support for display cross-posts - contribution from @micahmo ### Changed - Collapsed comments are easier to expand - contribution from @micahmo diff --git a/lib/community/pages/create_post_page.dart b/lib/community/pages/create_post_page.dart index 8d19bb8fe..a3b3bbfea 100644 --- a/lib/community/pages/create_post_page.dart +++ b/lib/community/pages/create_post_page.dart @@ -32,6 +32,7 @@ class CreatePostPage extends StatefulWidget { final DraftPost? previousDraftPost; // used create post from action sheet + final String? title; final String? text; final File? image; final String? url; @@ -45,6 +46,7 @@ class CreatePostPage extends StatefulWidget { this.previousDraftPost, this.onUpdateDraft, this.image, + this.title, this.text, this.url, this.prePopulated = false, @@ -100,6 +102,7 @@ class _CreatePostPageState extends State { }); if (widget.prePopulated == true) { + _titleTextController.text = widget.title ?? ''; _bodyTextController.text = widget.text ?? ''; _urlTextController.text = widget.url ?? ''; _getDataFromLink(); diff --git a/lib/core/enums/local_settings.dart b/lib/core/enums/local_settings.dart index c1305e886..415fdf9aa 100644 --- a/lib/core/enums/local_settings.dart +++ b/lib/core/enums/local_settings.dart @@ -38,6 +38,7 @@ enum LocalSettings { showPostAuthor(name: 'setting_general_show_post_author', label: 'Show Post Author'), dimReadPosts(name: 'setting_dim_read_posts', label: 'Dim Read Posts'), useAdvancedShareSheet(name: 'setting_use_advanced_share_sheet', label: 'Use Advanced Share Sheet'), + showCrossPosts(name: 'setting_show_cross_posts', label: 'Show Cross-Posts'), /// -------------------------- Post Page Related Settings -------------------------- // Comment Related Settings diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3bccef8ce..98ea2ed13 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -168,6 +168,9 @@ "search": "Search", "@search": {}, "account": "Account", + "crossPostedTo": "Cross-posted to", + "andXMore": "and {count} more", + "createNewCrossPost": "Create new cross-post", "@account": {}, "inbox": "Inbox", "@inbox": {}, diff --git a/lib/post/bloc/post_bloc.dart b/lib/post/bloc/post_bloc.dart index 9f420fa92..a42f861ac 100644 --- a/lib/post/bloc/post_bloc.dart +++ b/lib/post/bloc/post_bloc.dart @@ -104,14 +104,17 @@ class PostBloc extends Bloc { GetPostResponse? getPostResponse; - if (event.postId != null) { - getPostResponse = await lemmy.run(GetPost(id: event.postId!, auth: account?.jwt)).timeout(timeout, onTimeout: () { + // Retrieve the full post for moderators and cross-posts + int? postId = event.postId ?? event.postView?.postView.post.id; + if (postId != null) { + getPostResponse = await lemmy.run(GetPost(id: postId, auth: account?.jwt)).timeout(timeout, onTimeout: () { throw Exception(AppLocalizations.of(GlobalContext.context)!.timeoutComments); }); } PostViewMedia? postView = event.postView; List? moderators; + List? crossPosts; if (getPostResponse != null) { // Parse the posts and add in media information which is used elsewhere in the app @@ -120,6 +123,7 @@ class PostBloc extends Bloc { postView = posts.first; moderators = getPostResponse.moderators; + crossPosts = getPostResponse.crossPosts; } // If we can't get mods from the post response, fallback to getting the whole community. @@ -140,6 +144,7 @@ class PostBloc extends Bloc { postView: postView, communityId: postView?.postView.post.communityId, moderators: moderators, + crossPosts: crossPosts, selectedCommentPath: event.selectedCommentPath, selectedCommentId: event.selectedCommentId, newlyCreatedCommentId: event.newlyCreatedCommentId)); diff --git a/lib/post/bloc/post_state.dart b/lib/post/bloc/post_state.dart index e6ef069b2..39dfc6ef2 100644 --- a/lib/post/bloc/post_state.dart +++ b/lib/post/bloc/post_state.dart @@ -13,6 +13,7 @@ class PostState extends Equatable { this.commentCount = 0, this.communityId, this.moderators, + this.crossPosts, this.hasReachedCommentEnd = false, this.errorMessage, this.sortType, @@ -36,6 +37,7 @@ class PostState extends Equatable { final int? postId; final int? communityId; final List? moderators; + final List? crossPosts; final PostViewMedia? postView; // Comment related data @@ -71,6 +73,7 @@ class PostState extends Equatable { bool? hasReachedCommentEnd, int? communityId, List? moderators, + List? crossPosts, String? errorMessage, CommentSortType? sortType, IconData? sortTypeIcon, @@ -93,6 +96,7 @@ class PostState extends Equatable { hasReachedCommentEnd: hasReachedCommentEnd ?? this.hasReachedCommentEnd, communityId: communityId ?? this.communityId, moderators: moderators ?? this.moderators, + crossPosts: crossPosts ?? this.crossPosts, errorMessage: errorMessage ?? this.errorMessage, sortType: sortType ?? this.sortType, sortTypeIcon: sortTypeIcon ?? this.sortTypeIcon, @@ -116,6 +120,7 @@ class PostState extends Equatable { commentCount, communityId, moderators, + crossPosts, errorMessage, hasReachedCommentEnd, sortType, diff --git a/lib/post/pages/post_page.dart b/lib/post/pages/post_page.dart index ae9b19930..a322f42b8 100644 --- a/lib/post/pages/post_page.dart +++ b/lib/post/pages/post_page.dart @@ -401,6 +401,7 @@ class _PostPageState extends State { itemPositionsListener: _itemPositionsListener, hasReachedCommentEnd: state.hasReachedCommentEnd, moderators: state.moderators, + crossPosts: state.crossPosts, ), ); } diff --git a/lib/post/pages/post_page_success.dart b/lib/post/pages/post_page_success.dart index 63f3fdcd8..adf036900 100644 --- a/lib/post/pages/post_page_success.dart +++ b/lib/post/pages/post_page_success.dart @@ -38,6 +38,7 @@ class PostPageSuccess extends StatefulWidget { final bool viewFullCommentsRefreshing; final List? moderators; + final List? crossPosts; const PostPageSuccess({ super.key, @@ -52,6 +53,7 @@ class PostPageSuccess extends StatefulWidget { this.moddingCommentId, this.viewFullCommentsRefreshing = false, required this.moderators, + required this.crossPosts, }); @override @@ -160,6 +162,7 @@ class _PostPageSuccessState extends State { }); }, moderators: widget.moderators, + crossPosts: widget.crossPosts, ), ), ], diff --git a/lib/post/widgets/comment_view.dart b/lib/post/widgets/comment_view.dart index aa53cf442..e465528c2 100644 --- a/lib/post/widgets/comment_view.dart +++ b/lib/post/widgets/comment_view.dart @@ -37,6 +37,7 @@ class CommentSubview extends StatefulWidget { final DateTime now; final List? moderators; + final List? crossPosts; const CommentSubview({ super.key, @@ -58,6 +59,7 @@ class CommentSubview extends StatefulWidget { this.viewFullCommentsRefreshing = false, required this.now, required this.moderators, + required this.crossPosts, }); @override @@ -135,6 +137,7 @@ class _CommentSubviewState extends State with SingleTickerProvid useDisplayNames: state.useDisplayNames, postViewMedia: widget.postViewMedia!, moderators: widget.moderators, + crossPosts: widget.crossPosts, ); } if (widget.hasReachedCommentEnd == false && widget.comments.isEmpty) { diff --git a/lib/post/widgets/post_view.dart b/lib/post/widgets/post_view.dart index 0eb952906..2f1affaba 100644 --- a/lib/post/widgets/post_view.dart +++ b/lib/post/widgets/post_view.dart @@ -20,6 +20,7 @@ import 'package:thunder/core/singletons/preferences.dart'; import 'package:thunder/feed/utils/utils.dart'; import 'package:thunder/feed/view/feed_page.dart'; import 'package:thunder/post/pages/create_comment_page.dart'; +import 'package:thunder/post/utils/post.dart'; import 'package:thunder/shared/advanced_share_sheet.dart'; import 'package:thunder/shared/common_markdown_body.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; @@ -30,16 +31,19 @@ import 'package:thunder/shared/media_view.dart'; import 'package:thunder/thunder/thunder_icons.dart'; import 'package:thunder/user/utils/special_user_checks.dart'; import 'package:thunder/utils/instance.dart'; +import 'package:thunder/utils/navigate_create_post.dart'; +import 'package:thunder/utils/navigate_post.dart'; import 'package:thunder/utils/navigate_user.dart'; import 'package:thunder/utils/numbers.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:thunder/shared/snackbar.dart'; -class PostSubview extends StatelessWidget { +class PostSubview extends StatefulWidget { final PostViewMedia postViewMedia; final bool useDisplayNames; final int? selectedCommentId; final List? moderators; + final List? crossPosts; const PostSubview({ super.key, @@ -47,14 +51,23 @@ class PostSubview extends StatelessWidget { required this.useDisplayNames, required this.postViewMedia, required this.moderators, + required this.crossPosts, }); + @override + State createState() => _PostSubviewState(); +} + +class _PostSubviewState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final AppLocalizations l10n = AppLocalizations.of(context)!; + final bool useAdvancedShareSheet = context.read().state.useAdvancedShareSheet; + final bool showCrossPosts = context.read().state.showCrossPosts; - final PostView postView = postViewMedia.postView; + final PostView postView = widget.postViewMedia.postView; final Post post = postView.post; final bool isUserLoggedIn = context.watch().state.isLoggedIn; @@ -67,6 +80,8 @@ class PostSubview extends StatelessWidget { final bool isOwnComment = postView.creator.id == context.read().state.account?.userId; + final List sortedCrossPosts = List.from(widget.crossPosts ?? [])..sort((a, b) => b.counts.upvotes.compareTo(a.counts.upvotes)); + return Padding( padding: const EdgeInsets.only(left: 12.0, right: 12.0, bottom: 8.0), child: Column( @@ -84,32 +99,34 @@ class PostSubview extends StatelessWidget { MediaView( scrapeMissingPreviews: scrapeMissingPreviews, post: post, - postView: postViewMedia, + postView: widget.postViewMedia, hideNsfwPreviews: hideNsfwPreviews, markPostReadOnMediaView: markPostReadOnMediaView, isUserLoggedIn: isUserLoggedIn, ), - if (postViewMedia.postView.post.body != null) + if (widget.postViewMedia.postView.post.body != null) Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), + padding: const EdgeInsets.only(top: 8.0), child: CommonMarkdownBody( body: post.body ?? '', ), ), + if (showCrossPosts && sortedCrossPosts.isNotEmpty) CrossPosts(crossPosts: sortedCrossPosts, originalPost: widget.postViewMedia), Padding( - padding: EdgeInsets.only(left: isSpecialUser(context, isOwnComment, post, null, postView.creator, moderators) ? 8.0 : 3.0, right: 8.0, top: 16.0), + padding: EdgeInsets.only(left: isSpecialUser(context, isOwnComment, post, null, postView.creator, widget.moderators) ? 8.0 : 3.0, right: 8.0, top: 16.0), child: Row( // Row for post view: author, community, comment count and post time children: [ Tooltip( excludeFromSemantics: true, - message: '${postView.creator.name}@${fetchInstanceNameFromUrl(postView.creator.actorId) ?? '-'}${fetchUsernameDescriptor(isOwnComment, post, null, postView.creator, moderators)}', + message: + '${postView.creator.name}@${fetchInstanceNameFromUrl(postView.creator.actorId) ?? '-'}${fetchUsernameDescriptor(isOwnComment, post, null, postView.creator, widget.moderators)}', preferBelow: false, child: Material( - color: isSpecialUser(context, isOwnComment, post, null, postView.creator, moderators) - ? fetchUsernameColor(context, isOwnComment, post, null, postView.creator, moderators) ?? theme.colorScheme.onBackground + color: isSpecialUser(context, isOwnComment, post, null, postView.creator, widget.moderators) + ? fetchUsernameColor(context, isOwnComment, post, null, postView.creator, widget.moderators) ?? theme.colorScheme.onBackground : Colors.transparent, - borderRadius: isSpecialUser(context, isOwnComment, post, null, postView.creator, moderators) ? const BorderRadius.all(Radius.elliptical(5, 5)) : null, + borderRadius: isSpecialUser(context, isOwnComment, post, null, postView.creator, widget.moderators) ? const BorderRadius.all(Radius.elliptical(5, 5)) : null, child: InkWell( borderRadius: BorderRadius.circular(5), onTap: () { @@ -120,14 +137,14 @@ class PostSubview extends StatelessWidget { child: Row( children: [ Text( - postView.creator.displayName != null && useDisplayNames ? postView.creator.displayName! : postView.creator.name, + postView.creator.displayName != null && widget.useDisplayNames ? postView.creator.displayName! : postView.creator.name, textScaleFactor: MediaQuery.of(context).textScaleFactor * thunderState.metadataFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( - color: (isSpecialUser(context, isOwnComment, post, null, postView.creator, moderators) ? theme.colorScheme.onBackground : theme.textTheme.bodyMedium?.color) + color: (isSpecialUser(context, isOwnComment, post, null, postView.creator, widget.moderators) ? theme.colorScheme.onBackground : theme.textTheme.bodyMedium?.color) ?.withOpacity(0.75), ), ), - if (isSpecialUser(context, isOwnComment, post, null, postView.creator, moderators)) const SizedBox(width: 2.0), + if (isSpecialUser(context, isOwnComment, post, null, postView.creator, widget.moderators)) const SizedBox(width: 2.0), if (isOwnComment) Padding( padding: const EdgeInsets.only(left: 1), @@ -146,7 +163,7 @@ class PostSubview extends StatelessWidget { color: theme.colorScheme.onBackground, ), ), - if (isModerator(postView.creator, moderators)) + if (isModerator(postView.creator, widget.moderators)) Padding( padding: const EdgeInsets.only(left: 1), child: Icon( @@ -170,7 +187,7 @@ class PostSubview extends StatelessWidget { ), ), ), - if (isSpecialUser(context, isOwnComment, post, null, postView.creator, moderators)) const SizedBox(width: 8.0), + if (isSpecialUser(context, isOwnComment, post, null, postView.creator, widget.moderators)) const SizedBox(width: 8.0), Text( 'to', textScaleFactor: MediaQuery.of(context).textScaleFactor * thunderState.metadataFontSizeScale.textScaleFactor, @@ -203,9 +220,9 @@ class PostSubview extends StatelessWidget { Padding( padding: const EdgeInsets.only(right: 0.0), child: PostViewMetaData( - comments: postViewMedia.postView.counts.comments, - unreadComments: postViewMedia.postView.unreadComments, - hasBeenEdited: postViewMedia.postView.post.updated != null ? true : false, + comments: widget.postViewMedia.postView.counts.comments, + unreadComments: widget.postViewMedia.postView.unreadComments, + hasBeenEdited: widget.postViewMedia.postView.post.updated != null ? true : false, published: post.published, saved: postView.saved, ), @@ -241,7 +258,7 @@ class PostSubview extends StatelessWidget { ), const SizedBox(width: 4.0), Text( - formatNumberToK(postViewMedia.postView.counts.upvotes), + formatNumberToK(widget.postViewMedia.postView.counts.upvotes), style: TextStyle( color: isUserLoggedIn ? (postView.myVote == 1 ? Colors.orange : theme.textTheme.bodyMedium?.color) : null, ), @@ -275,7 +292,7 @@ class PostSubview extends StatelessWidget { ), const SizedBox(width: 4.0), Text( - formatNumberToK(postViewMedia.postView.counts.downvotes), + formatNumberToK(widget.postViewMedia.postView.counts.downvotes), style: TextStyle( color: isUserLoggedIn ? (postView.myVote == -1 ? Colors.blue : theme.textTheme.bodyMedium?.color) : null, ), @@ -309,7 +326,7 @@ class PostSubview extends StatelessWidget { onPressed: isUserLoggedIn ? () async { if (postView.post.locked) { - showSnackbar(context, AppLocalizations.of(context)!.postLocked); + showSnackbar(context, l10n.postLocked); return; } @@ -323,7 +340,7 @@ class PostSubview extends StatelessWidget { SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; DraftComment? newDraftComment; DraftComment? previousDraftComment; - String draftId = '${LocalSettings.draftsCache.name}-${postViewMedia.postView.post.id}'; + String draftId = '${LocalSettings.draftsCache.name}-${widget.postViewMedia.postView.post.id}'; String? draftCommentJson = prefs.getString(draftId); if (draftCommentJson != null) { previousDraftComment = DraftComment.fromJson(jsonDecode(draftCommentJson)); @@ -334,44 +351,46 @@ class PostSubview extends StatelessWidget { } }); - Navigator.of(context) - .push( - SwipeablePageRoute( - transitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : null, - canOnlySwipeFromEdge: true, - backGestureDetectionWidth: 45, - builder: (context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: postBloc), - BlocProvider.value(value: thunderBloc), - BlocProvider.value(value: accountBloc), - ], - child: CreateCommentPage( - postView: postViewMedia, - previousDraftComment: previousDraftComment, - onUpdateDraft: (c) => newDraftComment = c, - ), - ); - }, - ), - ) - .whenComplete(() async { - timer.cancel(); + if (context.mounted) { + Navigator.of(context) + .push( + SwipeablePageRoute( + transitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : null, + canOnlySwipeFromEdge: true, + backGestureDetectionWidth: 45, + builder: (context) { + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: postBloc), + BlocProvider.value(value: thunderBloc), + BlocProvider.value(value: accountBloc), + ], + child: CreateCommentPage( + postView: widget.postViewMedia, + previousDraftComment: previousDraftComment, + onUpdateDraft: (c) => newDraftComment = c, + ), + ); + }, + ), + ) + .whenComplete(() async { + timer.cancel(); - if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true) { - await Future.delayed(const Duration(milliseconds: 300)); - showSnackbar(context, AppLocalizations.of(context)!.commentSavedAsDraft); - prefs.setString(draftId, jsonEncode(newDraftComment!.toJson())); - } else { - prefs.remove(draftId); - } - }); + if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true) { + await Future.delayed(const Duration(milliseconds: 300)); + if (context.mounted) { + showSnackbar(context, l10n.commentSavedAsDraft); + } + prefs.setString(draftId, jsonEncode(newDraftComment!.toJson())); + } else { + prefs.remove(draftId); + } + }); + } } : null, - icon: postView.post.locked - ? Icon(Icons.lock, semanticLabel: AppLocalizations.of(context)!.postLocked, color: Colors.red) - : Icon(Icons.reply_rounded, semanticLabel: AppLocalizations.of(context)!.reply(0)), + icon: postView.post.locked ? Icon(Icons.lock, semanticLabel: l10n.postLocked, color: Colors.red) : Icon(Icons.reply_rounded, semanticLabel: l10n.reply(0)), ), ), Expanded( @@ -379,12 +398,12 @@ class PostSubview extends StatelessWidget { child: IconButton( icon: const Icon(Icons.share_rounded, semanticLabel: 'Share'), onPressed: useAdvancedShareSheet - ? () => showAdvancedShareSheet(context, postViewMedia) - : postViewMedia.media.isEmpty + ? () => showAdvancedShareSheet(context, widget.postViewMedia) + : widget.postViewMedia.media.isEmpty ? () => Share.share(post.apId) : () => showPostActionBottomModalSheet( context, - postViewMedia, + widget.postViewMedia, actionsToInclude: [PostCardAction.sharePost, PostCardAction.shareMedia, PostCardAction.shareLink], ), ), @@ -396,3 +415,185 @@ class PostSubview extends StatelessWidget { ); } } + +class CrossPosts extends StatefulWidget { + final List crossPosts; + final PostViewMedia originalPost; + + const CrossPosts({super.key, required this.crossPosts, required this.originalPost}); + + @override + State createState() => _CrossPostsState(); +} + +class _CrossPostsState extends State with SingleTickerProviderStateMixin { + bool _areCrossPostsExpanded = false; + + late final AnimationController _controller = AnimationController( + duration: const Duration(milliseconds: 100), + vsync: this, + ); + + late final Animation _offsetAnimation = Tween( + begin: Offset.zero, + end: const Offset(1.5, 0.0), + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.fastOutSlowIn, + )); + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final AppLocalizations l10n = AppLocalizations.of(context)!; + final TextStyle? crossPostTextStyle = theme.textTheme.bodySmall?.copyWith(fontStyle: FontStyle.italic); + final TextStyle? crossPostLinkTextStyle = crossPostTextStyle?.copyWith(color: Colors.blue); + + return Padding( + padding: const EdgeInsets.only(top: 10), + child: Container( + decoration: BoxDecoration( + color: theme.dividerColor.withOpacity(0.25), + borderRadius: BorderRadius.circular(10), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + InkWell( + onTap: () => setState(() => _areCrossPostsExpanded = !_areCrossPostsExpanded), + borderRadius: BorderRadius.circular(10), + child: Padding( + padding: const EdgeInsets.all(5), + child: Stack( + children: [ + Row( + children: [ + Expanded( + // The rich text handles overflow across multiple sections (TextSpan) of text + child: RichText( + overflow: TextOverflow.ellipsis, + maxLines: 1, + text: TextSpan( + children: [ + TextSpan( + text: l10n.crossPostedTo, + style: crossPostTextStyle, + ), + TextSpan( + text: ' ${widget.crossPosts[0].community.name}@${fetchInstanceNameFromUrl(widget.crossPosts[0].community.actorId)} ', + style: crossPostLinkTextStyle, + // This text is not tappable; there is an invisible widget above this that handles the InkWell and the tap gesture + ), + if (widget.crossPosts.length > 1) + TextSpan( + text: l10n.andXMore(widget.crossPosts.length - 1), + style: crossPostTextStyle, + ), + ], + ), + ), + ), + _areCrossPostsExpanded ? const Icon(Icons.arrow_drop_up_rounded) : const Icon(Icons.arrow_drop_down_rounded), + ], + ), + // This Row widget exists purely so that we can get an InkWell on the community link. + // However, the text is insvisible because we actually want the RichText to manage the text, + // including overflow. + Padding( + padding: const EdgeInsets.only(top: 5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + l10n.crossPostedTo, + style: crossPostTextStyle?.copyWith(color: Colors.transparent), + ), + InkWell( + borderRadius: BorderRadius.circular(5), + onTap: () async => navigateToPost(context, postViewMedia: (await parsePostViews([widget.crossPosts[0]])).first), + child: Text( + ' ${widget.crossPosts[0].community.name}@${fetchInstanceNameFromUrl(widget.crossPosts[0].community.actorId)} ', + style: crossPostLinkTextStyle?.copyWith(color: Colors.transparent), + ), + ), + ], + ), + ), + ], + ), + ), + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + transitionBuilder: (Widget child, Animation animation) { + return SizeTransition( + sizeFactor: animation, + child: SlideTransition(position: _offsetAnimation, child: child), + ); + }, + child: _areCrossPostsExpanded + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(left: 5), + child: Row( + children: [ + Text( + ' • ', + style: crossPostTextStyle, + ), + InkWell( + onTap: () async => navigateToPost(context, postViewMedia: (await parsePostViews([widget.crossPosts[index + 1]])).first), + borderRadius: BorderRadius.circular(5), + child: Text( + '${widget.crossPosts[index + 1].community.name}@${fetchInstanceNameFromUrl(widget.crossPosts[index + 1].community.actorId)}', + style: crossPostLinkTextStyle, + ), + ), + ], + ), + ); + }, + itemCount: widget.crossPosts.length - 1, + ), + InkWell( + onTap: () async { + await navigateToCreatePostPage( + context, + title: widget.originalPost.postView.post.name, + url: widget.originalPost.postView.post.url, + prePopulated: true, + ); + }, + borderRadius: BorderRadius.circular(10), + child: Padding( + padding: const EdgeInsets.all(5), + child: Row( + children: [ + Text( + l10n.createNewCrossPost, + style: crossPostTextStyle, + ), + const Icon(Icons.arrow_right_rounded) + ], + ), + ), + ), + ], + ) + : Container(), + ), + ], + ), + ), + ); + } +} diff --git a/lib/settings/pages/general_settings_page.dart b/lib/settings/pages/general_settings_page.dart index e494d1790..2e647ee3e 100644 --- a/lib/settings/pages/general_settings_page.dart +++ b/lib/settings/pages/general_settings_page.dart @@ -70,6 +70,7 @@ class _GeneralSettingsPageState extends State with SingleTi bool scoreCounters = false; bool dimReadPosts = true; bool useAdvancedShareSheet = true; + bool showCrossPosts = true; // Comment Related Settings SortType defaultSortType = DEFAULT_SORT_TYPE; @@ -204,6 +205,10 @@ class _GeneralSettingsPageState extends State with SingleTi await prefs.setBool(LocalSettings.useAdvancedShareSheet.name, value); setState(() => useAdvancedShareSheet = value); break; + case LocalSettings.showCrossPosts: + await prefs.setBool(LocalSettings.showCrossPosts.name, value); + setState(() => showCrossPosts = value); + break; // Comment Related Settings case LocalSettings.defaultCommentSortType: @@ -276,6 +281,7 @@ class _GeneralSettingsPageState extends State with SingleTi showPostAuthor = prefs.getBool(LocalSettings.showPostAuthor.name) ?? false; dimReadPosts = prefs.getBool(LocalSettings.dimReadPosts.name) ?? true; useAdvancedShareSheet = prefs.getBool(LocalSettings.useAdvancedShareSheet.name) ?? true; + showCrossPosts = prefs.getBool(LocalSettings.showCrossPosts.name) ?? true; // Comment Settings showCommentButtonActions = prefs.getBool(LocalSettings.showCommentActionButtons.name) ?? false; @@ -597,6 +603,13 @@ class _GeneralSettingsPageState extends State with SingleTi iconDisabled: Icons.screen_share_outlined, onToggle: (bool value) => setPreferences(LocalSettings.useAdvancedShareSheet, value), ), + ToggleOption( + description: LocalSettings.showCrossPosts.label, + value: showCrossPosts, + iconEnabled: Icons.repeat_on_rounded, + iconDisabled: Icons.repeat_rounded, + onToggle: (bool value) => setPreferences(LocalSettings.showCrossPosts, value), + ), ], ), ), diff --git a/lib/thunder/bloc/thunder_bloc.dart b/lib/thunder/bloc/thunder_bloc.dart index a624930d1..4ae5674ea 100644 --- a/lib/thunder/bloc/thunder_bloc.dart +++ b/lib/thunder/bloc/thunder_bloc.dart @@ -129,6 +129,7 @@ class ThunderBloc extends Bloc { bool scoreCounters = prefs.getBool(LocalSettings.scoreCounters.name) ?? false; bool dimReadPosts = prefs.getBool(LocalSettings.dimReadPosts.name) ?? true; bool useAdvancedShareSheet = prefs.getBool(LocalSettings.useAdvancedShareSheet.name) ?? true; + bool showCrossPosts = prefs.getBool(LocalSettings.showCrossPosts.name) ?? true; /// -------------------------- Post Page Related Settings -------------------------- // Comment Related Settings @@ -247,6 +248,7 @@ class ThunderBloc extends Bloc { scoreCounters: scoreCounters, dimReadPosts: dimReadPosts, useAdvancedShareSheet: useAdvancedShareSheet, + showCrossPosts: showCrossPosts, /// -------------------------- Post Page Related Settings -------------------------- // Comment Related Settings diff --git a/lib/thunder/bloc/thunder_state.dart b/lib/thunder/bloc/thunder_state.dart index 783ef35d2..256ec90e3 100644 --- a/lib/thunder/bloc/thunder_state.dart +++ b/lib/thunder/bloc/thunder_state.dart @@ -50,6 +50,7 @@ class ThunderState extends Equatable { this.showPostAuthor = false, this.dimReadPosts = true, this.useAdvancedShareSheet = true, + this.showCrossPosts = true, /// -------------------------- Post Page Related Settings -------------------------- this.disablePostFabs = false, @@ -167,6 +168,7 @@ class ThunderState extends Equatable { final bool scoreCounters; final bool dimReadPosts; final bool useAdvancedShareSheet; + final bool showCrossPosts; /// -------------------------- Post Page Related Settings -------------------------- final bool disablePostFabs; @@ -292,6 +294,7 @@ class ThunderState extends Equatable { bool? showPostAuthor, bool? dimReadPosts, bool? useAdvancedShareSheet, + bool? showCrossPosts, /// -------------------------- Post Page Related Settings -------------------------- // Comment Related Settings @@ -409,6 +412,7 @@ class ThunderState extends Equatable { showPostAuthor: showPostAuthor ?? this.showPostAuthor, dimReadPosts: dimReadPosts ?? this.dimReadPosts, useAdvancedShareSheet: useAdvancedShareSheet ?? this.useAdvancedShareSheet, + showCrossPosts: showCrossPosts ?? this.showCrossPosts, /// -------------------------- Post Page Related Settings -------------------------- disablePostFabs: disablePostFabs ?? this.disablePostFabs, @@ -533,6 +537,7 @@ class ThunderState extends Equatable { showPostAuthor, dimReadPosts, useAdvancedShareSheet, + showCrossPosts, /// -------------------------- Post Page Related Settings -------------------------- disablePostFabs, diff --git a/lib/utils/navigate_create_post.dart b/lib/utils/navigate_create_post.dart index 3dd2e0213..9302892a7 100644 --- a/lib/utils/navigate_create_post.dart +++ b/lib/utils/navigate_create_post.dart @@ -13,6 +13,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; Future navigateToCreatePostPage( BuildContext context, { + String? title, String? text, File? image, String? url, @@ -34,6 +35,7 @@ Future navigateToCreatePostPage( BlocProvider.value(value: accountBloc), ], child: CreatePostPage( + title: title, text: text, image: image, url: url, diff --git a/pubspec.lock b/pubspec.lock index d0ac804d9..dbe9bd9d5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -941,10 +941,10 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" mime: dependency: transitive description: @@ -1588,10 +1588,10 @@ packages: dependency: transitive description: name: web - sha256: "14f1f70c51119012600c5f1f60ca68efda5a9b6077748163c6af2893ec5df8fc" + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 url: "https://pub.dev" source: hosted - version: "0.2.1-beta" + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -1673,5 +1673,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-157.0.dev <4.0.0" + dart: ">=3.1.0 <4.0.0" flutter: ">=3.13.0"