Changes:
- Improve UI to match designs. - Extract `EditableAvatarView` component. - Create `LabelledOutlinedTextField`. - Get strings from Localazy.
This commit is contained in:
parent
fa277f062f
commit
41e614310f
16 changed files with 325 additions and 235 deletions
|
|
@ -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.NotificationSettingsNode
|
||||||
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode
|
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.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.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
|
|
@ -84,7 +85,7 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget
|
data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object UserProfile : NavTarget
|
data class UserProfile(val matrixUser: MatrixUser) : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -119,8 +120,8 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.AdvancedSettings)
|
backstack.push(NavTarget.AdvancedSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenUserProfile() {
|
override fun onOpenUserProfile(matrixUser: MatrixUser) {
|
||||||
backstack.push(NavTarget.UserProfile)
|
backstack.push(NavTarget.UserProfile(matrixUser))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
|
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
|
||||||
|
|
@ -157,8 +158,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
NavTarget.AdvancedSettings -> {
|
NavTarget.AdvancedSettings -> {
|
||||||
createNode<AdvancedSettingsNode>(buildContext)
|
createNode<AdvancedSettingsNode>(buildContext)
|
||||||
}
|
}
|
||||||
NavTarget.UserProfile -> {
|
is NavTarget.UserProfile -> {
|
||||||
createNode<UserPreferencesNode>(buildContext)
|
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.anvilannotations.ContributesNode
|
||||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
@ -47,7 +48,7 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
fun onOpenDeveloperSettings()
|
fun onOpenDeveloperSettings()
|
||||||
fun onOpenNotificationSettings()
|
fun onOpenNotificationSettings()
|
||||||
fun onOpenAdvancedSettings()
|
fun onOpenAdvancedSettings()
|
||||||
fun onOpenUserProfile()
|
fun onOpenUserProfile(matrixUser: MatrixUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onOpenBugReport() {
|
private fun onOpenBugReport() {
|
||||||
|
|
@ -92,8 +93,8 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
plugins<Callback>().forEach { it.onOpenNotificationSettings() }
|
plugins<Callback>().forEach { it.onOpenNotificationSettings() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onOpenUserProfile() {
|
private fun onOpenUserProfile(matrixUser: MatrixUser) {
|
||||||
plugins<Callback>().forEach { it.onOpenUserProfile() }
|
plugins<Callback>().forEach { it.onOpenUserProfile(matrixUser) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ fun PreferencesRootView(
|
||||||
onOpenAdvancedSettings: () -> Unit,
|
onOpenAdvancedSettings: () -> Unit,
|
||||||
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
|
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
|
||||||
onOpenNotificationSettings: () -> Unit,
|
onOpenNotificationSettings: () -> Unit,
|
||||||
onOpenUserProfile: () -> Unit,
|
onOpenUserProfile: (MatrixUser) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||||
|
|
@ -77,7 +77,7 @@ fun PreferencesRootView(
|
||||||
) {
|
) {
|
||||||
UserPreferences(
|
UserPreferences(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
onOpenUserProfile()
|
state.myUser?.let(onOpenUserProfile)
|
||||||
},
|
},
|
||||||
user = state.myUser,
|
user = state.myUser,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,13 @@
|
||||||
* limitations under the License.
|
* 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
|
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
|
|
||||||
sealed interface UserPreferencesEvents {
|
sealed interface EditUserProfileEvents {
|
||||||
data class HandleAvatarAction(val action: AvatarAction) : UserPreferencesEvents
|
data class HandleAvatarAction(val action: AvatarAction) : EditUserProfileEvents
|
||||||
data class UpdateDisplayName(val name: String) : UserPreferencesEvents
|
data class UpdateDisplayName(val name: String) : EditUserProfileEvents
|
||||||
data object Save : UserPreferencesEvents
|
data object Save : EditUserProfileEvents
|
||||||
data object CancelSaveChanges : UserPreferencesEvents
|
data object CancelSaveChanges : EditUserProfileEvents
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -24,22 +24,32 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
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.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class UserPreferencesNode @AssistedInject constructor(
|
class EditUserProfileNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: UserPreferencesPresenter,
|
presenterFactory: EditUserProfilePresenter.Factory,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
data class Inputs(
|
||||||
|
val matrixUser: MatrixUser
|
||||||
|
) : NodeInputs
|
||||||
|
|
||||||
|
val matrixUser = inputs<Inputs>().matrixUser
|
||||||
|
val presenter = presenterFactory.create(matrixUser)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
UserPreferencesView(
|
EditUserProfileView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackPressed = ::navigateUp,
|
onBackPressed = ::navigateUp,
|
||||||
onProfileEdited = ::navigateUp, // TODO: check if something else is needed
|
onProfileEdited = ::navigateUp,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -14,19 +14,19 @@
|
||||||
* limitations under the License.
|
* 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 android.net.Uri
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import dagger.assisted.Assisted
|
||||||
import androidx.compose.runtime.setValue
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
|
@ -41,19 +41,23 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
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 matrixClient: MatrixClient,
|
||||||
private val mediaPickerProvider: PickerProvider,
|
private val mediaPickerProvider: PickerProvider,
|
||||||
private val mediaPreProcessor: MediaPreProcessor,
|
private val mediaPreProcessor: MediaPreProcessor,
|
||||||
) : Presenter<UserPreferencesState> {
|
) : Presenter<EditUserProfileState> {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(matrixUser: MatrixUser): EditUserProfilePresenter
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): UserPreferencesState {
|
override fun present(): EditUserProfileState {
|
||||||
var currentUser by remember { mutableStateOf<MatrixUser?>(null) }
|
var userAvatarUri = remember { matrixUser.avatarUrl?.let { Uri.parse(it) } }
|
||||||
var userAvatarUri by rememberSaveable(currentUser) { mutableStateOf(currentUser?.avatarUrl?.let { Uri.parse(it) }) }
|
var userDisplayName = remember { matrixUser.displayName }
|
||||||
var userDisplayName by rememberSaveable(currentUser) { mutableStateOf(currentUser?.displayName) }
|
|
||||||
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
|
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
|
||||||
onResult = { uri -> if (uri != null) userAvatarUri = uri }
|
onResult = { uri -> if (uri != null) userAvatarUri = uri }
|
||||||
)
|
)
|
||||||
|
|
@ -61,10 +65,6 @@ class UserPreferencesPresenter @Inject constructor(
|
||||||
onResult = { uri -> if (uri != null) userAvatarUri = uri }
|
onResult = { uri -> if (uri != null) userAvatarUri = uri }
|
||||||
)
|
)
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
currentUser = matrixClient.getCurrentUser()
|
|
||||||
}
|
|
||||||
|
|
||||||
val avatarActions by remember(userAvatarUri) {
|
val avatarActions by remember(userAvatarUri) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
|
|
@ -77,12 +77,10 @@ class UserPreferencesPresenter @Inject constructor(
|
||||||
|
|
||||||
val saveAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
|
val saveAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
fun handleEvents(event: UserPreferencesEvents) {
|
fun handleEvents(event: EditUserProfileEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is UserPreferencesEvents.Save -> currentUser?.let {
|
is EditUserProfileEvents.Save -> localCoroutineScope.saveChanges(userDisplayName, userAvatarUri, matrixUser, saveAction)
|
||||||
localCoroutineScope.saveChanges(userDisplayName, userAvatarUri, it, saveAction)
|
is EditUserProfileEvents.HandleAvatarAction -> {
|
||||||
}
|
|
||||||
is UserPreferencesEvents.HandleAvatarAction -> {
|
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
||||||
AvatarAction.TakePhoto -> cameraPhotoPicker.launch()
|
AvatarAction.TakePhoto -> cameraPhotoPicker.launch()
|
||||||
|
|
@ -90,19 +88,19 @@ class UserPreferencesPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is UserPreferencesEvents.UpdateDisplayName -> userDisplayName = event.name
|
is EditUserProfileEvents.UpdateDisplayName -> userDisplayName = event.name
|
||||||
UserPreferencesEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized
|
EditUserProfileEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val canSave = remember(userDisplayName, userAvatarUri, currentUser) {
|
val canSave = remember(userDisplayName, userAvatarUri) {
|
||||||
val hasProfileChanged = hasDisplayNameChanged(userDisplayName, currentUser)
|
val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser)
|
||||||
|| hasAvatarUrlChanged(userAvatarUri, currentUser)
|
|| hasAvatarUrlChanged(userAvatarUri, matrixUser)
|
||||||
!userDisplayName.isNullOrBlank() && hasProfileChanged
|
!userDisplayName.isNullOrBlank() && hasProfileChanged
|
||||||
}
|
}
|
||||||
|
|
||||||
return UserPreferencesState(
|
return EditUserProfileState(
|
||||||
userId = currentUser?.userId,
|
userId = matrixUser.userId,
|
||||||
displayName = userDisplayName.orEmpty(),
|
displayName = userDisplayName.orEmpty(),
|
||||||
userAvatarUrl = userAvatarUri,
|
userAvatarUrl = userAvatarUri,
|
||||||
avatarActions = avatarActions,
|
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 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 {
|
private fun CoroutineScope.saveChanges(name: String?, avatarUri: Uri?, currentUser: MatrixUser, action: MutableState<Async<Unit>>) = launch {
|
||||||
matrixClient.getCurrentUser()
|
|
||||||
val results = mutableListOf<Result<Unit>>()
|
val results = mutableListOf<Result<Unit>>()
|
||||||
suspend {
|
suspend {
|
||||||
if (!name.isNullOrEmpty() && name.trim() != currentUser.displayName.orEmpty().trim()) {
|
if (!name.isNullOrEmpty() && name.trim() != currentUser.displayName.orEmpty().trim()) {
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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 android.net.Uri
|
||||||
import io.element.android.libraries.architecture.Async
|
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 io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class UserPreferencesState(
|
data class EditUserProfileState(
|
||||||
val userId: UserId?,
|
val userId: UserId?,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val userAvatarUrl: Uri?,
|
val userAvatarUrl: Uri?,
|
||||||
val avatarActions: ImmutableList<AvatarAction>,
|
val avatarActions: ImmutableList<AvatarAction>,
|
||||||
val saveButtonEnabled: Boolean,
|
val saveButtonEnabled: Boolean,
|
||||||
val saveAction: Async<Unit>,
|
val saveAction: Async<Unit>,
|
||||||
val eventSink: (UserPreferencesEvents) -> Unit
|
val eventSink: (EditUserProfileEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
@ -14,22 +14,22 @@
|
||||||
* limitations under the License.
|
* 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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
open class UserPreferencesStateProvider : PreviewParameterProvider<UserPreferencesState> {
|
open class EditUserProfileStateProvider : PreviewParameterProvider<EditUserProfileState> {
|
||||||
override val values: Sequence<UserPreferencesState>
|
override val values: Sequence<EditUserProfileState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aUserPreferencesState(),
|
aEditUserProfileState(),
|
||||||
// Add other states here
|
// Add other states here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aUserPreferencesState() = UserPreferencesState(
|
fun aEditUserProfileState() = EditUserProfileState(
|
||||||
userId = UserId("@john.doe:matrix.org"),
|
userId = UserId("@john.doe:matrix.org"),
|
||||||
displayName = "John Doe",
|
displayName = "John Doe",
|
||||||
userAvatarUrl = null,
|
userAvatarUrl = null,
|
||||||
|
|
@ -14,71 +14,59 @@
|
||||||
* limitations under the License.
|
* 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.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.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.ModalBottomSheetValue
|
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.material.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.focus.FocusManager
|
import androidx.compose.ui.focus.FocusManager
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.stringResource
|
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.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
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.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.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.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
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.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
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.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.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun UserPreferencesView(
|
fun EditUserProfileView(
|
||||||
state: UserPreferencesState,
|
state: EditUserProfileState,
|
||||||
onBackPressed: () -> Unit,
|
onBackPressed: () -> Unit,
|
||||||
onProfileEdited: () -> Unit,
|
onProfileEdited: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -102,7 +90,7 @@ fun UserPreferencesView(
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = "Edit profile",
|
text = stringResource(R.string.screen_edit_profile_title),
|
||||||
style = ElementTheme.typography.aliasScreenTitle,
|
style = ElementTheme.typography.aliasScreenTitle,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -113,7 +101,7 @@ fun UserPreferencesView(
|
||||||
enabled = state.saveButtonEnabled,
|
enabled = state.saveButtonEnabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
state.eventSink(UserPreferencesEvents.Save)
|
state.eventSink(EditUserProfileEvents.Save)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -129,33 +117,50 @@ fun UserPreferencesView(
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
EditableAvatarView(state, ::onAvatarClicked)
|
EditableAvatarView(
|
||||||
Spacer(modifier = Modifier.height(60.dp))
|
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(
|
LabelledOutlinedTextField(
|
||||||
label = "Display name",
|
label = stringResource(R.string.screen_edit_profile_display_name),
|
||||||
value = state.displayName,
|
value = state.displayName,
|
||||||
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
|
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
onValueChange = { state.eventSink(UserPreferencesEvents.UpdateDisplayName(it)) },
|
onValueChange = { state.eventSink(EditUserProfileEvents.UpdateDisplayName(it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AvatarActionBottomSheet(
|
AvatarActionBottomSheet(
|
||||||
actions = state.avatarActions,
|
actions = state.avatarActions,
|
||||||
modalBottomSheetState = itemActionsBottomSheetState,
|
modalBottomSheetState = itemActionsBottomSheetState,
|
||||||
onActionSelected = { state.eventSink(UserPreferencesEvents.HandleAvatarAction(it)) }
|
onActionSelected = { state.eventSink(EditUserProfileEvents.HandleAvatarAction(it)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
when (state.saveAction) {
|
when (state.saveAction) {
|
||||||
is Async.Loading -> {
|
is Async.Loading -> {
|
||||||
ProgressDialog(text = "Updating profile...")
|
ProgressDialog(text = stringResource(R.string.screen_edit_profile_updating_details))
|
||||||
}
|
}
|
||||||
|
|
||||||
is Async.Failure -> {
|
is Async.Failure -> {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
content = "Error updating profile",
|
title = stringResource(R.string.screen_edit_profile_error_title),
|
||||||
onDismiss = { state.eventSink(UserPreferencesEvents.CancelSaveChanges) },
|
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 =
|
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
|
||||||
pointerInput(Unit) {
|
this.pointerInput(Unit) {
|
||||||
detectTapGestures(onTap = {
|
detectTapGestures(onTap = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
})
|
})
|
||||||
|
|
@ -253,17 +184,17 @@ private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun UserPreferencesViewLightPreview(@PreviewParameter(UserPreferencesStateProvider::class) state: UserPreferencesState) =
|
fun EditUserProfileViewLightPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) =
|
||||||
ElementPreviewLight { ContentToPreview(state) }
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun UserPreferencesViewDarkPreview(@PreviewParameter(UserPreferencesStateProvider::class) state: UserPreferencesState) =
|
fun EditUserProfileViewDarkPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) =
|
||||||
ElementPreviewDark { ContentToPreview(state) }
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(state: UserPreferencesState) {
|
private fun ContentToPreview(state: EditUserProfileState) {
|
||||||
UserPreferencesView(
|
EditUserProfileView(
|
||||||
onBackPressed = {},
|
onBackPressed = {},
|
||||||
onProfileEdited = {},
|
onProfileEdited = {},
|
||||||
state = state,
|
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
|
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.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.ModalBottomSheetValue
|
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.material.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.focus.FocusManager
|
import androidx.compose.ui.focus.FocusManager
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
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.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.LabelledTextField
|
import io.element.android.libraries.designsystem.components.LabelledTextField
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
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.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
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.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
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.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.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -134,7 +121,14 @@ fun RoomDetailsEditView(
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
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))
|
Spacer(modifier = Modifier.height(60.dp))
|
||||||
|
|
||||||
if (state.canChangeName) {
|
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
|
@Composable
|
||||||
private fun LabelledReadOnlyField(
|
private fun LabelledReadOnlyField(
|
||||||
title: String,
|
title: String,
|
||||||
|
|
@ -279,7 +223,7 @@ private fun LabelledReadOnlyField(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
|
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
|
||||||
pointerInput(Unit) {
|
this.pointerInput(Unit) {
|
||||||
detectTapGestures(onTap = {
|
detectTapGestures(onTap = {
|
||||||
focusManager.clearFocus()
|
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),
|
RoomInviteItem(52.dp),
|
||||||
InviteSender(16.dp),
|
InviteSender(16.dp),
|
||||||
|
|
||||||
|
EditRoomDetails(70.dp),
|
||||||
|
|
||||||
NotificationsOptIn(32.dp),
|
NotificationsOptIn(32.dp),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ anvil {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.libraries.rustsdk)
|
// implementation(projects.libraries.rustsdk)
|
||||||
// implementation(libs.matrix.sdk)
|
implementation(libs.matrix.sdk)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.di)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.network)
|
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": [
|
"includeRegex": [
|
||||||
"screen_create_poll_.*"
|
"screen_create_poll_.*"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ":features:preferences:impl",
|
||||||
|
"includeRegex": [
|
||||||
|
"screen_edit_profile_.*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue