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.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.home.HomeScreen import be.ugent.sel.studeez.screens.home.HomeScreen
import be.ugent.sel.studeez.screens.log_in.LoginScreen 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.profile.ProfileScreen
import be.ugent.sel.studeez.screens.sign_up.SignUpScreen import be.ugent.sel.studeez.screens.sign_up.SignUpScreen
import be.ugent.sel.studeez.screens.splash.SplashScreen import be.ugent.sel.studeez.screens.splash.SplashScreen
@ -77,12 +78,16 @@ fun resources(): Resources {
fun NavGraphBuilder.studeezGraph(appState: StudeezAppstate) { fun NavGraphBuilder.studeezGraph(appState: StudeezAppstate) {
val openAndPopUp: (String, String) -> Unit = { val goBack: () -> Unit = {
route, popUp -> appState.navigateAndPopUp(route, popUp) appState.popUp()
} }
val open: (String) -> Unit = { val open: (String) -> Unit = {
route -> appState.navigate(route) route -> appState.navigate(route)
}
val openAndPopUp: (String, String) -> Unit = {
route, popUp -> appState.navigateAndPopUp(route, popUp)
} }
composable(StudeezDestinations.SPLASH_SCREEN) { composable(StudeezDestinations.SPLASH_SCREEN) {
@ -110,4 +115,9 @@ fun NavGraphBuilder.studeezGraph(appState: StudeezAppstate) {
// TODO Timers screen // TODO Timers screen
// TODO Settings 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 import androidx.compose.ui.unit.sp
@Composable @Composable
fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) { fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) {
TextButton(onClick = action, modifier = modifier) { Text(text = stringResource(text)) } TextButton(onClick = action, modifier = modifier) { Text(text = stringResource(text)) }
} }

View file

@ -1,8 +1,10 @@
package be.ugent.sel.studeez.common.composable package be.ugent.sel.studeez.common.composable
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -20,6 +22,7 @@ fun PrimaryScreenTemplate(
title: String, title: String,
open: (String) -> Unit, open: (String) -> Unit,
openAndPopUp: (String, String) -> Unit, openAndPopUp: (String, String) -> Unit,
action: @Composable RowScope.() -> Unit,
content: @Composable (PaddingValues) -> Unit content: @Composable (PaddingValues) -> Unit
) { ) {
val scaffoldState: ScaffoldState = rememberScaffoldState() val scaffoldState: ScaffoldState = rememberScaffoldState()
@ -39,7 +42,8 @@ fun PrimaryScreenTemplate(
contentDescription = resources().getString(R.string.menu) contentDescription = resources().getString(R.string.menu)
) )
} }
} },
actions = action
) }, ) },
drawerContent = { drawerContent = {
@ -62,7 +66,13 @@ fun PrimaryScreenPreview() {
PrimaryScreenTemplate( PrimaryScreenTemplate(
"Preview screen", "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.Lock
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.runtime.* 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.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation 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 @Composable
fun BasicField( 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 @Composable
fun UsernameField( fun UsernameField(
value: String, value: String,

View file

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

View file

@ -4,4 +4,10 @@ interface UserDAO {
suspend fun getUsername(): String? suspend fun getUsername(): String?
suspend fun save(newUsername: 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 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.AccountDAO
import be.ugent.sel.studeez.domain.UserDAO import be.ugent.sel.studeez.domain.UserDAO
import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.DocumentReference
import com.google.firebase.firestore.FirebaseFirestore 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 kotlinx.coroutines.tasks.await
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.coroutineContext
class FirebaseUserDAO @Inject constructor( class FirebaseUserDAO @Inject constructor(
private val firestore: FirebaseFirestore, private val firestore: FirebaseFirestore,
@ -34,4 +28,10 @@ class FirebaseUserDAO @Inject constructor(
companion object { companion object {
private const val USER_COLLECTION = "users" 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 TIMERS_SCREEN = "timers"
// const val SETTINGS_SCREEN = "settings" // 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 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.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -18,10 +22,21 @@ fun HomeScreen(
PrimaryScreenTemplate( PrimaryScreenTemplate(
title = resources().getString(R.string.home), title = resources().getString(R.string.home),
open = open, open = open,
openAndPopUp = openAndPopUp openAndPopUp = openAndPopUp,
action = { FriendsAction() }
) { ) {
BasicButton(R.string.start_session, Modifier.basicButton()) { BasicButton(R.string.start_session, Modifier.basicButton()) {
viewModel.onStartSessionClick(open) 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.AccountDAO
import be.ugent.sel.studeez.domain.LogService 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.HOME_SCREEN
import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_SCREEN import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_SCREEN
import be.ugent.sel.studeez.screens.StudeezViewModel 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 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.compose.runtime.*
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import be.ugent.sel.studeez.R import be.ugent.sel.studeez.R
@ -22,8 +26,22 @@ fun ProfileScreen(
PrimaryScreenTemplate( PrimaryScreenTemplate(
title = resources().getString(AppText.profile), title = resources().getString(AppText.profile),
open = open, open = open,
openAndPopUp = openAndPopUp openAndPopUp = openAndPopUp,
action = { EditAction { viewModel.onEditProfileClick(open) } }
) { ) {
Headline(text = (username ?: resources().getString(R.string.no_username))) 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 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.LogService
import be.ugent.sel.studeez.domain.UserDAO 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 be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel 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 import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -27,4 +17,8 @@ class ProfileViewModel @Inject constructor(
return userDAO.getUsername() 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.navigation.StudeezDestinations.SIGN_UP_SCREEN
import be.ugent.sel.studeez.screens.StudeezViewModel import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel 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 javax.inject.Inject
import be.ugent.sel.studeez.R.string as AppText
@HiltViewModel @HiltViewModel
class SignUpViewModel @Inject constructor( class SignUpViewModel @Inject constructor(

View file

@ -5,13 +5,21 @@
<string name="email">Email</string> <string name="email">Email</string>
<string name="password">Password</string> <string name="password">Password</string>
<string name="repeat_password">Repeat password</string> <string name="repeat_password">Repeat password</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> <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>
<!-- SignUpScreen --> <!-- SignUpScreen -->
<string name="create_account">Create account</string> <string name="create_account">Create account</string>
<string name="password_error">Your password should have at least six characters and include one digit, one lower case letter and one upper case letter.</string> <string name="password_error">Your password should have at least six characters and include one digit, one lower case letter and one upper case letter.</string>
@ -39,6 +47,12 @@
<!-- Profile --> <!-- Profile -->
<string name="profile">Profile</string> <string name="profile">Profile</string>
<string name="no_username">Unknown username</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 --> <!-- Drawer / SideMenu -->
<string name="log_out">Log out</string> <string name="log_out">Log out</string>