feat: allow post deletion and add changenotifiersystem

This commit is contained in:
Freek van de Ven 2023-11-21 20:02:01 +01:00
parent ca4ec03002
commit 753ecc039e
7 changed files with 156 additions and 90 deletions

View file

@ -7,11 +7,12 @@ import 'dart:typed_data';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_timeline_firebase/src/config/firebase_timeline_options.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
import 'package:uuid/uuid.dart';
class FirebaseTimelineService implements TimelineService {
class FirebaseTimelineService with ChangeNotifier implements TimelineService {
FirebaseTimelineService({
required TimelineUserService userService,
FirebaseApp? app,
@ -41,15 +42,18 @@ class FirebaseTimelineService implements TimelineService {
var updatedPost = post.copyWith(imageUrl: imageUrl, id: postId);
var postRef =
_db.collection(_options.timelineCollectionName).doc(updatedPost.id);
_posts.add(updatedPost);
await postRef.set(updatedPost.toJson());
_posts.add(updatedPost);
notifyListeners();
return updatedPost;
}
@override
Future<void> deletePost(TimelinePost post) async {
_posts = _posts.where((element) => element.id != post.id).toList();
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
return postRef.delete();
await postRef.delete();
notifyListeners();
}
@override
@ -64,11 +68,13 @@ class FirebaseTimelineService implements TimelineService {
}
var updatedPost = post.copyWith(reactions: updatedReactions);
_posts = _posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
notifyListeners();
return updatedPost;
}
@override
Future<List<TimelinePost>> fetchPosts(String? category) async {
debugPrint('fetching posts from firebase $category!!!');
var snapshot = (category != null)
? await _db
.collection(_options.timelineCollectionName)
@ -84,6 +90,7 @@ class FirebaseTimelineService implements TimelineService {
posts.add(post);
}
_posts = posts;
notifyListeners();
return posts;
}
@ -109,6 +116,7 @@ class FirebaseTimelineService implements TimelineService {
'likes': FieldValue.increment(1),
'liked_by': FieldValue.arrayUnion([userId]),
});
notifyListeners();
return updatedPost;
}
@ -129,6 +137,7 @@ class FirebaseTimelineService implements TimelineService {
'likes': FieldValue.increment(-1),
'liked_by': FieldValue.arrayRemove([userId]),
});
notifyListeners();
return updatedPost;
}
@ -161,6 +170,12 @@ class FirebaseTimelineService implements TimelineService {
'reaction': FieldValue.increment(1),
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]),
});
_posts = _posts
.map(
(p) => p.id == post.id ? updatedPost : p,
)
.toList();
notifyListeners();
return updatedPost;
}
}

View file

@ -137,7 +137,7 @@ class TimelinePost {
'liked_by': likedBy,
'reaction': reaction,
// reactions is a list of maps so we need to convert it to a map
'reactions': reactions?.map((e) => e.toJson()).toList() ?? {},
'reactions': reactions?.map((e) => e.toJson()).toList() ?? [],
'created_at': createdAt.toIso8601String(),
'reaction_enabled': reactionEnabled,
};

View file

