mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 10:33:44 +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:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_storage/firebase_storage.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_firebase/src/config/firebase_timeline_options.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class FirebaseTimelineService implements TimelineService {
|
class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
FirebaseTimelineService({
|
FirebaseTimelineService({
|
||||||
required TimelineUserService userService,
|
required TimelineUserService userService,
|
||||||
FirebaseApp? app,
|
FirebaseApp? app,
|
||||||
|
@ -41,15 +42,18 @@ class FirebaseTimelineService implements TimelineService {
|
||||||
var updatedPost = post.copyWith(imageUrl: imageUrl, id: postId);
|
var updatedPost = post.copyWith(imageUrl: imageUrl, id: postId);
|
||||||
var postRef =
|
var postRef =
|
||||||
_db.collection(_options.timelineCollectionName).doc(updatedPost.id);
|
_db.collection(_options.timelineCollectionName).doc(updatedPost.id);
|
||||||
_posts.add(updatedPost);
|
|
||||||
await postRef.set(updatedPost.toJson());
|
await postRef.set(updatedPost.toJson());
|
||||||
|
_posts.add(updatedPost);
|
||||||
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deletePost(TimelinePost post) async {
|
Future<void> deletePost(TimelinePost post) async {
|
||||||
|
_posts = _posts.where((element) => element.id != post.id).toList();
|
||||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||||
return postRef.delete();
|
await postRef.delete();
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -64,11 +68,13 @@ class FirebaseTimelineService implements TimelineService {
|
||||||
}
|
}
|
||||||
var updatedPost = post.copyWith(reactions: updatedReactions);
|
var updatedPost = post.copyWith(reactions: updatedReactions);
|
||||||
_posts = _posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
_posts = _posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
||||||
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<TimelinePost>> fetchPosts(String? category) async {
|
Future<List<TimelinePost>> fetchPosts(String? category) async {
|
||||||
|
debugPrint('fetching posts from firebase $category!!!');
|
||||||
var snapshot = (category != null)
|
var snapshot = (category != null)
|
||||||
? await _db
|
? await _db
|
||||||
.collection(_options.timelineCollectionName)
|
.collection(_options.timelineCollectionName)
|
||||||
|
@ -84,6 +90,7 @@ class FirebaseTimelineService implements TimelineService {
|
||||||
posts.add(post);
|
posts.add(post);
|
||||||
}
|
}
|
||||||
_posts = posts;
|
_posts = posts;
|
||||||
|
notifyListeners();
|
||||||
return posts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,6 +116,7 @@ class FirebaseTimelineService implements TimelineService {
|
||||||
'likes': FieldValue.increment(1),
|
'likes': FieldValue.increment(1),
|
||||||
'liked_by': FieldValue.arrayUnion([userId]),
|
'liked_by': FieldValue.arrayUnion([userId]),
|
||||||
});
|
});
|
||||||
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,6 +137,7 @@ class FirebaseTimelineService implements TimelineService {
|
||||||
'likes': FieldValue.increment(-1),
|
'likes': FieldValue.increment(-1),
|
||||||
'liked_by': FieldValue.arrayRemove([userId]),
|
'liked_by': FieldValue.arrayRemove([userId]),
|
||||||
});
|
});
|
||||||
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,6 +170,12 @@ class FirebaseTimelineService implements TimelineService {
|
||||||
'reaction': FieldValue.increment(1),
|
'reaction': FieldValue.increment(1),
|
||||||
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]),
|
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]),
|
||||||
});
|
});
|
||||||
|
_posts = _posts
|
||||||
|
.map(
|
||||||
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ class TimelinePost {
|
||||||
'liked_by': likedBy,
|
'liked_by': likedBy,
|
||||||
'reaction': reaction,
|
'reaction': reaction,
|
||||||
// reactions is a list of maps so we need to convert it to a map
|
// 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(),
|
'created_at': createdAt.toIso8601String(),
|
||||||
'reaction_enabled': reactionEnabled,
|
'reaction_enabled': reactionEnabled,
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,10 +4,11 @@
|
||||||
|
|
||||||
import 'dart:typed_data';
|
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_post.dart';
|
||||||
import 'package:flutter_timeline_interface/src/model/timeline_reaction.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<void> deletePost(TimelinePost post);
|
||||||
Future<TimelinePost> createPost(TimelinePost post);
|
Future<TimelinePost> createPost(TimelinePost post);
|
||||||
Future<List<TimelinePost>> fetchPosts(String? category);
|
Future<List<TimelinePost>> fetchPosts(String? category);
|
||||||
|
|
|
@ -15,7 +15,8 @@ class TimelineOptions {
|
||||||
this.translations = const TimelineTranslations(),
|
this.translations = const TimelineTranslations(),
|
||||||
this.imagePickerConfig = const ImagePickerConfig(),
|
this.imagePickerConfig = const ImagePickerConfig(),
|
||||||
this.imagePickerTheme = const ImagePickerTheme(),
|
this.imagePickerTheme = const ImagePickerTheme(),
|
||||||
this.sortCommentsAscending = false,
|
this.allowAllDeletion = false,
|
||||||
|
this.sortCommentsAscending = true,
|
||||||
this.sortPostsAscending = false,
|
this.sortPostsAscending = false,
|
||||||
this.dateformat,
|
this.dateformat,
|
||||||
this.timeFormat,
|
this.timeFormat,
|
||||||
|
@ -39,6 +40,10 @@ class TimelineOptions {
|
||||||
/// Whether to sort posts ascending or descending
|
/// Whether to sort posts ascending or descending
|
||||||
final bool sortPostsAscending;
|
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 TimelineTranslations translations;
|
||||||
|
|
||||||
final ButtonBuilder? buttonBuilder;
|
final ButtonBuilder? buttonBuilder;
|
||||||
|
|
|
@ -20,6 +20,7 @@ class TimelinePostScreen extends StatefulWidget {
|
||||||
required this.userService,
|
required this.userService,
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.post,
|
required this.post,
|
||||||
|
required this.onPostDelete,
|
||||||
this.onUserTap,
|
this.onUserTap,
|
||||||
this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
||||||
super.key,
|
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
|
/// If this is not null, the user can tap on the user avatar or name
|
||||||
final Function(String userId)? onUserTap;
|
final Function(String userId)? onUserTap;
|
||||||
|
|
||||||
|
final VoidCallback onPostDelete;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TimelinePostScreen> createState() => _TimelinePostScreenState();
|
State<TimelinePostScreen> createState() => _TimelinePostScreenState();
|
||||||
}
|
}
|
||||||
|
@ -57,19 +60,19 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
unawaited(loadPostDetails());
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await loadPostDetails();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadPostDetails() async {
|
Future<void> loadPostDetails() async {
|
||||||
try {
|
try {
|
||||||
// Assuming fetchPostDetails is an async function returning a TimelinePost
|
|
||||||
var loadedPost = await widget.service.fetchPostDetails(widget.post);
|
var loadedPost = await widget.service.fetchPostDetails(widget.post);
|
||||||
setState(() {
|
setState(() {
|
||||||
post = loadedPost;
|
post = loadedPost;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
});
|
});
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
// Handle any errors here
|
|
||||||
debugPrint('Error loading post: $e');
|
debugPrint('Error loading post: $e');
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
@ -102,8 +105,8 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
var post = this.post!;
|
var post = this.post!;
|
||||||
post.reactions?.sort(
|
post.reactions?.sort(
|
||||||
(a, b) => widget.options.sortCommentsAscending
|
(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(
|
return Stack(
|
||||||
|
@ -146,9 +149,37 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
widget.options.theme.moreIcon ??
|
if (widget.options.allowAllDeletion ||
|
||||||
const Icon(
|
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,
|
Icons.more_horiz_rounded,
|
||||||
|
color: widget.options.theme.iconColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -202,8 +233,9 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
if (post.reactionEnabled)
|
if (post.reactionEnabled)
|
||||||
widget.options.theme.commentIcon ??
|
widget.options.theme.commentIcon ??
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.chat_bubble_outline_rounded,
|
Icons.chat_bubble_outline_rounded,
|
||||||
|
color: widget.options.theme.iconColor,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -60,7 +60,6 @@ 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;
|
bool isLoading = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -73,11 +72,15 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (isLoading && widget.posts == null) {
|
if (isLoading && widget.posts == null) {
|
||||||
// Show loading indicator while data is being fetched
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
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
|
posts = posts
|
||||||
.where(
|
.where(
|
||||||
(p) =>
|
(p) =>
|
||||||
|
@ -89,11 +92,9 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
// sort posts by date
|
// sort posts by date
|
||||||
posts.sort(
|
posts.sort(
|
||||||
(a, b) => widget.options.sortPostsAscending
|
(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(
|
return SingleChildScrollView(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -107,12 +108,11 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
post: post,
|
post: post,
|
||||||
height: widget.timelinePostHeight,
|
height: widget.timelinePostHeight,
|
||||||
onTap: () => widget.onPostTap.call(post),
|
onTap: () => widget.onPostTap.call(post),
|
||||||
onTapLike: () async => updatePostInList(
|
onTapLike: () async =>
|
||||||
await widget.service.likePost(widget.userId, post),
|
widget.service.likePost(widget.userId, post),
|
||||||
),
|
onTapUnlike: () async =>
|
||||||
onTapUnlike: () async => updatePostInList(
|
widget.service.unlikePost(widget.userId, post),
|
||||||
await widget.service.unlikePost(widget.userId, post),
|
onPostDelete: () async => widget.service.deletePost(post),
|
||||||
),
|
|
||||||
onUserTap: widget.onUserTap,
|
onUserTap: widget.onUserTap,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -131,16 +131,15 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadPosts() async {
|
Future<void> loadPosts() async {
|
||||||
if (widget.posts != null) return;
|
if (widget.posts != null) return;
|
||||||
try {
|
try {
|
||||||
// Fetching posts from the service
|
|
||||||
var fetchedPosts =
|
|
||||||
await widget.service.fetchPosts(widget.timelineCategoryFilter);
|
await widget.service.fetchPosts(widget.timelineCategoryFilter);
|
||||||
setState(() {
|
setState(() {
|
||||||
posts = fetchedPosts;
|
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
});
|
});
|
||||||
} on Exception catch (e) {
|
} 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.options,
|
||||||
required this.post,
|
required this.post,
|
||||||
required this.height,
|
required this.height,
|
||||||
|
required this.onTap,
|
||||||
required this.onTapLike,
|
required this.onTapLike,
|
||||||
required this.onTapUnlike,
|
required this.onTapUnlike,
|
||||||
required this.onTap,
|
required this.onPostDelete,
|
||||||
this.onUserTap,
|
this.onUserTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
@ -29,6 +30,7 @@ class TimelinePostWidget extends StatelessWidget {
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final VoidCallback onTapLike;
|
final VoidCallback onTapLike;
|
||||||
final VoidCallback onTapUnlike;
|
final VoidCallback onTapUnlike;
|
||||||
|
final VoidCallback onPostDelete;
|
||||||
|
|
||||||
/// If this is not null, the user can tap on the user avatar or name
|
/// If this is not null, the user can tap on the user avatar or name
|
||||||
final Function(String userId)? onUserTap;
|
final Function(String userId)? onUserTap;
|
||||||
|
@ -76,9 +78,35 @@ class TimelinePostWidget extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
options.theme.moreIcon ??
|
if (options.allowAllDeletion || post.creator?.userId == userId)
|
||||||
const Icon(
|
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,
|
Icons.more_horiz_rounded,
|
||||||
|
color: options.theme.iconColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue