Repository: cuappdev/resell-android
Pull request #53: Fix resell preview by adding infinite transition
Author: login: thisjustin123
Created: 2025-03-02T20:20:08Z | Merged: 2025-03-03T16:35:42Z

<!-- IF A SECTION IS NOT APPLICABLE TO YOU, PLEASE DELETE IT!! -->
<!-- Your title should be able to summarize what changes you’ve made in one sentence. For example: “Exclude staff from the check for follows”. For stacked PRs, please indicate clearly in the title where in the stack you are. For example: “[Eatery Refactor][4/5] Converted all files to MVP model” -->
## Overview
<!-- Summarize your changes here. -->

Fixes `ResellCardPreview` to now actually show the preview.

Deletes unnecessary usages of `CompositionLocal`.

## Changes Made
<!-- Include details of what your changes actually are and how it is intended to work. -->

1. Tweaks `LocalInfiniteShimmer` (from `LocalInfiniteLoading`) to be a shimmer color directly.
2. Adds a `.shimmer()` Modifier that automatically pulls from the above composition local to apply the shimmer background automatically.
3. Deletes unused usages of CompositionLocal. 
4. Replaces shimmer in `ResellCard` to get its preview working, as well as `ChatImage`.

## Test Coverage
<!-- Describe how you tested this feature. Manual testing and/or unit testing. Please include repro steps and/or how to turn the feature on if applicable. -->

Ran the preview in Android Studio and on emulator.

## Screenshots (delete if not applicable)
<!-- This could include of screenshots of the new feature / proof that the changes work. -->
<details>
  <summary>Emulator screen recording w/ animation</summary>
  

https://github.com/user-attachments/assets/a25b6426-dee5-4d72-977a-73eed7a401d9

</details>




Diff:
diff --git a/app/src/main/java/com/cornellappdev/resell/android/MainActivity.kt b/app/src/main/java/com/cornellappdev/resell/android/MainActivity.kt
index 400b32c9..dd9ca3cb 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/MainActivity.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/MainActivity.kt
@@ -5,22 +5,18 @@ import android.content.Intent
 import android.content.pm.PackageManager
 import android.os.Build
 import android.os.Bundle
-import android.util.Log
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.activity.enableEdgeToEdge
 import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.core.app.ActivityCompat
 import androidx.core.content.ContextCompat
 import com.cornellappdev.resell.android.model.api.NotificationData
 import com.cornellappdev.resell.android.model.settings.NotificationsRepository
 import com.cornellappdev.resell.android.ui.screens.root.RootNavigation
 import com.cornellappdev.resell.android.ui.theme.ResellTheme
-import com.cornellappdev.resell.android.util.LocalFireStore
 import com.cornellappdev.resell.android.viewmodel.root.RootConfirmationRepository
 import com.google.firebase.FirebaseApp
-import com.google.firebase.firestore.FirebaseFirestore
 import dagger.hilt.android.AndroidEntryPoint
 import javax.inject.Inject
 
@@ -36,16 +32,13 @@ class MainActivity : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         FirebaseApp.initializeApp(this)
-        val firestore = FirebaseFirestore.getInstance()
         enableEdgeToEdge()
 
         handleNotificationIntent(intent)
 
         setContent {
             ResellTheme {
-                CompositionLocalProvider(LocalFireStore provides firestore) {
-                    RootNavigation()
-                }
+                RootNavigation()
             }
         }
     }
diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/components/chat/ChatImage.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/components/chat/ChatImage.kt
index 1dc600cb..fd6a728f 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/ui/components/chat/ChatImage.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/ui/components/chat/ChatImage.kt
@@ -20,10 +20,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
 import com.cornellappdev.resell.android.model.CoilRepository
 import com.cornellappdev.resell.android.model.classes.ResellApiResponse
 import com.cornellappdev.resell.android.ui.theme.Secondary
-import com.cornellappdev.resell.android.ui.theme.Stroke
-import com.cornellappdev.resell.android.ui.theme.Wash
-import com.cornellappdev.resell.android.ui.theme.interpolateColorHSV
-import com.cornellappdev.resell.android.util.LocalInfiniteLoading
+import com.cornellappdev.resell.android.util.shimmer
 import com.cornellappdev.resell.android.viewmodel.ResellViewModel
 import dagger.hilt.android.lifecycle.HiltViewModel
 import javax.inject.Inject
