mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 10:33:44 +02:00
Merge branch 'master' into feature/widgetbook
This commit is contained in:
commit
3bad6881ae
22 changed files with 732 additions and 574 deletions
10
README.md
10
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<GoRoute> 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(
|
||||
|
|
|
@ -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<GoRoute> getTimelineRoutes() => getTimelineStoryRoutes(
|
||||
getConfig(
|
||||
TestTimelineService(),
|
||||
),
|
||||
configuration: getConfig(TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
)),
|
||||
);
|
||||
|
||||
final _router = GoRouter(
|
||||
|
|
|
@ -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<MyHomePage> {
|
||||
var timelineService = TestTimelineService();
|
||||
var timelineService =
|
||||
TimelineService(postService: LocalTimelinePostService());
|
||||
var timelineOptions = options;
|
||||
|
||||
@override
|
||||
|
@ -64,7 +64,11 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: timeLineNavigatorUserStory(getConfig(timelineService), context),
|
||||
child: timeLineNavigatorUserStory(
|
||||
configuration: getConfig(
|
||||
timelineService,
|
||||
),
|
||||
context: context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<MyHomePage> {
|
||||
var timelineService = TestTimelineService();
|
||||
var timelineService = TimelineService(postService: LocalTimelinePostService());
|
||||
var timelineOptions = options;
|
||||
|
||||
@override
|
||||
|
@ -42,6 +42,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
heroTag: 'btn1',
|
||||
onPressed: () {
|
||||
createPost(context, timelineService, timelineOptions);
|
||||
},
|
||||
|
@ -54,6 +55,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
height: 8,
|
||||
),
|
||||
FloatingActionButton(
|
||||
heroTag: 'btn2',
|
||||
onPressed: () {
|
||||
generatePost(timelineService);
|
||||
},
|
||||
|
@ -64,31 +66,8 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
),
|
||||
],
|
||||
),
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -5,47 +5,80 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline/flutter_timeline.dart';
|
||||
|
||||
Widget timeLineNavigatorUserStory(
|
||||
TimelineUserStoryConfiguration configuration,
|
||||
BuildContext context,
|
||||
) =>
|
||||
_timelineScreenRoute(configuration, context);
|
||||
Widget timeLineNavigatorUserStory({
|
||||
required BuildContext context,
|
||||
TimelineUserStoryConfiguration? configuration,
|
||||
}) {
|
||||
var config = configuration ??
|
||||
TimelineUserStoryConfiguration(
|
||||
userId: 'test_user',
|
||||
service: TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
),
|
||||
optionsBuilder: (context) => const TimelineOptions(),
|
||||
);
|
||||
|
||||
Widget _timelineScreenRoute(
|
||||
TimelineUserStoryConfiguration configuration,
|
||||
BuildContext context,
|
||||
) =>
|
||||
TimelineScreen(
|
||||
service: configuration.service,
|
||||
options: configuration.optionsBuilder(context),
|
||||
userId: configuration.userId,
|
||||
return _timelineScreenRoute(configuration: config, context: context);
|
||||
}
|
||||
|
||||
Widget _timelineScreenRoute({
|
||||
required BuildContext context,
|
||||
TimelineUserStoryConfiguration? configuration,
|
||||
}) {
|
||||
var config = configuration ??
|
||||
TimelineUserStoryConfiguration(
|
||||
userId: 'test_user',
|
||||
service: TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
),
|
||||
optionsBuilder: (context) => const TimelineOptions(),
|
||||
);
|
||||
|
||||
return TimelineScreen(
|
||||
service: config.service,
|
||||
options: config.optionsBuilder(context),
|
||||
userId: config.userId,
|
||||
onPostTap: (post) async =>
|
||||
configuration.onPostTap?.call(context, post) ??
|
||||
config.onPostTap?.call(context, post) ??
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
_postDetailScreenRoute(configuration, context, post),
|
||||
builder: (context) => _postDetailScreenRoute(
|
||||
configuration: config,
|
||||
context: context,
|
||||
post: post,
|
||||
),
|
||||
),
|
||||
),
|
||||
onUserTap: (userId) {
|
||||
configuration.onUserTap?.call(context, userId);
|
||||
config.onUserTap?.call(context, userId);
|
||||
},
|
||||
filterEnabled: configuration.filterEnabled,
|
||||
postWidgetBuilder: configuration.postWidgetBuilder,
|
||||
filterEnabled: config.filterEnabled,
|
||||
postWidgetBuilder: config.postWidgetBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _postDetailScreenRoute({
|
||||
required BuildContext context,
|
||||
required TimelinePost post,
|
||||
TimelineUserStoryConfiguration? configuration,
|
||||
}) {
|
||||
var config = configuration ??
|
||||
TimelineUserStoryConfiguration(
|
||||
userId: 'test_user',
|
||||
service: TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
),
|
||||
optionsBuilder: (context) => const TimelineOptions(),
|
||||
);
|
||||
|
||||
Widget _postDetailScreenRoute(
|
||||
TimelineUserStoryConfiguration configuration,
|
||||
BuildContext context,
|
||||
TimelinePost post,
|
||||
) =>
|
||||
TimelinePostScreen(
|
||||
userId: configuration.userId,
|
||||
service: configuration.service,
|
||||
options: configuration.optionsBuilder(context),
|
||||
return TimelinePostScreen(
|
||||
userId: config.userId,
|
||||
service: config.service,
|
||||
options: config.optionsBuilder(context),
|
||||
post: post,
|
||||
onPostDelete: () async {
|
||||
configuration.onPostDelete?.call(context, post) ??
|
||||
await configuration.service.deletePost(post);
|
||||
config.onPostDelete?.call(context, post) ??
|
||||
await config.service.postService.deletePost(post);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,37 +3,43 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline/flutter_timeline.dart';
|
||||
import 'package:flutter_timeline/src/go_router.dart';
|
||||
import 'package:flutter_timeline/src/models/timeline_configuration.dart';
|
||||
import 'package:flutter_timeline/src/routes.dart';
|
||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
List<GoRoute> getTimelineStoryRoutes(
|
||||
TimelineUserStoryConfiguration configuration,
|
||||
) =>
|
||||
<GoRoute>[
|
||||
List<GoRoute> getTimelineStoryRoutes({
|
||||
TimelineUserStoryConfiguration? configuration,
|
||||
}) {
|
||||
var config = configuration ?? TimelineUserStoryConfiguration(
|
||||
userId: 'test_user',
|
||||
service: TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
),
|
||||
optionsBuilder: (context) => const TimelineOptions(),
|
||||
);
|
||||
|
||||
return <GoRoute>[
|
||||
GoRoute(
|
||||
path: TimelineUserStoryRoutes.timelineHome,
|
||||
pageBuilder: (context, state) {
|
||||
var timelineScreen = TimelineScreen(
|
||||
userId: configuration.userId,
|
||||
onUserTap: (user) => configuration.onUserTap?.call(context, user),
|
||||
service: configuration.service,
|
||||
options: configuration.optionsBuilder(context),
|
||||
userId: config.userId,
|
||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
||||
service: config.service,
|
||||
options: config.optionsBuilder(context),
|
||||
onPostTap: (post) async =>
|
||||
configuration.onPostTap?.call(context, post) ??
|
||||
config.onPostTap?.call(context, post) ??
|
||||
await context.push(
|
||||
TimelineUserStoryRoutes.timelineViewPath(post.id),
|
||||
),
|
||||
filterEnabled: configuration.filterEnabled,
|
||||
postWidgetBuilder: configuration.postWidgetBuilder,
|
||||
filterEnabled: config.filterEnabled,
|
||||
postWidgetBuilder: config.postWidgetBuilder,
|
||||
);
|
||||
|
||||
return buildScreenWithoutTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: configuration.openPageBuilder?.call(
|
||||
child: config.openPageBuilder?.call(
|
||||
context,
|
||||
timelineScreen,
|
||||
) ??
|
||||
|
@ -47,21 +53,21 @@ List<GoRoute> getTimelineStoryRoutes(
|
|||
path: TimelineUserStoryRoutes.timelineView,
|
||||
pageBuilder: (context, state) {
|
||||
var post =
|
||||
configuration.service.getPost(state.pathParameters['post']!)!;
|
||||
config.service.postService.getPost(state.pathParameters['post']!)!;
|
||||
|
||||
var timelinePostWidget = TimelinePostScreen(
|
||||
userId: configuration.userId,
|
||||
options: configuration.optionsBuilder(context),
|
||||
service: configuration.service,
|
||||
userId: config.userId,
|
||||
options: config.optionsBuilder(context),
|
||||
service: config.service,
|
||||
post: post,
|
||||
onPostDelete: () => configuration.onPostDelete?.call(context, post),
|
||||
onUserTap: (user) => configuration.onUserTap?.call(context, user),
|
||||
onPostDelete: () => config.onPostDelete?.call(context, post),
|
||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
||||
);
|
||||
|
||||
return buildScreenWithoutTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: configuration.openPageBuilder?.call(
|
||||
child: config.openPageBuilder?.call(
|
||||
context,
|
||||
timelinePostWidget,
|
||||
) ??
|
||||
|
@ -72,3 +78,4 @@ List<GoRoute> getTimelineStoryRoutes(
|
|||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
late FirebaseFirestore _db;
|
||||
late FirebaseStorage _storage;
|
||||
late TimelineUserService _userService;
|
||||
late FirebaseTimelineOptions _options;
|
||||
|
||||
final Map<String, TimelinePosterUserModel> _users = {};
|
||||
|
||||
@override
|
||||
Future<TimelinePost> createPost(TimelinePost post) async {
|
||||
var postId = const Uuid().v4();
|
||||
var user = await _userService.getUser(post.creatorId);
|
||||
var updatedPost = post.copyWith(id: postId, creator: user);
|
||||
if (post.image != null) {
|
||||
var imageRef =
|
||||
_storage.ref().child('${_options.timelineCollectionName}/$postId');
|
||||
var result = await imageRef.putData(post.image!);
|
||||
var imageUrl = await result.ref.getDownloadURL();
|
||||
updatedPost = updatedPost.copyWith(imageUrl: imageUrl);
|
||||
}
|
||||
var postRef =
|
||||
_db.collection(_options.timelineCollectionName).doc(updatedPost.id);
|
||||
await postRef.set(updatedPost.toJson());
|
||||
posts.add(updatedPost);
|
||||
notifyListeners();
|
||||
return updatedPost;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deletePost(TimelinePost post) async {
|
||||
posts = posts.where((element) => element.id != post.id).toList();
|
||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||
await postRef.delete();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TimelinePost> deletePostReaction(
|
||||
TimelinePost post,
|
||||
String reactionId,
|
||||
) async {
|
||||
if (post.reactions != null && post.reactions!.isNotEmpty) {
|
||||
var reaction =
|
||||
post.reactions!.firstWhere((element) => element.id == reactionId);
|
||||
var updatedPost = post.copyWith(
|
||||
reaction: post.reaction - 1,
|
||||
reactions: (post.reactions ?? [])..remove(reaction),
|
||||
);
|
||||
posts = posts
|
||||
.map(
|
||||
(p) => p.id == post.id ? updatedPost : p,
|
||||
)
|
||||
.toList();
|
||||
var postRef =
|
||||
_db.collection(_options.timelineCollectionName).doc(post.id);
|
||||
await postRef.update({
|
||||
'reaction': FieldValue.increment(-1),
|
||||
'reactions': FieldValue.arrayRemove(
|
||||
[reaction.toJsonWithMicroseconds()],
|
||||
),
|
||||
});
|
||||
notifyListeners();
|
||||
return updatedPost;
|
||||
}
|
||||
return post;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TimelinePost> fetchPostDetails(TimelinePost post) async {
|
||||
var reactions = post.reactions ?? [];
|
||||
var updatedReactions = <TimelinePostReaction>[];
|
||||
for (var reaction in reactions) {
|
||||
var user = await _userService.getUser(reaction.creatorId);
|
||||
if (user != null) {
|
||||
updatedReactions.add(reaction.copyWith(creator: user));
|
||||
}
|
||||
}
|
||||
var updatedPost = post.copyWith(reactions: updatedReactions);
|
||||
posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
||||
notifyListeners();
|
||||
return updatedPost;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TimelinePost>> fetchPosts(String? category) async {
|
||||
debugPrint('fetching posts from firebase with category: $category');
|
||||
var snapshot = (category != null)
|
||||
? await _db
|
||||
.collection(_options.timelineCollectionName)
|
||||
.where('category', isEqualTo: category)
|
||||
.get()
|
||||
: await _db.collection(_options.timelineCollectionName).get();
|
||||
|
||||
var posts = <TimelinePost>[];
|
||||
for (var doc in snapshot.docs) {
|
||||
var data = doc.data();
|
||||
var user = await _userService.getUser(data['creator_id']);
|
||||
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
||||
posts.add(post);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return posts;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TimelinePost>> fetchPostsPaginated(
|
||||
String? category,
|
||||
int limit,
|
||||
) async {
|
||||
// only take posts that are in our category
|
||||
var oldestPost = posts
|
||||
.where(
|
||||
(element) => category == null || element.category == category,
|
||||
)
|
||||
.fold(
|
||||
posts.first,
|
||||
(previousValue, element) =>
|
||||
(previousValue.createdAt.isBefore(element.createdAt))
|
||||
? previousValue
|
||||
: element,
|
||||
);
|
||||
var snapshot = (category != null)
|
||||
? await _db
|
||||
.collection(_options.timelineCollectionName)
|
||||
.where('category', isEqualTo: category)
|
||||
.orderBy('created_at', descending: true)
|
||||
.startAfter([oldestPost])
|
||||
.limit(limit)
|
||||
.get()
|
||||
: await _db
|
||||
.collection(_options.timelineCollectionName)
|
||||
.orderBy('created_at', descending: true)
|
||||
.startAfter([oldestPost.createdAt])
|
||||
.limit(limit)
|
||||
.get();
|
||||
// add the new posts to the list
|
||||
var newPosts = <TimelinePost>[];
|
||||
for (var doc in snapshot.docs) {
|
||||
var data = doc.data();
|
||||
var user = await _userService.getUser(data['creator_id']);
|
||||
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
||||
newPosts.add(post);
|
||||
}
|
||||
posts = [...posts, ...newPosts];
|
||||
notifyListeners();
|
||||
return newPosts;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TimelinePost> fetchPost(TimelinePost post) async {
|
||||
var doc = await _db
|
||||
.collection(_options.timelineCollectionName)
|
||||
.doc(post.id)
|
||||
.get();
|
||||
var data = doc.data();
|
||||
if (data == null) return post;
|
||||
var user = await _userService.getUser(data['creator_id']);
|
||||
var updatedPost = TimelinePost.fromJson(doc.id, data).copyWith(
|
||||
creator: user,
|
||||
);
|
||||
posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
||||
notifyListeners();
|
||||
return updatedPost;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TimelinePost>> refreshPosts(String? category) async {
|
||||
// fetch all posts between now and the newest posts we have
|
||||
var newestPostWeHave = posts
|
||||
.where(
|
||||
(element) => category == null || element.category == category,
|
||||
)
|
||||
.fold(
|
||||
posts.first,
|
||||
(previousValue, element) =>
|
||||
(previousValue.createdAt.isAfter(element.createdAt))
|
||||
? previousValue
|
||||
: element,
|
||||
);
|
||||
var snapshot = (category != null)
|
||||
? await _db
|
||||
.collection(_options.timelineCollectionName)
|
||||
.where('category', isEqualTo: category)
|
||||
.orderBy('created_at', descending: true)
|
||||
.endBefore([newestPostWeHave.createdAt]).get()
|
||||
: await _db
|
||||
.collection(_options.timelineCollectionName)
|
||||
.orderBy('created_at', descending: true)
|
||||
.endBefore([newestPostWeHave.createdAt]).get();
|
||||
// add the new posts to the list
|
||||
var newPosts = <TimelinePost>[];
|
||||
for (var doc in snapshot.docs) {
|
||||
var data = doc.data();
|
||||
var user = await _userService.getUser(data['creator_id']);
|
||||
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
||||
newPosts.add(post);
|
||||
}
|
||||
posts = [...posts, ...newPosts];
|
||||
notifyListeners();
|
||||
return newPosts;
|
||||
}
|
||||
|
||||
@override
|
||||
TimelinePost? getPost(String postId) =>
|
||||
(posts.any((element) => element.id == postId))
|
||||
? posts.firstWhere((element) => element.id == postId)
|
||||
: null;
|
||||
|
||||
@override
|
||||
List<TimelinePost> getPosts(String? category) => posts
|
||||
.where((element) => category == null || element.category == category)
|
||||
.toList();
|
||||
|
||||
@override
|
||||
Future<TimelinePost> likePost(String userId, TimelinePost post) async {
|
||||
// update the post with the new like
|
||||
var updatedPost = post.copyWith(
|
||||
likes: post.likes + 1,
|
||||
likedBy: post.likedBy?..add(userId),
|
||||
);
|
||||
posts = posts
|
||||
.map(
|
||||
(p) => p.id == post.id ? updatedPost : p,
|
||||
)
|
||||
.toList();
|
||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||
await postRef.update({
|
||||
'likes': FieldValue.increment(1),
|
||||
'liked_by': FieldValue.arrayUnion([userId]),
|
||||
});
|
||||
notifyListeners();
|
||||
return updatedPost;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TimelinePost> unlikePost(String userId, TimelinePost post) async {
|
||||
// update the post with the new like
|
||||
var updatedPost = post.copyWith(
|
||||
likes: post.likes - 1,
|
||||
likedBy: post.likedBy?..remove(userId),
|
||||
);
|
||||
posts = posts
|
||||
.map(
|
||||
(p) => p.id == post.id ? updatedPost : p,
|
||||
)
|
||||
.toList();
|
||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||
await postRef.update({
|
||||
'likes': FieldValue.increment(-1),
|
||||
'liked_by': FieldValue.arrayRemove([userId]),
|
||||
});
|
||||
notifyListeners();
|
||||
return updatedPost;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TimelinePost> reactToPost(
|
||||
TimelinePost post,
|
||||
TimelinePostReaction reaction, {
|
||||
Uint8List? image,
|
||||
}) async {
|
||||
var reactionId = const Uuid().v4();
|
||||
// also fetch the user information and add it to the reaction
|
||||
var user = await _userService.getUser(reaction.creatorId);
|
||||
var updatedReaction = reaction.copyWith(id: reactionId, creator: user);
|
||||
if (image != null) {
|
||||
var imageRef = _storage
|
||||
.ref()
|
||||
.child('${_options.timelineCollectionName}/${post.id}/$reactionId}');
|
||||
var result = await imageRef.putData(image);
|
||||
var imageUrl = await result.ref.getDownloadURL();
|
||||
updatedReaction = updatedReaction.copyWith(imageUrl: imageUrl);
|
||||
}
|
||||
|
||||
var updatedPost = post.copyWith(
|
||||
reaction: post.reaction + 1,
|
||||
reactions: post.reactions?..add(updatedReaction),
|
||||
firebaseUserService ??= FirebaseTimelineUserService(
|
||||
options: options,
|
||||
app: app,
|
||||
);
|
||||
|
||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||
await postRef.update({
|
||||
'reaction': FieldValue.increment(1),
|
||||
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]),
|
||||
});
|
||||
posts = posts
|
||||
.map(
|
||||
(p) => p.id == post.id ? updatedPost : p,
|
||||
)
|
||||
.toList();
|
||||
notifyListeners();
|
||||
return updatedPost;
|
||||
firebasePostService ??= FirebaseTimelinePostService(
|
||||
userService: userService,
|
||||
options: options,
|
||||
app: app,
|
||||
);
|
||||
}
|
||||
|
||||
CollectionReference<FirebaseUserDocument> get _userCollection => _db
|
||||
.collection(_options.usersCollectionName)
|
||||
.withConverter<FirebaseUserDocument>(
|
||||
fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson(
|
||||
snapshot.data()!,
|
||||
snapshot.id,
|
||||
),
|
||||
toFirestore: (user, _) => user.toJson(),
|
||||
);
|
||||
final FirebaseTimelineOptions? options;
|
||||
final FirebaseApp? app;
|
||||
TimelinePostService? firebasePostService;
|
||||
TimelineUserService? firebaseUserService;
|
||||
|
||||
@override
|
||||
Future<TimelinePosterUserModel?> getUser(String userId) async {
|
||||
if (_users.containsKey(userId)) {
|
||||
return _users[userId]!;
|
||||
}
|
||||
var data = (await _userCollection.doc(userId).get()).data();
|
||||
|
||||
var user = data == null
|
||||
? TimelinePosterUserModel(userId: userId)
|
||||
: TimelinePosterUserModel(
|
||||
userId: userId,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
imageUrl: data.imageUrl,
|
||||
TimelinePostService get postService {
|
||||
if (firebasePostService != null) {
|
||||
return firebasePostService!;
|
||||
} else {
|
||||
return FirebaseTimelinePostService(
|
||||
userService: userService,
|
||||
options: options,
|
||||
app: app,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_users[userId] = user;
|
||||
|
||||
return user;
|
||||
@override
|
||||
TimelineUserService get userService {
|
||||
if (firebaseUserService != null) {
|
||||
return firebaseUserService!;
|
||||
} else {
|
||||
return FirebaseTimelineUserService(
|
||||
options: options,
|
||||
app: app,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
|
||||
mixin TimelineFilterService on TimelineService {
|
||||
mixin TimelineFilterService on TimelinePostService {
|
||||
List<TimelinePost> filterPosts(
|
||||
String filterWord,
|
||||
Map<String, dynamic> options,
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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<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,
|
||||
class TimelineService {
|
||||
TimelineService({
|
||||
required this.postService,
|
||||
this.userService,
|
||||
});
|
||||
Future<TimelinePost> likePost(String userId, TimelinePost post);
|
||||
Future<TimelinePost> unlikePost(String userId, TimelinePost post);
|
||||
|
||||
final TimelinePostService postService;
|
||||
final TimelineUserService? userService;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -106,7 +106,8 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
|
||||
Future<void> 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: '',
|
||||
|
|
|
@ -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<TimelinePost>? 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<TimelineScreen> {
|
|||
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<TimelineScreen> {
|
|||
|
||||
// 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<TimelineScreen> {
|
|||
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<TimelineScreen> {
|
|||
Future<void> loadPosts() async {
|
||||
if (widget.posts != null) return;
|
||||
try {
|
||||
await service.fetchPosts(category);
|
||||
await service.postService.fetchPosts(category);
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
|
|
|
@ -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<TimelinePost> posts = [];
|
||||
|
||||
|
@ -61,8 +60,11 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
|
|||
var reactions = post.reactions ?? [];
|
||||
var updatedReactions = <TimelinePostReaction>[];
|
||||
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'));
|
||||
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<TimelinePost> getMockedPosts() {
|
||||
return [
|
||||
List<TimelinePost> getMockedPosts() => [
|
||||
TimelinePost(
|
||||
id: 'Post0',
|
||||
creatorId: 'test_user',
|
||||
title: 'Post 0',
|
||||
category: null,
|
||||
content: "Post 0 content",
|
||||
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',
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -160,12 +160,14 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
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<TimelinePostWidget> {
|
|||
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,
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue