Merge pull request #5508 from element-hq/feature/bma/noUriInComposeState

Replace Uri by String in States that are used in Composable function.
This commit is contained in:
Benoit Marty 2025-10-13 15:32:39 +02:00 committed by GitHub
commit 7ad659a4c8
16 changed files with 78 additions and 87 deletions

View file

@ -17,6 +17,7 @@ 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.core.net.toUri
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -157,7 +158,7 @@ class ConfigureRoomPresenter(
createRoomAction: MutableState<AsyncAction<RoomId>> createRoomAction: MutableState<AsyncAction<RoomId>>
) = launch { ) = launch {
suspend { suspend {
val avatarUrl = config.avatarUri?.let { uploadAvatar(it) } val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) }
val params = if (config.roomVisibility is RoomVisibilityState.Public) { val params = if (config.roomVisibility is RoomVisibilityState.Public) {
CreateRoomParameters( CreateRoomParameters(
name = config.roomName, name = config.roomName,

View file

@ -7,7 +7,6 @@
package io.element.android.features.createroom.impl.configureroom package io.element.android.features.createroom.impl.configureroom
import android.net.Uri
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -192,7 +191,7 @@ private fun ConfigureRoomToolbar(
@Composable @Composable
private fun RoomNameWithAvatar( private fun RoomNameWithAvatar(
avatarUri: Uri?, avatarUri: String?,
roomName: String, roomName: String,
onAvatarClick: () -> Unit, onAvatarClick: () -> Unit,
onChangeRoomName: (String) -> Unit, onChangeRoomName: (String) -> Unit,

View file

@ -7,18 +7,14 @@
package io.element.android.features.createroom.impl.configureroom package io.element.android.features.createroom.impl.configureroom
import android.net.Uri
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
// Annotate with @Immutable since `Uri` is unstable
@Immutable
data class CreateRoomConfig( data class CreateRoomConfig(
val roomName: String? = null, val roomName: String? = null,
val topic: String? = null, val topic: String? = null,
val avatarUri: Uri? = null, val avatarUri: String? = null,
val invites: ImmutableList<MatrixUser> = persistentListOf(), val invites: ImmutableList<MatrixUser> = persistentListOf(),
val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private, val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
) )

View file

@ -62,7 +62,7 @@ class CreateRoomConfigStore(
fun setAvatarUri(uri: Uri?, cached: Boolean = false) { fun setAvatarUri(uri: Uri?, cached: Boolean = false) {
cachedAvatarUri = uri.takeIf { cached } cachedAvatarUri = uri.takeIf { cached }
createRoomConfigFlow.getAndUpdate { config -> createRoomConfigFlow.getAndUpdate { config ->
config.copy(avatarUri = uri) config.copy(avatarUri = uri?.toString())
} }
} }

View file

@ -8,6 +8,7 @@
package io.element.android.features.startchat.impl.configureroom package io.element.android.features.startchat.impl.configureroom
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri
import app.cash.turbine.TurbineTestContext import app.cash.turbine.TurbineTestContext
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.CreatedRoom
@ -155,15 +156,15 @@ class ConfigureRoomPresenterTest {
// Pick avatar // Pick avatar
pickerProvider.givenResult(null) pickerProvider.givenResult(null)
// From gallery // From gallery
val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY) val uriFromGallery = AN_URI_FROM_GALLERY
pickerProvider.givenResult(uriFromGallery) pickerProvider.givenResult(uriFromGallery.toUri())
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
newState = awaitItem() newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery) expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
assertThat(newState.config).isEqualTo(expectedConfig) assertThat(newState.config).isEqualTo(expectedConfig)
// From camera // From camera
val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA) val uriFromCamera = AN_URI_FROM_CAMERA
pickerProvider.givenResult(uriFromCamera) pickerProvider.givenResult(uriFromCamera.toUri())
assertThat(newState.cameraPermissionState.permissionGranted).isFalse() assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem() newState = awaitItem()
@ -175,8 +176,8 @@ class ConfigureRoomPresenterTest {
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera) expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera)
assertThat(newState.config).isEqualTo(expectedConfig) assertThat(newState.config).isEqualTo(expectedConfig)
// Do it again, no permission is requested // Do it again, no permission is requested
val uriFromCamera2 = Uri.parse(AN_URI_FROM_CAMERA_2) val uriFromCamera2 = AN_URI_FROM_CAMERA_2
pickerProvider.givenResult(uriFromCamera2) pickerProvider.givenResult(uriFromCamera2.toUri())
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem() newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2) expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)

View file

@ -62,21 +62,21 @@ class EditUserProfilePresenter(
@Composable @Composable
override fun present(): EditUserProfileState { override fun present(): EditUserProfileState {
val cameraPermissionState = cameraPermissionPresenter.present() val cameraPermissionState = cameraPermissionPresenter.present()
var userAvatarUri by rememberSaveable { mutableStateOf(matrixUser.avatarUrl?.toUri()) } var userAvatarUri by rememberSaveable { mutableStateOf(matrixUser.avatarUrl) }
var userDisplayName by rememberSaveable { mutableStateOf(matrixUser.displayName) } var userDisplayName by rememberSaveable { mutableStateOf(matrixUser.displayName) }
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
onResult = { uri -> onResult = { uri ->
if (uri != null) { if (uri != null) {
temporaryUriDeleter.delete(userAvatarUri) temporaryUriDeleter.delete(userAvatarUri?.toUri())
userAvatarUri = uri userAvatarUri = uri.toString()
} }
} }
) )
val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker( val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker(
onResult = { uri -> onResult = { uri ->
if (uri != null) { if (uri != null) {
temporaryUriDeleter.delete(userAvatarUri) temporaryUriDeleter.delete(userAvatarUri?.toUri())
userAvatarUri = uri userAvatarUri = uri.toString()
} }
} }
) )
@ -102,7 +102,12 @@ class EditUserProfilePresenter(
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
fun handleEvents(event: EditUserProfileEvents) { fun handleEvents(event: EditUserProfileEvents) {
when (event) { when (event) {
is EditUserProfileEvents.Save -> localCoroutineScope.saveChanges(userDisplayName, userAvatarUri, matrixUser, saveAction) is EditUserProfileEvents.Save -> localCoroutineScope.saveChanges(
name = userDisplayName,
avatarUri = userAvatarUri?.toUri(),
currentUser = matrixUser,
action = saveAction,
)
is EditUserProfileEvents.HandleAvatarAction -> { is EditUserProfileEvents.HandleAvatarAction -> {
when (event.action) { when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch() AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
@ -113,7 +118,7 @@ class EditUserProfilePresenter(
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions) cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
} }
AvatarAction.Remove -> { AvatarAction.Remove -> {
temporaryUriDeleter.delete(userAvatarUri) temporaryUriDeleter.delete(userAvatarUri?.toUri())
userAvatarUri = null userAvatarUri = null
} }
} }
@ -145,9 +150,8 @@ class EditUserProfilePresenter(
private fun hasDisplayNameChanged(name: String?, currentUser: MatrixUser) = private fun hasDisplayNameChanged(name: String?, currentUser: MatrixUser) =
name?.trim() != currentUser.displayName?.trim() name?.trim() != currentUser.displayName?.trim()
private fun hasAvatarUrlChanged(avatarUri: Uri?, currentUser: MatrixUser) = private fun hasAvatarUrlChanged(avatarUri: String?, currentUser: MatrixUser) =
// Need to call `toUri()?.toString()` to make the test pass (we mockk Uri) avatarUri?.trim() != currentUser.avatarUrl?.trim()
avatarUri?.toString()?.trim() != currentUser.avatarUrl?.toUri()?.toString()?.trim()
private fun CoroutineScope.saveChanges( private fun CoroutineScope.saveChanges(
name: String?, name: String?,

View file

@ -7,20 +7,16 @@
package io.element.android.features.preferences.impl.user.editprofile package io.element.android.features.preferences.impl.user.editprofile
import android.net.Uri
import androidx.compose.runtime.Immutable
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.UserId 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 io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.PermissionsState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
// Annotate with @Immutable since `Uri` is unstable
@Immutable
data class EditUserProfileState( data class EditUserProfileState(
val userId: UserId, val userId: UserId,
val displayName: String, val displayName: String,
val userAvatarUrl: Uri?, val userAvatarUrl: String?,
val avatarActions: ImmutableList<AvatarAction>, val avatarActions: ImmutableList<AvatarAction>,
val saveButtonEnabled: Boolean, val saveButtonEnabled: Boolean,
val saveAction: AsyncAction<Unit>, val saveAction: AsyncAction<Unit>,

View file

@ -7,9 +7,7 @@
package io.element.android.features.preferences.impl.user.editprofile package io.element.android.features.preferences.impl.user.editprofile
import android.net.Uri
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.core.net.toUri
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.permissions.api.aPermissionsState import io.element.android.libraries.permissions.api.aPermissionsState
@ -19,13 +17,13 @@ open class EditUserProfileStateProvider : PreviewParameterProvider<EditUserProfi
override val values: Sequence<EditUserProfileState> override val values: Sequence<EditUserProfileState>
get() = sequenceOf( get() = sequenceOf(
aEditUserProfileState(), aEditUserProfileState(),
aEditUserProfileState(userAvatarUrl = "example://uri".toUri()), aEditUserProfileState(userAvatarUrl = "example://uri"),
// Add other states here // Add other states here
) )
} }
fun aEditUserProfileState( fun aEditUserProfileState(
userAvatarUrl: Uri? = null, userAvatarUrl: String? = null,
) = EditUserProfileState( ) = EditUserProfileState(
userId = UserId("@john.doe:matrix.org"), userId = UserId("@john.doe:matrix.org"),
displayName = "John Doe", displayName = "John Doe",

View file

@ -66,7 +66,9 @@ class EditUserProfilePresenterTest {
mockkStatic(Uri::class) mockkStatic(Uri::class)
every { Uri.parse(AN_AVATAR_URL) } returns userAvatarUri every { Uri.parse(AN_AVATAR_URL) } returns userAvatarUri
every { userAvatarUri.toString() } returns AN_AVATAR_URL
every { Uri.parse(ANOTHER_AVATAR_URL) } returns anotherAvatarUri every { Uri.parse(ANOTHER_AVATAR_URL) } returns anotherAvatarUri
every { anotherAvatarUri.toString() } returns ANOTHER_AVATAR_URL
} }
@After @After
@ -102,7 +104,7 @@ class EditUserProfilePresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.userId).isEqualTo(user.userId) assertThat(initialState.userId).isEqualTo(user.userId)
assertThat(initialState.displayName).isEqualTo(user.displayName) assertThat(initialState.displayName).isEqualTo(user.displayName)
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) assertThat(initialState.userAvatarUrl).isEqualTo(AN_AVATAR_URL)
assertThat(initialState.avatarActions).containsExactly( assertThat(initialState.avatarActions).containsExactly(
AvatarAction.ChoosePhoto, AvatarAction.ChoosePhoto,
AvatarAction.TakePhoto, AvatarAction.TakePhoto,
@ -127,16 +129,16 @@ class EditUserProfilePresenterTest {
}.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(AN_AVATAR_URL)
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(AN_AVATAR_URL)
} }
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(AN_AVATAR_URL)
} }
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply { awaitItem().apply {
@ -160,10 +162,10 @@ class EditUserProfilePresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) assertThat(initialState.userAvatarUrl).isEqualTo(AN_AVATAR_URL)
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply { awaitItem().apply {
assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri) assertThat(userAvatarUrl).isEqualTo(ANOTHER_AVATAR_URL)
} }
} }
} }
@ -185,7 +187,7 @@ class EditUserProfilePresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) assertThat(initialState.userAvatarUrl).isEqualTo(AN_AVATAR_URL)
assertThat(initialState.cameraPermissionState.permissionGranted).isFalse() assertThat(initialState.cameraPermissionState.permissionGranted).isFalse()
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto)) initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto))
val stateWithAskingPermission = awaitItem() val stateWithAskingPermission = awaitItem()
@ -194,12 +196,12 @@ class EditUserProfilePresenterTest {
val stateWithPermission = awaitItem() val stateWithPermission = awaitItem()
assertThat(stateWithPermission.cameraPermissionState.permissionGranted).isTrue() assertThat(stateWithPermission.cameraPermissionState.permissionGranted).isTrue()
val stateWithNewAvatar = awaitItem() val stateWithNewAvatar = awaitItem()
assertThat(stateWithNewAvatar.userAvatarUrl).isEqualTo(anotherAvatarUri) assertThat(stateWithNewAvatar.userAvatarUrl).isEqualTo(ANOTHER_AVATAR_URL)
// Do it again, no permission is requested // Do it again, no permission is requested
fakePickerProvider.givenResult(userAvatarUri) fakePickerProvider.givenResult(userAvatarUri)
stateWithNewAvatar.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto)) stateWithNewAvatar.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto))
val stateWithNewAvatar2 = awaitItem() val stateWithNewAvatar2 = awaitItem()
assertThat(stateWithNewAvatar2.userAvatarUrl).isEqualTo(userAvatarUri) assertThat(stateWithNewAvatar2.userAvatarUrl).isEqualTo(AN_AVATAR_URL)
deleteCallback.assertions().isCalledExactly(2).withSequence( deleteCallback.assertions().isCalledExactly(2).withSequence(
listOf(value(userAvatarUri)), listOf(value(userAvatarUri)),
listOf(value(anotherAvatarUri)), listOf(value(anotherAvatarUri)),

View file

@ -61,11 +61,11 @@ class RoomDetailsEditPresenter(
val cameraPermissionState = cameraPermissionPresenter.present() val cameraPermissionState = cameraPermissionPresenter.present()
val roomSyncUpdateFlow = room.syncUpdateFlow.collectAsState() val roomSyncUpdateFlow = room.syncUpdateFlow.collectAsState()
val roomAvatarUri = room.avatarUrl()?.toUri() val roomAvatarUri = room.avatarUrl()
var roomAvatarUriEdited by rememberSaveable { mutableStateOf<Uri?>(null) } var roomAvatarUriEdited by rememberSaveable { mutableStateOf<String?>(null) }
LaunchedEffect(roomAvatarUri) { LaunchedEffect(roomAvatarUri) {
// Every time the roomAvatar change (from sync), we can set the new avatar. // Every time the roomAvatar change (from sync), we can set the new avatar.
temporaryUriDeleter.delete(roomAvatarUriEdited) temporaryUriDeleter.delete(roomAvatarUriEdited?.toUri())
roomAvatarUriEdited = roomAvatarUri roomAvatarUriEdited = roomAvatarUri
} }
@ -107,16 +107,16 @@ class RoomDetailsEditPresenter(
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
onResult = { uri -> onResult = { uri ->
if (uri != null) { if (uri != null) {
temporaryUriDeleter.delete(roomAvatarUriEdited) temporaryUriDeleter.delete(roomAvatarUriEdited?.toUri())
roomAvatarUriEdited = uri roomAvatarUriEdited = uri.toString()
} }
} }
) )
val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker( val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker(
onResult = { uri -> onResult = { uri ->
if (uri != null) { if (uri != null) {
temporaryUriDeleter.delete(roomAvatarUriEdited) temporaryUriDeleter.delete(roomAvatarUriEdited?.toUri())
roomAvatarUriEdited = uri roomAvatarUriEdited = uri.toString()
} }
} }
) )
@ -147,8 +147,8 @@ class RoomDetailsEditPresenter(
newNameTrimmed = roomRawNameEdited.trim(), newNameTrimmed = roomRawNameEdited.trim(),
currentTopicTrimmed = roomTopicTrimmed, currentTopicTrimmed = roomTopicTrimmed,
newTopicTrimmed = roomTopicEdited.trim(), newTopicTrimmed = roomTopicEdited.trim(),
currentAvatar = roomAvatarUri, currentAvatar = roomAvatarUri?.toUri(),
newAvatarUri = roomAvatarUriEdited, newAvatarUri = roomAvatarUriEdited?.toUri(),
action = saveAction, action = saveAction,
) )
is RoomDetailsEditEvents.HandleAvatarAction -> { is RoomDetailsEditEvents.HandleAvatarAction -> {
@ -161,7 +161,7 @@ class RoomDetailsEditPresenter(
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions) cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
} }
AvatarAction.Remove -> { AvatarAction.Remove -> {
temporaryUriDeleter.delete(roomAvatarUriEdited) temporaryUriDeleter.delete(roomAvatarUriEdited?.toUri())
roomAvatarUriEdited = null roomAvatarUriEdited = null
} }
} }

