Merge branch 'master' into feature/widgetbook

This commit is contained in:
Niels Gorter 2024-01-30 11:27:42 +01:00
commit 3bad6881ae
22 changed files with 732 additions and 574 deletions

View file

@ -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 path: packages/flutter_timeline_firebase
``` ```
Add the following code in your `main` function, before the runApp().
```
initializeDateFormatting();
```
## How to use ## How to use
To use the module within your Flutter-application with predefined `Go_router` routes you should add the following: To use the module within your Flutter-application with predefined `Go_router` routes you should add the following:
@ -35,7 +40,7 @@ List<GoRoute> getTimelineStoryRoutes() => getTimelineStoryRoutes(
service: FirebaseTimelineService(), service: FirebaseTimelineService(),
userService: FirebaseUserService(), userService: FirebaseUserService(),
userId: currentUserId, userId: currentUserId,
optionsBuilder: (context) {}, optionsBuilder: (context) => FirebaseOptions(),
), ),
); );
``` ```
@ -74,13 +79,12 @@ TimelineScreen(
userId: currentUserId, userId: currentUserId,
service: timelineService, service: timelineService,
options: timelineOptions, 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`. `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( TimelineScreen(

View file

@ -1,13 +1,12 @@
import 'package:example/config/config.dart'; import 'package:example/config/config.dart';
import 'package:example/services/timeline_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_timeline/flutter_timeline.dart'; import 'package:flutter_timeline/flutter_timeline.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
List<GoRoute> getTimelineRoutes() => getTimelineStoryRoutes( List<GoRoute> getTimelineRoutes() => getTimelineStoryRoutes(
getConfig( configuration: getConfig(TimelineService(
TestTimelineService(), postService: LocalTimelinePostService(),
), )),
); );
final _router = GoRouter( final _router = GoRouter(

View file

@ -1,5 +1,4 @@
import 'package:example/config/config.dart'; import 'package:example/config/config.dart';
import 'package:example/services/timeline_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_timeline/flutter_timeline.dart'; import 'package:flutter_timeline/flutter_timeline.dart';
@ -32,7 +31,8 @@ class MyHomePage extends StatefulWidget {
} }
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
var timelineService = TestTimelineService(); var timelineService =
TimelineService(postService: LocalTimelinePostService());
var timelineOptions = options; var timelineOptions = options;
@override @override
@ -64,7 +64,11 @@ class _MyHomePageState extends State<MyHomePage> {
], ],
), ),
body: SafeArea( body: SafeArea(
child: timeLineNavigatorUserStory(getConfig(timelineService), context), child: timeLineNavigatorUserStory(
configuration: getConfig(
timelineService,
),
context: context),
), ),
); );
} }

View file

@ -1,5 +1,5 @@
import 'package:example/config/config.dart'; import 'package:example/config/config.dart';
import 'package:example/services/timeline_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_timeline/flutter_timeline.dart'; import 'package:flutter_timeline/flutter_timeline.dart';
@ -32,7 +32,7 @@ class MyHomePage extends StatefulWidget {
} }
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
var timelineService = TestTimelineService(); var timelineService = TimelineService(postService: LocalTimelinePostService());
var timelineOptions = options; var timelineOptions = options;
@override @override
@ -42,6 +42,7 @@ class _MyHomePageState extends State<MyHomePage> {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
FloatingActionButton( FloatingActionButton(
heroTag: 'btn1',
onPressed: () { onPressed: () {
createPost(context, timelineService, timelineOptions); createPost(context, timelineService, timelineOptions);
}, },
@ -54,6 +55,7 @@ class _MyHomePageState extends State<MyHomePage> {
height: 8, height: 8,
), ),
FloatingActionButton( FloatingActionButton(
heroTag: 'btn2',
onPressed: () { onPressed: () {
generatePost(timelineService); generatePost(timelineService);
}, },
@ -64,31 +66,8 @@ class _MyHomePageState extends State<MyHomePage> {
), ),
], ],
), ),
body: SafeArea( body: const SafeArea(
child: TimelineScreen( 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();
},
),
),
),
);
},
),
), ),
); );
} }

