Merge branch 'develop' into feature/bma/metro070

This commit is contained in:
Benoit Marty 2025-10-23 11:30:25 +02:00 committed by GitHub
commit 76493f52ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 306 additions and 233 deletions

View file

@ -39,7 +39,7 @@ class DefaultAccountProviderAccessControl(
// Ensure that Element Pro is not required for this account provider
val wellKnown = wellknownRetriever.getElementWellKnown(
baseUrl = accountProviderUrl.ensureProtocol(),
)
).dataOrNull()
if (wellKnown?.enforceElementPro == true) {
throw AccountProviderAccessException.NeedElementProException(
unauthorisedAccountProviderTitle = title,

View file

@ -46,7 +46,7 @@ class HomeserverResolver(
wellknownRetriever.getWellKnown(url)
}
}
val isValid = wellKnown?.isValid().orFalse()
val isValid = wellKnown?.dataOrNull()?.isValid().orFalse()
if (isValid) {
// Emit the list as soon as possible
currentList.add(

View file

@ -28,7 +28,7 @@ class DefaultWebClientUrlForAuthenticationRetriever(
Timber.w("Temporary account creation flow is only supported on matrix.org")
throw AccountCreationNotSupported()
}
val wellknown = wellknownRetriever.getElementWellKnown(homeServerUrl)
val wellknown = wellknownRetriever.getElementWellKnown(homeServerUrl).dataOrNull()
?: throw AccountCreationNotSupported()
val registrationHelperUrl = wellknown.registrationHelperUrl
return if (registrationHelperUrl != null) {

View file

@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows
import org.junit.Test
@ -155,7 +156,13 @@ class DefaultAccountProviderAccessControlTest {
defaultHomeserverListResult = { allowedAccountProviders },
),
wellknownRetriever = FakeWellknownRetriever(
getElementWellKnownResult = { elementWellKnown },
getElementWellKnownResult = {
if (elementWellKnown == null) {
WellknownRetrieverResult.NotFound
} else {
WellknownRetrieverResult.Success(elementWellKnown)
}
},
),
)

View file

@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.WellknownRetriever
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@ -114,9 +115,11 @@ class ChangeServerPresenterTest {
@Test
fun `present - change server element pro required error`() = runTest {
val getElementWellKnownResult = lambdaRecorder<String, ElementWellKnown> {
anElementWellKnown(
enforceElementPro = true,
val getElementWellKnownResult = lambdaRecorder<String, WellknownRetrieverResult<ElementWellKnown>> {
WellknownRetrieverResult.Success(
anElementWellKnown(
enforceElementPro = true,
)
)
}
createPresenter(

View file

@ -18,6 +18,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.wellknown.api.WellKnown
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@ -94,12 +95,12 @@ class SearchAccountProviderPresenterTest {
@Test
fun `present - enter text one result with wellknown`() = runTest {
val getWellKnownResult = lambdaRecorder<String, WellKnown> {
val getWellKnownResult = lambdaRecorder<String, WellknownRetrieverResult<WellKnown>> {
when (it) {
"https://test.org" -> error("not found")
"https://test.com" -> error("not found")
"https://test.io" -> aWellKnown()
"https://test" -> error("not found")
"https://test.org" -> WellknownRetrieverResult.NotFound
"https://test.com" -> WellknownRetrieverResult.NotFound
"https://test.io" -> WellknownRetrieverResult.Success(aWellKnown())
"https://test" -> WellknownRetrieverResult.NotFound
else -> error("should not happen")
}
}
@ -138,12 +139,12 @@ class SearchAccountProviderPresenterTest {
@Test
fun `present - enter text two results with wellknown`() = runTest {
val getWellKnownResult = lambdaRecorder<String, WellKnown> {
val getWellKnownResult = lambdaRecorder<String, WellknownRetrieverResult<WellKnown>> {
when (it) {
"https://test.org" -> aWellKnown()
"https://test.com" -> error("not found")
"https://test.io" -> aWellKnown()
"https://test" -> error("not found")
"https://test.org" -> WellknownRetrieverResult.Success(aWellKnown())
"https://test.com" -> WellknownRetrieverResult.NotFound
"https://test.io" -> WellknownRetrieverResult.Success(aWellKnown())
"https://test" -> WellknownRetrieverResult.NotFound
else -> error("should not happen")
}
}

View file

@ -164,7 +164,7 @@ class RoomDetailsPresenter(
val securityAndPrivacyPermissions = room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value)
val canShowSecurityAndPrivacy by remember {
derivedStateOf {
isKnockRequestsEnabled && roomType is RoomDetailsType.Room && securityAndPrivacyPermissions.value.hasAny
roomType is RoomDetailsType.Room && securityAndPrivacyPermissions.value.hasAny
}
}

View file

@ -27,6 +27,8 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.JoinedRoom
@ -45,6 +47,7 @@ class SecurityAndPrivacyPresenter(
@Assisted private val navigator: SecurityAndPrivacyNavigator,
private val matrixClient: MatrixClient,
private val room: JoinedRoom,
private val featureFlagService: FeatureFlagService,
) : Presenter<SecurityAndPrivacyState> {
@AssistedFactory
interface Factory {
@ -55,6 +58,9 @@ class SecurityAndPrivacyPresenter(
override fun present(): SecurityAndPrivacyState {
val coroutineScope = rememberCoroutineScope()
val isKnockEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
}.collectAsState(false)
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val homeserverName = remember { matrixClient.userIdServerName() }
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
@ -149,6 +155,7 @@ class SecurityAndPrivacyPresenter(
editedSettings = editedSettings,
homeserverName = homeserverName,
showEnableEncryptionConfirmation = showEnableEncryptionConfirmation,
isKnockEnabled = isKnockEnabled,
saveAction = saveAction.value,
permissions = permissions,
eventSink = ::handleEvents

View file

@ -19,6 +19,7 @@ data class SecurityAndPrivacyState(
val editedSettings: SecurityAndPrivacySettings,
val homeserverName: String,
val showEnableEncryptionConfirmation: Boolean,
val isKnockEnabled: Boolean,
val saveAction: AsyncAction<Unit>,
private val permissions: SecurityAndPrivacyPermissions,
val eventSink: (SecurityAndPrivacyEvents) -> Unit

View file

@ -30,7 +30,8 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAn
aSecurityAndPrivacyState(
savedSettings = aSecurityAndPrivacySettings(
roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember
)
),
isKnockEnabled = false,
),
aSecurityAndPrivacyState(
editedSettings = aSecurityAndPrivacySettings(
@ -54,6 +55,12 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAn
aSecurityAndPrivacyState(
saveAction = AsyncAction.Loading
),
aSecurityAndPrivacyState(
savedSettings = aSecurityAndPrivacySettings(
roomAccess = SecurityAndPrivacyRoomAccess.AskToJoin
),
isKnockEnabled = false,
),
)
}
@ -83,6 +90,7 @@ fun aSecurityAndPrivacyState(
canChangeEncryption = true,
canChangeRoomVisibility = true
),
isKnockEnabled: Boolean = true,
eventSink: (SecurityAndPrivacyEvents) -> Unit = {}
) = SecurityAndPrivacyState(
editedSettings = editedSettings,
@ -90,6 +98,7 @@ fun aSecurityAndPrivacyState(
homeserverName = homeserverName,
showEnableEncryptionConfirmation = showEncryptionConfirmation,
saveAction = saveAction,
isKnockEnabled = isKnockEnabled,
permissions = permissions,
eventSink = eventSink
)

View file

@ -81,6 +81,7 @@ fun SecurityAndPrivacyView(
modifier = Modifier.padding(top = 24.dp),
edited = state.editedSettings.roomAccess,
saved = state.savedSettings.roomAccess,
isKnockEnabled = state.isKnockEnabled,
onSelectOption = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) },
)
}
@ -176,6 +177,7 @@ private fun SecurityAndPrivacySection(
private fun RoomAccessSection(
edited: SecurityAndPrivacyRoomAccess,
saved: SecurityAndPrivacyRoomAccess,
isKnockEnabled: Boolean,
onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit,
modifier: Modifier = Modifier,
) {
@ -189,12 +191,18 @@ private fun RoomAccessSection(
trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.InviteOnly),
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.InviteOnly) },
)
ListItem(
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_title)) },
supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_description)) },
trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.AskToJoin),
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.AskToJoin) },
)
// Show Ask to join option in two cases:
// - the Knock FF is enabled
// - AskToJoin is the current saved value
if (saved == SecurityAndPrivacyRoomAccess.AskToJoin || isKnockEnabled) {
ListItem(
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_title)) },
supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_description)) },
trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.AskToJoin),
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.AskToJoin) },
enabled = isKnockEnabled,
)
}
ListItem(
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_anyone_option_title)) },
supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_anyone_option_description)) },

