Merge pull request #3868 from element-hq/feature/fga/create_room_improve_address
Create room : improve handling of room address
This commit is contained in:
@@ -19,6 +19,4 @@ data class CreateRoomConfig(
|
||||
val avatarUri: Uri? = null,
|
||||
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
|
||||
) {
|
||||
val isValid = roomName.isNullOrEmpty().not() && roomVisibility.isValid()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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<CreateRoomConfig> = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<ConfigureRoomState> {
|
||||
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>(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<AsyncAction<RoomId>> = 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<String>,
|
||||
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<AsyncAction<RoomId>>
|
||||
@@ -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(
|
||||
|
||||
@@ -20,6 +20,10 @@ data class ConfigureRoomState(
|
||||
val avatarActions: ImmutableList<AvatarAction>,
|
||||
val createRoomAction: AsyncAction<RoomId>,
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -28,9 +28,8 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
invites = aMatrixUserList().toImmutableList(),
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room 101"),
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
roomAddressErrorState = RoomAddressErrorState.None,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -40,12 +39,44 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
invites = aMatrixUserList().toImmutableList(),
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room 101"),
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
roomAddressErrorState = RoomAddressErrorState.None,
|
||||
),
|
||||
),
|
||||
),
|
||||
aConfigureRoomState(
|
||||
config = CreateRoomConfig(
|
||||
roomName = "Room 101",
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
),
|
||||
),
|
||||
roomAddressValidity = RoomAddressValidity.NotAvailable,
|
||||
),
|
||||
aConfigureRoomState(
|
||||
config = CreateRoomConfig(
|
||||
roomName = "Room 101",
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
),
|
||||
),
|
||||
roomAddressValidity = RoomAddressValidity.InvalidSymbols,
|
||||
),
|
||||
aConfigureRoomState(
|
||||
config = CreateRoomConfig(
|
||||
roomName = "Room 101",
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
roomVisibility = RoomVisibilityState.Public(
|
||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
||||
roomAccess = RoomAccess.Knocking,
|
||||
),
|
||||
),
|
||||
roomAddressValidity = RoomAddressValidity.Valid,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,6 +87,7 @@ fun aConfigureRoomState(
|
||||
createRoomAction: AsyncAction<RoomId> = 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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ConfigureRoomState>.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)
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
@@ -168,3 +168,13 @@ fun MatrixClient.getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<Ma
|
||||
.map { roomSummary -> 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<RoomAlias> {
|
||||
return runCatching {
|
||||
RoomAlias("#$name:${userIdServerName()}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@ data class CreateRoomParameters(
|
||||
val invite: List<UserId>? = null,
|
||||
val avatar: String? = null,
|
||||
val joinRuleOverride: JoinRuleOverride = JoinRuleOverride.None,
|
||||
val canonicalAlias: Optional<String> = Optional.empty(),
|
||||
val roomAliasName: Optional<String> = Optional.empty(),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user