Merge pull request #64 from SELab1/profile_edit

Profile edit & delete
This commit is contained in:
lbarraga 2023-04-17 11:56:47 +02:00 committed by GitHub Enterprise
commit 42e72dbbb8
17 changed files with 229 additions and 38 deletions

View file

@ -20,6 +20,7 @@ import be.ugent.sel.studeez.common.snackbar.SnackbarManager
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.home.HomeScreen
import be.ugent.sel.studeez.screens.log_in.LoginScreen
import be.ugent.sel.studeez.screens.profile.EditProfileScreen
import be.ugent.sel.studeez.screens.profile.ProfileScreen
import be.ugent.sel.studeez.screens.sign_up.SignUpScreen
import be.ugent.sel.studeez.screens.splash.SplashScreen
@ -77,14 +78,18 @@ fun resources(): Resources {
fun NavGraphBuilder.studeezGraph(appState: StudeezAppstate) {
val openAndPopUp: (String, String) -> Unit = {
route, popUp -> appState.navigateAndPopUp(route, popUp)
val goBack: () -> Unit = {
appState.popUp()
}
val open: (String) -> Unit = {
route -> appState.navigate(route)
}
val openAndPopUp: (String, String) -> Unit = {
route, popUp -> appState.navigateAndPopUp(route, popUp)
}
composable(StudeezDestinations.SPLASH_SCREEN) {
SplashScreen(openAndPopUp)
}
@ -110,4 +115,9 @@ fun NavGraphBuilder.studeezGraph(appState: StudeezAppstate) {
// TODO Timers screen
// TODO Settings screen
// Edit screens
composable(StudeezDestinations.EDIT_PROFILE_SCREEN) {
EditProfileScreen(goBack, openAndPopUp)
}
}

View file

@ -8,6 +8,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
@Composable
fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) {
TextButton(onClick = action, modifier = modifier) { Text(text = stringResource(text)) }
}

View file

@ -1,8 +1,10 @@
package be.ugent.sel.studeez.common.composable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Menu
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
@ -20,6 +22,7 @@ fun PrimaryScreenTemplate(
title: String,
open: (String) -> Unit,
openAndPopUp: (String, String) -> Unit,
action: @Composable RowScope.() -> Unit,
content: @Composable (PaddingValues) -> Unit
) {
val scaffoldState: ScaffoldState = rememberScaffoldState()
@ -39,7 +42,8 @@ fun PrimaryScreenTemplate(
contentDescription = resources().getString(R.string.menu)
)
}
}
},
actions = action
) },
drawerContent = {
@ -62,7 +66,13 @@ fun PrimaryScreenPreview() {
PrimaryScreenTemplate(
"Preview screen",
{ _ -> {}},
{ _, _ -> {}}
{ _, _ -> {}},
{ IconButton(onClick = { /*TODO*/ }) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = "Edit"
)
}}
) {}
}
}

View file

@ -11,14 +11,15 @@ import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Person
import androidx.compose.runtime.*
import be.ugent.sel.studeez.R.string as AppText
import be.ugent.sel.studeez.R.drawable as AppIcon
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import be.ugent.sel.studeez.common.ext.fieldModifier
import be.ugent.sel.studeez.R.drawable as AppIcon
import be.ugent.sel.studeez.R.string as AppText
@Composable
fun BasicField(
@ -36,6 +37,20 @@ fun BasicField(
)
}
@Composable
fun LabelledInputField(
value: String,
onNewValue: (String) -> Unit,
@StringRes label: Int
) {
OutlinedTextField(
value = value,
onValueChange = onNewValue,
label = { Text(text = stringResource(id = label)) },
modifier = Modifier.fieldModifier()
)
}
@Composable
fun UsernameField(
value: String,

View file

@ -29,5 +29,6 @@ interface AccountDAO {
suspend fun sendRecoveryEmail(email: String)
suspend fun signUpWithEmailAndPassword(email: String, password: String)
suspend fun deleteAccount()
suspend fun signOut()
}

View file

@ -4,4 +4,10 @@ interface UserDAO {
suspend fun getUsername(): String?
suspend fun save(newUsername: String)
/**
* Delete all references to this user in the database. Similar to the deleteCascade in
* relational databases.
*/
suspend fun deleteUserReferences()
}

View file

@ -1,19 +1,13 @@
package be.ugent.sel.studeez.domain.implementation
import androidx.compose.runtime.rememberCoroutineScope
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.common.snackbar.SnackbarManager
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.UserDAO
import com.google.firebase.firestore.DocumentReference
import com.google.firebase.firestore.FirebaseFirestore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import kotlin.coroutines.coroutineContext
class FirebaseUserDAO @Inject constructor(
private val firestore: FirebaseFirestore,
@ -34,4 +28,10 @@ class FirebaseUserDAO @Inject constructor(
companion object {
private const val USER_COLLECTION = "users"
}
override suspend fun deleteUserReferences() {
currentUserDocument().delete()
.addOnSuccessListener { SnackbarManager.showMessage(R.string.success) }
.addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) }
}
}

View file

@ -12,4 +12,7 @@ object StudeezDestinations {
// const val TIMERS_SCREEN = "timers"
// const val SETTINGS_SCREEN = "settings"
// Edit screens
const val EDIT_PROFILE_SCREEN = "edit_profile"
}

View file

@ -1,5 +1,9 @@
package be.ugent.sel.studeez.screens.home
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
@ -18,10 +22,21 @@ fun HomeScreen(
PrimaryScreenTemplate(
title = resources().getString(R.string.home),
open = open,
openAndPopUp = openAndPopUp
openAndPopUp = openAndPopUp,
action = { FriendsAction() }
) {
BasicButton(R.string.start_session, Modifier.basicButton()) {
viewModel.onStartSessionClick(open)
}
}
}
@Composable
fun FriendsAction () {
IconButton(onClick = { /*TODO*/ }) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = resources().getString(R.string.friends)
)
}
}