View file

@ -7,16 +7,12 @@
package io.element.android.features.roomdetails.impl.edit package io.element.android.features.roomdetails.impl.edit
import android.net.Uri
import androidx.compose.runtime.Immutable
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.PermissionsState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
// Annotate with @Immutable since `Uri` is unstable
@Immutable
data class RoomDetailsEditState( data class RoomDetailsEditState(
val roomId: RoomId, val roomId: RoomId,
/** The raw room name (i.e. the room name from the state event `m.room.name`), not the display name. */ /** The raw room name (i.e. the room name from the state event `m.room.name`), not the display name. */
@ -24,7 +20,7 @@ data class RoomDetailsEditState(
val canChangeName: Boolean, val canChangeName: Boolean,
val roomTopic: String, val roomTopic: String,
val canChangeTopic: Boolean, val canChangeTopic: Boolean,
val roomAvatarUrl: Uri?, val roomAvatarUrl: String?,
val canChangeAvatar: Boolean, val canChangeAvatar: Boolean,
val avatarActions: ImmutableList<AvatarAction>, val avatarActions: ImmutableList<AvatarAction>,
val saveButtonEnabled: Boolean, val saveButtonEnabled: Boolean,

View file

@ -7,9 +7,7 @@
package io.element.android.features.roomdetails.impl.edit package io.element.android.features.roomdetails.impl.edit
import android.net.Uri
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.core.net.toUri
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
@ -23,7 +21,7 @@ open class RoomDetailsEditStateProvider : PreviewParameterProvider<RoomDetailsEd
aRoomDetailsEditState(), aRoomDetailsEditState(),
aRoomDetailsEditState(roomTopic = ""), aRoomDetailsEditState(roomTopic = ""),
aRoomDetailsEditState(roomRawName = ""), aRoomDetailsEditState(roomRawName = ""),
aRoomDetailsEditState(roomAvatarUrl = "example://uri".toUri()), aRoomDetailsEditState(roomAvatarUrl = "example://uri"),
aRoomDetailsEditState(canChangeName = true, canChangeTopic = false, canChangeAvatar = true, saveButtonEnabled = false), aRoomDetailsEditState(canChangeName = true, canChangeTopic = false, canChangeAvatar = true, saveButtonEnabled = false),
aRoomDetailsEditState(canChangeName = false, canChangeTopic = true, canChangeAvatar = false, saveButtonEnabled = false), aRoomDetailsEditState(canChangeName = false, canChangeTopic = true, canChangeAvatar = false, saveButtonEnabled = false),
aRoomDetailsEditState(saveAction = AsyncAction.Loading), aRoomDetailsEditState(saveAction = AsyncAction.Loading),
@ -37,7 +35,7 @@ fun aRoomDetailsEditState(
canChangeName: Boolean = true, canChangeName: Boolean = true,
roomTopic: String = "a room topic that is quite long so should wrap onto multiple lines", roomTopic: String = "a room topic that is quite long so should wrap onto multiple lines",
canChangeTopic: Boolean = true, canChangeTopic: Boolean = true,
roomAvatarUrl: Uri? = null, roomAvatarUrl: String? = null,
canChangeAvatar: Boolean = true, canChangeAvatar: Boolean = true,
avatarActions: List<AvatarAction> = emptyList(), avatarActions: List<AvatarAction> = emptyList(),
saveButtonEnabled: Boolean = true, saveButtonEnabled: Boolean = true,

View file

@ -66,7 +66,9 @@ class RoomDetailsEditPresenterTest {
mockkStatic(Uri::class) mockkStatic(Uri::class)
every { Uri.parse(AN_AVATAR_URL) } returns roomAvatarUri every { Uri.parse(AN_AVATAR_URL) } returns roomAvatarUri
every { roomAvatarUri.toString() } returns AN_AVATAR_URL
every { Uri.parse(ANOTHER_AVATAR_URL) } returns anotherAvatarUri every { Uri.parse(ANOTHER_AVATAR_URL) } returns anotherAvatarUri
every { anotherAvatarUri.toString() } returns ANOTHER_AVATAR_URL
} }
@After @After
@ -107,7 +109,7 @@ class RoomDetailsEditPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.roomId).isEqualTo(room.roomId) assertThat(initialState.roomId).isEqualTo(room.roomId)
assertThat(initialState.roomRawName).isEqualTo(A_ROOM_RAW_NAME) assertThat(initialState.roomRawName).isEqualTo(A_ROOM_RAW_NAME)
assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri) assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
assertThat(initialState.roomTopic).isEqualTo(room.info().topic.orEmpty()) assertThat(initialState.roomTopic).isEqualTo(room.info().topic.orEmpty())
assertThat(initialState.avatarActions).containsExactly( assertThat(initialState.avatarActions).containsExactly(
AvatarAction.ChoosePhoto, AvatarAction.ChoosePhoto,
@ -233,24 +235,24 @@ class RoomDetailsEditPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.roomTopic).isEqualTo("My topic") assertThat(initialState.roomTopic).isEqualTo("My topic")
assertThat(initialState.roomRawName).isEqualTo("Name") assertThat(initialState.roomRawName).isEqualTo("Name")
assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri) assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name II")) initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name II"))
awaitItem().apply { awaitItem().apply {
assertThat(roomTopic).isEqualTo("My topic") assertThat(roomTopic).isEqualTo("My topic")
assertThat(roomRawName).isEqualTo("Name II") assertThat(roomRawName).isEqualTo("Name II")
assertThat(roomAvatarUrl).isEqualTo(roomAvatarUri) assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
} }
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name III")) initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name III"))
awaitItem().apply { awaitItem().apply {
assertThat(roomTopic).isEqualTo("My topic") assertThat(roomTopic).isEqualTo("My topic")
assertThat(roomRawName).isEqualTo("Name III") assertThat(roomRawName).isEqualTo("Name III")
assertThat(roomAvatarUrl).isEqualTo(roomAvatarUri) assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
} }
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("Another topic")) initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("Another topic"))
awaitItem().apply { awaitItem().apply {
assertThat(roomTopic).isEqualTo("Another topic") assertThat(roomTopic).isEqualTo("Another topic")
assertThat(roomRawName).isEqualTo("Name III") assertThat(roomRawName).isEqualTo("Name III")
assertThat(roomAvatarUrl).isEqualTo(roomAvatarUri) assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
} }
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply { awaitItem().apply {
@ -277,10 +279,10 @@ class RoomDetailsEditPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri) assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply { awaitItem().apply {
assertThat(roomAvatarUrl).isEqualTo(anotherAvatarUri) assertThat(roomAvatarUrl).isEqualTo(anotherAvatarUri.toString())
} }
} }
} }
@ -303,7 +305,7 @@ class RoomDetailsEditPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri) assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
assertThat(initialState.cameraPermissionState.permissionGranted).isFalse() assertThat(initialState.cameraPermissionState.permissionGranted).isFalse()
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto)) initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto))
val stateWithAskingPermission = awaitItem() val stateWithAskingPermission = awaitItem()
@ -312,12 +314,12 @@ class RoomDetailsEditPresenterTest {
val stateWithPermission = awaitItem() val stateWithPermission = awaitItem()
assertThat(stateWithPermission.cameraPermissionState.permissionGranted).isTrue() assertThat(stateWithPermission.cameraPermissionState.permissionGranted).isTrue()
val stateWithNewAvatar = awaitItem() val stateWithNewAvatar = awaitItem()
assertThat(stateWithNewAvatar.roomAvatarUrl).isEqualTo(anotherAvatarUri) assertThat(stateWithNewAvatar.roomAvatarUrl).isEqualTo(anotherAvatarUri.toString())
// Do it again, no permission is requested // Do it again, no permission is requested
fakePickerProvider.givenResult(roomAvatarUri) fakePickerProvider.givenResult(roomAvatarUri)
stateWithNewAvatar.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto)) stateWithNewAvatar.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto))
val stateWithNewAvatar2 = awaitItem() val stateWithNewAvatar2 = awaitItem()
assertThat(stateWithNewAvatar2.roomAvatarUrl).isEqualTo(roomAvatarUri) assertThat(stateWithNewAvatar2.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
deleteCallback.assertions().isCalledExactly(3).withSequence( deleteCallback.assertions().isCalledExactly(3).withSequence(
listOf(value(null)), listOf(value(null)),
listOf(value(roomAvatarUri)), listOf(value(roomAvatarUri)),

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.matrix.ui.components package io.element.android.libraries.matrix.ui.components
import android.net.Uri
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
@ -28,7 +27,6 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
@ -47,7 +45,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun EditableAvatarView( fun EditableAvatarView(
matrixId: String, matrixId: String,
displayName: String?, displayName: String?,
avatarUrl: Uri?, avatarUrl: String?,
avatarSize: AvatarSize, avatarSize: AvatarSize,
avatarType: AvatarType, avatarType: AvatarType,
onAvatarClick: () -> Unit, onAvatarClick: () -> Unit,
@ -71,13 +69,13 @@ fun EditableAvatarView(
contentDescription = a11yAvatar contentDescription = a11yAvatar
}, },
) { ) {
when (avatarUrl?.scheme) { when {
null, "mxc" -> { avatarUrl == null || avatarUrl.startsWith("mxc://") -> {
Avatar( Avatar(
avatarData = AvatarData( avatarData = AvatarData(
id = matrixId, id = matrixId,
name = displayName, name = displayName,
url = avatarUrl?.toString(), url = avatarUrl,
size = avatarSize, size = avatarSize,
), ),
avatarType = avatarType, avatarType = avatarType,
@ -114,7 +112,7 @@ fun EditableAvatarView(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun EditableAvatarViewPreview( internal fun EditableAvatarViewPreview(
@PreviewParameter(EditableAvatarViewUriProvider::class) uri: Uri? @PreviewParameter(EditableAvatarViewUriProvider::class) uri: String?
) = ElementPreview( ) = ElementPreview(
drawableFallbackForImages = CommonDrawables.sample_avatar, drawableFallbackForImages = CommonDrawables.sample_avatar,
) { ) {
@ -128,11 +126,11 @@ internal fun EditableAvatarViewPreview(
) )
} }
open class EditableAvatarViewUriProvider : PreviewParameterProvider<Uri?> { open class EditableAvatarViewUriProvider : PreviewParameterProvider<String?> {
override val values: Sequence<Uri?> override val values: Sequence<String?>
get() = sequenceOf( get() = sequenceOf(
null, null,
"mxc://matrix.org/123456".toUri(), "mxc://matrix.org/123456",
"https://example.com/avatar.jpg".toUri(), "https://example.com/avatar.jpg",
) )
} }

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.matrix.ui.components package io.element.android.libraries.matrix.ui.components
import android.net.Uri
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -43,7 +42,7 @@ import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
*/ */
@Composable @Composable
fun UnsavedAvatar( fun UnsavedAvatar(
avatarUri: Uri?, avatarUri: String?,
avatarSize: AvatarSize, avatarSize: AvatarSize,
avatarType: AvatarType, avatarType: AvatarType,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -86,8 +85,8 @@ internal fun UnsavedAvatarPreview() = ElementPreview {
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
UnsavedAvatar(null, AvatarSize.EditRoomDetails, AvatarType.User) UnsavedAvatar(null, AvatarSize.EditRoomDetails, AvatarType.User)
UnsavedAvatar(Uri.EMPTY, AvatarSize.EditRoomDetails, AvatarType.User) UnsavedAvatar("", AvatarSize.EditRoomDetails, AvatarType.User)
UnsavedAvatar(null, AvatarSize.EditRoomDetails, AvatarType.Space()) UnsavedAvatar(null, AvatarSize.EditRoomDetails, AvatarType.Space())
UnsavedAvatar(Uri.EMPTY, AvatarSize.EditRoomDetails, AvatarType.Space()) UnsavedAvatar("", AvatarSize.EditRoomDetails, AvatarType.Space())
} }
} }

View file

@ -62,6 +62,7 @@ class KonsistClassNameTest {
.withAllParentsOf(PreviewParameterProvider::class) .withAllParentsOf(PreviewParameterProvider::class)
.withoutName( .withoutName(
"AspectRatioProvider", "AspectRatioProvider",
"EditableAvatarViewUriProvider",
"LoginModeViewErrorProvider", "LoginModeViewErrorProvider",
"OverlapRatioProvider", "OverlapRatioProvider",
"TextFileContentProvider", "TextFileContentProvider",