Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Profile pictures for associations #381

Merged
merged 4 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ class CreateAssoScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withC
val name = firstArg<String>()
name != "nonValidName"
}

every { setLogo(any(), any(), any(), any()) } answers
{
val onSuccessCallback = thirdArg<() -> Unit>()
onSuccessCallback()
}
}
private val mockUserAPI =
mockk<UserAPI> {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -192,4 +199,20 @@ class CreateAssoScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withC
verify { mockNavActions.back() }
}
}

@Test
fun openProfileSheet() {
with(composeTestRule) {
onNodeWithTag("logo").performClick()
onNodeWithTag("photoSelectionSheet").assertIsDisplayed()
bigView.signalCameraPermissionDenied()
onNodeWithTag("snackbar").assertIsDisplayed()
}
}

@Test
fun setUri() {
bigView.setLogo(mockk())
with(composeTestRule) { onNodeWithTag("logo").assertIsDisplayed() }
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -31,21 +35,26 @@ 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
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

Expand All @@ -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),
Expand All @@ -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,
Expand Down Expand Up @@ -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() })
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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
)
Loading