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/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/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index bf76f208e3..9fd823445f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -212,7 +212,7 @@ class MessagesNode @AssistedInject constructor( state = state, onBackClick = this::navigateUp, onRoomDetailsClick = this::onRoomDetailsClick, - onEventClick = this::onEventClick, + onEventContentClick = this::onEventClick, onPreviewAttachments = this::onPreviewAttachments, onUserDataClick = this::onUserDataClick, onLinkClick = { url -> onLinkClick(activity, isDark, url, state.timelineState.eventSink) }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index e5d73fbed6..3e2918b3ce 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -114,7 +114,7 @@ fun MessagesView( state: MessagesState, onBackClick: () -> Unit, onRoomDetailsClick: () -> Unit, - onEventClick: (event: TimelineItem.Event) -> Boolean, + onEventContentClick: (event: TimelineItem.Event) -> Boolean, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, onPreviewAttachments: (ImmutableList) -> Unit, @@ -142,9 +142,9 @@ fun MessagesView( // This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose val localView = LocalView.current - fun onMessageClick(event: TimelineItem.Event) { + fun onContentClick(event: TimelineItem.Event) { Timber.v("onMessageClick= ${event.id}") - val hideKeyboard = onEventClick(event) + val hideKeyboard = onEventContentClick(event) if (hideKeyboard) { localView.hideKeyboard() } @@ -206,7 +206,7 @@ fun MessagesView( modifier = Modifier .padding(padding) .consumeWindowInsets(padding), - onMessageClick = ::onMessageClick, + onContentClick = ::onContentClick, onMessageLongClick = ::onMessageLongClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, @@ -306,7 +306,7 @@ private fun AttachmentStateView( @Composable private fun MessagesViewContent( state: MessagesState, - onMessageClick: (TimelineItem.Event) -> Unit, + onContentClick: (TimelineItem.Event) -> Unit, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, @@ -382,7 +382,7 @@ private fun MessagesViewContent( timelineProtectionState = state.timelineProtectionState, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, - onMessageClick = onMessageClick, + onContentClick = onContentClick, onMessageLongClick = onMessageLongClick, onSwipeToReply = onSwipeToReply, onReactionClick = onReactionClick, @@ -568,7 +568,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) state = state, onBackClick = {}, onRoomDetailsClick = {}, - onEventClick = { false }, + onEventContentClick = { false }, onUserDataClick = {}, onLinkClick = {}, onPreviewAttachments = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt index c34f072c6d..04750b6ad8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt @@ -33,7 +33,7 @@ internal fun MessagesViewWithIdentityChangePreview( ), onBackClick = {}, onRoomDetailsClick = {}, - onEventClick = { false }, + onEventContentClick = { false }, onUserDataClick = {}, onLinkClick = {}, onPreviewAttachments = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index f4a3247cba..7ae88b9647 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -216,7 +216,7 @@ private fun PinnedMessagesListLoaded( focusedEventId = null, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, - onClick = onEventClick, + onContentClick = onEventClick, onLongClick = ::onMessageLongClick, inReplyToClick = {}, onReactionClick = { _, _ -> }, @@ -230,6 +230,7 @@ private fun PinnedMessagesListLoaded( TimelineItemEventContentViewWrapper( event = event, timelineProtectionState = state.timelineProtectionState, + onContentClick = { onEventClick(event) }, onLinkClick = onLinkClick, modifier = contentModifier, onContentLayoutChange = onContentLayoutChange @@ -244,6 +245,7 @@ private fun PinnedMessagesListLoaded( private fun TimelineItemEventContentViewWrapper( event: TimelineItem.Event, timelineProtectionState: TimelineProtectionState, + onContentClick: () -> Unit, onLinkClick: (String) -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, @@ -258,10 +260,11 @@ private fun TimelineItemEventContentViewWrapper( TimelineItemEventContentView( content = event.content, hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), - onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, + onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = { }, modifier = modifier, + onContentClick = onContentClick, onContentLayoutChange = onContentLayoutChange ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index f0e976e368..6e34bba9e1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -76,7 +76,7 @@ fun TimelineView( timelineProtectionState: TimelineProtectionState, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, - onMessageClick: (TimelineItem.Event) -> Unit, + onContentClick: (TimelineItem.Event) -> Unit, onMessageLongClick: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit, onReactionClick: (emoji: String, TimelineItem.Event) -> Unit, @@ -141,7 +141,7 @@ fun TimelineView( focusedEventId = state.focusedEventId, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, - onClick = onMessageClick, + onContentClick = onContentClick, onLongClick = onMessageLongClick, inReplyToClick = ::inReplyToClick, onReactionClick = onReactionClick, @@ -322,7 +322,7 @@ internal fun TimelineViewPreview( timelineProtectionState = aTimelineProtectionState(), onUserDataClick = {}, onLinkClick = {}, - onMessageClick = {}, + onContentClick = {}, onMessageLongClick = {}, onSwipeToReply = {}, onReactionClick = { _, _ -> }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt index 99f69675fa..462e74137a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt @@ -41,7 +41,7 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview { timelineProtectionState = aTimelineProtectionState(), onUserDataClick = {}, onLinkClick = {}, - onMessageClick = {}, + onContentClick = {}, onMessageLongClick = {}, onSwipeToReply = {}, onReactionClick = { _, _ -> }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt index 1fa5e7f9a1..85e9854919 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt @@ -30,7 +30,7 @@ internal fun ATimelineItemEventRow( timelineProtectionState = timelineProtectionState, isLastOutgoingMessage = isLastOutgoingMessage, isHighlighted = isHighlighted, - onClick = {}, + onContentClick = {}, onLongClick = {}, onLinkClick = {}, onUserDataClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 2f0165cd7b..0061ebe6df 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -114,7 +114,7 @@ fun TimelineItemEventRow( renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, isHighlighted: Boolean, - onClick: () -> Unit, + onContentClick: () -> Unit, onLongClick: () -> Unit, onLinkClick: (String) -> Unit, onUserDataClick: (UserId) -> Unit, @@ -130,7 +130,8 @@ fun TimelineItemEventRow( TimelineItemEventContentView( content = event.content, hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), - onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, + onContentClick = onContentClick, + onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = eventSink, modifier = contentModifier, @@ -150,6 +151,12 @@ fun TimelineItemEventRow( inReplyToClick(inReplyToEventId) } + val onWholeItemClick = if (event.isWholeContentClickable) { + onContentClick + } else { + {} + } + Column(modifier = modifier.fillMaxWidth()) { if (event.groupPosition.isNew()) { Spacer(modifier = Modifier.height(16.dp)) @@ -173,7 +180,7 @@ fun TimelineItemEventRow( isHighlighted = isHighlighted, timelineRoomInfo = timelineRoomInfo, interactionSource = interactionSource, - onClick = onClick, + onContentClick = onWholeItemClick, onLongClick = onLongClick, inReplyToClick = ::inReplyToClick, onUserDataClick = ::onUserDataClick, @@ -207,7 +214,7 @@ fun TimelineItemEventRow( isHighlighted = isHighlighted, timelineRoomInfo = timelineRoomInfo, interactionSource = interactionSource, - onClick = onClick, + onContentClick = onWholeItemClick, onLongClick = onLongClick, inReplyToClick = ::inReplyToClick, onUserDataClick = ::onUserDataClick, @@ -263,7 +270,7 @@ private fun TimelineItemEventRowContent( isHighlighted: Boolean, timelineRoomInfo: TimelineRoomInfo, interactionSource: MutableInteractionSource, - onClick: () -> Unit, + onContentClick: () -> Unit, onLongClick: () -> Unit, inReplyToClick: () -> Unit, onUserDataClick: () -> Unit, @@ -340,7 +347,7 @@ private fun TimelineItemEventRowContent( }, state = bubbleState, interactionSource = interactionSource, - onClick = onClick, + onClick = onContentClick, onLongClick = onLongClick, ) { MessageEventBubbleContent( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index d280efa62c..fe89d6de02 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -57,10 +57,11 @@ fun TimelineItemGroupedEventsRow( TimelineItemEventContentView( content = event.content, hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), - onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, + onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = eventSink, modifier = contentModifier, + onContentClick = {}, onContentLayoutChange = onContentLayoutChange ) }, @@ -121,10 +122,11 @@ private fun TimelineItemGroupedEventsRowContent( TimelineItemEventContentView( content = event.content, hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), - onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, + onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = eventSink, modifier = contentModifier, + onContentClick = {}, onContentLayoutChange = onContentLayoutChange ) }, @@ -152,7 +154,7 @@ private fun TimelineItemGroupedEventsRowContent( focusedEventId = focusedEventId, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, - onClick = onClick, + onContentClick = onClick, onLongClick = onLongClick, inReplyToClick = inReplyToClick, onReactionClick = onReactionClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 13c247645b..047f9b3d04 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -28,7 +28,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState -import io.element.android.features.messages.impl.timeline.protection.mustBeProtected import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor import io.element.android.libraries.matrix.api.core.EventId @@ -44,7 +43,7 @@ internal fun TimelineItemRow( focusedEventId: EventId?, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, - onClick: (TimelineItem.Event) -> Unit, + onContentClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, @@ -60,7 +59,8 @@ internal fun TimelineItemRow( TimelineItemEventContentView( content = event.content, hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), - onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, + onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, + onContentClick = { onContentClick(event) }, onLinkClick = onLinkClick, eventSink = eventSink, modifier = contentModifier, @@ -95,7 +95,7 @@ internal fun TimelineItemRow( renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, isHighlighted = timelineItem.isEvent(focusedEventId), - onClick = { onClick(timelineItem) }, + onClick = { onContentClick(timelineItem) }, onReadReceiptsClick = onReadReceiptClick, onLongClick = { onLongClick(timelineItem) }, eventSink = eventSink, @@ -118,11 +118,7 @@ internal fun TimelineItemRow( timelineProtectionState = timelineProtectionState, isLastOutgoingMessage = isLastOutgoingMessage, isHighlighted = timelineItem.isEvent(focusedEventId), - onClick = if (timelineProtectionState.hideMediaContent(timelineItem.eventId) && timelineItem.mustBeProtected()) { - {} - } else { - { onClick(timelineItem) } - }, + onContentClick = { onContentClick(timelineItem) }, onLongClick = { onLongClick(timelineItem) }, onLinkClick = onLinkClick, onUserDataClick = onUserDataClick, @@ -148,7 +144,7 @@ internal fun TimelineItemRow( renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, focusedEventId = focusedEventId, - onClick = onClick, + onClick = onContentClick, onLongClick = onLongClick, inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt index f023a63c35..6172686016 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt @@ -72,8 +72,9 @@ fun TimelineItemStateEventRow( content = event.content, onLinkClick = {}, hideMediaContent = false, - onShowClick = {}, + onShowContentClick = {}, eventSink = eventSink, + onContentClick = {}, modifier = Modifier.defaultTimelineContentPadding() ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt index b24a5ca0be..3a3a43a6fc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt @@ -36,7 +36,8 @@ import io.element.android.libraries.architecture.Presenter fun TimelineItemEventContentView( content: TimelineItemEventContent, hideMediaContent: Boolean, - onShowClick: () -> Unit, + onContentClick: () -> Unit, + onShowContentClick: () -> Unit, onLinkClick: (url: String) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier, @@ -67,25 +68,31 @@ fun TimelineItemEventContentView( ) is TimelineItemLocationContent -> TimelineItemLocationView( content = content, + onContentClick = onContentClick, modifier = modifier ) is TimelineItemImageContent -> TimelineItemImageView( content = content, hideMediaContent = hideMediaContent, - onShowClick = onShowClick, + onContentClick = onContentClick, + onShowContentClick = onShowContentClick, + onLinkClick = onLinkClick, onContentLayoutChange = onContentLayoutChange, modifier = modifier, ) is TimelineItemStickerContent -> TimelineItemStickerView( content = content, hideMediaContent = hideMediaContent, - onShowClick = onShowClick, + onContentClick = onContentClick, + onShowClick = onShowContentClick, modifier = modifier, ) is TimelineItemVideoContent -> TimelineItemVideoView( content = content, hideMediaContent = hideMediaContent, - onShowClick = onShowClick, + onContentClick = onContentClick, + onShowContentClick = onShowContentClick, + onLinkClick = onLinkClick, onContentLayoutChange = onContentLayoutChange, modifier = modifier ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index 63bbac7f6f..123ba195f1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.timeline.components.event import android.text.SpannedString import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -50,7 +51,6 @@ import io.element.android.features.messages.impl.timeline.protection.ProtectedVi import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.wysiwyg.compose.EditorStyledText @@ -59,7 +59,9 @@ import io.element.android.wysiwyg.compose.EditorStyledText fun TimelineItemImageView( content: TimelineItemImageContent, hideMediaContent: Boolean, - onShowClick: () -> Unit, + onContentClick: () -> Unit, + onLinkClick: (String) -> Unit, + onShowContentClick: () -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, ) { @@ -78,13 +80,14 @@ fun TimelineItemImageView( ) { ProtectedView( hideContent = hideMediaContent, - onShowClick = onShowClick, + onShowClick = onShowContentClick, ) { var isLoaded by remember { mutableStateOf(false) } AsyncImage( modifier = Modifier .fillMaxWidth() - .then(if (isLoaded) Modifier.background(Color.White) else Modifier), + .then(if (isLoaded) Modifier.background(Color.White) else Modifier) + .clickable(onClick = onContentClick), model = content.thumbnailMediaRequestData, contentScale = ContentScale.Fit, alignment = Alignment.Center, @@ -99,9 +102,7 @@ fun TimelineItemImageView( val caption = if (LocalInspectionMode.current) { SpannedString(content.caption) } else { - content.formattedCaption?.body - ?.takeIf { content.formattedCaption.format == MessageFormat.HTML } - ?: SpannedString(content.caption) + content.formattedCaption ?: SpannedString(content.caption) } CompositionLocalProvider( LocalContentColor provides ElementTheme.colors.textPrimary, @@ -114,6 +115,7 @@ fun TimelineItemImageView( .widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio), text = caption, style = ElementRichTextEditorStyle.textStyle(), + onLinkClickedListener = onLinkClick, releaseOnDetach = false, onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange), ) @@ -128,7 +130,9 @@ internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageCon TimelineItemImageView( content = content, hideMediaContent = false, - onShowClick = {}, + onShowContentClick = {}, + onContentClick = {}, + onLinkClick = {}, onContentLayoutChange = {}, ) } @@ -139,7 +143,9 @@ internal fun TimelineItemImageViewHideMediaContentPreview() = ElementPreview { TimelineItemImageView( content = aTimelineItemImageContent(), hideMediaContent = true, - onShowClick = {}, + onShowContentClick = {}, + onContentClick = {}, + onLinkClick = {}, onContentLayoutChange = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt index 91cfcff1d8..5d402bc8f1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt @@ -7,6 +7,7 @@ package io.element.android.features.messages.impl.timeline.components.event +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn @@ -25,9 +26,10 @@ import io.element.android.libraries.designsystem.theme.components.Text @Composable fun TimelineItemLocationView( content: TimelineItemLocationContent, + onContentClick: () -> Unit, modifier: Modifier = Modifier, ) { - Column(modifier = modifier.fillMaxWidth()) { + Column(modifier = modifier.clickable(onClick = onContentClick).fillMaxWidth()) { content.description?.let { Text( text = it, @@ -51,5 +53,8 @@ fun TimelineItemLocationView( @Composable internal fun TimelineItemLocationViewPreview(@PreviewParameter(TimelineItemLocationContentProvider::class) content: TimelineItemLocationContent) = ElementPreview { - TimelineItemLocationView(content) + TimelineItemLocationView( + content = content, + onContentClick = {}, + ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt index 0574843dba..df087769ed 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -40,6 +41,7 @@ private const val STICKER_SIZE_IN_DP = 128 fun TimelineItemStickerView( content: TimelineItemStickerContent, hideMediaContent: Boolean, + onContentClick: () -> Unit, onShowClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -61,7 +63,8 @@ fun TimelineItemStickerView( AsyncImage( modifier = Modifier .fillMaxSize() - .then(if (isLoaded) Modifier.background(Color.White) else Modifier), + .then(if (isLoaded) Modifier.background(Color.White) else Modifier) + .clickable(onClick = onContentClick), model = MediaRequestData( source = content.preferredMediaSource, kind = MediaRequestData.Kind.File( @@ -85,6 +88,7 @@ internal fun TimelineItemStickerViewPreview(@PreviewParameter(TimelineItemSticke TimelineItemStickerView( content = content, hideMediaContent = false, + onContentClick = {}, onShowClick = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index 64e6d00d71..65f59cd631 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline.components.event import android.text.SpannedString import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -56,7 +57,6 @@ import io.element.android.libraries.designsystem.components.blurhash.blurHashBac import io.element.android.libraries.designsystem.modifiers.roundedBackground import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_HEIGHT import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH import io.element.android.libraries.matrix.ui.media.MediaRequestData @@ -68,7 +68,9 @@ import io.element.android.wysiwyg.compose.EditorStyledText fun TimelineItemVideoView( content: TimelineItemVideoContent, hideMediaContent: Boolean, - onShowClick: () -> Unit, + onContentClick: () -> Unit, + onShowContentClick: () -> Unit, + onLinkClick: (String) -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, ) { @@ -90,13 +92,14 @@ fun TimelineItemVideoView( ) { ProtectedView( hideContent = hideMediaContent, - onShowClick = onShowClick, + onShowClick = onShowContentClick, ) { var isLoaded by remember { mutableStateOf(false) } AsyncImage( modifier = Modifier .fillMaxWidth() - .then(if (isLoaded) Modifier.background(Color.White) else Modifier), + .then(if (isLoaded) Modifier.background(Color.White) else Modifier) + .clickable(onClick = onContentClick), model = MediaRequestData( source = content.thumbnailSource, kind = MediaRequestData.Kind.Thumbnail( @@ -128,9 +131,7 @@ fun TimelineItemVideoView( val caption = if (LocalInspectionMode.current) { SpannedString(content.caption) } else { - content.formattedCaption?.body - ?.takeIf { content.formattedCaption.format == MessageFormat.HTML } - ?: SpannedString(content.caption) + content.formattedCaption ?: SpannedString(content.caption) } CompositionLocalProvider( LocalContentColor provides ElementTheme.colors.textPrimary, @@ -142,6 +143,7 @@ fun TimelineItemVideoView( .padding(horizontal = 4.dp) // This is (12.dp - 8.dp) contentPadding from CommonLayout .widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio), text = caption, + onLinkClickedListener = onLinkClick, style = ElementRichTextEditorStyle.textStyle(), releaseOnDetach = false, onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange), @@ -157,7 +159,9 @@ internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoCon TimelineItemVideoView( content = content, hideMediaContent = false, - onShowClick = {}, + onShowContentClick = {}, + onContentClick = {}, + onLinkClick = {}, onContentLayoutChange = {}, ) } @@ -168,7 +172,9 @@ internal fun TimelineItemVideoViewHideMediaContentPreview() = ElementPreview { TimelineItemVideoView( content = aTimelineItemVideoContent(), hideMediaContent = true, - onShowClick = {}, + onShowContentClick = {}, + onContentClick = {}, + onLinkClick = {}, onContentLayoutChange = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index 001d40b254..ff5ca57c11 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -86,7 +86,7 @@ class TimelineItemContentMessageFactory @Inject constructor( TimelineItemImageContent( filename = messageType.filename, caption = messageType.caption?.trimEnd(), - formattedCaption = messageType.formattedCaption, + formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(), mediaSource = messageType.source, thumbnailSource = messageType.info?.thumbnailSource, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, @@ -105,7 +105,7 @@ class TimelineItemContentMessageFactory @Inject constructor( TimelineItemStickerContent( filename = messageType.filename, caption = messageType.caption?.trimEnd(), - formattedCaption = messageType.formattedCaption, + formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(), mediaSource = messageType.source, thumbnailSource = messageType.info?.thumbnailSource, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, @@ -142,7 +142,7 @@ class TimelineItemContentMessageFactory @Inject constructor( TimelineItemVideoContent( filename = messageType.filename, caption = messageType.caption?.trimEnd(), - formattedCaption = messageType.formattedCaption, + formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(), thumbnailSource = messageType.info?.thumbnailSource, videoSource = messageType.source, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, @@ -161,7 +161,7 @@ class TimelineItemContentMessageFactory @Inject constructor( TimelineItemAudioContent( filename = messageType.filename, caption = messageType.caption?.trimEnd(), - formattedCaption = messageType.formattedCaption, + formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(), mediaSource = messageType.source, duration = messageType.info?.duration ?: Duration.ZERO, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, @@ -176,7 +176,7 @@ class TimelineItemContentMessageFactory @Inject constructor( eventId = eventId, filename = messageType.filename, caption = messageType.caption?.trimEnd(), - formattedCaption = messageType.formattedCaption, + formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(), mediaSource = messageType.source, duration = messageType.info?.duration ?: Duration.ZERO, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, @@ -187,7 +187,7 @@ class TimelineItemContentMessageFactory @Inject constructor( TimelineItemAudioContent( filename = messageType.filename, caption = messageType.caption?.trimEnd(), - formattedCaption = messageType.formattedCaption, + formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(), mediaSource = messageType.source, duration = messageType.info?.duration ?: Duration.ZERO, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, @@ -202,7 +202,7 @@ class TimelineItemContentMessageFactory @Inject constructor( TimelineItemFileContent( filename = messageType.filename, caption = messageType.caption?.trimEnd(), - formattedCaption = messageType.formattedCaption, + formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(), thumbnailSource = messageType.info?.thumbnailSource, fileSource = messageType.source, mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index dcf0e16aa8..e2c81a3794 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -9,8 +9,10 @@ package io.element.android.features.messages.impl.timeline.model import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.EventId @@ -93,6 +95,17 @@ sealed interface TimelineItem { val isRemote = eventId != null + /** Whether a click on any part of the event bubble should trigger the 'onContentClick' callback. + * + * This is `true` for all events except for visual media events with a caption or formatted caption. + */ + val isWholeContentClickable = when (content) { + is TimelineItemStickerContent -> content.formattedCaption == null && content.caption == null + is TimelineItemImageContent -> content.formattedCaption == null && content.caption == null + is TimelineItemVideoContent -> content.formattedCaption == null && content.caption == null + else -> true + } + val eventOrTransactionId: EventOrTransactionId get() = EventOrTransactionId.from(eventId = eventId, transactionId = transactionId) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt index fa2957b860..0ecf45ec55 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt @@ -8,14 +8,13 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize import kotlin.time.Duration data class TimelineItemAudioContent( override val filename: String, override val caption: String?, - override val formattedCaption: FormattedBody?, + override val formattedCaption: CharSequence?, val duration: Duration, val mediaSource: MediaSource, val mimeType: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt index 10204a75d8..fafadcbf79 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt @@ -8,7 +8,6 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.runtime.Immutable -import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody @Immutable sealed interface TimelineItemEventContent { @@ -19,7 +18,7 @@ sealed interface TimelineItemEventContent { sealed interface TimelineItemEventContentWithAttachment : TimelineItemEventContent { val filename: String val caption: String? - val formattedCaption: FormattedBody? + val formattedCaption: CharSequence? val bestDescription: String get() = caption ?: filename diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt index f11325727c..b7007c8bbc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt @@ -8,13 +8,12 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize data class TimelineItemFileContent( override val filename: String, override val caption: String?, - override val formattedCaption: FormattedBody?, + override val formattedCaption: CharSequence?, val fileSource: MediaSource, val thumbnailSource: MediaSource?, val formattedFileSize: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt index e6e4bffb9b..b13c7a4e37 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt @@ -9,7 +9,6 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAnimatedImage import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_HEIGHT import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH import io.element.android.libraries.matrix.ui.media.MediaRequestData @@ -17,7 +16,7 @@ import io.element.android.libraries.matrix.ui.media.MediaRequestData data class TimelineItemImageContent( override val filename: String, override val caption: String?, - override val formattedCaption: FormattedBody?, + override val formattedCaption: CharSequence?, val mediaSource: MediaSource, val thumbnailSource: MediaSource?, val formattedFileSize: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContent.kt index 3476d97e2f..06886307ae 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContent.kt @@ -8,12 +8,11 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody data class TimelineItemStickerContent( override val filename: String, override val caption: String?, - override val formattedCaption: FormattedBody?, + override val formattedCaption: CharSequence?, val mediaSource: MediaSource, val thumbnailSource: MediaSource?, val formattedFileSize: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt index 5c0e601708..486a71b5d4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt @@ -8,13 +8,12 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import kotlin.time.Duration data class TimelineItemVideoContent( override val filename: String, override val caption: String?, - override val formattedCaption: FormattedBody?, + override val formattedCaption: CharSequence?, val duration: Duration, val videoSource: MediaSource, val thumbnailSource: MediaSource?, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt index 853cc01045..ebeabef715 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt @@ -9,7 +9,6 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import kotlinx.collections.immutable.ImmutableList import kotlin.time.Duration @@ -17,7 +16,7 @@ data class TimelineItemVoiceContent( val eventId: EventId?, override val filename: String, override val caption: String?, - override val formattedCaption: FormattedBody?, + override val formattedCaption: CharSequence?, val duration: Duration, val mediaSource: MediaSource, val mimeType: String, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index a20acaa5df..c040ce9926 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -529,7 +529,7 @@ private fun AndroidComposeTestRule.setMessa state = state, onBackClick = onBackClick, onRoomDetailsClick = onRoomDetailsClick, - onEventClick = onEventClick, + onEventContentClick = onEventClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, onPreviewAttachments = onPreviewAttachments, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 315abd8302..6a7b49c34c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -158,7 +158,7 @@ private fun AndroidComposeTestRule.setTimel timelineProtectionState = timelineProtectionState, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, - onMessageClick = onMessageClick, + onContentClick = onMessageClick, onMessageLongClick = onMessageLongClick, onSwipeToReply = onSwipeToReply, onReactionClick = onReactionClick, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt index e9343bf21d..a34e601e5d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt @@ -286,7 +286,7 @@ class TimelineItemContentMessageFactoryTest { val expected = TimelineItemVideoContent( filename = "body.mp4", caption = "body.mp4 caption", - formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"), + formattedCaption = SpannedString("formatted"), duration = 1.minutes, videoSource = MediaSource(url = "url", json = null), thumbnailSource = MediaSource("url_thumbnail"), @@ -527,7 +527,7 @@ class TimelineItemContentMessageFactoryTest { ) val expected = TimelineItemImageContent( filename = "body.jpg", - formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"), + formattedCaption = SpannedString("formatted"), caption = "body.jpg caption", mediaSource = MediaSource(url = "url", json = null), thumbnailSource = MediaSource("url_thumbnail"), 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/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index fa3264f784..9c6eeeff21 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -145,7 +145,7 @@ private fun RoomDirectoryRoomList( Text( text = stringResource(id = CommonStrings.common_no_results), style = ElementTheme.typography.fontBodyLgRegular, - color = ElementTheme.colors.textPlaceholder, + color = ElementTheme.colors.textSecondary, modifier = Modifier.padding(16.dp) ) } @@ -185,8 +185,8 @@ private fun SearchTextField( colors: TextFieldColors = TextFieldDefaults.colors( focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, - unfocusedPlaceholderColor = ElementTheme.colors.textPlaceholder, - focusedPlaceholderColor = ElementTheme.colors.textPlaceholder, + unfocusedPlaceholderColor = ElementTheme.colors.textSecondary, + focusedPlaceholderColor = ElementTheme.colors.textSecondary, focusedTextColor = ElementTheme.colors.textPrimary, unfocusedTextColor = ElementTheme.colors.textPrimary, focusedIndicatorColor = ElementTheme.colors.borderInteractiveSecondary, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/NumberedListMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/NumberedListMolecule.kt index 29cdfdab4f..8d03c82a2f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/NumberedListMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/NumberedListMolecule.kt @@ -41,7 +41,7 @@ fun NumberedListMolecule( private fun ItemNumber( index: Int, ) { - val color = ElementTheme.colors.textPlaceholder + val color = ElementTheme.colors.textSecondary Box( modifier = Modifier .border(1.dp, color, CircleShape) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt index 7516cfe46e..964c245d02 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt @@ -200,7 +200,7 @@ private fun DecorationBox( if (placeholder != null && isTextEmpty) { Text( text = placeholder, - color = ElementTheme.colors.textPlaceholder, + color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodyLgRegular, ) } 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) + } +} 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 c209e78ecd..0000000000 Binary files a/samples/minimal/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ 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 b2dfe3d1ba..0000000000 Binary files a/samples/minimal/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ 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 4f0f1d64e5..0000000000 Binary files a/samples/minimal/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/samples/minimal/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/samples/minimal/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611da08..0000000000 Binary files a/samples/minimal/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ 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 948a3070fe..0000000000 Binary files a/samples/minimal/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ 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 1b9a6956b3..0000000000 Binary files a/samples/minimal/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/samples/minimal/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/samples/minimal/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77f9f..0000000000 Binary files a/samples/minimal/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/samples/minimal/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/samples/minimal/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50836..0000000000 Binary files a/samples/minimal/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/samples/minimal/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/samples/minimal/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d6427e6..0000000000 Binary files a/samples/minimal/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/samples/minimal/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/samples/minimal/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae37cb..0000000000 Binary files a/samples/minimal/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ 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 @@ - - - -