diff --git a/app/src/androidTest/java/com/github/se/assocify/screens/TreasuryScreenTest.kt b/app/src/androidTest/java/com/github/se/assocify/screens/TreasuryScreenTest.kt index a3884e7ee..464d65ac5 100644 --- a/app/src/androidTest/java/com/github/se/assocify/screens/TreasuryScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/assocify/screens/TreasuryScreenTest.kt @@ -1,5 +1,6 @@ package com.github.se.assocify.screens +import android.net.Uri import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertIsSelected @@ -14,6 +15,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.se.assocify.model.CurrentUser import com.github.se.assocify.model.database.AccountingCategoryAPI import com.github.se.assocify.model.database.AccountingSubCategoryAPI +import com.github.se.assocify.model.database.AssociationAPI import com.github.se.assocify.model.database.BalanceAPI import com.github.se.assocify.model.database.BudgetAPI import com.github.se.assocify.model.database.ReceiptAPI @@ -46,7 +48,7 @@ class TreasuryScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withCom @get:Rule val composeTestRule = createComposeRule() @get:Rule val mockkRule = MockKRule(this) private val navActions = - mockk() { + mockk { every { navigateToMainTab(any()) } answers { tabSelected = true } every { navigateTo(any()) } answers {} } @@ -139,6 +141,14 @@ class TreasuryScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withCom onSuccessCallback(PermissionRole("roleUid", "testAssociation", RoleType.TREASURY)) } } + private val mockAssocAPI = + mockk() { + every { getLogo(any(), any(), any()) } answers + { + val onSuccessCallback = secondArg<(Uri) -> Unit>() + onSuccessCallback(mockk()) + } + } @Before fun testSetup() { @@ -152,7 +162,8 @@ class TreasuryScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withCom mockAccountingSubCategoryAPI, mockBalanceAPI, mockBudgetAPI, - mockUserAPI) + mockUserAPI, + mockAssocAPI) composeTestRule.setContent { TreasuryScreen(navActions, treasuryViewModel) } } diff --git a/app/src/androidTest/java/com/github/se/assocify/screens/profile/ProfileScreenTest.kt b/app/src/androidTest/java/com/github/se/assocify/screens/profile/ProfileScreenTest.kt index 4c932584c..b64db1b2b 100644 --- a/app/src/androidTest/java/com/github/se/assocify/screens/profile/ProfileScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/assocify/screens/profile/ProfileScreenTest.kt @@ -51,7 +51,7 @@ class ProfileScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withComp private val asso1 = Association("asso", "test", "test", LocalDate.EPOCH) private val asso2 = Association("asso2", "test2", "test2", LocalDate.EPOCH) private val mockAssocAPI = - mockk() { + mockk { every { getAssociation("asso", any(), any()) } answers { val onSuccessCallback = secondArg<(Association) -> Unit>() @@ -62,6 +62,7 @@ class ProfileScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withComp val onSuccessCallback = secondArg<(Association) -> Unit>() onSuccessCallback(asso2) } + every { getLogo(any(), any(), any()) } answers {} } private val mockUserAPI = mockk() { diff --git a/app/src/main/java/com/github/se/assocify/model/CurrentUser.kt b/app/src/main/java/com/github/se/assocify/model/CurrentUser.kt index 9d202743b..10098bfe4 100644 --- a/app/src/main/java/com/github/se/assocify/model/CurrentUser.kt +++ b/app/src/main/java/com/github/se/assocify/model/CurrentUser.kt @@ -1,6 +1,16 @@ package com.github.se.assocify.model +import android.net.Uri +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + object CurrentUser { var userUid: String? = null var associationUid: String? = null + private var _associationLogo: StateFlow = MutableStateFlow(null) + val associationLogo: StateFlow = _associationLogo + + fun setAssociationLogo(uri: Uri?) { + (_associationLogo as MutableStateFlow).value = uri + } } diff --git a/app/src/main/java/com/github/se/assocify/model/database/AssociationAPI.kt b/app/src/main/java/com/github/se/assocify/model/database/AssociationAPI.kt index 6da7e2e8e..91bc936c4 100644 --- a/app/src/main/java/com/github/se/assocify/model/database/AssociationAPI.kt +++ b/app/src/main/java/com/github/se/assocify/model/database/AssociationAPI.kt @@ -1,6 +1,8 @@ package com.github.se.assocify.model.database import android.net.Uri +import android.util.Log +import com.github.se.assocify.model.CurrentUser import com.github.se.assocify.model.entities.Association import com.github.se.assocify.model.entities.AssociationMember import com.github.se.assocify.model.entities.PermissionRole @@ -25,9 +27,11 @@ import kotlinx.serialization.json.JsonObject class AssociationAPI(private val db: SupabaseClient, cachePath: Path) : SupabaseApi() { private var associationCache = mapOf() private val imageCacher = ImageCacher(60 * 60_000, cachePath, db.storage["association"]) + private var currentAssociationCache: String? = null init { updateCache({}, {}) // Try and fill the cache as quickly as possible + currentAssociationCache = CurrentUser.associationUid } /** @@ -41,6 +45,7 @@ class AssociationAPI(private val db: SupabaseClient, cachePath: Path) : Supabase val assoc = db.from("association").select().decodeList() associationCache = assoc.associateBy { it.uid!! }.mapValues { it.value.toAssociation() } memberCache = null + currentAssociationCache = CurrentUser.associationUid onSuccess(associationCache) } } @@ -436,8 +441,12 @@ class AssociationAPI(private val db: SupabaseClient, cachePath: Path) : Supabase * @param onSuccess called on success with the URI of the logo * @param onFailure called on failure */ - fun getLogo(associationId: String, onSuccess: (Uri) -> Unit, onFailure: (Exception) -> Unit) { - imageCacher.fetchImage(associationId, { onSuccess(Uri.fromFile(it.toFile())) }, onFailure) + fun getLogo(associationId: String, onSuccess: (Uri?) -> Unit, onFailure: (Exception) -> Unit) { + Log.d("image", "getLogo from association $associationId") + if (associationId != currentAssociationCache) { + currentAssociationCache = associationId + imageCacher.fetchImage(associationId, { onSuccess(Uri.fromFile(it.toFile())) }, onFailure) + } } @Serializable diff --git a/app/src/main/java/com/github/se/assocify/model/database/ImageCacher.kt b/app/src/main/java/com/github/se/assocify/model/database/ImageCacher.kt index 44c8c9b9f..3fbb92227 100644 --- a/app/src/main/java/com/github/se/assocify/model/database/ImageCacher.kt +++ b/app/src/main/java/com/github/se/assocify/model/database/ImageCacher.kt @@ -88,6 +88,7 @@ class ImageCacher(val timeout: Long, val cacheDir: Path, private val bucket: Buc if (!renamed) { Log.w("IMG", "Failed to rename temporary image cache file ($pathInBucket)") } + onSuccess(imageFile) } } diff --git a/app/src/main/java/com/github/se/assocify/navigation/NavigationGraph.kt b/app/src/main/java/com/github/se/assocify/navigation/NavigationGraph.kt index a6d58c2d9..a20d5beea 100644 --- a/app/src/main/java/com/github/se/assocify/navigation/NavigationGraph.kt +++ b/app/src/main/java/com/github/se/assocify/navigation/NavigationGraph.kt @@ -40,7 +40,8 @@ fun NavGraphBuilder.mainNavGraph( receiptsAPI, accountingCategoriesAPI, accountingSubCategoryAPI, - userAPI) + userAPI, + associationAPI) eventGraph(navActions, eventAPI, taskAPI) profileGraph( navActions, diff --git a/app/src/main/java/com/github/se/assocify/ui/composables/MainTopBar.kt b/app/src/main/java/com/github/se/assocify/ui/composables/MainTopBar.kt index 91f6c384e..b085922fc 100644 --- a/app/src/main/java/com/github/se/assocify/ui/composables/MainTopBar.kt +++ b/app/src/main/java/com/github/se/assocify/ui/composables/MainTopBar.kt @@ -1,11 +1,17 @@ package com.github.se.assocify.ui.composables +import android.util.Log +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -13,14 +19,19 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.SearchBar import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.github.se.assocify.model.CurrentUser /** * Main tab top bar @@ -49,6 +60,9 @@ fun MainTopBar( // Page state var currentPage by remember { mutableIntStateOf(page) } + val associationLogoUri = CurrentUser.associationLogo.collectAsState() + val associationLogoUriValue = associationLogoUri.value + if (currentPage != page) { currentPage = page searchBarVisible = false @@ -60,9 +74,25 @@ fun MainTopBar( title = { Text(text = title) }, navigationIcon = { IconButton(modifier = Modifier.testTag("accountIconButton"), onClick = {}) { - Icon( - imageVector = Icons.Filled.AccountCircle, - contentDescription = "Association Account") + // profile picture + if (associationLogoUriValue != null) { + Log.d("image", "CurrentUser.associationLogo: ${associationLogoUriValue}") + AsyncImage( + modifier = + Modifier.size(80.dp) + .clip(CircleShape) // Clip the image to a circle shape + .aspectRatio(1f) + .testTag("profilePicture"), + model = associationLogoUriValue, + contentDescription = "profile picture", + contentScale = ContentScale.Crop) + } else { + Log.d("image", "CurrentUser.associationLogo: ${associationLogoUriValue}") + Icon( + modifier = Modifier.fillMaxSize(), + imageVector = Icons.Outlined.AccountCircle, + contentDescription = "default profile icon") + } } }, actions = { diff --git a/app/src/main/java/com/github/se/assocify/ui/screens/profile/ProfileViewModel.kt b/app/src/main/java/com/github/se/assocify/ui/screens/profile/ProfileViewModel.kt index 5ec456dfd..5172f15b7 100644 --- a/app/src/main/java/com/github/se/assocify/ui/screens/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/github/se/assocify/ui/screens/profile/ProfileViewModel.kt @@ -186,6 +186,10 @@ class ProfileViewModel( } val oldAssociationUid = CurrentUser.associationUid CurrentUser.associationUid = association.uid + assoAPI.getLogo( + CurrentUser.associationUid!!, + { uri -> CurrentUser.setAssociationLogo(uri) }, + { CurrentUser.setAssociationLogo(null) }) userAPI.getCurrentUserRole( { role -> _uiState.value = _uiState.value.copy(selectedAssociation = association) diff --git a/app/src/main/java/com/github/se/assocify/ui/screens/treasury/TreasuryGraph.kt b/app/src/main/java/com/github/se/assocify/ui/screens/treasury/TreasuryGraph.kt index ebcf5098b..8c2656a38 100644 --- a/app/src/main/java/com/github/se/assocify/ui/screens/treasury/TreasuryGraph.kt +++ b/app/src/main/java/com/github/se/assocify/ui/screens/treasury/TreasuryGraph.kt @@ -5,6 +5,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.github.se.assocify.model.database.AccountingCategoryAPI import com.github.se.assocify.model.database.AccountingSubCategoryAPI +import com.github.se.assocify.model.database.AssociationAPI import com.github.se.assocify.model.database.BalanceAPI import com.github.se.assocify.model.database.BudgetAPI import com.github.se.assocify.model.database.ReceiptAPI @@ -22,7 +23,8 @@ fun NavGraphBuilder.treasuryGraph( receiptsAPI: ReceiptAPI, accountingCategoryAPI: AccountingCategoryAPI, accountingSubCategoryAPI: AccountingSubCategoryAPI, - userAPI: UserAPI + userAPI: UserAPI, + associationAPI: AssociationAPI ) { composable( @@ -36,7 +38,8 @@ fun NavGraphBuilder.treasuryGraph( accountingSubCategoryAPI, balanceAPI, budgetAPI, - userAPI) + userAPI, + associationAPI) } TreasuryScreen(navigationActions, treasuryViewModel) } diff --git a/app/src/main/java/com/github/se/assocify/ui/screens/treasury/TreasuryViewModel.kt b/app/src/main/java/com/github/se/assocify/ui/screens/treasury/TreasuryViewModel.kt index 007271c16..b287c6fc3 100644 --- a/app/src/main/java/com/github/se/assocify/ui/screens/treasury/TreasuryViewModel.kt +++ b/app/src/main/java/com/github/se/assocify/ui/screens/treasury/TreasuryViewModel.kt @@ -1,8 +1,11 @@ package com.github.se.assocify.ui.screens.treasury +import android.util.Log import androidx.compose.material3.SnackbarHostState +import com.github.se.assocify.model.CurrentUser import com.github.se.assocify.model.database.AccountingCategoryAPI import com.github.se.assocify.model.database.AccountingSubCategoryAPI +import com.github.se.assocify.model.database.AssociationAPI import com.github.se.assocify.model.database.BalanceAPI import com.github.se.assocify.model.database.BudgetAPI import com.github.se.assocify.model.database.ReceiptAPI @@ -21,8 +24,19 @@ class TreasuryViewModel( accountingSubCategoryAPI: AccountingSubCategoryAPI, balanceAPI: BalanceAPI, budgetAPI: BudgetAPI, - userAPI: UserAPI + userAPI: UserAPI, + associationAPI: AssociationAPI ) { + init { + Log.d("image", "getting logo in View model at init") + associationAPI.getLogo( + CurrentUser.associationUid!!, + { uri -> + Log.d("image", "uri is $uri") + CurrentUser.setAssociationLogo(uri) + }, + { CurrentUser.setAssociationLogo(null) }) + } // ViewModel states private val _uiState: MutableStateFlow = MutableStateFlow(TreasuryUIState()) val uiState: StateFlow = _uiState @@ -72,7 +86,7 @@ class TreasuryViewModel( data class TreasuryUIState( val snackbarHostState: SnackbarHostState = SnackbarHostState(), val searchQuery: String = "", - val currentTab: TreasuryPageIndex = TreasuryPageIndex.Receipts + val currentTab: TreasuryPageIndex = TreasuryPageIndex.Receipts, ) /** Treasury tabs */