Changes:
- Improve UI to match designs. - Extract `EditableAvatarView` component. - Create `LabelledOutlinedTextField`. - Get strings from Localazy.
This commit is contained in:
@@ -38,11 +38,12 @@ import io.element.android.features.preferences.impl.developer.tracing.ConfigureT
|
||||
import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode
|
||||
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode
|
||||
import io.element.android.features.preferences.impl.root.PreferencesRootNode
|
||||
import io.element.android.features.preferences.impl.user.screen.UserPreferencesNode
|
||||
import io.element.android.features.preferences.impl.user.editprofile.EditUserProfileNode
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@@ -84,7 +85,7 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||
data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object UserProfile : NavTarget
|
||||
data class UserProfile(val matrixUser: MatrixUser) : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
@@ -119,8 +120,8 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||
backstack.push(NavTarget.AdvancedSettings)
|
||||
}
|
||||
|
||||
override fun onOpenUserProfile() {
|
||||
backstack.push(NavTarget.UserProfile)
|
||||
override fun onOpenUserProfile(matrixUser: MatrixUser) {
|
||||
backstack.push(NavTarget.UserProfile(matrixUser))
|
||||
}
|
||||
}
|
||||
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
|
||||
@@ -157,8 +158,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||
NavTarget.AdvancedSettings -> {
|
||||
createNode<AdvancedSettingsNode>(buildContext)
|
||||
}
|
||||
NavTarget.UserProfile -> {
|
||||
createNode<UserPreferencesNode>(buildContext)
|
||||
is NavTarget.UserProfile -> {
|
||||
val inputs = EditUserProfileNode.Inputs(navTarget.matrixUser)
|
||||
createNode<EditUserProfileNode>(buildContext, listOf(inputs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -47,7 +48,7 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||
fun onOpenDeveloperSettings()
|
||||
fun onOpenNotificationSettings()
|
||||
fun onOpenAdvancedSettings()
|
||||
fun onOpenUserProfile()
|
||||
fun onOpenUserProfile(matrixUser: MatrixUser)
|
||||
}
|
||||
|
||||
private fun onOpenBugReport() {
|
||||
@@ -92,8 +93,8 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||
plugins<Callback>().forEach { it.onOpenNotificationSettings() }
|
||||
}
|
||||
|
||||
private fun onOpenUserProfile() {
|
||||
plugins<Callback>().forEach { it.onOpenUserProfile() }
|
||||
private fun onOpenUserProfile(matrixUser: MatrixUser) {
|
||||
plugins<Callback>().forEach { it.onOpenUserProfile(matrixUser) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -63,7 +63,7 @@ fun PreferencesRootView(
|
||||
onOpenAdvancedSettings: () -> Unit,
|
||||
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
|
||||
onOpenNotificationSettings: () -> Unit,
|
||||
onOpenUserProfile: () -> Unit,
|
||||
onOpenUserProfile: (MatrixUser) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||
@@ -77,7 +77,7 @@ fun PreferencesRootView(
|
||||
) {
|
||||
UserPreferences(
|
||||
modifier = Modifier.clickable {
|
||||
onOpenUserProfile()
|
||||
state.myUser?.let(onOpenUserProfile)
|
||||
},
|
||||
user = state.myUser,
|
||||
)
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.user.screen
|
||||
package io.element.android.features.preferences.impl.user.editprofile
|
||||
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
|
||||
sealed interface UserPreferencesEvents {
|
||||
data class HandleAvatarAction(val action: AvatarAction) : UserPreferencesEvents
|
||||
data class UpdateDisplayName(val name: String) : UserPreferencesEvents
|
||||
data object Save : UserPreferencesEvents
|
||||
data object CancelSaveChanges : UserPreferencesEvents
|
||||
sealed interface EditUserProfileEvents {
|
||||
data class HandleAvatarAction(val action: AvatarAction) : EditUserProfileEvents
|
||||
data class UpdateDisplayName(val name: String) : EditUserProfileEvents
|
||||
data object Save : EditUserProfileEvents
|
||||
data object CancelSaveChanges : EditUserProfileEvents
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.user.screen
|
||||
package io.element.android.features.preferences.impl.user.editprofile
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -24,22 +24,32 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class UserPreferencesNode @AssistedInject constructor(
|
||||
class EditUserProfileNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: UserPreferencesPresenter,
|
||||
presenterFactory: EditUserProfilePresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
data class Inputs(
|
||||
val matrixUser: MatrixUser
|
||||
) : NodeInputs
|
||||
|
||||
val matrixUser = inputs<Inputs>().matrixUser
|
||||
val presenter = presenterFactory.create(matrixUser)
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
UserPreferencesView(
|
||||
EditUserProfileView(
|
||||
state = state,
|
||||
onBackPressed = ::navigateUp,
|
||||
onProfileEdited = ::navigateUp, // TODO: check if something else is needed
|
||||
onProfileEdited = ::navigateUp,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
@@ -14,19 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.user.screen
|
||||
package io.element.android.features.preferences.impl.user.editprofile
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
@@ -41,19 +41,23 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class UserPreferencesPresenter @Inject constructor(
|
||||
class EditUserProfilePresenter @AssistedInject constructor(
|
||||
@Assisted private val matrixUser: MatrixUser,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val mediaPickerProvider: PickerProvider,
|
||||
private val mediaPreProcessor: MediaPreProcessor,
|
||||
) : Presenter<UserPreferencesState> {
|
||||
) : Presenter<EditUserProfileState> {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(matrixUser: MatrixUser): EditUserProfilePresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): UserPreferencesState {
|
||||
var currentUser by remember { mutableStateOf<MatrixUser?>(null) }
|
||||
var userAvatarUri by rememberSaveable(currentUser) { mutableStateOf(currentUser?.avatarUrl?.let { Uri.parse(it) }) }
|
||||
var userDisplayName by rememberSaveable(currentUser) { mutableStateOf(currentUser?.displayName) }
|
||||
override fun present(): EditUserProfileState {
|
||||
var userAvatarUri = remember { matrixUser.avatarUrl?.let { Uri.parse(it) } }
|
||||
var userDisplayName = remember { matrixUser.displayName }
|
||||
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
|
||||
onResult = { uri -> if (uri != null) userAvatarUri = uri }
|
||||
)
|
||||
@@ -61,10 +65,6 @@ class UserPreferencesPresenter @Inject constructor(
|
||||
onResult = { uri -> if (uri != null) userAvatarUri = uri }
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
currentUser = matrixClient.getCurrentUser()
|
||||
}
|
||||
|
||||
val avatarActions by remember(userAvatarUri) {
|
||||
derivedStateOf {
|
||||
listOfNotNull(
|
||||
@@ -77,12 +77,10 @@ class UserPreferencesPresenter @Inject constructor(
|
||||
|
||||
val saveAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
fun handleEvents(event: UserPreferencesEvents) {
|
||||
fun handleEvents(event: EditUserProfileEvents) {
|
||||
when (event) {
|
||||
is UserPreferencesEvents.Save -> currentUser?.let {
|
||||
localCoroutineScope.saveChanges(userDisplayName, userAvatarUri, it, saveAction)
|
||||
}
|
||||
is UserPreferencesEvents.HandleAvatarAction -> {
|
||||
is EditUserProfileEvents.Save -> localCoroutineScope.saveChanges(userDisplayName, userAvatarUri, matrixUser, saveAction)
|
||||
is EditUserProfileEvents.HandleAvatarAction -> {
|
||||
when (event.action) {
|
||||
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
||||
AvatarAction.TakePhoto -> cameraPhotoPicker.launch()
|
||||
@@ -90,19 +88,19 @@ class UserPreferencesPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
is UserPreferencesEvents.UpdateDisplayName -> userDisplayName = event.name
|
||||
UserPreferencesEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized
|
||||
is EditUserProfileEvents.UpdateDisplayName -> userDisplayName = event.name
|
||||
EditUserProfileEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
val canSave = remember(userDisplayName, userAvatarUri, currentUser) {
|
||||
val hasProfileChanged = hasDisplayNameChanged(userDisplayName, currentUser)
|
||||
|| hasAvatarUrlChanged(userAvatarUri, currentUser)
|
||||
val canSave = remember(userDisplayName, userAvatarUri) {
|
||||
val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser)
|
||||
|| hasAvatarUrlChanged(userAvatarUri, matrixUser)
|
||||
!userDisplayName.isNullOrBlank() && hasProfileChanged
|
||||
}
|
||||
|
||||
return UserPreferencesState(
|
||||
userId = currentUser?.userId,
|
||||
return EditUserProfileState(
|
||||
userId = matrixUser.userId,
|
||||
displayName = userDisplayName.orEmpty(),
|
||||
userAvatarUrl = userAvatarUri,
|
||||
avatarActions = avatarActions,
|
||||
@@ -116,7 +114,6 @@ class UserPreferencesPresenter @Inject constructor(
|
||||
private fun hasAvatarUrlChanged(avatarUri: Uri?, currentUser: MatrixUser?) = avatarUri?.toString()?.trim() != currentUser?.avatarUrl?.trim()
|
||||
|
||||
private fun CoroutineScope.saveChanges(name: String?, avatarUri: Uri?, currentUser: MatrixUser, action: MutableState<Async<Unit>>) = launch {
|
||||
matrixClient.getCurrentUser()
|
||||
val results = mutableListOf<Result<Unit>>()
|
||||
suspend {
|
||||
if (!name.isNullOrEmpty() && name.trim() != currentUser.displayName.orEmpty().trim()) {
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.user.screen
|
||||
package io.element.android.features.preferences.impl.user.editprofile
|
||||
|
||||
import android.net.Uri
|
||||
import io.element.android.libraries.architecture.Async
|
||||
@@ -22,12 +22,12 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class UserPreferencesState(
|
||||
data class EditUserProfileState(
|
||||
val userId: UserId?,
|
||||
val displayName: String,
|
||||
val userAvatarUrl: Uri?,
|
||||
val avatarActions: ImmutableList<AvatarAction>,
|
||||
val saveButtonEnabled: Boolean,
|
||||
val saveAction: Async<Unit>,
|
||||
val eventSink: (UserPreferencesEvents) -> Unit
|
||||
val eventSink: (EditUserProfileEvents) -> Unit
|
||||
)
|
||||
@@ -14,22 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.user.screen
|
||||
package io.element.android.features.preferences.impl.user.editprofile
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class UserPreferencesStateProvider : PreviewParameterProvider<UserPreferencesState> {
|
||||
override val values: Sequence<UserPreferencesState>
|
||||
open class EditUserProfileStateProvider : PreviewParameterProvider<EditUserProfileState> {
|
||||
override val values: Sequence<EditUserProfileState>
|
||||
get() = sequenceOf(
|
||||
aUserPreferencesState(),
|
||||
aEditUserProfileState(),
|
||||
// Add other states here
|
||||
)
|
||||
}
|
||||
|
||||
fun aUserPreferencesState() = UserPreferencesState(
|
||||
fun aEditUserProfileState() = EditUserProfileState(
|
||||
userId = UserId("@john.doe:matrix.org"),
|
||||
displayName = "John Doe",
|
||||
userAvatarUrl = null,
|
||||
@@ -14,71 +14,59 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.user.screen
|
||||
package io.element.android.features.preferences.impl.user.editprofile
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AddAPhoto
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.LabelledTextField
|
||||
import io.element.android.libraries.designsystem.components.LabelledOutlinedTextField
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
||||
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
|
||||
import io.element.android.libraries.matrix.ui.components.EditableAvatarView
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun UserPreferencesView(
|
||||
state: UserPreferencesState,
|
||||
fun EditUserProfileView(
|
||||
state: EditUserProfileState,
|
||||
onBackPressed: () -> Unit,
|
||||
onProfileEdited: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -102,7 +90,7 @@ fun UserPreferencesView(
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Edit profile",
|
||||
text = stringResource(R.string.screen_edit_profile_title),
|
||||
style = ElementTheme.typography.aliasScreenTitle,
|
||||
)
|
||||
},
|
||||
@@ -113,7 +101,7 @@ fun UserPreferencesView(
|
||||
enabled = state.saveButtonEnabled,
|
||||
onClick = {
|
||||
focusManager.clearFocus()
|
||||
state.eventSink(UserPreferencesEvents.Save)
|
||||
state.eventSink(EditUserProfileEvents.Save)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -129,33 +117,50 @@ fun UserPreferencesView(
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
EditableAvatarView(state, ::onAvatarClicked)
|
||||
Spacer(modifier = Modifier.height(60.dp))
|
||||
EditableAvatarView(
|
||||
userId = state.userId?.value,
|
||||
displayName = state.displayName,
|
||||
avatarUrl = state.userAvatarUrl,
|
||||
avatarSize = AvatarSize.RoomHeader,
|
||||
onAvatarClicked = { onAvatarClicked() },
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
state.userId?.let {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = it.value,
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
LabelledTextField(
|
||||
label = "Display name",
|
||||
LabelledOutlinedTextField(
|
||||
label = stringResource(R.string.screen_edit_profile_display_name),
|
||||
value = state.displayName,
|
||||
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
|
||||
singleLine = true,
|
||||
onValueChange = { state.eventSink(UserPreferencesEvents.UpdateDisplayName(it)) },
|
||||
onValueChange = { state.eventSink(EditUserProfileEvents.UpdateDisplayName(it)) },
|
||||
)
|
||||
}
|
||||
|
||||
AvatarActionBottomSheet(
|
||||
actions = state.avatarActions,
|
||||
modalBottomSheetState = itemActionsBottomSheetState,
|
||||
onActionSelected = { state.eventSink(UserPreferencesEvents.HandleAvatarAction(it)) }
|
||||
onActionSelected = { state.eventSink(EditUserProfileEvents.HandleAvatarAction(it)) }
|
||||
)
|
||||
|
||||
when (state.saveAction) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog(text = "Updating profile...")
|
||||
ProgressDialog(text = stringResource(R.string.screen_edit_profile_updating_details))
|
||||
}
|
||||
|
||||
is Async.Failure -> {
|
||||
ErrorDialog(
|
||||
content = "Error updating profile",
|
||||
onDismiss = { state.eventSink(UserPreferencesEvents.CancelSaveChanges) },
|
||||
title = stringResource(R.string.screen_edit_profile_error_title),
|
||||
content = stringResource(R.string.screen_edit_profile_error),
|
||||
onDismiss = { state.eventSink(EditUserProfileEvents.CancelSaveChanges) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -170,82 +175,8 @@ fun UserPreferencesView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditableAvatarView(
|
||||
state: UserPreferencesState,
|
||||
onAvatarClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(70.dp)
|
||||
.clickable(onClick = onAvatarClicked)
|
||||
) {
|
||||
// TODO this might be able to be simplified into a single component once send/receive media is done
|
||||
when (state.userAvatarUrl?.scheme) {
|
||||
null, "mxc" -> {
|
||||
Avatar(
|
||||
avatarData = AvatarData(state.userId.toString(), state.displayName, state.userAvatarUrl?.toString(), size = AvatarSize.RoomHeader),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
UnsavedAvatar(
|
||||
avatarUri = state.userAvatarUrl,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.size(24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
imageVector = Icons.Outlined.AddAPhoto,
|
||||
contentDescription = "",
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LabelledReadOnlyField(
|
||||
title: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
text = title,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
text = value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
|
||||
pointerInput(Unit) {
|
||||
this.pointerInput(Unit) {
|
||||
detectTapGestures(onTap = {
|
||||
focusManager.clearFocus()
|
||||
})
|
||||
@@ -253,17 +184,17 @@ private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun UserPreferencesViewLightPreview(@PreviewParameter(UserPreferencesStateProvider::class) state: UserPreferencesState) =
|
||||
fun EditUserProfileViewLightPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun UserPreferencesViewDarkPreview(@PreviewParameter(UserPreferencesStateProvider::class) state: UserPreferencesState) =
|
||||
fun EditUserProfileViewDarkPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: UserPreferencesState) {
|
||||
UserPreferencesView(
|
||||
private fun ContentToPreview(state: EditUserProfileState) {
|
||||
EditUserProfileView(
|
||||
onBackPressed = {},
|
||||
onProfileEdited = {},
|
||||
state = state,
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_edit_profile_display_name">"Display name"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string>
|
||||
<string name="screen_edit_profile_error">"An unknown error was encountered and the information couldn\'t be changed."</string>
|
||||
<string name="screen_edit_profile_error_title">"Unable to update profile"</string>
|
||||
<string name="screen_edit_profile_title">"Edit profile"</string>
|
||||
<string name="screen_edit_profile_updating_details">"Updating profile…"</string>
|
||||
</resources>
|
||||
@@ -18,37 +18,27 @@
|
||||
|
||||
package io.element.android.features.roomdetails.impl.edit
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AddAPhoto
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
@@ -61,21 +51,18 @@ import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.LabelledTextField
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
||||
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
|
||||
import io.element.android.libraries.matrix.ui.components.EditableAvatarView
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -134,7 +121,14 @@ fun RoomDetailsEditView(
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
EditableAvatarView(state, ::onAvatarClicked)
|
||||
EditableAvatarView(
|
||||
userId = state.roomId,
|
||||
displayName = state.roomName,
|
||||
avatarUrl = state.roomAvatarUrl,
|
||||
avatarSize = AvatarSize.EditRoomDetails,
|
||||
onAvatarClicked = ::onAvatarClicked,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(60.dp))
|
||||
|
||||
if (state.canChangeName) {
|
||||
@@ -202,56 +196,6 @@ fun RoomDetailsEditView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditableAvatarView(
|
||||
state: RoomDetailsEditState,
|
||||
onAvatarClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(70.dp)
|
||||
.clickable(onClick = onAvatarClicked, enabled = state.canChangeAvatar)
|
||||
) {
|
||||
// TODO this might be able to be simplified into a single component once send/receive media is done
|
||||
when (state.roomAvatarUrl?.scheme) {
|
||||
null, "mxc" -> {
|
||||
Avatar(
|
||||
avatarData = AvatarData(state.roomId, state.roomName, state.roomAvatarUrl?.toString(), size = AvatarSize.RoomHeader),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
UnsavedAvatar(
|
||||
avatarUri = state.roomAvatarUrl,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.canChangeAvatar) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.size(24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
imageVector = Icons.Outlined.AddAPhoto,
|
||||
contentDescription = "",
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LabelledReadOnlyField(
|
||||
title: String,
|
||||
@@ -279,7 +223,7 @@ private fun LabelledReadOnlyField(
|
||||
}
|
||||
|
||||
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
|
||||
pointerInput(Unit) {
|
||||
this.pointerInput(Unit) {
|
||||
detectTapGestures(onTap = {
|
||||
focusManager.clearFocus()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
|
||||
@Composable
|
||||
fun LabelledOutlinedTextField(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier,
|
||||
placeholder: String? = null,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
onValueChange: (String) -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
text = label
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = value,
|
||||
placeholder = placeholder?.let { { Text(placeholder) } },
|
||||
onValueChange = onValueChange,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
keyboardOptions = keyboardOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun LabelledOutlinedTextFieldLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun LabelledOutlinedTextFieldDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Column {
|
||||
LabelledOutlinedTextField(
|
||||
label = "Room name",
|
||||
value = "",
|
||||
placeholder = "e.g. Product Sprint",
|
||||
)
|
||||
LabelledOutlinedTextField(
|
||||
label = "Room name",
|
||||
value = "a room name",
|
||||
placeholder = "e.g. Product Sprint",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -43,5 +43,7 @@ enum class AvatarSize(val dp: Dp) {
|
||||
RoomInviteItem(52.dp),
|
||||
InviteSender(16.dp),
|
||||
|
||||
EditRoomDetails(70.dp),
|
||||
|
||||
NotificationsOptIn(32.dp),
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ anvil {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.rustsdk)
|
||||
// implementation(libs.matrix.sdk)
|
||||
// implementation(projects.libraries.rustsdk)
|
||||
implementation(libs.matrix.sdk)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.network)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AddAPhoto
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
|
||||
@Composable
|
||||
fun EditableAvatarView(
|
||||
userId: String?,
|
||||
displayName: String?,
|
||||
avatarUrl: Uri?,
|
||||
avatarSize: AvatarSize,
|
||||
onAvatarClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(avatarSize.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onAvatarClicked,
|
||||
indication = rememberRipple(bounded = false),
|
||||
)
|
||||
) {
|
||||
when (avatarUrl?.scheme) {
|
||||
null, "mxc" -> {
|
||||
userId?.let {
|
||||
Avatar(
|
||||
avatarData = AvatarData(it, displayName, avatarUrl?.toString(), size = avatarSize),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
UnsavedAvatar(
|
||||
avatarUri = avatarUrl,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.size(24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
imageVector = Icons.Outlined.AddAPhoto,
|
||||
contentDescription = "",
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,12 @@
|
||||
"includeRegex": [
|
||||
"screen_create_poll_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": ":features:preferences:impl",
|
||||
"includeRegex": [
|
||||
"screen_edit_profile_.*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user