@ -4,10 +4,11 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_timeline_interface/src/model/timeline_post.dart';
import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart';
abstract class TimelineService {
abstract class TimelineService with ChangeNotifier {
Future<void> deletePost(TimelinePost post);
Future<TimelinePost> createPost(TimelinePost post);
Future<List<TimelinePost>> fetchPosts(String? category);

View file

@ -15,7 +15,8 @@ class TimelineOptions {
this.translations = const TimelineTranslations(),
this.imagePickerConfig = const ImagePickerConfig(),
this.imagePickerTheme = const ImagePickerTheme(),
this.sortCommentsAscending = false,
this.allowAllDeletion = false,
this.sortCommentsAscending = true,
this.sortPostsAscending = false,
this.dateformat,
this.timeFormat,
@ -39,6 +40,10 @@ class TimelineOptions {
/// Whether to sort posts ascending or descending
final bool sortPostsAscending;
/// Allow all posts to be deleted instead of
/// only the posts of the current user
final bool allowAllDeletion;
final TimelineTranslations translations;
final ButtonBuilder? buttonBuilder;

View file

@ -20,6 +20,7 @@ class TimelinePostScreen extends StatefulWidget {
required this.userService,
required this.options,
required this.post,
required this.onPostDelete,
this.onUserTap,
this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
super.key,
@ -46,6 +47,8 @@ class TimelinePostScreen extends StatefulWidget {
/// If this is not null, the user can tap on the user avatar or name
final Function(String userId)? onUserTap;
final VoidCallback onPostDelete;
@override
State<TimelinePostScreen> createState() => _TimelinePostScreenState();
}
@ -57,19 +60,19 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
@override
void initState() {
super.initState();
unawaited(loadPostDetails());
WidgetsBinding.instance.addPostFrameCallback((_) async {
await loadPostDetails();
});
}
Future<void> loadPostDetails() async {
try {
// Assuming fetchPostDetails is an async function returning a TimelinePost
var loadedPost = await widget.service.fetchPostDetails(widget.post);
setState(() {
post = loadedPost;
isLoading = false;
});
} on Exception catch (e) {
// Handle any errors here
debugPrint('Error loading post: $e');
setState(() {
isLoading = false;
@ -102,8 +105,8 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
var post = this.post!;
post.reactions?.sort(
(a, b) => widget.options.sortCommentsAscending
? b.createdAt.compareTo(a.createdAt)
: a.createdAt.compareTo(b.createdAt),
? a.createdAt.compareTo(b.createdAt)
: b.createdAt.compareTo(a.createdAt),
);
return Stack(
@ -146,9 +149,37 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
),
),
const Spacer(),
widget.options.theme.moreIcon ??
const Icon(
if (widget.options.allowAllDeletion ||
post.creator?.userId == widget.userId)
PopupMenuButton(
onSelected: (value) async {
if (value == 'delete') {
await widget.service.deletePost(post);
widget.onPostDelete();
}
},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'delete',
child: Row(
children: [
Text(widget.options.translations.deletePost),
const SizedBox(width: 8),
widget.options.theme.deleteIcon ??
Icon(
Icons.delete,
color: widget.options.theme.iconColor,
),
],
),
),
],
child: widget.options.theme.moreIcon ??
Icon(
Icons.more_horiz_rounded,
color: widget.options.theme.iconColor,
),
),
],
),
@ -202,8 +233,9 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
const SizedBox(width: 8),
if (post.reactionEnabled)
widget.options.theme.commentIcon ??
const Icon(
Icon(
Icons.chat_bubble_outline_rounded,
color: widget.options.theme.iconColor,
),
],
),

View file

@ -60,7 +60,6 @@ class TimelineScreen extends StatefulWidget {
class _TimelineScreenState extends State<TimelineScreen> {
late ScrollController controller;
late List<TimelinePost> posts;
bool isLoading = true;
@override
@ -73,11 +72,15 @@ class _TimelineScreenState extends State<TimelineScreen> {
@override
Widget build(BuildContext context) {
if (isLoading && widget.posts == null) {
// Show loading indicator while data is being fetched
return const Center(child: CircularProgressIndicator());
}
var posts = widget.posts ?? this.posts;
// Build the list of posts
return ListenableBuilder(
listenable: widget.service,
builder: (context, _) {
var posts = widget.posts ??
widget.service.getPosts(widget.timelineCategoryFilter);
posts = posts
.where(
(p) =>
@ -89,11 +92,9 @@ class _TimelineScreenState extends State<TimelineScreen> {
// sort posts by date
posts.sort(
(a, b) => widget.options.sortPostsAscending
? b.createdAt.compareTo(a.createdAt)
: a.createdAt.compareTo(b.createdAt),
? a.createdAt.compareTo(b.createdAt)
: b.createdAt.compareTo(a.createdAt),
);
// Build the list of posts
return SingleChildScrollView(
controller: controller,
child: Column(
@ -107,12 +108,11 @@ class _TimelineScreenState extends State<TimelineScreen> {
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),
),
onTapLike: () async =>
widget.service.likePost(widget.userId, post),
onTapUnlike: () async =>
widget.service.unlikePost(widget.userId, post),
onPostDelete: () async => widget.service.deletePost(post),
onUserTap: widget.onUserTap,
),
),
@ -131,16 +131,15 @@ class _TimelineScreenState extends State<TimelineScreen> {
],
),
);
},
);
}
Future<void> loadPosts() async {
if (widget.posts != null) return;
try {
// Fetching posts from the service
var fetchedPosts =
await widget.service.fetchPosts(widget.timelineCategoryFilter);
setState(() {
posts = fetchedPosts;
isLoading = false;
});
} on Exception catch (e) {
@ -151,18 +150,4 @@ class _TimelineScreenState extends State<TimelineScreen> {
});
}
}
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];
});
}
}
}

View file

@ -13,9 +13,10 @@ class TimelinePostWidget extends StatelessWidget {
required this.options,
required this.post,
required this.height,
required this.onTap,
required this.onTapLike,
required this.onTapUnlike,
required this.onTap,
required this.onPostDelete,
this.onUserTap,
super.key,
});
@ -29,6 +30,7 @@ class TimelinePostWidget extends StatelessWidget {
final VoidCallback onTap;
final VoidCallback onTapLike;
final VoidCallback onTapUnlike;
final VoidCallback onPostDelete;
/// If this is not null, the user can tap on the user avatar or name
final Function(String userId)? onUserTap;
@ -76,9 +78,35 @@ class TimelinePostWidget extends StatelessWidget {
),
),
const Spacer(),
options.theme.moreIcon ??
const Icon(
if (options.allowAllDeletion || post.creator?.userId == userId)
PopupMenuButton(
onSelected: (value) {
if (value == 'delete') {
onPostDelete();
}
},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'delete',
child: Row(
children: [
Text(options.translations.deletePost),
const SizedBox(width: 8),
options.theme.deleteIcon ??
Icon(
Icons.delete,
color: options.theme.iconColor,
),
],
),
),
],
child: options.theme.moreIcon ??
Icon(
Icons.more_horiz_rounded,
color: options.theme.iconColor,
),
),
],
),