diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index 0ee29dd1f9..6cf0390db2 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -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(buildContext, plugins = listOf(callback)) @@ -157,8 +158,9 @@ class PreferencesFlowNode @AssistedInject constructor( NavTarget.AdvancedSettings -> { createNode(buildContext) } - NavTarget.UserProfile -> { - createNode(buildContext) + is NavTarget.UserProfile -> { + val inputs = EditUserProfileNode.Inputs(navTarget.matrixUser) + createNode(buildContext, listOf(inputs)) } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index 3a12ba4478..407832627b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt @@ -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().forEach { it.onOpenNotificationSettings() } } - private fun onOpenUserProfile() { - plugins().forEach { it.onOpenUserProfile() } + private fun onOpenUserProfile(matrixUser: MatrixUser) { + plugins().forEach { it.onOpenUserProfile(matrixUser) } } @Composable diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index d8a05b0db8..933f22bc13 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -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, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt similarity index 64% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesEvents.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt index c54ea98afc..5c53ec23c4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt @@ -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 } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt similarity index 69% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesNode.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt index eaccb6fbbd..738ae4ec6d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt @@ -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, - private val presenter: UserPreferencesPresenter, + presenterFactory: EditUserProfilePresenter.Factory, ) : Node(buildContext, plugins = plugins) { + data class Inputs( + val matrixUser: MatrixUser + ) : NodeInputs + + val matrixUser = 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 ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt similarity index 77% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesPresenter.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt index 49e28144bb..6557079de3 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt @@ -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 { +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(matrixUser: MatrixUser): EditUserProfilePresenter + } @Composable - override fun present(): UserPreferencesState { - var currentUser by remember { mutableStateOf(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> = 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>) = launch { - matrixClient.getCurrentUser() val results = mutableListOf>() suspend { if (!name.isNullOrEmpty() && name.trim() != currentUser.displayName.orEmpty().trim()) { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt similarity index 87% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesState.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt index ee70194638..87668e6f45 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt @@ -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, val saveButtonEnabled: Boolean, val saveAction: Async, - val eventSink: (UserPreferencesEvents) -> Unit + val eventSink: (EditUserProfileEvents) -> Unit ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt similarity index 78% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt index 2c87c591c6..5e4ccb95cb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt @@ -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 { - override val values: Sequence +open class EditUserProfileStateProvider : PreviewParameterProvider { + override val values: Sequence 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, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt similarity index 57% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesView.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt index aba122cef4..02423470a9 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/screen/UserPreferencesView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt @@ -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, diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..f01ae2b5e1 --- /dev/null +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -0,0 +1,9 @@ + + + "Display name" + "Your display name" + "An unknown error was encountered and the information couldn\'t be changed." + "Unable to update profile" + "Edit profile" + "Updating profile…" + diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt index cd0cbf878e..4553161d73 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt @@ -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() }) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt new file mode 100644 index 0000000000..a273acf25c --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt @@ -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", + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 38b1df6dce..45d7780393 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -43,5 +43,7 @@ enum class AvatarSize(val dp: Dp) { RoomInviteItem(52.dp), InviteSender(16.dp), + EditRoomDetails(70.dp), + NotificationsOptIn(32.dp), } diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 97eb7b6864..88cc929e79 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -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) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt new file mode 100644 index 0000000000..f97227be7e --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt @@ -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, + ) + } + } + } +} diff --git a/tools/localazy/config.json b/tools/localazy/config.json index b92e02f670..a69b7d2273 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -128,6 +128,12 @@ "includeRegex": [ "screen_create_poll_.*" ] + }, + { + "name": ":features:preferences:impl", + "includeRegex": [ + "screen_edit_profile_.*" + ] } ] }