Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update comment count when adding a comment #349

Merged
merged 24 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d627cfe
feat(comment-count): add post comment count view model
JoachimFavre May 25, 2024
04c6e01
feat(comment-count): make UI use comment count view model
JoachimFavre May 25, 2024
9c4c2a5
feat(comment-count): refresh the comment count when the comment list …
JoachimFavre May 25, 2024
feff9b3
docs(comment-count): add documentation to view-model
JoachimFavre May 25, 2024
4e9beda
refactor(comment-count): remove unnecessary await
JoachimFavre May 25, 2024
3f56292
feat(comment-count): refresh the comment count when a comment is deleted
JoachimFavre May 25, 2024
e5c2f6c
docs(comment-count): improve documentation about auto-dispose
JoachimFavre May 25, 2024
7b901e4
feat(delete-comment-action): remove assumption that user has a single…
JoachimFavre May 25, 2024
ef39f27
test(comment-count): verify comment count update on navigation
JoachimFavre May 25, 2024
810bf09
fix(comment-count-view-model): set loading state while refreshing
JoachimFavre May 26, 2024
70ac3f7
fix(comment-count): use AsyncValue::value instead of valueOrNull
JoachimFavre May 26, 2024
35238cc
refactor(comment-count): remove setCount function
JoachimFavre May 26, 2024
af713c8
fix(home-view-model-override): also override the comment count provider
JoachimFavre May 26, 2024
9067b9c
docs: add documentation to MockPostCommentCountViewModel
JoachimFavre May 26, 2024
071b91a
test(comment-count-vm): Test for refresh after comment list refresh
JoachimFavre May 26, 2024
9afda32
test(comment-count-vm): Test for refresh after comment deletion
JoachimFavre May 26, 2024
82f37a4
Merge branch 'main' into update-comment-count
JoachimFavre May 26, 2024
e91ddb5
fix(comment-count-vm): make it not auto-dispose
JoachimFavre May 26, 2024
9e8d0b2
fix(comment-count-vm): make its mock not auto-dispose too
JoachimFavre May 26, 2024
4e7a21e
fix(comment-count-vm-test): delete the comment using the correct view…
JoachimFavre May 26, 2024
7ff49ad
opt(comment-count-vm): make refresh closer to the point where the new…
JoachimFavre May 26, 2024
55927de
Merge branch 'main' into update-comment-count
JoachimFavre May 27, 2024
ecbc16f
style(comment-count-test): reuse already existing variable
JoachimFavre May 27, 2024
cdcffc8
Merge branch 'main' into update-comment-count
JoachimFavre May 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/viewmodels/comments_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "package:proxima/models/database/post/post_id_firestore.dart";
import "package:proxima/models/ui/comment_details.dart";
import "package:proxima/services/database/comment/comment_repository_service.dart";
import "package:proxima/services/database/user_repository_service.dart";
import "package:proxima/viewmodels/post_comment_count_view_model.dart";

/// This view model is used to fetch the comments of a post.
/// It fetches the comments under the post with the id [arg] and returns
Expand All @@ -21,6 +22,9 @@ class CommentsViewModel extends AutoDisposeFamilyAsyncNotifier<

final commentsFirestore = await commentRepository.getPostComments(arg);

// Update the post comment count
ref.read(postCommentCountProvider(arg).notifier).refresh();

final commentOwnersId =
commentsFirestore.map((comment) => comment.data.ownerId).toSet();

Expand Down
28 changes: 28 additions & 0 deletions lib/viewmodels/post_comment_count_view_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:proxima/models/database/post/post_id_firestore.dart";
import "package:proxima/services/database/post_repository_service.dart";

/// This view model is used to keep in memory the number of comments of a post.
/// It is refreshed every time the post comment list is refreshed, to stay consistent
/// with it. It is also refreshed when a comment is deleted.
/// This cannot be auto-dispose because, otherwise, it might get unmounted in the middle
/// of its refresh method. See https://github.com/rrousselGit/riverpod/discussions/2502.
class PostCommentCountViewModel
extends FamilyAsyncNotifier<int, PostIdFirestore> {
@override
Future<int> build(PostIdFirestore arg) async {
final postRepo = ref.watch(postRepositoryServiceProvider);
final post = await postRepo.getPost(arg);
return post.data.commentCount;
}

Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => build(arg));
gruvw marked this conversation as resolved.
Show resolved Hide resolved
}
}

final postCommentCountProvider = AsyncNotifierProvider.family<
PostCommentCountViewModel, int, PostIdFirestore>(
() => PostCommentCountViewModel(),
);
3 changes: 3 additions & 0 deletions lib/viewmodels/user_comments_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "package:proxima/models/database/post/post_id_firestore.dart";
import "package:proxima/models/ui/user_comment_details.dart";
import "package:proxima/services/database/comment/comment_repository_service.dart";
import "package:proxima/viewmodels/login_view_model.dart";
import "package:proxima/viewmodels/post_comment_count_view_model.dart";
import "package:proxima/viewmodels/posts_feed_view_model.dart";

