Skip to content

Commit

Permalink
Merge branch 'main' into fix-errors-map
Browse files Browse the repository at this point in the history
  • Loading branch information
TheTexanCodeur authored May 29, 2024
2 parents d8621f4 + 9604188 commit 26361f6
Show file tree
Hide file tree
Showing 15 changed files with 303 additions and 76 deletions.
36 changes: 36 additions & 0 deletions lib/models/ui/comment_count_details.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import "package:flutter/material.dart";

@immutable

/// This class represents the details to display a comment count/icon on the post card.
class CommentCountDetails {
final int count;
final bool isIconBlue;

// An empty instance of CommentCountDetails
// to be used as a default value.
static const CommentCountDetails empty = CommentCountDetails(
count: 0,
isIconBlue: false,
);

const CommentCountDetails({
required this.count,
required this.isIconBlue,
});

@override
bool operator ==(Object other) {
return other is CommentCountDetails &&
other.count == count &&
other.isIconBlue == isIconBlue;
}

@override
int get hashCode {
return Object.hash(
count,
isIconBlue,
);
}
}
7 changes: 6 additions & 1 deletion lib/models/ui/validation/new_comment_validation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
/// It contains the content error message (null if there is no error)
/// and a flag to indicate if the comment was posted
class NewCommentValidation {
static const defaultValue = NewCommentValidation(
contentError: null,
posted: false,
);

final String? contentError;
final bool posted;

NewCommentValidation({
const NewCommentValidation({
required this.contentError,
required this.posted,
});
Expand Down
8 changes: 8 additions & 0 deletions lib/services/database/comment/comment_repository_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ class CommentRepositoryService {
// The post comments are deleted in a batch
await _postCommentRepo.deleteAllComments(parentPostId, batch);
}

/// Check if the user with id [userId] has commented at least once
/// under the post with id [parentPostId].
Future<bool> hasUserCommentedUnderPost(
UserIdFirestore userId,
PostIdFirestore parentPostId,
) =>
_userCommentRepo.hasUserCommentedUnderPost(userId, parentPostId);
}

final commentRepositoryServiceProvider = Provider<CommentRepositoryService>(
Expand Down
21 changes: 21 additions & 0 deletions lib/services/database/comment/user_comment_repository_service.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import "package:cloud_firestore/cloud_firestore.dart";
import "package:proxima/models/database/comment/comment_id_firestore.dart";
import "package:proxima/models/database/post/post_id_firestore.dart";
import "package:proxima/models/database/user/user_firestore.dart";
import "package:proxima/models/database/user/user_id_firestore.dart";
import "package:proxima/models/database/user_comment/user_comment_data.dart";
import "package:proxima/models/database/user_comment/user_comment_firestore.dart";

/// This class is responsible for managing the user's comments in the firestore database.
Expand Down Expand Up @@ -58,4 +60,23 @@ class UserCommentRepositoryService {

batch.delete(commentToDelete);
}

/// Check if the user with id [userId] has commented at least once
/// under the post with id [parentPostId].
Future<bool> hasUserCommentedUnderPost(
UserIdFirestore userId,
PostIdFirestore parentPostId,
) async {
final userCommentCollection = _userCommentCollection(userId);

// Here, we limit the query to 1 because we only need to know if the user
// has commented at least once under the post. This limits the number of
// documents that need to be retrieved and improves the performance.
final userCommentQuery = await userCommentCollection
.where(UserCommentData.parentPostIdField, isEqualTo: parentPostId.value)
.limit(1)
.get();

return userCommentQuery.docs.isNotEmpty;
}
}
14 changes: 4 additions & 10 deletions lib/viewmodels/new_comment_view_model.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import "dart:async";

import "package:cloud_firestore/cloud_firestore.dart";
import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:proxima/models/database/comment/comment_data.dart";
import "package:proxima/models/database/post/post_id_firestore.dart";
Expand All @@ -13,24 +12,19 @@ import "package:proxima/viewmodels/login_view_model.dart";
/// post id [PostIdFirestore] is provided as an argument.
class NewCommentViewModel
extends FamilyAsyncNotifier<NewCommentValidation, PostIdFirestore> {
static const String contentEmptyError = "Please fill out your comment";

// The controller for the content of the comment
// is kept in the view model to avoid losing the content of the comment
// if the user navigates away from the page inadvertedly.
final contentController = TextEditingController();
static const contentEmptyError = "Please fill out your comment";

@override
Future<NewCommentValidation> build(PostIdFirestore arg) async {
return NewCommentValidation(contentError: null, posted: false);
return NewCommentValidation.defaultValue;
}

/// Validates that the content is not empty.
/// If it is empty, the state is updated with the appropriate error message.
/// Returns true if the content is not empty, false otherwise.
bool validate(String content) {
if (content.isEmpty) {
state = AsyncData(
state = const AsyncData(
NewCommentValidation(
contentError: contentEmptyError,
posted: false,
Expand Down Expand Up @@ -85,7 +79,7 @@ class NewCommentViewModel

await commentRepository.addComment(postId, commentData);

state = AsyncData(
state = const AsyncData(
NewCommentValidation(
contentError: null,
posted: true,
Expand Down
36 changes: 30 additions & 6 deletions lib/viewmodels/post_comment_count_view_model.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:proxima/models/database/post/post_id_firestore.dart";
import "package:proxima/models/ui/comment_count_details.dart";
import "package:proxima/services/database/comment/comment_repository_service.dart";
import "package:proxima/services/database/post_repository_service.dart";
import "package:proxima/viewmodels/login_view_model.dart";

/// This view model is used to keep in memory the number of comments of a post.
/// This view model is used to keep in memory the state of the comment count 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> {
extends FamilyAsyncNotifier<CommentCountDetails, PostIdFirestore> {
@override
Future<int> build(PostIdFirestore arg) async {
Future<CommentCountDetails> build(PostIdFirestore arg) async {
final postRepo = ref.watch(postRepositoryServiceProvider);
final post = await postRepo.getPost(arg);
return post.data.commentCount;
final commentRepo = ref.watch(commentRepositoryServiceProvider);
final userId = ref.read(loggedInUserIdProvider);
if (userId == null) {
throw Exception("User not logged in");
}

final postFuture = postRepo.getPost(arg);
final hasUserCommentedFuture =
commentRepo.hasUserCommentedUnderPost(userId, arg);

// Wait for both futures in parallel
final results = await (
postFuture,
hasUserCommentedFuture,
).wait;

final post = results.$1;
final hasUserCommented = results.$2;

return CommentCountDetails(
count: post.data.commentCount,
isIconBlue: hasUserCommented,
);
}

Future<void> refresh() async {
Expand All @@ -23,6 +47,6 @@ class PostCommentCountViewModel
}

final postCommentCountProvider = AsyncNotifierProvider.family<
PostCommentCountViewModel, int, PostIdFirestore>(
PostCommentCountViewModel, CommentCountDetails, PostIdFirestore>(
() => PostCommentCountViewModel(),
);
15 changes: 10 additions & 5 deletions lib/views/pages/home/content/feed/components/comment_count.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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/models/ui/comment_count_details.dart";
import "package:proxima/viewmodels/post_comment_count_view_model.dart";

/// This widget is used to display the comment number in the post card.
Expand All @@ -16,16 +17,20 @@ class CommentCount extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncCount = ref.watch(postCommentCountProvider(postId));
final count = asyncCount.value ?? 0;
final countDetails = asyncCount.value ?? CommentCountDetails.empty;

const icon = Icon(Icons.comment, size: 20);
final countText = Text(count.toString());
final icon = Icon(
Icons.comment,
size: 20,
color: countDetails.isIconBlue ? Colors.blue : null,
);
final countText = Text(countDetails.count.toString());

final content = Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Padding(
padding: EdgeInsets.all(4),
Padding(
padding: const EdgeInsets.all(4),
child: icon,
),
Padding(
Expand Down
71 changes: 31 additions & 40 deletions lib/views/pages/post/components/bottom_bar_add_comment.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:proxima/models/database/post/post_id_firestore.dart";
import "package:proxima/models/ui/validation/new_comment_validation.dart";
import "package:proxima/viewmodels/new_comment_view_model.dart";
import "package:proxima/views/components/async/circular_value.dart";
import "package:proxima/views/helpers/types/result.dart";
import "package:proxima/views/pages/post/components/new_comment/new_comment_button.dart";
import "package:proxima/views/pages/post/components/new_comment/new_comment_textfield.dart";
import "package:proxima/views/pages/post/components/new_comment/new_comment_user_avatar.dart";

class BottomBarAddComment extends ConsumerWidget {
class BottomBarAddComment extends HookConsumerWidget {
final PostIdFirestore parentPostId;

const BottomBarAddComment({
Expand All @@ -18,45 +18,36 @@ class BottomBarAddComment extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final newCommentViewModel = ref.read(
newCommentViewModelProvider(parentPostId).notifier,
final asyncNewCommentState =
ref.watch(newCommentViewModelProvider(parentPostId));

final contentController = useTextEditingController();

// The avatar of the current user on the left
const userAvatar = NewCommentUserAvatar();

// The field in which the user can write a comment
final commentTextField = NewCommentTextField(
commentContentController: contentController,
newCommentState: asyncNewCommentState.maybeWhen(
data: (data) => data,
orElse: () => NewCommentValidation.defaultValue,
),
);

// The button to post the comment
final addCommentButton = NewCommentButton(
commentContentController: contentController,
parentPostId: parentPostId,
);

final asyncNewCommentState = ref
.watch(
newCommentViewModelProvider(parentPostId).future,
)
.mapRes();

final contentController = newCommentViewModel.contentController;

return CircularValue(
future: asyncNewCommentState,
builder: (context, newCommentState) {
// The avatar of the current user on the left
const userAvatar = NewCommentUserAvatar();

// The field in which the user can write a comment
final commentTextField = NewCommentTextField(
commentContentController: contentController,
newCommentState: newCommentState,
);

// The button to post the comment
final addCommentButton = NewCommentButton(
commentContentController: contentController,
parentPostId: parentPostId,
);

return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
userAvatar,
commentTextField,
addCommentButton,
],
);
},
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
userAvatar,
commentTextField,
addCommentButton,
],
);
}
}
10 changes: 10 additions & 0 deletions test/mocks/data/comment_count.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import "package:proxima/models/ui/comment_count_details.dart";

// Comment count details for testing purposes.
const List<CommentCountDetails> testCommentCounts = [
CommentCountDetails(count: 1324, isIconBlue: true),
CommentCountDetails(count: 0, isIconBlue: false),
CommentCountDetails(count: 1, isIconBlue: true),
CommentCountDetails(count: 100, isIconBlue: false),
CommentCountDetails(count: 999, isIconBlue: true),
];
15 changes: 9 additions & 6 deletions test/mocks/overrides/override_post_comment_count_view_model.dart
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:proxima/models/database/post/post_id_firestore.dart";
import "package:proxima/models/ui/comment_count_details.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>
extends FamilyAsyncNotifier<CommentCountDetails, PostIdFirestore>
implements PostCommentCountViewModel {
final int count;
final CommentCountDetails countDetails;

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

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

@override
Expand Down
11 changes: 11 additions & 0 deletions test/mocks/services/mock_comment_repository_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,15 @@ class MockCommentRepositoryService extends Mock
returnValue: Future.value(),
);
}

@override
Future<bool> hasUserCommentedUnderPost(
UserIdFirestore? userId,
PostIdFirestore? parentPostId,
) {
return super.noSuchMethod(
Invocation.method(#hasUserCommentedUnderPost, [userId, parentPostId]),
returnValue: Future.value(false),
);
}
}
Loading

0 comments on commit 26361f6

Please sign in to comment.