From 6641afdd17daf2aa62067458de8c1f7125ad9b60 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 9 May 2023 18:08:45 +0200 Subject: [PATCH 01/15] WIP pick avatar image --- features/createroom/impl/build.gradle.kts | 1 + .../impl/configureroom/ConfigureRoomEvents.kt | 2 - .../configureroom/ConfigureRoomPresenter.kt | 32 ++++- .../impl/configureroom/ConfigureRoomState.kt | 2 + .../ConfigureRoomStateProvider.kt | 9 +- .../impl/configureroom/ConfigureRoomView.kt | 33 ++++- .../impl/configureroom/avatar/AvatarAction.kt | 34 +++++ .../avatar/AvatarActionListEvents.kt | 21 +++ .../avatar/AvatarActionListState.kt | 26 ++++ .../avatar/AvatarActionListStateProvider.kt | 30 +++++ .../avatar/AvatarActionListView.kt | 121 ++++++++++++++++++ .../libraries/mediapickers/api/PickerType.kt | 7 + .../mediapickers/impl/PickerProviderImpl.kt | 14 ++ 13 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarAction.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListEvents.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListState.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListStateProvider.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 0515bbbfc7..6a9b0c28ff 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(projects.features.userlist.api) api(projects.features.createroom.api) implementation(libs.coil.compose) // FIXME temp + implementation(projects.libraries.mediapickers) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index f2c454656c..58cdae613c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -16,14 +16,12 @@ package io.element.android.features.createroom.impl.configureroom -import android.net.Uri import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface ConfigureRoomEvents { data class RoomNameChanged(val name: String) : ConfigureRoomEvents data class TopicChanged(val topic: String) : ConfigureRoomEvents - data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index fa61c7f643..e38b9c06a7 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -26,6 +26,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListEvents +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListState import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.execute @@ -34,6 +37,8 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomVisibility +import io.element.android.libraries.mediapickers.PickerProvider +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -41,6 +46,7 @@ import javax.inject.Inject class ConfigureRoomPresenter @Inject constructor( private val dataStore: CreateRoomDataStore, private val matrixClient: MatrixClient, + private val mediaPickerProvider: PickerProvider, ) : Presenter { @Composable @@ -52,6 +58,13 @@ class ConfigureRoomPresenter @Inject constructor( } } + val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { uri -> + if (uri != null) dataStore.setAvatarUrl(uri.toString()) + }) + val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker(onResult = { uri -> + if (uri != null) dataStore.setAvatarUrl(uri.toString()) + }) + val localCoroutineScope = rememberCoroutineScope() val createRoomAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } @@ -62,7 +75,6 @@ class ConfigureRoomPresenter @Inject constructor( fun handleEvents(event: ConfigureRoomEvents) { when (event) { - is ConfigureRoomEvents.AvatarUriChanged -> dataStore.setAvatarUrl(event.uri?.toString()) is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name) is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic) is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy) @@ -72,9 +84,27 @@ class ConfigureRoomPresenter @Inject constructor( } } + fun handleAvatarEvents(event: AvatarActionListEvents) { + when (event) { + is AvatarActionListEvents.HandleAction -> when (event.action) { + AvatarAction.ChoosePhoto -> galleryImagePicker.launch() + AvatarAction.TakePhoto -> cameraPhotoPicker.launch() + } + } + } + + val avatarActionListState = AvatarActionListState( + actions = persistentListOf( + AvatarAction.ChoosePhoto, + AvatarAction.TakePhoto, + ), + eventSink = ::handleAvatarEvents, + ) + return ConfigureRoomState( config = createRoomConfig.value, isCreateButtonEnabled = isCreateButtonEnabled, + avatarActionListState = avatarActionListState, createRoomAction = createRoomAction.value, eventSink = ::handleEvents, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index b5f804fcde..1b01c3a642 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -17,12 +17,14 @@ package io.element.android.features.createroom.impl.configureroom import io.element.android.features.createroom.impl.CreateRoomConfig +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListState import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId data class ConfigureRoomState( val config: CreateRoomConfig, val isCreateButtonEnabled: Boolean, + val avatarActionListState: AvatarActionListState, val createRoomAction: Async, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 67e0b91bba..3383b13b2a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -18,8 +18,11 @@ package io.element.android.features.createroom.impl.configureroom import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.createroom.impl.CreateRoomConfig +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListState import io.element.android.features.userlist.api.aListOfSelectedUsers import io.element.android.libraries.architecture.Async +import kotlinx.collections.immutable.persistentListOf open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence @@ -40,6 +43,10 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider Unit = {}, onRoomCreated: (RoomId) -> Unit = {}, ) { + val coroutineScope = rememberCoroutineScope() + val itemActionsBottomSheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + ) + if (state.createRoomAction is Async.Success) { LaunchedEffect(state.createRoomAction) { onRoomCreated(state.createRoomAction.state) } } - val context = LocalContext.current + fun onAvatarClicked() { + coroutineScope.launch { + itemActionsBottomSheetState.show() + } + } + Scaffold( modifier = modifier, topBar = { @@ -92,7 +107,7 @@ fun ConfigureRoomView( modifier = Modifier.padding(horizontal = 16.dp), avatarUri = state.config.avatarUrl?.toUri(), roomName = state.config.roomName.orEmpty(), - onAvatarClick = { Toast.makeText(context, "not implemented yet", Toast.LENGTH_SHORT).show() }, + onAvatarClick = ::onAvatarClicked, onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) }, ) RoomTopic( @@ -114,10 +129,17 @@ fun ConfigureRoomView( } } + AvatarActionListView( + state = state.avatarActionListState, + modalBottomSheetState = itemActionsBottomSheetState, + onActionSelected = { state.avatarActionListState.eventSink(AvatarActionListEvents.HandleAction(it)) } + ) + when (state.createRoomAction) { is Async.Loading -> { ProgressDialog(text = stringResource(StringR.string.common_creating_room)) } + is Async.Failure -> { RetryDialog( content = stringResource(R.string.screen_create_room_error_creating_room), @@ -125,6 +147,7 @@ fun ConfigureRoomView( onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) }, ) } + else -> Unit } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarAction.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarAction.kt new file mode 100644 index 0000000000..029732f173 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarAction.kt @@ -0,0 +1,34 @@ +/* + * 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.features.createroom.impl.configureroom.avatar + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PhotoCamera +import androidx.compose.material.icons.filled.PhotoLibrary +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.vector.ImageVector +import io.element.android.libraries.ui.strings.R + +@Immutable +sealed class AvatarAction( + @StringRes val titleResId: Int, + val icon: ImageVector, +) { + object TakePhoto : AvatarAction(titleResId = R.string.action_take_photo, icon = Icons.Default.PhotoCamera) + object ChoosePhoto : AvatarAction(titleResId = R.string.action_choose_photo, icon = Icons.Default.PhotoLibrary) +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListEvents.kt new file mode 100644 index 0000000000..2e81ea6500 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListEvents.kt @@ -0,0 +1,21 @@ +/* + * 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.features.createroom.impl.configureroom.avatar + +sealed interface AvatarActionListEvents { + data class HandleAction(val action: AvatarAction) : AvatarActionListEvents +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListState.kt new file mode 100644 index 0000000000..35a34b20a9 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListState.kt @@ -0,0 +1,26 @@ +/* + * 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.features.createroom.impl.configureroom.avatar + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +data class AvatarActionListState( + val actions: ImmutableList, + val eventSink: (AvatarActionListEvents) -> Unit, +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListStateProvider.kt new file mode 100644 index 0000000000..6cb6ce1361 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListStateProvider.kt @@ -0,0 +1,30 @@ +/* + * 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.features.createroom.impl.configureroom.avatar + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import kotlinx.collections.immutable.persistentListOf + +open class ActionListStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(anActionListState()) +} + +fun anActionListState() = AvatarActionListState( + actions = persistentListOf(AvatarAction.TakePhoto, AvatarAction.ChoosePhoto), + eventSink = {} +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt new file mode 100644 index 0000000000..515faee0f5 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt @@ -0,0 +1,121 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalMaterialApi::class, ExperimentalMaterialApi::class) + +package io.element.android.features.createroom.impl.configureroom.avatar + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ListItem +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout +import kotlinx.coroutines.launch + +@Composable +fun AvatarActionListView( + state: AvatarActionListState, + modalBottomSheetState: ModalBottomSheetState, + modifier: Modifier = Modifier, + onActionSelected: (action: AvatarAction) -> Unit = {}, +) { + val coroutineScope = rememberCoroutineScope() + fun onItemActionClicked(itemAction: AvatarAction) { + onActionSelected(itemAction) + coroutineScope.launch { + modalBottomSheetState.hide() + } + } + + ModalBottomSheetLayout( + modifier = modifier, + sheetState = modalBottomSheetState, + sheetContent = { + SheetContent( + state = state, + onActionClicked = ::onItemActionClicked, + modifier = Modifier + .navigationBarsPadding() + .imePadding() + ) + } + ) +} + +@Composable +private fun SheetContent( + state: AvatarActionListState, + modifier: Modifier = Modifier, + onActionClicked: (AvatarAction) -> Unit = { }, +) { + val actions = state.actions + LazyColumn( + modifier = modifier.fillMaxWidth() + ) { + items( + items = actions, + ) { action -> + ListItem( + modifier = Modifier.clickable { onActionClicked(action) }, + text = { + Text(text = stringResource(action.titleResId)) + }, + icon = { + Icon( + imageVector = action.icon, + contentDescription = stringResource(action.titleResId), + ) + } + ) + } + } +} + +@Preview +@Composable +fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) state: AvatarActionListState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) state: AvatarActionListState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: AvatarActionListState) { + AvatarActionListView( + state = state, + modalBottomSheetState = ModalBottomSheetState( + initialValue = ModalBottomSheetValue.Expanded + ), + ) +} diff --git a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt index ad89175ddf..7c86009d34 100644 --- a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt +++ b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt @@ -26,6 +26,13 @@ sealed interface PickerType { fun getContract(): ActivityResultContract fun getDefaultRequest(): Input + object Image : PickerType { + override fun getContract() = ActivityResultContracts.PickVisualMedia() + override fun getDefaultRequest(): PickVisualMediaRequest { + return PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + } + } + object ImageAndVideo : PickerType { override fun getContract() = ActivityResultContracts.PickVisualMedia() override fun getDefaultRequest(): PickVisualMediaRequest { diff --git a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt index b7e9bad420..11f172fd4c 100644 --- a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt +++ b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt @@ -59,6 +59,20 @@ class PickerProviderImpl constructor(private val isInTest: Boolean) : PickerProv } } + /** + * Remembers and returns a [PickerLauncher] for a gallery picture. + * [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected. + */ + @Composable + fun registerGalleryImagePicker(onResult: (Uri?) -> Unit): PickerLauncher { + // Tests and UI preview can't handle Contexts, so we might as well disable the whole picker + return if (LocalInspectionMode.current || isInTest) { + NoOpPickerLauncher { onResult(null) } + } else { + rememberPickerLauncher(type = PickerType.Image) { uri -> onResult(uri) } + } + } + /** * Remembers and returns a [PickerLauncher] for a gallery item, either a picture or a video. * [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected. From 554ce9f6500a4ec4b7576636217570ae716a9b82 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 9 May 2023 22:08:47 +0200 Subject: [PATCH 02/15] Rework and add remove button --- .../impl/configureroom/ConfigureRoomEvents.kt | 2 + .../configureroom/ConfigureRoomPresenter.kt | 44 ++++++++++--------- .../impl/configureroom/ConfigureRoomState.kt | 5 ++- .../ConfigureRoomStateProvider.kt | 7 +-- .../impl/configureroom/ConfigureRoomView.kt | 5 +-- .../impl/configureroom/avatar/AvatarAction.kt | 11 +++-- .../avatar/AvatarActionListEvents.kt | 21 --------- .../avatar/AvatarActionListState.kt | 26 ----------- .../avatar/AvatarActionListStateProvider.kt | 30 ------------- .../avatar/AvatarActionListView.kt | 29 +++++++----- 10 files changed, 55 insertions(+), 125 deletions(-) delete mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListEvents.kt delete mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListState.kt delete mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListStateProvider.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index 58cdae613c..52389d97fb 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -17,6 +17,7 @@ package io.element.android.features.createroom.impl.configureroom import io.element.android.features.createroom.impl.CreateRoomConfig +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface ConfigureRoomEvents { @@ -25,5 +26,6 @@ sealed interface ConfigureRoomEvents { data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents + data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents object CancelCreateRoom : ConfigureRoomEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index e38b9c06a7..399bb2d763 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -27,8 +27,6 @@ import androidx.compose.runtime.rememberCoroutineScope import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction -import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListEvents -import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListState import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.execute @@ -38,7 +36,7 @@ import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomVisibility import io.element.android.libraries.mediapickers.PickerProvider -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -65,6 +63,19 @@ class ConfigureRoomPresenter @Inject constructor( if (uri != null) dataStore.setAvatarUrl(uri.toString()) }) + val avatarActions by remember(createRoomConfig.value.avatarUrl) { + derivedStateOf { + mutableListOf( + AvatarAction.TakePhoto, + AvatarAction.ChoosePhoto, + ).apply { + if (createRoomConfig.value.avatarUrl != null) { + add(AvatarAction.Remove) + } + }.toImmutableList() + } + } + val localCoroutineScope = rememberCoroutineScope() val createRoomAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } @@ -80,31 +91,22 @@ class ConfigureRoomPresenter @Inject constructor( is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy) is ConfigureRoomEvents.RemoveFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser) is ConfigureRoomEvents.CreateRoom -> createRoom(event.config) + is ConfigureRoomEvents.HandleAvatarAction -> { + when (event.action) { + AvatarAction.ChoosePhoto -> galleryImagePicker.launch() + AvatarAction.TakePhoto -> cameraPhotoPicker.launch() + AvatarAction.Remove -> dataStore.setAvatarUrl(null) + } + } + ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = Async.Uninitialized } } - fun handleAvatarEvents(event: AvatarActionListEvents) { - when (event) { - is AvatarActionListEvents.HandleAction -> when (event.action) { - AvatarAction.ChoosePhoto -> galleryImagePicker.launch() - AvatarAction.TakePhoto -> cameraPhotoPicker.launch() - } - } - } - - val avatarActionListState = AvatarActionListState( - actions = persistentListOf( - AvatarAction.ChoosePhoto, - AvatarAction.TakePhoto, - ), - eventSink = ::handleAvatarEvents, - ) - return ConfigureRoomState( config = createRoomConfig.value, isCreateButtonEnabled = isCreateButtonEnabled, - avatarActionListState = avatarActionListState, + avatarActions = avatarActions, createRoomAction = createRoomAction.value, eventSink = ::handleEvents, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index 1b01c3a642..0e0c465b52 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -17,14 +17,15 @@ package io.element.android.features.createroom.impl.configureroom import io.element.android.features.createroom.impl.CreateRoomConfig -import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListState +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.collections.immutable.ImmutableList data class ConfigureRoomState( val config: CreateRoomConfig, val isCreateButtonEnabled: Boolean, - val avatarActionListState: AvatarActionListState, + val avatarActions: ImmutableList, val createRoomAction: Async, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 3383b13b2a..2dd4854f3a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -18,8 +18,6 @@ package io.element.android.features.createroom.impl.configureroom import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.createroom.impl.CreateRoomConfig -import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction -import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListState import io.element.android.features.userlist.api.aListOfSelectedUsers import io.element.android.libraries.architecture.Async import kotlinx.collections.immutable.persistentListOf @@ -43,10 +41,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider, - val eventSink: (AvatarActionListEvents) -> Unit, -) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListStateProvider.kt deleted file mode 100644 index 6cb6ce1361..0000000000 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListStateProvider.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.features.createroom.impl.configureroom.avatar - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import kotlinx.collections.immutable.persistentListOf - -open class ActionListStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf(anActionListState()) -} - -fun anActionListState() = AvatarActionListState( - actions = persistentListOf(AvatarAction.TakePhoto, AvatarAction.ChoosePhoto), - eventSink = {} -) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt index 515faee0f5..f68db3a4b7 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt @@ -29,21 +29,23 @@ import androidx.compose.material.ListItem import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.launch @Composable fun AvatarActionListView( - state: AvatarActionListState, + actions: ImmutableList, modalBottomSheetState: ModalBottomSheetState, modifier: Modifier = Modifier, onActionSelected: (action: AvatarAction) -> Unit = {}, @@ -61,7 +63,7 @@ fun AvatarActionListView( sheetState = modalBottomSheetState, sheetContent = { SheetContent( - state = state, + actions = actions, onActionClicked = ::onItemActionClicked, modifier = Modifier .navigationBarsPadding() @@ -73,11 +75,10 @@ fun AvatarActionListView( @Composable private fun SheetContent( - state: AvatarActionListState, + actions: ImmutableList, modifier: Modifier = Modifier, onActionClicked: (AvatarAction) -> Unit = { }, ) { - val actions = state.actions LazyColumn( modifier = modifier.fillMaxWidth() ) { @@ -87,12 +88,16 @@ private fun SheetContent( ListItem( modifier = Modifier.clickable { onActionClicked(action) }, text = { - Text(text = stringResource(action.titleResId)) + Text( + text = stringResource(action.titleResId), + color = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary, + ) }, icon = { Icon( imageVector = action.icon, contentDescription = stringResource(action.titleResId), + tint = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary, ) } ) @@ -102,18 +107,18 @@ private fun SheetContent( @Preview @Composable -fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) state: AvatarActionListState) = - ElementPreviewLight { ContentToPreview(state) } +fun SheetContentLightPreview() = + ElementPreviewLight { ContentToPreview() } @Preview @Composable -fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) state: AvatarActionListState) = - ElementPreviewDark { ContentToPreview(state) } +fun SheetContentDarkPreview() = + ElementPreviewDark { ContentToPreview() } @Composable -private fun ContentToPreview(state: AvatarActionListState) { +private fun ContentToPreview() { AvatarActionListView( - state = state, + actions = persistentListOf(AvatarAction.ChoosePhoto, AvatarAction.TakePhoto, AvatarAction.Remove), modalBottomSheetState = ModalBottomSheetState( initialValue = ModalBottomSheetValue.Expanded ), From a38fbb3419e7b02c8bd83ea2bc7c397c8fb59f97 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 10 May 2023 11:08:43 +0200 Subject: [PATCH 03/15] Delete cached avatar URI --- .../createroom/impl/CreateRoomConfig.kt | 3 ++- .../createroom/impl/CreateRoomDataStore.kt | 12 +++++++++-- .../configureroom/ConfigureRoomPresenter.kt | 21 ++++++++++--------- .../impl/configureroom/ConfigureRoomView.kt | 3 +-- .../ConfigureRoomPresenterTests.kt | 21 ++++++++++++++----- 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt index 21662fdb64..23b6f3239d 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt @@ -16,6 +16,7 @@ package io.element.android.features.createroom.impl +import android.net.Uri import io.element.android.features.createroom.impl.configureroom.RoomPrivacy import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList @@ -24,7 +25,7 @@ import kotlinx.collections.immutable.persistentListOf data class CreateRoomConfig( val roomName: String? = null, val topic: String? = null, - val avatarUrl: String? = null, + val avatarUri: Uri? = null, val invites: ImmutableList = persistentListOf(), val privacy: RoomPrivacy? = null, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt index e55fca755d..8d124d25de 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt @@ -16,6 +16,7 @@ package io.element.android.features.createroom.impl +import android.net.Uri import io.element.android.features.createroom.impl.configureroom.RoomPrivacy import io.element.android.features.createroom.impl.di.CreateRoomScope import io.element.android.features.userlist.api.UserListDataStore @@ -24,6 +25,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import java.io.File import javax.inject.Inject @SingleIn(CreateRoomScope::class) @@ -32,6 +34,11 @@ class CreateRoomDataStore @Inject constructor( ) { private val createRoomConfigFlow: MutableStateFlow = MutableStateFlow(CreateRoomConfig()) + private var cachedAvatarUri: Uri? = null + set(value) { + field?.path?.let { File(it) }?.delete() + field = value + } fun getCreateRoomConfig(): Flow = combine( selectedUserListDataStore.selectedUsers(), @@ -48,8 +55,9 @@ class CreateRoomDataStore @Inject constructor( createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(topic = topic?.takeIf { it.isNotEmpty() })) } - fun setAvatarUrl(avatarUrl: String?) { - createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(avatarUrl = avatarUrl)) + fun setAvatarUri(uri: Uri?, cached: Boolean = false) { + cachedAvatarUri = uri.takeIf { cached } + createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(avatarUri = uri)) } fun setPrivacy(privacy: RoomPrivacy?) { diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 399bb2d763..941a03f84c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -56,20 +56,21 @@ class ConfigureRoomPresenter @Inject constructor( } } - val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { uri -> - if (uri != null) dataStore.setAvatarUrl(uri.toString()) - }) - val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker(onResult = { uri -> - if (uri != null) dataStore.setAvatarUrl(uri.toString()) - }) + val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( + onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) }, + deleteAfter = false, + ) + val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker( + onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri) } + ) - val avatarActions by remember(createRoomConfig.value.avatarUrl) { + val avatarActions by remember(createRoomConfig.value.avatarUri) { derivedStateOf { mutableListOf( AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, ).apply { - if (createRoomConfig.value.avatarUrl != null) { + if (createRoomConfig.value.avatarUri != null) { add(AvatarAction.Remove) } }.toImmutableList() @@ -95,7 +96,7 @@ class ConfigureRoomPresenter @Inject constructor( when (event.action) { AvatarAction.ChoosePhoto -> galleryImagePicker.launch() AvatarAction.TakePhoto -> cameraPhotoPicker.launch() - AvatarAction.Remove -> dataStore.setAvatarUrl(null) + AvatarAction.Remove -> dataStore.setAvatarUri(uri = null) } } @@ -122,7 +123,7 @@ class ConfigureRoomPresenter @Inject constructor( visibility = if (config.privacy == RoomPrivacy.Public) RoomVisibility.PUBLIC else RoomVisibility.PRIVATE, preset = if (config.privacy == RoomPrivacy.Public) RoomPreset.PUBLIC_CHAT else RoomPreset.PRIVATE_CHAT, invite = config.invites.map { it.userId }, - avatar = config.avatarUrl, + avatar = config.avatarUri?.toString(), ) matrixClient.createRoom(params).getOrThrow() }.execute(createRoomAction) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 214ec89efb..b019e693e0 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.net.toUri import io.element.android.features.createroom.impl.R import io.element.android.features.createroom.impl.components.Avatar import io.element.android.features.createroom.impl.components.LabelledTextField @@ -104,7 +103,7 @@ fun ConfigureRoomView( ) { RoomNameWithAvatar( modifier = Modifier.padding(horizontal = 16.dp), - avatarUri = state.config.avatarUrl?.toUri(), + avatarUri = state.config.avatarUri, roomName = state.config.roomName.orEmpty(), onAvatarClick = ::onAvatarClicked, onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) }, diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index 8be1e777a8..e1ed1f9c71 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -25,6 +25,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction import io.element.android.features.userlist.api.UserListDataStore import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId @@ -34,6 +35,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.mediapickers.PickerProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -48,15 +50,18 @@ class ConfigureRoomPresenterTests { private lateinit var presenter: ConfigureRoomPresenter private lateinit var userListDataStore: UserListDataStore + private lateinit var createRoomDataStore: CreateRoomDataStore private lateinit var fakeMatrixClient: FakeMatrixClient @Before fun setup() { fakeMatrixClient = FakeMatrixClient() userListDataStore = UserListDataStore() + createRoomDataStore = CreateRoomDataStore(userListDataStore) presenter = ConfigureRoomPresenter( - dataStore = CreateRoomDataStore(userListDataStore), - matrixClient = fakeMatrixClient + dataStore = createRoomDataStore, + matrixClient = fakeMatrixClient, + mediaPickerProvider = PickerProvider(isInTest = true), ) } @@ -70,7 +75,7 @@ class ConfigureRoomPresenterTests { assertThat(initialState.config.roomName).isNull() assertThat(initialState.config.topic).isNull() assertThat(initialState.config.invites).isEmpty() - assertThat(initialState.config.avatarUrl).isNull() + assertThat(initialState.config.avatarUri).isNull() assertThat(initialState.config.privacy).isNull() } } @@ -139,10 +144,16 @@ class ConfigureRoomPresenterTests { assertThat(newState.config).isEqualTo(expectedConfig) // Room avatar + // Add val anUri = Uri.parse(AN_AVATAR_URL) - newState.eventSink(ConfigureRoomEvents.AvatarUriChanged(anUri)) + createRoomDataStore.setAvatarUri(anUri) newState = awaitItem() - expectedConfig = expectedConfig.copy(avatarUrl = anUri.toString()) + expectedConfig = expectedConfig.copy(avatarUri = anUri) + assertThat(newState.config).isEqualTo(expectedConfig) + // Remove + newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove)) + newState = awaitItem() + expectedConfig = expectedConfig.copy(avatarUri = null) assertThat(newState.config).isEqualTo(expectedConfig) // Room privacy From c81e673111c64427c134639b6ed554a7c691dce6 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 10 May 2023 11:45:27 +0200 Subject: [PATCH 04/15] Fix tests after rebase --- features/createroom/impl/build.gradle.kts | 5 +++-- .../impl/configureroom/ConfigureRoomPresenter.kt | 3 +-- .../impl/configureroom/ConfigureRoomPresenterTests.kt | 9 ++++++--- .../android/libraries/mediapickers/api/PickerProvider.kt | 6 +++++- .../libraries/mediapickers/impl/PickerProviderImpl.kt | 2 +- .../libraries/mediapickers/test/FakePickerProvider.kt | 5 +++++ 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 6a9b0c28ff..08cd6fa0e8 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -46,9 +46,9 @@ dependencies { implementation(projects.libraries.elementresources) implementation(projects.libraries.uiStrings) implementation(projects.features.userlist.api) + implementation(projects.libraries.mediapickers.api) + implementation(libs.coil.compose) api(projects.features.createroom.api) - implementation(libs.coil.compose) // FIXME temp - implementation(projects.libraries.mediapickers) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -59,6 +59,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.userlist.impl) testImplementation(projects.features.userlist.test) + testImplementation(projects.libraries.mediapickers.test) androidTestImplementation(libs.test.junitext) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 941a03f84c..553ddf795b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -35,7 +35,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomVisibility -import io.element.android.libraries.mediapickers.PickerProvider +import io.element.android.libraries.mediapickers.api.PickerProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -58,7 +58,6 @@ class ConfigureRoomPresenter @Inject constructor( val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) }, - deleteAfter = false, ) val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker( onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri) } diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index e1ed1f9c71..9eefdb8929 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -35,7 +35,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.ui.components.aMatrixUser -import io.element.android.libraries.mediapickers.PickerProvider +import io.element.android.libraries.mediapickers.test.FakePickerProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -52,16 +52,18 @@ class ConfigureRoomPresenterTests { private lateinit var userListDataStore: UserListDataStore private lateinit var createRoomDataStore: CreateRoomDataStore private lateinit var fakeMatrixClient: FakeMatrixClient + private lateinit var fakePickerProvider: FakePickerProvider @Before fun setup() { fakeMatrixClient = FakeMatrixClient() userListDataStore = UserListDataStore() createRoomDataStore = CreateRoomDataStore(userListDataStore) + fakePickerProvider = FakePickerProvider() presenter = ConfigureRoomPresenter( dataStore = createRoomDataStore, matrixClient = fakeMatrixClient, - mediaPickerProvider = PickerProvider(isInTest = true), + mediaPickerProvider = fakePickerProvider, ) } @@ -146,7 +148,8 @@ class ConfigureRoomPresenterTests { // Room avatar // Add val anUri = Uri.parse(AN_AVATAR_URL) - createRoomDataStore.setAvatarUri(anUri) + fakePickerProvider.givenResult(anUri) + newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) newState = awaitItem() expectedConfig = expectedConfig.copy(avatarUri = anUri) assertThat(newState.config).isEqualTo(expectedConfig) diff --git a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt index 9b0b248aa5..9becdc8aee 100644 --- a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt +++ b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt @@ -27,6 +27,11 @@ interface PickerProvider { onResult: (uri: Uri?, mimeType: String?) -> Unit ): PickerLauncher + @Composable + fun registerGalleryImagePicker( + onResult: (Uri?) -> Unit + ): PickerLauncher + @Composable fun registerFilePicker( mimeType: String, @@ -38,5 +43,4 @@ interface PickerProvider { @Composable fun registerCameraVideoPicker(onResult: (Uri?) -> Unit): PickerLauncher - } diff --git a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt index 11f172fd4c..201fd9b069 100644 --- a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt +++ b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt @@ -64,7 +64,7 @@ class PickerProviderImpl constructor(private val isInTest: Boolean) : PickerProv * [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected. */ @Composable - fun registerGalleryImagePicker(onResult: (Uri?) -> Unit): PickerLauncher { + override fun registerGalleryImagePicker(onResult: (Uri?) -> Unit): PickerLauncher { // Tests and UI preview can't handle Contexts, so we might as well disable the whole picker return if (LocalInspectionMode.current || isInTest) { NoOpPickerLauncher { onResult(null) } diff --git a/libraries/mediapickers/test/src/main/kotlin/io/element/android/libraries/mediapickers/test/FakePickerProvider.kt b/libraries/mediapickers/test/src/main/kotlin/io/element/android/libraries/mediapickers/test/FakePickerProvider.kt index 2a32387db4..e243a22138 100644 --- a/libraries/mediapickers/test/src/main/kotlin/io/element/android/libraries/mediapickers/test/FakePickerProvider.kt +++ b/libraries/mediapickers/test/src/main/kotlin/io/element/android/libraries/mediapickers/test/FakePickerProvider.kt @@ -33,6 +33,11 @@ class FakePickerProvider : PickerProvider { return NoOpPickerLauncher { onResult(result, mimeType) } } + @Composable + override fun registerGalleryImagePicker(onResult: (uri: Uri?) -> Unit): PickerLauncher { + return NoOpPickerLauncher { onResult(result) } + } + @Composable override fun registerFilePicker(mimeType: String, onResult: (Uri?) -> Unit): PickerLauncher { return NoOpPickerLauncher { onResult(result) } From 4fd3cb4c0e696ebcb53c1bb6249f5288114a3ef7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 10 May 2023 15:13:56 +0200 Subject: [PATCH 05/15] Add todo and screenshot tests --- .../createroom/impl/configureroom/ConfigureRoomPresenter.kt | 1 + ...ultGroup_SheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...ltGroup_SheetContentLightPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ 3 files changed, 7 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 553ddf795b..3117be0657 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -114,6 +114,7 @@ class ConfigureRoomPresenter @Inject constructor( private fun CoroutineScope.createRoom(config: CreateRoomConfig, createRoomAction: MutableState>) = launch { suspend { + // TODO pre-process and upload the avatar before creating the room val params = CreateRoomParameters( name = config.roomName, topic = config.topic, diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6b5ba60f36 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58efadd0883968cedfe5d74c4269edb3e7cadd1150fd0e9bd3425efcc717ec01 +size 14040 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fdfcbb6e11 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cd407cdbaab5e3fe6b42ceb98c5b9b6a4012e3781ad7e754531c2ce04d9ee4e +size 14810 From 83463a731bcda5d1b65b05820400ace146d380dd Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 10 May 2023 15:29:46 +0200 Subject: [PATCH 06/15] changelogs --- changelog.d/122.feature | 1 + changelog.d/123.feature | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/122.feature create mode 100644 changelog.d/123.feature diff --git a/changelog.d/122.feature b/changelog.d/122.feature new file mode 100644 index 0000000000..c163e6d53b --- /dev/null +++ b/changelog.d/122.feature @@ -0,0 +1 @@ +[Create and join rooms] Select a media from the camera diff --git a/changelog.d/123.feature b/changelog.d/123.feature new file mode 100644 index 0000000000..f8b4d36f2d --- /dev/null +++ b/changelog.d/123.feature @@ -0,0 +1 @@ +[Create and join rooms] Select a media from the gallery From 4a444c24c4dff02ea5a1b377adf607e6f3a3fed8 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 10 May 2023 16:16:26 +0200 Subject: [PATCH 07/15] Add missing tests --- .../ConfigureRoomPresenterTests.kt | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index 9eefdb8929..8710a28735 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -29,7 +29,6 @@ import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAc import io.element.android.features.userlist.api.UserListDataStore import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_THROWABLE @@ -45,6 +44,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +private const val AN_URI_FROM_CAMERA = "content://uri_from_camera" +private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery" + @RunWith(RobolectricTestRunner::class) class ConfigureRoomPresenterTests { @@ -146,12 +148,23 @@ class ConfigureRoomPresenterTests { assertThat(newState.config).isEqualTo(expectedConfig) // Room avatar - // Add - val anUri = Uri.parse(AN_AVATAR_URL) - fakePickerProvider.givenResult(anUri) + // Pick avatar + fakePickerProvider.givenResult(null) + newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) + newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) + // From gallery + val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY) + fakePickerProvider.givenResult(uriFromGallery) newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) newState = awaitItem() - expectedConfig = expectedConfig.copy(avatarUri = anUri) + expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery) + assertThat(newState.config).isEqualTo(expectedConfig) + // From camera + val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA) + fakePickerProvider.givenResult(uriFromCamera) + newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) + newState = awaitItem() + expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera) assertThat(newState.config).isEqualTo(expectedConfig) // Remove newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove)) From 5a0665a27aa19285f10b3e8725b8ec57394c4721 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 11 May 2023 08:32:08 +0200 Subject: [PATCH 08/15] upload avatar within the room creation --- features/createroom/impl/build.gradle.kts | 1 + .../configureroom/ConfigureRoomPresenter.kt | 21 ++++++++++++++++--- .../libraries/matrix/api/MatrixClient.kt | 4 +++- .../libraries/matrix/impl/RustMatrixClient.kt | 21 ++++++++++++------- .../libraries/matrix/test/FakeMatrixClient.kt | 12 +++++++---- 5 files changed, 44 insertions(+), 15 deletions(-) diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 08cd6fa0e8..2f8894b402 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.features.userlist.api) implementation(projects.libraries.mediapickers.api) + implementation(projects.libraries.mediaupload.api) implementation(libs.coil.compose) api(projects.features.createroom.api) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 3117be0657..0d60158b8d 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -16,6 +16,7 @@ package io.element.android.features.createroom.impl.configureroom +import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState @@ -30,12 +31,16 @@ import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAc import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.execute +import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomVisibility import io.element.android.libraries.mediapickers.api.PickerProvider +import io.element.android.libraries.mediaupload.api.MediaPreProcessor +import io.element.android.libraries.mediaupload.api.MediaType +import io.element.android.libraries.mediaupload.api.MediaUploadInfo import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -45,6 +50,7 @@ class ConfigureRoomPresenter @Inject constructor( private val dataStore: CreateRoomDataStore, private val matrixClient: MatrixClient, private val mediaPickerProvider: PickerProvider, + private val mediaPreProcessor: MediaPreProcessor, ) : Presenter { @Composable @@ -112,9 +118,12 @@ class ConfigureRoomPresenter @Inject constructor( ) } - private fun CoroutineScope.createRoom(config: CreateRoomConfig, createRoomAction: MutableState>) = launch { + private fun CoroutineScope.createRoom( + config: CreateRoomConfig, + createRoomAction: MutableState> + ) = launch { + val mxc = config.avatarUri?.let { uploadAvatar(it) } suspend { - // TODO pre-process and upload the avatar before creating the room val params = CreateRoomParameters( name = config.roomName, topic = config.topic, @@ -123,9 +132,15 @@ class ConfigureRoomPresenter @Inject constructor( visibility = if (config.privacy == RoomPrivacy.Public) RoomVisibility.PUBLIC else RoomVisibility.PRIVATE, preset = if (config.privacy == RoomPrivacy.Public) RoomPreset.PUBLIC_CHAT else RoomPreset.PRIVATE_CHAT, invite = config.invites.map { it.userId }, - avatar = config.avatarUri?.toString(), + avatar = mxc, ) matrixClient.createRoom(params).getOrThrow() }.execute(createRoomAction) } + + private suspend fun uploadAvatar(avatarUri: Uri): String? { + val preprocessed = mediaPreProcessor.process(avatarUri, MediaType.Image).getOrThrow() as? MediaUploadInfo.Image + val byteArray = preprocessed?.file?.readBytes() + return byteArray?.let { matrixClient.uploadMedia(MimeTypes.Jpeg, it) }?.getOrThrow() + } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index dce2b15485..6d357ad912 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -42,6 +42,7 @@ interface MatrixClient : Closeable { suspend fun createRoom(createRoomParams: CreateRoomParameters): Result suspend fun createDM(userId: UserId): Result suspend fun getProfile(userId: UserId): Result + suspend fun searchUsers(searchTerm: String, limit: Long): Result fun startSync() fun stopSync() fun mediaResolver(): MediaResolver @@ -58,9 +59,10 @@ interface MatrixClient : Closeable { height: Long ): Result + suspend fun uploadMedia(mimeType: String, data: ByteArray): Result + fun onSlidingSyncUpdate() fun roomMembershipObserver(): RoomMembershipObserver - suspend fun searchUsers(searchTerm: String, limit: Long): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index cce7ac90c7..0381869503 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -280,6 +280,13 @@ class RustMatrixClient constructor( } } + override suspend fun searchUsers(searchTerm: String, limit: Long): Result = + withContext(dispatchers.io) { + runCatching { + client.searchUsers(searchTerm, limit.toULong()).let(UserSearchResultMapper::map) + } + } + override fun mediaResolver(): MediaResolver = mediaResolver override fun sessionVerificationService(): SessionVerificationService = verificationService @@ -368,6 +375,13 @@ class RustMatrixClient constructor( } } + @OptIn(ExperimentalUnsignedTypes::class) + override suspend fun uploadMedia(mimeType: String, data: ByteArray): Result = withContext(dispatchers.io) { + runCatching { + client.uploadMedia(mimeType, data.toUByteArray().toList()) + } + } + override fun onSlidingSyncUpdate() { if (!verificationService.isReady.value) { try { @@ -380,13 +394,6 @@ class RustMatrixClient constructor( override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver - override suspend fun searchUsers(searchTerm: String, limit: Long): Result = - withContext(dispatchers.io) { - runCatching { - client.searchUsers(searchTerm, limit.toULong()).let(UserSearchResultMapper::map) - } - } - private fun File.deleteSessionDirectory(userID: String): Boolean { // Rust sanitises the user ID replacing invalid characters with an _ val sanitisedUserID = userID.replace(":", "_") diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 9d3ce40da1..51088115ed 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -91,6 +91,10 @@ class FakeMatrixClient( return getProfileResults[userId] ?: Result.failure(IllegalStateException("No profile found for $userId")) } + override suspend fun searchUsers(searchTerm: String, limit: Long): Result { + return searchUserResults[searchTerm] ?: Result.failure(IllegalStateException("No response defined for $searchTerm")) + } + override fun startSync() = Unit override fun stopSync() = Unit @@ -122,6 +126,10 @@ class FakeMatrixClient( return Result.success(ByteArray(0)) } + override suspend fun uploadMedia(mimeType: String, data: ByteArray): Result { + return Result.success("") + } + override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService override fun pushersService(): PushersService = pushersService @@ -134,10 +142,6 @@ class FakeMatrixClient( return RoomMembershipObserver() } - override suspend fun searchUsers(searchTerm: String, limit: Long): Result { - return searchUserResults[searchTerm] ?: Result.failure(IllegalStateException("No response defined for $searchTerm")) - } - // Mocks fun givenLogoutError(failure: Throwable?) { From 1b1dbca96b0373153b8c5502ad1c1a4a8840800b Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 11 May 2023 16:30:20 +0200 Subject: [PATCH 09/15] Clear cached image after room creation --- .../android/features/createroom/impl/CreateRoomDataStore.kt | 4 ++++ .../createroom/impl/configureroom/ConfigureRoomPresenter.kt | 1 + 2 files changed, 5 insertions(+) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt index 8d124d25de..3c594a0f1e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt @@ -63,4 +63,8 @@ class CreateRoomDataStore @Inject constructor( fun setPrivacy(privacy: RoomPrivacy?) { createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(privacy = privacy)) } + + fun clearCachedData() { + cachedAvatarUri = null + } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 0d60158b8d..8ce4a58e43 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -135,6 +135,7 @@ class ConfigureRoomPresenter @Inject constructor( avatar = mxc, ) matrixClient.createRoom(params).getOrThrow() + .also { dataStore.clearCachedData() } }.execute(createRoomAction) } From 14fba4c5597d34c5f9069ef1f7d99c312aaed30e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 11 May 2023 17:31:46 +0200 Subject: [PATCH 10/15] Reformat some code --- .../impl/configureroom/ConfigureRoomPresenter.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 8ce4a58e43..c0a43784f7 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -71,14 +71,11 @@ class ConfigureRoomPresenter @Inject constructor( val avatarActions by remember(createRoomConfig.value.avatarUri) { derivedStateOf { - mutableListOf( + listOfNotNull( AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, - ).apply { - if (createRoomConfig.value.avatarUri != null) { - add(AvatarAction.Remove) - } - }.toImmutableList() + AvatarAction.Remove.takeIf { createRoomConfig.value.avatarUri != null }, + ).toImmutableList() } } From 5e38049e75509c3dc3e2303888d75e673ed9f501 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 11 May 2023 17:34:40 +0200 Subject: [PATCH 11/15] Fix test build --- features/createroom/impl/build.gradle.kts | 1 + .../impl/configureroom/ConfigureRoomPresenterTests.kt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 2f8894b402..4d3e8f191d 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { testImplementation(projects.features.userlist.impl) testImplementation(projects.features.userlist.test) testImplementation(projects.libraries.mediapickers.test) + testImplementation(projects.libraries.mediaupload.test) androidTestImplementation(libs.test.junitext) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index 8710a28735..71ea4062f1 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.mediapickers.test.FakePickerProvider +import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -66,6 +67,7 @@ class ConfigureRoomPresenterTests { dataStore = createRoomDataStore, matrixClient = fakeMatrixClient, mediaPickerProvider = fakePickerProvider, + mediaPreProcessor = FakeMediaPreProcessor(), ) } From deea472001d6816a4af568121c2653528c17bc84 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 11 May 2023 23:03:47 +0200 Subject: [PATCH 12/15] Add more tests --- features/createroom/impl/build.gradle.kts | 1 + .../configureroom/ConfigureRoomPresenter.kt | 2 +- .../ConfigureRoomPresenterTests.kt | 44 ++++++++++++++++++- .../libraries/matrix/test/FakeMatrixClient.kt | 7 ++- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 4d3e8f191d..2026ac5214 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { api(projects.features.createroom.api) testImplementation(libs.test.junit) + testImplementation(libs.test.mockk) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index c0a43784f7..7659226bd9 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -119,8 +119,8 @@ class ConfigureRoomPresenter @Inject constructor( config: CreateRoomConfig, createRoomAction: MutableState> ) = launch { - val mxc = config.avatarUri?.let { uploadAvatar(it) } suspend { + val mxc = config.avatarUri?.let { uploadAvatar(it) } val params = CreateRoomParameters( name = config.roomName, topic = config.topic, diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index 71ea4062f1..eadad19d23 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -29,21 +29,29 @@ import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAc import io.element.android.features.userlist.api.UserListDataStore import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.mediapickers.test.FakePickerProvider +import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import java.io.File private const val AN_URI_FROM_CAMERA = "content://uri_from_camera" private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery" @@ -56,6 +64,7 @@ class ConfigureRoomPresenterTests { private lateinit var createRoomDataStore: CreateRoomDataStore private lateinit var fakeMatrixClient: FakeMatrixClient private lateinit var fakePickerProvider: FakePickerProvider + private lateinit var fakeMediaPreProcessor: FakeMediaPreProcessor @Before fun setup() { @@ -63,12 +72,21 @@ class ConfigureRoomPresenterTests { userListDataStore = UserListDataStore() createRoomDataStore = CreateRoomDataStore(userListDataStore) fakePickerProvider = FakePickerProvider() + fakeMediaPreProcessor = FakeMediaPreProcessor() presenter = ConfigureRoomPresenter( dataStore = createRoomDataStore, matrixClient = fakeMatrixClient, mediaPickerProvider = fakePickerProvider, - mediaPreProcessor = FakeMediaPreProcessor(), + mediaPreProcessor = fakeMediaPreProcessor, ) + + mockkStatic(File::readBytes) + every { any().readBytes() } returns byteArrayOf() + } + + @After + fun tearDown() { + unmockkAll() } @Test @@ -206,6 +224,30 @@ class ConfigureRoomPresenterTests { } } + @Test + fun `present - trigger create room with upload error and retry`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY)) + fakeMediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), null))) + fakeMatrixClient.givenUploadMediaResult(Result.failure(A_THROWABLE)) + + val initialState = awaitItem() + initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) + val stateAfterCreateRoom = awaitItem() + assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Failure::class.java) + + fakeMatrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL)) + stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) + assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java) + assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java) + assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Success::class.java) + + } + } + @Test fun `present - trigger retry and cancel actions`() = runTest { moleculeFlow(RecompositionClock.Immediate) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 51088115ed..bdf76c8e05 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -59,6 +59,7 @@ class FakeMatrixClient( private val getRoomResults = mutableMapOf() private val searchUserResults = mutableMapOf>() private val getProfileResults = mutableMapOf>() + private var uploadMediaResult: Result = Result.success(AN_AVATAR_URL) override fun getRoom(roomId: RoomId): MatrixRoom? { return getRoomResults[roomId] @@ -127,7 +128,7 @@ class FakeMatrixClient( } override suspend fun uploadMedia(mimeType: String, data: ByteArray): Result { - return Result.success("") + return uploadMediaResult } override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService @@ -183,4 +184,8 @@ class FakeMatrixClient( fun givenGetProfileResult(userId: UserId, result: Result) { getProfileResults[userId] = result } + + fun givenUploadMediaResult(result: Result) { + uploadMediaResult = result + } } From da57e17dfb7e27f52bc480b27abd97d3adf1fce6 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 16 May 2023 14:29:18 +0200 Subject: [PATCH 13/15] Use material3 ListItem --- .../impl/configureroom/avatar/AvatarActionListView.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt index f68db3a4b7..422a187290 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterialApi::class, ExperimentalMaterialApi::class) +@file:OptIn(ExperimentalMaterialApi::class) package io.element.android.features.createroom.impl.configureroom.avatar @@ -25,10 +25,10 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ListItem import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Text +import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope @@ -87,13 +87,13 @@ private fun SheetContent( ) { action -> ListItem( modifier = Modifier.clickable { onActionClicked(action) }, - text = { + headlineContent = { Text( text = stringResource(action.titleResId), color = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary, ) }, - icon = { + leadingContent = { Icon( imageVector = action.icon, contentDescription = stringResource(action.titleResId), @@ -118,7 +118,7 @@ fun SheetContentDarkPreview() = @Composable private fun ContentToPreview() { AvatarActionListView( - actions = persistentListOf(AvatarAction.ChoosePhoto, AvatarAction.TakePhoto, AvatarAction.Remove), + actions = persistentListOf(AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, AvatarAction.Remove), modalBottomSheetState = ModalBottomSheetState( initialValue = ModalBottomSheetValue.Expanded ), From 723e0d561c99813634823400a74a6ecb4b5ef8d7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 16 May 2023 14:32:23 +0200 Subject: [PATCH 14/15] Update screenshots --- ...ltGroup_SheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...tGroup_SheetContentLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png index 6b5ba60f36..1a8de3bbb4 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58efadd0883968cedfe5d74c4269edb3e7cadd1150fd0e9bd3425efcc717ec01 -size 14040 +oid sha256:c824fcf4167c6315d0d6568a4d300375a7bf6d4818e3d0dae9ed055f3269313d +size 13072 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentLightPreview_0_null,NEXUS_5,1.0,en].png index fdfcbb6e11..809b8135eb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cd407cdbaab5e3fe6b42ceb98c5b9b6a4012e3781ad7e754531c2ce04d9ee4e -size 14810 +oid sha256:8bad887952347b21606a783a19390fe73e5d6deb19390e2d413304398cff810c +size 13872 From 9c3a639fec207de405c4d97b3900e8f357bb1c20 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 16 May 2023 15:37:22 +0200 Subject: [PATCH 15/15] Fix test compilation --- .../impl/configureroom/ConfigureRoomPresenterTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index 0c3f64b056..b4632ab0e8 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -228,7 +228,7 @@ class ConfigureRoomPresenterTests { }.test { skipItems(1) createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY)) - fakeMediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), null))) + fakeMediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk()))) fakeMatrixClient.givenUploadMediaResult(Result.failure(A_THROWABLE)) val initialState = awaitItem()