View file

@ -719,10 +719,6 @@ class RoomDetailsPresenterTest {
val presenter = createRoomDetailsPresenter(room = room, featureFlagService = featureFlagService)
presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) {
skipItems(1)
with(awaitItem()) {
assertThat(canShowSecurityAndPrivacy).isFalse()
}
featureFlagService.setFeatureEnabled(FeatureFlags.Knock, true)
with(awaitItem()) {
assertThat(canShowSecurityAndPrivacy).isTrue()
}

View file

@ -10,6 +10,9 @@ package io.element.android.features.roomdetails.impl.securityandprivacy
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
@ -38,6 +41,7 @@ class SecurityAndPrivacyPresenterTest {
assertThat(showRoomVisibilitySections).isFalse()
assertThat(showHistoryVisibilitySection).isFalse()
assertThat(showEncryptionSection).isFalse()
assertThat(isKnockEnabled).isFalse()
}
with(awaitItem()) {
assertThat(editedSettings).isEqualTo(savedSettings)
@ -48,6 +52,7 @@ class SecurityAndPrivacyPresenterTest {
assertThat(showRoomVisibilitySections).isFalse()
assertThat(showHistoryVisibilitySection).isTrue()
assertThat(showEncryptionSection).isTrue()
assertThat(isKnockEnabled).isFalse()
}
}
}
@ -56,14 +61,14 @@ class SecurityAndPrivacyPresenterTest {
fun `present - room info change updates saved and edited settings`() = runTest {
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
canSendStateResult = { _, _ -> Result.success(true) },
initialRoomInfo = aRoomInfo(
joinRule = JoinRule.Public,
historyVisibility = RoomHistoryVisibility.WorldReadable,
canonicalAlias = A_ROOM_ALIAS,
canSendStateResult = { _, _ -> Result.success(true) },
initialRoomInfo = aRoomInfo(
joinRule = JoinRule.Public,
historyVisibility = RoomHistoryVisibility.WorldReadable,
canonicalAlias = A_ROOM_ALIAS,
)
)
)
)
val presenter = createSecurityAndPrivacyPresenter(room = room)
presenter.test {
skipItems(1)
@ -163,10 +168,10 @@ class SecurityAndPrivacyPresenterTest {
fun `present - room visibility loading and change`() = runTest {
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
canSendStateResult = { _, _ -> Result.success(true) },
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared)
)
canSendStateResult = { _, _ -> Result.success(true) },
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared)
)
)
val presenter = createSecurityAndPrivacyPresenter(room = room)
presenter.test {
@ -212,10 +217,10 @@ class SecurityAndPrivacyPresenterTest {
val updateRoomHistoryVisibilityLambda = lambdaRecorder<RoomHistoryVisibility, Result<Unit>> { Result.success(Unit) }
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
canSendStateResult = { _, _ -> Result.success(true) },
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite, historyVisibility = RoomHistoryVisibility.Shared)
),
canSendStateResult = { _, _ -> Result.success(true) },
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite, historyVisibility = RoomHistoryVisibility.Shared)
),
enableEncryptionResult = enableEncryptionLambda,
updateJoinRuleResult = updateJoinRuleLambda,
updateRoomVisibilityResult = updateRoomVisibilityLambda,
@ -279,10 +284,10 @@ class SecurityAndPrivacyPresenterTest {
val updateRoomHistoryVisibilityLambda = lambdaRecorder<RoomHistoryVisibility, Result<Unit>> { Result.success(Unit) }
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
canSendStateResult = { _, _ -> Result.success(true) },
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private)
),
canSendStateResult = { _, _ -> Result.success(true) },
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private)
),
enableEncryptionResult = enableEncryptionLambda,
updateJoinRuleResult = updateJoinRuleLambda,
updateRoomVisibilityResult = updateRoomVisibilityLambda,
@ -323,7 +328,8 @@ class SecurityAndPrivacyPresenterTest {
)
// Saved settings are updated 2 times to match the edited settings
skipItems(3)
with(awaitItem()) {
val state = awaitItem()
with(state) {
assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat(savedSettings.isVisibleInRoomDirectory).isNotEqualTo(editedSettings.isVisibleInRoomDirectory)
assertThat(canBeSaved).isTrue()
@ -332,6 +338,26 @@ class SecurityAndPrivacyPresenterTest {
assert(updateJoinRuleLambda).isCalledOnce()
assert(updateRoomVisibilityLambda).isCalledOnce()
assert(updateRoomHistoryVisibilityLambda).isCalledOnce()
// Clear error
state.eventSink(SecurityAndPrivacyEvents.DismissSaveError)
with(awaitItem()) {
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
}
}
}
@Test
fun `present - isKnockEnabled is true if the Knock feature flag is enabled`() = runTest {
val presenter = createSecurityAndPrivacyPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(
FeatureFlags.Knock.key to true,
)
)
)
presenter.test {
assertThat(awaitItem().isKnockEnabled).isFalse()
assertThat(awaitItem().isKnockEnabled).isTrue()
}
}
@ -345,13 +371,15 @@ class SecurityAndPrivacyPresenterTest {
),
),
navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
): SecurityAndPrivacyPresenter {
return SecurityAndPrivacyPresenter(
room = room,
matrixClient = FakeMatrixClient(
userIdServerNameLambda = { serverName },
),
navigator = navigator
navigator = navigator,
featureFlagService = featureFlagService,
)
}
}