View file

@ -2,7 +2,6 @@ package be.ugent.sel.studeez.screens.navbar
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN
import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_SCREEN
import be.ugent.sel.studeez.screens.StudeezViewModel

View file

@ -0,0 +1,55 @@
package be.ugent.sel.studeez.screens.profile
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.common.composable.BasicTextButton
import be.ugent.sel.studeez.common.composable.LabelledInputField
import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
import be.ugent.sel.studeez.common.ext.textButton
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.ui.theme.StudeezTheme
@Composable
fun EditProfileScreen(
goBack: () -> Unit,
openAndPopUp: (String, String) -> Unit,
viewModel: ProfileEditViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState
SecondaryScreenTemplate(
title = resources().getString(R.string.editing_profile),
popUp = goBack
) {
Column {
LabelledInputField(
value = uiState.username,
onNewValue = viewModel::onUsernameChange,
label = R.string.username
)
BasicTextButton(text = R.string.save, Modifier.textButton()) {
viewModel.onSaveClick()
}
BasicTextButton(text = R.string.delete_profile, Modifier.textButton()) {
viewModel.onDeleteClick(openAndPopUp)
}
}
}
}
@Preview
@Composable
fun EditProfileScreenComposable() {
StudeezTheme {
EditProfileScreen (
{},
{_, _ -> {}}
)
}
}

View file

@ -0,0 +1,5 @@
package be.ugent.sel.studeez.screens.profile
data class ProfileEditUiState (
val username: String = ""
)

View file

@ -0,0 +1,48 @@
package be.ugent.sel.studeez.screens.profile
import androidx.compose.runtime.mutableStateOf
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.common.snackbar.SnackbarManager
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.UserDAO
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ProfileEditViewModel @Inject constructor(
private val accountDAO: AccountDAO,
private val userDAO: UserDAO,
logService: LogService
) : StudeezViewModel(logService) {
var uiState = mutableStateOf(ProfileEditUiState())
private set
init {
launchCatching {
uiState.value = uiState.value.copy(username = userDAO.getUsername()!!)
}
}
fun onUsernameChange(newValue: String) {
uiState.value = uiState.value.copy(username = newValue)
}
fun onSaveClick() {
launchCatching {
userDAO.save(uiState.value.username)
SnackbarManager.showMessage(R.string.success)
}
}
fun onDeleteClick(openAndPopUp: (String, String) -> Unit) {
launchCatching {
userDAO.deleteUserReferences() // Delete references
accountDAO.deleteAccount() // Delete authentication
}
openAndPopUp(StudeezDestinations.SIGN_UP_SCREEN, StudeezDestinations.EDIT_PROFILE_SCREEN)
}
}

View file

@ -1,5 +1,9 @@
package be.ugent.sel.studeez.screens.profile
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.*
import androidx.hilt.navigation.compose.hiltViewModel
import be.ugent.sel.studeez.R
@ -22,8 +26,22 @@ fun ProfileScreen(
PrimaryScreenTemplate(
title = resources().getString(AppText.profile),
open = open,
openAndPopUp = openAndPopUp
openAndPopUp = openAndPopUp,
action = { EditAction { viewModel.onEditProfileClick(open) } }
) {
Headline(text = (username ?: resources().getString(R.string.no_username)))
}
}
@Composable
fun EditAction(
onClick: () -> Unit
) {
IconButton(onClick = onClick) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = resources().getString(AppText.edit_profile)
)
}
}

View file

@ -1,20 +1,10 @@
package be.ugent.sel.studeez.screens.profile
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.viewModelScope
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.UserDAO
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
@ -27,4 +17,8 @@ class ProfileViewModel @Inject constructor(
return userDAO.getUsername()
}
fun onEditProfileClick(open: (String) -> Unit) {
open(StudeezDestinations.EDIT_PROFILE_SCREEN)
}
}

View file

@ -13,11 +13,8 @@ import be.ugent.sel.studeez.navigation.StudeezDestinations.LOGIN_SCREEN
import be.ugent.sel.studeez.navigation.StudeezDestinations.SIGN_UP_SCREEN
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.take
import be.ugent.sel.studeez.R.string as AppText
import javax.inject.Inject
import be.ugent.sel.studeez.R.string as AppText
@HiltViewModel
class SignUpViewModel @Inject constructor(

View file

@ -5,12 +5,20 @@
<string name="email">Email</string>
<string name="password">Password</string>
<string name="repeat_password">Repeat password</string>
<string name="menu">Menu</string>
<!-- Actions -->
<string name="confirm">Confirm</string>
<string name="save">Save</string>
<string name="cancel">Cancel</string>
<string name="go_back">Go back</string>
<string name="next">Next</string>
<!-- Messages -->
<string name="success">Success!</string>
<string name="try_again">Try again</string>
<string name="generic_error">Something wrong happened. Please try again.</string>
<string name="email_error">Please insert a valid email.</string>
<string name="cancel">Cancel</string>
<string name="try_again">Try again</string>
<string name="go_back">Go back</string>
<string name="menu">Menu</string>
<!-- SignUpScreen -->
<string name="create_account">Create account</string>
@ -39,6 +47,12 @@
<!-- Profile -->
<string name="profile">Profile</string>
<string name="no_username">Unknown username</string>
<string name="edit_profile">Edit profile</string>
<string name="editing_profile">Editing profile</string>
<string name="delete_profile">Delete profile</string>
<!-- Friends -->
<string name="friends">Friends</string>
<!-- Drawer / SideMenu -->
<string name="log_out">Log out</string>