fix: Made parameters nullable

This commit is contained in:
Jacques 2024-01-29 15:30:26 +01:00
parent d0b4db1eb0
commit 179841f930
9 changed files with 537 additions and 446 deletions

View file

@ -40,7 +40,7 @@ List<GoRoute> getTimelineStoryRoutes() => getTimelineStoryRoutes(
service: FirebaseTimelineService(),
userService: FirebaseUserService(),
userId: currentUserId,
optionsBuilder: (context) {},
optionsBuilder: (context) => FirebaseOptions(),
),
);
```
@ -79,13 +79,12 @@ TimelineScreen(
userId: currentUserId,
service: timelineService,
options: timelineOptions,
onPostTap: (post) {}
),
````
`TimelineScreen` is supplied with a standard `TimelinePostScreen` which opens the detail page of the selected post. Needed parameter like `TimelineService` and `TimelineOptions` will be the same as the ones supplied to the `TimelineScreen`.
The standard `TimelinePostScreen` can be overridden by supplying `onPostTap` as shown below.
The standard `TimelinePostScreen` can be overridden by defining `onPostTap` as shown below.
```
TimelineScreen(

View file

@ -4,7 +4,7 @@ import 'package:flutter_timeline/flutter_timeline.dart';
import 'package:go_router/go_router.dart';
List<GoRoute> getTimelineRoutes() => getTimelineStoryRoutes(
getConfig(TimelineService(
configuration: getConfig(TimelineService(
postService: LocalTimelinePostService(),
)),
);

View file

@ -31,8 +31,7 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
var timelineService =
TimelineService(postService: LocalTimelinePostService());
var timelineService = TimelineService(postService: LocalTimelinePostService());
var timelineOptions = options;
@override

View file

@ -32,8 +32,7 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
var timelineService =
TimelineService(postService: LocalTimelinePostService());
var timelineService = TimelineService(postService: LocalTimelinePostService());
var timelineOptions = options;
@override

View file

@ -5,47 +5,80 @@
import 'package:flutter/material.dart';
import 'package:flutter_timeline/flutter_timeline.dart';
Widget timeLineNavigatorUserStory(
TimelineUserStoryConfiguration configuration,
BuildContext context,
) =>
_timelineScreenRoute(configuration, context);
Widget timeLineNavigatorUserStory({
required BuildContext context,
TimelineUserStoryConfiguration? configuration,
}) {
var config = configuration ??
TimelineUserStoryConfiguration(
userId: 'test_user',
service: TimelineService(
postService: LocalTimelinePostService(),
),
optionsBuilder: (context) => const TimelineOptions(),
);
Widget _timelineScreenRoute(
TimelineUserStoryConfiguration configuration,
BuildContext context,
) =>
TimelineScreen(
service: configuration.service,
options: configuration.optionsBuilder(context),
userId: configuration.userId,
return _timelineScreenRoute(configuration: config, context: context);
}
Widget _timelineScreenRoute({
required BuildContext context,
TimelineUserStoryConfiguration? configuration,
}) {
var config = configuration ??
TimelineUserStoryConfiguration(
userId: 'test_user',
service: TimelineService(
postService: LocalTimelinePostService(),
),
optionsBuilder: (context) => const TimelineOptions(),
);
return TimelineScreen(
service: config.service,
options: config.optionsBuilder(context),
userId: config.userId,
onPostTap: (post) async =>
configuration.onPostTap?.call(context, post) ??
config.onPostTap?.call(context, post) ??
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
_postDetailScreenRoute(configuration, context, post),
builder: (context) => _postDetailScreenRoute(
configuration: config,
context: context,
post: post,
),
),
),
onUserTap: (userId) {
configuration.onUserTap?.call(context, userId);
config.onUserTap?.call(context, userId);
},
filterEnabled: configuration.filterEnabled,
postWidgetBuilder: configuration.postWidgetBuilder,
filterEnabled: config.filterEnabled,
postWidgetBuilder: config.postWidgetBuilder,
);
}
Widget _postDetailScreenRoute({
required BuildContext context,
required TimelinePost post,
TimelineUserStoryConfiguration? configuration,
}) {
var config = configuration ??
TimelineUserStoryConfiguration(
userId: 'test_user',
service: TimelineService(
postService: LocalTimelinePostService(),
),
optionsBuilder: (context) => const TimelineOptions(),
);
Widget _postDetailScreenRoute(
TimelineUserStoryConfiguration configuration,
BuildContext context,
TimelinePost post,
) =>
TimelinePostScreen(
userId: configuration.userId,
service: configuration.service,
options: configuration.optionsBuilder(context),
return TimelinePostScreen(
userId: config.userId,
service: config.service,
options: config.optionsBuilder(context),
post: post,
onPostDelete: () async {
configuration.onPostDelete?.call(context, post) ??
await configuration.service.postService.deletePost(post);
config.onPostDelete?.call(context, post) ??
await config.service.postService.deletePost(post);
},
);
}

View file

@ -3,37 +3,43 @@
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_timeline/flutter_timeline.dart';
import 'package:flutter_timeline/src/go_router.dart';
import 'package:flutter_timeline/src/models/timeline_configuration.dart';
import 'package:flutter_timeline/src/routes.dart';
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
import 'package:go_router/go_router.dart';
List<GoRoute> getTimelineStoryRoutes(
TimelineUserStoryConfiguration configuration,
) =>
<GoRoute>[
List<GoRoute> getTimelineStoryRoutes({
TimelineUserStoryConfiguration? configuration,
}) {
var config = configuration ??= TimelineUserStoryConfiguration(
userId: 'test_user',
service: TimelineService(
postService: LocalTimelinePostService(),
),
optionsBuilder: (context) => const TimelineOptions(),
);
return <GoRoute>[
GoRoute(
path: TimelineUserStoryRoutes.timelineHome,
pageBuilder: (context, state) {
var timelineScreen = TimelineScreen(
userId: configuration.userId,
onUserTap: (user) => configuration.onUserTap?.call(context, user),
service: configuration.service,
options: configuration.optionsBuilder(context),
userId: config.userId,
onUserTap: (user) => config.onUserTap?.call(context, user),
service: config.service,
options: config.optionsBuilder(context),
onPostTap: (post) async =>
configuration.onPostTap?.call(context, post) ??
config.onPostTap?.call(context, post) ??
await context.push(
TimelineUserStoryRoutes.timelineViewPath(post.id),
),
filterEnabled: configuration.filterEnabled,
postWidgetBuilder: configuration.postWidgetBuilder,
filterEnabled: config.filterEnabled,
postWidgetBuilder: config.postWidgetBuilder,
);
return buildScreenWithoutTransition(
context: context,
state: state,
child: configuration.openPageBuilder?.call(
child: config.openPageBuilder?.call(
context,
timelineScreen,
) ??
@ -46,22 +52,22 @@ List<GoRoute> getTimelineStoryRoutes(
GoRoute(
path: TimelineUserStoryRoutes.timelineView,
pageBuilder: (context, state) {
var post = configuration.service.postService
.getPost(state.pathParameters['post']!)!;
var post =
config.service.postService.getPost(state.pathParameters['post']!)!;
var timelinePostWidget = TimelinePostScreen(
userId: configuration.userId,
options: configuration.optionsBuilder(context),
service: configuration.service,
userId: config.userId,
options: config.optionsBuilder(context),
service: config.service,
post: post,
onPostDelete: () => configuration.onPostDelete?.call(context, post),
onUserTap: (user) => configuration.onUserTap?.call(context, user),
onPostDelete: () => config.onPostDelete?.call(context, post),
onUserTap: (user) => config.onUserTap?.call(context, user),
);
return buildScreenWithoutTransition(
context: context,
state: state,
child: configuration.openPageBuilder?.call(
child: config.openPageBuilder?.call(
context,
timelinePostWidget,
) ??
@ -72,3 +78,4 @@ List<GoRoute> getTimelineStoryRoutes(
},
),
];
}

View file

@ -6,5 +6,5 @@
library flutter_timeline_firebase;
export 'src/config/firebase_timeline_options.dart';
export 'src/service/firebase_timeline_service.dart';
export 'src/service/firebase_post_service.dart';
export 'src/service/firebase_user_service.dart';

View file

@ -0,0 +1,352 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
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_firebase/src/models/firebase_user_document.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
import 'package:uuid/uuid.dart';
class FirebaseTimelinePostService extends TimelinePostService
with TimelineUserService {
FirebaseTimelinePostService({
required TimelineUserService userService,
FirebaseApp? app,
options = const FirebaseTimelineOptions(),
}) {
var appInstance = app ?? Firebase.app();
_db = FirebaseFirestore.instanceFor(app: appInstance);
_storage = FirebaseStorage.instanceFor(app: appInstance);
_userService = userService;
_options = options;
}
late FirebaseFirestore _db;
late FirebaseStorage _storage;
late TimelineUserService _userService;
late FirebaseTimelineOptions _options;
final Map<String, TimelinePosterUserModel> _users = {};
@override
Future<TimelinePost> createPost(TimelinePost post) async {
var postId = const Uuid().v4();
var user = await _userService.getUser(post.creatorId);
var updatedPost = post.copyWith(id: postId, creator: user);
if (post.image != null) {
var imageRef =
_storage.ref().child('${_options.timelineCollectionName}/$postId');
var result = await imageRef.putData(post.image!);
var imageUrl = await result.ref.getDownloadURL();
updatedPost = updatedPost.copyWith(imageUrl: imageUrl);
}
var postRef =
_db.collection(_options.timelineCollectionName).doc(updatedPost.id);
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);
await postRef.delete();
notifyListeners();
}
@override
Future<TimelinePost> deletePostReaction(
TimelinePost post,
String reactionId,
) async {
if (post.reactions != null && post.reactions!.isNotEmpty) {
var reaction =
post.reactions!.firstWhere((element) => element.id == reactionId);
var updatedPost = post.copyWith(
reaction: post.reaction - 1,
reactions: (post.reactions ?? [])..remove(reaction),
);
posts = posts
.map(
(p) => p.id == post.id ? updatedPost : p,
)
.toList();
var postRef =
_db.collection(_options.timelineCollectionName).doc(post.id);
await postRef.update({
'reaction': FieldValue.increment(-1),
'reactions': FieldValue.arrayRemove(
[reaction.toJsonWithMicroseconds()],
),
});
notifyListeners();
return updatedPost;
}
return post;
}
@override
Future<TimelinePost> fetchPostDetails(TimelinePost post) async {
var reactions = post.reactions ?? [];
var updatedReactions = <TimelinePostReaction>[];
for (var reaction in reactions) {
var user = await _userService.getUser(reaction.creatorId);
if (user != null) {
updatedReactions.add(reaction.copyWith(creator: user));
}
}
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 with category: $category');
var snapshot = (category != null)
? await _db
.collection(_options.timelineCollectionName)
.where('category', isEqualTo: category)
.get()
: await _db.collection(_options.timelineCollectionName).get();
var posts = <TimelinePost>[];
for (var doc in snapshot.docs) {
var data = doc.data();
var user = await _userService.getUser(data['creator_id']);
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
posts.add(post);
}
notifyListeners();
return posts;
}
@override
Future<List<TimelinePost>> fetchPostsPaginated(
String? category,
int limit,
) async {
// only take posts that are in our category
var oldestPost = posts
.where(
(element) => category == null || element.category == category,
)
.fold(
posts.first,
(previousValue, element) =>
(previousValue.createdAt.isBefore(element.createdAt))
? previousValue
: element,
);
var snapshot = (category != null)
? await _db
.collection(_options.timelineCollectionName)
.where('category', isEqualTo: category)
.orderBy('created_at', descending: true)
.startAfter([oldestPost])
.limit(limit)
.get()
: await _db
.collection(_options.timelineCollectionName)
.orderBy('created_at', descending: true)
.startAfter([oldestPost.createdAt])
.limit(limit)
.get();
// add the new posts to the list
var newPosts = <TimelinePost>[];
for (var doc in snapshot.docs) {
var data = doc.data();
var user = await _userService.getUser(data['creator_id']);
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
newPosts.add(post);
}
posts = [...posts, ...newPosts];
notifyListeners();
return newPosts;
}
@override
Future<TimelinePost> fetchPost(TimelinePost post) async {
var doc = await _db
.collection(_options.timelineCollectionName)
.doc(post.id)
.get();
var data = doc.data();
if (data == null) return post;
var user = await _userService.getUser(data['creator_id']);
var updatedPost = TimelinePost.fromJson(doc.id, data).copyWith(
creator: user,
);
posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
notifyListeners();
return updatedPost;
}
@override
Future<List<TimelinePost>> refreshPosts(String? category) async {
// fetch all posts between now and the newest posts we have
var newestPostWeHave = posts
.where(
(element) => category == null || element.category == category,
)
.fold(
posts.first,
(previousValue, element) =>
(previousValue.createdAt.isAfter(element.createdAt))
? previousValue
: element,
);
var snapshot = (category != null)
? await _db
.collection(_options.timelineCollectionName)
.where('category', isEqualTo: category)
.orderBy('created_at', descending: true)
.endBefore([newestPostWeHave.createdAt]).get()
: await _db
.collection(_options.timelineCollectionName)
.orderBy('created_at', descending: true)
.endBefore([newestPostWeHave.createdAt]).get();
// add the new posts to the list
var newPosts = <TimelinePost>[];
for (var doc in snapshot.docs) {
var data = doc.data();
var user = await _userService.getUser(data['creator_id']);
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
newPosts.add(post);
}
posts = [...posts, ...newPosts];
notifyListeners();
return newPosts;
}
@override
TimelinePost? getPost(String postId) =>
(posts.any((element) => element.id == postId))
? posts.firstWhere((element) => element.id == postId)
: null;
@override
List<TimelinePost> getPosts(String? category) => posts
.where((element) => category == null || element.category == category)
.toList();
@override
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 ? updatedPost : p,
)
.toList();
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
await postRef.update({
'likes': FieldValue.increment(1),
'liked_by': FieldValue.arrayUnion([userId]),
});
notifyListeners();
return updatedPost;
}
@override
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 ? updatedPost : p,
)
.toList();
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
await postRef.update({
'likes': FieldValue.increment(-1),
'liked_by': FieldValue.arrayRemove([userId]),
});
notifyListeners();
return updatedPost;
}
@override
Future<TimelinePost> reactToPost(
TimelinePost post,
TimelinePostReaction reaction, {
Uint8List? image,
}) async {
var reactionId = const Uuid().v4();
// also fetch the user information and add it to the reaction
var user = await _userService.getUser(reaction.creatorId);
var updatedReaction = reaction.copyWith(id: reactionId, creator: user);
if (image != null) {
var imageRef = _storage
.ref()
.child('${_options.timelineCollectionName}/${post.id}/$reactionId}');
var result = await imageRef.putData(image);
var imageUrl = await result.ref.getDownloadURL();
updatedReaction = updatedReaction.copyWith(imageUrl: imageUrl);
}
var updatedPost = post.copyWith(
reaction: post.reaction + 1,
reactions: post.reactions?..add(updatedReaction),
);
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
await postRef.update({
'reaction': FieldValue.increment(1),
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]),
});
posts = posts
.map(
(p) => p.id == post.id ? updatedPost : p,
)
.toList();
notifyListeners();
return updatedPost;
}
CollectionReference<FirebaseUserDocument> get _userCollection => _db
.collection(_options.usersCollectionName)
.withConverter<FirebaseUserDocument>(
fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson(
snapshot.data()!,
snapshot.id,
),
toFirestore: (user, _) => user.toJson(),
);
@override
Future<TimelinePosterUserModel?> getUser(String userId) async {
if (_users.containsKey(userId)) {
return _users[userId]!;
}
var data = (await _userCollection.doc(userId).get()).data();
var user = data == null
? TimelinePosterUserModel(userId: userId)
: TimelinePosterUserModel(
userId: userId,
firstName: data.firstName,
lastName: data.lastName,
imageUrl: data.imageUrl,
);
_users[userId] = user;
return user;
}
}

View file

@ -1,352 +1,54 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
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_firebase/src/models/firebase_user_document.dart';
import 'package:flutter_timeline_firebase/flutter_timeline_firebase.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
import 'package:uuid/uuid.dart';
class FirebaseTimelineService extends TimelinePostService
with TimelineUserService {
class FirebaseTimelineService implements TimelineService {
FirebaseTimelineService({
required TimelineUserService userService,
FirebaseApp? app,
options = const FirebaseTimelineOptions(),
this.options,
this.app,
this.firebasePostService,
this.firebaseUserService,
}) {
var appInstance = app ?? Firebase.app();
_db = FirebaseFirestore.instanceFor(app: appInstance);
_storage = FirebaseStorage.instanceFor(app: appInstance);
_userService = userService;
_options = options;
}
late FirebaseFirestore _db;
late FirebaseStorage _storage;
late TimelineUserService _userService;
late FirebaseTimelineOptions _options;
final Map<String, TimelinePosterUserModel> _users = {};
@override
Future<TimelinePost> createPost(TimelinePost post) async {
var postId = const Uuid().v4();
var user = await _userService.getUser(post.creatorId);
var updatedPost = post.copyWith(id: postId, creator: user);
if (post.image != null) {
var imageRef =
_storage.ref().child('${_options.timelineCollectionName}/$postId');
var result = await imageRef.putData(post.image!);
var imageUrl = await result.ref.getDownloadURL();
updatedPost = updatedPost.copyWith(imageUrl: imageUrl);
}
var postRef =
_db.collection(_options.timelineCollectionName).doc(updatedPost.id);
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);
await postRef.delete();
notifyListeners();
}
@override
Future<TimelinePost> deletePostReaction(
TimelinePost post,
String reactionId,
) async {
if (post.reactions != null && post.reactions!.isNotEmpty) {
var reaction =
post.reactions!.firstWhere((element) => element.id == reactionId);
var updatedPost = post.copyWith(
reaction: post.reaction - 1,
reactions: (post.reactions ?? [])..remove(reaction),
);
posts = posts
.map(
(p) => p.id == post.id ? updatedPost : p,
)
.toList();
var postRef =
_db.collection(_options.timelineCollectionName).doc(post.id);
await postRef.update({
'reaction': FieldValue.increment(-1),
'reactions': FieldValue.arrayRemove(
[reaction.toJsonWithMicroseconds()],
),
});
notifyListeners();
return updatedPost;
}
return post;
}
@override
Future<TimelinePost> fetchPostDetails(TimelinePost post) async {
var reactions = post.reactions ?? [];
var updatedReactions = <TimelinePostReaction>[];
for (var reaction in reactions) {
var user = await _userService.getUser(reaction.creatorId);
if (user != null) {
updatedReactions.add(reaction.copyWith(creator: user));
}
}
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 with category: $category');
var snapshot = (category != null)
? await _db
.collection(_options.timelineCollectionName)
.where('category', isEqualTo: category)
.get()
: await _db.collection(_options.timelineCollectionName).get();
var posts = <TimelinePost>[];
for (var doc in snapshot.docs) {
var data = doc.data();
var user = await _userService.getUser(data['creator_id']);
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
posts.add(post);
}
notifyListeners();
return posts;
}
@override
Future<List<TimelinePost>> fetchPostsPaginated(
String? category,
int limit,
) async {
// only take posts that are in our category
var oldestPost = posts
.where(
(element) => category == null || element.category == category,
)
.fold(
posts.first,
(previousValue, element) =>
(previousValue.createdAt.isBefore(element.createdAt))
? previousValue
: element,
);
var snapshot = (category != null)
? await _db
.collection(_options.timelineCollectionName)
.where('category', isEqualTo: category)
.orderBy('created_at', descending: true)
.startAfter([oldestPost])
.limit(limit)
.get()
: await _db
.collection(_options.timelineCollectionName)
.orderBy('created_at', descending: true)
.startAfter([oldestPost.createdAt])
.limit(limit)
.get();
// add the new posts to the list
var newPosts = <TimelinePost>[];
for (var doc in snapshot.docs) {
var data = doc.data();
var user = await _userService.getUser(data['creator_id']);
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
newPosts.add(post);
}
posts = [...posts, ...newPosts];
notifyListeners();
return newPosts;
}
@override
Future<TimelinePost> fetchPost(TimelinePost post) async {
var doc = await _db
.collection(_options.timelineCollectionName)
.doc(post.id)
.get();
var data = doc.data();
if (data == null) return post;
var user = await _userService.getUser(data['creator_id']);
var updatedPost = TimelinePost.fromJson(doc.id, data).copyWith(
creator: user,
);
posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
notifyListeners();
return updatedPost;
}
@override
Future<List<TimelinePost>> refreshPosts(String? category) async {
// fetch all posts between now and the newest posts we have
var newestPostWeHave = posts
.where(
(element) => category == null || element.category == category,
)
.fold(
posts.first,
(previousValue, element) =>
(previousValue.createdAt.isAfter(element.createdAt))
? previousValue
: element,
);
var snapshot = (category != null)
? await _db
.collection(_options.timelineCollectionName)
.where('category', isEqualTo: category)
.orderBy('created_at', descending: true)
.endBefore([newestPostWeHave.createdAt]).get()
: await _db
.collection(_options.timelineCollectionName)
.orderBy('created_at', descending: true)
.endBefore([newestPostWeHave.createdAt]).get();
// add the new posts to the list
var newPosts = <TimelinePost>[];
for (var doc in snapshot.docs) {
var data = doc.data();
var user = await _userService.getUser(data['creator_id']);
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
newPosts.add(post);
}
posts = [...posts, ...newPosts];
notifyListeners();
return newPosts;
}
@override
TimelinePost? getPost(String postId) =>
(posts.any((element) => element.id == postId))
? posts.firstWhere((element) => element.id == postId)
: null;
@override
List<TimelinePost> getPosts(String? category) => posts
.where((element) => category == null || element.category == category)
.toList();
@override
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 ? updatedPost : p,
)
.toList();
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
await postRef.update({
'likes': FieldValue.increment(1),
'liked_by': FieldValue.arrayUnion([userId]),
});
notifyListeners();
return updatedPost;
}
@override
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 ? updatedPost : p,
)
.toList();
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
await postRef.update({
'likes': FieldValue.increment(-1),
'liked_by': FieldValue.arrayRemove([userId]),
});
notifyListeners();
return updatedPost;
}
@override
Future<TimelinePost> reactToPost(
TimelinePost post,
TimelinePostReaction reaction, {
Uint8List? image,
}) async {
var reactionId = const Uuid().v4();
// also fetch the user information and add it to the reaction
var user = await _userService.getUser(reaction.creatorId);
var updatedReaction = reaction.copyWith(id: reactionId, creator: user);
if (image != null) {
var imageRef = _storage
.ref()
.child('${_options.timelineCollectionName}/${post.id}/$reactionId}');
var result = await imageRef.putData(image);
var imageUrl = await result.ref.getDownloadURL();
updatedReaction = updatedReaction.copyWith(imageUrl: imageUrl);
}
var updatedPost = post.copyWith(
reaction: post.reaction + 1,
reactions: post.reactions?..add(updatedReaction),
firebaseUserService ??= FirebaseTimelinePostService(
userService: userService,
options: options,
app: app,
);
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
await postRef.update({
'reaction': FieldValue.increment(1),
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]),
});
posts = posts
.map(
(p) => p.id == post.id ? updatedPost : p,
)
.toList();
notifyListeners();
return updatedPost;
firebasePostService ??= FirebaseTimelinePostService(
userService: userService,
options: options,
app: app,
);
}
CollectionReference<FirebaseUserDocument> get _userCollection => _db
.collection(_options.usersCollectionName)
.withConverter<FirebaseUserDocument>(
fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson(
snapshot.data()!,
snapshot.id,
),
toFirestore: (user, _) => user.toJson(),
);
final FirebaseTimelineOptions? options;
final FirebaseApp? app;
TimelinePostService? firebasePostService;
TimelineUserService? firebaseUserService;
@override
Future<TimelinePosterUserModel?> getUser(String userId) async {
if (_users.containsKey(userId)) {
return _users[userId]!;
}
var data = (await _userCollection.doc(userId).get()).data();
var user = data == null
? TimelinePosterUserModel(userId: userId)
: TimelinePosterUserModel(
userId: userId,
firstName: data.firstName,
lastName: data.lastName,
imageUrl: data.imageUrl,
TimelinePostService get postService {
if (firebasePostService != null) {
return firebasePostService!;
} else {
return FirebaseTimelinePostService(
userService: userService,
options: options,
app: app,
);
}
}
_users[userId] = user;
return user;
@override
TimelineUserService get userService {
if (firebaseUserService != null) {
return firebaseUserService!;
} else {
return FirebaseUserService(
options: options,
app: app,
);
}
}
}