mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 10:33:44 +02:00
feat: add like functionality
This commit is contained in:
parent
4f2aba4cc4
commit
8792079fa4
7 changed files with 221 additions and 108 deletions
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -148,41 +148,73 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
),
|
||||
],
|
||||
// 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,
|
||||
|
|
|
@ -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) {
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue