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 27, 2024
2 parents 2a07efe + ef14a13 commit c65bca8
Show file tree
Hide file tree
Showing 43 changed files with 716 additions and 217 deletions.
8 changes: 8 additions & 0 deletions lib/models/ui/comment_details.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import "package:flutter/foundation.dart";
import "package:proxima/models/database/comment/comment_data.dart";
import "package:proxima/models/database/user/user_firestore.dart";
import "package:proxima/models/database/user/user_id_firestore.dart";

/// This class contains all the details of a comment
/// that are needed to display it in the UI.
@immutable
class CommentDetails {
final String content;
final String ownerDisplayName;
final String ownerUsername;
final UserIdFirestore ownerUserID;
final int ownerCentauriPoints;
final DateTime publicationDate;

const CommentDetails({
required this.content,
required this.ownerDisplayName,
required this.ownerUsername,
required this.ownerUserID,
required this.ownerCentauriPoints,
required this.publicationDate,
});
Expand All @@ -26,6 +31,7 @@ class CommentDetails {
other.content == content &&
other.ownerDisplayName == ownerDisplayName &&
other.ownerUsername == ownerUsername &&
other.ownerUserID == ownerUserID &&
other.ownerCentauriPoints == ownerCentauriPoints &&
other.publicationDate == publicationDate;
}
Expand All @@ -36,6 +42,7 @@ class CommentDetails {
content,
ownerDisplayName,
ownerUsername,
ownerUserID,
ownerCentauriPoints,
publicationDate,
);
Expand All @@ -53,6 +60,7 @@ class CommentDetails {
content: commentData.content,
ownerDisplayName: ownerData.displayName,
ownerUsername: ownerData.username,
ownerUserID: owner.uid,
ownerCentauriPoints: ownerData.centauriPoints,
publicationDate: DateTime.fromMillisecondsSinceEpoch(
commentData.publicationTime.millisecondsSinceEpoch,
Expand Down
6 changes: 6 additions & 0 deletions lib/models/ui/post_details.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "package:geoflutterfire_plus/geoflutterfire_plus.dart";
import "package:proxima/models/database/post/post_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";

@immutable

Expand All @@ -15,6 +16,7 @@ class PostDetails {
final int commentNumber;
final String ownerDisplayName;
final String ownerUsername;
final UserIdFirestore ownerUserID;
final int ownerCentauriPoints;
final DateTime publicationDate;
final int distance; // in meters
Expand All @@ -28,6 +30,7 @@ class PostDetails {
required this.commentNumber,
required this.ownerDisplayName,
required this.ownerUsername,
required this.ownerUserID,
required this.ownerCentauriPoints,
required this.publicationDate,
required this.distance,
Expand All @@ -46,6 +49,7 @@ class PostDetails {
other.commentNumber == commentNumber &&
other.ownerDisplayName == ownerDisplayName &&
other.ownerUsername == ownerUsername &&
other.ownerUserID == ownerUserID &&
other.ownerCentauriPoints == ownerCentauriPoints &&
other.publicationDate == publicationDate &&
other.distance == distance &&
Expand All @@ -62,6 +66,7 @@ class PostDetails {
commentNumber,
ownerDisplayName,
ownerUsername,
ownerUserID,
ownerCentauriPoints,
publicationDate,
distance,
Expand All @@ -86,6 +91,7 @@ class PostDetails {
commentNumber: postFirestore.data.commentCount,
ownerDisplayName: userFirestore.data.displayName,
ownerUsername: userFirestore.data.username,
ownerUserID: userFirestore.uid,
ownerCentauriPoints: userFirestore.data.centauriPoints,
publicationDate: postFirestore.data.publicationTime.toDate(),
distance: (geoFirePoint.distanceBetweenInKm(
Expand Down
7 changes: 7 additions & 0 deletions lib/models/ui/ranking/ranking_element_details.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "package:flutter/foundation.dart";
import "package:proxima/models/database/user/user_id_firestore.dart";

/// A class that stores data for each ranking UI element.
/// The [userRank] is nullable to allow the current user to not have a rank.
Expand All @@ -7,6 +8,7 @@ class RankingElementDetails {
const RankingElementDetails({
required this.userDisplayName,
required this.userUserName,
required this.userID,
required this.centauriPoints,
required this.userRank,
});
Expand All @@ -17,6 +19,9 @@ class RankingElementDetails {
/// Username of the user.
final String userUserName;

/// ID of the user.
final UserIdFirestore userID;

/// Centauri points of the user.
final int centauriPoints;

Expand All @@ -30,6 +35,7 @@ class RankingElementDetails {
return other is RankingElementDetails &&
other.userDisplayName == userDisplayName &&
other.userUserName == userUserName &&
other.userID == userID &&
other.centauriPoints == centauriPoints &&
other.userRank == userRank;
}
Expand All @@ -38,6 +44,7 @@ class RankingElementDetails {
int get hashCode => Object.hash(
userDisplayName,
userUserName,
userID,
centauriPoints,
userRank,
);
Expand Down
24 changes: 13 additions & 11 deletions lib/models/ui/user_avatar_details.dart
Original file line number Diff line number Diff line change
@@ -1,42 +1,44 @@
import "package:flutter/foundation.dart";
import "package:proxima/models/database/user/user_data.dart";
import "package:proxima/models/database/user/user_firestore.dart";

/// A class that stores data for a user avatar UI.
@immutable
class UserAvatarDetails {
final String displayName;
final int? userCentauriPoints;
final int? centauriPoints;

/// Creates a [UserAvatarDetails] object.
/// [displayName] is the user's display name, of which
/// the first letter is displayed on the avatar.
/// [userCentauriPoints] is the user's centauri points,
/// which is used to color the avatar background.
/// [centauriPoints] is the user's centauri points.
/// The [centauriPoints] parameter can be null, which is useful for loading states.
const UserAvatarDetails({
required this.displayName,
required this.userCentauriPoints,
required this.centauriPoints,
});

/// Converts a [UserData] object, [userData], to a [UserAvatarDetails] object.
factory UserAvatarDetails.fromUserData(UserData userData) {
/// Converts a [UserFirestore] object, [user], to a [UserAvatarDetails] object.
factory UserAvatarDetails.fromUser(
UserFirestore user,
) {
return UserAvatarDetails(
displayName: userData.displayName,
userCentauriPoints: userData.centauriPoints,
displayName: user.data.displayName,
centauriPoints: user.data.centauriPoints,
);
}

@override
bool operator ==(Object other) {
return other is UserAvatarDetails &&
other.displayName == displayName &&
other.userCentauriPoints == userCentauriPoints;
other.centauriPoints == centauriPoints;
}

@override
int get hashCode {
return Object.hash(
displayName,
userCentauriPoints,
centauriPoints,
);
}
}
46 changes: 46 additions & 0 deletions lib/utils/user_avatar_color.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import "package:flutter/material.dart";
import "package:proxima/models/ui/linear_segmented_hsv_colormap.dart";
import "package:proxima/services/database/challenge_repository_service.dart";

/// Stops for the colormap used to color the user's avatar based on their centauri points.
/// The stops are defined as the number of challenges completed.
const _challengesStops = [
// sqrt(10) ~= 3, which is the approximate step between each stop
0,
10, // ~ 3 days of daily challenge
30,
100,
300, // ~ 3 months of daily challenge
1000,
3000, // ~ 3 years of daily challenge
10000,
];

/// Color used when the user's centauri points are null.
const loadingUserAvatarColor = Colors.transparent;

/// Converts some amount of [centauriPoints] to a color, based on a uniform
/// [LinearSegmentedHSVColormap] (defined by _challengesStop). [brightness]
/// defines the value and saturation of the colormap.
/// If [centauriPoints] is null, the color is transparent.
Color centauriToUserAvatarColor(int? centauriPoints, Brightness brightness) {
if (centauriPoints == null) return loadingUserAvatarColor;

final hsvValue = switch (brightness) {
Brightness.light => 0.9,
Brightness.dark => 0.5,
};
final hsvSaturation = switch (brightness) {
Brightness.light => 0.4,
Brightness.dark => 0.8,
};

const chalReward = ChallengeRepositoryService.soloChallengeReward;
final colorMap = LinearSegmentedHSVColormap.uniform(
_challengesStops.map((nChallenges) => nChallenges * chalReward).toList(),
value: hsvValue,
saturation: hsvSaturation,
);

return colorMap(centauriPoints).toColor();
}
8 changes: 8 additions & 0 deletions lib/viewmodels/challenge_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "package:proxima/models/ui/challenge_details.dart";
import "package:proxima/services/database/challenge_repository_service.dart";
import "package:proxima/services/database/post_repository_service.dart";
import "package:proxima/services/sensors/geolocation_service.dart";
import "package:proxima/viewmodels/dynamic_user_avatar_view_model.dart";
import "package:proxima/viewmodels/login_view_model.dart";
import "package:proxima/viewmodels/map/map_pin_view_model.dart";

Expand Down Expand Up @@ -98,6 +99,13 @@ class ChallengeViewModel

// Refresh the map pins after challenge completion
ref.read(mapPinViewModelProvider.notifier).refresh();

// Refresh the user centauri points after challenge completion
// Note: null is the current user id as represented in dynamicUserAvatarViewModelProvider
// So we have to refresh both [currentUser] and the null user
for (final user in [null, currentUser]) {
ref.read(dynamicUserAvatarViewModelProvider(user).notifier).refresh();
}
}

return pointsAwarded;
Expand Down
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
72 changes: 19 additions & 53 deletions lib/viewmodels/dynamic_user_avatar_view_model.dart
Original file line number Diff line number Diff line change
@@ -1,87 +1,53 @@
import "dart:async";

import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:proxima/models/database/user/user_firestore.dart";
import "package:proxima/models/database/user/user_id_firestore.dart";
import "package:proxima/models/ui/linear_segmented_hsv_colormap.dart";
import "package:proxima/models/ui/user_avatar_details.dart";
import "package:proxima/services/database/challenge_repository_service.dart";
import "package:proxima/services/database/user_repository_service.dart";
import "package:proxima/viewmodels/login_view_model.dart";

// TODO: Remove the fact that the current user is "null" and accept a non-nullable user id.
/// View model for the dynamic user avatar.
/// This view model is used to fetch the user's display name given its id.
/// If the id is null, the current user's display name is fetched.
/// This view model is used to fetch the user's display name and centauri points
/// given its id. If the id is null, the current user's information is fetched.
class DynamicUserAvatarViewModel extends AutoDisposeFamilyAsyncNotifier<
UserAvatarDetails, UserIdFirestore?> {
DynamicUserAvatarViewModel();

/// Stops for the colormap used to color the user's avatar based on their centauri points.
/// The stops are defined as the number of challenges completed.
static const _challengesStops = [
// sqrt(10) ~= 3, which is the approximate step between each stop
0,
10, // ~ 3 days of daily challenge
30,
100,
300, // ~ 3 months of daily challenge
1000,
3000, // ~ 3 years of daily challenge
10000,
];

/// Converts some amount of [centauriPoints] to a color, based on a uniform
/// [LinearSegmentedHSVColormap] (defined by _challengesStop). [brightness]
/// defines the value and saturation of the colormap.
/// If [centauriPoints] is null, the color is transparent.
static Color centauriToColor(int? centauriPoints, Brightness brightness) {
if (centauriPoints == null) return Colors.transparent;

final hsvValue = switch (brightness) {
Brightness.light => 0.9,
Brightness.dark => 0.5,
};
final hsvSaturation = switch (brightness) {
Brightness.light => 0.4,
Brightness.dark => 0.8,
};

const chalReward = ChallengeRepositoryService.soloChallengeReward;
final colorMap = LinearSegmentedHSVColormap.uniform(
_challengesStops.map((nChallenges) => nChallenges * chalReward).toList(),
value: hsvValue,
saturation: hsvSaturation,
);

return colorMap(centauriPoints).toColor();
}

@override
Future<UserAvatarDetails> build(UserIdFirestore? arg) async {
final userID = arg;
final currentUID = ref.watch(loggedInUserIdProvider);
final userDataBase = ref.watch(userRepositoryServiceProvider);

late final UserFirestore user;
late final UserIdFirestore userID;

if (userID == null) {
if (arg == null) {
if (currentUID == null) {
throw Exception("User is not logged in.");
}

user = await userDataBase.getUser(currentUID);
userID = currentUID;
} else {
user = await userDataBase.getUser(userID);
userID = arg;
}

return UserAvatarDetails.fromUserData(user.data);
user = await userDataBase.getUser(userID);

return UserAvatarDetails.fromUser(user);
}

/// Refresh the user's information.
Future<void> refresh() async {
state = const AsyncValue.loading();

state = await AsyncValue.guard(() => build(arg));
}
}

//TODO: Extend to fetch the user's avatar image.
/// Flexible provider allowing to retrieve the user's display name given its id.
/// If the id is null, the current user's display name is fetched.
/// Flexible provider allowing to retrieve the user's display name and centauri points
/// given its id. If the id is null, the current user's information is fetched.
final dynamicUserAvatarViewModelProvider = AsyncNotifierProvider.autoDispose
.family<DynamicUserAvatarViewModel, UserAvatarDetails, UserIdFirestore?>(
() => DynamicUserAvatarViewModel(),
Expand Down
Loading

0 comments on commit c65bca8

Please sign in to comment.