Merge branch 'develop' into feature/valere/timeline_active_call_rendering

This commit is contained in:
Valere
2026-07-15 14:12:54 +02:00
33 changed files with 184 additions and 44 deletions
@@ -0,0 +1,21 @@
appId: ${MAESTRO_APP_ID}
---
# Clean up the room list if it was left in a bad state by previous tests
- runFlow:
when:
visible: 'aRoomName'
commands:
- longPressOn: 'aRoomName'
- tapOn:
text: "Leave room"
index: 0
- tapOn: "Leave"
- runFlow:
when:
visible: ${MAESTRO_INVITEE1_MXID}
commands:
- longPressOn: ${MAESTRO_INVITEE1_MXID}
- tapOn:
text: "Leave room"
index: 0
- tapOn: "Leave"
+10 -1
View File
@@ -7,7 +7,16 @@ appId: ${MAESTRO_APP_ID}
- tapOn: - tapOn:
text: ${MAESTRO_INVITEE1_MXID} text: ${MAESTRO_INVITEE1_MXID}
index: 1 index: 1
- tapOn: "Continue" - runFlow:
when:
visible: 'Start a chat with this new contact?'
commands:
- tapOn: 'Continue'
- runFlow:
when:
visible: 'Send invite?'
commands:
- tapOn: 'Send invite'
- takeScreenshot: build/maestro/330-createAndDeleteDM - takeScreenshot: build/maestro/330-createAndDeleteDM
- tapOn: "maestroelement2" - tapOn: "maestroelement2"
- scroll - scroll
@@ -15,6 +15,12 @@ appId: ${MAESTRO_APP_ID}
text: ${MAESTRO_INVITEE1_MXID} text: ${MAESTRO_INVITEE1_MXID}
index: 1 index: 1
- tapOn: "Finish" - tapOn: "Finish"
- runFlow:
when:
visible: 'Invite new contact to this room?'
commands:
- tapOn:
id: "confirm_invite_unknown"
- tapOn: "aRoomName" - tapOn: "aRoomName"
- tapOn: "Invite" - tapOn: "Invite"
# assert there's 1 member and 1 invitee # assert there's 1 member and 1 invitee
@@ -23,6 +29,10 @@ appId: ${MAESTRO_APP_ID}
- tapOn: - tapOn:
text: ${MAESTRO_INVITEE2_MXID} text: ${MAESTRO_INVITEE2_MXID}
index: 1 index: 1
# Hide keyboard by tapping on an external element in case it's displayed, otherwise it will 'eat' the back button press
- tapOn:
# Navigation title
text: "Invite people"
- tapOn: "Invite" - tapOn: "Invite"
- runFlow: - runFlow:
when: when:
@@ -30,10 +40,6 @@ appId: ${MAESTRO_APP_ID}
commands: commands:
- tapOn: - tapOn:
id: "confirm_invite_unknown" id: "confirm_invite_unknown"
# Close the keyboard if it's still open
- tapOn: "Back"
# Go back to the room details screen
- tapOn: "Back"
- scrollUntilVisible: - scrollUntilVisible:
direction: DOWN direction: DOWN
element: element:
+1
View File
@@ -1,5 +1,6 @@
appId: ${MAESTRO_APP_ID} appId: ${MAESTRO_APP_ID}
--- ---
- runFlow: cleanUpRoomList.yaml
- runFlow: searchRoomList.yaml - runFlow: searchRoomList.yaml
- takeScreenshot: build/maestro/300-RoomList - takeScreenshot: build/maestro/300-RoomList
- runFlow: timeline/timeline.yaml - runFlow: timeline/timeline.yaml
@@ -22,7 +22,6 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.call.api.CallData import io.element.android.features.call.api.CallData
import io.element.android.features.call.impl.data.WidgetMessage import io.element.android.features.call.impl.data.WidgetMessage
import io.element.android.features.call.impl.utils.ActiveCallManager import io.element.android.features.call.impl.utils.ActiveCallManager
@@ -84,7 +83,7 @@ class CallScreenPresenter(
var ignoreWebViewError by rememberSaveable { mutableStateOf(false) } var ignoreWebViewError by rememberSaveable { mutableStateOf(false) }
var webViewError by remember { mutableStateOf<String?>(null) } var webViewError by remember { mutableStateOf<String?>(null) }
val languageTag = languageTagProvider.provideLanguageTag() val languageTag = languageTagProvider.provideLanguageTag()
val theme = if (ElementTheme.isLightTheme) "light" else "dark" val theme = "dark"
DisposableEffect(Unit) { DisposableEffect(Unit) {
coroutineScope.launch { coroutineScope.launch {
@@ -38,6 +38,7 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.compound.colors.SemanticColorsLightDark import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.compound.theme.ForcedDarkElementTheme
import io.element.android.features.call.api.CallData import io.element.android.features.call.api.CallData
import io.element.android.features.call.impl.DefaultElementCallEntryPoint import io.element.android.features.call.impl.DefaultElementCallEntryPoint
import io.element.android.features.call.impl.di.CallBindings import io.element.android.features.call.impl.di.CallBindings
@@ -143,24 +144,28 @@ class ElementCallActivity :
compoundDark = colors.dark, compoundDark = colors.dark,
buildMeta = buildMeta, buildMeta = buildMeta,
) { ) {
val state = presenter.present() ForcedDarkElementTheme(
eventSink = state.eventSink colors = colors,
LaunchedEffect(state.isCallActive) { ) {
if (state.isCallActive) { val state = presenter.present()
setCallIsActive() eventSink = state.eventSink
LaunchedEffect(state.isCallActive) {
if (state.isCallActive) {
setCallIsActive()
}
} }
CallScreenView(
state = state,
pipState = pipState,
onConsoleMessage = {
consoleMessageLogger.log("ElementCall", it)
},
requestPermissions = { permissions, callback ->
requestPermissionCallback = callback
requestPermissionsLauncher.launch(permissions)
}
)
} }
CallScreenView(
state = state,
pipState = pipState,
onConsoleMessage = {
consoleMessageLogger.log("ElementCall", it)
},
requestPermissions = { permissions, callback ->
requestPermissionCallback = callback
requestPermissionsLauncher.launch(permissions)
}
)
} }
} }
} }
@@ -43,6 +43,7 @@ dependencies {
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.api)
implementation(projects.features.invitepeople.api) implementation(projects.features.invitepeople.api)
implementation(projects.features.enterprise.api)
api(projects.features.createroom.api) api(projects.features.createroom.api)
testCommonDependencies(libs, true) testCommonDependencies(libs, true)
@@ -53,4 +54,5 @@ dependencies {
testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.permissions.test)
testImplementation(projects.features.startchat.test) testImplementation(projects.features.startchat.test)
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.features.enterprise.test)
} }
@@ -24,6 +24,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
@@ -73,6 +74,7 @@ class ConfigureRoomPresenter(
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val roomAliasHelper: RoomAliasHelper, private val roomAliasHelper: RoomAliasHelper,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider, private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
private val sessionEnterpriseService: SessionEnterpriseService,
) : Presenter<ConfigureRoomState> { ) : Presenter<ConfigureRoomState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@@ -256,7 +258,7 @@ class ConfigureRoomPresenter(
CreateRoomParameters( CreateRoomParameters(
name = config.roomName, name = config.roomName,
topic = config.topic, topic = config.topic,
isEncrypted = true, isEncrypted = !sessionEnterpriseService.isEncryptionDisabledByHomeserver(),
isDirect = false, isDirect = false,
visibility = RoomVisibility.Private, visibility = RoomVisibility.Private,
historyVisibilityOverride = RoomHistoryVisibility.Invited, historyVisibilityOverride = RoomHistoryVisibility.Invited,
@@ -20,11 +20,13 @@ import io.element.android.features.createroom.impl.configureroom.CreateRoomConfi
import io.element.android.features.createroom.impl.configureroom.JoinRuleItem import io.element.android.features.createroom.impl.configureroom.JoinRuleItem
import io.element.android.features.createroom.impl.configureroom.RoomAddress import io.element.android.features.createroom.impl.configureroom.RoomAddress
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId 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.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias 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.api.room.alias.RoomAliasHelper
@@ -56,6 +58,7 @@ import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.matching
import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.robolectric.RobolectricTest
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import io.mockk.mockk import io.mockk.mockk
@@ -517,6 +520,30 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
} }
} }
@Test
fun `present - creating a private room when encryption is disabled by the HS creates it unencrypted`() = runTest {
val sessionEnterpriseService = FakeSessionEnterpriseService(isEncryptionDisabledResult = { true })
val createRoomLambda = lambdaRecorder<CreateRoomParameters, Result<RoomId>> { Result.success(A_ROOM_ID) }
val matrixClient = createMatrixClient().apply {
createRoomResult = createRoomLambda
}
val presenter = createConfigureRoomPresenter(
matrixClient = matrixClient,
sessionEnterpriseService = sessionEnterpriseService,
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PrivateVisibility.Private))
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
assertThat(awaitItem().createRoomAction.isLoading()).isTrue()
assertThat(awaitItem().createRoomAction.isSuccess()).isTrue()
createRoomLambda.assertions().isCalledOnce().with(matching<CreateRoomParameters> { !it.isEncrypted })
}
}
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState { private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
skipItems(1) skipItems(1)
return awaitItem() return awaitItem()
@@ -552,6 +579,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
isKnockFeatureEnabled: Boolean = true, isKnockFeatureEnabled: Boolean = true,
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
sessionEnterpriseService: FakeSessionEnterpriseService = FakeSessionEnterpriseService(isEncryptionDisabledResult = { false }),
) = ConfigureRoomPresenter( ) = ConfigureRoomPresenter(
isSpace = isSpace, isSpace = isSpace,
initialParentSpaceId = initialParenSpaceId, initialParentSpaceId = initialParenSpaceId,
@@ -566,5 +594,6 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled) mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled)
), ),
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider, mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
sessionEnterpriseService = sessionEnterpriseService,
) )
} }
@@ -12,5 +12,7 @@ interface SessionEnterpriseService {
suspend fun isElementCallAvailable(): Boolean suspend fun isElementCallAvailable(): Boolean
suspend fun tweakMasUrl(url: String): String suspend fun tweakMasUrl(url: String): String
suspend fun isEncryptionDisabledByHomeserver(): Boolean
suspend fun init() suspend fun init()
} }
@@ -17,4 +17,5 @@ class DefaultSessionEnterpriseService : SessionEnterpriseService {
override suspend fun init() = Unit override suspend fun init() = Unit
override suspend fun tweakMasUrl(url: String): String = url override suspend fun tweakMasUrl(url: String): String = url
override suspend fun isElementCallAvailable(): Boolean = true override suspend fun isElementCallAvailable(): Boolean = true
override suspend fun isEncryptionDisabledByHomeserver(): Boolean = false
} }
@@ -15,6 +15,7 @@ import io.element.android.tests.testutils.simulateLongTask
class FakeSessionEnterpriseService( class FakeSessionEnterpriseService(
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() }, private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
private val tweakMasUrlResult: (String) -> String = { lambdaError() }, private val tweakMasUrlResult: (String) -> String = { lambdaError() },
private val isEncryptionDisabledResult: () -> Boolean = { lambdaError() },
) : SessionEnterpriseService { ) : SessionEnterpriseService {
override suspend fun init() { override suspend fun init() {
} }
@@ -26,4 +27,8 @@ class FakeSessionEnterpriseService(
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask { override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
isElementCallAvailableResult() isElementCallAvailableResult()
} }
override suspend fun isEncryptionDisabledByHomeserver(): Boolean {
return isEncryptionDisabledResult()
}
} }
@@ -302,13 +302,13 @@ private fun InvitePeopleConfirmModal(
text = stringResource(CommonStrings.action_remove), text = stringResource(CommonStrings.action_remove),
onClick = onRemove, onClick = onRemove,
leadingIcon = IconSource.Vector(CompoundIcons.Close()), leadingIcon = IconSource.Vector(CompoundIcons.Close()),
modifier = Modifier.weight(1f).testTag(TestTags.confirmInviteUnknown), modifier = Modifier.weight(1f),
) )
Button( Button(
text = stringResource(CommonStrings.action_invite), text = stringResource(CommonStrings.action_invite),
onClick = onInvite, onClick = onInvite,
leadingIcon = IconSource.Vector(CompoundIcons.Check()), leadingIcon = IconSource.Vector(CompoundIcons.Check()),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f).testTag(TestTags.confirmInviteUnknown),
) )
} }
} }
@@ -41,10 +41,12 @@ dependencies {
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.api)
implementation(projects.features.enterprise.api)
testCommonDependencies(libs, true) testCommonDependencies(libs, true)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.testtags) testImplementation(projects.libraries.testtags)
testImplementation(projects.features.enterprise.test)
} }
@@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator
@@ -63,6 +64,7 @@ class SecurityAndPrivacyPresenter(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val room: JoinedRoom, private val room: JoinedRoom,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val sessionEnterpriseService: SessionEnterpriseService,
) : Presenter<SecurityAndPrivacyState> { ) : Presenter<SecurityAndPrivacyState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@@ -83,6 +85,10 @@ class SecurityAndPrivacyPresenter(
val homeserverName = remember { matrixClient.userIdServerName() } val homeserverName = remember { matrixClient.userIdServerName() }
val roomInfo by room.roomInfoFlow.collectAsState() val roomInfo by room.roomInfoFlow.collectAsState()
val isEncryptionDisabledByHomeserver by produceState(false) {
value = sessionEnterpriseService.isEncryptionDisabledByHomeserver()
}
val savedIsVisibleInRoomDirectory = remember { mutableStateOf<AsyncData<Boolean>>(AsyncData.Uninitialized) } val savedIsVisibleInRoomDirectory = remember { mutableStateOf<AsyncData<Boolean>>(AsyncData.Uninitialized) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
isRoomVisibleInRoomDirectory(savedIsVisibleInRoomDirectory) isRoomVisibleInRoomDirectory(savedIsVisibleInRoomDirectory)
@@ -247,6 +253,7 @@ class SecurityAndPrivacyPresenter(
isSpace = roomInfo.isSpace, isSpace = roomInfo.isSpace,
selectableJoinedSpaces = selectableJoinedSpaces, selectableJoinedSpaces = selectableJoinedSpaces,
spaceSelectionMode = spaceSelectionMode, spaceSelectionMode = spaceSelectionMode,
isEncryptionDisabledByHomeserver = isEncryptionDisabledByHomeserver,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
@@ -27,6 +27,7 @@ data class SecurityAndPrivacyState(
// the settings the user wants to apply. // the settings the user wants to apply.
val editedSettings: SecurityAndPrivacySettings, val editedSettings: SecurityAndPrivacySettings,
val homeserverName: String, val homeserverName: String,
val isEncryptionDisabledByHomeserver: Boolean,
val showEnableEncryptionConfirmation: Boolean, val showEnableEncryptionConfirmation: Boolean,
private val isKnockEnabled: Boolean, private val isKnockEnabled: Boolean,
val saveAction: AsyncAction<Unit>, val saveAction: AsyncAction<Unit>,
@@ -84,7 +85,7 @@ data class SecurityAndPrivacyState(
editedSettings.roomAccess.canConfigureRoomVisibility() editedSettings.roomAccess.canConfigureRoomVisibility()
val showHistoryVisibilitySection = permissions.canChangeHistoryVisibility && !isSpace val showHistoryVisibilitySection = permissions.canChangeHistoryVisibility && !isSpace
val showEncryptionSection = permissions.canChangeEncryption && !isSpace val showEncryptionSection = !isEncryptionDisabledByHomeserver && permissions.canChangeEncryption && !isSpace
@Composable @Composable
fun spaceMemberDescription(): String { fun spaceMemberDescription(): String {
@@ -138,6 +138,7 @@ fun aSecurityAndPrivacyState(
isSpace: Boolean = false, isSpace: Boolean = false,
selectableJoinedSpaces: Set<SpaceRoom> = emptySet(), selectableJoinedSpaces: Set<SpaceRoom> = emptySet(),
spaceSelectionMode: SpaceSelectionMode = SpaceSelectionMode.None, spaceSelectionMode: SpaceSelectionMode = SpaceSelectionMode.None,
isEncryptionDisabledByHomeserver: Boolean = false,
eventSink: (SecurityAndPrivacyEvent) -> Unit = {} eventSink: (SecurityAndPrivacyEvent) -> Unit = {}
) = SecurityAndPrivacyState( ) = SecurityAndPrivacyState(
editedSettings = editedSettings, editedSettings = editedSettings,
@@ -150,5 +151,6 @@ fun aSecurityAndPrivacyState(
isSpace = isSpace, isSpace = isSpace,
selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(), selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(),
spaceSelectionMode = spaceSelectionMode, spaceSelectionMode = spaceSelectionMode,
isEncryptionDisabledByHomeserver = isEncryptionDisabledByHomeserver,
eventSink = eventSink, eventSink = eventSink,
) )
@@ -8,6 +8,7 @@
package io.element.android.features.securityandprivacy.impl.root package io.element.android.features.securityandprivacy.impl.root
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
import io.element.android.features.securityandprivacy.impl.FakeSecurityAndPrivacyNavigator import io.element.android.features.securityandprivacy.impl.FakeSecurityAndPrivacyNavigator
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator
import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.SpaceSelectionStateHolder import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.SpaceSelectionStateHolder
@@ -1046,6 +1047,20 @@ class SecurityAndPrivacyPresenterTest {
} }
} }
@Test
fun `present - showEncryptionSection is false if the HS disabled encryption`() = runTest {
val sessionEnterpriseService = FakeSessionEnterpriseService(isEncryptionDisabledResult = { true })
val presenter = createSecurityAndPrivacyPresenter(
sessionEnterpriseService = sessionEnterpriseService,
)
presenter.test {
skipItems(1)
// The HS has disabled encryption, so the encryption section should not be shown
assertThat(awaitItem().showEncryptionSection).isFalse()
}
}
private fun roomPermissions( private fun roomPermissions(
canChangeRoomAccess: Boolean = true, canChangeRoomAccess: Boolean = true,
canChangeHistoryVisibility: Boolean = true, canChangeHistoryVisibility: Boolean = true,
@@ -1084,6 +1099,9 @@ class SecurityAndPrivacyPresenterTest {
), ),
), ),
spaceSelectionStateHolder: SpaceSelectionStateHolder = SpaceSelectionStateHolder(), spaceSelectionStateHolder: SpaceSelectionStateHolder = SpaceSelectionStateHolder(),
sessionEnterpriseService: FakeSessionEnterpriseService = FakeSessionEnterpriseService(
isEncryptionDisabledResult = { false },
)
): SecurityAndPrivacyPresenter { ): SecurityAndPrivacyPresenter {
return SecurityAndPrivacyPresenter( return SecurityAndPrivacyPresenter(
room = room, room = room,
@@ -1091,6 +1109,7 @@ class SecurityAndPrivacyPresenterTest {
navigator = navigator, navigator = navigator,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
spaceSelectionStateHolder = spaceSelectionStateHolder, spaceSelectionStateHolder = spaceSelectionStateHolder,
sessionEnterpriseService = sessionEnterpriseService,
) )
} }
} }
+3 -3
View File
@@ -175,7 +175,7 @@ test_truth = "com.google.truth:truth:1.4.5"
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.22" test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.22"
test_robolectric = "org.robolectric:robolectric:4.16.1" test_robolectric = "org.robolectric:robolectric:4.16.1"
test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" }
test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.9.0" test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.9.1"
test_detekt_api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" } test_detekt_api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" }
test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" } test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" }
@@ -209,7 +209,7 @@ sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", ver
sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
sqlcipher = "net.zetetic:sqlcipher-android:4.16.0" sqlcipher = "net.zetetic:sqlcipher-android:4.16.0"
sqlite = "androidx.sqlite:sqlite-ktx:2.6.2" sqlite = "androidx.sqlite:sqlite-ktx:2.7.0"
unifiedpush = "org.unifiedpush.android:connector:3.3.3" unifiedpush = "org.unifiedpush.android:connector:3.3.3"
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0" vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil3", version.ref = "telephoto" } telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil3", version.ref = "telephoto" }
@@ -220,7 +220,7 @@ maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
maplibre_compose = "org.maplibre.compose:maplibre-compose:0.13.0" maplibre_compose = "org.maplibre.compose:maplibre-compose:0.13.0"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
opusencoder = "io.element.android:opusencoder:1.2.0" opusencoder = "io.element.android:opusencoder:1.2.0"
zxing_cpp = "io.github.zxing-cpp:android:3.0.2" zxing_cpp = "io.github.zxing-cpp:android:3.1.0"
google_zxing = "com.google.zxing:core:3.5.4" google_zxing = "com.google.zxing:core:3.5.4"
google_guava = "com.google.guava:guava:33.6.0-android" google_guava = "com.google.guava:guava:33.6.0-android"
@@ -90,7 +90,6 @@ sealed interface NotificationContent {
val question: String, val question: String,
) : MessageLike ) : MessageLike
// TODO put real implementation
data object Beacon : MessageLike data object Beacon : MessageLike
} }
@@ -119,9 +118,7 @@ sealed interface NotificationContent {
data class RoomTopic(val topic: String) : StateEvent data class RoomTopic(val topic: String) : StateEvent
data object SpaceChild : StateEvent data object SpaceChild : StateEvent
data object SpaceParent : StateEvent data object SpaceParent : StateEvent
data object BeaconInfo : StateEvent
// TODO put real implementation
data object Beacon : StateEvent
} }
data class Invite( data class Invite(
@@ -71,7 +71,7 @@ private fun StateEventContent.toContent(): NotificationContent.StateEvent {
is StateEventContent.RoomTopic -> NotificationContent.StateEvent.RoomTopic(topic) is StateEventContent.RoomTopic -> NotificationContent.StateEvent.RoomTopic(topic)
StateEventContent.SpaceChild -> NotificationContent.StateEvent.SpaceChild StateEventContent.SpaceChild -> NotificationContent.StateEvent.SpaceChild
StateEventContent.SpaceParent -> NotificationContent.StateEvent.SpaceParent StateEventContent.SpaceParent -> NotificationContent.StateEvent.SpaceParent
StateEventContent.BeaconInfo -> NotificationContent.StateEvent.Beacon StateEventContent.BeaconInfo -> NotificationContent.StateEvent.BeaconInfo
} }
} }
@@ -90,6 +90,8 @@ class RustMatrixClientTest {
userId = userId, userId = userId,
displayName = A_USER_NAME, displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
status = null,
call = null,
) )
}, },
), ),
@@ -9,14 +9,20 @@
package io.element.android.libraries.matrix.impl.fixtures.factories package io.element.android.libraries.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import org.matrix.rustcomponents.sdk.UserCall
import org.matrix.rustcomponents.sdk.UserProfile import org.matrix.rustcomponents.sdk.UserProfile
import org.matrix.rustcomponents.sdk.UserStatus
internal fun aRustUserProfile( internal fun aRustUserProfile(
userId: String = A_USER_ID.value, userId: String = A_USER_ID.value,
displayName: String = "displayName", displayName: String = "displayName",
avatarUrl: String = "avatarUrl", avatarUrl: String = "avatarUrl",
status: UserStatus? = null,
call: UserCall? = null,
) = UserProfile( ) = UserProfile(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,
status = status,
call = call,
) )
@@ -9,6 +9,7 @@
package io.element.android.libraries.matrix.impl.fixtures.fakes package io.element.android.libraries.matrix.impl.fixtures.fakes
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUserProfile
import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
@@ -49,7 +50,7 @@ class FakeFfiClient(
private val session: Session = aRustSession(), private val session: Session = aRustSession(),
private val clearCachesResult: () -> Unit = { lambdaError() }, private val clearCachesResult: () -> Unit = { lambdaError() },
private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() }, private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() },
private val getProfileResult: (String) -> UserProfile = { UserProfile(userId = userId, displayName = null, avatarUrl = null) }, private val getProfileResult: (String) -> UserProfile = { aRustUserProfile() },
private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() }, private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() },
private val getStoreSizesResult: () -> StoreSizes = { lambdaError() }, private val getStoreSizesResult: () -> StoreSizes = { lambdaError() },
private val createRoomResult: (CreateRoomParameters) -> String = { lambdaError() }, private val createRoomResult: (CreateRoomParameters) -> String = { lambdaError() },
@@ -126,7 +126,7 @@ class FakeMatrixClient(
private val getMapStyleUrlResult: () -> Result<String?> = { lambdaError() }, private val getMapStyleUrlResult: () -> Result<String?> = { lambdaError() },
private val getDatabaseSizesLambda: () -> Result<SdkStoreSizes> = { lambdaError() }, private val getDatabaseSizesLambda: () -> Result<SdkStoreSizes> = { lambdaError() },
private val resetWellKnownConfigLambda: () -> Result<Unit> = { lambdaError() }, private val resetWellKnownConfigLambda: () -> Result<Unit> = { lambdaError() },
override val contentScanner: ContentScanner? = null override val contentScanner: ContentScanner? = null,
) : MatrixClient { ) : MatrixClient {
var setDisplayNameCalled: Boolean = false var setDisplayNameCalled: Boolean = false
private set private set
@@ -144,7 +144,7 @@ class FakeMatrixClient(
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl)) private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl))
override val userProfile: StateFlow<MatrixUser> = _userProfile override val userProfile: StateFlow<MatrixUser> = _userProfile
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID) var createRoomResult: (CreateRoomParameters) -> Result<RoomId> = { Result.success(A_ROOM_ID) }
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID) private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var findDmResult: Result<RoomId?> = Result.success(A_ROOM_ID) private var findDmResult: Result<RoomId?> = Result.success(A_ROOM_ID)
private val getRoomResults = mutableMapOf<RoomId, BaseRoom>() private val getRoomResults = mutableMapOf<RoomId, BaseRoom>()
@@ -193,7 +193,7 @@ class FakeMatrixClient(
} }
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> = simulateLongTask { override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> = simulateLongTask {
return createRoomResult return createRoomResult(createRoomParams)
} }
override suspend fun createDM(userId: UserId): Result<RoomId> = simulateLongTask { override suspend fun createDM(userId: UserId): Result<RoomId> = simulateLongTask {
@@ -297,7 +297,7 @@ class FakeMatrixClient(
// Mocks // Mocks
fun givenCreateRoomResult(result: Result<RoomId>) { fun givenCreateRoomResult(result: Result<RoomId>) {
createRoomResult = result createRoomResult = { result }
} }
fun givenCreateDmResult(result: Result<RoomId>) { fun givenCreateDmResult(result: Result<RoomId>) {
@@ -299,6 +299,10 @@ class DefaultNotifiableEventResolver(
Timber.tag(loggerTag.value).d("Ignoring notification for sticker") Timber.tag(loggerTag.value).d("Ignoring notification for sticker")
throw NotificationResolverException.EventFilteredOut throw NotificationResolverException.EventFilteredOut
} }
NotificationContent.MessageLike.Beacon -> {
Timber.tag(loggerTag.value).d("Ignoring notification for beacon")
throw NotificationResolverException.EventFilteredOut
}
is NotificationContent.StateEvent.RoomMemberContent, is NotificationContent.StateEvent.RoomMemberContent,
NotificationContent.StateEvent.PolicyRuleRoom, NotificationContent.StateEvent.PolicyRuleRoom,
NotificationContent.StateEvent.PolicyRuleServer, NotificationContent.StateEvent.PolicyRuleServer,
@@ -318,7 +322,8 @@ class DefaultNotifiableEventResolver(
NotificationContent.StateEvent.RoomTombstone, NotificationContent.StateEvent.RoomTombstone,
is NotificationContent.StateEvent.RoomTopic, is NotificationContent.StateEvent.RoomTopic,
NotificationContent.StateEvent.SpaceChild, NotificationContent.StateEvent.SpaceChild,
NotificationContent.StateEvent.SpaceParent -> { NotificationContent.StateEvent.SpaceParent,
NotificationContent.StateEvent.BeaconInfo -> {
Timber.tag(loggerTag.value).d("Ignoring notification for state event ${content.javaClass.simpleName}") Timber.tag(loggerTag.value).d("Ignoring notification for state event ${content.javaClass.simpleName}")
throw NotificationResolverException.EventFilteredOut throw NotificationResolverException.EventFilteredOut
} }
@@ -17,4 +17,5 @@ data class ElementWellKnown(
val identityProviderAppScheme: String?, val identityProviderAppScheme: String?,
val customRecoveryPassphrase: CustomRecoveryPassphrase?, val customRecoveryPassphrase: CustomRecoveryPassphrase?,
val contentScannerUrl: String?, val contentScannerUrl: String?,
val forceDisableE2EE: Boolean?,
) )
@@ -46,6 +46,8 @@ data class InternalElementWellKnown(
val customRecoveryPassphrase: InternalCustomRecoveryPassphrase? = null, val customRecoveryPassphrase: InternalCustomRecoveryPassphrase? = null,
@SerialName("content_scanner_url") @SerialName("content_scanner_url")
val contentScannerUrl: String? = null, val contentScannerUrl: String? = null,
@SerialName("force_disable_e2ee")
val forceDisableE2EE: Boolean? = null,
) )
@Serializable @Serializable
@@ -31,6 +31,7 @@ fun InternalElementWellKnown.map() = ElementWellKnown(
identityProviderAppScheme = identityProviderAppScheme, identityProviderAppScheme = identityProviderAppScheme,
customRecoveryPassphrase = customRecoveryPassphrase?.toPublic(), customRecoveryPassphrase = customRecoveryPassphrase?.toPublic(),
contentScannerUrl = contentScannerUrl, contentScannerUrl = contentScannerUrl,
forceDisableE2EE = forceDisableE2EE,
) )
private fun InternalCustomRecoveryPassphrase.toPublic(): CustomRecoveryPassphrase { private fun InternalCustomRecoveryPassphrase.toPublic(): CustomRecoveryPassphrase {
@@ -50,6 +50,7 @@ class DefaultSessionWellknownRetrieverTest {
identityProviderAppScheme = null, identityProviderAppScheme = null,
customRecoveryPassphrase = null, customRecoveryPassphrase = null,
contentScannerUrl = null, contentScannerUrl = null,
forceDisableE2EE = null,
) )
) )
) )
@@ -77,6 +78,7 @@ class DefaultSessionWellknownRetrieverTest {
identityProviderAppScheme = "an_app_scheme", identityProviderAppScheme = "an_app_scheme",
customRecoveryPassphrase = null, customRecoveryPassphrase = null,
contentScannerUrl = "https://content-scanner.example.com", contentScannerUrl = "https://content-scanner.example.com",
forceDisableE2EE = false,
) )
) )
) )
@@ -108,6 +110,7 @@ class DefaultSessionWellknownRetrieverTest {
identityProviderAppScheme = null, identityProviderAppScheme = null,
contentScannerUrl = null, contentScannerUrl = null,
customRecoveryPassphrase = null, customRecoveryPassphrase = null,
forceDisableE2EE = null,
) )
) )
) )
@@ -255,6 +258,7 @@ class DefaultSessionWellknownRetrieverTest {
identityProviderAppScheme = "an_app_scheme", identityProviderAppScheme = "an_app_scheme",
customRecoveryPassphrase = null, customRecoveryPassphrase = null,
contentScannerUrl = "https://content-scanner.example.com", contentScannerUrl = "https://content-scanner.example.com",
forceDisableE2EE = false,
) )
) )
) )
@@ -303,6 +307,7 @@ class DefaultSessionWellknownRetrieverTest {
identityProviderAppScheme = "an_app_scheme", identityProviderAppScheme = "an_app_scheme",
customRecoveryPassphrase = null, customRecoveryPassphrase = null,
contentScannerUrl = "https://content-scanner.example.com", contentScannerUrl = "https://content-scanner.example.com",
forceDisableE2EE = false,
) )
) )
) )
@@ -340,7 +345,8 @@ class DefaultSessionWellknownRetrieverTest {
"brand_color": "#FF0000", "brand_color": "#FF0000",
"notification_sound": "a_notification_sound.flac", "notification_sound": "a_notification_sound.flac",
"idp_app_scheme": "an_app_scheme", "idp_app_scheme": "an_app_scheme",
"content_scanner_url": "https://content-scanner.example.com" "content_scanner_url": "https://content-scanner.example.com",
"force_disable_e2ee": false,
}""" }"""
} }
} }
@@ -50,6 +50,7 @@ class DefaultWellknownRetrieverTest {
identityProviderAppScheme = null, identityProviderAppScheme = null,
customRecoveryPassphrase = null, customRecoveryPassphrase = null,
contentScannerUrl = null, contentScannerUrl = null,
forceDisableE2EE = null,
) )
) )
) )
@@ -72,6 +73,7 @@ class DefaultWellknownRetrieverTest {
identityProviderAppScheme = "an_app_scheme", identityProviderAppScheme = "an_app_scheme",
customRecoveryPassphrase = null, customRecoveryPassphrase = null,
contentScannerUrl = "https://content-scanner.example.com", contentScannerUrl = "https://content-scanner.example.com",
forceDisableE2EE = false,
) )
) )
) )
@@ -101,6 +103,7 @@ class DefaultWellknownRetrieverTest {
identityProviderAppScheme = null, identityProviderAppScheme = null,
contentScannerUrl = null, contentScannerUrl = null,
customRecoveryPassphrase = null, customRecoveryPassphrase = null,
forceDisableE2EE = null,
) )
) )
) )
@@ -266,7 +269,8 @@ class DefaultWellknownRetrieverTest {
"brand_color": "#FF0000", "brand_color": "#FF0000",
"notification_sound": "a_notification_sound.flac", "notification_sound": "a_notification_sound.flac",
"idp_app_scheme": "an_app_scheme", "idp_app_scheme": "an_app_scheme",
"content_scanner_url": "https://content-scanner.example.com" "content_scanner_url": "https://content-scanner.example.com",
"force_disable_e2ee": false
}""" }"""
} }
} }
@@ -20,6 +20,7 @@ fun anElementWellKnown(
identityProviderAppScheme: String? = null, identityProviderAppScheme: String? = null,
customRecoveryPassphrase: CustomRecoveryPassphrase? = null, customRecoveryPassphrase: CustomRecoveryPassphrase? = null,
contentScannerUrl: String? = null, contentScannerUrl: String? = null,
forceDisableE2EE: Boolean? = null,
) = ElementWellKnown( ) = ElementWellKnown(
registrationHelperUrl = registrationHelperUrl, registrationHelperUrl = registrationHelperUrl,
enforceElementPro = enforceElementPro, enforceElementPro = enforceElementPro,
@@ -29,6 +30,7 @@ fun anElementWellKnown(
identityProviderAppScheme = identityProviderAppScheme, identityProviderAppScheme = identityProviderAppScheme,
customRecoveryPassphrase = customRecoveryPassphrase, customRecoveryPassphrase = customRecoveryPassphrase,
contentScannerUrl = contentScannerUrl, contentScannerUrl = contentScannerUrl,
forceDisableE2EE = forceDisableE2EE,
) )
fun aCustomRecoveryPassphrase( fun aCustomRecoveryPassphrase(