View file

@ -1,11 +1,9 @@
import 'package:example/apps/widgets/screens/post_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_timeline/flutter_timeline.dart'; import 'package:flutter_timeline/flutter_timeline.dart';
TimelineUserStoryConfiguration getConfig(TimelineService service) { TimelineUserStoryConfiguration getConfig(TimelineService service) {
return TimelineUserStoryConfiguration( return TimelineUserStoryConfiguration(
service: service, service: service,
userService: TestUserService(),
userId: 'test_user', userId: 'test_user',
optionsBuilder: (context) => options); optionsBuilder: (context) => options);
} }
@ -56,9 +54,9 @@ void createPost(BuildContext context, TimelineService service,
} }
void generatePost(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( TimelinePost(
id: 'Post$amountOfPosts', id: 'Post$amountOfPosts',
creatorId: 'test_user', creatorId: 'test_user',

View file

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

View file

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

View file

@ -9,10 +9,9 @@ import 'package:flutter_timeline_view/flutter_timeline_view.dart';
@immutable @immutable
class TimelineUserStoryConfiguration { class TimelineUserStoryConfiguration {
const TimelineUserStoryConfiguration({ const TimelineUserStoryConfiguration({
required this.userId,
required this.service, required this.service,
required this.userService,
required this.optionsBuilder, required this.optionsBuilder,
this.userId = 'test_user',
this.openPageBuilder, this.openPageBuilder,
this.onPostTap, this.onPostTap,
this.onUserTap, this.onUserTap,
@ -25,8 +24,6 @@ class TimelineUserStoryConfiguration {
final TimelineService service; final TimelineService service;
final TimelineUserService userService;
final TimelineOptions Function(BuildContext context) optionsBuilder; final TimelineOptions Function(BuildContext context) optionsBuilder;
final Function(BuildContext context, String userId)? onUserTap; final Function(BuildContext context, String userId)? onUserTap;

View file

@ -6,5 +6,6 @@
library flutter_timeline_firebase; library flutter_timeline_firebase;
export 'src/config/firebase_timeline_options.dart'; 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_timeline_service.dart';
export 'src/service/firebase_user_service.dart'; export 'src/service/firebase_user_service.dart';

View file

@ -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<String, TimelinePosterUserModel> _users = {};
@override
List<TimelinePost> posts = [];
@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 fetchedPosts = <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);
fetchedPosts.add(post);
}
posts = fetchedPosts;
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,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_core/firebase_core.dart';
import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter_timeline_firebase/flutter_timeline_firebase.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:flutter_timeline_interface/flutter_timeline_interface.dart';
import 'package:uuid/uuid.dart';
class FirebaseTimelineService extends TimelineService with TimelineUserService { class FirebaseTimelineService implements TimelineService {
FirebaseTimelineService({ FirebaseTimelineService({
required TimelineUserService userService, this.options,
FirebaseApp? app, this.app,
options = const FirebaseTimelineOptions(), this.firebasePostService,
this.firebaseUserService,
}) { }) {
var appInstance = app ?? Firebase.app(); firebaseUserService ??= FirebaseTimelineUserService(
_db = FirebaseFirestore.instanceFor(app: appInstance); options: options,
_storage = FirebaseStorage.instanceFor(app: appInstance); app: app,
_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); firebasePostService ??= FirebaseTimelinePostService(
await postRef.update({ userService: userService,
'reaction': FieldValue.increment(1), options: options,
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]), app: app,
}); );
posts = posts
.map(
(p) => p.id == post.id ? updatedPost : p,
)
.toList();
notifyListeners();
return updatedPost;
} }
CollectionReference<FirebaseUserDocument> get _userCollection => _db final FirebaseTimelineOptions? options;
.collection(_options.usersCollectionName) final FirebaseApp? app;
.withConverter<FirebaseUserDocument>( TimelinePostService? firebasePostService;
fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson( TimelineUserService? firebaseUserService;
snapshot.data()!,
snapshot.id,
),
toFirestore: (user, _) => user.toJson(),
);
@override @override
Future<TimelinePosterUserModel?> getUser(String userId) async { TimelinePostService get postService {
if (_users.containsKey(userId)) { if (firebasePostService != null) {
return _users[userId]!; return firebasePostService!;
} } else {
var data = (await _userCollection.doc(userId).get()).data(); return FirebaseTimelinePostService(
userService: userService,
var user = data == null options: options,
? TimelinePosterUserModel(userId: userId) app: app,
: TimelinePosterUserModel(
userId: userId,
firstName: data.firstName,
lastName: data.lastName,
imageUrl: data.imageUrl,
); );
}
_users[userId] = user; }
return user; @override
TimelineUserService get userService {
if (firebaseUserService != null) {
return firebaseUserService!;
} else {
return FirebaseTimelineUserService(
options: options,
app: app,
);
}
} }
} }

View file

@ -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_firebase/src/models/firebase_user_document.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
class FirebaseUserService implements TimelineUserService { class FirebaseTimelineUserService implements TimelineUserService {
FirebaseUserService({ FirebaseTimelineUserService({
FirebaseApp? app, FirebaseApp? app,
options = const FirebaseTimelineOptions(), FirebaseTimelineOptions? options,
}) { }) {
var appInstance = app ?? Firebase.app(); var appInstance = app ?? Firebase.app();
_db = FirebaseFirestore.instanceFor(app: appInstance); _db = FirebaseFirestore.instanceFor(app: appInstance);
_options = options; _options = options ?? const FirebaseTimelineOptions();
} }
late FirebaseFirestore _db; late FirebaseFirestore _db;

View file

@ -9,5 +9,6 @@ export 'src/model/timeline_post.dart';
export 'src/model/timeline_poster.dart'; export 'src/model/timeline_poster.dart';
export 'src/model/timeline_reaction.dart'; export 'src/model/timeline_reaction.dart';
export 'src/services/filter_service.dart'; export 'src/services/filter_service.dart';
export 'src/services/timeline_post_service.dart';
export 'src/services/timeline_service.dart'; export 'src/services/timeline_service.dart';
export 'src/services/user_service.dart'; export 'src/services/user_service.dart';

View file

@ -4,7 +4,7 @@
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
mixin TimelineFilterService on TimelineService { mixin TimelineFilterService on TimelinePostService {
List<TimelinePost> filterPosts( List<TimelinePost> filterPosts(
String filterWord, String filterWord,
Map<String, dynamic> options, Map<String, dynamic> options,

View file

@ -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<TimelinePost> posts = [];
Future<void> deletePost(TimelinePost post);
Future<TimelinePost> deletePostReaction(TimelinePost post, String reactionId);
Future<TimelinePost> createPost(TimelinePost post);
Future<List<TimelinePost>> fetchPosts(String? category);
Future<TimelinePost> fetchPost(TimelinePost post);
Future<List<TimelinePost>> fetchPostsPaginated(String? category, int limit);
TimelinePost? getPost(String postId);
List<TimelinePost> getPosts(String? category);
Future<List<TimelinePost>> refreshPosts(String? category);
Future<TimelinePost> fetchPostDetails(TimelinePost post);
Future<TimelinePost> reactToPost(
TimelinePost post,
TimelinePostReaction reaction, {
Uint8List image,
});
Future<TimelinePost> likePost(String userId, TimelinePost post);
Future<TimelinePost> unlikePost(String userId, TimelinePost post);
}

View file

@ -1,31 +1,12 @@
// SPDX-FileCopyrightText: 2023 Iconica import 'package:flutter_timeline_interface/src/services/timeline_post_service.dart';
// import 'package:flutter_timeline_interface/src/services/user_service.dart';
// SPDX-License-Identifier: BSD-3-Clause
import 'dart:typed_data'; class TimelineService {
TimelineService({
import 'package:flutter/material.dart'; required this.postService,
import 'package:flutter_timeline_interface/src/model/timeline_post.dart'; this.userService,
import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart';
abstract class TimelineService with ChangeNotifier {
List<TimelinePost> posts = [];
Future<void> deletePost(TimelinePost post);
Future<TimelinePost> deletePostReaction(TimelinePost post, String reactionId);
Future<TimelinePost> createPost(TimelinePost post);
Future<List<TimelinePost>> fetchPosts(String? category);
Future<TimelinePost> fetchPost(TimelinePost post);
Future<List<TimelinePost>> fetchPostsPaginated(String? category, int limit);
TimelinePost? getPost(String postId);
List<TimelinePost> getPosts(String? category);
Future<List<TimelinePost>> refreshPosts(String? category);
Future<TimelinePost> fetchPostDetails(TimelinePost post);
Future<TimelinePost> reactToPost(
TimelinePost post,
TimelinePostReaction reaction, {
Uint8List image,
}); });
Future<TimelinePost> likePost(String userId, TimelinePost post);
Future<TimelinePost> unlikePost(String userId, TimelinePost post); final TimelinePostService postService;
final TimelineUserService? userService;
} }

View file

@ -12,6 +12,7 @@ export 'src/screens/timeline_post_creation_screen.dart';
export 'src/screens/timeline_post_screen.dart'; export 'src/screens/timeline_post_screen.dart';
export 'src/screens/timeline_screen.dart'; export 'src/screens/timeline_screen.dart';
export 'src/screens/timeline_selection_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.dart';
export 'src/widgets/category_selector_button.dart'; export 'src/widgets/category_selector_button.dart';
export 'src/widgets/timeline_post_widget.dart'; export 'src/widgets/timeline_post_widget.dart';

View file

@ -82,7 +82,7 @@ class _TimelinePostCreationScreenState
reactionEnabled: allowComments, reactionEnabled: allowComments,
image: image, image: image,
); );
var newPost = await widget.service.createPost(post); var newPost = await widget.service.postService.createPost(post);
widget.onPostCreated.call(newPost); widget.onPostCreated.call(newPost);
} }

View file

@ -106,7 +106,8 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
Future<void> loadPostDetails() async { Future<void> loadPostDetails() async {
try { try {
var loadedPost = await widget.service.fetchPostDetails(widget.post); var loadedPost =
await widget.service.postService.fetchPostDetails(widget.post);
setState(() { setState(() {
post = loadedPost; post = loadedPost;
isLoading = false; isLoading = false;
@ -157,8 +158,8 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
RefreshIndicator( RefreshIndicator(
onRefresh: () async { onRefresh: () async {
updatePost( updatePost(
await widget.service.fetchPostDetails( await widget.service.postService.fetchPostDetails(
await widget.service.fetchPost( await widget.service.postService.fetchPost(
post, post,
), ),
), ),
@ -269,12 +270,14 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
late TimelinePost result; late TimelinePost result;
if (!liked) { if (!liked) {
result = await widget.service.likePost( result =
await widget.service.postService.likePost(
userId, userId,
post, post,
); );
} else { } else {
result = await widget.service.unlikePost( result = await widget.service.postService
.unlikePost(
userId, userId,
post, post,
); );
@ -303,7 +306,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
InkWell( InkWell(
onTap: () async { onTap: () async {
updatePost( updatePost(
await widget.service.unlikePost( await widget.service.postService.unlikePost(
widget.userId, widget.userId,
post, post,
), ),
@ -322,7 +325,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
InkWell( InkWell(
onTap: () async { onTap: () async {
updatePost( updatePost(
await widget.service.likePost( await widget.service.postService.likePost(
widget.userId, widget.userId,
post, post,
), ),
@ -450,7 +453,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
if (value == 'delete') { if (value == 'delete') {
// Call service to delete reaction // Call service to delete reaction
updatePost( updatePost(
await widget.service await widget.service.postService
.deletePostReaction(post, reaction.id), .deletePostReaction(post, reaction.id),
); );
} }
@ -568,7 +571,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
); );
if (result != null) { if (result != null) {
updatePost( updatePost(
await widget.service.reactToPost( await widget.service.postService.reactToPost(
post, post,
TimelinePostReaction( TimelinePostReaction(
id: '', id: '',
@ -582,7 +585,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
} }
}, },
onReactionSubmit: (reaction) async => updatePost( onReactionSubmit: (reaction) async => updatePost(
await widget.service.reactToPost( await widget.service.postService.reactToPost(
post, post,
TimelinePostReaction( TimelinePostReaction(
id: '', id: '',

View file

@ -10,10 +10,10 @@ import 'package:flutter_timeline_view/flutter_timeline_view.dart';
class TimelineScreen extends StatefulWidget { class TimelineScreen extends StatefulWidget {
const TimelineScreen({ const TimelineScreen({
required this.userId, this.userId = 'test_user',
required this.service, this.service,
required this.options, this.options = const TimelineOptions(),
required this.onPostTap, this.onPostTap,
this.scrollController, this.scrollController,
this.onUserTap, this.onUserTap,
this.posts, this.posts,
@ -27,7 +27,7 @@ class TimelineScreen extends StatefulWidget {
final String userId; final String userId;
/// The service to use for fetching and manipulating posts /// The service to use for fetching and manipulating posts
final TimelineService service; final TimelineService? service;
/// All the configuration options for the timelinescreens and widgets /// All the configuration options for the timelinescreens and widgets
final TimelineOptions options; final TimelineOptions options;
@ -43,7 +43,7 @@ class TimelineScreen extends StatefulWidget {
final List<TimelinePost>? posts; final List<TimelinePost>? posts;
/// Called when a post is tapped /// 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 /// 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;
@ -63,7 +63,10 @@ class _TimelineScreenState extends State<TimelineScreen> {
late var textFieldController = TextEditingController( late var textFieldController = TextEditingController(
text: widget.options.filterOptions.initialFilterWord, text: widget.options.filterOptions.initialFilterWord,
); );
late var service = widget.service; late var service = widget.service ??
TimelineService(
postService: LocalTimelinePostService(),
);
bool isLoading = true; bool isLoading = true;
@ -86,14 +89,14 @@ class _TimelineScreenState extends State<TimelineScreen> {
// Build the list of posts // Build the list of posts
return ListenableBuilder( return ListenableBuilder(
listenable: service, listenable: service.postService,
builder: (context, _) { builder: (context, _) {
var posts = widget.posts ?? service.getPosts(category); var posts = widget.posts ?? service.postService.getPosts(category);
if (widget.filterEnabled && filterWord != null) { if (widget.filterEnabled && filterWord != null) {
if (service is TimelineFilterService?) { if (service.postService is TimelineFilterService) {
posts = posts = (service.postService as TimelineFilterService)
(service as TimelineFilterService).filterPosts(filterWord!, {}); .filterPosts(filterWord!, {});
} else { } else {
debugPrint('Timeline service needs to mixin' debugPrint('Timeline service needs to mixin'
' with TimelineFilterService'); ' with TimelineFilterService');
@ -203,17 +206,41 @@ class _TimelineScreenState extends State<TimelineScreen> {
padding: widget.options.postPadding, padding: widget.options.postPadding,
child: widget.postWidgetBuilder?.call(post) ?? child: widget.postWidgetBuilder?.call(post) ??
TimelinePostWidget( TimelinePostWidget(
service: widget.service, service: service,
userId: widget.userId, userId: widget.userId,
options: widget.options, options: widget.options,
post: post, post: post,
onTap: () => widget.onPostTap(post), onTap: () async {
onTapLike: () async => if (widget.onPostTap != null) {
service.likePost(widget.userId, post), widget.onPostTap!.call(post);
onTapUnlike: () async =>
service.unlikePost(widget.userId, 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 => onPostDelete: () async =>
service.deletePost(post), service.postService.deletePost(post),
onUserTap: widget.onUserTap, onUserTap: widget.onUserTap,
), ),
), ),
@ -246,7 +273,7 @@ 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 {
await service.fetchPosts(category); await service.postService.fetchPosts(category);
setState(() { setState(() {
isLoading = false; isLoading = false;
}); });

View file

@ -5,12 +5,11 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; 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 class LocalTimelinePostService
import 'package:uuid/uuid.dart'; with ChangeNotifier
implements TimelinePostService {
class TestTimelineService with ChangeNotifier implements TimelineService {
@override @override
List<TimelinePost> posts = []; List<TimelinePost> posts = [];
@ -61,8 +60,11 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
var reactions = post.reactions ?? []; var reactions = post.reactions ?? [];
var updatedReactions = <TimelinePostReaction>[]; var updatedReactions = <TimelinePostReaction>[];
for (var reaction in reactions) { for (var reaction in reactions) {
updatedReactions.add(reaction.copyWith( updatedReactions.add(
creator: const TimelinePosterUserModel(userId: 'test_user'))); reaction.copyWith(
creator: const TimelinePosterUserModel(userId: 'test_user'),
),
);
} }
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();
@ -150,10 +152,12 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
TimelinePostReaction reaction, { TimelinePostReaction reaction, {
Uint8List? image, Uint8List? image,
}) async { }) async {
var reactionId = const Uuid().v4(); var reactionId = DateTime.now().millisecondsSinceEpoch.toString();
var updatedReaction = reaction.copyWith( var updatedReaction = reaction.copyWith(
id: reactionId, id: reactionId,
creator: const TimelinePosterUserModel(userId: 'test_user')); creator: const TimelinePosterUserModel(userId: 'test_user'),
);
var updatedPost = post.copyWith( var updatedPost = post.copyWith(
reaction: post.reaction + 1, reaction: post.reaction + 1,
@ -169,19 +173,45 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
return updatedPost; return updatedPost;
} }
List<TimelinePost> getMockedPosts() { List<TimelinePost> getMockedPosts() => [
return [
TimelinePost( TimelinePost(
id: 'Post0', id: 'Post0',
creatorId: 'test_user', creatorId: 'test_user',
title: 'Post 0', title: 'Post 0',
category: null, category: null,
content: "Post 0 content", content: 'Standard post without image made by the current user',
likes: 0, likes: 0,
reaction: 0, reaction: 0,
createdAt: DateTime.now(), createdAt: DateTime.now(),
reactionEnabled: false, 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',
),
]; ];
} }
}

View file

@ -160,12 +160,14 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
late TimelinePost result; late TimelinePost result;
if (!liked) { if (!liked) {
result = await widget.service.likePost( result =
await widget.service.postService.likePost(
userId, userId,
widget.post, widget.post,
); );
} else { } else {
result = await widget.service.unlikePost( result =
await widget.service.postService.unlikePost(
userId, userId,
widget.post, widget.post,
); );
@ -197,12 +199,12 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
widget.post.likedBy?.contains(userId) ?? false; widget.post.likedBy?.contains(userId) ?? false;
if (!liked) { if (!liked) {
await widget.service.likePost( await widget.service.postService.likePost(
userId, userId,
widget.post, widget.post,
); );
} else { } else {
await widget.service.unlikePost( await widget.service.postService.unlikePost(
userId, userId,
widget.post, widget.post,
); );