@@ -49,13 +46,7 @@ fun ChatImage(
                     Box(
                         modifier = Modifier
                             .height(175.dp)
-                            .background(
-                                interpolateColorHSV(
-                                    Wash,
-                                    Stroke,
-                                    LocalInfiniteLoading.current
-                                )
-                            )
+                            .shimmer()
                             .clip(RoundedCornerShape(16.dp))
                             .fillMaxWidth()
                     )
diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellCard.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellCard.kt
index f21d2d6b..33b23ec1 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellCard.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellCard.kt
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.heightIn
@@ -33,12 +32,11 @@ import androidx.hilt.navigation.compose.hiltViewModel
 import com.cornellappdev.resell.android.model.CoilRepository
 import com.cornellappdev.resell.android.model.classes.ResellApiResponse
 import com.cornellappdev.resell.android.ui.theme.Padding
+import com.cornellappdev.resell.android.ui.theme.ResellPreview
 import com.cornellappdev.resell.android.ui.theme.Secondary
 import com.cornellappdev.resell.android.ui.theme.Stroke
 import com.cornellappdev.resell.android.ui.theme.Style
-import com.cornellappdev.resell.android.ui.theme.Wash
-import com.cornellappdev.resell.android.ui.theme.interpolateColorHSV
-import com.cornellappdev.resell.android.util.LocalInfiniteLoading
+import com.cornellappdev.resell.android.util.shimmer
 import com.cornellappdev.resell.android.viewmodel.ResellViewModel
 import dagger.hilt.android.lifecycle.HiltViewModel
 import javax.inject.Inject
@@ -55,9 +53,28 @@ fun ResellCard(
     viewModel: ResellCardViewModel = hiltViewModel(),
     onClick: () -> Unit,
 ) {
-    val screenWidth = LocalConfiguration.current.screenWidthDp.dp
     val image by viewModel.getImageUrlState(imageUrl)
 
+    // Separated into a different function to allow `@Preview` despite the viewModel.
+    ResellCardContent(
+        modifier = modifier,
+        onClick = onClick,
+        image = image,
+        title = title,
+        price = price,
+    )
+}
+
+@Composable
+private fun ResellCardContent(
+    modifier: Modifier = Modifier,
+    onClick: () -> Unit,
+    image: ResellApiResponse<ImageBitmap>,
+    title: String,
+    price: String,
+) {
+    val screenWidth = LocalConfiguration.current.screenWidthDp.dp
+
     Column(
         modifier = modifier
             .widthIn(max = 0.5f * screenWidth)
@@ -102,13 +119,7 @@ private fun AnimatedClampedAsyncImage(image: ResellApiResponse<ImageBitmap>) {
                     Box(
                         modifier = Modifier
                             .height(175.dp)
-                            .background(
-                                interpolateColorHSV(
-                                    Wash,
-                                    Stroke,
-                                    LocalInfiniteLoading.current
-                                )
-                            )
+                            .shimmer()
                             .fillMaxWidth()
                     )
                 }
@@ -140,33 +151,33 @@ private fun AnimatedClampedAsyncImage(image: ResellApiResponse<ImageBitmap>) {
 
 @Preview
 @Composable
-private fun PreviewListingCard() {
-    Column(modifier = Modifier.fillMaxSize()) {
-        ResellCard(
-            imageUrl = "",
-            title = "Title",
-            price = "$10.00",
-            onClick = {}
-        )
+private fun ResellCardPreview() = ResellPreview(
+    backgroundColor = Color.Transparent
+) {
+    ResellCardContent(
+        image = ResellApiResponse.Pending,
+        title = "Title",
+        price = "$10.00",
+        onClick = {},
+    )
 
-        Spacer(modifier = Modifier.padding(Padding.large))
+    Spacer(modifier = Modifier.padding(Padding.large))
 
-        ResellCard(
-            imageUrl = "",
-            title = "Richie man",
-            price = "$999.99",
-            onClick = {}
-        )
+    ResellCardContent(
+        image = ResellApiResponse.Error,
+        title = "Richie man",
+        price = "$999.99",
+        onClick = {},
+    )
 
-        Spacer(modifier = Modifier.padding(Padding.large))
+    Spacer(modifier = Modifier.padding(Padding.large))
 
-        ResellCard(
-            imageUrl = "",
-            title = "Richie man with a damn long listing name",
-            price = "$999.99",
-            onClick = {}
-        )
-    }
+    ResellCardContent(
+        image = ResellApiResponse.Success(ImageBitmap(1, 1)),
+        title = "Richie man with a damn long listing name",
+        price = "$999.99",
+        onClick = {},
+    )
 }
 
 // Semantics to be able to call this function in the composable...
diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/MainTabNavigation.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/MainTabNavigation.kt
index 38f0ce0d..893e0eff 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/MainTabNavigation.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/MainTabNavigation.kt
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.navigationBars
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
@@ -33,7 +32,6 @@ import com.cornellappdev.resell.android.ui.components.main.PostFloatingActionBut
 import com.cornellappdev.resell.android.ui.components.main.ShadeOverlay
 import com.cornellappdev.resell.android.ui.components.nav.NavBar
 import com.cornellappdev.resell.android.ui.screens.main.ResellMainScreen.Home.toResellMainScreen
-import com.cornellappdev.resell.android.util.LocalMainNavigator
 import com.cornellappdev.resell.android.util.closeApp
 import com.cornellappdev.resell.android.viewmodel.navigation.MainNavigationViewModel
 import kotlinx.serialization.Serializable
@@ -58,77 +56,75 @@ fun MainTabNavigation(
         closeApp(context)
     }
 
-    CompositionLocalProvider(LocalMainNavigator provides mainNav) {
-        Box(
-            modifier = Modifier
-                .fillMaxSize()
-                .background(Color.White)
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .background(Color.White)
+    ) {
+        NavHost(
+            navController = mainNav,
+            startDestination = ResellMainScreen.Home,
+            modifier = Modifier.fillMaxSize()
         ) {
-            NavHost(
-                navController = LocalMainNavigator.current,
-                startDestination = ResellMainScreen.Home,
-                modifier = Modifier.fillMaxSize()
-            ) {
-                composable<ResellMainScreen.Home> {
-                    HomeScreen()
-                }
-
-                composable<ResellMainScreen.Bookmarks> {
-                    SavedScreen()
-                }
-
-                composable<ResellMainScreen.Messages> {
-                    MessagesScreen()
-                }
-
-                composable<ResellMainScreen.User> {
-                    ProfileScreen()
-                }
+            composable<ResellMainScreen.Home> {
+                HomeScreen()
+            }
+
+            composable<ResellMainScreen.Bookmarks> {
+                SavedScreen()
+            }
+
+            composable<ResellMainScreen.Messages> {
+                MessagesScreen()
             }
 
-            NavBar(
-                onHomeClick = {
-                    mainNav.navigate(ResellMainScreen.Home)
-                },
-                onBookmarksClick = {
-                    mainNav.navigate(ResellMainScreen.Bookmarks)
-                },
-                onMessagesClick = {
-                    mainNav.navigate(ResellMainScreen.Messages)
-                },
-                onUserClick = {
-                    mainNav.navigate(ResellMainScreen.User)
-                },
-                selectedTab = navDestination?.destination?.route?.toResellMainScreen()
-                    ?: ResellMainScreen.Home,
-                modifier = Modifier.align(Alignment.BottomCenter),
-                enabled = uiState.bottomBarEnabled
-            )
-
-            ShadeOverlay(
-                onTapped = {
-                    mainNavigationViewModel.onShadeTapped()
-                },
-                visible = uiState.newPostExpanded
-            )
-
-            FloatingActionContent(
-                expanded = uiState.newPostExpanded,
-                onRequestClick = {
-                    mainNavigationViewModel.onNewRequestClick()
-                },
-                onPostClick = {
-                    mainNavigationViewModel.onNewPostClick()
-                },
-                onExpandClick = {
-                    mainNavigationViewModel.onNewPostExpandClick()
-                },
-                visible = listOf(
-                    ResellMainScreen.Home,
-                    ResellMainScreen.User,
-                ).contains(navDestination?.destination?.route?.toResellMainScreen())
-            )
+            composable<ResellMainScreen.User> {
+                ProfileScreen()
+            }
         }
+
+        NavBar(
+            onHomeClick = {
+                mainNav.navigate(ResellMainScreen.Home)
+            },
+            onBookmarksClick = {
+                mainNav.navigate(ResellMainScreen.Bookmarks)
+            },
+            onMessagesClick = {
+                mainNav.navigate(ResellMainScreen.Messages)
+            },
+            onUserClick = {
+                mainNav.navigate(ResellMainScreen.User)
+            },
+            selectedTab = navDestination?.destination?.route?.toResellMainScreen()
+                ?: ResellMainScreen.Home,
+            modifier = Modifier.align(Alignment.BottomCenter),
+            enabled = uiState.bottomBarEnabled
+        )
+
+        ShadeOverlay(
+            onTapped = {
+                mainNavigationViewModel.onShadeTapped()
+            },
+            visible = uiState.newPostExpanded
+        )
+
+        FloatingActionContent(
+            expanded = uiState.newPostExpanded,
+            onRequestClick = {
+                mainNavigationViewModel.onNewRequestClick()
+            },
+            onPostClick = {
+                mainNavigationViewModel.onNewPostClick()
+            },
+            onExpandClick = {
+                mainNavigationViewModel.onNewPostExpandClick()
+            },
+            visible = listOf(
+                ResellMainScreen.Home,
+                ResellMainScreen.User,
+            ).contains(navDestination?.destination?.route?.toResellMainScreen())
+        )
     }
 }
 
diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/newpost/NewPostNavigation.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/newpost/NewPostNavigation.kt
index f84f8951..6525f1bd 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/newpost/NewPostNavigation.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/newpost/NewPostNavigation.kt
@@ -2,14 +2,12 @@ package com.cornellappdev.resell.android.ui.screens.newpost
 
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.Modifier
 import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.rememberNavController
-import com.cornellappdev.resell.android.util.LocalNewPostNavigator
 import com.cornellappdev.resell.android.viewmodel.navigation.NewPostNavigationViewModel
 import kotlinx.serialization.Serializable
 
@@ -26,19 +24,17 @@ fun NewPostNavigation(
         }
     }
 
-    CompositionLocalProvider(LocalNewPostNavigator provides nav) {
-        NavHost(
-            navController = LocalNewPostNavigator.current,
-            startDestination = ResellNewPostScreen.ImageUpload,
-            modifier = Modifier.fillMaxSize()
-        ) {
-            composable<ResellNewPostScreen.ImageUpload> {
-                ImageUploadScreen()
-            }
-
-            composable<ResellNewPostScreen.PostDetails> {
-                PostDetailsEntryScreen()
-            }
+    NavHost(
+        navController = nav,
+        startDestination = ResellNewPostScreen.ImageUpload,
+        modifier = Modifier.fillMaxSize()
+    ) {
+        composable<ResellNewPostScreen.ImageUpload> {
+            ImageUploadScreen()
+        }
+
+        composable<ResellNewPostScreen.PostDetails> {
+            PostDetailsEntryScreen()
         }
     }
 }
diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/onboarding/OnboardingNavigation.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/onboarding/OnboardingNavigation.kt
index 4fed4292..53577e26 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/onboarding/OnboardingNavigation.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/onboarding/OnboardingNavigation.kt
@@ -2,14 +2,12 @@ package com.cornellappdev.resell.android.ui.screens.onboarding
 
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.Modifier
 import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.rememberNavController
-import com.cornellappdev.resell.android.util.LocalOnboardingNavigator
 import com.cornellappdev.resell.android.viewmodel.navigation.OnboardingNavigationViewModel
 import kotlinx.serialization.Serializable
 
@@ -26,19 +24,17 @@ fun OnboardingNavigation(
         }
     }
 
-    CompositionLocalProvider(LocalOnboardingNavigator provides onboardingNav) {
-        NavHost(
-            navController = LocalOnboardingNavigator.current,
-            startDestination = ResellOnboardingScreen.Setup,
-            modifier = Modifier.fillMaxSize()
-        ) {
-            composable<ResellOnboardingScreen.Setup> {
-                SetupScreen()
-            }
+    NavHost(
+        navController = onboardingNav,
+        startDestination = ResellOnboardingScreen.Setup,
+        modifier = Modifier.fillMaxSize()
+    ) {
+        composable<ResellOnboardingScreen.Setup> {
+            SetupScreen()
+        }
 
-            composable<ResellOnboardingScreen.Venmo> {
-                VenmoFieldScreen()
-            }
+        composable<ResellOnboardingScreen.Venmo> {
+            VenmoFieldScreen()
         }
     }
 }
diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/reporting/ReportNavigation.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/reporting/ReportNavigation.kt
index 036ac2b8..bb4c3886 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/reporting/ReportNavigation.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/reporting/ReportNavigation.kt
@@ -2,14 +2,12 @@ package com.cornellappdev.resell.android.ui.screens.reporting
 
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.Modifier
 import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.rememberNavController
-import com.cornellappdev.resell.android.util.LocalReportNavigator
 import com.cornellappdev.resell.android.viewmodel.navigation.ReportNavigationViewModel
 import kotlinx.serialization.Serializable
 
@@ -18,35 +16,33 @@ fun ReportNavigation(
     reportNavigationViewModel: ReportNavigationViewModel = hiltViewModel()
 ) {
     val uiState = reportNavigationViewModel.collectUiStateValue()
-    val onboardingNav = rememberNavController()
+    val navController = rememberNavController()
 
     LaunchedEffect(uiState.route) {
         uiState.route?.consumeSuspend {
-            onboardingNav.navigate(it)
+            navController.navigate(it)
         }
     }
 
-    CompositionLocalProvider(LocalReportNavigator provides onboardingNav) {
-        NavHost(
-            navController = LocalReportNavigator.current,
-            startDestination = uiState.initialPage ?: ReportScreen.Reason(
-                reportPost = true,
-                postId = "",
-                userId = ""
-            ),
-            modifier = Modifier.fillMaxSize()
-        ) {
-            composable<ReportScreen.Reason> {
-                ReportReasonScreen()
-            }
+    NavHost(
+        navController = navController,
+        startDestination = uiState.initialPage ?: ReportScreen.Reason(
+            reportPost = true,
+            postId = "",
+            userId = ""
+        ),
+        modifier = Modifier.fillMaxSize()
+    ) {
+        composable<ReportScreen.Reason> {
+            ReportReasonScreen()
+        }
 
-            composable<ReportScreen.Details> {
-                ReportDetailsScreen()
-            }
+        composable<ReportScreen.Details> {
+            ReportDetailsScreen()
+        }
 
-            composable<ReportScreen.Confirmation> {
-                ReportConfirmationScreen()
-            }
+        composable<ReportScreen.Confirmation> {
+            ReportConfirmationScreen()
         }
     }
 }
diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/root/RootNavigation.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/root/RootNavigation.kt
index ecf20aa5..03b933d5 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/root/RootNavigation.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/root/RootNavigation.kt
@@ -2,16 +2,12 @@ package com.cornellappdev.resell.android.ui.screens.root
 
 import android.Manifest
 import android.os.Build
-import androidx.compose.animation.core.InfiniteRepeatableSpec
-import androidx.compose.animation.core.animateFloat
-import androidx.compose.animation.core.keyframes
-import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.activity.compose.LocalActivity
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -20,7 +16,6 @@ import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
 import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
@@ -38,8 +33,6 @@ import com.cornellappdev.resell.android.ui.screens.onboarding.OnboardingNavigati
 import com.cornellappdev.resell.android.ui.screens.pdp.PostDetailPage
 import com.cornellappdev.resell.android.ui.screens.reporting.ReportNavigation
 import com.cornellappdev.resell.android.ui.screens.settings.SettingsNavigation
-import com.cornellappdev.resell.android.util.LocalInfiniteLoading
-import com.cornellappdev.resell.android.util.LocalRootNavigator
 import com.cornellappdev.resell.android.viewmodel.root.RootNavigationViewModel
 import com.cornellappdev.resell.android.viewmodel.root.RootSheet
 import kotlinx.coroutines.launch
@@ -61,25 +54,7 @@ fun RootNavigation(
     val coroutineScope = rememberCoroutineScope()
     var showBottomSheet by remember { mutableStateOf(false) }
 
-    // Create an infinite transition for animation
-    val transition = rememberInfiniteTransition()
-
-    val context = LocalContext.current as? MainActivity
-
-    // Animate a value from 0 to 1 infinitely
-    val animatedValue = transition.animateFloat(
-        initialValue = 0f,
-        targetValue = 1f,
-        animationSpec = InfiniteRepeatableSpec(
-            animation = keyframes {
-                durationMillis = 2000
-                0f at 0
-                1f at 1000
-                0f at 2000
-            }
-        ),
-        label = "infinite loading"
-    ).value
+    val context = LocalActivity.current as? MainActivity
 
     LaunchedEffect(uiState.sheetEvent) {
         uiState.sheetEvent?.consumeSuspend { sheet ->
@@ -134,85 +109,79 @@ fun RootNavigation(
         }
     }
 
-
-    CompositionLocalProvider(
-        LocalRootNavigator provides navController,
-        LocalInfiniteLoading provides animatedValue
+    NavHost(
+        navController = navController,
+        startDestination = ResellRootRoute.LANDING,
+        modifier = Modifier
+            .fillMaxSize()
+            .background(Color.White)
     ) {
-        NavHost(
-            navController = LocalRootNavigator.current,
-            startDestination = ResellRootRoute.LANDING,
-            modifier = Modifier
-                .fillMaxSize()
-                .background(Color.White)
-        ) {
-            composable<ResellRootRoute.LANDING> {
-                LandingScreen()
-            }
+        composable<ResellRootRoute.LANDING> {
+            LandingScreen()
+        }
 
-            composable<ResellRootRoute.MAIN> {
-                MainTabNavigation()
-            }
+        composable<ResellRootRoute.MAIN> {
+            MainTabNavigation()
+        }
 
-            composable<ResellRootRoute.ONBOARDING> {
-                OnboardingNavigation()
-            }
+        composable<ResellRootRoute.ONBOARDING> {
+            OnboardingNavigation()
+        }
 
-            composable<ResellRootRoute.SETTINGS> {
-                SettingsNavigation()
-            }
+        composable<ResellRootRoute.SETTINGS> {
+            SettingsNavigation()
+        }
 
-            composable<ResellRootRoute.NEW_POST> {
-                NewPostNavigation()
-            }
+        composable<ResellRootRoute.NEW_POST> {
+            NewPostNavigation()
+        }
 
-            composable<ResellRootRoute.NEW_REQUEST> {
-                RequestDetailsEntryScreen()
-            }
+        composable<ResellRootRoute.NEW_REQUEST> {
+            RequestDetailsEntryScreen()
+        }
 
-            composable<ResellRootRoute.PDP> {
-                PostDetailPage()
-            }
+        composable<ResellRootRoute.PDP> {
+            PostDetailPage()
+        }
 
-            composable<ResellRootRoute.CHAT> {
-                ChatScreen()
-            }
+        composable<ResellRootRoute.CHAT> {
+            ChatScreen()
+        }
 
-            composable<ResellRootRoute.REPORT> {
-                ReportNavigation()
-            }
+        composable<ResellRootRoute.REPORT> {
+            ReportNavigation()
+        }
 
-            composable<ResellRootRoute.EXTERNAL_PROFILE> {
-                ExternalProfileNavigation()
-            }
+        composable<ResellRootRoute.EXTERNAL_PROFILE> {
+            ExternalProfileNavigation()
+        }
 
-            composable<ResellRootRoute.SEARCH> {
-                AllSearchScreen()
-            }
+        composable<ResellRootRoute.SEARCH> {
+            AllSearchScreen()
+        }
 
-            composable<ResellRootRoute.REQUEST_MATCHES> {
-                RequestMatchesScreen()
-            }
+        composable<ResellRootRoute.REQUEST_MATCHES> {
+            RequestMatchesScreen()
         }
+    }
 
-        RootConfirmationOverlay()
+    RootConfirmationOverlay()
 
-        RootSheetOverlay(
-            sheetState = sheetState,
-            onDismissRequest = {
-                coroutineScope.launch {
-                    sheetState.hide()
-                    showBottomSheet = false
-                }
-            },
-            sheetType = lastSheetValue,
-            showBottomSheet = showBottomSheet,
-        )
+    RootSheetOverlay(
+        sheetState = sheetState,
+        onDismissRequest = {
+            coroutineScope.launch {
+                sheetState.hide()
+                showBottomSheet = false
+            }
+        },
+        sheetType = lastSheetValue,
+        showBottomSheet = showBottomSheet,
+    )
 
-        RootOptionsMenuOverlay()
+    RootOptionsMenuOverlay()
 
-        RootDialogOverlay()
-    }
+    RootDialogOverlay()
 }
 
 
diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/settings/SettingsNavigation.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/settings/SettingsNavigation.kt
index dff97213..e379700f 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/settings/SettingsNavigation.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/settings/SettingsNavigation.kt
@@ -1,13 +1,11 @@
 package com.cornellappdev.resell.android.ui.screens.settings
 
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.rememberNavController
-import com.cornellappdev.resell.android.util.LocalSettingsNavigator
 import com.cornellappdev.resell.android.viewmodel.navigation.SettingsNavigationViewModel
 import kotlinx.serialization.Serializable
 
@@ -30,26 +28,24 @@ fun SettingsNavigation(
         }
     }
 
-    CompositionLocalProvider(LocalSettingsNavigator provides navController) {
-        NavHost(
-            navController = LocalSettingsNavigator.current,
-            startDestination = SettingsRoute.SettingsLanding
-        ) {
-            composable<SettingsRoute.SettingsLanding> {
-                SettingsLandingScreen()
-            }
-            composable<SettingsRoute.EditProfile> {
-                EditProfileScreen()
-            }
-            composable<SettingsRoute.Notifications> {
-                NotificationSettings()
-            }
-            composable<SettingsRoute.Feedback> {
-                SendFeedbackScreen()
-            }
-            composable<SettingsRoute.BlockedUsers> {
-                BlockedUsersScreen()
-            }
+    NavHost(
+        navController = navController,
+        startDestination = SettingsRoute.SettingsLanding
+    ) {
+        composable<SettingsRoute.SettingsLanding> {
+            SettingsLandingScreen()
+        }
+        composable<SettingsRoute.EditProfile> {
+            EditProfileScreen()
+        }
+        composable<SettingsRoute.Notifications> {
+            NotificationSettings()
+        }
+        composable<SettingsRoute.Feedback> {
+            SendFeedbackScreen()
+        }
+        composable<SettingsRoute.BlockedUsers> {
+            BlockedUsersScreen()
         }
     }
 }
diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/theme/ResellPreview.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/theme/ResellPreview.kt
index b93bd45c..738a28f3 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/ui/theme/ResellPreview.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/ui/theme/ResellPreview.kt
@@ -15,11 +15,13 @@ fun ResellPreview(
     backgroundColor: Color = Color.White,
     content: @Composable () -> Unit
 ) {
-    Column(
-        modifier = Modifier
-            .background(backgroundColor)
-            .padding(padding)
-    ) {
-        content()
+    ResellTheme {
+        Column(
+            modifier = Modifier
+                .background(backgroundColor)
+                .padding(padding)
+        ) {
+            content()
+        }
     }
 }
diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/theme/Theme.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/theme/Theme.kt
index 3a1febd1..71bc9f81 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/ui/theme/Theme.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/ui/theme/Theme.kt
@@ -1,13 +1,19 @@
 package com.cornellappdev.resell.android.ui.theme
 
 import android.os.Build
+import androidx.compose.animation.core.InfiniteRepeatableSpec
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.rememberInfiniteTransition
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.darkColorScheme
 import androidx.compose.material3.dynamicDarkColorScheme
 import androidx.compose.material3.dynamicLightColorScheme
 import androidx.compose.material3.lightColorScheme
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.platform.LocalContext
+import com.cornellappdev.resell.android.util.LocalInfiniteShimmer
 
 private val DarkColorScheme = darkColorScheme(
     primary = Purple80,
@@ -38,9 +44,35 @@ fun ResellTheme(
         else -> LightColorScheme
     }
 
-    MaterialTheme(
-        colorScheme = colorScheme,
-        typography = Typography,
-        content = content
-    )
+    // Create an infinite transition for animation
+    val transition = rememberInfiniteTransition()
+
+    // Animate a value from 0 to 1 infinitely
+    val animatedColor = transition.animateFloat(
+        initialValue = 0f,
+        targetValue = 1f,
+        animationSpec = InfiniteRepeatableSpec(
+            animation = keyframes {
+                durationMillis = 2000
+                0f at 0
+                1f at 1000
+                0f at 2000
+            }
+        ),
+        label = "infinite loading"
+    ).value.let {
+        interpolateColorHSV(
+            startColor = Wash,
+            endColor = Stroke,
+            fraction = it
+        )
+    }
+
+    CompositionLocalProvider(LocalInfiniteShimmer provides animatedColor) {
+        MaterialTheme(
+            colorScheme = colorScheme,
+            typography = Typography,
+            content = content
+        )
+    }
 }
diff --git a/app/src/main/java/com/cornellappdev/resell/android/util/CompositionLocal.kt b/app/src/main/java/com/cornellappdev/resell/android/util/CompositionLocal.kt
new file mode 100644
index 00000000..89041cda
--- /dev/null
+++ b/app/src/main/java/com/cornellappdev/resell/android/util/CompositionLocal.kt
@@ -0,0 +1,6 @@
+package com.cornellappdev.resell.android.util
+
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.graphics.Color
+
+val LocalInfiniteShimmer = compositionLocalOf<Color> { error("No infinite loading provided") }
diff --git a/app/src/main/java/com/cornellappdev/resell/android/util/CompositonLocal.kt b/app/src/main/java/com/cornellappdev/resell/android/util/CompositonLocal.kt
deleted file mode 100644
index 2f754bd4..00000000
--- a/app/src/main/java/com/cornellappdev/resell/android/util/CompositonLocal.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.cornellappdev.resell.android.util
-
-import android.content.Context
-import android.content.Intent
-import androidx.compose.runtime.compositionLocalOf
-import androidx.navigation.NavHostController
-import com.google.firebase.firestore.FirebaseFirestore
-
-val LocalRootNavigator = compositionLocalOf<NavHostController> { error("No navigator provided") }
-val LocalMainNavigator = compositionLocalOf<NavHostController> { error("No navigator provided") }
-val LocalNewPostNavigator = compositionLocalOf<NavHostController> { error("No navigator provided") }
-val LocalOnboardingNavigator =
-    compositionLocalOf<NavHostController> { error("No navigator provided") }
-val LocalSettingsNavigator =
-    compositionLocalOf<NavHostController> { error("No navigator provided") }
-val LocalReportNavigator = compositionLocalOf<NavHostController> { error("No navigator provided") }
-val LocalFireStore = compositionLocalOf<FirebaseFirestore> { error("No FireStore provided") }
-val LocalInfiniteLoading = compositionLocalOf<Float> { error("No infinite loading provided") }
-
-fun closeApp(context: Context) {
-    val intent = Intent(Intent.ACTION_MAIN)
-    intent.addCategory(Intent.CATEGORY_HOME)
-    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
-    context.startActivity(intent)
-}
diff --git a/app/src/main/java/com/cornellappdev/resell/android/util/Functions.kt b/app/src/main/java/com/cornellappdev/resell/android/util/Functions.kt
index 620506f0..8d17c9ad 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/util/Functions.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/util/Functions.kt
@@ -1,5 +1,7 @@
 package com.cornellappdev.resell.android.util
 
+import android.content.Context
+import android.content.Intent
 import android.graphics.Bitmap
 import android.text.format.DateUtils
 import android.util.Base64
@@ -110,3 +112,10 @@ fun getRelativeTimeSpan(isoString: String): String {
         DateUtils.MINUTE_IN_MILLIS
     ).toString()
 }
+
+fun closeApp(context: Context) {
+    val intent = Intent(Intent.ACTION_MAIN)
+    intent.addCategory(Intent.CATEGORY_HOME)
+    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+    context.startActivity(intent)
+}
diff --git a/app/src/main/java/com/cornellappdev/resell/android/util/Modifiers.kt b/app/src/main/java/com/cornellappdev/resell/android/util/Modifiers.kt
index 5ca760ba..af76694f 100644
--- a/app/src/main/java/com/cornellappdev/resell/android/util/Modifiers.kt
+++ b/app/src/main/java/com/cornellappdev/resell/android/util/Modifiers.kt
@@ -1,5 +1,6 @@
 package com.cornellappdev.resell.android.util
 
+import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.padding
@@ -20,3 +21,9 @@ fun Modifier.clickableNoIndication(onClick: () -> Unit) = this.clickable(
     interactionSource = remember { MutableInteractionSource() },
     onClick = onClick,
 )
+
+@Composable
+fun Modifier.shimmer(): Modifier {
+    val infiniteShimmer = LocalInfiniteShimmer.current
+    return this.background(infiniteShimmer)
+}


Reviews:
user: login: zachseidner1
body: I don't agree with the way you're using `CompositionLocal`, I think it would be good to refactor to not use this to provide an infinite transition value. In fact, I really don't like any of the uses of `CompositionLocal` in this project. I feel like it's rare that using this feature is ever a good decision. But if you can justify why you think it is good in this case, please do so as I am curious to hear why.

Please see [Deciding whether to use CompositionLocal](https://developer.android.com/develop/ui/compose/compositionlocal#deciding)
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-02T22:54:22Z
user: login: zachseidner1
body: Awesome work, thanks again for the discussion about CompositionLocal! Resell codebase takes a W with this one.
Left a couple nits but as always they're up to you.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-03T15:39:37Z

Review comments:
diff_hunk: @@ -55,9 +56,28 @@ fun ResellCard(
     viewModel: ResellCardViewModel = hiltViewModel(),
path: app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellCard.kt
user: login: zachseidner1
body: I think `ResellCard` should take `MutableState<ResellApiResponse<ImageBitmap>>` as a parameter instead of an `imageUrl` to make the component dumber, but I guess that's outside the scope of this PR?
created_at: 2025-03-02T22:43:37Z
author_association: COLLABORATOR
diff_hunk: @@ -1,25 +1,55 @@
 package com.cornellappdev.resell.android.ui.theme
 
+import android.content.res.Configuration
+import androidx.compose.animation.core.InfiniteRepeatableSpec
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.rememberInfiniteTransition
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import com.cornellappdev.resell.android.util.LocalInfiniteLoading
 
 @Composable
 fun ResellPreview(
     padding: Dp = 0.dp,
     backgroundColor: Color = Color.White,
     content: @Composable () -> Unit
 ) {
-    Column(
-        modifier = Modifier
-            .background(backgroundColor)
-            .padding(padding)
+    // Create an infinite transition for animation
+    val transition = rememberInfiniteTransition()
+
+    // Animate a value from 0 to 1 infinitely
+    val animatedValue = transition.animateFloat(
+        initialValue = 0f,
+        targetValue = 1f,
+        animationSpec = InfiniteRepeatableSpec(
+            animation = keyframes {
+                durationMillis = 2000
+                0f at 0
+                1f at 1000
+                0f at 2000
+            }
+        ),
+        label = "infinite loading"
+    ).value
+
+    CompositionLocalProvider(
+        LocalInfiniteLoading provides animatedValue
path: app/src/main/java/com/cornellappdev/resell/android/ui/theme/ResellPreview.kt
user: login: zachseidner1
body: Why are we using `CompositionLocal` for providing infinite transitions in the first place? Why not use a Composable function instead? Have you seen [Deciding whether to use CompositionLocal](https://developer.android.com/develop/ui/compose/compositionlocal#deciding)? I don't think this makes sense since there's no good default value to put in this case. Can you justify why it's needed here?
created_at: 2025-03-02T22:49:14Z
author_association: COLLABORATOR
diff_hunk: @@ -38,9 +44,35 @@ fun ResellTheme(
         else -> LightColorScheme
     }
 
-    MaterialTheme(
-        colorScheme = colorScheme,
-        typography = Typography,
-        content = content
-    )
+    // Create an infinite transition for animation
+    val transition = rememberInfiniteTransition()
+
+    // Animate a value from 0 to 1 infinitely
+    val animatedColor = transition.animateFloat(
+        initialValue = 0f,
+        targetValue = 1f,
+        animationSpec = InfiniteRepeatableSpec(
+            animation = keyframes {
+                durationMillis = 2000
+                0f at 0
+                1f at 1000
+                0f at 2000
+            }
+        ),
+        label = "infinite loading"
+    ).value.let {
+        interpolateColorHSV(
+            startColor = Wash,
+            endColor = Stroke,
+            fraction = it
+        )
+    }
+
+    CompositionLocalProvider(LocalInfiniteShimmer provides animatedColor) {
path: app/src/main/java/com/cornellappdev/resell/android/ui/theme/Theme.kt
user: login: zachseidner1
body: Putting this as a part of the theme makes a ton of sense, love it.
created_at: 2025-03-03T14:53:18Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+package com.cornellappdev.resell.android.util
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.background
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+
+val LocalInfiniteShimmer = compositionLocalOf<Color> { error("No infinite loading provided") }
+
+@SuppressLint("ComposableNaming")
path: app/src/main/java/com/cornellappdev/resell/android/util/CompositionLocal.kt
user: login: zachseidner1
body: I don't think this violates the Composable naming lint? I removed the lint and didn't get a warning. 
created_at: 2025-03-03T15:36:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+package com.cornellappdev.resell.android.util
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.background
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+
+val LocalInfiniteShimmer = compositionLocalOf<Color> { error("No infinite loading provided") }
+
+@SuppressLint("ComposableNaming")
+@Composable
+fun Modifier.shimmer(): Modifier {
path: app/src/main/java/com/cornellappdev/resell/android/util/CompositionLocal.kt
user: login: zachseidner1
body: Nit: we probably don't want to put this modifier in the `CompositionLocal` file?
created_at: 2025-03-03T15:38:48Z
author_association: COLLABORATOR

PR comments:
user: login: thisjustin123
created_at: 2025-03-03T01:08:14Z
body: Can't test change because of this weird lint thing: https://cornellappdev.slack.com/archives/C07Q4MJCW95/p1740963607947819

Will reopen when I can
author_association: CONTRIBUTOR
user: login: thisjustin123
created_at: 2025-03-03T02:48:47Z
body: Tweaked into `LocalInfiniteShimmer` and put into `ResellTheme`.
author_association: CONTRIBUTOR