feat: add like functionality

This commit is contained in:
Freek van de Ven 2023-11-21 12:33:46 +01:00
parent 4f2aba4cc4
commit 8792079fa4
7 changed files with 221 additions and 108 deletions

View file

@ -93,43 +93,43 @@ class FirebaseTimelineService implements TimelineService {
.toList();
@override
Future<void> likePost(String userId, TimelinePost post) {
Future<TimelinePost> 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<void> unlikePost(String userId, TimelinePost post) {
Future<TimelinePost> 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;

View file

@ -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<dynamic>?)?.cast<String>(),
likedBy: (json['liked_by'] as List<dynamic>?)?.cast<String>() ?? [],
reaction: json['reaction'] as int,
reactions: (json['reactions'] as List<dynamic>?)
?.map(

View file

@ -18,6 +18,6 @@ abstract class TimelineService {
TimelinePostReaction reaction, {
Uint8List image,
});
Future<void> likePost(String userId, TimelinePost post);
Future<void> unlikePost(String userId, TimelinePost post);
Future<TimelinePost> likePost(String userId, TimelinePost post);
Future<TimelinePost> unlikePost(String userId, TimelinePost post);
}

View file

@ -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;
}

View file

@ -148,41 +148,73 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
),
],
// post information
Row(
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
// like icon
IconButton(
onPressed: () {},
icon: const Icon(Icons.thumb_up_rounded),
if (post.likedBy?.contains(widget.userId) ?? false) ...[
InkWell(
onTap: () async {
updatePost(
await widget.service.unlikePost(
widget.userId,
post,
),
// comment icon
IconButton(
onPressed: () {},
icon: const Icon(
Icons.chat_bubble_outline_rounded,
);
},
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 ?? '',
const SizedBox(height: 4),
Text.rich(
TextSpan(
text: post.creator?.fullName ??
widget.options.translations.anonymousUser,
style: theme.textTheme.titleSmall,
),
const SizedBox(width: 8),
Text(
post.title,
children: [
const TextSpan(text: ' '),
TextSpan(
text: post.title,
style: theme.textTheme.bodyMedium,
overflow: TextOverflow.fade,
),
],
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
post.content,
style: theme.textTheme.bodyMedium,

View file

@ -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<TimelineScreen> {
late ScrollController controller;
late List<TimelinePost> posts;
bool isLoading = true;
@override
void initState() {
super.initState();
controller = widget.controller ?? ScrollController();
unawaited(loadPosts());
}
Future<void> 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) {
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: [
for (var post in snapshot.data!)
if (widget.timelineCategoryFilter == null ||
post.category == widget.timelineCategoryFilter)
Padding(
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),
),
),
],
),
)
.toList(),
),
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
);
}

View file

@ -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,41 +76,58 @@ class TimelinePostWidget extends StatelessWidget {
),
],
// post information
Row(
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
// like icon
IconButton(
onPressed: () {},
icon: const Icon(Icons.thumb_up_rounded),
if (post.likedBy?.contains(userId) ?? false) ...[
InkWell(
onTap: onTapUnlike,
child: options.theme.likedIcon ??
Icon(
Icons.thumb_up_rounded,
color: options.theme.iconColor,
),
// comment icon
IconButton(
onPressed: () {},
icon: const Icon(
Icons.chat_bubble_outline_rounded,
),
] 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 ?? '',
const SizedBox(height: 4),
Text.rich(
TextSpan(
text: post.creator?.fullName ??
options.translations.anonymousUser,
style: theme.textTheme.titleSmall,
),
const SizedBox(width: 8),
Text(
post.title,
children: [
const TextSpan(text: ' '),
TextSpan(
text: post.title,
style: theme.textTheme.bodyMedium,
overflow: TextOverflow.fade,
),
],
),
overflow: TextOverflow.ellipsis,
),
Text(
options.translations.viewPost,
style: theme.textTheme.bodySmall,