diff --git a/CHANGELOG.md b/CHANGELOG.md index bc17f87..ca7a474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Next + +- Add minimal spacing between a post author and title in the post widget +- Use listPostCreatorTitleStyle for post creator localizations when showing posts in a list +- Share more code between the various widgets within flutter_timeline_view + ## 5.1.1 - Be honest about which Dart and Flutter versions we support diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart index 450f207..aa927cd 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart @@ -9,9 +9,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:flutter_timeline_view/src/config/timeline_options.dart'; +import 'package:flutter_timeline_view/src/widgets/post_components/header.dart'; +import 'package:flutter_timeline_view/src/widgets/post_components/image.dart'; +import 'package:flutter_timeline_view/src/widgets/post_components/info.dart'; import 'package:flutter_timeline_view/src/widgets/reaction_bottom.dart'; -import 'package:flutter_timeline_view/src/widgets/tappable_image.dart'; -import 'package:flutter_timeline_view/src/widgets/timeline_post_widget.dart'; import 'package:intl/intl.dart'; class TimelinePostScreen extends StatefulWidget { @@ -172,204 +173,54 @@ class _TimelinePostScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - if (post.creator != null) - InkWell( - onTap: widget.onUserTap != null - ? () => - widget.onUserTap?.call(post.creator!.userId) - : null, - child: Row( - children: [ - if (post.creator!.imageUrl != null) ...[ - widget.options.userAvatarBuilder?.call( - post.creator!, - 28, - ) ?? - CircleAvatar( - radius: 14, - backgroundImage: - CachedNetworkImageProvider( - post.creator!.imageUrl!, - ), - ), - ] else ...[ - widget.options.anonymousAvatarBuilder?.call( - post.creator!, - 28, - ) ?? - const CircleAvatar( - radius: 14, - child: Icon( - Icons.person, - ), - ), - ], - const SizedBox(width: 10), - Text( - widget.options.nameBuilder - ?.call(post.creator) ?? - post.creator?.fullName ?? - widget.options.translations.anonymousUser, - style: widget.options.theme.textStyles - .postCreatorTitleStyle ?? - theme.textTheme.titleSmall!.copyWith( - color: Colors.black, - ), - ), - ], - ), - ), - const Spacer(), - if (!(widget.isOverviewScreen ?? false) && - (widget.allowAllDeletion || - post.creator?.userId == widget.userId)) ...[ - PopupMenuButton( - onSelected: (value) async { - if (value == 'delete') { - await showPostDeletionConfirmationDialog( - widget.options, - context, - widget.onPostDelete, - ); - } - }, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Text( - widget.options.translations.deletePost, - style: widget.options.theme.textStyles - .deletePostStyle ?? - theme.textTheme.bodyMedium, - ), - const SizedBox(width: 8), - widget.options.theme.deleteIcon ?? - Icon( - Icons.delete, - color: widget.options.theme.iconColor, - ), - ], - ), - ), - ], - child: widget.options.theme.moreIcon ?? - Icon( - Icons.more_horiz_rounded, - color: widget.options.theme.iconColor, - ), - ), - ], - ], + PostHeader( + service: widget.service, + options: widget.options, + userId: widget.userId, + post: widget.post, + allowDeletion: !(widget.isOverviewScreen ?? false) && + (widget.allowAllDeletion || + post.creator?.userId == widget.userId), + onUserTap: widget.onUserTap, + onPostDelete: widget.onPostDelete, ), - // image of the posts if (post.imageUrl != null || post.image != null) ...[ - const SizedBox(height: 8), - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: widget.options.doubleTapTolike - ? TappableImage( - likeAndDislikeIcon: widget - .options.likeAndDislikeIconsForDoubleTap, - post: post, - userId: widget.userId, - onLike: ({required bool liked}) async { - var userId = widget.userId; - - late TimelinePost result; - - if (!liked) { - result = - await widget.service.postService.likePost( - userId, - post, - ); - } else { - result = await widget.service.postService - .unlikePost( - userId, - post, - ); - } - - await loadPostDetails(); - - return result.likedBy?.contains(userId) ?? - false; - }, - ) - : post.image != null - ? Image.memory( - width: double.infinity, - post.image!, - fit: BoxFit.fitHeight, - ) - : CachedNetworkImage( - width: double.infinity, - imageUrl: post.imageUrl!, - fit: BoxFit.fitHeight, - ), + const SizedBox(height: 8.0), + PostImage( + options: widget.options, + service: widget.service, + userId: widget.userId, + post: widget.post, + flexible: false, + onUpdatePost: loadPostDetails, ), ], - const SizedBox( - height: 8, - ), + const SizedBox(height: 8.0), // post information - Row( - children: [ - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () async { - if (widget.isOverviewScreen ?? false) return; - if (isLikedByUser) { - updatePost( - await widget.service.postService.unlikePost( - widget.userId, - post, - ), - ); - setState(() {}); - } else { - updatePost( - await widget.service.postService.likePost( - widget.userId, - post, - ), - ); - setState(() {}); - } - }, - icon: isLikedByUser - ? widget.options.theme.likedIcon ?? - Icon( - Icons.favorite_rounded, - color: widget.options.theme.iconColor, - size: widget.options.iconSize, - ) - : widget.options.theme.likeIcon ?? - Icon( - Icons.favorite_outline_outlined, - color: widget.options.theme.iconColor, - size: widget.options.iconSize, - ), - ), - const SizedBox(width: 8), - if (post.reactionEnabled) - widget.options.theme.commentIcon ?? - SvgPicture.asset( - 'assets/Comment.svg', - package: 'flutter_timeline_view', - // ignore: deprecated_member_use - color: widget.options.theme.iconColor, - width: widget.options.iconSize, - height: widget.options.iconSize, - ), - ], + _PostLikeAndReactionsInformation( + options: widget.options, + post: widget.post, + isLikedByUser: isLikedByUser, + onLikePressed: () async { + if (widget.isOverviewScreen ?? false) return; + if (isLikedByUser) { + updatePost( + await widget.service.postService.unlikePost( + widget.userId, + post, + ), + ); + setState(() {}); + } else { + updatePost( + await widget.service.postService.likePost( + widget.userId, + post, + ), + ); + setState(() {}); + } + }, ), const SizedBox(height: 8), // ignore: avoid_bool_literals_in_conditional_expressions @@ -385,24 +236,10 @@ class _TimelinePostScreenState extends State { ?.copyWith(color: Colors.black), ), ], - Text.rich( - TextSpan( - text: widget.options.nameBuilder?.call(post.creator) ?? - post.creator?.fullName ?? - widget.options.translations.anonymousUser, - style: widget - .options.theme.textStyles.postCreatorNameStyle ?? - theme.textTheme.titleSmall! - .copyWith(color: Colors.black), - children: [ - TextSpan( - text: post.title, - style: - widget.options.theme.textStyles.postTitleStyle ?? - theme.textTheme.bodySmall, - ), - ], - ), + PostTitle( + options: widget.options, + post: post, + isForList: false, ), const SizedBox(height: 20), Text( @@ -420,198 +257,71 @@ class _TimelinePostScreenState extends State { if (post.reactionEnabled && widget.isOverviewScreen != null ? !widget.isOverviewScreen! : false) ...[ - Text( - widget.options.translations.commentsTitleOnPost, - style: theme.textTheme.titleSmall! - .copyWith(color: Colors.black), - ), - for (var reaction - in post.reactions ?? []) ...[ - const SizedBox(height: 4), - GestureDetector( - onLongPressStart: (details) async { - if (reaction.creatorId == widget.userId || - widget.allowAllDeletion) { - var overlay = Overlay.of(context) - .context - .findRenderObject()! as RenderBox; - var position = RelativeRect.fromRect( - Rect.fromPoints( - details.globalPosition, - details.globalPosition, - ), - Offset.zero & overlay.size, - ); - // Show popup menu for deletion - var value = await showMenu( - context: context, - position: position, - items: [ - PopupMenuItem( - value: 'delete', - child: Text( - widget.options.translations.deleteReaction, - ), - ), - ], - ); - if (value == 'delete') { - // Call service to delete reaction - updatePost( - await widget.service.postService - .deletePostReaction(post, reaction.id), - ); - } - } - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (reaction.creator?.imageUrl != null && - reaction.creator!.imageUrl!.isNotEmpty) ...[ - widget.options.userAvatarBuilder?.call( - reaction.creator!, - 14, - ) ?? - CircleAvatar( - radius: 14, - backgroundImage: CachedNetworkImageProvider( - reaction.creator!.imageUrl!, - ), - ), - ] else ...[ - widget.options.anonymousAvatarBuilder?.call( - reaction.creator!, - 14, - ) ?? - const CircleAvatar( - radius: 14, - child: Icon( - Icons.person, - ), - ), - ], - const SizedBox(width: 10), - if (reaction.imageUrl != null) ...[ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.options.nameBuilder - ?.call(reaction.creator) ?? - reaction.creator?.fullName ?? - widget.options.translations - .anonymousUser, - style: theme.textTheme.titleSmall! - .copyWith(color: Colors.black), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: CachedNetworkImage( - imageUrl: reaction.imageUrl!, - fit: BoxFit.fitWidth, - ), - ), - ], - ), - ), - ] else ...[ - Expanded( - child: Text.rich( - TextSpan( - text: widget.options.nameBuilder - ?.call(reaction.creator) ?? - reaction.creator?.fullName ?? - widget - .options.translations.anonymousUser, - style: theme.textTheme.titleSmall! - .copyWith(color: Colors.black), - children: [ - const TextSpan(text: ' '), - TextSpan( - text: reaction.reaction ?? '', - style: theme.textTheme.bodySmall, - ), - const TextSpan(text: '\n'), - TextSpan( - text: dateFormat - .format(reaction.createdAt), - style: theme.textTheme.labelSmall! - .copyWith( - color: theme - .textTheme.labelSmall!.color! - .withOpacity(0.5), - letterSpacing: 0.5, - ), - ), - - // text should go to new line - ], - ), - ), - ), - ], - Builder( - builder: (context) { - var isLikedByUser = - reaction.likedBy?.contains(widget.userId) ?? - false; - return IconButton( - padding: const EdgeInsets.only(left: 12), - constraints: const BoxConstraints(), - onPressed: () async { - if (isLikedByUser) { - updatePost( - await widget.service.postService - .unlikeReaction( - widget.userId, - post, - reaction.id, - ), - ); - setState(() {}); - } else { - updatePost( - await widget.service.postService - .likeReaction( - widget.userId, - post, - reaction.id, - ), - ); - setState(() {}); - } - }, - icon: isLikedByUser - ? widget.options.theme.likedIcon ?? - Icon( - Icons.favorite_rounded, - color: - widget.options.theme.iconColor, - size: 14, - ) - : widget.options.theme.likeIcon ?? - Icon( - Icons.favorite_outline_outlined, - color: - widget.options.theme.iconColor, - size: 14, - ), - ); - }, + _CommentSection( + options: widget.options, + userId: widget.userId, + post: widget.post, + dateFormat: dateFormat, + onReactionLostPress: ( + LongPressStartDetails details, { + required TimelinePostReaction reaction, + }) async { + if (reaction.creatorId == widget.userId || + widget.allowAllDeletion) { + var overlay = Overlay.of(context) + .context + .findRenderObject()! as RenderBox; + var position = RelativeRect.fromRect( + Rect.fromPoints( + details.globalPosition, + details.globalPosition, ), - ], - ), - ), - const SizedBox(height: 4), - ], - if (post.reactions?.isEmpty ?? true) ...[ - Text( - widget.options.translations.firstComment, - style: theme.textTheme.bodySmall, - ), - ], + Offset.zero & overlay.size, + ); + // Show popup menu for deletion + var value = await showMenu( + context: context, + position: position, + items: [ + PopupMenuItem( + value: 'delete', + child: Text( + widget.options.translations.deleteReaction, + ), + ), + ], + ); + if (value == 'delete') { + // Call service to delete reaction + updatePost( + await widget.service.postService + .deletePostReaction(post, reaction.id), + ); + } + } + }, + onLikeReaction: (TimelinePostReaction reaction) async { + if (isLikedByUser) { + updatePost( + await widget.service.postService.unlikeReaction( + widget.userId, + post, + reaction.id, + ), + ); + setState(() {}); + } else { + updatePost( + await widget.service.postService.likeReaction( + widget.userId, + post, + reaction.id, + ), + ); + setState(() {}); + } + }, + ), const SizedBox(height: 120), ], ], @@ -693,3 +403,210 @@ class _TimelinePostScreenState extends State { ); } } + +class _PostLikeAndReactionsInformation extends StatelessWidget { + const _PostLikeAndReactionsInformation({ + required this.options, + required this.post, + required this.isLikedByUser, + required this.onLikePressed, + }); + + final TimelineOptions options; + final TimelinePost post; + final bool isLikedByUser; + final VoidCallback onLikePressed; + + @override + Widget build(BuildContext context) => Row( + children: [ + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: onLikePressed, + icon: isLikedByUser + ? options.theme.likedIcon ?? + Icon( + Icons.favorite_rounded, + color: options.theme.iconColor, + size: options.iconSize, + ) + : options.theme.likeIcon ?? + Icon( + Icons.favorite_outline_outlined, + color: options.theme.iconColor, + size: options.iconSize, + ), + ), + const SizedBox(width: 8.0), + if (post.reactionEnabled) + options.theme.commentIcon ?? + SvgPicture.asset( + 'assets/Comment.svg', + package: 'flutter_timeline_view', + // ignore: deprecated_member_use + color: options.theme.iconColor, + width: options.iconSize, + height: options.iconSize, + ), + ], + ); +} + +class _CommentSection extends StatelessWidget { + const _CommentSection({ + required this.options, + required this.userId, + required this.post, + required this.dateFormat, + required this.onReactionLostPress, + required this.onLikeReaction, + }); + + final TimelineOptions options; + final String userId; + final TimelinePost post; + final DateFormat dateFormat; + + final void Function( + LongPressStartDetails details, { + required TimelinePostReaction reaction, + }) onReactionLostPress; + final void Function(TimelinePostReaction reaction) onLikeReaction; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return Column( + children: [ + Text( + options.translations.commentsTitleOnPost, + style: theme.textTheme.titleSmall!.copyWith(color: Colors.black), + ), + for (var reaction in post.reactions ?? []) ...[ + const SizedBox(height: 4), + GestureDetector( + onLongPressStart: (details) async { + onReactionLostPress(details, reaction: reaction); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (reaction.creator?.imageUrl != null && + reaction.creator!.imageUrl!.isNotEmpty) ...[ + options.userAvatarBuilder?.call( + reaction.creator!, + 14, + ) ?? + CircleAvatar( + radius: 14, + backgroundImage: CachedNetworkImageProvider( + reaction.creator!.imageUrl!, + ), + ), + ] else ...[ + options.anonymousAvatarBuilder?.call( + reaction.creator!, + 14, + ) ?? + const CircleAvatar( + radius: 14, + child: Icon( + Icons.person, + ), + ), + ], + const SizedBox(width: 10), + if (reaction.imageUrl != null) ...[ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + options.nameBuilder?.call(reaction.creator) ?? + reaction.creator?.fullName ?? + options.translations.anonymousUser, + style: theme.textTheme.titleSmall! + .copyWith(color: Colors.black), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: CachedNetworkImage( + imageUrl: reaction.imageUrl!, + fit: BoxFit.fitWidth, + ), + ), + ], + ), + ), + ] else ...[ + Expanded( + child: Text.rich( + TextSpan( + text: options.nameBuilder?.call(reaction.creator) ?? + reaction.creator?.fullName ?? + options.translations.anonymousUser, + style: theme.textTheme.titleSmall! + .copyWith(color: Colors.black), + children: [ + const TextSpan(text: ' '), + TextSpan( + text: reaction.reaction ?? '', + style: theme.textTheme.bodySmall, + ), + const TextSpan(text: '\n'), + TextSpan( + text: dateFormat.format(reaction.createdAt), + style: theme.textTheme.labelSmall!.copyWith( + color: theme.textTheme.labelSmall!.color! + .withOpacity(0.5), + letterSpacing: 0.5, + ), + ), + + // text should go to new line + ], + ), + ), + ), + ], + Builder( + builder: (context) { + var isLikedByUser = + reaction.likedBy?.contains(userId) ?? false; + return IconButton( + padding: const EdgeInsets.only(left: 12), + constraints: const BoxConstraints(), + onPressed: () => onLikeReaction(reaction), + icon: isLikedByUser + ? options.theme.likedIcon ?? + Icon( + Icons.favorite_rounded, + color: options.theme.iconColor, + size: 14, + ) + : options.theme.likeIcon ?? + Icon( + Icons.favorite_outline_outlined, + color: options.theme.iconColor, + size: 14, + ), + ); + }, + ), + ], + ), + ), + const SizedBox(height: 4), + ], + if (post.reactions?.isEmpty ?? true) ...[ + Text( + options.translations.firstComment, + style: theme.textTheme.bodySmall, + ), + ], + ], + ); + } +} diff --git a/packages/flutter_timeline_view/lib/src/widgets/post_components/header.dart b/packages/flutter_timeline_view/lib/src/widgets/post_components/header.dart new file mode 100644 index 0000000..0c30a5c --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/widgets/post_components/header.dart @@ -0,0 +1,115 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; +import 'package:flutter_timeline_view/flutter_timeline_view.dart'; + +class PostHeader extends StatelessWidget { + const PostHeader({ + required this.service, + required this.options, + required this.userId, + required this.post, + required this.allowDeletion, + required this.onUserTap, + required this.onPostDelete, + super.key, + }); + + final TimelineService service; + final TimelineOptions options; + final String userId; + final TimelinePost post; + final bool allowDeletion; + final void Function(String userId)? onUserTap; + final VoidCallback onPostDelete; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return Row( + children: [ + if (post.creator != null) ...[ + InkWell( + onTap: onUserTap != null + ? () => onUserTap?.call(post.creator!.userId) + : null, + child: Row( + children: [ + if (post.creator!.imageUrl != null) ...[ + options.userAvatarBuilder?.call( + post.creator!, + 28, + ) ?? + CircleAvatar( + radius: 14, + backgroundImage: + CachedNetworkImageProvider(post.creator!.imageUrl!), + ), + ] else ...[ + options.anonymousAvatarBuilder?.call( + post.creator!, + 28, + ) ?? + const CircleAvatar( + radius: 14, + child: Icon( + Icons.person, + ), + ), + ], + const SizedBox(width: 10.0), + Text( + options.nameBuilder?.call(post.creator) ?? + post.creator?.fullName ?? + options.translations.anonymousUser, + style: options.theme.textStyles.listPostCreatorTitleStyle ?? + theme.textTheme.titleSmall!.copyWith(color: Colors.black), + ), + ], + ), + ), + ], + const Spacer(), + if (allowDeletion) ...[ + PopupMenuButton( + onSelected: (value) async { + if (value == 'delete') { + await showPostDeletionConfirmationDialog( + options, + context, + onPostDelete, + ); + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Text( + options.translations.deletePost, + style: options.theme.textStyles.deletePostStyle ?? + theme.textTheme.bodyMedium, + ), + const SizedBox(width: 8.0), + options.theme.deleteIcon ?? + Icon( + Icons.delete, + color: options.theme.iconColor, + ), + ], + ), + ), + ], + child: options.theme.moreIcon ?? + Icon( + Icons.more_horiz_rounded, + color: options.theme.iconColor, + ), + ), + ], + ], + ); + } +} diff --git a/packages/flutter_timeline_view/lib/src/widgets/post_components/image.dart b/packages/flutter_timeline_view/lib/src/widgets/post_components/image.dart new file mode 100644 index 0000000..6228dec --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/widgets/post_components/image.dart @@ -0,0 +1,75 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; +import 'package:flutter_timeline_view/flutter_timeline_view.dart'; +import 'package:flutter_timeline_view/src/widgets/tappable_image.dart'; + +class PostImage extends StatelessWidget { + const PostImage({ + required this.options, + required this.service, + required this.userId, + required this.post, + this.flexible = true, + this.onUpdatePost, + super.key, + }); + + final TimelineOptions options; + final TimelineService service; + final String userId; + final TimelinePost post; + final bool flexible; + + final VoidCallback? onUpdatePost; + + @override + Widget build(BuildContext context) { + var body = ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: options.doubleTapTolike + ? TappableImage( + likeAndDislikeIcon: options.likeAndDislikeIconsForDoubleTap, + post: post, + userId: userId, + onLike: ({required bool liked}) async { + TimelinePost result; + + if (!liked) { + result = await service.postService.likePost( + userId, + post, + ); + } else { + result = await service.postService.unlikePost( + userId, + post, + ); + } + + onUpdatePost?.call(); + + return result.likedBy?.contains(userId) ?? false; + }, + ) + : post.imageUrl != null + ? CachedNetworkImage( + width: double.infinity, + imageUrl: post.imageUrl!, + fit: BoxFit.fitWidth, + ) + : Image.memory( + width: double.infinity, + post.image!, + fit: BoxFit.fitWidth, + ), + ); + + if (!flexible) return body; + + return Flexible( + flex: options.postWidgetHeight != null ? 1 : 0, + child: body, + ); + } +} diff --git a/packages/flutter_timeline_view/lib/src/widgets/post_components/info.dart b/packages/flutter_timeline_view/lib/src/widgets/post_components/info.dart new file mode 100644 index 0000000..3194468 --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/widgets/post_components/info.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; +import 'package:flutter_timeline_view/flutter_timeline_view.dart'; + +class PostTitle extends StatelessWidget { + const PostTitle({ + required this.options, + required this.post, + this.isForList = false, + super.key, + }); + + final TimelineOptions options; + final TimelinePost post; + + final bool isForList; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var creatorNameStyle = (isForList + ? options.theme.textStyles.listCreatorNameStyle + : options.theme.textStyles.postCreatorNameStyle) ?? + theme.textTheme.titleSmall?.copyWith(color: Colors.black); + + return Row( + children: [ + Text( + options.nameBuilder?.call(post.creator) ?? + post.creator?.fullName ?? + options.translations.anonymousUser, + style: creatorNameStyle, + ), + const SizedBox(width: 4.0), + Text( + post.title, + style: options.theme.textStyles.listPostTitleStyle ?? + theme.textTheme.bodySmall, + ), + ], + ); + } +} diff --git a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart index 103aac9..562290f 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart @@ -2,13 +2,14 @@ // // SPDX-License-Identifier: BSD-3-Clause -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:flutter_timeline_view/src/config/timeline_options.dart'; import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart'; -import 'package:flutter_timeline_view/src/widgets/tappable_image.dart'; +import 'package:flutter_timeline_view/src/widgets/post_components/header.dart'; +import 'package:flutter_timeline_view/src/widgets/post_components/image.dart'; +import 'package:flutter_timeline_view/src/widgets/post_components/info.dart'; class TimelinePostWidget extends StatefulWidget { const TimelinePostWidget({ @@ -53,7 +54,6 @@ class TimelinePostWidget extends StatefulWidget { class _TimelinePostWidgetState extends State { @override Widget build(BuildContext context) { - var theme = Theme.of(context); var isLikedByUser = widget.post.likedBy?.contains(widget.userId) ?? false; return SizedBox( @@ -64,300 +64,171 @@ class _TimelinePostWidgetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - if (widget.post.creator != null) ...[ - InkWell( - onTap: widget.onUserTap != null - ? () => - widget.onUserTap?.call(widget.post.creator!.userId) - : null, - child: Row( - children: [ - if (widget.post.creator!.imageUrl != null) ...[ - widget.options.userAvatarBuilder?.call( - widget.post.creator!, - 28, - ) ?? - CircleAvatar( - radius: 14, - backgroundImage: CachedNetworkImageProvider( - widget.post.creator!.imageUrl!, - ), - ), - ] else ...[ - widget.options.anonymousAvatarBuilder?.call( - widget.post.creator!, - 28, - ) ?? - const CircleAvatar( - radius: 14, - child: Icon( - Icons.person, - ), - ), - ], - const SizedBox(width: 10), - Text( - widget.options.nameBuilder?.call(widget.post.creator) ?? - widget.post.creator?.fullName ?? - widget.options.translations.anonymousUser, - style: widget.options.theme.textStyles - .postCreatorTitleStyle ?? - theme.textTheme.titleSmall!.copyWith( - color: Colors.black, - ), - ), - ], - ), - ), - ], - const Spacer(), - if (widget.allowAllDeletion || - widget.post.creator?.userId == widget.userId) ...[ - PopupMenuButton( - onSelected: (value) async { - if (value == 'delete') { - await showPostDeletionConfirmationDialog( - widget.options, - context, - widget.onPostDelete, - ); - } - }, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Text( - widget.options.translations.deletePost, - style: widget - .options.theme.textStyles.deletePostStyle ?? - theme.textTheme.bodyMedium, - ), - const SizedBox(width: 8), - widget.options.theme.deleteIcon ?? - Icon( - Icons.delete, - color: widget.options.theme.iconColor, - ), - ], - ), - ), - ], - child: widget.options.theme.moreIcon ?? - Icon( - Icons.more_horiz_rounded, - color: widget.options.theme.iconColor, - ), - ), - ], - ], + PostHeader( + service: widget.service, + options: widget.options, + userId: widget.userId, + post: widget.post, + allowDeletion: widget.allowAllDeletion || + widget.post.creator?.userId == widget.userId, + onUserTap: widget.onUserTap, + onPostDelete: widget.onPostDelete, ), - // image of the post if (widget.post.imageUrl != null || widget.post.image != null) ...[ - const SizedBox(height: 8), - Flexible( - flex: widget.options.postWidgetHeight != null ? 1 : 0, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: widget.options.doubleTapTolike - ? TappableImage( - likeAndDislikeIcon: - widget.options.likeAndDislikeIconsForDoubleTap, - post: widget.post, - userId: widget.userId, - onLike: ({required bool liked}) async { - var userId = widget.userId; - - late TimelinePost result; - - if (!liked) { - result = await widget.service.postService.likePost( - userId, - widget.post, - ); - } else { - result = - await widget.service.postService.unlikePost( - userId, - widget.post, - ); - } - - return result.likedBy?.contains(userId) ?? false; - }, - ) - : widget.post.imageUrl != null - ? CachedNetworkImage( - width: double.infinity, - imageUrl: widget.post.imageUrl!, - fit: BoxFit.fitWidth, - ) - : Image.memory( - width: double.infinity, - widget.post.image!, - fit: BoxFit.fitWidth, - ), - ), - ), - ], - const SizedBox( - height: 8, - ), - // post information - if (widget.options.iconsWithValues) ...[ - Row( - children: [ - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () async { - var userId = widget.userId; - - if (!isLikedByUser) { - await widget.service.postService.likePost( - userId, - widget.post, - ); - } else { - await widget.service.postService.unlikePost( - userId, - widget.post, - ); - } - }, - icon: widget.options.theme.likeIcon ?? - Icon( - isLikedByUser - ? Icons.favorite_rounded - : Icons.favorite_outline_outlined, - color: widget.options.theme.iconColor, - size: widget.options.iconSize, - ), - ), - const SizedBox( - width: 4, - ), - Text('${widget.post.likes}'), - if (widget.post.reactionEnabled) ...[ - const SizedBox( - width: 8, - ), - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: widget.onTap, - icon: widget.options.theme.commentIcon ?? - SvgPicture.asset( - 'assets/Comment.svg', - package: 'flutter_timeline_view', - // ignore: deprecated_member_use - color: widget.options.theme.iconColor, - width: widget.options.iconSize, - height: widget.options.iconSize, - ), - ), - const SizedBox( - width: 4, - ), - Text('${widget.post.reaction}'), - ], - ], - ), - ] else ...[ - Row( - children: [ - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: - isLikedByUser ? widget.onTapUnlike : widget.onTapLike, - icon: (isLikedByUser - ? widget.options.theme.likedIcon - : widget.options.theme.likeIcon) ?? - Icon( - isLikedByUser - ? Icons.favorite_rounded - : Icons.favorite_outline, - color: widget.options.theme.iconColor, - size: widget.options.iconSize, - ), - ), - const SizedBox(width: 8), - if (widget.post.reactionEnabled) ...[ - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: widget.onTap, - icon: widget.options.theme.commentIcon ?? - SvgPicture.asset( - 'assets/Comment.svg', - package: 'flutter_timeline_view', - // ignore: deprecated_member_use - color: widget.options.theme.iconColor, - width: widget.options.iconSize, - height: widget.options.iconSize, - ), - ), - ], - ], - ), - ], - - const SizedBox( - height: 8, - ), - - if (widget.options.itemInfoBuilder != null) ...[ - widget.options.itemInfoBuilder!( - post: widget.post, - ), - ] else ...[ - _PostLikeCountText( - post: widget.post, + const SizedBox(height: 8.0), + PostImage( + service: widget.service, options: widget.options, - ), - Text.rich( - TextSpan( - text: widget.options.nameBuilder?.call(widget.post.creator) ?? - widget.post.creator?.fullName ?? - widget.options.translations.anonymousUser, - style: widget.options.theme.textStyles.listCreatorNameStyle ?? - theme.textTheme.titleSmall!.copyWith( - color: Colors.black, - ), - children: [ - TextSpan( - text: widget.post.title, - style: widget.options.theme.textStyles.listPostTitleStyle ?? - theme.textTheme.bodySmall, - ), - ], - ), - ), - const SizedBox(height: 4), - InkWell( - onTap: widget.onTap, - child: Text( - widget.options.translations.viewPost, - style: widget.options.theme.textStyles.viewPostStyle ?? - theme.textTheme.titleSmall!.copyWith( - color: const Color(0xFF8D8D8D), - ), - ), + userId: widget.userId, + post: widget.post, ), ], - if (widget.options.dividerBuilder != null) + const SizedBox(height: 8.0), + _PostLikeAndReactionsInformation( + service: widget.service, + options: widget.options, + userId: widget.userId, + post: widget.post, + isLikedByUser: isLikedByUser, + onTapComment: widget.onTap, + ), + const SizedBox(height: 8.0), + if (widget.options.itemInfoBuilder != null) ...[ + widget.options.itemInfoBuilder!(post: widget.post), + ] else ...[ + _PostInfo( + options: widget.options, + post: widget.post, + onTap: widget.onTap, + ), + ], + if (widget.options.dividerBuilder != null) ...[ widget.options.dividerBuilder!(), + ], ], ), ); } } +class _PostLikeAndReactionsInformation extends StatelessWidget { + const _PostLikeAndReactionsInformation({ + required this.service, + required this.options, + required this.userId, + required this.post, + required this.isLikedByUser, + required this.onTapComment, + }); + + final TimelineService service; + final TimelineOptions options; + final String userId; + final TimelinePost post; + final bool isLikedByUser; + final VoidCallback onTapComment; + + @override + Widget build(BuildContext context) => Row( + children: [ + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () async { + if (!isLikedByUser) { + await service.postService.likePost( + userId, + post, + ); + } else { + await service.postService.unlikePost( + userId, + post, + ); + } + }, + icon: options.theme.likeIcon ?? + Icon( + isLikedByUser + ? Icons.favorite_rounded + : Icons.favorite_outline_outlined, + color: options.theme.iconColor, + size: options.iconSize, + ), + ), + const SizedBox(width: 4.0), + if (options.iconsWithValues) ...[ + Text('${post.likes}'), + ], + if (post.reactionEnabled) ...[ + const SizedBox(width: 8.0), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: onTapComment, + icon: options.theme.commentIcon ?? + SvgPicture.asset( + 'assets/Comment.svg', + package: 'flutter_timeline_view', + // ignore: deprecated_member_use + color: options.theme.iconColor, + width: options.iconSize, + height: options.iconSize, + ), + ), + if (options.iconsWithValues) ...[ + const SizedBox(width: 4.0), + Text('${post.reaction}'), + ], + ], + ], + ); +} + +class _PostInfo extends StatelessWidget { + const _PostInfo({ + required this.options, + required this.post, + required this.onTap, + }); + + final TimelineOptions options; + final TimelinePost post; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _PostLikeCountText( + post: post, + options: options, + ), + const SizedBox(height: 4.0), + PostTitle( + options: options, + post: post, + isForList: true, + ), + const SizedBox(height: 4.0), + InkWell( + onTap: onTap, + child: Text( + options.translations.viewPost, + style: options.theme.textStyles.viewPostStyle ?? + theme.textTheme.titleSmall!.copyWith( + color: const Color(0xFF8D8D8D), + ), + ), + ), + ], + ); + } +} + class _PostLikeCountText extends StatelessWidget { const _PostLikeCountText({ required this.post,