typedef UserCommentsState = List<UserCommentDetails>;
Expand Down Expand Up @@ -53,6 +54,8 @@ class UserCommentViewModel extends AutoDisposeAsyncNotifier<UserCommentsState> {
// Refresh the home feed after comment deletion, that way the comment
// count will be updated
ref.read(postsFeedViewModelProvider.notifier).refresh();
// Also update the comment count for the post
ref.read(postCommentCountProvider(postId).notifier).refresh();
gruvw marked this conversation as resolved.
Show resolved Hide resolved
}

/// Refresh the list of posts
Expand Down
14 changes: 10 additions & 4 deletions lib/views/pages/home/content/feed/components/comment_count.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:proxima/models/database/post/post_id_firestore.dart";
import "package:proxima/viewmodels/post_comment_count_view_model.dart";

/// This widget is used to display the comment number in the post card.
/// It contains the comment icon and the number of comments.
class CommentCount extends StatelessWidget {
final int count;
class CommentCount extends ConsumerWidget {
final PostIdFirestore postId;

const CommentCount({
super.key,
required this.count,
required this.postId,
});

@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final asyncCount = ref.watch(postCommentCountProvider(postId));
final count = asyncCount.value ?? 0;

const icon = Icon(Icons.comment, size: 20);
final countText = Text(count.toString());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class PostCard extends ConsumerWidget {
onTap: () => _onPostSelect(context, postDetails, ref),
child: CommentCount(
key: postCardCommentsNumberKey,
count: postDetails.commentNumber,
postId: postDetails.postId,
),
),
],
Expand Down
18 changes: 14 additions & 4 deletions test/end2end/app_actions.dart
gruvw marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -322,11 +322,21 @@ class AppActions {
await tester.pumpAndSettle();

// Check that the post content is displayed
expect(find.text(comment), findsOneWidget);
final commentText = find.text(comment);
expect(commentText, findsOneWidget);

// Find the delete button on card
final deleteButton = find.byKey(ProfileInfoCard.deleteButtonCardKey);
expect(deleteButton, findsOneWidget);
// Find and tap the delete button on the card
final commentCard = find.ancestor(
of: commentText,
matching: find.byKey(ProfileInfoCard.infoCardKey),
);
expect(commentCard, findsOne);

final deleteButton = find.descendant(
of: commentCard,
matching: find.byKey(ProfileInfoCard.deleteButtonCardKey),
);
expect(deleteButton, findsOne);

await tester.tap(deleteButton);
await tester.pumpAndSettle(delayNeededForAsyncFunctionExecution);
Expand Down
25 changes: 13 additions & 12 deletions test/end2end/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import "package:proxima/services/database/firestore_service.dart";
import "package:proxima/services/sensors/geolocation_service.dart";
import "package:proxima/views/navigation/bottom_navigation_bar/navigation_bar_routes.dart";
import "package:proxima/views/pages/home/content/challenge/challenge_list.dart";
import "package:proxima/views/pages/home/content/feed/post_feed.dart";
import "package:proxima/views/pages/profile/components/profile_app_bar.dart";
import "package:proxima/views/proxima_app.dart";

Expand Down Expand Up @@ -161,17 +160,19 @@ void main() {
await AppActions.loginToCreateAccount(tester);
await AppActions.createAccountToHome(tester);
await AppActions.createPost(tester, testPostTitle, testPostDescription);
await AppActions.openPost(tester, testPostTitle);
const comment = "I like turtles too!";
await AppActions.addComment(tester, comment);

// back to feed
await AppActions.navigateBack(tester);
await AppActions.flingRefresh(tester, find.byType(PostFeed));
await AppActions.expectCommentCount(tester, testPostTitle, 0);

// Add two comments
const comments = ["I like turtles too!", "I prefer elephants"];
for (final (i, comment) in comments.indexed) {
await AppActions.openPost(tester, testPostTitle);
await AppActions.addComment(tester, comment);
await AppActions.navigateBack(tester);
await AppActions.expectCommentCount(tester, testPostTitle, i + 1);
}

// expect comment count to be correct
AppActions.expectCommentCount(tester, testPostTitle, 1);
await AppActions.deleteComment(tester, comment);
AppActions.expectCommentCount(tester, testPostTitle, 0);
// Deleting a comment should reduce the comment count
await AppActions.deleteComment(tester, comments.first);
await AppActions.expectCommentCount(tester, testPostTitle, 1);
});
}
30 changes: 30 additions & 0 deletions test/mocks/overrides/override_post_comment_count_view_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:proxima/models/database/post/post_id_firestore.dart";
import "package:proxima/viewmodels/post_comment_count_view_model.dart";

/// A mock implementation of the [PostCommentCountViewModel] class.
/// Its state is always the same and can be set in the constructor.
class MockPostCommentCountViewModel
extends FamilyAsyncNotifier<int, PostIdFirestore>
implements PostCommentCountViewModel {
final int count;

/// Creates a new [MockPostCommentCountViewModel] with the given [count],
/// which is the value that will always be returned by this view-model.
MockPostCommentCountViewModel({this.count = 0});

@override
Future<int> build(PostIdFirestore arg) async {
return count;
}

@override
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => build(arg));
}
}

