From 72fb14038e072d68f654dab6d9e1983de079b85f Mon Sep 17 00:00:00 2001 From: SekoiaTree Date: Sun, 26 May 2024 14:51:26 +0200 Subject: [PATCH 1/3] feat: association logos in creation --- .../CreateAssociationScreen.kt | 55 ++++++++++++++++--- .../CreateAssociationViewmodel.kt | 37 ++++++++++++- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/github/se/assocify/ui/screens/createAssociation/CreateAssociationScreen.kt b/app/src/main/java/com/github/se/assocify/ui/screens/createAssociation/CreateAssociationScreen.kt index d09d7c725..4544f49ba 100644 --- a/app/src/main/java/com/github/se/assocify/ui/screens/createAssociation/CreateAssociationScreen.kt +++ b/app/src/main/java/com/github/se/assocify/ui/screens/createAssociation/CreateAssociationScreen.kt @@ -1,15 +1,19 @@ package com.github.se.assocify.ui.screens.createAssociation import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -31,6 +35,8 @@ import androidx.compose.material3.OutlinedIconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -38,14 +44,17 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment 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.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import coil.compose.AsyncImage import com.github.se.assocify.R import com.github.se.assocify.model.entities.RoleType import com.github.se.assocify.navigation.NavigationActions +import com.github.se.assocify.ui.composables.PhotoSelectionSheet import com.github.se.assocify.ui.composables.UserSearchState import com.github.se.assocify.ui.composables.UserSearchTextField @@ -71,6 +80,13 @@ fun CreateAssociationScreen( }, title = { Text(text = "Create your association") }) }, + snackbarHost = { + SnackbarHost( + hostState = state.snackbarHostState, + snackbar = { snackbarData -> + Snackbar(snackbarData = snackbarData, modifier = Modifier.testTag("snackbar")) + }) + }, contentWindowInsets = WindowInsets(20.dp, 10.dp, 20.dp, 20.dp)) { innerPadding -> Column( modifier = Modifier.fillMaxSize().padding(innerPadding), @@ -81,15 +97,29 @@ fun CreateAssociationScreen( horizontalArrangement = Arrangement.spacedBy(15.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - OutlinedIconButton( - modifier = Modifier.testTag("logo"), - onClick = { - /* TODO : can add association logo // note : nowhere to put it yet because picture not handled in DB */ - }) { - Icon( - painter = painterResource(id = R.drawable.landscape), - contentDescription = "Logo") - } + + // profile picture + if (state.imageUri != null) { + AsyncImage( + modifier = + Modifier.size(48.dp) + .clip(CircleShape) // Clip the image to a circle shape + .aspectRatio(1f) + .clickable { viewmodel.controlBottomSheet(true) } + .testTag("logo"), + model = state.imageUri, + contentDescription = "profile picture", + contentScale = ContentScale.Crop) + } else { + OutlinedIconButton( + modifier = Modifier.testTag("logo"), + onClick = { viewmodel.controlBottomSheet(true) }) { + Icon( + painter = painterResource(id = R.drawable.landscape), + contentDescription = "Logo") + } + } + OutlinedTextField( value = state.name, singleLine = true, @@ -207,5 +237,12 @@ fun CreateAssociationScreen( } } } + + // open bottom sheet to select a (profile) picture + PhotoSelectionSheet( + visible = state.showBottomSheet, + hideSheet = { viewmodel.controlBottomSheet(false) }, + setImageUri = { viewmodel.setLogo(it) }, + signalCameraPermissionDenied = { viewmodel.signalCameraPermissionDenied() }) } } diff --git a/app/src/main/java/com/github/se/assocify/ui/screens/createAssociation/CreateAssociationViewmodel.kt b/app/src/main/java/com/github/se/assocify/ui/screens/createAssociation/CreateAssociationViewmodel.kt index 879bab797..f38f2be7b 100644 --- a/app/src/main/java/com/github/se/assocify/ui/screens/createAssociation/CreateAssociationViewmodel.kt +++ b/app/src/main/java/com/github/se/assocify/ui/screens/createAssociation/CreateAssociationViewmodel.kt @@ -1,6 +1,9 @@ package com.github.se.assocify.ui.screens.createAssociation +import android.net.Uri import android.util.Log +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState import androidx.lifecycle.ViewModel import com.github.se.assocify.model.CurrentUser import com.github.se.assocify.model.database.AssociationAPI @@ -13,8 +16,11 @@ import com.github.se.assocify.model.entities.User import com.github.se.assocify.navigation.NavigationActions import java.time.LocalDate import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch class CreateAssociationViewmodel( private val assoAPI: AssociationAPI, @@ -186,11 +192,37 @@ class CreateAssociationViewmodel( userAPI.updateCurrentUserAssociationCache({ navActions.goFromCreateAsso() }, {}) }, {}) + if (_uiState.value.imageUri != null) { + assoAPI.setLogo( + association.uid, + _uiState.value.imageUri!!, + {}, + { exception -> + // We might already have navigated away, there's no way to show a snackbar... + Log.e("CreateAssoViewModel", "Failed to set logo: ${exception.message}") + }) + } }, onFailure = { exception -> Log.e("CreateAssoViewModel", "Failed to add asso: ${exception.message}") }) } + + fun controlBottomSheet(show: Boolean) { + _uiState.value = _uiState.value.copy(showBottomSheet = show) + } + + fun setLogo(image: Uri?) { + if (image == null) return + _uiState.value = _uiState.value.copy(imageUri = image) + } + + fun signalCameraPermissionDenied() { + CoroutineScope(Dispatchers.Main).launch { + _uiState.value.snackbarHostState.showSnackbar( + message = "Camera permission denied", duration = SnackbarDuration.Short) + } + } } data class CreateAssoUIState( @@ -204,6 +236,9 @@ data class CreateAssoUIState( val savable: Boolean = (members.any { member -> member.user.uid == CurrentUser.userUid }) && name.isNotBlank(), // whether the association can be saved + // the snackbar host state + val snackbarHostState: SnackbarHostState = SnackbarHostState(), val nameError: String? = null, // error message when the name is invalid - // there should be a logo val but not implemented yet + val showBottomSheet: Boolean = false, // whether the bottom sheet is shown + val imageUri: Uri? = null // the image URI of the logo ) From 5f78ce7a64fe8996196a7951a275efaf0729e1ec Mon Sep 17 00:00:00 2001 From: SekoiaTree Date: Thu, 30 May 2024 18:20:08 +0200 Subject: [PATCH 2/3] test: fix tests --- .../assocify/screens/CreateAssoScreenTest.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/src/androidTest/java/com/github/se/assocify/screens/CreateAssoScreenTest.kt b/app/src/androidTest/java/com/github/se/assocify/screens/CreateAssoScreenTest.kt index 5bc220c26..9c0c4ffb4 100644 --- a/app/src/androidTest/java/com/github/se/assocify/screens/CreateAssoScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/assocify/screens/CreateAssoScreenTest.kt @@ -66,6 +66,12 @@ class CreateAssoScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withC val name = firstArg() name != "nonValidName" } + + every { setLogo(any(), any(), any(), any()) } answers + { + val onSuccessCallback = thirdArg<() -> Unit>() + onSuccessCallback() + } } private val mockUserAPI = mockk { @@ -157,6 +163,7 @@ class CreateAssoScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withC @Test fun testCantCreateAsso() { + bigView.setLogo(mockk()) with(composeTestRule) { onNodeWithTag("create").assertIsNotEnabled() onNodeWithTag("name").performTextInput("nonValidName") @@ -192,4 +199,20 @@ class CreateAssoScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withC verify { mockNavActions.back() } } } + + @Test + fun openProfileSheet() { + with(composeTestRule) { + onNodeWithTag("default profile icon").performClick() + onNodeWithTag("photoSelectionSheet").assertIsDisplayed() + bigView.signalCameraPermissionDenied() + onNodeWithTag("snackbar").assertIsDisplayed() + } + } + + @Test + fun setUri() { + bigView.setLogo(mockk()) + with(composeTestRule) { onNodeWithTag("profilePicture").assertIsDisplayed() } + } } From 9a9cdbd29249b075e913f4cf9c1a2268e8a0f339 Mon Sep 17 00:00:00 2001 From: SekoiaTree Date: Thu, 30 May 2024 18:47:57 +0200 Subject: [PATCH 3/3] test: fix tests --- .../com/github/se/assocify/screens/CreateAssoScreenTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/github/se/assocify/screens/CreateAssoScreenTest.kt b/app/src/androidTest/java/com/github/se/assocify/screens/CreateAssoScreenTest.kt index 9c0c4ffb4..324b4b670 100644 --- a/app/src/androidTest/java/com/github/se/assocify/screens/CreateAssoScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/assocify/screens/CreateAssoScreenTest.kt @@ -203,7 +203,7 @@ class CreateAssoScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withC @Test fun openProfileSheet() { with(composeTestRule) { - onNodeWithTag("default profile icon").performClick() + onNodeWithTag("logo").performClick() onNodeWithTag("photoSelectionSheet").assertIsDisplayed() bigView.signalCameraPermissionDenied() onNodeWithTag("snackbar").assertIsDisplayed() @@ -213,6 +213,6 @@ class CreateAssoScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withC @Test fun setUri() { bigView.setLogo(mockk()) - with(composeTestRule) { onNodeWithTag("profilePicture").assertIsDisplayed() } + with(composeTestRule) { onNodeWithTag("logo").assertIsDisplayed() } } }