Browse Source

Cleanup and compact code. Also prefer usage of DayNightPreview.

pull/1351/head
Benoit Marty 1 year ago
parent
commit
f7f9a78101
  1. 6
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt
  2. 26
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt
  3. 95
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt
  4. 2
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt
  5. 17
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt

6
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt

@ -95,8 +95,8 @@ class EditUserProfilePresenter @AssistedInject constructor(
} }
val canSave = remember(userDisplayName, userAvatarUri) { val canSave = remember(userDisplayName, userAvatarUri) {
val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser) val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser) ||
|| hasAvatarUrlChanged(userAvatarUri, matrixUser) hasAvatarUrlChanged(userAvatarUri, matrixUser)
!userDisplayName.isNullOrBlank() && hasProfileChanged !userDisplayName.isNullOrBlank() && hasProfileChanged
} }
@ -139,6 +139,6 @@ class EditUserProfilePresenter @AssistedInject constructor(
} else { } else {
matrixClient.removeAvatar().getOrThrow() matrixClient.removeAvatar().getOrThrow()
} }
}.onFailure { it.printStackTrace() } }.onFailure { Timber.e(it, "Unable to update avatar") }
} }
} }

26
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt

@ -40,7 +40,6 @@ 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.text.style.TextAlign
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.features.preferences.impl.R
@ -50,8 +49,8 @@ import io.element.android.libraries.designsystem.components.ProgressDialog
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.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.aliasScreenTitle
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
@ -135,7 +134,6 @@ fun EditUserProfileView(
) )
} }
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(40.dp))
LabelledOutlinedTextField( LabelledOutlinedTextField(
label = stringResource(R.string.screen_edit_profile_display_name), label = stringResource(R.string.screen_edit_profile_display_name),
value = state.displayName, value = state.displayName,
@ -155,7 +153,6 @@ fun EditUserProfileView(
is Async.Loading -> { is Async.Loading -> {
ProgressDialog(text = stringResource(R.string.screen_edit_profile_updating_details)) ProgressDialog(text = stringResource(R.string.screen_edit_profile_updating_details))
} }
is Async.Failure -> { is Async.Failure -> {
ErrorDialog( ErrorDialog(
title = stringResource(R.string.screen_edit_profile_error_title), title = stringResource(R.string.screen_edit_profile_error_title),
@ -163,40 +160,31 @@ fun EditUserProfileView(
onDismiss = { state.eventSink(EditUserProfileEvents.CancelSaveChanges) }, onDismiss = { state.eventSink(EditUserProfileEvents.CancelSaveChanges) },
) )
} }
is Async.Success -> { is Async.Success -> {
LaunchedEffect(state.saveAction) { LaunchedEffect(state.saveAction) {
onProfileEdited() onProfileEdited()
} }
} }
else -> Unit else -> Unit
} }
} }
} }
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
this.pointerInput(Unit) { pointerInput(Unit) {
detectTapGestures(onTap = { detectTapGestures(onTap = {
focusManager.clearFocus() focusManager.clearFocus()
}) })
} }
@Preview @DayNightPreviews
@Composable @Composable
fun EditUserProfileViewLightPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) = internal fun EditUserProfileViewPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) =
ElementPreviewLight { ContentToPreview(state) } ElementPreview {
@Preview
@Composable
fun EditUserProfileViewDarkPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: EditUserProfileState) {
EditUserProfileView( EditUserProfileView(
onBackPressed = {}, onBackPressed = {},
onProfileEdited = {}, onProfileEdited = {},
state = state, state = state,
) )
} }

95
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt

