diff --git a/README.md b/README.md index 83062cf..7242285 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,11 @@ If you are going to use Firebase as the back-end of the Timeline, you should als path: packages/flutter_timeline_firebase ``` +Add the following code in your `main` function, before the runApp(). +``` + initializeDateFormatting(); +``` + ## How to use To use the module within your Flutter-application with predefined `Go_router` routes you should add the following: @@ -35,7 +40,7 @@ List getTimelineStoryRoutes() => getTimelineStoryRoutes( service: FirebaseTimelineService(), userService: FirebaseUserService(), userId: currentUserId, - optionsBuilder: (context) {}, + optionsBuilder: (context) => FirebaseOptions(), ), ); ``` @@ -74,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( diff --git a/packages/flutter_timeline/example/lib/apps/go_router/app.dart b/packages/flutter_timeline/example/lib/apps/go_router/app.dart index ef18fef..7f222a7 100644 --- a/packages/flutter_timeline/example/lib/apps/go_router/app.dart +++ b/packages/flutter_timeline/example/lib/apps/go_router/app.dart @@ -1,13 +1,12 @@ import 'package:example/config/config.dart'; -import 'package:example/services/timeline_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_timeline/flutter_timeline.dart'; import 'package:go_router/go_router.dart'; List getTimelineRoutes() => getTimelineStoryRoutes( - getConfig( - TestTimelineService(), - ), + configuration: getConfig(TimelineService( + postService: LocalTimelinePostService(), + )), ); final _router = GoRouter( diff --git a/packages/flutter_timeline/example/lib/apps/navigator/app.dart b/packages/flutter_timeline/example/lib/apps/navigator/app.dart index 2473d64..434d595 100644 --- a/packages/flutter_timeline/example/lib/apps/navigator/app.dart +++ b/packages/flutter_timeline/example/lib/apps/navigator/app.dart @@ -1,5 +1,4 @@ import 'package:example/config/config.dart'; -import 'package:example/services/timeline_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_timeline/flutter_timeline.dart'; @@ -32,7 +31,8 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - var timelineService = TestTimelineService(); + var timelineService = + TimelineService(postService: LocalTimelinePostService()); var timelineOptions = options; @override @@ -64,7 +64,11 @@ class _MyHomePageState extends State { ], ), body: SafeArea( - child: timeLineNavigatorUserStory(getConfig(timelineService), context), + child: timeLineNavigatorUserStory( + configuration: getConfig( + timelineService, + ), + context: context), ), ); } diff --git a/packages/flutter_timeline/example/lib/apps/widgets/app.dart b/packages/flutter_timeline/example/lib/apps/widgets/app.dart index 8367742..1ac4aad 100644 --- a/packages/flutter_timeline/example/lib/apps/widgets/app.dart +++ b/packages/flutter_timeline/example/lib/apps/widgets/app.dart @@ -1,5 +1,5 @@ import 'package:example/config/config.dart'; -import 'package:example/services/timeline_service.dart'; + import 'package:flutter/material.dart'; import 'package:flutter_timeline/flutter_timeline.dart'; @@ -32,7 +32,7 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - var timelineService = TestTimelineService(); + var timelineService = TimelineService(postService: LocalTimelinePostService()); var timelineOptions = options; @override @@ -42,6 +42,7 @@ class _MyHomePageState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton( + heroTag: 'btn1', onPressed: () { createPost(context, timelineService, timelineOptions); }, @@ -54,6 +55,7 @@ class _MyHomePageState extends State { height: 8, ), FloatingActionButton( + heroTag: 'btn2', onPressed: () { generatePost(timelineService); }, @@ -64,31 +66,8 @@ class _MyHomePageState extends State { ), ], ), - body: SafeArea( - child: TimelineScreen( - userId: 'test_user', - service: timelineService, - options: timelineOptions, - onPostTap: (post) async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Scaffold( - body: TimelinePostScreen( - userId: 'test_user', - service: timelineService, - options: timelineOptions, - post: post, - onPostDelete: () { - timelineService.deletePost(post); - Navigator.of(context).pop(); - }, - ), - ), - ), - ); - }, - ), + body: const SafeArea( + child: TimelineScreen(), ), ); } diff --git a/packages/flutter_timeline/example/lib/config/config.dart b/packages/flutter_timeline/example/lib/config/config.dart index 734c1eb..0728bdf 100644 --- a/packages/flutter_timeline/example/lib/config/config.dart +++ b/packages/flutter_timeline/example/lib/config/config.dart @@ -1,11 +1,9 @@ -import 'package:example/apps/widgets/screens/post_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_timeline/flutter_timeline.dart'; TimelineUserStoryConfiguration getConfig(TimelineService service) { return TimelineUserStoryConfiguration( service: service, - userService: TestUserService(), userId: 'test_user', optionsBuilder: (context) => options); } @@ -56,9 +54,9 @@ void createPost(BuildContext context, TimelineService service, } void generatePost(TimelineService service) { - var amountOfPosts = service.getPosts(null).length; + var amountOfPosts = service.postService.getPosts(null).length; - service.createPost( + service.postService.createPost( TimelinePost( id: 'Post$amountOfPosts', creatorId: 'test_user', diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart index 468b521..6c708f4 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart @@ -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, - onPostTap: (post) async => - configuration.onPostTap?.call(context, post) ?? - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - _postDetailScreenRoute(configuration, context, post), + 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 => + config.onPostTap?.call(context, post) ?? + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _postDetailScreenRoute( + configuration: config, + context: context, + post: post, ), ), - onUserTap: (userId) { - configuration.onUserTap?.call(context, userId); - }, - filterEnabled: configuration.filterEnabled, - postWidgetBuilder: configuration.postWidgetBuilder, - ); + ), + onUserTap: (userId) { + config.onUserTap?.call(context, userId); + }, + filterEnabled: config.filterEnabled, + postWidgetBuilder: config.postWidgetBuilder, + ); +} -Widget _postDetailScreenRoute( - TimelineUserStoryConfiguration configuration, - BuildContext context, - TimelinePost post, -) => - TimelinePostScreen( - userId: configuration.userId, - service: configuration.service, - options: configuration.optionsBuilder(context), - post: post, - onPostDelete: () async { - configuration.onPostDelete?.call(context, post) ?? - await configuration.service.deletePost(post); - }, - ); +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(), + ); + + return TimelinePostScreen( + userId: config.userId, + service: config.service, + options: config.optionsBuilder(context), + post: post, + onPostDelete: () async { + config.onPostDelete?.call(context, post) ?? + await config.service.postService.deletePost(post); + }, + ); +} diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart index cdd4ba5..a40b7b1 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart @@ -3,72 +3,79 @@ // 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 getTimelineStoryRoutes( - TimelineUserStoryConfiguration configuration, -) => - [ - 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), - onPostTap: (post) async => - configuration.onPostTap?.call(context, post) ?? - await context.push( - TimelineUserStoryRoutes.timelineViewPath(post.id), - ), - filterEnabled: configuration.filterEnabled, - postWidgetBuilder: configuration.postWidgetBuilder, - ); +List getTimelineStoryRoutes({ + TimelineUserStoryConfiguration? configuration, +}) { + var config = configuration ?? TimelineUserStoryConfiguration( + userId: 'test_user', + service: TimelineService( + postService: LocalTimelinePostService(), + ), + optionsBuilder: (context) => const TimelineOptions(), + ); - return buildScreenWithoutTransition( - context: context, - state: state, - child: configuration.openPageBuilder?.call( - context, - timelineScreen, - ) ?? - Scaffold( - body: timelineScreen, - ), - ); - }, - ), - GoRoute( - path: TimelineUserStoryRoutes.timelineView, - pageBuilder: (context, state) { - var post = - configuration.service.getPost(state.pathParameters['post']!)!; + return [ + GoRoute( + path: TimelineUserStoryRoutes.timelineHome, + pageBuilder: (context, state) { + var timelineScreen = TimelineScreen( + userId: config.userId, + onUserTap: (user) => config.onUserTap?.call(context, user), + service: config.service, + options: config.optionsBuilder(context), + onPostTap: (post) async => + config.onPostTap?.call(context, post) ?? + await context.push( + TimelineUserStoryRoutes.timelineViewPath(post.id), + ), + filterEnabled: config.filterEnabled, + postWidgetBuilder: config.postWidgetBuilder, + ); - var timelinePostWidget = TimelinePostScreen( - userId: configuration.userId, - options: configuration.optionsBuilder(context), - service: configuration.service, - post: post, - onPostDelete: () => configuration.onPostDelete?.call(context, post), - onUserTap: (user) => configuration.onUserTap?.call(context, user), - ); + return buildScreenWithoutTransition( + context: context, + state: state, + child: config.openPageBuilder?.call( + context, + timelineScreen, + ) ?? + Scaffold( + body: timelineScreen, + ), + ); + }, + ), + GoRoute( + path: TimelineUserStoryRoutes.timelineView, + pageBuilder: (context, state) { + var post = + config.service.postService.getPost(state.pathParameters['post']!)!; - return buildScreenWithoutTransition( - context: context, - state: state, - child: configuration.openPageBuilder?.call( - context, - timelinePostWidget, - ) ?? - Scaffold( - body: timelinePostWidget, - ), - ); - }, - ), - ]; + var timelinePostWidget = TimelinePostScreen( + userId: config.userId, + options: config.optionsBuilder(context), + service: config.service, + post: post, + onPostDelete: () => config.onPostDelete?.call(context, post), + onUserTap: (user) => config.onUserTap?.call(context, user), + ); + + return buildScreenWithoutTransition( + context: context, + state: state, + child: config.openPageBuilder?.call( + context, + timelinePostWidget, + ) ?? + Scaffold( + body: timelinePostWidget, + ), + ); + }, + ), + ]; +} diff --git a/packages/flutter_timeline/lib/src/models/timeline_configuration.dart b/packages/flutter_timeline/lib/src/models/timeline_configuration.dart index 8708d3b..650f19d 100644 --- a/packages/flutter_timeline/lib/src/models/timeline_configuration.dart +++ b/packages/flutter_timeline/lib/src/models/timeline_configuration.dart @@ -9,10 +9,9 @@ import 'package:flutter_timeline_view/flutter_timeline_view.dart'; @immutable class TimelineUserStoryConfiguration { const TimelineUserStoryConfiguration({ - required this.userId, required this.service, - required this.userService, required this.optionsBuilder, + this.userId = 'test_user', this.openPageBuilder, this.onPostTap, this.onUserTap, @@ -25,8 +24,6 @@ class TimelineUserStoryConfiguration { final TimelineService service; - final TimelineUserService userService; - final TimelineOptions Function(BuildContext context) optionsBuilder; final Function(BuildContext context, String userId)? onUserTap; diff --git a/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart b/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart index 9ad1f86..b138166 100644 --- a/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart +++ b/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart @@ -6,5 +6,6 @@ library flutter_timeline_firebase; export 'src/config/firebase_timeline_options.dart'; +export 'src/service/firebase_post_service.dart'; export 'src/service/firebase_timeline_service.dart'; export 'src/service/firebase_user_service.dart'; diff --git a/packages/flutter_timeline_firebase/lib/src/service/firebase_post_service.dart b/packages/flutter_timeline_firebase/lib/src/service/firebase_post_service.dart new file mode 100644 index 0000000..76a6c54 --- /dev/null +++ b/packages/flutter_timeline_firebase/lib/src/service/firebase_post_service.dart @@ -0,0 +1,358 @@ +// 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 + with TimelineUserService, ChangeNotifier + implements TimelinePostService { + FirebaseTimelinePostService({ + required TimelineUserService userService, + FirebaseApp? app, + FirebaseTimelineOptions? options, + }) { + var appInstance = app ?? Firebase.app(); + _db = FirebaseFirestore.instanceFor(app: appInstance); + _storage = FirebaseStorage.instanceFor(app: appInstance); + _userService = userService; + _options = options ?? const FirebaseTimelineOptions(); + } + + late FirebaseFirestore _db; + late FirebaseStorage _storage; + late TimelineUserService _userService; + late FirebaseTimelineOptions _options; + + final Map _users = {}; + + @override + List posts = []; + + @override + Future 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 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 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 fetchPostDetails(TimelinePost post) async { + var reactions = post.reactions ?? []; + var updatedReactions = []; + 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> 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 fetchedPosts = []; + 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); + fetchedPosts.add(post); + } + + posts = fetchedPosts; + + notifyListeners(); + return posts; + } + + @override + Future> 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 = []; + 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 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> 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 = []; + 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 getPosts(String? category) => posts + .where((element) => category == null || element.category == category) + .toList(); + + @override + Future 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 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 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 get _userCollection => _db + .collection(_options.usersCollectionName) + .withConverter( + fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson( + snapshot.data()!, + snapshot.id, + ), + toFirestore: (user, _) => user.toJson(), + ); + @override + Future 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; + } +} diff --git a/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart b/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart index b74ff5d..2e56f8d 100644 --- a/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart +++ b/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart @@ -1,351 +1,53 @@ -// 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 TimelineService 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; + firebaseUserService ??= FirebaseTimelineUserService( + options: options, + app: app, + ); + + firebasePostService ??= FirebaseTimelinePostService( + userService: userService, + options: options, + app: app, + ); } - late FirebaseFirestore _db; - late FirebaseStorage _storage; - late TimelineUserService _userService; - late FirebaseTimelineOptions _options; - - final Map _users = {}; + final FirebaseTimelineOptions? options; + final FirebaseApp? app; + TimelinePostService? firebasePostService; + TimelineUserService? firebaseUserService; @override - Future 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 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 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), + TimelinePostService get postService { + if (firebasePostService != null) { + return firebasePostService!; + } else { + return FirebaseTimelinePostService( + userService: userService, + options: options, + app: app, ); - 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 fetchPostDetails(TimelinePost post) async { - var reactions = post.reactions ?? []; - var updatedReactions = []; - 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> 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 = []; - 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> 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 = []; - 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 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> 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 = []; - 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 getPosts(String? category) => posts - .where((element) => category == null || element.category == category) - .toList(); - - @override - Future 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 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 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 get _userCollection => _db - .collection(_options.usersCollectionName) - .withConverter( - fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson( - snapshot.data()!, - snapshot.id, - ), - toFirestore: (user, _) => user.toJson(), + TimelineUserService get userService { + if (firebaseUserService != null) { + return firebaseUserService!; + } else { + return FirebaseTimelineUserService( + options: options, + app: app, ); - @override - Future 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; } } diff --git a/packages/flutter_timeline_firebase/lib/src/service/firebase_user_service.dart b/packages/flutter_timeline_firebase/lib/src/service/firebase_user_service.dart index fb1da17..bfde3d5 100644 --- a/packages/flutter_timeline_firebase/lib/src/service/firebase_user_service.dart +++ b/packages/flutter_timeline_firebase/lib/src/service/firebase_user_service.dart @@ -8,14 +8,14 @@ import 'package:flutter_timeline_firebase/src/config/firebase_timeline_options.d import 'package:flutter_timeline_firebase/src/models/firebase_user_document.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; -class FirebaseUserService implements TimelineUserService { - FirebaseUserService({ +class FirebaseTimelineUserService implements TimelineUserService { + FirebaseTimelineUserService({ FirebaseApp? app, - options = const FirebaseTimelineOptions(), + FirebaseTimelineOptions? options, }) { var appInstance = app ?? Firebase.app(); _db = FirebaseFirestore.instanceFor(app: appInstance); - _options = options; + _options = options ?? const FirebaseTimelineOptions(); } late FirebaseFirestore _db; diff --git a/packages/flutter_timeline_interface/lib/flutter_timeline_interface.dart b/packages/flutter_timeline_interface/lib/flutter_timeline_interface.dart index d0da25d..8fb0bf9 100644 --- a/packages/flutter_timeline_interface/lib/flutter_timeline_interface.dart +++ b/packages/flutter_timeline_interface/lib/flutter_timeline_interface.dart @@ -9,5 +9,6 @@ export 'src/model/timeline_post.dart'; export 'src/model/timeline_poster.dart'; export 'src/model/timeline_reaction.dart'; export 'src/services/filter_service.dart'; +export 'src/services/timeline_post_service.dart'; export 'src/services/timeline_service.dart'; export 'src/services/user_service.dart'; diff --git a/packages/flutter_timeline_interface/lib/src/services/filter_service.dart b/packages/flutter_timeline_interface/lib/src/services/filter_service.dart index dc3441f..029dbec 100644 --- a/packages/flutter_timeline_interface/lib/src/services/filter_service.dart +++ b/packages/flutter_timeline_interface/lib/src/services/filter_service.dart @@ -4,7 +4,7 @@ import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; -mixin TimelineFilterService on TimelineService { +mixin TimelineFilterService on TimelinePostService { List filterPosts( String filterWord, Map options, diff --git a/packages/flutter_timeline_interface/lib/src/services/timeline_post_service.dart b/packages/flutter_timeline_interface/lib/src/services/timeline_post_service.dart new file mode 100644 index 0000000..4933c4e --- /dev/null +++ b/packages/flutter_timeline_interface/lib/src/services/timeline_post_service.dart @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +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 TimelinePostService with ChangeNotifier { + List posts = []; + + Future deletePost(TimelinePost post); + Future deletePostReaction(TimelinePost post, String reactionId); + Future createPost(TimelinePost post); + Future> fetchPosts(String? category); + Future fetchPost(TimelinePost post); + Future> fetchPostsPaginated(String? category, int limit); + TimelinePost? getPost(String postId); + List getPosts(String? category); + Future> refreshPosts(String? category); + Future fetchPostDetails(TimelinePost post); + Future reactToPost( + TimelinePost post, + TimelinePostReaction reaction, { + Uint8List image, + }); + Future likePost(String userId, TimelinePost post); + Future unlikePost(String userId, TimelinePost post); +} diff --git a/packages/flutter_timeline_interface/lib/src/services/timeline_service.dart b/packages/flutter_timeline_interface/lib/src/services/timeline_service.dart index 6f4907d..4e06b2c 100644 --- a/packages/flutter_timeline_interface/lib/src/services/timeline_service.dart +++ b/packages/flutter_timeline_interface/lib/src/services/timeline_service.dart @@ -1,31 +1,12 @@ -// SPDX-FileCopyrightText: 2023 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause +import 'package:flutter_timeline_interface/src/services/timeline_post_service.dart'; +import 'package:flutter_timeline_interface/src/services/user_service.dart'; -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 with ChangeNotifier { - List posts = []; - - Future deletePost(TimelinePost post); - Future deletePostReaction(TimelinePost post, String reactionId); - Future createPost(TimelinePost post); - Future> fetchPosts(String? category); - Future fetchPost(TimelinePost post); - Future> fetchPostsPaginated(String? category, int limit); - TimelinePost? getPost(String postId); - List getPosts(String? category); - Future> refreshPosts(String? category); - Future fetchPostDetails(TimelinePost post); - Future reactToPost( - TimelinePost post, - TimelinePostReaction reaction, { - Uint8List image, +class TimelineService { + TimelineService({ + required this.postService, + this.userService, }); - Future likePost(String userId, TimelinePost post); - Future unlikePost(String userId, TimelinePost post); + + final TimelinePostService postService; + final TimelineUserService? userService; } diff --git a/packages/flutter_timeline_view/lib/flutter_timeline_view.dart b/packages/flutter_timeline_view/lib/flutter_timeline_view.dart index 88cdb20..1ef76bd 100644 --- a/packages/flutter_timeline_view/lib/flutter_timeline_view.dart +++ b/packages/flutter_timeline_view/lib/flutter_timeline_view.dart @@ -12,6 +12,7 @@ export 'src/screens/timeline_post_creation_screen.dart'; export 'src/screens/timeline_post_screen.dart'; export 'src/screens/timeline_screen.dart'; export 'src/screens/timeline_selection_screen.dart'; +export 'src/services/local_post_service.dart'; export 'src/widgets/category_selector.dart'; export 'src/widgets/category_selector_button.dart'; export 'src/widgets/timeline_post_widget.dart'; diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_post_creation_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_post_creation_screen.dart index 2238f4e..2a7c409 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_post_creation_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_post_creation_screen.dart @@ -82,7 +82,7 @@ class _TimelinePostCreationScreenState reactionEnabled: allowComments, image: image, ); - var newPost = await widget.service.createPost(post); + var newPost = await widget.service.postService.createPost(post); widget.onPostCreated.call(newPost); } diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart index 467da65..1c810e9 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart @@ -106,7 +106,8 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { Future loadPostDetails() async { try { - var loadedPost = await widget.service.fetchPostDetails(widget.post); + var loadedPost = + await widget.service.postService.fetchPostDetails(widget.post); setState(() { post = loadedPost; isLoading = false; @@ -157,8 +158,8 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { RefreshIndicator( onRefresh: () async { updatePost( - await widget.service.fetchPostDetails( - await widget.service.fetchPost( + await widget.service.postService.fetchPostDetails( + await widget.service.postService.fetchPost( post, ), ), @@ -269,12 +270,14 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { late TimelinePost result; if (!liked) { - result = await widget.service.likePost( + result = + await widget.service.postService.likePost( userId, post, ); } else { - result = await widget.service.unlikePost( + result = await widget.service.postService + .unlikePost( userId, post, ); @@ -303,7 +306,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { InkWell( onTap: () async { updatePost( - await widget.service.unlikePost( + await widget.service.postService.unlikePost( widget.userId, post, ), @@ -322,7 +325,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { InkWell( onTap: () async { updatePost( - await widget.service.likePost( + await widget.service.postService.likePost( widget.userId, post, ), @@ -450,7 +453,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { if (value == 'delete') { // Call service to delete reaction updatePost( - await widget.service + await widget.service.postService .deletePostReaction(post, reaction.id), ); } @@ -568,7 +571,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { ); if (result != null) { updatePost( - await widget.service.reactToPost( + await widget.service.postService.reactToPost( post, TimelinePostReaction( id: '', @@ -582,7 +585,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { } }, onReactionSubmit: (reaction) async => updatePost( - await widget.service.reactToPost( + await widget.service.postService.reactToPost( post, TimelinePostReaction( id: '', diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart index d367d87..9bd5a82 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -10,10 +10,10 @@ import 'package:flutter_timeline_view/flutter_timeline_view.dart'; class TimelineScreen extends StatefulWidget { const TimelineScreen({ - required this.userId, - required this.service, - required this.options, - required this.onPostTap, + this.userId = 'test_user', + this.service, + this.options = const TimelineOptions(), + this.onPostTap, this.scrollController, this.onUserTap, this.posts, @@ -27,7 +27,7 @@ class TimelineScreen extends StatefulWidget { final String userId; /// The service to use for fetching and manipulating posts - final TimelineService service; + final TimelineService? service; /// All the configuration options for the timelinescreens and widgets final TimelineOptions options; @@ -43,7 +43,7 @@ class TimelineScreen extends StatefulWidget { final List? posts; /// Called when a post is tapped - final Function(TimelinePost) onPostTap; + final Function(TimelinePost)? onPostTap; /// If this is not null, the user can tap on the user avatar or name final Function(String userId)? onUserTap; @@ -63,7 +63,10 @@ class _TimelineScreenState extends State { late var textFieldController = TextEditingController( text: widget.options.filterOptions.initialFilterWord, ); - late var service = widget.service; + late var service = widget.service ?? + TimelineService( + postService: LocalTimelinePostService(), + ); bool isLoading = true; @@ -86,14 +89,14 @@ class _TimelineScreenState extends State { // Build the list of posts return ListenableBuilder( - listenable: service, + listenable: service.postService, builder: (context, _) { - var posts = widget.posts ?? service.getPosts(category); + var posts = widget.posts ?? service.postService.getPosts(category); if (widget.filterEnabled && filterWord != null) { - if (service is TimelineFilterService?) { - posts = - (service as TimelineFilterService).filterPosts(filterWord!, {}); + if (service.postService is TimelineFilterService) { + posts = (service.postService as TimelineFilterService) + .filterPosts(filterWord!, {}); } else { debugPrint('Timeline service needs to mixin' ' with TimelineFilterService'); @@ -203,17 +206,41 @@ class _TimelineScreenState extends State { padding: widget.options.postPadding, child: widget.postWidgetBuilder?.call(post) ?? TimelinePostWidget( - service: widget.service, + service: service, userId: widget.userId, options: widget.options, post: post, - onTap: () => widget.onPostTap(post), - onTapLike: () async => - service.likePost(widget.userId, post), - onTapUnlike: () async => - service.unlikePost(widget.userId, post), + onTap: () async { + if (widget.onPostTap != null) { + widget.onPostTap!.call(post); + + return; + } + + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Scaffold( + body: TimelinePostScreen( + userId: 'test_user', + service: service, + options: widget.options, + post: post, + onPostDelete: () { + service.postService.deletePost(post); + Navigator.of(context).pop(); + }, + ), + ), + ), + ); + }, + onTapLike: () async => service.postService + .likePost(widget.userId, post), + onTapUnlike: () async => service.postService + .unlikePost(widget.userId, post), onPostDelete: () async => - service.deletePost(post), + service.postService.deletePost(post), onUserTap: widget.onUserTap, ), ), @@ -246,7 +273,7 @@ class _TimelineScreenState extends State { Future loadPosts() async { if (widget.posts != null) return; try { - await service.fetchPosts(category); + await service.postService.fetchPosts(category); setState(() { isLoading = false; }); diff --git a/packages/flutter_timeline/example/lib/services/timeline_service.dart b/packages/flutter_timeline_view/lib/src/services/local_post_service.dart similarity index 68% rename from packages/flutter_timeline/example/lib/services/timeline_service.dart rename to packages/flutter_timeline_view/lib/src/services/local_post_service.dart index 3788e41..3989df9 100644 --- a/packages/flutter_timeline/example/lib/services/timeline_service.dart +++ b/packages/flutter_timeline_view/lib/src/services/local_post_service.dart @@ -5,12 +5,11 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:flutter_timeline/flutter_timeline.dart'; +import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; -// ignore: depend_on_referenced_packages -import 'package:uuid/uuid.dart'; - -class TestTimelineService with ChangeNotifier implements TimelineService { +class LocalTimelinePostService + with ChangeNotifier + implements TimelinePostService { @override List posts = []; @@ -61,8 +60,11 @@ class TestTimelineService with ChangeNotifier implements TimelineService { var reactions = post.reactions ?? []; var updatedReactions = []; for (var reaction in reactions) { - updatedReactions.add(reaction.copyWith( - creator: const TimelinePosterUserModel(userId: 'test_user'))); + updatedReactions.add( + reaction.copyWith( + creator: const TimelinePosterUserModel(userId: 'test_user'), + ), + ); } var updatedPost = post.copyWith(reactions: updatedReactions); posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList(); @@ -150,10 +152,12 @@ class TestTimelineService with ChangeNotifier implements TimelineService { TimelinePostReaction reaction, { Uint8List? image, }) async { - var reactionId = const Uuid().v4(); + var reactionId = DateTime.now().millisecondsSinceEpoch.toString(); + var updatedReaction = reaction.copyWith( - id: reactionId, - creator: const TimelinePosterUserModel(userId: 'test_user')); + id: reactionId, + creator: const TimelinePosterUserModel(userId: 'test_user'), + ); var updatedPost = post.copyWith( reaction: post.reaction + 1, @@ -169,19 +173,45 @@ class TestTimelineService with ChangeNotifier implements TimelineService { return updatedPost; } - List getMockedPosts() { - return [ - TimelinePost( - id: 'Post0', - creatorId: 'test_user', - title: 'Post 0', - category: null, - content: "Post 0 content", - likes: 0, - reaction: 0, - createdAt: DateTime.now(), - reactionEnabled: false, - ) - ]; - } + List getMockedPosts() => [ + TimelinePost( + id: 'Post0', + creatorId: 'test_user', + title: 'Post 0', + category: null, + content: 'Standard post without image made by the current user', + likes: 0, + reaction: 0, + createdAt: DateTime.now(), + reactionEnabled: false, + ), + TimelinePost( + id: 'Post1', + creatorId: 'test_user2', + title: 'Post 1', + category: null, + content: 'Standard post with image made by a different user and ' + 'reactions enabled', + likes: 0, + reaction: 0, + createdAt: DateTime.now(), + reactionEnabled: false, + imageUrl: + 'https://s3-eu-west-1.amazonaws.com/sortlist-core-api/6qpvvqjtmniirpkvp8eg83bicnc2', + ), + TimelinePost( + id: 'Post2', + creatorId: 'test_user', + title: 'Post 2', + category: null, + content: 'Standard post with image made by the current user and' + ' reactions enabled', + likes: 0, + reaction: 0, + createdAt: DateTime.now(), + reactionEnabled: true, + imageUrl: + 'https://s3-eu-west-1.amazonaws.com/sortlist-core-api/6qpvvqjtmniirpkvp8eg83bicnc2', + ), + ]; } diff --git a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart index 513fd89..ff1976b 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart @@ -160,12 +160,14 @@ class _TimelinePostWidgetState extends State { late TimelinePost result; if (!liked) { - result = await widget.service.likePost( + result = + await widget.service.postService.likePost( userId, widget.post, ); } else { - result = await widget.service.unlikePost( + result = + await widget.service.postService.unlikePost( userId, widget.post, ); @@ -197,12 +199,12 @@ class _TimelinePostWidgetState extends State { widget.post.likedBy?.contains(userId) ?? false; if (!liked) { - await widget.service.likePost( + await widget.service.postService.likePost( userId, widget.post, ); } else { - await widget.service.unlikePost( + await widget.service.postService.unlikePost( userId, widget.post, );