diff --git a/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart b/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart index 54bb9b2..77ef4e4 100644 --- a/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart +++ b/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart @@ -93,43 +93,43 @@ class FirebaseTimelineService implements TimelineService { .toList(); @override - Future likePost(String userId, TimelinePost post) { + Future likePost(String userId, TimelinePost post) async { // update the post with the new like + var updatedPost = post.copyWith( + likes: post.likes + 1, + likedBy: post.likedBy?..add(userId), + ); _posts = _posts .map( - (p) => (p.id == post.id) - ? p.copyWith( - likes: p.likes + 1, - likedBy: p.likedBy?..add(userId), - ) - : p, + (p) => p.id == post.id ? updatedPost : p, ) .toList(); var postRef = _db.collection(_options.timelineCollectionName).doc(post.id); - return postRef.update({ + await postRef.update({ 'likes': FieldValue.increment(1), 'liked_by': FieldValue.arrayUnion([userId]), }); + return updatedPost; } @override - Future unlikePost(String userId, TimelinePost post) { + Future unlikePost(String userId, TimelinePost post) async { // update the post with the new like + var updatedPost = post.copyWith( + likes: post.likes - 1, + likedBy: post.likedBy?..remove(userId), + ); _posts = _posts .map( - (p) => (p.id == post.id) - ? p.copyWith( - likes: p.likes - 1, - likedBy: p.likedBy?..remove(userId), - ) - : p, + (p) => p.id == post.id ? updatedPost : p, ) .toList(); var postRef = _db.collection(_options.timelineCollectionName).doc(post.id); - return postRef.update({ + await postRef.update({ 'likes': FieldValue.increment(-1), 'liked_by': FieldValue.arrayRemove([userId]), }); + return updatedPost; } @override @@ -159,8 +159,6 @@ class FirebaseTimelineService implements TimelineService { var postRef = _db.collection(_options.timelineCollectionName).doc(post.id); await postRef.update({ 'reaction': FieldValue.increment(1), - // 'reactions' is a map of reactions, so we need to add the new reaction - // to the map 'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]), }); return updatedPost; 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 68780e2..213f728 100644 --- a/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart +++ b/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart @@ -37,7 +37,7 @@ class TimelinePost { imageUrl: json['image_url'] as String?, content: json['content'] as String, likes: json['likes'] as int, - likedBy: (json['liked_by'] as List?)?.cast(), + likedBy: (json['liked_by'] as List?)?.cast() ?? [], reaction: json['reaction'] as int, reactions: (json['reactions'] as List?) ?.map( diff --git a/packages/flutter_timeline_interface/lib/src/services/timeline_service.dart b/packages/flutter_timeline_interface/lib/src/services/timeline_service.dart index 8791e1c..8305d1b 100644 --- a/packages/flutter_timeline_interface/lib/src/services/timeline_service.dart +++ b/packages/flutter_timeline_interface/lib/src/services/timeline_service.dart @@ -18,6 +18,6 @@ abstract class TimelineService { TimelinePostReaction reaction, { Uint8List image, }); - Future likePost(String userId, TimelinePost post); - Future unlikePost(String userId, TimelinePost post); + Future likePost(String userId, TimelinePost post); + Future unlikePost(String userId, TimelinePost post); } diff --git a/packages/flutter_timeline_view/lib/src/config/timeline_theme.dart b/packages/flutter_timeline_view/lib/src/config/timeline_theme.dart index 57d8235..85d4cea 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_theme.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_theme.dart @@ -8,7 +8,23 @@ import 'package:flutter/material.dart'; class TimelineTheme { const TimelineTheme({ this.iconColor, + this.likeIcon, + this.commentIcon, + this.likedIcon, + this.sendIcon, }); final Color? iconColor; + + /// The icon to display when the post is not yet liked + final Widget? likeIcon; + + /// The icon to display to indicate that a post has comments enabled + final Widget? commentIcon; + + /// The icon to display when the post is liked + final Widget? likedIcon; + + /// The icon to display to submit a comment + final Widget? sendIcon; } 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 5577137..2526115 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 @@ -148,41 +148,73 @@ class _TimelinePostScreenState extends State { ), ], // post information - Row( - children: [ - // like icon - IconButton( - onPressed: () {}, - icon: const Icon(Icons.thumb_up_rounded), - ), - // comment icon - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.chat_bubble_outline_rounded, - ), - ), - ], + 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 ?? + const Icon( + Icons.chat_bubble_outline_rounded, + ), + ], + ), ), Text( '${post.likes} ${widget.options.translations.likesTitle}', style: theme.textTheme.titleSmall, ), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - post.creator?.fullName ?? '', - style: theme.textTheme.titleSmall, - ), - const SizedBox(width: 8), - Text( - post.title, - style: theme.textTheme.bodyMedium, - overflow: TextOverflow.fade, - ), - ], + const SizedBox(height: 4), + Text.rich( + TextSpan( + text: post.creator?.fullName ?? + widget.options.translations.anonymousUser, + style: theme.textTheme.titleSmall, + children: [ + const TextSpan(text: ' '), + TextSpan( + text: post.title, + style: theme.textTheme.bodyMedium, + ), + ], + ), + overflow: TextOverflow.ellipsis, ), + const SizedBox(height: 4), Text( post.content, style: theme.textTheme.bodyMedium, diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart index 3282979..6d01f82 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: BSD-3-Clause +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:flutter_timeline_view/src/config/timeline_options.dart'; @@ -20,6 +22,7 @@ class TimelineScreen extends StatefulWidget { super.key, }); + /// The user id of the current user final String userId; final TimelineService service; @@ -42,40 +45,80 @@ class TimelineScreen extends StatefulWidget { class _TimelineScreenState extends State { late ScrollController controller; + late List posts; + bool isLoading = true; @override void initState() { super.initState(); controller = widget.controller ?? ScrollController(); + unawaited(loadPosts()); + } + + Future loadPosts() async { + try { + // Fetching posts from the service + var fetchedPosts = + await widget.service.fetchPosts(widget.timelineCategoryFilter); + setState(() { + posts = fetchedPosts; + isLoading = false; + }); + } on Exception catch (e) { + // Handle errors here + debugPrint('Error loading posts: $e'); + setState(() { + isLoading = false; + }); + } + } + + void updatePostInList(TimelinePost updatedPost) { + if (posts.any((p) => p.id == updatedPost.id)) + setState(() { + posts = posts + .map((p) => (p.id == updatedPost.id) ? updatedPost : p) + .toList(); + }); + else { + setState(() { + posts = [updatedPost, ...posts]; + }); + } } @override - Widget build(BuildContext context) => FutureBuilder( - // ignore: discarded_futures - future: widget.service.fetchPosts(widget.timelineCategoryFilter), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data != null) { - return SingleChildScrollView( - child: Column( - children: [ - for (var post in snapshot.data!) - if (widget.timelineCategoryFilter == null || - post.category == widget.timelineCategoryFilter) - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: TimelinePostWidget( - options: widget.options, - post: post, - height: widget.timelinePostHeight, - onTap: () => widget.onPostTap.call(post), - ), - ), - ], + Widget build(BuildContext context) { + if (isLoading) { + // Show loading indicator while data is being fetched + return const Center(child: CircularProgressIndicator()); + } + + // Build the list of posts + return SingleChildScrollView( + controller: controller, + child: Column( + children: posts + .map( + (post) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: TimelinePostWidget( + userId: widget.userId, + options: widget.options, + post: post, + height: widget.timelinePostHeight, + onTap: () => widget.onPostTap.call(post), + onTapLike: () async => updatePostInList( + await widget.service.likePost(widget.userId, post), + ), + onTapUnlike: () async => updatePostInList( + await widget.service.unlikePost(widget.userId, post), + ), + ), ), - ); - } else { - return const Center(child: CircularProgressIndicator()); - } - }, - ); + ) + .toList(), + ), + ); + } } 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 277bec3..49be26a 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 @@ -5,18 +5,25 @@ import 'package:flutter_timeline_view/src/config/timeline_options.dart'; class TimelinePostWidget extends StatelessWidget { const TimelinePostWidget({ + required this.userId, required this.options, required this.post, required this.height, - this.onTap, + required this.onTapLike, + required this.onTapUnlike, + required this.onTap, super.key, }); + /// The user id of the current user + final String userId; final TimelineOptions options; final TimelinePost post; final double height; - final VoidCallback? onTap; + final VoidCallback onTap; + final VoidCallback onTapLike; + final VoidCallback onTapUnlike; @override Widget build(BuildContext context) { @@ -69,40 +76,57 @@ class TimelinePostWidget extends StatelessWidget { ), ], // post information - Row( - children: [ - // like icon - IconButton( - onPressed: () {}, - icon: const Icon(Icons.thumb_up_rounded), - ), - // comment icon - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.chat_bubble_outline_rounded, - ), - ), - ], + 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 ?? + const Icon( + Icons.chat_bubble_outline_rounded, + ), + ], + ), ), Text( '${post.likes} ${options.translations.likesTitle}', style: theme.textTheme.titleSmall, ), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - post.creator?.fullName ?? '', - style: theme.textTheme.titleSmall, - ), - const SizedBox(width: 8), - Text( - post.title, - style: theme.textTheme.bodyMedium, - overflow: TextOverflow.fade, - ), - ], + const SizedBox(height: 4), + Text.rich( + TextSpan( + text: post.creator?.fullName ?? + options.translations.anonymousUser, + style: theme.textTheme.titleSmall, + children: [ + const TextSpan(text: ' '), + TextSpan( + text: post.title, + style: theme.textTheme.bodyMedium, + ), + ], + ), + overflow: TextOverflow.ellipsis, ), Text( options.translations.viewPost,