diff --git a/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart b/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart index 9b8c3ae..1e08d71 100644 --- a/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart +++ b/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart @@ -26,6 +26,7 @@ class TimelinePost { this.reactions, this.imageUrl, this.image, + this.data = const {}, }); factory TimelinePost.fromJson(String id, Map json) => @@ -50,6 +51,7 @@ class TimelinePost { .toList(), createdAt: DateTime.parse(json['created_at'] as String), reactionEnabled: json['reaction_enabled'] as bool, + data: json['data'] ?? {}, ); /// The unique identifier of the post. @@ -94,6 +96,9 @@ class TimelinePost { /// If reacting is enabled on the post. final bool reactionEnabled; + /// Option to add extra data to a timelinepost that won't be shown anywhere + final Map data; + TimelinePost copyWith({ String? id, String? creatorId, @@ -109,6 +114,7 @@ class TimelinePost { List? reactions, DateTime? createdAt, bool? reactionEnabled, + Map? data, }) => TimelinePost( id: id ?? this.id, @@ -125,6 +131,7 @@ class TimelinePost { reactions: reactions ?? this.reactions, createdAt: createdAt ?? this.createdAt, reactionEnabled: reactionEnabled ?? this.reactionEnabled, + data: data ?? this.data, ); Map toJson() => { @@ -140,5 +147,6 @@ class TimelinePost { 'reactions': reactions?.map((e) => e.toJson()).toList() ?? [], 'created_at': createdAt.toIso8601String(), 'reaction_enabled': reactionEnabled, + 'data': data, }; } diff --git a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart index 21792be..42e390a 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -24,6 +24,8 @@ class TimelineOptions { this.buttonBuilder, this.textInputBuilder, this.userAvatarBuilder, + this.anonymousAvatarBuilder, + this.nameBuilder, }); /// Theming options for the timeline @@ -56,6 +58,12 @@ class TimelineOptions { final UserAvatarBuilder? userAvatarBuilder; + /// When the imageUrl is null this anonymousAvatarBuilder will be used + /// You can use it to display a default avatarW + final UserAvatarBuilder? anonymousAvatarBuilder; + + final String Function(TimelinePosterUserModel?)? nameBuilder; + /// ImagePickerTheme can be used to change the UI of the /// Image Picker Widget to change the text/icons to your liking. final ImagePickerTheme imagePickerTheme; @@ -78,7 +86,7 @@ typedef TextInputBuilder = Widget Function( String hintText, ); -typedef UserAvatarBuilder = Widget Function( +typedef UserAvatarBuilder = Widget? Function( TimelinePosterUserModel user, double size, ); diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_post_creation_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_post_creation_screen.dart index 19c639f..ae01025 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_post_creation_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_post_creation_screen.dart @@ -123,6 +123,7 @@ class _TimelinePostCreationScreenState height: 100, child: TextField( controller: contentController, + textCapitalization: TextCapitalization.sentences, expands: true, maxLines: null, minLines: null, 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 8e2f547..6082096 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 @@ -153,16 +153,28 @@ class _TimelinePostScreenState extends State { post.creator!.imageUrl!, ), ), + ] else ...[ + widget.options.anonymousAvatarBuilder?.call( + post.creator!, + 40, + ) ?? + const CircleAvatar( + radius: 20, + child: Icon( + Icons.person, + ), + ), ], const SizedBox(width: 10), - if (post.creator!.fullName != null) ...[ - Text( - post.creator!.fullName!, - style: widget.options.theme.textStyles - .postCreatorTitleStyle ?? - theme.textTheme.titleMedium, - ), - ], + Text( + widget.options.nameBuilder + ?.call(post.creator) ?? + post.creator?.fullName ?? + widget.options.translations.anonymousUser, + style: widget.options.theme.textStyles + .postCreatorTitleStyle ?? + theme.textTheme.titleMedium, + ), ], ), ), @@ -206,63 +218,67 @@ class _TimelinePostScreenState extends State { ), ], ), - const SizedBox(height: 8), // image of the post if (post.imageUrl != null) ...[ - CachedNetworkImage( - imageUrl: post.imageUrl!, - width: double.infinity, - fit: BoxFit.fitHeight, + const SizedBox(height: 8), + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: CachedNetworkImage( + width: double.infinity, + imageUrl: post.imageUrl!, + fit: BoxFit.fitHeight, + ), ), ], + const SizedBox( + height: 8, + ), // post information - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Row( - children: [ - if (post.likedBy?.contains(widget.userId) ?? false) ...[ - InkWell( - onTap: () async { - updatePost( - await widget.service.unlikePost( - widget.userId, - post, - ), - ); - }, - child: widget.options.theme.likedIcon ?? - Icon( - Icons.thumb_up_rounded, - color: widget.options.theme.iconColor, - ), - ), - ] else ...[ - InkWell( - onTap: () async { - updatePost( - await widget.service.likePost( - widget.userId, - post, - ), - ); - }, - child: widget.options.theme.likeIcon ?? - Icon( - Icons.thumb_up_alt_outlined, - color: widget.options.theme.iconColor, - ), - ), - ], - const SizedBox(width: 8), - if (post.reactionEnabled) - widget.options.theme.commentIcon ?? + Row( + children: [ + if (post.likedBy?.contains(widget.userId) ?? false) ...[ + InkWell( + onTap: () async { + updatePost( + await widget.service.unlikePost( + widget.userId, + post, + ), + ); + }, + child: widget.options.theme.likedIcon ?? Icon( - Icons.chat_bubble_outline_rounded, + Icons.thumb_up_rounded, color: widget.options.theme.iconColor, ), + ), + ] else ...[ + InkWell( + onTap: () async { + updatePost( + await widget.service.likePost( + widget.userId, + post, + ), + ); + }, + child: widget.options.theme.likeIcon ?? + Icon( + Icons.thumb_up_alt_outlined, + color: widget.options.theme.iconColor, + ), + ), ], - ), + const SizedBox(width: 8), + if (post.reactionEnabled) + widget.options.theme.commentIcon ?? + Icon( + Icons.chat_bubble_outline_rounded, + color: widget.options.theme.iconColor, + ), + ], ), + const SizedBox(height: 8), Text( '${post.likes} ${widget.options.translations.likesTitle}', style: widget @@ -272,7 +288,8 @@ class _TimelinePostScreenState extends State { const SizedBox(height: 4), Text.rich( TextSpan( - text: post.creator?.fullName ?? + text: widget.options.nameBuilder?.call(post.creator) ?? + post.creator?.fullName ?? widget.options.translations.anonymousUser, style: widget .options.theme.textStyles.postCreatorNameStyle ?? @@ -287,9 +304,8 @@ class _TimelinePostScreenState extends State { ), ], ), - overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 4), + const SizedBox(height: 20), Html( data: post.content, style: { @@ -300,15 +316,20 @@ class _TimelinePostScreenState extends State { '#': Style( maxLines: 3, textOverflow: TextOverflow.ellipsis, + padding: HtmlPaddings.zero, + margin: Margins.zero, ), 'H1': Style( - margin: Margins.all(0), + padding: HtmlPaddings.zero, + margin: Margins.zero, ), 'H2': Style( - margin: Margins.all(0), + padding: HtmlPaddings.zero, + margin: Margins.zero, ), 'H3': Style( - margin: Margins.all(0), + padding: HtmlPaddings.zero, + margin: Margins.zero, ), }, ), @@ -319,7 +340,7 @@ class _TimelinePostScreenState extends State { '${timeFormat.format(post.createdAt)}', style: theme.textTheme.bodySmall, ), - const SizedBox(height: 12), + const SizedBox(height: 20), if (post.reactionEnabled) ...[ Text( widget.options.translations.commentsTitle, @@ -381,6 +402,17 @@ class _TimelinePostScreenState extends State { reaction.creator!.imageUrl!, ), ), + ] else ...[ + widget.options.anonymousAvatarBuilder?.call( + reaction.creator!, + 25, + ) ?? + const CircleAvatar( + radius: 20, + child: Icon( + Icons.person, + ), + ), ], const SizedBox(width: 10), if (reaction.imageUrl != null) ...[ @@ -389,7 +421,9 @@ class _TimelinePostScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - reaction.creator?.fullName ?? + widget.options.nameBuilder + ?.call(post.creator) ?? + reaction.creator?.fullName ?? widget.options.translations .anonymousUser, style: theme.textTheme.titleSmall, @@ -408,7 +442,9 @@ class _TimelinePostScreenState extends State { Expanded( child: Text.rich( TextSpan( - text: reaction.creator?.fullName ?? + text: widget.options.nameBuilder + ?.call(post.creator) ?? + reaction.creator?.fullName ?? widget .options.translations.anonymousUser, style: theme.textTheme.titleSmall, 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 ffdcd58..88a21e6 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 @@ -68,16 +68,27 @@ class TimelinePostWidget extends StatelessWidget { post.creator!.imageUrl!, ), ), + ] else ...[ + options.anonymousAvatarBuilder?.call( + post.creator!, + 40, + ) ?? + const CircleAvatar( + radius: 20, + child: Icon( + Icons.person, + ), + ), ], const SizedBox(width: 10), - if (post.creator!.fullName != null) ...[ - Text( - post.creator!.fullName!, - style: options.theme.textStyles - .listPostCreatorTitleStyle ?? - theme.textTheme.titleMedium, - ), - ], + Text( + options.nameBuilder?.call(post.creator) ?? + post.creator?.fullName ?? + options.translations.anonymousUser, + style: + options.theme.textStyles.postCreatorTitleStyle ?? + theme.textTheme.titleMedium, + ), ], ), ), @@ -118,52 +129,59 @@ class TimelinePostWidget extends StatelessWidget { ), ], ), - const SizedBox(height: 8), // image of the post if (post.imageUrl != null) ...[ + const SizedBox(height: 8), Flexible( flex: height != null ? 1 : 0, - child: CachedNetworkImage( - imageUrl: post.imageUrl!, - width: double.infinity, - fit: BoxFit.fitWidth, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: CachedNetworkImage( + width: double.infinity, + imageUrl: post.imageUrl!, + fit: BoxFit.fitWidth, + ), ), ), ], + const SizedBox( + height: 8, + ), // post information - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Row( - children: [ - if (post.likedBy?.contains(userId) ?? false) ...[ - InkWell( - onTap: onTapUnlike, - child: options.theme.likedIcon ?? - Icon( - Icons.thumb_up_rounded, - color: options.theme.iconColor, - ), - ), - ] else ...[ - InkWell( - onTap: onTapLike, - child: options.theme.likeIcon ?? - Icon( - Icons.thumb_up_alt_outlined, - color: options.theme.iconColor, - ), - ), - ], - const SizedBox(width: 8), - if (post.reactionEnabled) - options.theme.commentIcon ?? + Row( + children: [ + if (post.likedBy?.contains(userId) ?? false) ...[ + InkWell( + onTap: onTapUnlike, + child: options.theme.likedIcon ?? Icon( - Icons.chat_bubble_outline_rounded, + Icons.thumb_up_rounded, color: options.theme.iconColor, ), + ), + ] else ...[ + InkWell( + onTap: onTapLike, + child: options.theme.likeIcon ?? + Icon( + Icons.thumb_up_alt_outlined, + color: options.theme.iconColor, + ), + ), ], - ), + const SizedBox(width: 8), + if (post.reactionEnabled) + options.theme.commentIcon ?? + Icon( + Icons.chat_bubble_outline_rounded, + color: options.theme.iconColor, + ), + ], ), + const SizedBox( + height: 8, + ), + Text( '${post.likes} ${options.translations.likesTitle}', style: options.theme.textStyles.listPostLikeTitleAndAmount ?? @@ -172,7 +190,8 @@ class TimelinePostWidget extends StatelessWidget { const SizedBox(height: 4), Text.rich( TextSpan( - text: post.creator?.fullName ?? + text: options.nameBuilder?.call(post.creator) ?? + post.creator?.fullName ?? options.translations.anonymousUser, style: options.theme.textStyles.listCreatorNameStyle ?? theme.textTheme.titleSmall, @@ -185,8 +204,8 @@ class TimelinePostWidget extends StatelessWidget { ), ], ), - overflow: TextOverflow.ellipsis, ), + const SizedBox(height: 4), Text( options.translations.viewPost, style: options.theme.textStyles.viewPostStyle ??