mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 18:43:45 +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();
|
.toList();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> likePost(String userId, TimelinePost post) {
|
Future<TimelinePost> likePost(String userId, TimelinePost post) async {
|
||||||
// update the post with the new like
|
// update the post with the new like
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
likes: post.likes + 1,
|
||||||
|
likedBy: post.likedBy?..add(userId),
|
||||||
|
);
|
||||||
_posts = _posts
|
_posts = _posts
|
||||||
.map(
|
.map(
|
||||||
(p) => (p.id == post.id)
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
? p.copyWith(
|
|
||||||
likes: p.likes + 1,
|
|
||||||
likedBy: p.likedBy?..add(userId),
|
|
||||||
)
|
|
||||||
: p,
|
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||||
return postRef.update({
|
await postRef.update({
|
||||||
'likes': FieldValue.increment(1),
|
'likes': FieldValue.increment(1),
|
||||||
'liked_by': FieldValue.arrayUnion([userId]),
|
'liked_by': FieldValue.arrayUnion([userId]),
|
||||||
});
|
});
|
||||||
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> unlikePost(String userId, TimelinePost post) {
|
Future<TimelinePost> unlikePost(String userId, TimelinePost post) async {
|
||||||
// update the post with the new like
|
// update the post with the new like
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
likes: post.likes - 1,
|
||||||
|
likedBy: post.likedBy?..remove(userId),
|
||||||
|
);
|
||||||
_posts = _posts
|
_posts = _posts
|
||||||
.map(
|
.map(
|
||||||
(p) => (p.id == post.id)
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
? p.copyWith(
|
|
||||||
likes: p.likes - 1,
|
|
||||||
likedBy: p.likedBy?..remove(userId),
|
|
||||||
)
|
|
||||||
: p,
|
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||||
return postRef.update({
|
await postRef.update({
|
||||||
'likes': FieldValue.increment(-1),
|
'likes': FieldValue.increment(-1),
|
||||||
'liked_by': FieldValue.arrayRemove([userId]),
|
'liked_by': FieldValue.arrayRemove([userId]),
|
||||||
});
|
});
|
||||||
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -159,8 +159,6 @@ class FirebaseTimelineService implements TimelineService {
|
||||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||||
await postRef.update({
|
await postRef.update({
|
||||||
'reaction': FieldValue.increment(1),
|
'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()]),
|
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]),
|
||||||
});
|
});
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
|
|
|
@ -37,7 +37,7 @@ class TimelinePost {
|
||||||
imageUrl: json['image_url'] as String?,
|
imageUrl: json['image_url'] as String?,
|
||||||
content: json['content'] as String,
|
content: json['content'] as String,
|
||||||
likes: json['likes'] as int,
|
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,
|
reaction: json['reaction'] as int,
|
||||||
reactions: (json['reactions'] as List<dynamic>?)
|
reactions: (json['reactions'] as List<dynamic>?)
|
||||||
?.map(
|
?.map(
|
||||||
|
|
|
@ -18,6 +18,6 @@ abstract class TimelineService {
|
||||||
TimelinePostReaction reaction, {
|
TimelinePostReaction reaction, {
|
||||||
Uint8List image,
|
Uint8List image,
|
||||||
});
|
});
|
||||||
Future<void> likePost(String userId, TimelinePost post);
|
Future<TimelinePost> likePost(String userId, TimelinePost post);
|
||||||
Future<void> unlikePost(String userId, TimelinePost post);
|
Future<TimelinePost> unlikePost(String userId, TimelinePost post);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,23 @@ import 'package:flutter/material.dart';
|
||||||
class TimelineTheme {
|
class TimelineTheme {
|
||||||
const TimelineTheme({
|
const TimelineTheme({
|
||||||
this.iconColor,
|
this.iconColor,
|
||||||
|
this.likeIcon,
|
||||||
|
this.commentIcon,
|
||||||
|
this.likedIcon,
|
||||||
|
this.sendIcon,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Color? iconColor;
|
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
|
// post information
|
||||||
Row(
|
Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
// like icon
|
child: Row(
|
||||||
IconButton(
|
children: [
|
||||||
onPressed: () {},
|
if (post.likedBy?.contains(widget.userId) ?? false) ...[
|
||||||
icon: const Icon(Icons.thumb_up_rounded),
|
InkWell(
|
||||||
),
|
onTap: () async {
|
||||||
// comment icon
|
updatePost(
|
||||||
IconButton(
|
await widget.service.unlikePost(
|
||||||
onPressed: () {},
|
widget.userId,
|
||||||
icon: const Icon(
|
post,
|
||||||
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(
|
Text(
|
||||||
'${post.likes} ${widget.options.translations.likesTitle}',
|
'${post.likes} ${widget.options.translations.likesTitle}',
|
||||||
style: theme.textTheme.titleSmall,
|
style: theme.textTheme.titleSmall,
|
||||||
),
|
),
|
||||||
Row(
|
const SizedBox(height: 4),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
Text.rich(
|
||||||
children: [
|
TextSpan(
|
||||||
Text(
|
text: post.creator?.fullName ??
|
||||||
post.creator?.fullName ?? '',
|
widget.options.translations.anonymousUser,
|
||||||
style: theme.textTheme.titleSmall,
|
style: theme.textTheme.titleSmall,
|
||||||
),
|
children: [
|
||||||
const SizedBox(width: 8),
|
const TextSpan(text: ' '),
|
||||||
Text(
|
TextSpan(
|
||||||
post.title,
|
text: post.title,
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
overflow: TextOverflow.fade,
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
post.content,
|
post.content,
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.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/config/timeline_options.dart';
|
||||||
|
@ -20,6 +22,7 @@ class TimelineScreen extends StatefulWidget {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// The user id of the current user
|
||||||
final String userId;
|
final String userId;
|
||||||
|
|
||||||
final TimelineService service;
|
final TimelineService service;
|
||||||
|
@ -42,40 +45,80 @@ class TimelineScreen extends StatefulWidget {
|
||||||
|
|
||||||
class _TimelineScreenState extends State<TimelineScreen> {
|
class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
late ScrollController controller;
|
late ScrollController controller;
|
||||||
|
late List<TimelinePost> posts;
|
||||||
|
bool isLoading = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
controller = widget.controller ?? ScrollController();
|
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
|
@override
|
||||||
Widget build(BuildContext context) => FutureBuilder(
|
Widget build(BuildContext context) {
|
||||||
// ignore: discarded_futures
|
if (isLoading) {
|
||||||
future: widget.service.fetchPosts(widget.timelineCategoryFilter),
|
// Show loading indicator while data is being fetched
|
||||||
builder: (context, snapshot) {
|
return const Center(child: CircularProgressIndicator());
|
||||||
if (snapshot.hasData && snapshot.data != null) {
|
}
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Column(
|
// Build the list of posts
|
||||||
children: [
|
return SingleChildScrollView(
|
||||||
for (var post in snapshot.data!)
|
controller: controller,
|
||||||
if (widget.timelineCategoryFilter == null ||
|
child: Column(
|
||||||
post.category == widget.timelineCategoryFilter)
|
children: posts
|
||||||
Padding(
|
.map(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
(post) => Padding(
|
||||||
child: TimelinePostWidget(
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
options: widget.options,
|
child: TimelinePostWidget(
|
||||||
post: post,
|
userId: widget.userId,
|
||||||
height: widget.timelinePostHeight,
|
options: widget.options,
|
||||||
onTap: () => widget.onPostTap.call(post),
|
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 {
|
.toList(),
|
||||||
return const Center(child: CircularProgressIndicator());
|
),
|
||||||
}
|
);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,25 @@ import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||||
|
|
||||||
class TimelinePostWidget extends StatelessWidget {
|
class TimelinePostWidget extends StatelessWidget {
|
||||||
const TimelinePostWidget({
|
const TimelinePostWidget({
|
||||||
|
required this.userId,
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.post,
|
required this.post,
|
||||||
required this.height,
|
required this.height,
|
||||||
this.onTap,
|
required this.onTapLike,
|
||||||
|
required this.onTapUnlike,
|
||||||
|
required this.onTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// The user id of the current user
|
||||||
|
final String userId;
|
||||||
final TimelineOptions options;
|
final TimelineOptions options;
|
||||||
|
|
||||||
final TimelinePost post;
|
final TimelinePost post;
|
||||||
final double height;
|
final double height;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback onTap;
|
||||||
|
final VoidCallback onTapLike;
|
||||||
|
final VoidCallback onTapUnlike;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -69,40 +76,57 @@ class TimelinePostWidget extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
// post information
|
// post information
|
||||||
Row(
|
Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
// like icon
|
child: Row(
|
||||||
IconButton(
|
children: [
|
||||||
onPressed: () {},
|
if (post.likedBy?.contains(userId) ?? false) ...[
|
||||||
icon: const Icon(Icons.thumb_up_rounded),
|
InkWell(
|
||||||
),
|
onTap: onTapUnlike,
|
||||||
// comment icon
|
child: options.theme.likedIcon ??
|
||||||
IconButton(
|
Icon(
|
||||||
onPressed: () {},
|
Icons.thumb_up_rounded,
|
||||||
icon: const Icon(
|
color: options.theme.iconColor,
|
||||||
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(
|
Text(
|
||||||
'${post.likes} ${options.translations.likesTitle}',
|
'${post.likes} ${options.translations.likesTitle}',
|
||||||
style: theme.textTheme.titleSmall,
|
style: theme.textTheme.titleSmall,
|
||||||
),
|
),
|
||||||
Row(
|
const SizedBox(height: 4),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
Text.rich(
|
||||||
children: [
|
TextSpan(
|
||||||
Text(
|
text: post.creator?.fullName ??
|
||||||
post.creator?.fullName ?? '',
|
options.translations.anonymousUser,
|
||||||
style: theme.textTheme.titleSmall,
|
style: theme.textTheme.titleSmall,
|
||||||
),
|
children: [
|
||||||
const SizedBox(width: 8),
|
const TextSpan(text: ' '),
|
||||||
Text(
|
TextSpan(
|
||||||
post.title,
|
text: post.title,
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
overflow: TextOverflow.fade,
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
options.translations.viewPost,
|
options.translations.viewPost,
|
||||||
|
|
Loading…
Reference in a new issue