final zeroPostCommentCountOverride = [
postCommentCountProvider.overrideWith(() => MockPostCommentCountViewModel()),
];
3 changes: 3 additions & 0 deletions test/mocks/overrides/override_posts_feed_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "package:proxima/models/ui/post_details.dart";
import "package:proxima/viewmodels/posts_feed_view_model.dart";

import "../data/post_overview.dart";
import "override_post_comment_count_view_model.dart";

/// A mock implementation of the [PostsFeedViewModel] class.
/// This class is particularly useful for the UI tests where we want to expose
Expand Down Expand Up @@ -35,6 +36,8 @@ final mockNonEmptyHomeViewModelOverride = [
postsFeedViewModelProvider.overrideWith(
() => MockPostsFeedViewModel(build: () async => testPosts),
),
// The posts don't exist in the database, so we also override their comment count to 0
...zeroPostCommentCountOverride,
];

final mockLoadingHomeViewModelOverride = [
Expand Down
134 changes: 134 additions & 0 deletions test/viewmodels/post_comment_count_view_model_integration_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import "package:collection/collection.dart";
import "package:fake_cloud_firestore/fake_cloud_firestore.dart";
import "package:flutter_test/flutter_test.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:mockito/mockito.dart";
import "package:proxima/models/database/comment/comment_firestore.dart";
import "package:proxima/models/database/post/post_firestore.dart";
import "package:proxima/models/database/post/post_id_firestore.dart";
import "package:proxima/services/database/comment/comment_repository_service.dart";
import "package:proxima/services/database/firestore_service.dart";
import "package:proxima/services/sensors/geolocation_service.dart";
import "package:proxima/viewmodels/comments_view_model.dart";
import "package:proxima/viewmodels/login_view_model.dart";
import "package:proxima/viewmodels/post_comment_count_view_model.dart";
import "package:proxima/viewmodels/user_comments_view_model.dart";

import "../mocks/data/comment_data.dart";
import "../mocks/data/firestore_post.dart";
import "../mocks/data/firestore_user.dart";
import "../mocks/data/geopoint.dart";
import "../mocks/services/mock_geo_location_service.dart";
import "../mocks/services/setup_firebase_mocks.dart";
import "../utils/delay_async_func.dart";

void main() {
late MockGeolocationService geoLocationService;
late FakeFirebaseFirestore fakeFireStore;
late ProviderContainer container;

late CommentRepositoryService commentRepository;

setUp(() {
setupFirebaseAuthMocks();

geoLocationService = MockGeolocationService();
fakeFireStore = FakeFirebaseFirestore();
when(geoLocationService.getCurrentPosition()).thenAnswer(
(_) async => userPosition0,
);

container = ProviderContainer(
overrides: [
geolocationServiceProvider.overrideWithValue(geoLocationService),
loggedInUserIdProvider.overrideWithValue(testingUserFirestoreId),
firestoreProvider.overrideWithValue(fakeFireStore),
],
);

commentRepository = container.read(commentRepositoryServiceProvider);
});

Future<void> expectCommentCount(
PostIdFirestore postId,
int expectedCount,
) async {
final actualCount = await container.read(
postCommentCountProvider(postId).future,
);
expect(actualCount, equals(expectedCount));
}

group("Comment count refresh", () {
const int startCommentCount = 5;
late PostFirestore post;
late List<CommentFirestore> comments;

setUp(() async {
// Create and add a post to the database
final postGenerator = FirestorePostGenerator();
post = postGenerator.generatePostAt(userPosition0);
await setPostFirestore(post, fakeFireStore);

// Create comments
final commentDataGenerator = CommentDataGenerator();
final commentDatas = List.generate(
startCommentCount,
(_) => commentDataGenerator.createMockCommentData(
ownerId: testingUserFirestoreId,
),
).toList();

// Add the comments to the database
final commentIds = await Future.wait(
commentDatas.map(
(commentData) => commentRepository.addComment(post.id, commentData),
),
);

comments = commentIds
.mapIndexed(
(i, id) => CommentFirestore(id: id, data: commentDatas[i]),
)
.toList();
});

test("Refresh on comment list refresh", () async {
await expectCommentCount(post.id, startCommentCount);

// Add a new comment
final newComment = CommentDataGenerator().createMockCommentData();
await commentRepository.addComment(post.id, newComment);

// This should not have refreshed for now
await expectCommentCount(post.id, startCommentCount);

// Refresh the comment list
await container
.read(commentsViewModelProvider(post.id).notifier)
.refresh();
await Future.delayed(delayNeededForAsyncFunctionExecution);

// The comment count should now be updated
await expectCommentCount(post.id, startCommentCount + 1);
});

test("Refresh on comment deletion in view-model", () async {
await expectCommentCount(post.id, startCommentCount);

// Delete the post
final comment = comments.first;
final userCommentViewModel = container.read(
userCommentsViewModelProvider.notifier,
);
await userCommentViewModel.deleteComment(
post.id,
comment.id,
);
await Future.delayed(delayNeededForAsyncFunctionExecution);

// The comment count should now be 1 less
await expectCommentCount(post.id, startCommentCount - 1);
});
});
}