@ -74,7 +74,7 @@ class EditUserProfilePresenterTest {
unmockkAll() unmockkAll()
} }
private fun anEditUserProfilePresenter( private fun createEditUserProfilePresenter(
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(),
matrixUser: MatrixUser = aMatrixUser(), matrixUser: MatrixUser = aMatrixUser(),
): EditUserProfilePresenter { ): EditUserProfilePresenter {
@ -89,8 +89,7 @@ class EditUserProfilePresenterTest {
@Test @Test
fun `present - initial state is created from room info`() = runTest { fun `present - initial state is created from room info`() = runTest {
val user = aMatrixUser(avatarUrl = AN_AVATAR_URL) val user = aMatrixUser(avatarUrl = AN_AVATAR_URL)
val presenter = anEditUserProfilePresenter(matrixUser = user) val presenter = createEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -111,27 +110,23 @@ class EditUserProfilePresenterTest {
@Test @Test
fun `present - updates state in response to changes`() = runTest { fun `present - updates state in response to changes`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val presenter = anEditUserProfilePresenter(matrixUser = user) val presenter = createEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.displayName).isEqualTo("Name") assertThat(initialState.displayName).isEqualTo("Name")
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri)
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II"))
awaitItem().apply { awaitItem().apply {
assertThat(displayName).isEqualTo("Name II") assertThat(displayName).isEqualTo("Name II")
assertThat(userAvatarUrl).isEqualTo(userAvatarUri) assertThat(userAvatarUrl).isEqualTo(userAvatarUri)
} }
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name III")) initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name III"))
awaitItem().apply { awaitItem().apply {
assertThat(displayName).isEqualTo("Name III") assertThat(displayName).isEqualTo("Name III")
assertThat(userAvatarUrl).isEqualTo(userAvatarUri) assertThat(userAvatarUrl).isEqualTo(userAvatarUri)
} }
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply { awaitItem().apply {
assertThat(displayName).isEqualTo("Name III") assertThat(displayName).isEqualTo("Name III")
@ -143,17 +138,13 @@ class EditUserProfilePresenterTest {
@Test @Test
fun `present - obtains avatar uris from gallery`() = runTest { fun `present - obtains avatar uris from gallery`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
fakePickerProvider.givenResult(anotherAvatarUri) fakePickerProvider.givenResult(anotherAvatarUri)
val presenter = createEditUserProfilePresenter(matrixUser = user)
val presenter = anEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri)
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply { awaitItem().apply {
assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri) assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri)
@ -164,17 +155,13 @@ class EditUserProfilePresenterTest {
@Test @Test
fun `present - obtains avatar uris from camera`() = runTest { fun `present - obtains avatar uris from camera`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
fakePickerProvider.givenResult(anotherAvatarUri) fakePickerProvider.givenResult(anotherAvatarUri)
val presenter = createEditUserProfilePresenter(matrixUser = user)
val presenter = anEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri)
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto))
awaitItem().apply { awaitItem().apply {
assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri) assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri)
@ -185,35 +172,28 @@ class EditUserProfilePresenterTest {
@Test @Test
fun `present - updates save button state`() = runTest { fun `present - updates save button state`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
fakePickerProvider.givenResult(userAvatarUri) fakePickerProvider.givenResult(userAvatarUri)
val presenter = createEditUserProfilePresenter(matrixUser = user)
val presenter = anEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.saveButtonEnabled).isEqualTo(false) assertThat(initialState.saveButtonEnabled).isEqualTo(false)
// Once a change is made, the save button is enabled // Once a change is made, the save button is enabled
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isEqualTo(true) assertThat(saveButtonEnabled).isEqualTo(true)
} }
// If it's reverted then the save disables again // If it's reverted then the save disables again
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name")) initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isEqualTo(false) assertThat(saveButtonEnabled).isEqualTo(false)
} }
// Make a change... // Make a change...
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isEqualTo(true) assertThat(saveButtonEnabled).isEqualTo(true)
} }
// Revert it... // Revert it...
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply { awaitItem().apply {
@ -225,35 +205,28 @@ class EditUserProfilePresenterTest {
@Test @Test
fun `present - updates save button state when initial values are null`() = runTest { fun `present - updates save button state when initial values are null`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = null) val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = null)
fakePickerProvider.givenResult(userAvatarUri) fakePickerProvider.givenResult(userAvatarUri)
val presenter = createEditUserProfilePresenter(matrixUser = user)
val presenter = anEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.saveButtonEnabled).isEqualTo(false) assertThat(initialState.saveButtonEnabled).isEqualTo(false)
// Once a change is made, the save button is enabled // Once a change is made, the save button is enabled
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isEqualTo(true) assertThat(saveButtonEnabled).isEqualTo(true)
} }
// If it's reverted then the save disables again // If it's reverted then the save disables again
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("fallback")) initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("fallback"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isEqualTo(false) assertThat(saveButtonEnabled).isEqualTo(false)
} }
// Make a change... // Make a change...
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isEqualTo(true) assertThat(saveButtonEnabled).isEqualTo(true)
} }
// Revert it... // Revert it...
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply { awaitItem().apply {
@ -266,26 +239,21 @@ class EditUserProfilePresenterTest {
fun `present - save changes room details if different`() = runTest { fun `present - save changes room details if different`() = runTest {
val matrixClient = FakeMatrixClient() val matrixClient = FakeMatrixClient()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val presenter = createEditUserProfilePresenter(
val presenter = anEditUserProfilePresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
matrixUser = user matrixUser = user
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("New name")) initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("New name"))
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvents.Save)
skipItems(5) skipItems(5)
assertThat(matrixClient.setDisplayNameCalled).isTrue() assertThat(matrixClient.setDisplayNameCalled).isTrue()
assertThat(matrixClient.removeAvatarCalled).isTrue() assertThat(matrixClient.removeAvatarCalled).isTrue()
assertThat(matrixClient.uploadAvatarCalled).isFalse() assertThat(matrixClient.uploadAvatarCalled).isFalse()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@ -294,24 +262,19 @@ class EditUserProfilePresenterTest {
fun `present - save doesn't change room details if they're the same trimmed`() = runTest { fun `present - save doesn't change room details if they're the same trimmed`() = runTest {
val matrixClient = FakeMatrixClient() val matrixClient = FakeMatrixClient()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val presenter = createEditUserProfilePresenter(
val presenter = anEditUserProfilePresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
matrixUser = user matrixUser = user
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(" Name ")) initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(" Name "))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvents.Save)
assertThat(matrixClient.setDisplayNameCalled).isTrue() assertThat(matrixClient.setDisplayNameCalled).isTrue()
assertThat(matrixClient.uploadAvatarCalled).isFalse() assertThat(matrixClient.uploadAvatarCalled).isFalse()
assertThat(matrixClient.removeAvatarCalled).isFalse() assertThat(matrixClient.removeAvatarCalled).isFalse()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@ -320,24 +283,19 @@ class EditUserProfilePresenterTest {
fun `present - save doesn't change name if it's now empty`() = runTest { fun `present - save doesn't change name if it's now empty`() = runTest {
val matrixClient = FakeMatrixClient() val matrixClient = FakeMatrixClient()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val presenter = createEditUserProfilePresenter(
val presenter = anEditUserProfilePresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
matrixUser = user matrixUser = user
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("")) initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(""))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvents.Save)
assertThat(matrixClient.setDisplayNameCalled).isFalse() assertThat(matrixClient.setDisplayNameCalled).isFalse()
assertThat(matrixClient.uploadAvatarCalled).isFalse() assertThat(matrixClient.uploadAvatarCalled).isFalse()
assertThat(matrixClient.removeAvatarCalled).isFalse() assertThat(matrixClient.removeAvatarCalled).isFalse()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@ -346,23 +304,18 @@ class EditUserProfilePresenterTest {
fun `present - save processes and sets avatar when processor returns successfully`() = runTest { fun `present - save processes and sets avatar when processor returns successfully`() = runTest {
val matrixClient = FakeMatrixClient() val matrixClient = FakeMatrixClient()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
givenPickerReturnsFile() givenPickerReturnsFile()
val presenter = createEditUserProfilePresenter(
val presenter = anEditUserProfilePresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
matrixUser = user matrixUser = user
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvents.Save)
skipItems(2) skipItems(2)
assertThat(matrixClient.uploadAvatarCalled).isTrue() assertThat(matrixClient.uploadAvatarCalled).isTrue()
} }
} }
@ -371,26 +324,20 @@ class EditUserProfilePresenterTest {
fun `present - save does not set avatar data if processor fails`() = runTest { fun `present - save does not set avatar data if processor fails`() = runTest {
val matrixClient = FakeMatrixClient() val matrixClient = FakeMatrixClient()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val presenter = createEditUserProfilePresenter(
val presenter = anEditUserProfilePresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
matrixUser = user matrixUser = user
) )
fakePickerProvider.givenResult(anotherAvatarUri) fakePickerProvider.givenResult(anotherAvatarUri)
fakeMediaPreProcessor.givenResult(Result.failure(Throwable("Oh no"))) fakeMediaPreProcessor.givenResult(Result.failure(Throwable("Oh no")))
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvents.Save)
skipItems(2) skipItems(2)
assertThat(matrixClient.uploadAvatarCalled).isFalse() assertThat(matrixClient.uploadAvatarCalled).isFalse()
assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java)
} }
} }
@ -401,7 +348,6 @@ class EditUserProfilePresenterTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
givenSetDisplayNameResult(Result.failure(Throwable("!"))) givenSetDisplayNameResult(Result.failure(Throwable("!")))
} }
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.UpdateDisplayName("New name")) saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.UpdateDisplayName("New name"))
} }
@ -411,61 +357,49 @@ class EditUserProfilePresenterTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
givenRemoveAvatarResult(Result.failure(Throwable("!"))) givenRemoveAvatarResult(Result.failure(Throwable("!")))
} }
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
} }
@Test @Test
fun `present - sets save action to failure if setting avatar fails`() = runTest { fun `present - sets save action to failure if setting avatar fails`() = runTest {
givenPickerReturnsFile() givenPickerReturnsFile()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
givenUploadAvatarResult(Result.failure(Throwable("!"))) givenUploadAvatarResult(Result.failure(Throwable("!")))
} }
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
} }
@Test @Test
fun `present - CancelSaveChanges resets save action state`() = runTest { fun `present - CancelSaveChanges resets save action state`() = runTest {
givenPickerReturnsFile() givenPickerReturnsFile()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
givenSetDisplayNameResult(Result.failure(Throwable("!"))) givenSetDisplayNameResult(Result.failure(Throwable("!")))
} }
val presenter = createEditUserProfilePresenter(matrixUser = user)
val presenter = anEditUserProfilePresenter(matrixUser = user)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("foo")) initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("foo"))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvents.Save)
skipItems(2) skipItems(2)
assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java)
initialState.eventSink(EditUserProfileEvents.CancelSaveChanges) initialState.eventSink(EditUserProfileEvents.CancelSaveChanges)
assertThat(awaitItem().saveAction).isInstanceOf(Async.Uninitialized::class.java) assertThat(awaitItem().saveAction).isInstanceOf(Async.Uninitialized::class.java)
} }
} }
private suspend fun saveAndAssertFailure(matrixUser: MatrixUser, matrixClient: MatrixClient, event: EditUserProfileEvents) { private suspend fun saveAndAssertFailure(matrixUser: MatrixUser, matrixClient: MatrixClient, event: EditUserProfileEvents) {
val presenter = anEditUserProfilePresenter(matrixUser = matrixUser, matrixClient = matrixClient) val presenter = createEditUserProfilePresenter(matrixUser = matrixUser, matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(event) initialState.eventSink(event)
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvents.Save)
skipItems(1) skipItems(1)
assertThat(awaitItem().saveAction).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().saveAction).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java)
} }
@ -476,7 +410,6 @@ class EditUserProfilePresenterTest {
val processedFile: File = mockk { val processedFile: File = mockk {
every { readBytes() } returns fakeFileContents every { readBytes() } returns fakeFileContents
} }
fakePickerProvider.givenResult(anotherAvatarUri) fakePickerProvider.givenResult(anotherAvatarUri)
fakeMediaPreProcessor.givenResult( fakeMediaPreProcessor.givenResult(
Result.success( Result.success(

2
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt

@ -223,7 +223,7 @@ private fun LabelledReadOnlyField(
} }
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
this.pointerInput(Unit) { pointerInput(Unit) {
detectTapGestures(onTap = { detectTapGestures(onTap = {
focusManager.clearFocus() focusManager.clearFocus()
}) })

17
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt

@ -24,10 +24,9 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.ElementTheme
@ -66,16 +65,9 @@ fun LabelledOutlinedTextField(
} }
} }
@Preview @DayNightPreviews
@Composable @Composable
internal fun LabelledOutlinedTextFieldLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun LabelledOutlinedTextFieldPreview() = ElementPreview {
@Preview
@Composable
internal fun LabelledOutlinedTextFieldDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
Column { Column {
LabelledOutlinedTextField( LabelledOutlinedTextField(
label = "Room name", label = "Room name",
@ -89,3 +81,4 @@ private fun ContentToPreview() {
) )
} }
} }

Loading…
Cancel
Save