mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 02:23:46 +02:00
feat: allow post deletion and add changenotifiersystem
This commit is contained in:
parent
ca4ec03002
commit
753ecc039e
7 changed files with 156 additions and 90 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,10 +149,38 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
),
|
||||
),
|
||||
const Spacer(),
|
||||
widget.options.theme.moreIcon ??
|
||||
const Icon(
|
||||
Icons.more_horiz_rounded,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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,74 +72,74 @@ 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;
|
||||
posts = posts
|
||||
.where(
|
||||
(p) =>
|
||||
widget.timelineCategoryFilter == null ||
|
||||
p.category == widget.timelineCategoryFilter,
|
||||
)
|
||||
.toList();
|
||||
|
||||
// sort posts by date
|
||||
posts.sort(
|
||||
(a, b) => widget.options.sortPostsAscending
|
||||
? b.createdAt.compareTo(a.createdAt)
|
||||
: a.createdAt.compareTo(b.createdAt),
|
||||
);
|
||||
|
||||
// Build the list of posts
|
||||
return SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
children: [
|
||||
...posts.map(
|
||||
(post) => Padding(
|
||||
padding: widget.padding,
|
||||
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),
|
||||
return ListenableBuilder(
|
||||
listenable: widget.service,
|
||||
builder: (context, _) {
|
||||
var posts = widget.posts ??
|
||||
widget.service.getPosts(widget.timelineCategoryFilter);
|
||||
posts = posts
|
||||
.where(
|
||||
(p) =>
|
||||
widget.timelineCategoryFilter == null ||
|
||||
p.category == widget.timelineCategoryFilter,
|
||||
)
|
||||
.toList();
|
||||
|
||||
// sort posts by date
|
||||
posts.sort(
|
||||
(a, b) => widget.options.sortPostsAscending
|
||||
? a.createdAt.compareTo(b.createdAt)
|
||||
: b.createdAt.compareTo(a.createdAt),
|
||||
);
|
||||
return SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
children: [
|
||||
...posts.map(
|
||||
(post) => Padding(
|
||||
padding: widget.padding,
|
||||
child: TimelinePostWidget(
|
||||
userId: widget.userId,
|
||||
options: widget.options,
|
||||
post: post,
|
||||
height: widget.timelinePostHeight,
|
||||
onTap: () => widget.onPostTap.call(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,
|
||||
),
|
||||
),
|
||||
onTapUnlike: () async => updatePostInList(
|
||||
await widget.service.unlikePost(widget.userId, post),
|
||||
),
|
||||
onUserTap: widget.onUserTap,
|
||||
),
|
||||
),
|
||||
if (posts.isEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
widget.timelineCategoryFilter == null
|
||||
? widget.options.translations.noPosts
|
||||
: widget.options.translations.noPostsWithFilter,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (posts.isEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
widget.timelineCategoryFilter == null
|
||||
? widget.options.translations.noPosts
|
||||
: widget.options.translations.noPostsWithFilter,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadPosts() async {
|
||||
if (widget.posts != null) return;
|
||||
try {
|
||||
// Fetching posts from the service
|
||||
var fetchedPosts =
|
||||
await widget.service.fetchPosts(widget.timelineCategoryFilter);
|
||||
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];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,10 +78,36 @@ class TimelinePostWidget extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const Spacer(),
|
||||
options.theme.moreIcon ??
|
||||
const Icon(
|
||||
Icons.more_horiz_rounded,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
|
Loading…
Reference in a new issue