From 5c1a10381878ed318343af7ff4ef39d30d68f9ab Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 13 Nov 2024 17:25:05 +0100 Subject: [PATCH 1/4] create room : improve handling of room address --- .../createroom/impl/CreateRoomConfig.kt | 4 +- .../createroom/impl/CreateRoomDataStore.kt | 21 ++-- .../configureroom/ConfigureRoomPresenter.kt | 60 ++++++++++-- .../impl/configureroom/ConfigureRoomState.kt | 6 +- .../ConfigureRoomStateProvider.kt | 41 +++++++- .../impl/configureroom/ConfigureRoomView.kt | 17 +++- .../configureroom/RoomAddressErrorState.kt | 17 ---- .../impl/configureroom/RoomAddressValidity.kt | 26 +++++ .../impl/configureroom/RoomVisibilityState.kt | 8 -- .../impl/addpeople/AddPeoplePresenterTest.kt | 3 +- .../ConfigureRoomPresenterTest.kt | 97 +++++++++++++++++-- ...est.kt => RoomAliasHelperPresenterTest.kt} | 2 +- ...ViewTest.kt => RoomAliasHelperViewTest.kt} | 2 +- .../libraries/matrix/api/MatrixClient.kt | 10 ++ .../api/createroom/CreateRoomParameters.kt | 2 +- .../matrix/api/room/alias/RoomAliasHelper.kt | 15 +++ .../libraries/matrix/impl/RustMatrixClient.kt | 2 +- .../impl/room/alias/DefaultRoomAliasHelper.kt | 25 +++++ .../test/room/alias/FakeRoomAliasHelper.kt | 26 +++++ 19 files changed, 320 insertions(+), 64 deletions(-) delete mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddressErrorState.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddressValidity.kt rename features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/{RoomAliasResolverPresenterTest.kt => RoomAliasHelperPresenterTest.kt} (98%) rename features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/{RoomAliasResolverViewTest.kt => RoomAliasHelperViewTest.kt} (98%) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/RoomAliasHelper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/alias/DefaultRoomAliasHelper.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/alias/FakeRoomAliasHelper.kt 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 ab6e116598..a83673886d 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 @@ -19,6 +19,4 @@ data class CreateRoomConfig( val avatarUri: Uri? = null, val invites: ImmutableList = persistentListOf(), val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private, -) { - val isValid = roomName.isNullOrEmpty().not() && roomVisibility.isValid() -} +) 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 56ebe326dd..67e697e8e4 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 @@ -11,13 +11,13 @@ import android.net.Uri import io.element.android.features.createroom.impl.configureroom.RoomAccess import io.element.android.features.createroom.impl.configureroom.RoomAccessItem import io.element.android.features.createroom.impl.configureroom.RoomAddress -import io.element.android.features.createroom.impl.configureroom.RoomAddressErrorState import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState import io.element.android.features.createroom.impl.di.CreateRoomScope import io.element.android.features.createroom.impl.userlist.UserListDataStore import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -29,6 +29,7 @@ import javax.inject.Inject @SingleIn(CreateRoomScope::class) class CreateRoomDataStore @Inject constructor( val selectedUserListDataStore: UserListDataStore, + private val roomAliasHelper: RoomAliasHelper, ) { private val createRoomConfigFlow: MutableStateFlow = MutableStateFlow(CreateRoomConfig()) private var cachedAvatarUri: Uri? = null @@ -46,13 +47,13 @@ class CreateRoomDataStore @Inject constructor( fun setRoomName(roomName: String) { createRoomConfigFlow.getAndUpdate { config -> - /* val newVisibility = when (config.roomVisibility) { is RoomVisibilityState.Public -> { val roomAddress = config.roomVisibility.roomAddress if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) { + val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName) config.roomVisibility.copy( - roomAddress = RoomAddress.AutoFilled(roomName), + roomAddress = RoomAddress.AutoFilled(roomAliasName), ) } else { config.roomVisibility @@ -60,9 +61,9 @@ class CreateRoomDataStore @Inject constructor( } else -> config.roomVisibility } - */ config.copy( roomName = roomName.takeIf { it.isNotEmpty() }, + roomVisibility = newVisibility, ) } } @@ -85,11 +86,13 @@ class CreateRoomDataStore @Inject constructor( config.copy( roomVisibility = when (visibility) { RoomVisibilityItem.Private -> RoomVisibilityState.Private - RoomVisibilityItem.Public -> RoomVisibilityState.Public( - roomAddress = RoomAddress.AutoFilled(config.roomName.orEmpty()), - roomAddressErrorState = RoomAddressErrorState.None, - roomAccess = RoomAccess.Anyone, - ) + RoomVisibilityItem.Public -> { + val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty()) + RoomVisibilityState.Public( + roomAddress = RoomAddress.AutoFilled(roomAliasName), + roomAccess = RoomAccess.Anyone, + ) + } } ) } 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 bbefc987ef..3ace410c78 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 @@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import im.vector.app.features.analytics.plan.CreatedRoom import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore @@ -31,6 +32,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.matrix.api.room.alias.RoomAliasHelper +import io.element.android.libraries.matrix.api.roomAliasFromName import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor @@ -39,9 +42,12 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber +import java.util.Optional import javax.inject.Inject +import kotlin.jvm.optionals.getOrNull class ConfigureRoomPresenter @Inject constructor( private val dataStore: CreateRoomDataStore, @@ -51,6 +57,7 @@ class ConfigureRoomPresenter @Inject constructor( private val analyticsService: AnalyticsService, permissionsPresenterFactory: PermissionsPresenter.Factory, private val featureFlagService: FeatureFlagService, + private val roomAliasHelper: RoomAliasHelper, ) : Presenter { private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA) private var pendingPermissionRequest = false @@ -58,9 +65,12 @@ class ConfigureRoomPresenter @Inject constructor( @Composable override fun present(): ConfigureRoomState { val cameraPermissionState = cameraPermissionPresenter.present() - val createRoomConfig = dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig()) + val createRoomConfig by dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig()) val homeserverName = remember { matrixClient.userIdServerName() } val isKnockFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(initial = false) + val roomAddressValidity = remember { + mutableStateOf(RoomAddressValidity.Unknown) + } val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) }, @@ -69,12 +79,12 @@ class ConfigureRoomPresenter @Inject constructor( onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri) } ) - val avatarActions by remember(createRoomConfig.value.avatarUri) { + val avatarActions by remember(createRoomConfig.avatarUri) { derivedStateOf { listOfNotNull( AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, - AvatarAction.Remove.takeIf { createRoomConfig.value.avatarUri != null }, + AvatarAction.Remove.takeIf { createRoomConfig.avatarUri != null }, ).toImmutableList() } } @@ -86,6 +96,10 @@ class ConfigureRoomPresenter @Inject constructor( } } + RoomAddressValidityEffect(createRoomConfig.roomVisibility.roomAddress()) { newRoomAddressValidity -> + roomAddressValidity.value = newRoomAddressValidity + } + val localCoroutineScope = rememberCoroutineScope() val createRoomAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } @@ -102,7 +116,7 @@ class ConfigureRoomPresenter @Inject constructor( is ConfigureRoomEvents.RemoveUserFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser) is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess) is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress) - is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig.value) + is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig) is ConfigureRoomEvents.HandleAvatarAction -> { when (event.action) { AvatarAction.ChoosePhoto -> galleryImagePicker.launch() @@ -122,15 +136,49 @@ class ConfigureRoomPresenter @Inject constructor( return ConfigureRoomState( isKnockFeatureEnabled = isKnockFeatureEnabled, - config = createRoomConfig.value, + config = createRoomConfig, avatarActions = avatarActions, createRoomAction = createRoomAction.value, cameraPermissionState = cameraPermissionState, homeserverName = homeserverName, + roomAddressValidity = roomAddressValidity.value, eventSink = ::handleEvents, ) } + @Composable + private fun RoomAddressValidityEffect( + roomAddress: Optional, + onRoomAddressValidityChange: (RoomAddressValidity) -> Unit, + ) { + val onChange by rememberUpdatedState(onRoomAddressValidityChange) + LaunchedEffect(roomAddress) { + val roomAliasName = roomAddress.getOrNull().orEmpty() + if (roomAliasName.isEmpty()) { + onChange(RoomAddressValidity.Unknown) + return@LaunchedEffect + } + // debounce the room address validation + delay(300) + val roomAlias = matrixClient.roomAliasFromName(roomAliasName).getOrNull() + if (roomAlias == null || !roomAliasHelper.isRoomAliasValid(roomAlias)) { + onChange(RoomAddressValidity.InvalidSymbols) + } else { + matrixClient.resolveRoomAlias(roomAlias) + .onSuccess { resolved -> + if (resolved.isPresent) { + onChange(RoomAddressValidity.NotAvailable) + } else { + onChange(RoomAddressValidity.Valid) + } + } + .onFailure { + onChange(RoomAddressValidity.Valid) + } + } + } + } + private fun CoroutineScope.createRoom( config: CreateRoomConfig, createRoomAction: MutableState> @@ -148,7 +196,7 @@ class ConfigureRoomPresenter @Inject constructor( preset = RoomPreset.PUBLIC_CHAT, invite = config.invites.map { it.userId }, avatar = avatarUrl, - canonicalAlias = config.roomVisibility.roomAddress() + roomAliasName = config.roomVisibility.roomAddress() ) } else { CreateRoomParameters( 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 8a2122c306..2730efb19f 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 @@ -20,6 +20,10 @@ data class ConfigureRoomState( val avatarActions: ImmutableList, val createRoomAction: AsyncAction, val cameraPermissionState: PermissionsState, + val roomAddressValidity: RoomAddressValidity, val homeserverName: String, val eventSink: (ConfigureRoomEvents) -> Unit -) +) { + val isValid: Boolean = config.roomName?.isNotEmpty() == true && + (config.roomVisibility is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid) +} 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 80445fbff4..0de6404424 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 @@ -28,9 +28,8 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false), homeserverName: String = "matrix.org", + roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid, eventSink: (ConfigureRoomEvents) -> Unit = { }, ) = ConfigureRoomState( config = config, @@ -64,5 +96,6 @@ fun aConfigureRoomState( createRoomAction = createRoomAction, cameraPermissionState = cameraPermissionState, homeserverName = homeserverName, + roomAddressValidity = roomAddressValidity, eventSink = eventSink, ) 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 c832fab464..e34bb27a8b 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 @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding @@ -79,7 +80,7 @@ fun ConfigureRoomView( modifier = modifier.clearFocusOnTap(focusManager), topBar = { ConfigureRoomToolbar( - isNextActionEnabled = state.config.isValid, + isNextActionEnabled = state.isValid, onBackClick = onBackClick, onNextClick = { focusManager.clearFocus() @@ -143,8 +144,10 @@ fun ConfigureRoomView( modifier = Modifier.padding(horizontal = 16.dp), address = state.config.roomVisibility.roomAddress, homeserverName = state.homeserverName, + addressValidity = state.roomAddressValidity, onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) }, ) + Spacer(Modifier) } } } @@ -319,6 +322,7 @@ private fun RoomAccessOptions( private fun RoomAddressField( address: RoomAddress, homeserverName: String, + addressValidity: RoomAddressValidity, onAddressChange: (String) -> Unit, modifier: Modifier = Modifier, ) { @@ -340,7 +344,16 @@ private fun RoomAddressField( color = ElementTheme.colors.textSecondary, ) }, - supportingText = stringResource(R.string.screen_create_room_room_address_section_footer), + supportingText = when (addressValidity) { + RoomAddressValidity.InvalidSymbols -> { + stringResource(R.string.screen_create_room_room_address_invalid_symbols_error_description) + } + RoomAddressValidity.NotAvailable -> { + stringResource(R.string.screen_create_room_room_address_not_available_error_description) + } + else -> stringResource(R.string.screen_create_room_room_address_section_footer) + }, + isError = addressValidity.isError(), onValueChange = onAddressChange, singleLine = true, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddressErrorState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddressErrorState.kt deleted file mode 100644 index f2cadfd6bb..0000000000 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddressErrorState.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.createroom.impl.configureroom - -/** - * Represents the error state of a room address. - */ -sealed interface RoomAddressErrorState { - data object InvalidCharacters : RoomAddressErrorState - data object AlreadyExists : RoomAddressErrorState - data object None : RoomAddressErrorState -} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddressValidity.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddressValidity.kt new file mode 100644 index 0000000000..536f48f94e --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddressValidity.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.createroom.impl.configureroom + +import androidx.compose.runtime.Immutable + +/** + * Represents the validity state of a room address. + * ie. whether it contains invalid characters, is already taken, or is valid. + */ +@Immutable +sealed interface RoomAddressValidity { + data object Unknown : RoomAddressValidity + data object InvalidSymbols : RoomAddressValidity + data object NotAvailable : RoomAddressValidity + data object Valid : RoomAddressValidity + + fun isError(): Boolean { + return this is InvalidSymbols || this is NotAvailable + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomVisibilityState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomVisibilityState.kt index cc5af7836e..502e307557 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomVisibilityState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomVisibilityState.kt @@ -14,7 +14,6 @@ sealed interface RoomVisibilityState { data class Public( val roomAddress: RoomAddress, - val roomAddressErrorState: RoomAddressErrorState, val roomAccess: RoomAccess, ) : RoomVisibilityState @@ -24,11 +23,4 @@ sealed interface RoomVisibilityState { is Public -> Optional.of(roomAddress.value) } } - - fun isValid(): Boolean { - return when (this) { - is Private -> true - is Public -> roomAddressErrorState is RoomAddressErrorState.None && roomAddress.value.isNotEmpty() - } - } } diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTest.kt index 7a9ae7321e..69205e6178 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTest.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTest.kt @@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory import io.element.android.features.createroom.impl.userlist.UserListDataStore +import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper import io.element.android.libraries.usersearch.test.FakeUserRepository import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest @@ -32,7 +33,7 @@ class AddPeoplePresenterTest { presenter = AddPeoplePresenter( FakeUserListPresenterFactory(), FakeUserRepository(), - CreateRoomDataStore(UserListDataStore()) + CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper()) ) } diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTest.kt index fefb09c241..b39e08ec80 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTest.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTest.kt @@ -19,11 +19,15 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias +import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper 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_ID 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.test.room.alias.FakeRoomAliasHelper import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.api.PickerProvider @@ -44,6 +48,8 @@ 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.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before @@ -52,6 +58,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.io.File +import java.util.Optional private const val AN_URI_FROM_CAMERA = "content://uri_from_camera" private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2" @@ -95,21 +102,21 @@ class ConfigureRoomPresenterTest { presenter.test { val initialState = initialState() var config = initialState.config - assertThat(initialState.config.isValid).isFalse() + assertThat(initialState.isValid).isFalse() // Room name not empty initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) var newState: ConfigureRoomState = awaitItem() config = config.copy(roomName = A_ROOM_NAME) assertThat(newState.config).isEqualTo(config) - assertThat(newState.config.isValid).isTrue() + assertThat(newState.isValid).isTrue() // Clear room name newState.eventSink(ConfigureRoomEvents.RoomNameChanged("")) newState = awaitItem() config = config.copy(roomName = null) assertThat(newState.config).isEqualTo(config) - assertThat(newState.config.isValid).isFalse() + assertThat(newState.isValid).isFalse() } } @@ -118,8 +125,9 @@ class ConfigureRoomPresenterTest { val userListDataStore = UserListDataStore() val pickerProvider = FakePickerProvider() val permissionsPresenter = FakePermissionsPresenter() + val roomAliasHelper = FakeRoomAliasHelper() val presenter = createConfigureRoomPresenter( - createRoomDataStore = CreateRoomDataStore(userListDataStore), + createRoomDataStore = CreateRoomDataStore(userListDataStore, roomAliasHelper), pickerProvider = pickerProvider, permissionsPresenter = permissionsPresenter, ) @@ -191,8 +199,7 @@ class ConfigureRoomPresenterTest { newState = awaitItem() expectedConfig = expectedConfig.copy( roomVisibility = RoomVisibilityState.Public( - roomAddress = RoomAddress.AutoFilled(expectedConfig.roomName ?: ""), - roomAddressErrorState = RoomAddressErrorState.None, + roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")), roomAccess = RoomAccess.Anyone, ) ) @@ -254,7 +261,7 @@ class ConfigureRoomPresenterTest { val matrixClient = createMatrixClient() val analyticsService = FakeAnalyticsService() val mediaPreProcessor = FakeMediaPreProcessor() - val createRoomDataStore = CreateRoomDataStore(UserListDataStore()) + val createRoomDataStore = CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper()) val presenter = createConfigureRoomPresenter( createRoomDataStore = createRoomDataStore, mediaPreProcessor = mediaPreProcessor, @@ -315,17 +322,88 @@ class ConfigureRoomPresenterTest { } } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - address is invalid when format is invalid`() = runTest { + val aliasHelper = FakeRoomAliasHelper( + isRoomAliasValidLambda = { false } + ) + val presenter = createConfigureRoomPresenter( + roomAliasHelper = aliasHelper + ) + presenter.test { + val initialState = initialState() + initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public)) + skipItems(1) + initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address")) + skipItems(1) + advanceUntilIdle() + awaitItem().also { state -> + assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.InvalidSymbols) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - address is not available when alias is not available`() = runTest { + val fakeMatrixClient = createMatrixClient(isAliasAvailable = false) + val presenter = createConfigureRoomPresenter( + matrixClient = fakeMatrixClient, + ) + presenter.test { + val initialState = initialState() + initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public)) + skipItems(1) + initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address")) + skipItems(1) + advanceUntilIdle() + awaitItem().also { state -> + assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.NotAvailable) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - address is valid when alias is available and format is valid`() = runTest { + val fakeMatrixClient = createMatrixClient(isAliasAvailable = true) + val presenter = createConfigureRoomPresenter( + matrixClient = fakeMatrixClient, + ) + presenter.test { + val initialState = initialState() + initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public)) + skipItems(1) + initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address")) + skipItems(1) + advanceUntilIdle() + awaitItem().also { state -> + assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) + } + } + } + private suspend fun TurbineTestContext.initialState(): ConfigureRoomState { skipItems(1) return awaitItem() } - private fun createMatrixClient() = FakeMatrixClient( + private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient( userIdServerNameLambda = { "matrix.org" }, + resolveRoomAliasResult = { + val resolvedRoomAlias = if (isAliasAvailable) { + Optional.empty() + } else { + Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList())) + } + Result.success(resolvedRoomAlias) + } ) private fun createConfigureRoomPresenter( - createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore()), + roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(), + createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore(), roomAliasHelper), matrixClient: MatrixClient = createMatrixClient(), pickerProvider: PickerProvider = FakePickerProvider(), mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(), @@ -339,6 +417,7 @@ class ConfigureRoomPresenterTest { mediaPreProcessor = mediaPreProcessor, analyticsService = analyticsService, permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter), + roomAliasHelper = roomAliasHelper, featureFlagService = FakeFeatureFlagService( mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled) ) diff --git a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenterTest.kt b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperPresenterTest.kt similarity index 98% rename from features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenterTest.kt rename to features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperPresenterTest.kt index 9894c2b342..ab4d9bc8a6 100644 --- a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenterTest.kt +++ b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperPresenterTest.kt @@ -26,7 +26,7 @@ import org.junit.Rule import org.junit.Test import java.util.Optional -class RoomAliasResolverPresenterTest { +class RoomAliasHelperPresenterTest { @get:Rule val warmUpRule = WarmUpRule() diff --git a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverViewTest.kt b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt similarity index 98% rename from features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverViewTest.kt rename to features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt index 6fc3aa64a2..7f4f26cc3d 100644 --- a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverViewTest.kt +++ b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt @@ -27,7 +27,7 @@ import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class RoomAliasResolverViewTest { +class RoomAliasHelperViewTest { @get:Rule val rule = createAndroidComposeRule() @Test 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 504db1c6d9..eca03afb0c 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 @@ -168,3 +168,13 @@ fun MatrixClient.getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow roomSummary.map { it.info } } .distinctUntilChanged() } + +/** + * Returns a room alias from a room alias name. + * @param name the room alias name ie. the local part of the room alias. + */ +fun MatrixClient.roomAliasFromName(name: String): Result { + return runCatching { + RoomAlias("#$name:${userIdServerName()}") + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt index 2e13623c96..93ac71ef17 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt @@ -20,5 +20,5 @@ data class CreateRoomParameters( val invite: List? = null, val avatar: String? = null, val joinRuleOverride: JoinRuleOverride = JoinRuleOverride.None, - val canonicalAlias: Optional = Optional.empty(), + val roomAliasName: Optional = Optional.empty(), ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/RoomAliasHelper.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/RoomAliasHelper.kt new file mode 100644 index 0000000000..eb746e1f0d --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/RoomAliasHelper.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.room.alias + +import io.element.android.libraries.matrix.api.core.RoomAlias + +interface RoomAliasHelper { + fun roomAliasNameFromRoomDisplayName(name: String): String + fun isRoomAliasValid(roomAlias: RoomAlias): Boolean +} 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 3165cef1bf..dc852c900d 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 @@ -334,7 +334,7 @@ class RustMatrixClient( JoinRuleOverride.Knock -> RustJoinRule.Knock JoinRuleOverride.None -> null }, - canonicalAlias = createRoomParams.canonicalAlias.getOrNull(), + canonicalAlias = createRoomParams.roomAliasName.getOrNull(), ) val roomId = RoomId(client.createRoom(rustParams)) // Wait to receive the room back from the sync but do not returns failure if it fails. diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/alias/DefaultRoomAliasHelper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/alias/DefaultRoomAliasHelper.kt new file mode 100644 index 0000000000..a2fa36f65c --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/alias/DefaultRoomAliasHelper.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room.alias + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultRoomAliasHelper @Inject constructor() : RoomAliasHelper { + override fun roomAliasNameFromRoomDisplayName(name: String): String { + return org.matrix.rustcomponents.sdk.roomAliasNameFromRoomDisplayName(name) + } + + override fun isRoomAliasValid(roomAlias: RoomAlias): Boolean { + return org.matrix.rustcomponents.sdk.isRoomAliasFormatValid(roomAlias.value) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/alias/FakeRoomAliasHelper.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/alias/FakeRoomAliasHelper.kt new file mode 100644 index 0000000000..d832b9de5a --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/alias/FakeRoomAliasHelper.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.test.room.alias + +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper + +class FakeRoomAliasHelper( + private val roomAliasNameFromRoomDisplayNameLambda: (String) -> String = { name -> + name.trimStart().trimEnd().replace(" ", "_") + }, + private val isRoomAliasValidLambda: (RoomAlias) -> Boolean = { true } +) : RoomAliasHelper { + override fun roomAliasNameFromRoomDisplayName(name: String): String { + return roomAliasNameFromRoomDisplayNameLambda(name) + } + + override fun isRoomAliasValid(roomAlias: RoomAlias): Boolean { + return isRoomAliasValidLambda(roomAlias) + } +} From 57da2330f7f3d2d75b4b570c1984d2740c37f58b Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 13 Nov 2024 17:07:27 +0000 Subject: [PATCH 2/4] Update screenshots --- ...eateroom.impl.configureroom_ConfigureRoomViewDark_2_en.png | 4 ++-- ...eateroom.impl.configureroom_ConfigureRoomViewDark_3_en.png | 3 +++ ...eateroom.impl.configureroom_ConfigureRoomViewDark_4_en.png | 3 +++ ...eateroom.impl.configureroom_ConfigureRoomViewDark_5_en.png | 3 +++ ...ateroom.impl.configureroom_ConfigureRoomViewLight_2_en.png | 4 ++-- ...ateroom.impl.configureroom_ConfigureRoomViewLight_3_en.png | 3 +++ ...ateroom.impl.configureroom_ConfigureRoomViewLight_4_en.png | 3 +++ ...ateroom.impl.configureroom_ConfigureRoomViewLight_5_en.png | 3 +++ 8 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en.png index 80f53da42a..9bcb3813ac 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6270f3b7935c47da76ce4e81aa7e8eac13a9340e7ccb198f8cd6956d746dfa8 -size 59452 +oid sha256:ffbdcd28c32808fba8ffb7071b6440578716d3ae89149eb319c22d3f3b78b1ff +size 59499 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en.png new file mode 100644 index 0000000000..e9d68af4ee --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccbb514ad9bd6a95ecff2e278574ac5bff9bd8a63a5fc91fa35302dbaa9e71f2 +size 55276 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en.png new file mode 100644 index 0000000000..43814bce07 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2ea9839e3dd1288d99c2115c37bc7fe72716af9785c4de3e328a77a57d12d70 +size 56633 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en.png new file mode 100644 index 0000000000..b254eeceb3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b28da0e8ff5cbedf8777b5af8ff6bab2bd5c476df80878b2c28dd96a354d978 +size 54383 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en.png index 397dd1dfb7..bd45e398b4 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3b02b0bc0d557bac4d5aa945daf04d5d58dab44d9e4284d030804e6b5ed6a49 -size 60703 +oid sha256:9dc54d0a6c3cb3069482b1c27ed6d80fd63b0bcad1a550bf5c4edb09f69db3a7 +size 60749 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en.png new file mode 100644 index 0000000000..ae54d119d7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71c42c595070733ac8ac2c50da116e73ea6eea7dc120f6f0d28022b5a5f925bd +size 57099 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en.png new file mode 100644 index 0000000000..9229891a5f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e10c1c3f2b4045d20c9513c70bc4ea041e091976df5385b93d45c2d03b8f6e5f +size 58513 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en.png new file mode 100644 index 0000000000..a9ab02f20e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb54ae79de75af32e32642063d9dc9e0c24b0089f187a043f0fc330151c72c75 +size 56170 From 4c61e5dfa23bbba5870da85bb4e52f3f8cb91bb1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2024 09:06:13 +0100 Subject: [PATCH 3/4] Remove :samples:minimal module --- .github/workflows/build.yml | 5 +- CONTRIBUTING.md | 2 - docs/continuous_integration.md | 2 +- .../main/kotlin/extension/KoverExtension.kt | 2 - ...ent.android-compose-application.gradle.kts | 2 +- samples/minimal/.gitignore | 1 - samples/minimal/build.gradle.kts | 67 ------- samples/minimal/src/main/AndroidManifest.xml | 27 --- .../AlwaysEnabledFeatureFlagService.kt | 23 --- .../android/samples/minimal/LoginScreen.kt | 43 ----- .../android/samples/minimal/MainActivity.kt | 95 ---------- .../samples/minimal/NoOpProxyProvider.kt | 14 -- .../minimal/NoOpUserCertificatesProvider.kt | 14 -- .../minimal/NullPassphraseGenerator.kt | 14 -- .../minimal/OnlyFallbackPermalinkParser.kt | 18 -- .../android/samples/minimal/RoomListScreen.kt | 174 ------------------ .../android/samples/minimal/Singleton.kt | 55 ------ .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 1404 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2898 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 982 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 1772 -> 0 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 1900 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3918 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 2884 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 5914 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 3844 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 7778 -> 0 bytes .../src/main/res/values-night/themes.xml | 10 - .../minimal/src/main/res/values/strings.xml | 9 - .../minimal/src/main/res/values/themes.xml | 10 - settings.gradle.kts | 2 - tools/quality/check.sh | 1 - 32 files changed, 3 insertions(+), 587 deletions(-) delete mode 100644 samples/minimal/.gitignore delete mode 100644 samples/minimal/build.gradle.kts delete mode 100644 samples/minimal/src/main/AndroidManifest.xml delete mode 100644 samples/minimal/src/main/kotlin/io/element/android/samples/minimal/AlwaysEnabledFeatureFlagService.kt delete mode 100644 samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt delete mode 100644 samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt delete mode 100644 samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpProxyProvider.kt delete mode 100644 samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpUserCertificatesProvider.kt delete mode 100644 samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NullPassphraseGenerator.kt delete mode 100644 samples/minimal/src/main/kotlin/io/element/android/samples/minimal/OnlyFallbackPermalinkParser.kt delete mode 100644 samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt delete mode 100644 samples/minimal/src/main/kotlin/io/element/android/samples/minimal/Singleton.kt delete mode 100644 samples/minimal/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 samples/minimal/src/main/res/mipmap-hdpi/ic_launcher_round.webp delete mode 100644 samples/minimal/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 samples/minimal/src/main/res/mipmap-mdpi/ic_launcher_round.webp delete mode 100644 samples/minimal/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 samples/minimal/src/main/res/mipmap-xhdpi/ic_launcher_round.webp delete mode 100644 samples/minimal/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 samples/minimal/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp delete mode 100644 samples/minimal/src/main/res/mipmap-xxxhdpi/ic_launcher.webp delete mode 100644 samples/minimal/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp delete mode 100644 samples/minimal/src/main/res/values-night/themes.xml delete mode 100644 samples/minimal/src/main/res/values/strings.xml delete mode 100644 samples/minimal/src/main/res/values/themes.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39e4364658..aef924c392 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - variant: [debug, release, nightly, samples] + variant: [debug, release, nightly] fail-fast: false # Allow all jobs on develop. Just one per PR. concurrency: @@ -82,6 +82,3 @@ jobs: - name: Compile nightly sources if: ${{ matrix.variant == 'nightly' }} run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES - - name: Compile samples minimal - if: ${{ matrix.variant == 'samples' }} - run: ./gradlew :samples:minimal:assemble $CI_GRADLE_ARG_PROPERTIES diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6acfa7981f..bc4acfd5aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,8 +49,6 @@ Please ensure that you're using the project formatting rules (which are in the p This project should compile without any special action. Just clone it and open it with Android Studio, or compile from command line using `gradlew`. -Note: please make sure that the configuration is `app` and not `samples.minimal`. - ## Strings The strings of the project are managed externally using [https://localazy.com](https://localazy.com) and shared with Element X iOS. diff --git a/docs/continuous_integration.md b/docs/continuous_integration.md index 4ff0ebb983..85a869cbe9 100644 --- a/docs/continuous_integration.md +++ b/docs/continuous_integration.md @@ -40,7 +40,7 @@ We want: The CI checks that: -1. The code is compiling, without any warnings, for all the app build types and variants and for the minimal app +1. The code is compiling, without any warnings, for all the app build types and variants 2. The tests are passing 3. The code quality is good (detekt, ktlint, lint) 4. The code is running and smoke tests are passing (maestro) diff --git a/plugins/src/main/kotlin/extension/KoverExtension.kt b/plugins/src/main/kotlin/extension/KoverExtension.kt index 6182fde1f5..ccc077b0a3 100644 --- a/plugins/src/main/kotlin/extension/KoverExtension.kt +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -32,10 +32,8 @@ val localAarProjects = listOf( val excludedKoverSubProjects = listOf( ":app", - ":samples", ":anvilannotations", ":anvilcodegen", - ":samples:minimal", ":tests:testutils", // Exclude `:libraries:matrix:impl` module, it contains only wrappers to access the Rust Matrix // SDK api, so it is not really relevant to unit test it: there is no logic to test. diff --git a/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts b/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts index 967e4fd707..579bda8873 100644 --- a/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts @@ -6,7 +6,7 @@ */ /** - * This will generate the plugin "io.element.android-compose-application" to use by app and samples modules + * This will generate the plugin "io.element.android-compose-application" to use by app */ import extension.androidConfig import extension.commonDependencies diff --git a/samples/minimal/.gitignore b/samples/minimal/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/samples/minimal/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts deleted file mode 100644 index 48713af442..0000000000 --- a/samples/minimal/build.gradle.kts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ -plugins { - id("io.element.android-compose-application") - alias(libs.plugins.kotlin.android) -} - -android { - namespace = "io.element.android.samples.minimal" - - defaultConfig { - applicationId = "io.element.android.samples.minimal" - targetSdk = Versions.TARGET_SDK - versionCode = Versions.VERSION_CODE - versionName = Versions.VERSION_NAME - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - buildFeatures { - buildConfig = true - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } -} - -dependencies { - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.preference) - implementation(projects.libraries.matrix.api) - implementation(projects.libraries.matrix.impl) - implementation(projects.libraries.permissions.noop) - implementation(projects.libraries.sessionStorage.implMemory) - implementation(projects.libraries.designsystem) - implementation(projects.libraries.androidutils) - implementation(projects.libraries.architecture) - implementation(projects.libraries.core) - implementation(projects.libraries.network) - implementation(projects.libraries.dateformatter.impl) - implementation(projects.libraries.eventformatter.impl) - implementation(projects.libraries.fullscreenintent.impl) - implementation(projects.libraries.preferences.impl) - implementation(projects.libraries.preferences.test) - implementation(projects.libraries.indicator.impl) - implementation(projects.features.invite.impl) - implementation(projects.features.roomlist.impl) - implementation(projects.features.leaveroom.impl) - implementation(projects.features.login.impl) - implementation(projects.features.logout.impl) - implementation(projects.features.networkmonitor.impl) - implementation(projects.services.toolbox.impl) - implementation(projects.libraries.featureflag.impl) - implementation(projects.services.analytics.noop) - implementation(libs.coroutines.core) - implementation(projects.libraries.push.test) -} diff --git a/samples/minimal/src/main/AndroidManifest.xml b/samples/minimal/src/main/AndroidManifest.xml deleted file mode 100644 index 3f189bf096..0000000000 --- a/samples/minimal/src/main/AndroidManifest.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/AlwaysEnabledFeatureFlagService.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/AlwaysEnabledFeatureFlagService.kt deleted file mode 100644 index 67151d5aa4..0000000000 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/AlwaysEnabledFeatureFlagService.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.samples.minimal - -import io.element.android.libraries.featureflag.api.Feature -import io.element.android.libraries.featureflag.api.FeatureFlagService -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -class AlwaysEnabledFeatureFlagService : FeatureFlagService { - override fun isFeatureEnabledFlow(feature: Feature): Flow { - return flowOf(true) - } - - override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean { - return true - } -} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt deleted file mode 100644 index 1e4324d99f..0000000000 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.samples.minimal - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import io.element.android.features.login.impl.DefaultLoginUserStory -import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordPresenter -import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordView -import io.element.android.features.login.impl.util.defaultAccountProvider -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService - -class LoginScreen(private val authenticationService: MatrixAuthenticationService) { - @Composable - fun Content(modifier: Modifier = Modifier) { - val presenter = remember { - LoginPasswordPresenter( - authenticationService = authenticationService, - AccountProviderDataSource(), - DefaultLoginUserStory(), - ) - } - - LaunchedEffect(Unit) { - authenticationService.setHomeserver(defaultAccountProvider.url) - } - - val state = presenter.present() - LoginPasswordView( - state = state, - modifier = modifier, - onBackClick = {}, - ) - } -} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt deleted file mode 100644 index 1a3c68b478..0000000000 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.samples.minimal - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.core.view.WindowCompat -import io.element.android.compound.theme.ElementTheme -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.impl.RustClientBuilderProvider -import io.element.android.libraries.matrix.impl.RustMatrixClientFactory -import io.element.android.libraries.matrix.impl.auth.OidcConfigurationProvider -import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService -import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory -import io.element.android.libraries.matrix.impl.room.RustTimelineEventTypeFilterFactory -import io.element.android.libraries.network.useragent.SimpleUserAgentProvider -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore -import io.element.android.libraries.sessionstorage.api.LoggedInState -import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore -import io.element.android.services.analytics.noop.NoopAnalyticsService -import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock -import kotlinx.coroutines.runBlocking -import java.io.File - -class MainActivity : ComponentActivity() { - private val matrixAuthenticationService: MatrixAuthenticationService by lazy { - val baseDirectory = File(applicationContext.filesDir, "sessions") - val userAgentProvider = SimpleUserAgentProvider("MinimalSample") - val sessionStore = InMemorySessionStore() - val userCertificatesProvider = NoOpUserCertificatesProvider() - val proxyProvider = NoOpProxyProvider() - RustMatrixAuthenticationService( - sessionPathsFactory = SessionPathsFactory(baseDirectory, applicationContext.cacheDir), - coroutineDispatchers = Singleton.coroutineDispatchers, - sessionStore = sessionStore, - rustMatrixClientFactory = RustMatrixClientFactory( - baseDirectory = baseDirectory, - cacheDirectory = applicationContext.cacheDir, - appCoroutineScope = Singleton.appScope, - coroutineDispatchers = Singleton.coroutineDispatchers, - sessionStore = sessionStore, - userAgentProvider = userAgentProvider, - userCertificatesProvider = userCertificatesProvider, - proxyProvider = proxyProvider, - clock = DefaultSystemClock(), - analyticsService = NoopAnalyticsService(), - featureFlagService = AlwaysEnabledFeatureFlagService(), - timelineEventTypeFilterFactory = RustTimelineEventTypeFilterFactory(), - clientBuilderProvider = RustClientBuilderProvider(), - ), - passphraseGenerator = NullPassphraseGenerator(), - oidcConfigurationProvider = OidcConfigurationProvider(baseDirectory), - appPreferencesStore = InMemoryAppPreferencesStore(), - ) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - WindowCompat.setDecorFitsSystemWindows(window, false) - setContent { - ElementTheme { - val loggedInState by matrixAuthenticationService.loggedInStateFlow().collectAsState(initial = LoggedInState.NotLoggedIn) - Content(isLoggedIn = loggedInState is LoggedInState.LoggedIn, modifier = Modifier.fillMaxSize()) - } - } - } - - @Composable - fun Content( - isLoggedIn: Boolean, - modifier: Modifier = Modifier - ) { - if (!isLoggedIn) { - LoginScreen(authenticationService = matrixAuthenticationService).Content(modifier) - } else { - val matrixClient = runBlocking { - val sessionId = matrixAuthenticationService.getLatestSessionId()!! - matrixAuthenticationService.restoreSession(sessionId).getOrNull() - } - RoomListScreen(LocalContext.current, matrixClient!!).Content(modifier) - } - } -} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpProxyProvider.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpProxyProvider.kt deleted file mode 100644 index cda2f870ce..0000000000 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpProxyProvider.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.samples.minimal - -import io.element.android.libraries.matrix.impl.proxy.ProxyProvider - -class NoOpProxyProvider : ProxyProvider { - override fun provides(): String? = null -} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpUserCertificatesProvider.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpUserCertificatesProvider.kt deleted file mode 100644 index fbaac27e79..0000000000 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpUserCertificatesProvider.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.samples.minimal - -import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider - -class NoOpUserCertificatesProvider : UserCertificatesProvider { - override fun provides(): List = emptyList() -} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NullPassphraseGenerator.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NullPassphraseGenerator.kt deleted file mode 100644 index 2ee4cbd4e5..0000000000 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NullPassphraseGenerator.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.samples.minimal - -import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator - -class NullPassphraseGenerator : PassphraseGenerator { - override fun generatePassphrase(): String? = null -} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/OnlyFallbackPermalinkParser.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/OnlyFallbackPermalinkParser.kt deleted file mode 100644 index 254bd893c8..0000000000 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/OnlyFallbackPermalinkParser.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.samples.minimal - -import android.net.Uri -import io.element.android.libraries.matrix.api.permalink.PermalinkData -import io.element.android.libraries.matrix.api.permalink.PermalinkParser - -class OnlyFallbackPermalinkParser : PermalinkParser { - override fun parse(uriString: String): PermalinkData { - return PermalinkData.FallbackLink(Uri.parse(uriString)) - } -} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt deleted file mode 100644 index 0db3c9da51..0000000000 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.samples.minimal - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.ui.Modifier -import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter -import io.element.android.features.invite.impl.response.AcceptDeclineInviteView -import io.element.android.features.leaveroom.impl.LeaveRoomPresenter -import io.element.android.features.logout.impl.direct.DirectLogoutPresenter -import io.element.android.features.networkmonitor.impl.DefaultNetworkMonitor -import io.element.android.features.roomlist.impl.RoomListPresenter -import io.element.android.features.roomlist.impl.RoomListView -import io.element.android.features.roomlist.impl.datasource.RoomListDataSource -import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory -import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter -import io.element.android.features.roomlist.impl.filters.selection.DefaultFilterSelectionStrategy -import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource -import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter -import io.element.android.libraries.androidutils.system.DefaultDateTimeObserver -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.dateformatter.impl.DateFormatters -import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter -import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider -import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher -import io.element.android.libraries.eventformatter.impl.DefaultRoomLastMessageFormatter -import io.element.android.libraries.eventformatter.impl.ProfileChangeContentFormatter -import io.element.android.libraries.eventformatter.impl.RoomMembershipContentFormatter -import io.element.android.libraries.eventformatter.impl.StateContentFormatter -import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState -import io.element.android.libraries.indicator.impl.DefaultIndicatorService -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.room.RoomMembershipObserver -import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.impl.room.join.DefaultJoinRoom -import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore -import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner -import io.element.android.services.analytics.noop.NoopAnalyticsService -import io.element.android.services.toolbox.impl.strings.AndroidStringProvider -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import timber.log.Timber -import java.util.Locale - -class RoomListScreen( - context: Context, - private val matrixClient: MatrixClient, - private val coroutineDispatchers: CoroutineDispatchers = Singleton.coroutineDispatchers, -) { - private val clock = Clock.System - private val locale = Locale.getDefault() - private val dateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.currentSystemDefault() } - private val dateFormatters = DateFormatters(locale, clock) { TimeZone.currentSystemDefault() } - private val sessionVerificationService = matrixClient.sessionVerificationService() - private val encryptionService = matrixClient.encryptionService() - private val stringProvider = AndroidStringProvider(context.resources) - private val featureFlagService = AlwaysEnabledFeatureFlagService() - private val roomListRoomSummaryFactory = RoomListRoomSummaryFactory( - lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter( - localDateTimeProvider = dateTimeProvider, - dateFormatters = dateFormatters - ), - roomLastMessageFormatter = DefaultRoomLastMessageFormatter( - sp = stringProvider, - roomMembershipContentFormatter = RoomMembershipContentFormatter( - matrixClient = matrixClient, - sp = stringProvider - ), - profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), - stateContentFormatter = StateContentFormatter(stringProvider), - permalinkParser = OnlyFallbackPermalinkParser(), - ), - ) - private val presenter = RoomListPresenter( - client = matrixClient, - networkMonitor = DefaultNetworkMonitor(context, Singleton.appScope), - snackbarDispatcher = SnackbarDispatcher(), - leaveRoomPresenter = LeaveRoomPresenter(matrixClient, RoomMembershipObserver(), coroutineDispatchers), - roomListDataSource = RoomListDataSource( - roomListService = matrixClient.roomListService, - roomListRoomSummaryFactory = roomListRoomSummaryFactory, - coroutineDispatchers = coroutineDispatchers, - notificationSettingsService = matrixClient.notificationSettingsService(), - appScope = Singleton.appScope, - dateTimeObserver = DefaultDateTimeObserver(context), - ), - indicatorService = DefaultIndicatorService( - sessionVerificationService = sessionVerificationService, - encryptionService = encryptionService, - ), - featureFlagService = featureFlagService, - searchPresenter = RoomListSearchPresenter( - RoomListSearchDataSource( - roomListService = matrixClient.roomListService, - roomSummaryFactory = roomListRoomSummaryFactory, - coroutineDispatchers = coroutineDispatchers, - ), - featureFlagService = featureFlagService, - ), - sessionPreferencesStore = DefaultSessionPreferencesStore( - context = context, - sessionId = matrixClient.sessionId, - sessionCoroutineScope = Singleton.appScope - ), - filtersPresenter = RoomListFiltersPresenter( - roomListService = matrixClient.roomListService, - filterSelectionStrategy = DefaultFilterSelectionStrategy(), - ), - acceptDeclineInvitePresenter = AcceptDeclineInvitePresenter( - client = matrixClient, - joinRoom = DefaultJoinRoom(matrixClient, NoopAnalyticsService()), - notificationCleaner = FakeNotificationCleaner(), - ), - analyticsService = NoopAnalyticsService(), - fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() }, - notificationCleaner = FakeNotificationCleaner(), - logoutPresenter = DirectLogoutPresenter(matrixClient, encryptionService), - ) - - @Composable - fun Content(modifier: Modifier = Modifier) { - fun onRoomClick(roomId: RoomId) { - Singleton.appScope.launch { - withContext(coroutineDispatchers.io) { - matrixClient.getRoom(roomId)!!.use { room -> - room.liveTimeline.paginate(Timeline.PaginationDirection.BACKWARDS) - } - } - } - } - - val state = presenter.present() - RoomListView( - state = state, - onRoomClick = ::onRoomClick, - onSettingsClick = {}, - onSetUpRecoveryClick = {}, - onConfirmRecoveryKeyClick = {}, - onCreateRoomClick = {}, - onRoomSettingsClick = {}, - onMenuActionClick = {}, - onRoomDirectorySearchClick = {}, - modifier = modifier, - acceptDeclineInviteView = { - AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onAcceptInvite = {}, onDeclineInvite = {}) - }, - onMigrateToNativeSlidingSyncClick = {}, - ) - - DisposableEffect(Unit) { - Timber.w("Start sync!") - runBlocking { - matrixClient.syncService().startSync() - } - onDispose { - Timber.w("Stop sync!") - runBlocking { - matrixClient.syncService().stopSync() - } - } - } - } -} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/Singleton.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/Singleton.kt deleted file mode 100644 index 7b33dda53b..0000000000 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/Singleton.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.samples.minimal - -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.core.meta.BuildType -import io.element.android.libraries.matrix.api.tracing.TracingConfiguration -import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations -import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration -import io.element.android.libraries.matrix.impl.tracing.RustTracingService -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.plus - -object Singleton { - val buildMeta = BuildMeta( - isDebuggable = true, - buildType = BuildType.DEBUG, - applicationName = "EAX-Minimal", - productionApplicationName = "EAX-Minimal", - desktopApplicationName = "EAX-Minimal-Desktop", - applicationId = "io.element.android.samples.minimal", - isEnterpriseBuild = false, - lowPrivacyLoggingEnabled = false, - versionName = "0.1.0", - versionCode = 1, - gitRevision = "", - gitBranchName = "", - flavorDescription = "NA", - flavorShortDescription = "NA", - ) - - init { - val tracingConfiguration = TracingConfiguration( - filterConfiguration = TracingFilterConfigurations.debug, - writesToLogcat = true, - writesToFilesConfiguration = WriteToFilesConfiguration.Disabled - ) - RustTracingService(buildMeta).setupTracing(tracingConfiguration) - } - - val appScope = MainScope() + CoroutineName("Minimal Scope") - val coroutineDispatchers = CoroutineDispatchers( - io = Dispatchers.IO, - computation = Dispatchers.Default, - main = Dispatchers.Main, - ) -} diff --git a/samples/minimal/src/main/res/mipmap-hdpi/ic_launcher.webp b/samples/minimal/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78ecd372343283f4157dcfd918ec5165bb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG diff --git a/samples/minimal/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/samples/minimal/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2898 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 diff --git a/samples/minimal/src/main/res/mipmap-mdpi/ic_launcher.webp b/samples/minimal/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i diff --git a/samples/minimal/src/main/res/mipmap-xhdpi/ic_launcher.webp b/samples/minimal/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070fe34c611c42c0d3ad3013a0dce358be0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? diff --git a/samples/minimal/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/samples/minimal/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s diff --git a/samples/minimal/src/main/res/values-night/themes.xml b/samples/minimal/src/main/res/values-night/themes.xml deleted file mode 100644 index bcbbf3cdbd..0000000000 --- a/samples/minimal/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - -