From c3a44a24e5d70d27e31a9b9755b1e8c57570bb38 Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Fri, 13 Dec 2024 03:10:50 +0100 Subject: [PATCH 01/14] fix: Change post rating logic - Removed unnecessary post update in MapView - Updated onRatingChanged passed to PostItem mappings in Feed and MapView to ensure correctness of rating averages --- .../lookupgroup27/lookup/ui/feed/Feed.kt | 26 ++++++++++++------- .../lookup/ui/googlemap/components/MapView.kt | 22 +++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt index 9abd3aaf3..323a1be49 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt @@ -161,23 +161,29 @@ fun FeedScreen( val isReturningUser = post.ratedBy.contains(userEmail) val newStarsCount = - if (isReturningUser) post.starsCount - oldStarCounts + starsCount + if (starsCount == 0) { + post.starsCount + } else if (isReturningUser) post.starsCount - oldStarCounts + starsCount else post.starsCount + starsCount - val newUsersNumber = - if (isReturningUser) post.usersNumber else post.usersNumber + 1 - val newAvg = newStarsCount.toDouble() / newUsersNumber + val newRatedBy = + if (starsCount == 0) { + post.ratedBy.filter { x -> x != userEmail } + } else if (!isReturningUser) { + post.ratedBy + userEmail + } else { + post.ratedBy + } + val newUsersNumber = post.ratedBy.size + val newAvg = + if (newUsersNumber != 0) newStarsCount.toDouble() / newUsersNumber + else 0.0 postsViewModel.updatePost( post.copy( averageStars = newAvg, starsCount = newStarsCount, usersNumber = newUsersNumber, - ratedBy = - if (!isReturningUser) { - post.ratedBy + userEmail - } else { - post.ratedBy - })) + ratedBy = newRatedBy)) }) Spacer(modifier = Modifier.height(20.dp)) } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt index d672124d4..f3043141a 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt @@ -68,15 +68,6 @@ fun MapView( } } - LaunchedEffect(profile, posts) { - posts.forEach { post -> - val starsCount = postRatings[post.uid]?.count { it } ?: 0 - val usersNumber = postRatings[post.uid]?.size ?: 0 - val avg = if (usersNumber == 0) 0.0 else starsCount.toDouble() / usersNumber - updatePost(post, avg, starsCount, usersNumber, post.ratedBy) - } - } - GoogleMap( modifier = Modifier.fillMaxSize().padding(padding), properties = mapProperties, @@ -111,16 +102,21 @@ fun MapView( val isReturningUser = it.ratedBy.contains(userEmail) val newStarsCount = - if (isReturningUser) it.starsCount - oldStarCounts + starsCount + if (starsCount == 0) { + it.starsCount + } else if (isReturningUser) it.starsCount - oldStarCounts + starsCount else it.starsCount + starsCount - val newUsersNumber = if (isReturningUser) it.usersNumber else it.usersNumber + 1 - val newAvg = newStarsCount.toDouble() / newUsersNumber val newRatedBy = - if (!isReturningUser) { + if (starsCount == 0) { + it.ratedBy.filter { x -> x != userEmail } + } else if (!isReturningUser) { it.ratedBy + userEmail } else { it.ratedBy } + val newUsersNumber = newRatedBy.size + val newAvg = newStarsCount.toDouble() / newUsersNumber + updatePost(it, newAvg, newStarsCount, newUsersNumber, newRatedBy) }) } From 70235d7f27974c94e396c3dfb8826400a03efe45 Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Fri, 13 Dec 2024 03:18:24 +0100 Subject: [PATCH 02/14] fix: Update newUsersNumber in Feed's posts onRatingChange --- .../main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt index 323a1be49..c71e3522f 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt @@ -173,7 +173,7 @@ fun FeedScreen( } else { post.ratedBy } - val newUsersNumber = post.ratedBy.size + val newUsersNumber = newRatedBy.size val newAvg = if (newUsersNumber != 0) newStarsCount.toDouble() / newUsersNumber else 0.0 From 51e7acc1ec7b78b472180f2594fbbff50007db59 Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Fri, 13 Dec 2024 03:23:29 +0100 Subject: [PATCH 03/14] fix: Update newStarsCount in MapView and Feed's PostItem onRatingChange --- .../main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt | 2 +- .../lookupgroup27/lookup/ui/googlemap/components/MapView.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt index c71e3522f..4cc5fc25f 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt @@ -162,7 +162,7 @@ fun FeedScreen( val isReturningUser = post.ratedBy.contains(userEmail) val newStarsCount = if (starsCount == 0) { - post.starsCount + post.starsCount - oldStarCounts } else if (isReturningUser) post.starsCount - oldStarCounts + starsCount else post.starsCount + starsCount val newRatedBy = diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt index f3043141a..94b92ee2f 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt @@ -103,7 +103,7 @@ fun MapView( val isReturningUser = it.ratedBy.contains(userEmail) val newStarsCount = if (starsCount == 0) { - it.starsCount + it.starsCount - oldStarCounts } else if (isReturningUser) it.starsCount - oldStarCounts + starsCount else it.starsCount + starsCount val newRatedBy = From 2ac40e2dc1b2db77d9346ccb88629477bb0ec6aa Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Fri, 13 Dec 2024 03:42:20 +0100 Subject: [PATCH 04/14] fix: Update newAvg in MapView's PostItem's onRatingChanges to avoid NaN --- .../lookupgroup27/lookup/ui/googlemap/components/MapView.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt index 94b92ee2f..b44a33990 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt @@ -115,7 +115,8 @@ fun MapView( it.ratedBy } val newUsersNumber = newRatedBy.size - val newAvg = newStarsCount.toDouble() / newUsersNumber + val newAvg = if (newUsersNumber != 0) newStarsCount.toDouble() / newUsersNumber + else 0.0 updatePost(it, newAvg, newStarsCount, newUsersNumber, newRatedBy) }) From 9bbd0f45e268fd26f83d879c22c06cb871bc8476 Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Mon, 16 Dec 2024 00:36:46 +0100 Subject: [PATCH 05/14] fix: Correct starsCount for map posts update --- .../lookup/ui/googlemap/components/MapView.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt index b44a33990..21431bb43 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt @@ -68,6 +68,15 @@ fun MapView( } } + LaunchedEffect(profile, posts) { + posts.forEach { post -> + val starsCount = postRatings[post.uid]?.count { it } ?: 0 + val usersNumber = post.ratedBy.size + val avg = if (usersNumber == 0) 0.0 else starsCount.toDouble() / usersNumber + updatePost(post, avg, starsCount, usersNumber, post.ratedBy) + } + } + GoogleMap( modifier = Modifier.fillMaxSize().padding(padding), properties = mapProperties, @@ -115,8 +124,8 @@ fun MapView( it.ratedBy } val newUsersNumber = newRatedBy.size - val newAvg = if (newUsersNumber != 0) newStarsCount.toDouble() / newUsersNumber - else 0.0 + val newAvg = + if (newUsersNumber != 0) newStarsCount.toDouble() / newUsersNumber else 0.0 updatePost(it, newAvg, newStarsCount, newUsersNumber, newRatedBy) }) From c2c4a71f812d4512f5cf5b5f96d1b636a6a3c1df Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Thu, 19 Dec 2024 20:31:28 +0100 Subject: [PATCH 06/14] fix: Make selectedPost's ImagePreviewDialog update its post values after update - Updated the selected post of the viewmodel to ensure the mapview's selectedPost value is set to the updated post. This allows MapView to maintain continuous updates of the parameters in the image preview dialog. - Removed unnecessary launched effect --- .../lookupgroup27/lookup/ui/googlemap/GoogleMap.kt | 8 ++++++-- .../lookup/ui/googlemap/components/MapView.kt | 13 ++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt index 57cb60dc3..a8c0fe0a2 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt @@ -130,6 +130,7 @@ fun GoogleMapScreen( hasLocationPermission, locationProvider.currentLocation.value, autoCenteringEnabled, // Pass the state + postsViewModel, allPosts, userEmail, updateProfile = { profile, updatedRatings -> @@ -148,12 +149,15 @@ fun GoogleMapScreen( }, profile = profile, updatePost = { post, newAvg, newStarsCount, newUsersNumber, newRatedBy -> - postsViewModel.updatePost( + val newPost = post.copy( averageStars = newAvg, starsCount = newStarsCount, usersNumber = newUsersNumber, - ratedBy = newRatedBy)) + ratedBy = newRatedBy) + + postsViewModel.selectPost(newPost) + postsViewModel.updatePost(newPost) }, postRatings = postRatings) } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt index 21431bb43..0c4a3d655 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.Modifier import com.github.lookupgroup27.lookup.model.post.Post import com.github.lookupgroup27.lookup.model.profile.UserProfile import com.github.lookupgroup27.lookup.ui.image.ImagePreviewDialog +import com.github.lookupgroup27.lookup.ui.post.PostsViewModel import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.model.LatLng import com.google.maps.android.compose.* @@ -32,6 +33,7 @@ fun MapView( hasLocationPermission: Boolean, location: Location?, autoCenteringEnabled: Boolean, + postsViewModel: PostsViewModel, posts: List, userEmail: String, updateProfile: (UserProfile?, MutableMap?) -> Unit, @@ -68,15 +70,6 @@ fun MapView( } } - LaunchedEffect(profile, posts) { - posts.forEach { post -> - val starsCount = postRatings[post.uid]?.count { it } ?: 0 - val usersNumber = post.ratedBy.size - val avg = if (usersNumber == 0) 0.0 else starsCount.toDouble() / usersNumber - updatePost(post, avg, starsCount, usersNumber, post.ratedBy) - } - } - GoogleMap( modifier = Modifier.fillMaxSize().padding(padding), properties = mapProperties, @@ -128,6 +121,8 @@ fun MapView( if (newUsersNumber != 0) newStarsCount.toDouble() / newUsersNumber else 0.0 updatePost(it, newAvg, newStarsCount, newUsersNumber, newRatedBy) + + selectedPost = postsViewModel.post.value }) } } From ff8fabb7c3564ffa5a561146d9be1698deb3e8c5 Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:38:58 +0100 Subject: [PATCH 07/14] fix: Remove all code related with the display of ratings in GoogleMap view - Updated PostItem to display ratings only if wanted - Refactored GoogleMap and MapView so that ratings don't display when selecting a post - Made ImagePreviewDialog deny the display of ratings --- .../lookup/ui/feed/components/PostItem.kt | 64 ++++++++++--------- .../lookup/ui/googlemap/GoogleMap.kt | 33 +--------- .../lookup/ui/googlemap/components/MapView.kt | 45 +------------ .../lookup/ui/image/ImagePreviewDialog.kt | 1 + 4 files changed, 38 insertions(+), 105 deletions(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt index c1ad724fc..5ae8e018a 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt @@ -37,6 +37,7 @@ fun PostItem( onRatingChanged: (List) -> Unit, color: Color = Color.Black, textForUsername: String = post.username, + showStars: Boolean = true, showAverage: Boolean = true ) { @@ -88,36 +89,39 @@ fun PostItem( .testTag("DescriptionTag_${post.uid}") // Tagging description for testing ) // Star rating row - Row { - // Loop through each star - starStates.forEachIndexed { index, isFilled -> - IconButton( - onClick = { - // Toggle stars up to the clicked index - val newRating = - starStates.mapIndexed { i, _ -> if (isFilled) i < index else i <= index } - onRatingChanged(newRating) - }, - modifier = Modifier.size(36.dp).testTag("Star_${index + 1}_${post.uid}")) { - Image( - painter = - painterResource( - id = if (isFilled) R.drawable.full_star else R.drawable.empty_star), - contentDescription = "Star") - } - } - if (showAverage) { - // Display the average rating at the end of the row - Text( - text = "Average rating: ${"%.1f".format(post.averageStars)}", - modifier = - Modifier.fillMaxWidth() - .padding(start = 4.dp) - .testTag("AverageRatingTag_${post.uid}"), - textAlign = TextAlign.End, - style = - MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Bold, color = color)) + if (showStars) { + Row { + // Loop through each star + starStates.forEachIndexed { index, isFilled -> + IconButton( + onClick = { + // Toggle stars up to the clicked index + val newRating = + starStates.mapIndexed { i, _ -> if (isFilled) i < index else i <= index } + onRatingChanged(newRating) + }, + modifier = Modifier.size(36.dp).testTag("Star_${index + 1}_${post.uid}")) { + Image( + painter = + painterResource( + id = if (isFilled) R.drawable.full_star else R.drawable.empty_star), + contentDescription = "Star") + } + } + + if (showAverage) { + // Display the average rating at the end of the row + Text( + text = "Average rating: ${"%.1f".format(post.averageStars)}", + modifier = + Modifier.fillMaxWidth() + .padding(start = 4.dp) + .testTag("AverageRatingTag_${post.uid}"), + textAlign = TextAlign.End, + style = + MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold, color = color)) + } } } } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt index a8c0fe0a2..9f97f246f 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt @@ -16,7 +16,6 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.lifecycle.viewmodel.compose.viewModel import com.github.lookupgroup27.lookup.model.location.LocationProviderSingleton -import com.github.lookupgroup27.lookup.model.profile.UserProfile import com.github.lookupgroup27.lookup.ui.googlemap.components.* import com.github.lookupgroup27.lookup.ui.navigation.* import com.github.lookupgroup27.lookup.ui.post.PostsViewModel @@ -46,7 +45,6 @@ fun GoogleMapScreen( var autoCenteringEnabled by remember { mutableStateOf(true) } // New state for auto-centering val auth = remember { FirebaseAuth.getInstance() } val isLoggedIn = auth.currentUser != null - val allPosts by postsViewModel.allPosts.collectAsState() profileViewModel.fetchUserProfile() @@ -130,36 +128,7 @@ fun GoogleMapScreen( hasLocationPermission, locationProvider.currentLocation.value, autoCenteringEnabled, // Pass the state - postsViewModel, - allPosts, - userEmail, - updateProfile = { profile, updatedRatings -> - val newProfile: UserProfile = - profile?.copy( - username = username, - bio = bio, - email = email, - ratings = updatedRatings ?: emptyMap()) - ?: UserProfile( - username = username, - bio = bio, - email = email, - ratings = updatedRatings ?: emptyMap()) - profileViewModel.updateUserProfile(newProfile) - }, - profile = profile, - updatePost = { post, newAvg, newStarsCount, newUsersNumber, newRatedBy -> - val newPost = - post.copy( - averageStars = newAvg, - starsCount = newStarsCount, - usersNumber = newUsersNumber, - ratedBy = newRatedBy) - - postsViewModel.selectPost(newPost) - postsViewModel.updatePost(newPost) - }, - postRatings = postRatings) + allPosts) } }) } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt index 0c4a3d655..6ccd7f453 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt @@ -6,9 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import com.github.lookupgroup27.lookup.model.post.Post -import com.github.lookupgroup27.lookup.model.profile.UserProfile import com.github.lookupgroup27.lookup.ui.image.ImagePreviewDialog -import com.github.lookupgroup27.lookup.ui.post.PostsViewModel import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.model.LatLng import com.google.maps.android.compose.* @@ -33,13 +31,7 @@ fun MapView( hasLocationPermission: Boolean, location: Location?, autoCenteringEnabled: Boolean, - postsViewModel: PostsViewModel, posts: List, - userEmail: String, - updateProfile: (UserProfile?, MutableMap?) -> Unit, - profile: UserProfile?, - updatePost: (Post, Double, Int, Int, List) -> Unit, - postRatings: MutableMap> ) { var mapProperties by remember { @@ -89,41 +81,8 @@ fun MapView( post = it, username = it.username, onDismiss = { selectedPost = null }, - starStates = postRatings[it.uid] ?: mutableListOf(false, false, false), - onRatingChanged = { newRating -> - val oldPostRatings = postRatings[it.uid] ?: mutableListOf(false, false, false) - val oldStarCounts = oldPostRatings.count { it } - // Directly modify the existing starStates list to avoid creating a new list - postRatings[it.uid] = newRating.toList() - // Update the stars count based on the new rating - val starsCount = newRating.count { it } - // Update user profile with the new rating count - val updatedRatings = profile?.ratings?.toMutableMap() - updatedRatings?.set(it.uid, starsCount) - updateProfile(profile, updatedRatings) - - val isReturningUser = it.ratedBy.contains(userEmail) - val newStarsCount = - if (starsCount == 0) { - it.starsCount - oldStarCounts - } else if (isReturningUser) it.starsCount - oldStarCounts + starsCount - else it.starsCount + starsCount - val newRatedBy = - if (starsCount == 0) { - it.ratedBy.filter { x -> x != userEmail } - } else if (!isReturningUser) { - it.ratedBy + userEmail - } else { - it.ratedBy - } - val newUsersNumber = newRatedBy.size - val newAvg = - if (newUsersNumber != 0) newStarsCount.toDouble() / newUsersNumber else 0.0 - - updatePost(it, newAvg, newStarsCount, newUsersNumber, newRatedBy) - - selectedPost = postsViewModel.post.value - }) + starStates = mutableListOf(false, false, false), + onRatingChanged = {}) } } } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/ImagePreviewDialog.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/ImagePreviewDialog.kt index 53c5b6ba0..c7734dea6 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/ImagePreviewDialog.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/ImagePreviewDialog.kt @@ -51,6 +51,7 @@ fun ImagePreviewDialog( onRatingChanged = onRatingChanged, color = Color.White, textForUsername = "Posted by: $username", + showStars = false, showAverage = false) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = onDismiss) { Text(text = "Close") } From 7c24eefef9e9f8f0261318209b9540af3b75a600 Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:40:16 +0100 Subject: [PATCH 08/14] test: Update ImagePreviewDialogTest - Replaced testStarIsDisplayed with testStarIsNotDisplay - Commented out testStarClickClassUdpatePost as it isn't meaningful anymore --- .../lookup/ui/image/ImagePreviewDialogTest.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/ImagePreviewDialogTest.kt b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/ImagePreviewDialogTest.kt index 55bb3c2fb..62be0ca2c 100644 --- a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/ImagePreviewDialogTest.kt +++ b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/ImagePreviewDialogTest.kt @@ -111,7 +111,7 @@ class ImagePreviewDialogTest { assert(dialogDismissed) } - @Test + /*@Test fun testStarClickCallsUpdatePost() { // Set the Compose content to ImagePreviewDialog composeTestRule.setContent { @@ -124,7 +124,7 @@ class ImagePreviewDialogTest { // Verify that updatePost was called in the postsViewModel verify(postsRepository).updatePost(eq(testPost), any(), any()) - } + }*/ /*@Test fun testStarClickCallsUpdateUserProfile() { @@ -141,7 +141,7 @@ class ImagePreviewDialogTest { }*/ @Test - fun testStarIsDisplayed() { + fun testStarIsNotDisplayed() { composeTestRule.setContent { ImagePreviewDialog( post = testPost, username = "User1", onDismiss = {}, testStarStates, onRatingChanged = {}) @@ -149,7 +149,6 @@ class ImagePreviewDialogTest { // Perform click on the first star icon of a post with uid "1" composeTestRule .onNodeWithTag("Star_1_1") - .assertIsDisplayed() - .performClick() // Click on the first star + .assertIsNotDisplayed() // Click on the first star } } From 23e83b0dfc7b590439c92f792762dec93b19e95b Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:56:44 +0100 Subject: [PATCH 09/14] style: Apply ktfmtFormat --- .../lookup/ui/image/ImagePreviewDialogTest.kt | 6 +- .../lookupgroup27/lookup/ui/feed/Feed.kt | 47 +++++++------ .../lookup/ui/feed/components/PostItem.kt | 66 +++++++++---------- .../lookup/ui/googlemap/GoogleMap.kt | 1 - .../lookup/ui/googlemap/components/MapView.kt | 3 - 5 files changed, 56 insertions(+), 67 deletions(-) diff --git a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/ImagePreviewDialogTest.kt b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/ImagePreviewDialogTest.kt index 62be0ca2c..9871e2ddb 100644 --- a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/ImagePreviewDialogTest.kt +++ b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/ImagePreviewDialogTest.kt @@ -10,8 +10,6 @@ import org.junit.* import org.mockito.Mock import org.mockito.Mockito.* import org.mockito.kotlin.any -import org.mockito.kotlin.eq -import org.mockito.kotlin.verify class ImagePreviewDialogTest { @@ -147,8 +145,6 @@ class ImagePreviewDialogTest { post = testPost, username = "User1", onDismiss = {}, testStarStates, onRatingChanged = {}) } // Perform click on the first star icon of a post with uid "1" - composeTestRule - .onNodeWithTag("Star_1_1") - .assertIsNotDisplayed() // Click on the first star + composeTestRule.onNodeWithTag("Star_1_1").assertIsNotDisplayed() // Click on the first star } } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt index 3315d4b09..baf96e6c4 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt @@ -159,7 +159,7 @@ fun FeedScreen( contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize()) - + Scaffold( containerColor = Color.Transparent, topBar = { @@ -329,29 +329,26 @@ fun updateProfileRatings( * @return An updated [Post] with recalculated ratings and user counts. */ fun calculatePostUpdates(post: Post, userEmail: String, starsCount: Int, oldStarCounts: Int): Post { - val isReturningUser = post.ratedBy.contains(userEmail) - val newStarsCount = - if (starsCount == 0) { - post.starsCount - oldStarCounts - } else if (isReturningUser) post.starsCount - oldStarCounts + starsCount - else post.starsCount + starsCount - val newRatedBy = - if (starsCount == 0) { - post.ratedBy.filter { x -> x != userEmail } - } else if (!isReturningUser) { - post.ratedBy + userEmail - } else { - post.ratedBy - } - val newUsersNumber = newRatedBy.size - val newAvg = - if (newUsersNumber != 0) newStarsCount.toDouble() / newUsersNumber - else 0.0 + val isReturningUser = post.ratedBy.contains(userEmail) + val newStarsCount = + if (starsCount == 0) { + post.starsCount - oldStarCounts + } else if (isReturningUser) post.starsCount - oldStarCounts + starsCount + else post.starsCount + starsCount + val newRatedBy = + if (starsCount == 0) { + post.ratedBy.filter { x -> x != userEmail } + } else if (!isReturningUser) { + post.ratedBy + userEmail + } else { + post.ratedBy + } + val newUsersNumber = newRatedBy.size + val newAvg = if (newUsersNumber != 0) newStarsCount.toDouble() / newUsersNumber else 0.0 - return post.copy( - averageStars = newAvg, - starsCount = newStarsCount, - usersNumber = newUsersNumber, - ratedBy = newRatedBy) + return post.copy( + averageStars = newAvg, + starsCount = newStarsCount, + usersNumber = newUsersNumber, + ratedBy = newRatedBy) } - diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt index 9ddf46ac8..38dff23e4 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt @@ -101,40 +101,40 @@ fun PostItem( style = MaterialTheme.typography.bodyMedium.copy(color = Color.White), modifier = Modifier.testTag("DescriptionTag_${post.uid}")) } - if(showStars){ - // Rating Row - Row( - verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier.fillMaxWidth()) { - starStates.forEachIndexed { index, isFilled -> - IconButton( - onClick = { - val newRating = starStates.mapIndexed { i, _ -> i <= index } - onRatingChanged(newRating) - }, - modifier = - Modifier.size(36.dp).testTag("Star_${index + 1}_${post.uid}")) { - Image( - painter = - painterResource( - id = - if (isFilled) R.drawable.full_star2 - else R.drawable.empty_star2), - contentDescription = "Star") - } + if (showStars) { + // Rating Row + Row( + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.fillMaxWidth()) { + starStates.forEachIndexed { index, isFilled -> + IconButton( + onClick = { + val newRating = starStates.mapIndexed { i, _ -> i <= index } + onRatingChanged(newRating) + }, + modifier = + Modifier.size(36.dp).testTag("Star_${index + 1}_${post.uid}")) { + Image( + painter = + painterResource( + id = + if (isFilled) R.drawable.full_star2 + else R.drawable.empty_star2), + contentDescription = "Star") + } + } + Spacer(modifier = Modifier.weight(1f)) + if (showAverage) { + Text( + text = "Avg: ${"%.1f".format(post.averageStars)}", + style = + MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium, color = color), + modifier = Modifier.testTag("AverageRatingTag_${post.uid}")) + } } - Spacer(modifier = Modifier.weight(1f)) - if (showAverage) { - Text( - text = "Avg: ${"%.1f".format(post.averageStars)}", - style = - MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Medium, color = color), - modifier = Modifier.testTag("AverageRatingTag_${post.uid}")) - } - } - } + } } } } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt index 474e84781..17c1ff177 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt @@ -170,7 +170,6 @@ fun GoogleMapScreen( autoCenteringEnabled = autoCenteringEnabled, onCenteringToggle = { autoCenteringEnabled = it }) - // Map view below the buttons MapView( padding, diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt index 644c6931d..144f3e424 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/components/MapView.kt @@ -64,8 +64,6 @@ fun MapView( } } - - LaunchedEffect(highlightedPost) { highlightedPost?.let { post -> // Zoom to highlighted marker position with zoom level 15f @@ -75,7 +73,6 @@ fun MapView( } } - GoogleMap( modifier = Modifier.fillMaxSize().padding(padding), properties = mapProperties, From 08e973a127d6cbc9ada998182897fc2082e3ee98 Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Fri, 20 Dec 2024 01:36:00 +0100 Subject: [PATCH 10/14] refactor: Remove unnecessary condition starcount == 0 --- .../com/github/lookupgroup27/lookup/ui/feed/Feed.kt | 10 ++++------ .../lookup/ui/feed/components/PostItem.kt | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt index baf96e6c4..16f137b6a 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt @@ -330,15 +330,13 @@ fun updateProfileRatings( */ fun calculatePostUpdates(post: Post, userEmail: String, starsCount: Int, oldStarCounts: Int): Post { val isReturningUser = post.ratedBy.contains(userEmail) + val newStarsCount = - if (starsCount == 0) { - post.starsCount - oldStarCounts - } else if (isReturningUser) post.starsCount - oldStarCounts + starsCount + if (isReturningUser) post.starsCount - oldStarCounts + starsCount else post.starsCount + starsCount + val newRatedBy = - if (starsCount == 0) { - post.ratedBy.filter { x -> x != userEmail } - } else if (!isReturningUser) { + if (!isReturningUser) { post.ratedBy + userEmail } else { post.ratedBy diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt index 38dff23e4..f9c3c9f48 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt @@ -110,7 +110,7 @@ fun PostItem( starStates.forEachIndexed { index, isFilled -> IconButton( onClick = { - val newRating = starStates.mapIndexed { i, _ -> i <= index } + val newRating = List(starStates.size) { i -> i <= index } onRatingChanged(newRating) }, modifier = From 66a5b978644d77902b348932c70b82d5c1ac0893 Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Fri, 20 Dec 2024 01:49:33 +0100 Subject: [PATCH 11/14] fix: Revert unintentional change in function call --- .../github/lookupgroup27/lookup/ui/feed/components/PostItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt index f9c3c9f48..38dff23e4 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/components/PostItem.kt @@ -110,7 +110,7 @@ fun PostItem( starStates.forEachIndexed { index, isFilled -> IconButton( onClick = { - val newRating = List(starStates.size) { i -> i <= index } + val newRating = starStates.mapIndexed { i, _ -> i <= index } onRatingChanged(newRating) }, modifier = From 53a773b4ffe1b9a6f9022f351e02aafb2bdb7fc1 Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Fri, 20 Dec 2024 04:40:08 +0100 Subject: [PATCH 12/14] fix: Refactor rating flow and concurrency handling in PostsViewModel and FeedScreen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Moved rating logic and concurrency-safe updates (Firestore transactions) into PostsViewModel. - Removed direct rating calculations from FeedScreen, relying instead on ViewModel’s state and methods. - Initialize and observe post ratings and user profiles in the ViewModel to ensure a single source of truth. - Improved performance and stability by avoiding repeated rating initializations and ensuring atomic updates. - Enhanced code maintainability and reduced UI complexity by centralizing business logic in the ViewModel. --- .../model/post/PostsRepositoryFirestore.kt | 12 +- .../lookupgroup27/lookup/ui/feed/Feed.kt | 137 ++---------- .../lookup/ui/post/PostsViewModel.kt | 208 ++++++++++++++---- 3 files changed, 190 insertions(+), 167 deletions(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestore.kt b/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestore.kt index 4f0c0df26..6c8358db3 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestore.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestore.kt @@ -1,3 +1,9 @@ +package com.github.lookupgroup27.lookup.model.post + +import android.util.Log +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore + /** * Implementation of the [PostsRepository] interface using Firebase Firestore as the data source. * @@ -7,12 +13,6 @@ * * @property db The [FirebaseFirestore] instance used to interact with the Firestore database. */ -package com.github.lookupgroup27.lookup.model.post - -import android.util.Log -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.firestore.FirebaseFirestore - class PostsRepositoryFirestore(private val db: FirebaseFirestore) : PostsRepository { private val auth = FirebaseAuth.getInstance() diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt index 16f137b6a..afc3cc1da 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt @@ -1,3 +1,15 @@ +/** + * Main screen for displaying a feed of nearby posts. Each post includes the user's image, username, + * location, and an average rating. The feed updates dynamically based on the user's location. + * + * The screen includes a background image, a top app bar, and a bottom navigation menu. If the user + * hasn't granted location permissions or there are no posts, appropriate messages are shown. + * + * @param postsViewModel ViewModel for managing posts. + * @param navigationActions Actions for navigating within the app. + * @param profileViewModel ViewModel for managing user profiles. + * @param initialNearbyPosts Optional parameter for testing, allows pre-loading posts. + */ package com.github.lookupgroup27.lookup.ui.feed import android.Manifest @@ -28,7 +40,6 @@ import androidx.core.content.ContextCompat import com.github.lookupgroup27.lookup.R import com.github.lookupgroup27.lookup.model.location.LocationProviderSingleton import com.github.lookupgroup27.lookup.model.post.Post -import com.github.lookupgroup27.lookup.model.profile.UserProfile import com.github.lookupgroup27.lookup.ui.feed.components.PostItem import com.github.lookupgroup27.lookup.ui.googlemap.components.SelectedPostMarker import com.github.lookupgroup27.lookup.ui.navigation.BottomNavigationMenu @@ -43,18 +54,6 @@ import kotlinx.coroutines.delay private const val LOCATION_PERMISSION_REQUEST_CODE = 1001 private const val NUMBER_OF_STARS = 3 -/** - * Main screen for displaying a feed of nearby posts. Each post includes the user's image, username, - * location, and an average rating. The feed updates dynamically based on the user's location. - * - * The screen includes a background image, a top app bar, and a bottom navigation menu. If the user - * hasn't granted location permissions or there are no posts, appropriate messages are shown. - * - * @param postsViewModel ViewModel for managing posts. - * @param navigationActions Actions for navigating within the app. - * @param profileViewModel ViewModel for managing user profiles. - * @param initialNearbyPosts Optional parameter for testing, allows pre-loading posts. - */ @OptIn(ExperimentalMaterial3Api::class) @SuppressLint("StateFlowValueCalledInComposition") @Composable @@ -71,14 +70,18 @@ fun FeedScreen( profileViewModel.fetchUserProfile() } - // User-related state + // Observe profile and user login state val profile by profileViewModel.userProfile.collectAsState() val user = FirebaseAuth.getInstance().currentUser val isUserLoggedIn = user != null val userEmail = user?.email ?: "" - val username by remember { mutableStateOf(profile?.username ?: "") } - val bio by remember { mutableStateOf(profile?.bio ?: "") } - val email by remember { mutableStateOf(userEmail) } + + // Once profile is available, initialize in PostsViewModel + LaunchedEffect(profile) { + if (profile != null) { + postsViewModel.setUserProfile(profile!!) + } + } // Location setup val context = LocalContext.current @@ -128,18 +131,8 @@ fun FeedScreen( ?: postsViewModel.nearbyPosts.collectAsState()) val nearbyPosts = unfilteredPosts.filter { it.userMail != userEmail } - val postRatings = remember { mutableStateMapOf>() } - - // Initialize post ratings based on the user profile. - LaunchedEffect(nearbyPosts, profile) { - nearbyPosts.forEach { post -> - if (!postRatings.containsKey(post.uid)) { - val savedRating = profile?.ratings?.get(post.uid) ?: 0 - val initialRating = List(NUMBER_OF_STARS) { index -> index < savedRating } - postRatings[post.uid] = initialRating.toMutableList() - } - } - } + // Observe post ratings from ViewModel + val postRatings by postsViewModel.postRatings.collectAsState() // UI Structure Box( @@ -245,31 +238,8 @@ fun FeedScreen( starStates = postRatings[post.uid] ?: List(NUMBER_OF_STARS) { false }, onRatingChanged = { newRating -> - val oldPostRatings = - postRatings[post.uid] ?: List(NUMBER_OF_STARS) { false } - val oldStarCounts = oldPostRatings.count { it } - postRatings[post.uid] = newRating.toList() - val starsCount = newRating.count { it } - - // Update user profile ratings - val newProfile = - updateProfileRatings( - currentProfile = profile, - postUid = post.uid, - starsCount = starsCount, - username = username, - bio = bio, - email = email) - profileViewModel.updateUserProfile(newProfile) - - // Update post details - val updatedPost = - calculatePostUpdates( - post = post, - userEmail = userEmail, - starsCount = starsCount, - oldStarCounts = oldStarCounts) - postsViewModel.updatePost(updatedPost) + // Delegation to ViewModel for concurrency-safe rating update + postsViewModel.updateUserRatingForPost(post.uid, newRating) }, onAddressClick = { clickedPost -> val selectedMarker = @@ -291,62 +261,3 @@ fun FeedScreen( } } } -/** - * Updates the user's profile ratings. - * - * @param currentProfile The current user profile. - * @param postUid The unique identifier of the post being rated. - * @param starsCount The number of stars given to the post. - * @param username The user's username. - * @param bio The user's bio. - * @param email The user's email. - * @return An updated [UserProfile] with the new rating. - */ -fun updateProfileRatings( - currentProfile: UserProfile?, - postUid: String, - starsCount: Int, - username: String, - bio: String, - email: String -): UserProfile { - val updatedRatings = - currentProfile?.ratings?.toMutableMap()?.apply { this[postUid] = starsCount } - ?: mutableMapOf(postUid to starsCount) - - return currentProfile?.copy( - username = username, bio = bio, email = email, ratings = updatedRatings) - ?: UserProfile(username = username, bio = bio, email = email, ratings = updatedRatings) -} - -/** - * Calculates the updated state of a post after a user rates it. - * - * @param post The original post. - * @param userEmail The email of the user rating the post. - * @param starsCount The number of stars the user has given. - * @param oldStarCounts The previous number of stars the user had given. - * @return An updated [Post] with recalculated ratings and user counts. - */ -fun calculatePostUpdates(post: Post, userEmail: String, starsCount: Int, oldStarCounts: Int): Post { - val isReturningUser = post.ratedBy.contains(userEmail) - - val newStarsCount = - if (isReturningUser) post.starsCount - oldStarCounts + starsCount - else post.starsCount + starsCount - - val newRatedBy = - if (!isReturningUser) { - post.ratedBy + userEmail - } else { - post.ratedBy - } - val newUsersNumber = newRatedBy.size - val newAvg = if (newUsersNumber != 0) newStarsCount.toDouble() / newUsersNumber else 0.0 - - return post.copy( - averageStars = newAvg, - starsCount = newStarsCount, - usersNumber = newUsersNumber, - ratedBy = newRatedBy) -} diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt index 8b3eb18dc..da423f143 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt @@ -1,13 +1,3 @@ -/** - * ViewModel for managing posts in the application. - * - * The `PostsViewModel` acts as the intermediary between the UI and the [PostsRepository]. It - * handles the retrieval, addition, updating, and deletion of posts, along with managing - * authentication states. The ViewModel ensures a reactive flow of data to the UI using [StateFlow] - * and [mutableStateOf]. - * - * @property repository The repository responsible for performing operations on posts. - */ package com.github.lookupgroup27.lookup.ui.post import android.annotation.SuppressLint @@ -22,6 +12,7 @@ import com.github.lookupgroup27.lookup.model.location.LocationProviderSingleton import com.github.lookupgroup27.lookup.model.post.Post import com.github.lookupgroup27.lookup.model.post.PostsRepository import com.github.lookupgroup27.lookup.model.post.PostsRepositoryFirestore +import com.github.lookupgroup27.lookup.model.profile.UserProfile import com.github.lookupgroup27.lookup.util.LocationUtils import com.google.firebase.Firebase import com.google.firebase.auth.FirebaseAuth @@ -33,7 +24,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.jetbrains.annotations.VisibleForTesting + +private const val NUMBER_OF_STARS = 3 /** * ViewModel for managing posts and user interactions in the application. @@ -51,37 +43,23 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { /** Holds the currently selected post. */ val post = mutableStateOf(null) - init { - repository.init { - auth?.addAuthStateListener(authListener) - getPosts() // to ensure posts are loaded initially - } - } - @SuppressLint("StaticFieldLeak") private var context: Context? = null // LocationProvider instance to get user's current location private lateinit var locationProvider: LocationProvider - // Method to initialize context - fun setContext(context: Context) { - this.context = context - locationProvider = context.let { LocationProviderSingleton.getInstance(it) } - // Start monitoring - startLocationMonitoring() - // Start periodic fetching - startPeriodicPostFetching() - } + /** Internal mutable state to hold all posts. */ + private val _allPosts = MutableStateFlow>(emptyList()) + /** Publicly exposed [StateFlow] to observe the list of all posts. */ + val allPosts: StateFlow> = _allPosts.asStateFlow() // MutableStateFlow to hold the list of nearby posts private val _nearbyPosts = MutableStateFlow>(emptyList()) val nearbyPosts: StateFlow> = _nearbyPosts - /** Internal mutable state to hold all posts. */ - private val _allPosts = MutableStateFlow>(emptyList()) - - /** Publicly exposed [StateFlow] to observe the list of all posts. */ - val allPosts: StateFlow> = _allPosts.asStateFlow() + // Holds user ratings for posts: map of postUid to a list of booleans representing star states + private val _postRatings = MutableStateFlow>>(emptyMap()) + val postRatings: StateFlow>> = _postRatings.asStateFlow() private val auth: FirebaseAuth? = try { @@ -101,6 +79,30 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { } } + // User profile data, needed for rating initialization + private var userProfile: UserProfile? = null + private val userEmail: String? + get() = auth?.currentUser?.email + + init { + repository.init { + auth?.addAuthStateListener(authListener) + getPosts() // to ensure posts are loaded initially + } + } + + /** + * Sets the application [Context] and initializes location monitoring and periodic post fetching. + * + * @param context The application [Context]. + */ + fun setContext(context: Context) { + this.context = context + locationProvider = LocationProviderSingleton.getInstance(context) + startLocationMonitoring() + startPeriodicPostFetching() + } + /** * Selects a post and updates the `post` state. * @@ -132,6 +134,10 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { onSuccess = { if (it != null) { _allPosts.value = it + // Initialize ratings if profile is already set + if (userProfile != null) { + initializeRatings(it, userProfile) + } } }, onFailure = {}) @@ -158,12 +164,11 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { * @param onFailure Callback executed on deletion failure. */ fun deletePost(postUid: String, onSuccess: () -> Unit = {}, onFailure: (Exception) -> Unit = {}) { - repository.deletePost(postUid, onSuccess, onFailure) } /** - * Updates an existing post in the repository. + * Updates an existing post in the repository using concurrency-safe operations. * * @param post The [Post] object containing updated data. * @param onSuccess Callback executed on successful update. @@ -235,13 +240,6 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { * Sorts and filters a list of posts based on their distance from the user's location and the time * they were posted. * - * This function: - * - Calculates the distance from the user's current location to each post's location. - * - Sorts posts by distance in ascending order (closest posts first). - * - If two posts have the same distance, sorts them by timestamp in descending order (most recent - * posts first). - * - Limits the result to the 10 closest posts. - * * @param posts The list of [Post] objects to sort and filter. * @param userLatitude The latitude of the user's current location. * @param userLongitude The longitude of the user's current location. @@ -257,14 +255,14 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { val distance = LocationUtils.calculateDistance( userLatitude, userLongitude, post.latitude, post.longitude) - post to distance // Pair each post with its calculated distance + post to distance } .sortedWith( compareBy> { it.second } // Sort by distance (ascending) .thenByDescending { it.first.timestamp } // Then sort by timestamp (descending) ) - .take(10) // Take the 10 closest posts - .map { it.first } // Extract only the posts + .take(10) + .map { it.first } } /** @@ -274,7 +272,7 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { private fun startLocationMonitoring() { viewModelScope.launch { while (true) { - val location = locationProvider?.currentLocation?.value + val location = locationProvider.currentLocation.value if (location != null) { fetchSortedPosts() } @@ -296,9 +294,74 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { } } - @VisibleForTesting - fun setLocationProviderForTesting(provider: LocationProvider) { - locationProvider = provider + /** + * Call this when the user profile is available to initialize ratings. Ensure that posts are + * already fetched before calling this. + * + * @param profile The [UserProfile] of the currently logged-in user. + */ + fun setUserProfile(profile: UserProfile) { + this.userProfile = profile + initializeRatings(_allPosts.value, profile) + } + + /** + * Initializes post ratings for the current user once both posts and profile are available. + * + * @param posts The current list of posts. + * @param profile The current user's profile with rating data. + */ + private fun initializeRatings(posts: List, profile: UserProfile?) { + if (profile == null || posts.isEmpty()) return + + val newRatings = mutableMapOf>() + for (post in posts) { + val savedRating = profile.ratings[post.uid] ?: 0 + val initialRating = List(NUMBER_OF_STARS) { index -> index < savedRating } + newRatings[post.uid] = initialRating + } + _postRatings.value = newRatings + } + + /** + * Handles user rating updates. This function: + * - Updates the local rating state. + * - Updates the user profile ratings in memory (the calling code should persist profile changes). + * - Executes a concurrency-safe transaction to update the post in the repository. + * + * @param postUid The unique ID of the post being rated. + * @param newRating The new rating state as a list of booleans representing stars. + */ + fun updateUserRatingForPost(postUid: String, newRating: List) { + val oldMap = _postRatings.value.toMutableMap() + val oldRating = oldMap[postUid] ?: List(NUMBER_OF_STARS) { false } + val oldStarCount = oldRating.count { it } + val starsCount = newRating.count { it } + + oldMap[postUid] = newRating + _postRatings.value = oldMap + + val currentProfile = userProfile + val currentPosts = _allPosts.value + val postToUpdate = currentPosts.find { it.uid == postUid } + + if (postToUpdate != null && currentProfile != null && userEmail != null) { + // Update user profile ratings in memory + val updatedProfile = + updateProfileRatings( + currentProfile, + postUid, + starsCount, + currentProfile.username, + currentProfile.bio, + currentProfile.email) + userProfile = updatedProfile + // Here you would typically call a profile repository or method to persist the updated profile + + // Calculate the new post state + val updatedPost = calculatePostUpdates(postToUpdate, userEmail!!, starsCount, oldStarCount) + updatePost(updatedPost) + } } companion object { @@ -316,3 +379,52 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { } } } + +/** + * Updates the user's profile ratings. + * + * @param currentProfile The current user profile. + * @param postUid The unique identifier of the post being rated. + * @param starsCount The number of stars given to the post. + * @param username The user's username. + * @param bio The user's bio. + * @param email The user's email. + * @return An updated [UserProfile] with the new rating. + */ +fun updateProfileRatings( + currentProfile: UserProfile, + postUid: String, + starsCount: Int, + username: String, + bio: String, + email: String +): UserProfile { + val updatedRatings = currentProfile.ratings.toMutableMap().apply { this[postUid] = starsCount } + return currentProfile.copy( + username = username, bio = bio, email = email, ratings = updatedRatings) +} + +/** + * Calculates the updated state of a post after a user rates it. + * + * @param post The original post. + * @param userEmail The email of the user rating the post. + * @param starsCount The number of stars the user has given. + * @param oldStarCounts The previous number of stars the user had given. + * @return An updated [Post] with recalculated ratings and user counts. + */ +fun calculatePostUpdates(post: Post, userEmail: String, starsCount: Int, oldStarCounts: Int): Post { + val isReturningUser = post.ratedBy.contains(userEmail) + val newStarsCount = + if (isReturningUser) post.starsCount - oldStarCounts + starsCount + else post.starsCount + starsCount + val newRatedBy = if (!isReturningUser) post.ratedBy + userEmail else post.ratedBy + val newUsersNumber = newRatedBy.size + val newAvg = if (newUsersNumber != 0) newStarsCount.toDouble() / newUsersNumber else 0.0 + + return post.copy( + averageStars = newAvg, + starsCount = newStarsCount, + usersNumber = newUsersNumber, + ratedBy = newRatedBy) +} From ed79301f181c7b15caea3e331bc68e48594a5d80 Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Fri, 20 Dec 2024 04:59:51 +0100 Subject: [PATCH 13/14] fix: Revert changes of last commit to implement simple solution --- .../model/post/PostsRepositoryFirestore.kt | 12 +- .../lookupgroup27/lookup/ui/feed/Feed.kt | 116 +++++++--- .../lookup/ui/post/PostsViewModel.kt | 208 ++++-------------- 3 files changed, 140 insertions(+), 196 deletions(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestore.kt b/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestore.kt index 6c8358db3..4f0c0df26 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestore.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestore.kt @@ -1,9 +1,3 @@ -package com.github.lookupgroup27.lookup.model.post - -import android.util.Log -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.firestore.FirebaseFirestore - /** * Implementation of the [PostsRepository] interface using Firebase Firestore as the data source. * @@ -13,6 +7,12 @@ import com.google.firebase.firestore.FirebaseFirestore * * @property db The [FirebaseFirestore] instance used to interact with the Firestore database. */ +package com.github.lookupgroup27.lookup.model.post + +import android.util.Log +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore + class PostsRepositoryFirestore(private val db: FirebaseFirestore) : PostsRepository { private val auth = FirebaseAuth.getInstance() diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt index afc3cc1da..728670c16 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt @@ -1,22 +1,12 @@ -/** - * Main screen for displaying a feed of nearby posts. Each post includes the user's image, username, - * location, and an average rating. The feed updates dynamically based on the user's location. - * - * The screen includes a background image, a top app bar, and a bottom navigation menu. If the user - * hasn't granted location permissions or there are no posts, appropriate messages are shown. - * - * @param postsViewModel ViewModel for managing posts. - * @param navigationActions Actions for navigating within the app. - * @param profileViewModel ViewModel for managing user profiles. - * @param initialNearbyPosts Optional parameter for testing, allows pre-loading posts. - */ package com.github.lookupgroup27.lookup.ui.feed import android.Manifest import android.annotation.SuppressLint +import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.util.Log import android.widget.Toast +import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image @@ -27,6 +17,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -40,6 +31,7 @@ import androidx.core.content.ContextCompat import com.github.lookupgroup27.lookup.R import com.github.lookupgroup27.lookup.model.location.LocationProviderSingleton import com.github.lookupgroup27.lookup.model.post.Post +import com.github.lookupgroup27.lookup.model.profile.UserProfile import com.github.lookupgroup27.lookup.ui.feed.components.PostItem import com.github.lookupgroup27.lookup.ui.googlemap.components.SelectedPostMarker import com.github.lookupgroup27.lookup.ui.navigation.BottomNavigationMenu @@ -54,6 +46,18 @@ import kotlinx.coroutines.delay private const val LOCATION_PERMISSION_REQUEST_CODE = 1001 private const val NUMBER_OF_STARS = 3 +/** + * Main screen for displaying a feed of nearby posts. Each post includes the user's image, username, + * location, and an average rating. The feed updates dynamically based on the user's location. + * + * The screen includes a background image, a top app bar, and a bottom navigation menu. If the user + * hasn't granted location permissions or there are no posts, appropriate messages are shown. + * + * @param postsViewModel ViewModel for managing posts. + * @param navigationActions Actions for navigating within the app. + * @param profileViewModel ViewModel for managing user profiles. + * @param initialNearbyPosts Optional parameter for testing, allows pre-loading posts. + */ @OptIn(ExperimentalMaterial3Api::class) @SuppressLint("StateFlowValueCalledInComposition") @Composable @@ -64,27 +68,26 @@ fun FeedScreen( initialNearbyPosts: List? = null, testNoLoca: Boolean = false ) { - // Fetch user profile - LaunchedEffect(Unit) { - Log.d("FeedScreen", "Fetching user profile") - profileViewModel.fetchUserProfile() + val context = LocalContext.current + // Lock the screen orientation to portrait mode. + DisposableEffect(Unit) { + val activity = context as? ComponentActivity + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + onDispose { activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } } - // Observe profile and user login state + // User-related state + + profileViewModel.fetchUserProfile() val profile by profileViewModel.userProfile.collectAsState() val user = FirebaseAuth.getInstance().currentUser val isUserLoggedIn = user != null val userEmail = user?.email ?: "" - - // Once profile is available, initialize in PostsViewModel - LaunchedEffect(profile) { - if (profile != null) { - postsViewModel.setUserProfile(profile!!) - } - } + val username by remember { mutableStateOf(profile?.username ?: "") } + val bio by remember { mutableStateOf(profile?.bio ?: "") } + val email by remember { mutableStateOf(userEmail) } // Location setup - val context = LocalContext.current val locationProvider = LocationProviderSingleton.getInstance(context) var locationPermissionGranted by remember { mutableStateOf( @@ -131,8 +134,18 @@ fun FeedScreen( ?: postsViewModel.nearbyPosts.collectAsState()) val nearbyPosts = unfilteredPosts.filter { it.userMail != userEmail } - // Observe post ratings from ViewModel - val postRatings by postsViewModel.postRatings.collectAsState() + val postRatings = remember { mutableStateMapOf>() } + + // Initialize post ratings based on the user profile. + LaunchedEffect(nearbyPosts, profile) { + nearbyPosts.forEach { post -> + if (!postRatings.containsKey(post.uid)) { + val savedRating = profile?.ratings?.get(post.uid) ?: 0 + val initialRating = List(NUMBER_OF_STARS) { index -> index < savedRating } + postRatings[post.uid] = initialRating.toMutableList() + } + } + } // UI Structure Box( @@ -151,7 +164,7 @@ fun FeedScreen( painter = painterResource(R.drawable.background_blurred), contentDescription = null, contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize()) + modifier = Modifier.fillMaxSize().blur(20.dp)) Scaffold( containerColor = Color.Transparent, @@ -238,8 +251,51 @@ fun FeedScreen( starStates = postRatings[post.uid] ?: List(NUMBER_OF_STARS) { false }, onRatingChanged = { newRating -> - // Delegation to ViewModel for concurrency-safe rating update - postsViewModel.updateUserRatingForPost(post.uid, newRating) + val oldPostRatings = + postRatings[post.uid] ?: mutableListOf(false, false, false) + val oldStarCounts = oldPostRatings.count { it } + // Directly modify the existing starStates list to avoid + // creating a new list + postRatings[post.uid] = newRating.toList() + // Update the stars count based on the new rating + val starsCount = newRating.count { it } + // Update user profile with the new rating count + val updatedRatings = profile?.ratings?.toMutableMap() + updatedRatings?.set(post.uid, starsCount) + val newProfile: UserProfile = + profile?.copy( + username = username, + bio = bio, + email = email, + ratings = updatedRatings ?: emptyMap()) + ?: UserProfile( + username = username, + bio = bio, + email = email, + ratings = updatedRatings ?: emptyMap()) + profileViewModel.updateUserProfile(newProfile) + + val isReturningUser = post.ratedBy.contains(userEmail) + val newStarsCount = + if (isReturningUser) + post.starsCount - oldStarCounts + starsCount + else post.starsCount + starsCount + val newUsersNumber = + if (isReturningUser) post.usersNumber + else post.usersNumber + 1 + val newAvg = newStarsCount.toDouble() / newUsersNumber + + postsViewModel.updatePost( + post.copy( + averageStars = newAvg, + starsCount = newStarsCount, + usersNumber = newUsersNumber, + ratedBy = + if (!isReturningUser) { + post.ratedBy + userEmail + } else { + post.ratedBy + })) }, onAddressClick = { clickedPost -> val selectedMarker = diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt index da423f143..8b3eb18dc 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt @@ -1,3 +1,13 @@ +/** + * ViewModel for managing posts in the application. + * + * The `PostsViewModel` acts as the intermediary between the UI and the [PostsRepository]. It + * handles the retrieval, addition, updating, and deletion of posts, along with managing + * authentication states. The ViewModel ensures a reactive flow of data to the UI using [StateFlow] + * and [mutableStateOf]. + * + * @property repository The repository responsible for performing operations on posts. + */ package com.github.lookupgroup27.lookup.ui.post import android.annotation.SuppressLint @@ -12,7 +22,6 @@ import com.github.lookupgroup27.lookup.model.location.LocationProviderSingleton import com.github.lookupgroup27.lookup.model.post.Post import com.github.lookupgroup27.lookup.model.post.PostsRepository import com.github.lookupgroup27.lookup.model.post.PostsRepositoryFirestore -import com.github.lookupgroup27.lookup.model.profile.UserProfile import com.github.lookupgroup27.lookup.util.LocationUtils import com.google.firebase.Firebase import com.google.firebase.auth.FirebaseAuth @@ -24,8 +33,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch - -private const val NUMBER_OF_STARS = 3 +import org.jetbrains.annotations.VisibleForTesting /** * ViewModel for managing posts and user interactions in the application. @@ -43,23 +51,37 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { /** Holds the currently selected post. */ val post = mutableStateOf(null) + init { + repository.init { + auth?.addAuthStateListener(authListener) + getPosts() // to ensure posts are loaded initially + } + } + @SuppressLint("StaticFieldLeak") private var context: Context? = null // LocationProvider instance to get user's current location private lateinit var locationProvider: LocationProvider - /** Internal mutable state to hold all posts. */ - private val _allPosts = MutableStateFlow>(emptyList()) - /** Publicly exposed [StateFlow] to observe the list of all posts. */ - val allPosts: StateFlow> = _allPosts.asStateFlow() + // Method to initialize context + fun setContext(context: Context) { + this.context = context + locationProvider = context.let { LocationProviderSingleton.getInstance(it) } + // Start monitoring + startLocationMonitoring() + // Start periodic fetching + startPeriodicPostFetching() + } // MutableStateFlow to hold the list of nearby posts private val _nearbyPosts = MutableStateFlow>(emptyList()) val nearbyPosts: StateFlow> = _nearbyPosts - // Holds user ratings for posts: map of postUid to a list of booleans representing star states - private val _postRatings = MutableStateFlow>>(emptyMap()) - val postRatings: StateFlow>> = _postRatings.asStateFlow() + /** Internal mutable state to hold all posts. */ + private val _allPosts = MutableStateFlow>(emptyList()) + + /** Publicly exposed [StateFlow] to observe the list of all posts. */ + val allPosts: StateFlow> = _allPosts.asStateFlow() private val auth: FirebaseAuth? = try { @@ -79,30 +101,6 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { } } - // User profile data, needed for rating initialization - private var userProfile: UserProfile? = null - private val userEmail: String? - get() = auth?.currentUser?.email - - init { - repository.init { - auth?.addAuthStateListener(authListener) - getPosts() // to ensure posts are loaded initially - } - } - - /** - * Sets the application [Context] and initializes location monitoring and periodic post fetching. - * - * @param context The application [Context]. - */ - fun setContext(context: Context) { - this.context = context - locationProvider = LocationProviderSingleton.getInstance(context) - startLocationMonitoring() - startPeriodicPostFetching() - } - /** * Selects a post and updates the `post` state. * @@ -134,10 +132,6 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { onSuccess = { if (it != null) { _allPosts.value = it - // Initialize ratings if profile is already set - if (userProfile != null) { - initializeRatings(it, userProfile) - } } }, onFailure = {}) @@ -164,11 +158,12 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { * @param onFailure Callback executed on deletion failure. */ fun deletePost(postUid: String, onSuccess: () -> Unit = {}, onFailure: (Exception) -> Unit = {}) { + repository.deletePost(postUid, onSuccess, onFailure) } /** - * Updates an existing post in the repository using concurrency-safe operations. + * Updates an existing post in the repository. * * @param post The [Post] object containing updated data. * @param onSuccess Callback executed on successful update. @@ -240,6 +235,13 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { * Sorts and filters a list of posts based on their distance from the user's location and the time * they were posted. * + * This function: + * - Calculates the distance from the user's current location to each post's location. + * - Sorts posts by distance in ascending order (closest posts first). + * - If two posts have the same distance, sorts them by timestamp in descending order (most recent + * posts first). + * - Limits the result to the 10 closest posts. + * * @param posts The list of [Post] objects to sort and filter. * @param userLatitude The latitude of the user's current location. * @param userLongitude The longitude of the user's current location. @@ -255,14 +257,14 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { val distance = LocationUtils.calculateDistance( userLatitude, userLongitude, post.latitude, post.longitude) - post to distance + post to distance // Pair each post with its calculated distance } .sortedWith( compareBy> { it.second } // Sort by distance (ascending) .thenByDescending { it.first.timestamp } // Then sort by timestamp (descending) ) - .take(10) - .map { it.first } + .take(10) // Take the 10 closest posts + .map { it.first } // Extract only the posts } /** @@ -272,7 +274,7 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { private fun startLocationMonitoring() { viewModelScope.launch { while (true) { - val location = locationProvider.currentLocation.value + val location = locationProvider?.currentLocation?.value if (location != null) { fetchSortedPosts() } @@ -294,74 +296,9 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { } } - /** - * Call this when the user profile is available to initialize ratings. Ensure that posts are - * already fetched before calling this. - * - * @param profile The [UserProfile] of the currently logged-in user. - */ - fun setUserProfile(profile: UserProfile) { - this.userProfile = profile - initializeRatings(_allPosts.value, profile) - } - - /** - * Initializes post ratings for the current user once both posts and profile are available. - * - * @param posts The current list of posts. - * @param profile The current user's profile with rating data. - */ - private fun initializeRatings(posts: List, profile: UserProfile?) { - if (profile == null || posts.isEmpty()) return - - val newRatings = mutableMapOf>() - for (post in posts) { - val savedRating = profile.ratings[post.uid] ?: 0 - val initialRating = List(NUMBER_OF_STARS) { index -> index < savedRating } - newRatings[post.uid] = initialRating - } - _postRatings.value = newRatings - } - - /** - * Handles user rating updates. This function: - * - Updates the local rating state. - * - Updates the user profile ratings in memory (the calling code should persist profile changes). - * - Executes a concurrency-safe transaction to update the post in the repository. - * - * @param postUid The unique ID of the post being rated. - * @param newRating The new rating state as a list of booleans representing stars. - */ - fun updateUserRatingForPost(postUid: String, newRating: List) { - val oldMap = _postRatings.value.toMutableMap() - val oldRating = oldMap[postUid] ?: List(NUMBER_OF_STARS) { false } - val oldStarCount = oldRating.count { it } - val starsCount = newRating.count { it } - - oldMap[postUid] = newRating - _postRatings.value = oldMap - - val currentProfile = userProfile - val currentPosts = _allPosts.value - val postToUpdate = currentPosts.find { it.uid == postUid } - - if (postToUpdate != null && currentProfile != null && userEmail != null) { - // Update user profile ratings in memory - val updatedProfile = - updateProfileRatings( - currentProfile, - postUid, - starsCount, - currentProfile.username, - currentProfile.bio, - currentProfile.email) - userProfile = updatedProfile - // Here you would typically call a profile repository or method to persist the updated profile - - // Calculate the new post state - val updatedPost = calculatePostUpdates(postToUpdate, userEmail!!, starsCount, oldStarCount) - updatePost(updatedPost) - } + @VisibleForTesting + fun setLocationProviderForTesting(provider: LocationProvider) { + locationProvider = provider } companion object { @@ -379,52 +316,3 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { } } } - -/** - * Updates the user's profile ratings. - * - * @param currentProfile The current user profile. - * @param postUid The unique identifier of the post being rated. - * @param starsCount The number of stars given to the post. - * @param username The user's username. - * @param bio The user's bio. - * @param email The user's email. - * @return An updated [UserProfile] with the new rating. - */ -fun updateProfileRatings( - currentProfile: UserProfile, - postUid: String, - starsCount: Int, - username: String, - bio: String, - email: String -): UserProfile { - val updatedRatings = currentProfile.ratings.toMutableMap().apply { this[postUid] = starsCount } - return currentProfile.copy( - username = username, bio = bio, email = email, ratings = updatedRatings) -} - -/** - * Calculates the updated state of a post after a user rates it. - * - * @param post The original post. - * @param userEmail The email of the user rating the post. - * @param starsCount The number of stars the user has given. - * @param oldStarCounts The previous number of stars the user had given. - * @return An updated [Post] with recalculated ratings and user counts. - */ -fun calculatePostUpdates(post: Post, userEmail: String, starsCount: Int, oldStarCounts: Int): Post { - val isReturningUser = post.ratedBy.contains(userEmail) - val newStarsCount = - if (isReturningUser) post.starsCount - oldStarCounts + starsCount - else post.starsCount + starsCount - val newRatedBy = if (!isReturningUser) post.ratedBy + userEmail else post.ratedBy - val newUsersNumber = newRatedBy.size - val newAvg = if (newUsersNumber != 0) newStarsCount.toDouble() / newUsersNumber else 0.0 - - return post.copy( - averageStars = newAvg, - starsCount = newStarsCount, - usersNumber = newUsersNumber, - ratedBy = newRatedBy) -} From 1b5fef4def36c00d6baa3d9a9ccf359d540f0747 Mon Sep 17 00:00:00 2001 From: Mehdi <166954476+mehdi-hamirifou@users.noreply.github.com> Date: Fri, 20 Dec 2024 05:08:06 +0100 Subject: [PATCH 14/14] style: Apply ktfmtFormat --- .../main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt index 1c8526aa6..241e88916 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt @@ -76,7 +76,6 @@ fun FeedScreen( onDispose { activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } } - // User-related state profileViewModel.fetchUserProfile()