Merge branch 'develop' into feature/fga/fix_left_room_membership_change
This commit is contained in:
commit
b25f9571f2
153 changed files with 2885 additions and 1088 deletions
2
.github/workflows/maestro-local.yml
vendored
2
.github/workflows/maestro-local.yml
vendored
|
|
@ -11,7 +11,7 @@ env:
|
||||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
|
||||||
ARCH: x86_64
|
ARCH: x86_64
|
||||||
DEVICE: pixel_7_pro
|
DEVICE: pixel_7_pro
|
||||||
API_LEVEL: 35
|
API_LEVEL: 33
|
||||||
TARGET: google_apis
|
TARGET: google_apis
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
appId: ${MAESTRO_APP_ID}
|
appId: ${MAESTRO_APP_ID}
|
||||||
|
androidWebViewHierarchy: devtools
|
||||||
---
|
---
|
||||||
## Check that all env variables required in the whole test suite are declared (to fail faster)
|
## Check that all env variables required in the whole test suite are declared (to fail faster)
|
||||||
- runScript: ./scripts/checkEnv.js
|
- runScript: ./scripts/checkEnv.js
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,22 @@ appId: ${MAESTRO_APP_ID}
|
||||||
visible: 'Use without an account'
|
visible: 'Use without an account'
|
||||||
commands:
|
commands:
|
||||||
- tapOn: "Use without an account"
|
- tapOn: "Use without an account"
|
||||||
|
## For older chrome versions
|
||||||
|
- runFlow:
|
||||||
|
when:
|
||||||
|
visible: 'Accept & continue'
|
||||||
|
commands:
|
||||||
|
- tapOn: "Accept & continue"
|
||||||
|
- runFlow:
|
||||||
|
when:
|
||||||
|
visible: 'No thanks'
|
||||||
|
commands:
|
||||||
|
- tapOn: "No thanks"
|
||||||
## Working when running Maestro locally, but not on the CI yet.
|
## Working when running Maestro locally, but not on the CI yet.
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible:
|
||||||
|
id: "form-1"
|
||||||
|
timeout: 10000
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "form-1"
|
id: "form-1"
|
||||||
- inputText: ${MAESTRO_USERNAME}
|
- inputText: ${MAESTRO_USERNAME}
|
||||||
|
|
|
||||||
|
|
@ -354,3 +354,12 @@ fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy {
|
||||||
|
dependencySubstitution {
|
||||||
|
val tink = libs.google.tink.get()
|
||||||
|
substitute(module("com.google.crypto.tink:tink")).using(module("${tink.group}:${tink.name}:${tink.version}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -51,6 +52,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
roomComponentFactory: RoomComponentFactory,
|
roomComponentFactory: RoomComponentFactory,
|
||||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
|
|
@ -85,6 +87,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
onCreate = {
|
onCreate = {
|
||||||
Timber.v("OnCreate => ${inputs.room.roomId}")
|
Timber.v("OnCreate => ${inputs.room.roomId}")
|
||||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||||
|
activeRoomsHolder.addRoom(inputs.room)
|
||||||
fetchRoomMembers()
|
fetchRoomMembers()
|
||||||
trackVisitedRoom()
|
trackVisitedRoom()
|
||||||
},
|
},
|
||||||
|
|
@ -95,6 +98,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
},
|
},
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
Timber.v("OnDestroy")
|
Timber.v("OnDestroy")
|
||||||
|
activeRoomsHolder.removeRoom(inputs.room.sessionId, inputs.room.roomId)
|
||||||
inputs.room.destroy()
|
inputs.room.destroy()
|
||||||
appNavigationStateService.onLeavingRoom(id)
|
appNavigationStateService.onLeavingRoom(id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,18 @@ import io.element.android.features.messages.api.MessagesEntryPoint
|
||||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
import io.element.android.libraries.architecture.childNode
|
import io.element.android.libraries.architecture.childNode
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class JoinBaseRoomLoadedFlowNodeTest {
|
class JoinedRoomLoadedFlowNodeTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
|
@ -96,20 +98,21 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createJoinedRoomLoadedFlowNode(
|
private fun TestScope.createJoinedRoomLoadedFlowNode(
|
||||||
plugins: List<Plugin>,
|
plugins: List<Plugin>,
|
||||||
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
||||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||||
coroutineScope: CoroutineScope,
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
) = JoinedRoomLoadedFlowNode(
|
) = JoinedRoomLoadedFlowNode(
|
||||||
buildContext = BuildContext.root(savedStateMap = null),
|
buildContext = BuildContext.root(savedStateMap = null),
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
messagesEntryPoint = messagesEntryPoint,
|
messagesEntryPoint = messagesEntryPoint,
|
||||||
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
||||||
appNavigationStateService = FakeAppNavigationStateService(),
|
appNavigationStateService = FakeAppNavigationStateService(),
|
||||||
appCoroutineScope = coroutineScope,
|
appCoroutineScope = this,
|
||||||
roomComponentFactory = FakeRoomComponentFactory(),
|
roomComponentFactory = FakeRoomComponentFactory(),
|
||||||
matrixClient = FakeMatrixClient(),
|
matrixClient = FakeMatrixClient(),
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -121,7 +124,6 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
plugins = listOf(inputs),
|
plugins = listOf(inputs),
|
||||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
coroutineScope = this
|
|
||||||
)
|
)
|
||||||
// WHEN
|
// WHEN
|
||||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
|
|
@ -144,7 +146,6 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||||
plugins = listOf(inputs),
|
plugins = listOf(inputs),
|
||||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||||
coroutineScope = this
|
|
||||||
)
|
)
|
||||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
// WHEN
|
// WHEN
|
||||||
|
|
@ -154,4 +155,53 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||||
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
||||||
assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
|
assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the ActiveRoomsHolder will be updated with the loaded room on create`() = runTest {
|
||||||
|
// GIVEN
|
||||||
|
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
|
||||||
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
|
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||||
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||||
|
val activeRoomsHolder = ActiveRoomsHolder()
|
||||||
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
|
plugins = listOf(inputs),
|
||||||
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
|
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
|
||||||
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
|
// WHEN
|
||||||
|
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.CREATED)
|
||||||
|
// THEN
|
||||||
|
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the ActiveRoomsHolder will be removed on destroy`() = runTest {
|
||||||
|
// GIVEN
|
||||||
|
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
|
||||||
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
|
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||||
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||||
|
val activeRoomsHolder = ActiveRoomsHolder().apply {
|
||||||
|
addRoom(room)
|
||||||
|
}
|
||||||
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
|
plugins = listOf(inputs),
|
||||||
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
|
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
|
)
|
||||||
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
|
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.CREATED)
|
||||||
|
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNotNull()
|
||||||
|
// WHEN
|
||||||
|
roomFlowNode.updateLifecycleState(Lifecycle.State.DESTROYED)
|
||||||
|
// THEN
|
||||||
|
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.DESTROYED)
|
||||||
|
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit d3dffc97bf8b39386ace2db7d857bbff05c73c18
|
Subproject commit 4a07c862a23a9fd1418eabf132cf9d6b25ea4927
|
||||||
|
|
@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||||
import io.element.android.services.analytics.api.ScreenTracker
|
import io.element.android.services.analytics.api.ScreenTracker
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -62,6 +63,7 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
private val activeCallManager: ActiveCallManager,
|
private val activeCallManager: ActiveCallManager,
|
||||||
private val languageTagProvider: LanguageTagProvider,
|
private val languageTagProvider: LanguageTagProvider,
|
||||||
private val appForegroundStateService: AppForegroundStateService,
|
private val appForegroundStateService: AppForegroundStateService,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
) : Presenter<CallScreenState> {
|
) : Presenter<CallScreenState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -241,8 +243,10 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
|
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
|
||||||
if (!notifiedCallStart) {
|
if (!notifiedCallStart) {
|
||||||
getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
|
val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
|
||||||
?.onSuccess { notifiedCallStart = true }
|
val sendCallNotificationResult = activeRoomForSession?.sendCallNotificationIfNeeded()
|
||||||
|
?: getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
|
||||||
|
sendCallNotificationResult?.onSuccess { notifiedCallStart = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ class DefaultCallWidgetProvider @Inject constructor(
|
||||||
private val matrixClientsProvider: MatrixClientProvider,
|
private val matrixClientsProvider: MatrixClientProvider,
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
) : CallWidgetProvider {
|
) : CallWidgetProvider {
|
||||||
override suspend fun getWidget(
|
override suspend fun getWidget(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
|
|
@ -33,7 +35,9 @@ class DefaultCallWidgetProvider @Inject constructor(
|
||||||
theme: String?,
|
theme: String?,
|
||||||
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
|
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
|
||||||
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
|
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
|
||||||
val room = matrixClient.getJoinedRoom(roomId) ?: error("Room not found")
|
val room = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
|
||||||
|
?: matrixClient.getJoinedRoom(roomId)
|
||||||
|
?: error("Room not found")
|
||||||
|
|
||||||
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
|
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
|
||||||
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
|
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||||
import io.element.android.services.analytics.api.ScreenTracker
|
import io.element.android.services.analytics.api.ScreenTracker
|
||||||
import io.element.android.services.analytics.test.FakeScreenTracker
|
import io.element.android.services.analytics.test.FakeScreenTracker
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
|
@ -367,6 +368,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
|
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
|
||||||
screenTracker: ScreenTracker = FakeScreenTracker(),
|
screenTracker: ScreenTracker = FakeScreenTracker(),
|
||||||
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
||||||
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
): CallScreenPresenter {
|
): CallScreenPresenter {
|
||||||
val userAgentProvider = object : UserAgentProvider {
|
val userAgentProvider = object : UserAgentProvider {
|
||||||
override fun provide(): String {
|
override fun provide(): String {
|
||||||
|
|
@ -387,6 +389,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
languageTagProvider = FakeLanguageTagProvider("en-US"),
|
languageTagProvider = FakeLanguageTagProvider("en-US"),
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
appCoroutineScope = backgroundScope,
|
appCoroutineScope = backgroundScope,
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceTimeBy
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
|
@ -63,30 +59,28 @@ class DefaultActiveCallManagerTest {
|
||||||
fun `registerIncomingCall - sets the incoming call as active`() = runTest {
|
fun `registerIncomingCall - sets the incoming call as active`() = runTest {
|
||||||
setupShadowPowerManager()
|
setupShadowPowerManager()
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
|
||||||
|
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNull()
|
||||||
|
|
||||||
val callNotificationData = aCallNotificationData()
|
val callNotificationData = aCallNotificationData()
|
||||||
manager.registerIncomingCall(callNotificationData)
|
manager.registerIncomingCall(callNotificationData)
|
||||||
|
|
||||||
assertThat(manager.activeCall.value).isEqualTo(
|
assertThat(manager.activeCall.value).isEqualTo(
|
||||||
ActiveCall(
|
ActiveCall(
|
||||||
callType = CallType.RoomCall(
|
callType = CallType.RoomCall(
|
||||||
sessionId = callNotificationData.sessionId,
|
sessionId = callNotificationData.sessionId,
|
||||||
roomId = callNotificationData.roomId,
|
roomId = callNotificationData.roomId,
|
||||||
),
|
),
|
||||||
callState = CallState.Ringing(callNotificationData)
|
callState = CallState.Ringing(callNotificationData)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
verify { notificationManagerCompat.notify(notificationId, any()) }
|
verify { notificationManagerCompat.notify(notificationId, any()) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -94,42 +88,38 @@ class DefaultActiveCallManagerTest {
|
||||||
fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest {
|
fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest {
|
||||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||||
val onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
val onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(
|
||||||
val manager = createActiveCallManager(
|
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
||||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
// Register existing call
|
// Register existing call
|
||||||
val callNotificationData = aCallNotificationData()
|
val callNotificationData = aCallNotificationData()
|
||||||
manager.registerIncomingCall(callNotificationData)
|
manager.registerIncomingCall(callNotificationData)
|
||||||
val activeCall = manager.activeCall.value
|
val activeCall = manager.activeCall.value
|
||||||
|
|
||||||
// Now add a new call
|
// Now add a new call
|
||||||
manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2))
|
manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2))
|
||||||
|
|
||||||
assertThat(manager.activeCall.value).isEqualTo(activeCall)
|
assertThat(manager.activeCall.value).isEqualTo(activeCall)
|
||||||
assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2)
|
assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2)
|
||||||
|
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
|
|
||||||
addMissedCallNotificationLambda.assertions()
|
addMissedCallNotificationLambda.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID))
|
.with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest {
|
fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest {
|
||||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(
|
||||||
val manager = createActiveCallManager(
|
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
||||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
manager.incomingCallTimedOut(displayMissedCallNotification = true)
|
manager.incomingCallTimedOut(displayMissedCallNotification = true)
|
||||||
|
|
||||||
addMissedCallNotificationLambda.assertions().isNeverCalled()
|
addMissedCallNotificationLambda.assertions().isNeverCalled()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -138,90 +128,80 @@ class DefaultActiveCallManagerTest {
|
||||||
setupShadowPowerManager()
|
setupShadowPowerManager()
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(
|
||||||
val manager = createActiveCallManager(
|
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda),
|
||||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda),
|
notificationManagerCompat = notificationManagerCompat,
|
||||||
notificationManagerCompat = notificationManagerCompat,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
manager.registerIncomingCall(aCallNotificationData())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
|
|
||||||
manager.incomingCallTimedOut(displayMissedCallNotification = true)
|
manager.incomingCallTimedOut(displayMissedCallNotification = true)
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
|
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||||
addMissedCallNotificationLambda.assertions().isCalledOnce()
|
addMissedCallNotificationLambda.assertions().isCalledOnce()
|
||||||
verify { notificationManagerCompat.cancel(notificationId) }
|
verify { notificationManagerCompat.cancel(notificationId) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
|
fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
|
||||||
setupShadowPowerManager()
|
setupShadowPowerManager()
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
// Create a cancellable coroutine scope to cancel the test when needed
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
inCancellableScope {
|
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
|
||||||
|
|
||||||
val notificationData = aCallNotificationData()
|
val notificationData = aCallNotificationData()
|
||||||
manager.registerIncomingCall(notificationData)
|
manager.registerIncomingCall(notificationData)
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
|
|
||||||
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||||
|
|
||||||
verify { notificationManagerCompat.cancel(notificationId) }
|
verify { notificationManagerCompat.cancel(notificationId) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
|
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
|
||||||
setupShadowPowerManager()
|
setupShadowPowerManager()
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
// Create a cancellable coroutine scope to cancel the test when needed
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
inCancellableScope {
|
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
|
||||||
|
|
||||||
manager.registerIncomingCall(aCallNotificationData())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
|
|
||||||
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
|
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
|
|
||||||
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
|
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest {
|
fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest {
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
assertThat(manager.activeCall.value).isNull()
|
||||||
assertThat(manager.activeCall.value).isNull()
|
|
||||||
|
|
||||||
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
|
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
|
||||||
assertThat(manager.activeCall.value).isEqualTo(
|
assertThat(manager.activeCall.value).isEqualTo(
|
||||||
ActiveCall(
|
ActiveCall(
|
||||||
callType = CallType.RoomCall(
|
callType = CallType.RoomCall(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
),
|
),
|
||||||
callState = CallState.InCall,
|
callState = CallState.InCall,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
verify { notificationManagerCompat.cancel(notificationId) }
|
verify { notificationManagerCompat.cancel(notificationId) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -233,22 +213,19 @@ class DefaultActiveCallManagerTest {
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
// Create a cancellable coroutine scope to cancel the test when needed
|
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) })
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
|
||||||
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) })
|
|
||||||
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
|
|
||||||
|
|
||||||
manager.registerIncomingCall(aCallNotificationData())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
|
|
||||||
// Call is active (the other user join the call)
|
// Call is active (the other user join the call)
|
||||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
// Call is cancelled (the other user left the call)
|
// Call is cancelled (the other user left the call)
|
||||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
|
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNull()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -260,44 +237,34 @@ class DefaultActiveCallManagerTest {
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
// Create a cancellable coroutine scope to cancel the test when needed
|
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("Matrix client not found")) })
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
|
||||||
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("Matrix client not found")) })
|
|
||||||
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
|
|
||||||
|
|
||||||
// No matrix client
|
// No matrix client
|
||||||
|
|
||||||
manager.registerIncomingCall(aCallNotificationData())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
|
|
||||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
|
|
||||||
// The call should still be active
|
// The call should still be active
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
|
|
||||||
// No room
|
// No room
|
||||||
client.givenGetRoomResult(A_ROOM_ID, null)
|
client.givenGetRoomResult(A_ROOM_ID, null)
|
||||||
matrixClientProvider.getClient = { Result.success(client) }
|
matrixClientProvider.getClient = { Result.success(client) }
|
||||||
|
|
||||||
manager.registerIncomingCall(aCallNotificationData())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
|
|
||||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
|
|
||||||
// The call should still be active
|
// The call should still be active
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TestScope.inCancellableScope(block: suspend CoroutineScope.() -> Unit) {
|
|
||||||
launch(SupervisorJob()) {
|
|
||||||
block()
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupShadowPowerManager() {
|
private fun setupShadowPowerManager() {
|
||||||
|
|
@ -306,14 +273,13 @@ class DefaultActiveCallManagerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.createActiveCallManager(
|
private fun TestScope.createActiveCallManager(
|
||||||
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
|
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
|
||||||
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
|
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
|
||||||
coroutineScope: CoroutineScope = this,
|
|
||||||
) = DefaultActiveCallManager(
|
) = DefaultActiveCallManager(
|
||||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = backgroundScope,
|
||||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
||||||
ringingCallNotificationCreator = RingingCallNotificationCreator(
|
ringingCallNotificationCreator = RingingCallNotificationCreator(
|
||||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -77,6 +79,29 @@ class DefaultCallWidgetProviderTest {
|
||||||
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull()
|
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getWidget - reuses the active room if possible`() = runTest {
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
// No room from the client
|
||||||
|
givenGetRoomResult(A_ROOM_ID, null)
|
||||||
|
}
|
||||||
|
val activeRoomsHolder = ActiveRoomsHolder().apply {
|
||||||
|
// A current active room with the same room id
|
||||||
|
addRoom(
|
||||||
|
FakeJoinedRoom(
|
||||||
|
baseRoom = FakeBaseRoom(roomId = A_ROOM_ID),
|
||||||
|
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
|
||||||
|
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val provider = createProvider(
|
||||||
|
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
|
||||||
|
activeRoomsHolder = activeRoomsHolder
|
||||||
|
)
|
||||||
|
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isSuccess).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
|
|
@ -104,9 +129,11 @@ class DefaultCallWidgetProviderTest {
|
||||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
||||||
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
) = DefaultCallWidgetProvider(
|
) = DefaultCallWidgetProvider(
|
||||||
matrixClientsProvider = matrixClientProvider,
|
matrixClientsProvider = matrixClientProvider,
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
callWidgetSettingsProvider = callWidgetSettingsProvider,
|
callWidgetSettingsProvider = callWidgetSettingsProvider,
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,21 @@ interface EnterpriseService {
|
||||||
fun defaultHomeserverList(): List<String>
|
fun defaultHomeserverList(): List<String>
|
||||||
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
||||||
|
|
||||||
|
suspend fun isElementCallAvailable(): Boolean
|
||||||
|
|
||||||
fun semanticColorsLight(): SemanticColors
|
fun semanticColorsLight(): SemanticColors
|
||||||
fun semanticColorsDark(): SemanticColors
|
fun semanticColorsDark(): SemanticColors
|
||||||
|
|
||||||
fun firebasePushGateway(): String?
|
fun firebasePushGateway(): String?
|
||||||
fun unifiedPushDefaultPushGateway(): String?
|
fun unifiedPushDefaultPushGateway(): String?
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ANY_ACCOUNT_PROVIDER = "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EnterpriseService.canConnectToAnyHomeserver(): Boolean {
|
||||||
|
return defaultHomeserverList().let {
|
||||||
|
it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService {
|
||||||
override fun defaultHomeserverList(): List<String> = emptyList()
|
override fun defaultHomeserverList(): List<String> = emptyList()
|
||||||
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
|
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
|
||||||
|
|
||||||
|
override suspend fun isElementCallAvailable(): Boolean = true
|
||||||
|
|
||||||
override fun semanticColorsLight(): SemanticColors = compoundColorsLight
|
override fun semanticColorsLight(): SemanticColors = compoundColorsLight
|
||||||
|
|
||||||
override fun semanticColorsDark(): SemanticColors = compoundColorsDark
|
override fun semanticColorsDark(): SemanticColors = compoundColorsDark
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class FakeEnterpriseService(
|
||||||
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
|
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
|
||||||
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
|
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
|
||||||
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
|
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
|
||||||
|
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
|
||||||
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
|
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
|
||||||
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
|
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
|
||||||
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
|
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
|
||||||
|
|
@ -35,6 +36,10 @@ class FakeEnterpriseService(
|
||||||
isAllowedToConnectToHomeserverResult(homeserverUrl)
|
isAllowedToConnectToHomeserverResult(homeserverUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
|
||||||
|
isElementCallAvailableResult()
|
||||||
|
}
|
||||||
|
|
||||||
override fun semanticColorsLight(): SemanticColors {
|
override fun semanticColorsLight(): SemanticColors {
|
||||||
return semanticColorsLightResult()
|
return semanticColorsLightResult()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,14 @@ import io.element.android.features.lockscreen.impl.storage.LockScreenStore
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class LockScreenSettingsPresenterTest {
|
class LockScreenSettingsPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - remove pin option is hidden when mandatory`() = runTest {
|
fun `present - remove pin option is hidden when mandatory`() = runTest {
|
||||||
val presenter = createLockScreenSettingsPresenter(this, lockScreenConfig = aLockScreenConfig(isPinMandatory = true))
|
val presenter = createLockScreenSettingsPresenter(lockScreenConfig = aLockScreenConfig(isPinMandatory = true))
|
||||||
presenter.test {
|
presenter.test {
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.showRemovePinOption).isFalse()
|
assertThat(state.showRemovePinOption).isFalse()
|
||||||
|
|
@ -37,7 +37,7 @@ class LockScreenSettingsPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - remove pin flow`() = runTest {
|
fun `present - remove pin flow`() = runTest {
|
||||||
val presenter = createLockScreenSettingsPresenter(this)
|
val presenter = createLockScreenSettingsPresenter()
|
||||||
presenter.test {
|
presenter.test {
|
||||||
consumeItemsUntilPredicate { state ->
|
consumeItemsUntilPredicate { state ->
|
||||||
state.showRemovePinOption
|
state.showRemovePinOption
|
||||||
|
|
@ -71,7 +71,6 @@ class LockScreenSettingsPresenterTest {
|
||||||
isDeviceSecured = true,
|
isDeviceSecured = true,
|
||||||
)
|
)
|
||||||
val presenter = createLockScreenSettingsPresenter(
|
val presenter = createLockScreenSettingsPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|
@ -88,7 +87,6 @@ class LockScreenSettingsPresenterTest {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val presenter = createLockScreenSettingsPresenter(
|
val presenter = createLockScreenSettingsPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|
@ -110,7 +108,6 @@ class LockScreenSettingsPresenterTest {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val presenter = createLockScreenSettingsPresenter(
|
val presenter = createLockScreenSettingsPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|
@ -130,7 +127,6 @@ class LockScreenSettingsPresenterTest {
|
||||||
)
|
)
|
||||||
val lockScreenStore = InMemoryLockScreenStore()
|
val lockScreenStore = InMemoryLockScreenStore()
|
||||||
val presenter = createLockScreenSettingsPresenter(
|
val presenter = createLockScreenSettingsPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
lockScreenStore = lockScreenStore,
|
lockScreenStore = lockScreenStore,
|
||||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||||
)
|
)
|
||||||
|
|
@ -148,8 +144,7 @@ class LockScreenSettingsPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createLockScreenSettingsPresenter(
|
private suspend fun TestScope.createLockScreenSettingsPresenter(
|
||||||
coroutineScope: CoroutineScope,
|
|
||||||
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
|
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
|
||||||
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
||||||
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
|
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
|
||||||
|
|
@ -160,7 +155,7 @@ class LockScreenSettingsPresenterTest {
|
||||||
return LockScreenSettingsPresenter(
|
return LockScreenSettingsPresenter(
|
||||||
lockScreenStore = lockScreenStore,
|
lockScreenStore = lockScreenStore,
|
||||||
pinCodeManager = pinCodeManager,
|
pinCodeManager = pinCodeManager,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = this,
|
||||||
lockScreenConfig = lockScreenConfig,
|
lockScreenConfig = lockScreenConfig,
|
||||||
biometricAuthenticatorManager = biometricAuthenticatorManager,
|
biometricAuthenticatorManager = biometricAuthenticatorManager,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ class PinUnlockPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - success verify flow`() = runTest {
|
fun `present - success verify flow`() = runTest {
|
||||||
val presenter = createPinUnlockPresenter(this)
|
val presenter = createPinUnlockPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -71,7 +71,7 @@ class PinUnlockPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - failure verify flow`() = runTest {
|
fun `present - failure verify flow`() = runTest {
|
||||||
val presenter = createPinUnlockPresenter(this)
|
val presenter = createPinUnlockPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -100,7 +100,7 @@ class PinUnlockPresenterTest {
|
||||||
fun `present - forgot pin flow`() = runTest {
|
fun `present - forgot pin flow`() = runTest {
|
||||||
val signOutLambda = lambdaRecorder<Boolean, Unit> {}
|
val signOutLambda = lambdaRecorder<Boolean, Unit> {}
|
||||||
val signOut = FakeLogoutUseCase(signOutLambda)
|
val signOut = FakeLogoutUseCase(signOutLambda)
|
||||||
val presenter = createPinUnlockPresenter(this, logoutUseCase = signOut)
|
val presenter = createPinUnlockPresenter(logoutUseCase = signOut)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -135,8 +135,7 @@ class PinUnlockPresenterTest {
|
||||||
dataOrNull()?.assertText(text)
|
dataOrNull()?.assertText(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createPinUnlockPresenter(
|
private suspend fun TestScope.createPinUnlockPresenter(
|
||||||
scope: CoroutineScope,
|
|
||||||
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
||||||
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
||||||
logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }),
|
logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }),
|
||||||
|
|
@ -149,7 +148,7 @@ class PinUnlockPresenterTest {
|
||||||
pinCodeManager = pinCodeManager,
|
pinCodeManager = pinCodeManager,
|
||||||
biometricAuthenticatorManager = biometricAuthenticatorManager,
|
biometricAuthenticatorManager = biometricAuthenticatorManager,
|
||||||
logoutUseCase = logoutUseCase,
|
logoutUseCase = logoutUseCase,
|
||||||
coroutineScope = scope,
|
coroutineScope = this,
|
||||||
pinUnlockHelper = PinUnlockHelper(biometricAuthenticatorManager, pinCodeManager),
|
pinUnlockHelper = PinUnlockHelper(biometricAuthenticatorManager, pinCodeManager),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import io.element.android.features.login.api.LoginEntryPoint
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
|
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
|
||||||
import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode
|
import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode
|
||||||
|
import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderNode
|
||||||
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode
|
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode
|
||||||
import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode
|
import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode
|
||||||
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
|
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
|
||||||
|
|
@ -107,6 +108,9 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
val isAccountCreation: Boolean,
|
val isAccountCreation: Boolean,
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object ChooseAccountProvider : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object ChangeAccountProvider : NavTarget
|
data object ChangeAccountProvider : NavTarget
|
||||||
|
|
||||||
|
|
@ -133,9 +137,13 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSignIn() {
|
override fun onSignIn(mustChooseAccountProvider: Boolean) {
|
||||||
backstack.push(
|
backstack.push(
|
||||||
NavTarget.ConfirmAccountProvider(isAccountCreation = false)
|
if (mustChooseAccountProvider) {
|
||||||
|
NavTarget.ChooseAccountProvider
|
||||||
|
} else {
|
||||||
|
NavTarget.ConfirmAccountProvider(isAccountCreation = false)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,6 +174,22 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
createNode<OnBoardingNode>(buildContext, listOf(callback, inputs))
|
createNode<OnBoardingNode>(buildContext, listOf(callback, inputs))
|
||||||
}
|
}
|
||||||
|
NavTarget.ChooseAccountProvider -> {
|
||||||
|
val callback = object : ChooseAccountProviderNode.Callback {
|
||||||
|
override fun onOidcDetails(oidcDetails: OidcDetails) {
|
||||||
|
navigateToMas(oidcDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateAccountContinue(url: String) {
|
||||||
|
backstack.push(NavTarget.CreateAccount(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoginPasswordNeeded() {
|
||||||
|
backstack.push(NavTarget.LoginPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<ChooseAccountProviderNode>(buildContext, listOf(callback))
|
||||||
|
}
|
||||||
NavTarget.QrCode -> {
|
NavTarget.QrCode -> {
|
||||||
createNode<QrCodeLoginFlowNode>(buildContext)
|
createNode<QrCodeLoginFlowNode>(buildContext)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,16 @@ import javax.inject.Inject
|
||||||
class AccountProviderDataSource @Inject constructor(
|
class AccountProviderDataSource @Inject constructor(
|
||||||
enterpriseService: EnterpriseService,
|
enterpriseService: EnterpriseService,
|
||||||
) {
|
) {
|
||||||
private val defaultAccountProvider = (enterpriseService.defaultHomeserverList().firstOrNull() ?: AuthenticationConfig.MATRIX_ORG_URL)
|
private val defaultAccountProvider =
|
||||||
.let { url ->
|
(enterpriseService.defaultHomeserverList().firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } ?: AuthenticationConfig.MATRIX_ORG_URL)
|
||||||
AccountProvider(
|
.let { url ->
|
||||||
url = url,
|
AccountProvider(
|
||||||
subtitle = null,
|
url = url,
|
||||||
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
subtitle = null,
|
||||||
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
)
|
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow(
|
private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow(
|
||||||
defaultAccountProvider
|
defaultAccountProvider
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.accountprovider
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.login.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AccountProviderOtherView(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onClick() }
|
||||||
|
) {
|
||||||
|
HorizontalDivider()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 44.dp)
|
||||||
|
.padding(vertical = 4.dp, horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
RoundedIconAtom(
|
||||||
|
size = RoundedIconAtomSize.Medium,
|
||||||
|
imageVector = CompoundIcons.Search(),
|
||||||
|
tint = ElementTheme.colors.iconPrimary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.weight(1f),
|
||||||
|
text = stringResource(R.string.screen_change_account_provider_other),
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun AccountProviderOtherViewPreview() = ElementPreview {
|
||||||
|
AccountProviderOtherView(
|
||||||
|
onClick = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -23,10 +23,14 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
|
||||||
|
|
||||||
fun anAccountProvider(
|
fun anAccountProvider(
|
||||||
url: String = AuthenticationConfig.MATRIX_ORG_URL,
|
url: String = AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
|
subtitle: String? = "Matrix.org is an open network for secure, decentralized communication.",
|
||||||
|
isPublic: Boolean = true,
|
||||||
|
isMatrixOrg: Boolean = true,
|
||||||
|
isValid: Boolean = true,
|
||||||
) = AccountProvider(
|
) = AccountProvider(
|
||||||
url = url,
|
url = url,
|
||||||
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
|
subtitle = subtitle,
|
||||||
isPublic = true,
|
isPublic = isPublic,
|
||||||
isMatrixOrg = true,
|
isMatrixOrg = isMatrixOrg,
|
||||||
isValid = true,
|
isValid = isValid,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ fun AccountProviderView(
|
||||||
item: AccountProvider,
|
item: AccountProvider,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
selected: Boolean = false,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
@ -66,7 +67,7 @@ fun AccountProviderView(
|
||||||
} else {
|
} else {
|
||||||
RoundedIconAtom(
|
RoundedIconAtom(
|
||||||
size = RoundedIconAtomSize.Medium,
|
size = RoundedIconAtomSize.Medium,
|
||||||
imageVector = CompoundIcons.Search(),
|
imageVector = CompoundIcons.Host(),
|
||||||
tint = ElementTheme.colors.iconPrimary,
|
tint = ElementTheme.colors.iconPrimary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +89,15 @@ fun AccountProviderView(
|
||||||
tint = ElementTheme.colors.iconSecondary,
|
tint = ElementTheme.colors.iconSecondary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (selected) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp),
|
||||||
|
imageVector = CompoundIcons.Check(),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = ElementTheme.colors.iconAccentPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (item.subtitle != null) {
|
if (item.subtitle != null) {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
|
import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter
|
||||||
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter
|
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter
|
||||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||||
import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter
|
import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter
|
||||||
|
|
@ -31,7 +32,8 @@ import javax.inject.Inject
|
||||||
/**
|
/**
|
||||||
* This class is responsible for managing the login flow, including handling OIDC actions and
|
* This class is responsible for managing the login flow, including handling OIDC actions and
|
||||||
* submitting login requests.
|
* submitting login requests.
|
||||||
* It's an helper to avoid code duplication. It is used by [OnBoardingPresenter] and [ConfirmAccountProviderPresenter].
|
* It's an helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter]
|
||||||
|
* and [ChooseAccountProviderPresenter].
|
||||||
*/
|
*/
|
||||||
class LoginHelper @Inject constructor(
|
class LoginHelper @Inject constructor(
|
||||||
private val oidcActionFlow: OidcActionFlow,
|
private val oidcActionFlow: OidcActionFlow,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import io.element.android.appconfig.AuthenticationConfig
|
import io.element.android.appconfig.AuthenticationConfig
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -25,6 +26,7 @@ class ChangeAccountProviderPresenter @Inject constructor(
|
||||||
override fun present(): ChangeAccountProviderState {
|
override fun present(): ChangeAccountProviderState {
|
||||||
val staticAccountProviderList = remember {
|
val staticAccountProviderList = remember {
|
||||||
enterpriseService.defaultHomeserverList()
|
enterpriseService.defaultHomeserverList()
|
||||||
|
.filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
|
||||||
.map { it.ensureProtocol() }
|
.map { it.ensureProtocol() }
|
||||||
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
|
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
|
||||||
.map { url ->
|
.map { url ->
|
||||||
|
|
@ -38,9 +40,14 @@ class ChangeAccountProviderPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val canSearchForAccountProviders = remember {
|
||||||
|
enterpriseService.canConnectToAnyHomeserver()
|
||||||
|
}
|
||||||
|
|
||||||
val changeServerState = changeServerPresenter.present()
|
val changeServerState = changeServerPresenter.present()
|
||||||
return ChangeAccountProviderState(
|
return ChangeAccountProviderState(
|
||||||
accountProviders = staticAccountProviderList,
|
accountProviders = staticAccountProviderList,
|
||||||
|
canSearchForAccountProviders = canSearchForAccountProviders,
|
||||||
changeServerState = changeServerState,
|
changeServerState = changeServerState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,6 @@ import io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||||
// Do not use default value, so no member get forgotten in the presenters.
|
// Do not use default value, so no member get forgotten in the presenters.
|
||||||
data class ChangeAccountProviderState(
|
data class ChangeAccountProviderState(
|
||||||
val accountProviders: List<AccountProvider>,
|
val accountProviders: List<AccountProvider>,
|
||||||
|
val canSearchForAccountProviders: Boolean,
|
||||||
val changeServerState: ChangeServerState,
|
val changeServerState: ChangeServerState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,28 @@
|
||||||
package io.element.android.features.login.impl.screens.changeaccountprovider
|
package io.element.android.features.login.impl.screens.changeaccountprovider
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||||
|
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||||
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
||||||
|
|
||||||
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
|
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
|
||||||
override val values: Sequence<ChangeAccountProviderState>
|
override val values: Sequence<ChangeAccountProviderState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aChangeAccountProviderState(),
|
aChangeAccountProviderState(),
|
||||||
|
aChangeAccountProviderState(canSearchForAccountProviders = false),
|
||||||
// Add other state here
|
// Add other state here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aChangeAccountProviderState() = ChangeAccountProviderState(
|
fun aChangeAccountProviderState(
|
||||||
accountProviders = listOf(
|
accountProviders: List<AccountProvider> = listOf(
|
||||||
anAccountProvider()
|
anAccountProvider()
|
||||||
),
|
),
|
||||||
changeServerState = aChangeServerState(),
|
canSearchForAccountProviders: Boolean = true,
|
||||||
|
changeServerState: ChangeServerState = aChangeServerState(),
|
||||||
|
) = ChangeAccountProviderState(
|
||||||
|
accountProviders = accountProviders,
|
||||||
|
canSearchForAccountProviders = canSearchForAccountProviders,
|
||||||
|
changeServerState = changeServerState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProviderOtherView
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderView
|
import io.element.android.features.login.impl.accountprovider.AccountProviderView
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
|
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerView
|
import io.element.android.features.login.impl.changeserver.ChangeServerView
|
||||||
|
|
@ -95,13 +95,11 @@ fun ChangeAccountProviderView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Other
|
// Other
|
||||||
AccountProviderView(
|
if (state.canSearchForAccountProviders) {
|
||||||
item = AccountProvider(
|
AccountProviderOtherView(
|
||||||
url = "",
|
onClick = onOtherProviderClick
|
||||||
title = stringResource(id = R.string.screen_change_account_provider_other),
|
)
|
||||||
),
|
}
|
||||||
onClick = onOtherProviderClick
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
ChangeServerView(
|
ChangeServerView(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.chooseaccountprovider
|
||||||
|
|
||||||
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
|
|
||||||
|
sealed interface ChooseAccountProviderEvents {
|
||||||
|
data class SelectAccountProvider(val accountProvider: AccountProvider) : ChooseAccountProviderEvents
|
||||||
|
data object Continue : ChooseAccountProviderEvents
|
||||||
|
data object ClearError : ChooseAccountProviderEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.chooseaccountprovider
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.login.impl.util.openLearnMorePage
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
|
|
||||||
|
@ContributesNode(AppScope::class)
|
||||||
|
class ChooseAccountProviderNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val presenter: ChooseAccountProviderPresenter,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onLoginPasswordNeeded()
|
||||||
|
fun onOidcDetails(oidcDetails: OidcDetails)
|
||||||
|
fun onCreateAccountContinue(url: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onOidcDetails(oidcDetails: OidcDetails) {
|
||||||
|
plugins<Callback>().forEach { it.onOidcDetails(oidcDetails) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLoginPasswordNeeded() {
|
||||||
|
plugins<Callback>().forEach { it.onLoginPasswordNeeded() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCreateAccountContinue(url: String) {
|
||||||
|
plugins<Callback>().forEach { it.onCreateAccountContinue(url) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state = presenter.present()
|
||||||
|
val context = LocalContext.current
|
||||||
|
ChooseAccountProviderView(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
|
onBackClick = ::navigateUp,
|
||||||
|
onOidcDetails = ::onOidcDetails,
|
||||||
|
onNeedLoginPassword = ::onLoginPasswordNeeded,
|
||||||
|
onLearnMoreClick = { openLearnMorePage(context) },
|
||||||
|
onCreateAccountContinue = ::onCreateAccountContinue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.chooseaccountprovider
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.appconfig.AuthenticationConfig
|
||||||
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
|
import io.element.android.features.login.impl.login.LoginHelper
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ChooseAccountProviderPresenter @Inject constructor(
|
||||||
|
private val enterpriseService: EnterpriseService,
|
||||||
|
private val loginHelper: LoginHelper,
|
||||||
|
) : Presenter<ChooseAccountProviderState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): ChooseAccountProviderState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
val loginMode by loginHelper.collectLoginMode()
|
||||||
|
|
||||||
|
var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
|
fun handleEvent(event: ChooseAccountProviderEvents) {
|
||||||
|
when (event) {
|
||||||
|
ChooseAccountProviderEvents.Continue -> {
|
||||||
|
selectedAccountProvider?.let {
|
||||||
|
loginHelper.submit(
|
||||||
|
coroutineScope = localCoroutineScope,
|
||||||
|
isAccountCreation = false,
|
||||||
|
homeserverUrl = it.url,
|
||||||
|
loginHint = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ChooseAccountProviderEvents.SelectAccountProvider -> {
|
||||||
|
// Ensure that the user do not change the server during processing
|
||||||
|
if (loginMode is AsyncData.Uninitialized) {
|
||||||
|
selectedAccountProvider = event.accountProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChooseAccountProviderEvents.ClearError -> loginHelper.clearError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val staticAccountProviderList = remember {
|
||||||
|
// The list cannot contains ANY_ACCOUNT_PROVIDER ("*") and cannot be empty at this point
|
||||||
|
enterpriseService.defaultHomeserverList()
|
||||||
|
.map { it.ensureProtocol() }
|
||||||
|
.map { url ->
|
||||||
|
AccountProvider(
|
||||||
|
url = url,
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
|
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
|
isValid = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChooseAccountProviderState(
|
||||||
|
accountProviders = staticAccountProviderList,
|
||||||
|
selectedAccountProvider = selectedAccountProvider,
|
||||||
|
loginMode = loginMode,
|
||||||
|
eventSink = ::handleEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.chooseaccountprovider
|
||||||
|
|
||||||
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
|
import io.element.android.features.login.impl.login.LoginMode
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
|
// Do not use default value, so no member get forgotten in the presenters.
|
||||||
|
data class ChooseAccountProviderState(
|
||||||
|
val accountProviders: List<AccountProvider>,
|
||||||
|
val selectedAccountProvider: AccountProvider?,
|
||||||
|
val loginMode: AsyncData<LoginMode>,
|
||||||
|
val eventSink: (ChooseAccountProviderEvents) -> Unit,
|
||||||
|
) {
|
||||||
|
val submitEnabled: Boolean
|
||||||
|
get() = selectedAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.chooseaccountprovider
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
|
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||||
|
import io.element.android.features.login.impl.login.LoginMode
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
|
open class ChooseAccountProviderStateProvider : PreviewParameterProvider<ChooseAccountProviderState> {
|
||||||
|
private val server1 = anAccountProvider(
|
||||||
|
url = "https://server1.io",
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = false,
|
||||||
|
isMatrixOrg = false,
|
||||||
|
)
|
||||||
|
private val server2 = anAccountProvider(
|
||||||
|
url = "https://server2.io",
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = false,
|
||||||
|
isMatrixOrg = false,
|
||||||
|
)
|
||||||
|
private val server3 = anAccountProvider(
|
||||||
|
url = "https://server3.io",
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = false,
|
||||||
|
isMatrixOrg = false,
|
||||||
|
)
|
||||||
|
override val values: Sequence<ChooseAccountProviderState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aChooseAccountProviderState(
|
||||||
|
accountProviders = listOf(
|
||||||
|
server1,
|
||||||
|
server2,
|
||||||
|
server3,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aChooseAccountProviderState(
|
||||||
|
accountProviders = listOf(
|
||||||
|
server1,
|
||||||
|
server2,
|
||||||
|
server3,
|
||||||
|
),
|
||||||
|
selectedAccountProvider = server2,
|
||||||
|
),
|
||||||
|
aChooseAccountProviderState(
|
||||||
|
accountProviders = listOf(
|
||||||
|
server1,
|
||||||
|
server2,
|
||||||
|
server3,
|
||||||
|
),
|
||||||
|
selectedAccountProvider = server2,
|
||||||
|
loginMode = AsyncData.Loading(),
|
||||||
|
),
|
||||||
|
// Add other state here
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aChooseAccountProviderState(
|
||||||
|
accountProviders: List<AccountProvider> = listOf(
|
||||||
|
anAccountProvider()
|
||||||
|
),
|
||||||
|
selectedAccountProvider: AccountProvider? = null,
|
||||||
|
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||||
|
eventSink: (ChooseAccountProviderEvents) -> Unit = {},
|
||||||
|
) = ChooseAccountProviderState(
|
||||||
|
accountProviders = accountProviders,
|
||||||
|
selectedAccountProvider = selectedAccountProvider,
|
||||||
|
loginMode = loginMode,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.chooseaccountprovider
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.login.impl.R
|
||||||
|
import io.element.android.features.login.impl.accountprovider.AccountProviderView
|
||||||
|
import io.element.android.features.login.impl.login.LoginModeView
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChooseAccountProviderView(
|
||||||
|
state: ChooseAccountProviderState,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onOidcDetails: (OidcDetails) -> Unit,
|
||||||
|
onNeedLoginPassword: () -> Unit,
|
||||||
|
onLearnMoreClick: () -> Unit,
|
||||||
|
onCreateAccountContinue: (url: String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val isLoading by remember(state.loginMode) {
|
||||||
|
derivedStateOf {
|
||||||
|
state.loginMode is AsyncData.Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {},
|
||||||
|
navigationIcon = { BackButton(onClick = onBackClick) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.imePadding()
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(state = rememberScrollState())
|
||||||
|
) {
|
||||||
|
IconTitleSubtitleMolecule(
|
||||||
|
modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp),
|
||||||
|
iconStyle = BigIcon.Style.Default(CompoundIcons.HomeSolid()),
|
||||||
|
title = stringResource(id = R.string.screen_server_confirmation_title_picker_mode),
|
||||||
|
subTitle = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
state.accountProviders.forEach { item ->
|
||||||
|
val alteredItem = if (item.isMatrixOrg) {
|
||||||
|
// Set the subtitle from the resource
|
||||||
|
item.copy(
|
||||||
|
subtitle = stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
item
|
||||||
|
}
|
||||||
|
AccountProviderView(
|
||||||
|
item = alteredItem,
|
||||||
|
selected = item == state.selectedAccountProvider,
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(item))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
// Flexible spacing to keep the submit button at the bottom
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Button(
|
||||||
|
text = stringResource(id = CommonStrings.action_continue),
|
||||||
|
showProgress = isLoading,
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(ChooseAccountProviderEvents.Continue)
|
||||||
|
},
|
||||||
|
enabled = state.submitEnabled || isLoading,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
}
|
||||||
|
LoginModeView(
|
||||||
|
loginMode = state.loginMode,
|
||||||
|
onClearError = {
|
||||||
|
state.eventSink(ChooseAccountProviderEvents.ClearError)
|
||||||
|
},
|
||||||
|
onLearnMoreClick = onLearnMoreClick,
|
||||||
|
onOidcDetails = onOidcDetails,
|
||||||
|
onNeedLoginPassword = onNeedLoginPassword,
|
||||||
|
onCreateAccountContinue = onCreateAccountContinue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun ChooseAccountProviderViewPreview(@PreviewParameter(ChooseAccountProviderStateProvider::class) state: ChooseAccountProviderState) = ElementPreview {
|
||||||
|
ChooseAccountProviderView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = { },
|
||||||
|
onLearnMoreClick = { },
|
||||||
|
onOidcDetails = { },
|
||||||
|
onNeedLoginPassword = { },
|
||||||
|
onCreateAccountContinue = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -34,7 +34,7 @@ class OnBoardingNode @AssistedInject constructor(
|
||||||
) {
|
) {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onSignUp()
|
fun onSignUp()
|
||||||
fun onSignIn()
|
fun onSignIn(mustChooseAccountProvider: Boolean)
|
||||||
fun onSignInWithQrCode()
|
fun onSignInWithQrCode()
|
||||||
fun onReportProblem()
|
fun onReportProblem()
|
||||||
fun onLoginPasswordNeeded()
|
fun onLoginPasswordNeeded()
|
||||||
|
|
@ -53,8 +53,8 @@ class OnBoardingNode @AssistedInject constructor(
|
||||||
params = params,
|
params = params,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun onSignIn() {
|
private fun onSignIn(mustChooseAccountProvider: Boolean) {
|
||||||
plugins<Callback>().forEach { it.onSignIn() }
|
plugins<Callback>().forEach { it.onSignIn(mustChooseAccountProvider) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSignUp() {
|
private fun onSignUp() {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.appconfig.OnBoardingConfig
|
import io.element.android.appconfig.OnBoardingConfig
|
||||||
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.login.LoginHelper
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -27,6 +29,7 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||||
@Assisted private val params: OnBoardingNode.Params,
|
@Assisted private val params: OnBoardingNode.Params,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val enterpriseService: EnterpriseService,
|
||||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||||
private val loginHelper: LoginHelper,
|
private val loginHelper: LoginHelper,
|
||||||
) : Presenter<OnBoardingState> {
|
) : Presenter<OnBoardingState> {
|
||||||
|
|
@ -37,15 +40,33 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||||
): OnBoardingPresenter
|
): OnBoardingPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val defaultAccountProvider = params.accountProvider
|
|
||||||
private val loginHint = params.loginHint
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): OnBoardingState {
|
override fun present(): OnBoardingState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
val forcedAccountProvider = remember {
|
||||||
val canLoginWithQrCode by produceState(initialValue = false) {
|
// If defaultHomeserverList() returns a singleton list, this is the default account provider.
|
||||||
value = defaultAccountProvider == null &&
|
// In this case, the user can sign in using this homeserver, or use QrCode login
|
||||||
|
enterpriseService.defaultHomeserverList().singleOrNull()
|
||||||
|
}
|
||||||
|
val canConnectToAnyHomeserver = remember {
|
||||||
|
enterpriseService.canConnectToAnyHomeserver()
|
||||||
|
}
|
||||||
|
val mustChooseAccountProvider = remember {
|
||||||
|
!canConnectToAnyHomeserver && enterpriseService.defaultHomeserverList().size > 1
|
||||||
|
}
|
||||||
|
val linkAccountProvider by produceState<String?>(initialValue = null) {
|
||||||
|
// Account provider from the link, if allowed by the enterprise service
|
||||||
|
value = params.accountProvider?.takeIf {
|
||||||
|
enterpriseService.isAllowedToConnectToHomeserver(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val defaultAccountProvider = remember(linkAccountProvider) {
|
||||||
|
// If there is a forced account provider, this is the default account provider
|
||||||
|
// Else use the account provider passed in the params if any and if allowed
|
||||||
|
forcedAccountProvider ?: linkAccountProvider
|
||||||
|
}
|
||||||
|
val canLoginWithQrCode by produceState(initialValue = false, linkAccountProvider) {
|
||||||
|
value = linkAccountProvider == null &&
|
||||||
featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
|
featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
|
||||||
}
|
}
|
||||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
||||||
|
|
@ -58,7 +79,7 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||||
coroutineScope = localCoroutineScope,
|
coroutineScope = localCoroutineScope,
|
||||||
isAccountCreation = false,
|
isAccountCreation = false,
|
||||||
homeserverUrl = event.defaultAccountProvider,
|
homeserverUrl = event.defaultAccountProvider,
|
||||||
loginHint = loginHint,
|
loginHint = params.loginHint?.takeIf { forcedAccountProvider == null },
|
||||||
)
|
)
|
||||||
OnBoardingEvents.ClearError -> loginHelper.clearError()
|
OnBoardingEvents.ClearError -> loginHelper.clearError()
|
||||||
}
|
}
|
||||||
|
|
@ -67,8 +88,9 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||||
return OnBoardingState(
|
return OnBoardingState(
|
||||||
productionApplicationName = buildMeta.productionApplicationName,
|
productionApplicationName = buildMeta.productionApplicationName,
|
||||||
defaultAccountProvider = defaultAccountProvider,
|
defaultAccountProvider = defaultAccountProvider,
|
||||||
|
mustChooseAccountProvider = mustChooseAccountProvider,
|
||||||
canLoginWithQrCode = canLoginWithQrCode,
|
canLoginWithQrCode = canLoginWithQrCode,
|
||||||
canCreateAccount = defaultAccountProvider == null && OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
loginMode = loginMode,
|
loginMode = loginMode,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.architecture.AsyncData
|
||||||
data class OnBoardingState(
|
data class OnBoardingState(
|
||||||
val productionApplicationName: String,
|
val productionApplicationName: String,
|
||||||
val defaultAccountProvider: String?,
|
val defaultAccountProvider: String?,
|
||||||
|
val mustChooseAccountProvider: Boolean,
|
||||||
val canLoginWithQrCode: Boolean,
|
val canLoginWithQrCode: Boolean,
|
||||||
val canCreateAccount: Boolean,
|
val canCreateAccount: Boolean,
|
||||||
val canReportBug: Boolean,
|
val canReportBug: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
|
||||||
fun anOnBoardingState(
|
fun anOnBoardingState(
|
||||||
productionApplicationName: String = "Element",
|
productionApplicationName: String = "Element",
|
||||||
defaultAccountProvider: String? = null,
|
defaultAccountProvider: String? = null,
|
||||||
|
mustChooseAccountProvider: Boolean = false,
|
||||||
canLoginWithQrCode: Boolean = false,
|
canLoginWithQrCode: Boolean = false,
|
||||||
canCreateAccount: Boolean = false,
|
canCreateAccount: Boolean = false,
|
||||||
canReportBug: Boolean = false,
|
canReportBug: Boolean = false,
|
||||||
|
|
@ -34,6 +35,7 @@ fun anOnBoardingState(
|
||||||
) = OnBoardingState(
|
) = OnBoardingState(
|
||||||
productionApplicationName = productionApplicationName,
|
productionApplicationName = productionApplicationName,
|
||||||
defaultAccountProvider = defaultAccountProvider,
|
defaultAccountProvider = defaultAccountProvider,
|
||||||
|
mustChooseAccountProvider = mustChooseAccountProvider,
|
||||||
canLoginWithQrCode = canLoginWithQrCode,
|
canLoginWithQrCode = canLoginWithQrCode,
|
||||||
canCreateAccount = canCreateAccount,
|
canCreateAccount = canCreateAccount,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
fun OnBoardingView(
|
fun OnBoardingView(
|
||||||
state: OnBoardingState,
|
state: OnBoardingState,
|
||||||
onSignInWithQrCode: () -> Unit,
|
onSignInWithQrCode: () -> Unit,
|
||||||
onSignIn: () -> Unit,
|
onSignIn: (mustChooseAccountProvider: Boolean) -> Unit,
|
||||||
onCreateAccount: () -> Unit,
|
onCreateAccount: () -> Unit,
|
||||||
onOidcDetails: (OidcDetails) -> Unit,
|
onOidcDetails: (OidcDetails) -> Unit,
|
||||||
onNeedLoginPassword: () -> Unit,
|
onNeedLoginPassword: () -> Unit,
|
||||||
|
|
@ -143,7 +143,7 @@ private fun OnBoardingContent(state: OnBoardingState) {
|
||||||
private fun OnBoardingButtons(
|
private fun OnBoardingButtons(
|
||||||
state: OnBoardingState,
|
state: OnBoardingState,
|
||||||
onSignInWithQrCode: () -> Unit,
|
onSignInWithQrCode: () -> Unit,
|
||||||
onSignIn: () -> Unit,
|
onSignIn: (mustChooseAccountProvider: Boolean) -> Unit,
|
||||||
onCreateAccount: () -> Unit,
|
onCreateAccount: () -> Unit,
|
||||||
onReportProblem: () -> Unit,
|
onReportProblem: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -171,7 +171,9 @@ private fun OnBoardingButtons(
|
||||||
if (defaultAccountProvider == null) {
|
if (defaultAccountProvider == null) {
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(id = signInButtonStringRes),
|
text = stringResource(id = signInButtonStringRes),
|
||||||
onClick = onSignIn,
|
onClick = {
|
||||||
|
onSignIn(state.mustChooseAccountProvider)
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.testTag(TestTags.onBoardingSignIn)
|
.testTag(TestTags.onBoardingSignIn)
|
||||||
|
|
|
||||||
|
|
@ -89,5 +89,6 @@ Try signing in manually, or scan the QR code with another device."</string>
|
||||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix is an open network for secure, decentralised communication."</string>
|
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix is an open network for secure, decentralised communication."</string>
|
||||||
<string name="screen_server_confirmation_message_register">"This is where your conversations will live — just like you would use an email provider to keep your emails."</string>
|
<string name="screen_server_confirmation_message_register">"This is where your conversations will live — just like you would use an email provider to keep your emails."</string>
|
||||||
<string name="screen_server_confirmation_title_login">"You’re about to sign in to %1$s"</string>
|
<string name="screen_server_confirmation_title_login">"You’re about to sign in to %1$s"</string>
|
||||||
|
<string name="screen_server_confirmation_title_picker_mode">"Choose account provider"</string>
|
||||||
<string name="screen_server_confirmation_title_register">"You’re about to create an account on %1$s"</string>
|
<string name="screen_server_confirmation_title_register">"You’re about to create an account on %1$s"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.features.login.impl.accountprovider
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appconfig.AuthenticationConfig
|
import io.element.android.appconfig.AuthenticationConfig
|
||||||
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -60,6 +61,28 @@ class AccountProviderDataSourceTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - ensure that default homeserver is not star char`() = runTest {
|
||||||
|
val sut = AccountProviderDataSource(
|
||||||
|
FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = { listOf(EnterpriseService.ANY_ACCOUNT_PROVIDER, AuthenticationConfig.MATRIX_ORG_URL) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sut.flow.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState).isEqualTo(
|
||||||
|
AccountProvider(
|
||||||
|
url = AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
|
title = "matrix.org",
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = true,
|
||||||
|
isMatrixOrg = true,
|
||||||
|
isValid = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - user change and reset`() = runTest {
|
fun `present - user change and reset`() = runTest {
|
||||||
val sut = AccountProviderDataSource(FakeEnterpriseService())
|
val sut = AccountProviderDataSource(FakeEnterpriseService())
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,12 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
||||||
|
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.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -27,7 +30,9 @@ class ChangeAccountProviderPresenterTest {
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = ChangeAccountProviderPresenter(
|
val presenter = ChangeAccountProviderPresenter(
|
||||||
changeServerPresenter = { aChangeServerState() },
|
changeServerPresenter = { aChangeServerState() },
|
||||||
enterpriseService = FakeEnterpriseService(),
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = { emptyList() }
|
||||||
|
),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -45,6 +50,75 @@ class ChangeAccountProviderPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
assertThat(initialState.canSearchForAccountProviders).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - fixed list of account providers`() = runTest {
|
||||||
|
val presenter = ChangeAccountProviderPresenter(
|
||||||
|
changeServerPresenter = { aChangeServerState() },
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = {
|
||||||
|
listOf(AN_ACCOUNT_PROVIDER, AN_ACCOUNT_PROVIDER_2)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.accountProviders).isEqualTo(
|
||||||
|
listOf(
|
||||||
|
AccountProvider(
|
||||||
|
url = "https://matrix.org",
|
||||||
|
title = "matrix.org",
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = true,
|
||||||
|
isMatrixOrg = true,
|
||||||
|
isValid = true,
|
||||||
|
),
|
||||||
|
AccountProvider(
|
||||||
|
url = "https://element.io",
|
||||||
|
title = "element.io",
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = false,
|
||||||
|
isMatrixOrg = false,
|
||||||
|
isValid = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(initialState.canSearchForAccountProviders).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - opened list of account providers`() = runTest {
|
||||||
|
val presenter = ChangeAccountProviderPresenter(
|
||||||
|
changeServerPresenter = { aChangeServerState() },
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = {
|
||||||
|
listOf(AN_ACCOUNT_PROVIDER, EnterpriseService.ANY_ACCOUNT_PROVIDER)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.accountProviders).isEqualTo(
|
||||||
|
listOf(
|
||||||
|
AccountProvider(
|
||||||
|
url = "https://matrix.org",
|
||||||
|
title = "matrix.org",
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = true,
|
||||||
|
isMatrixOrg = true,
|
||||||
|
isValid = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(initialState.canSearchForAccountProviders).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.chooseaccountprovider
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
|
import io.element.android.features.login.impl.login.LoginHelper
|
||||||
|
import io.element.android.features.login.impl.screens.onboarding.createLoginHelper
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
|
||||||
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_3
|
||||||
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ChooseAccountProviderPresenterTest {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ACCOUNT_PROVIDER_FROM_CONFIG_1 = AN_ACCOUNT_PROVIDER_2
|
||||||
|
private const val ACCOUNT_PROVIDER_FROM_CONFIG_2 = AN_ACCOUNT_PROVIDER_3
|
||||||
|
val accountProvider1 = AccountProvider(
|
||||||
|
url = ACCOUNT_PROVIDER_FROM_CONFIG_1.ensureProtocol(),
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = false,
|
||||||
|
isMatrixOrg = false,
|
||||||
|
isValid = true,
|
||||||
|
)
|
||||||
|
val accountProvider2 = AccountProvider(
|
||||||
|
url = ACCOUNT_PROVIDER_FROM_CONFIG_2.ensureProtocol(),
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = false,
|
||||||
|
isMatrixOrg = false,
|
||||||
|
isValid = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - ensure initial conditions`() {
|
||||||
|
assertThat(
|
||||||
|
setOf(
|
||||||
|
ACCOUNT_PROVIDER_FROM_CONFIG_1,
|
||||||
|
ACCOUNT_PROVIDER_FROM_CONFIG_2,
|
||||||
|
).size
|
||||||
|
).isEqualTo(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = createPresenter(
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.accountProviders).containsExactly(
|
||||||
|
accountProvider1,
|
||||||
|
accountProvider2,
|
||||||
|
)
|
||||||
|
assertThat(initialState.selectedAccountProvider).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - Continue when no account provider is selected has no effect`() = runTest {
|
||||||
|
val authenticationService = FakeMatrixAuthenticationService()
|
||||||
|
val presenter = createPresenter(
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
|
||||||
|
),
|
||||||
|
loginHelper = createLoginHelper(
|
||||||
|
authenticationService = authenticationService,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also {
|
||||||
|
assertThat(it.selectedAccountProvider).isNull()
|
||||||
|
it.eventSink(ChooseAccountProviderEvents.Continue)
|
||||||
|
expectNoEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - select account provider and continue - error then clear error`() = runTest {
|
||||||
|
val authenticationService = FakeMatrixAuthenticationService()
|
||||||
|
val presenter = createPresenter(
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
|
||||||
|
),
|
||||||
|
loginHelper = createLoginHelper(
|
||||||
|
authenticationService = authenticationService,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also {
|
||||||
|
assertThat(it.selectedAccountProvider).isNull()
|
||||||
|
it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider1))
|
||||||
|
}
|
||||||
|
awaitItem().also {
|
||||||
|
assertThat(it.selectedAccountProvider).isEqualTo(accountProvider1)
|
||||||
|
authenticationService.givenChangeServerError(A_THROWABLE)
|
||||||
|
it.eventSink(ChooseAccountProviderEvents.Continue)
|
||||||
|
skipItems(1) // Loading
|
||||||
|
|
||||||
|
// Check an error was returned
|
||||||
|
val submittedState = awaitItem()
|
||||||
|
assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||||
|
|
||||||
|
// Assert the error is then cleared
|
||||||
|
submittedState.eventSink(ChooseAccountProviderEvents.ClearError)
|
||||||
|
val clearedState = awaitItem()
|
||||||
|
assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - default account provider - select account provider during login has no effect`() = runTest {
|
||||||
|
val authenticationService = FakeMatrixAuthenticationService()
|
||||||
|
val presenter = createPresenter(
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
|
||||||
|
),
|
||||||
|
loginHelper = createLoginHelper(
|
||||||
|
authenticationService = authenticationService,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also {
|
||||||
|
assertThat(it.selectedAccountProvider).isNull()
|
||||||
|
it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider1))
|
||||||
|
}
|
||||||
|
awaitItem().also {
|
||||||
|
assertThat(it.selectedAccountProvider).isEqualTo(accountProvider1)
|
||||||
|
it.eventSink(ChooseAccountProviderEvents.Continue)
|
||||||
|
}
|
||||||
|
awaitItem().also {
|
||||||
|
assertThat(it.loginMode.isLoading()).isTrue()
|
||||||
|
it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider2))
|
||||||
|
}
|
||||||
|
expectNoEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPresenter(
|
||||||
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
|
loginHelper: LoginHelper = createLoginHelper(),
|
||||||
|
) = ChooseAccountProviderPresenter(
|
||||||
|
enterpriseService = enterpriseService,
|
||||||
|
loginHelper = loginHelper,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.chooseaccountprovider
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ChooseAccountProviderViewTest {
|
||||||
|
@get:Rule
|
||||||
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back invokes the expected callback`() {
|
||||||
|
val eventSink = EventsRecorder<ChooseAccountProviderEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setChooseAccountProviderView(
|
||||||
|
state = aChooseAccountProviderState(
|
||||||
|
eventSink = eventSink,
|
||||||
|
),
|
||||||
|
onBackClick = it,
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `selecting an account provider emits the the expected event`() {
|
||||||
|
val eventSink = EventsRecorder<ChooseAccountProviderEvents>()
|
||||||
|
rule.setChooseAccountProviderView(
|
||||||
|
state = aChooseAccountProviderState(
|
||||||
|
accountProviders = listOf(
|
||||||
|
ChooseAccountProviderPresenterTest.accountProvider1,
|
||||||
|
ChooseAccountProviderPresenterTest.accountProvider2,
|
||||||
|
),
|
||||||
|
selectedAccountProvider = anAccountProvider(),
|
||||||
|
eventSink = eventSink,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(ChooseAccountProviderPresenterTest.accountProvider1.title).performClick()
|
||||||
|
eventSink.assertSingle(ChooseAccountProviderEvents.SelectAccountProvider(ChooseAccountProviderPresenterTest.accountProvider1))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when error is displayed - closing the dialog emits the expected event`() {
|
||||||
|
val eventSink = EventsRecorder<ChooseAccountProviderEvents>()
|
||||||
|
rule.setChooseAccountProviderView(
|
||||||
|
state = aChooseAccountProviderState(
|
||||||
|
loginMode = AsyncData.Failure(AN_EXCEPTION),
|
||||||
|
eventSink = eventSink,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_ok)
|
||||||
|
eventSink.assertSingle(ChooseAccountProviderEvents.ClearError)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChooseAccountProviderView(
|
||||||
|
state: ChooseAccountProviderState,
|
||||||
|
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onNeedLoginPassword: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
ChooseAccountProviderView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onOidcDetails = onOidcDetails,
|
||||||
|
onNeedLoginPassword = onNeedLoginPassword,
|
||||||
|
onLearnMoreClick = onLearnMoreClick,
|
||||||
|
onCreateAccountContinue = onCreateAccountContinue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,8 @@ package io.element.android.features.login.impl.screens.onboarding
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appconfig.OnBoardingConfig
|
import io.element.android.appconfig.OnBoardingConfig
|
||||||
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.login.LoginHelper
|
||||||
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
||||||
|
|
@ -19,6 +21,9 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
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.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
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_3
|
||||||
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_LOGIN_HINT
|
import io.element.android.libraries.matrix.test.A_LOGIN_HINT
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
|
|
@ -36,6 +41,23 @@ class OnBoardingPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ACCOUNT_PROVIDER_FROM_LINK = AN_ACCOUNT_PROVIDER
|
||||||
|
private const val ACCOUNT_PROVIDER_FROM_CONFIG = AN_ACCOUNT_PROVIDER_2
|
||||||
|
private const val ACCOUNT_PROVIDER_FROM_CONFIG_2 = AN_ACCOUNT_PROVIDER_3
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - ensure initial conditions`() {
|
||||||
|
assertThat(
|
||||||
|
setOf(
|
||||||
|
ACCOUNT_PROVIDER_FROM_LINK,
|
||||||
|
ACCOUNT_PROVIDER_FROM_CONFIG,
|
||||||
|
ACCOUNT_PROVIDER_FROM_CONFIG_2,
|
||||||
|
).size
|
||||||
|
).isEqualTo(3)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val buildMeta = aBuildMeta(
|
val buildMeta = aBuildMeta(
|
||||||
|
|
@ -50,10 +72,14 @@ class OnBoardingPresenterTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) },
|
||||||
|
),
|
||||||
rageshakeFeatureAvailability = { true },
|
rageshakeFeatureAvailability = { true },
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.defaultAccountProvider).isNull()
|
||||||
assertThat(initialState.canLoginWithQrCode).isFalse()
|
assertThat(initialState.canLoginWithQrCode).isFalse()
|
||||||
assertThat(initialState.productionApplicationName).isEqualTo("B")
|
assertThat(initialState.productionApplicationName).isEqualTo("B")
|
||||||
assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT)
|
assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT)
|
||||||
|
|
@ -74,22 +100,79 @@ class OnBoardingPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - default account provider`() = runTest {
|
fun `present - opening the app using link with allowed account provider, and the app does not force account provider`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
params = OnBoardingNode.Params(
|
params = OnBoardingNode.Params(
|
||||||
accountProvider = A_HOMESERVER_URL,
|
accountProvider = ACCOUNT_PROVIDER_FROM_LINK,
|
||||||
loginHint = null,
|
loginHint = null,
|
||||||
),
|
),
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.QrCodeLogin.key to true),
|
||||||
|
),
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) },
|
||||||
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
skipItems(3)
|
||||||
awaitItem().also {
|
awaitItem().also {
|
||||||
assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL)
|
assertThat(it.defaultAccountProvider).isEqualTo(ACCOUNT_PROVIDER_FROM_LINK)
|
||||||
assertThat(it.canLoginWithQrCode).isFalse()
|
assertThat(it.canLoginWithQrCode).isFalse()
|
||||||
assertThat(it.canCreateAccount).isFalse()
|
assertThat(it.canCreateAccount).isFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - opening the app using link with not allowed account provider, and the app does not force account provider`() = runTest {
|
||||||
|
val presenter = createPresenter(
|
||||||
|
params = OnBoardingNode.Params(
|
||||||
|
accountProvider = ACCOUNT_PROVIDER_FROM_LINK,
|
||||||
|
loginHint = null,
|
||||||
|
),
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.QrCodeLogin.key to true),
|
||||||
|
),
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
|
||||||
|
isAllowedToConnectToHomeserverResult = { false },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also {
|
||||||
|
assertThat(it.defaultAccountProvider).isNull()
|
||||||
|
assertThat(it.canLoginWithQrCode).isTrue()
|
||||||
|
assertThat(it.canCreateAccount).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - opening the app using link, and the app forces account provider`() = runTest {
|
||||||
|
val presenter = createPresenter(
|
||||||
|
params = OnBoardingNode.Params(
|
||||||
|
accountProvider = ACCOUNT_PROVIDER_FROM_LINK,
|
||||||
|
loginHint = null,
|
||||||
|
),
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.QrCodeLogin.key to true),
|
||||||
|
),
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG) },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also {
|
||||||
|
assertThat(it.defaultAccountProvider).isEqualTo(ACCOUNT_PROVIDER_FROM_CONFIG)
|
||||||
|
assertThat(it.canLoginWithQrCode).isTrue()
|
||||||
|
assertThat(it.canCreateAccount).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - default account provider - login and clear error`() = runTest {
|
fun `present - default account provider - login and clear error`() = runTest {
|
||||||
val authenticationService = FakeMatrixAuthenticationService()
|
val authenticationService = FakeMatrixAuthenticationService()
|
||||||
|
|
@ -98,11 +181,15 @@ class OnBoardingPresenterTest {
|
||||||
accountProvider = A_HOMESERVER_URL,
|
accountProvider = A_HOMESERVER_URL,
|
||||||
loginHint = A_LOGIN_HINT,
|
loginHint = A_LOGIN_HINT,
|
||||||
),
|
),
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
|
),
|
||||||
loginHelper = createLoginHelper(
|
loginHelper = createLoginHelper(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
skipItems(3)
|
||||||
awaitItem().also {
|
awaitItem().also {
|
||||||
assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL)
|
assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL)
|
||||||
authenticationService.givenChangeServerError(A_THROWABLE)
|
authenticationService.givenChangeServerError(A_THROWABLE)
|
||||||
|
|
@ -126,12 +213,14 @@ private fun createPresenter(
|
||||||
params: OnBoardingNode.Params = OnBoardingNode.Params(null, null),
|
params: OnBoardingNode.Params = OnBoardingNode.Params(null, null),
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
rageshakeFeatureAvailability: () -> Boolean = { true },
|
rageshakeFeatureAvailability: () -> Boolean = { true },
|
||||||
loginHelper: LoginHelper = createLoginHelper(),
|
loginHelper: LoginHelper = createLoginHelper(),
|
||||||
) = OnBoardingPresenter(
|
) = OnBoardingPresenter(
|
||||||
params = params,
|
params = params,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
enterpriseService = enterpriseService,
|
||||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||||
loginHelper = loginHelper,
|
loginHelper = loginHelper,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.TestRule
|
import org.junit.rules.TestRule
|
||||||
|
|
@ -56,10 +57,28 @@ class OnboardingViewTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when can login with QR code - clicking on sign in manually calls the expected callback`() {
|
fun `when can login with QR code - clicking on sign in manually calls the expected callback - can search account provider`() {
|
||||||
ensureCalledOnce { callback ->
|
`when can login with QR code - clicking on sign in manually calls the expected callback`(
|
||||||
|
mustChooseAccountProvider = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when can login with QR code - clicking on sign in manually calls the expected callback - cannot search account provider`() {
|
||||||
|
`when can login with QR code - clicking on sign in manually calls the expected callback`(
|
||||||
|
mustChooseAccountProvider = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun `when can login with QR code - clicking on sign in manually calls the expected callback`(
|
||||||
|
mustChooseAccountProvider: Boolean,
|
||||||
|
) {
|
||||||
|
ensureCalledOnceWithParam(mustChooseAccountProvider) { callback ->
|
||||||
rule.setOnboardingView(
|
rule.setOnboardingView(
|
||||||
state = anOnBoardingState(canLoginWithQrCode = true),
|
state = anOnBoardingState(
|
||||||
|
canLoginWithQrCode = true,
|
||||||
|
mustChooseAccountProvider = mustChooseAccountProvider,
|
||||||
|
),
|
||||||
onSignIn = callback,
|
onSignIn = callback,
|
||||||
)
|
)
|
||||||
rule.clickOn(R.string.screen_onboarding_sign_in_manually)
|
rule.clickOn(R.string.screen_onboarding_sign_in_manually)
|
||||||
|
|
@ -67,12 +86,28 @@ class OnboardingViewTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`() {
|
fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback - can search account provider`() {
|
||||||
ensureCalledOnce { callback ->
|
`when cannot login with QR code or create account - clicking on continue calls the sign in callback`(
|
||||||
|
mustChooseAccountProvider = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback - cannot search account provider`() {
|
||||||
|
`when cannot login with QR code or create account - clicking on continue calls the sign in callback`(
|
||||||
|
mustChooseAccountProvider = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`(
|
||||||
|
mustChooseAccountProvider: Boolean,
|
||||||
|
) {
|
||||||
|
ensureCalledOnceWithParam(mustChooseAccountProvider) { callback ->
|
||||||
rule.setOnboardingView(
|
rule.setOnboardingView(
|
||||||
state = anOnBoardingState(
|
state = anOnBoardingState(
|
||||||
canLoginWithQrCode = false,
|
canLoginWithQrCode = false,
|
||||||
canCreateAccount = false,
|
canCreateAccount = false,
|
||||||
|
mustChooseAccountProvider = mustChooseAccountProvider,
|
||||||
),
|
),
|
||||||
onSignIn = callback,
|
onSignIn = callback,
|
||||||
)
|
)
|
||||||
|
|
@ -137,7 +172,7 @@ class OnboardingViewTest {
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOnboardingView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOnboardingView(
|
||||||
state: OnBoardingState,
|
state: OnBoardingState,
|
||||||
onSignInWithQrCode: () -> Unit = EnsureNeverCalled(),
|
onSignInWithQrCode: () -> Unit = EnsureNeverCalled(),
|
||||||
onSignIn: () -> Unit = EnsureNeverCalled(),
|
onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onCreateAccount: () -> Unit = EnsureNeverCalled(),
|
onCreateAccount: () -> Unit = EnsureNeverCalled(),
|
||||||
onReportProblem: () -> Unit = EnsureNeverCalled(),
|
onReportProblem: () -> Unit = EnsureNeverCalled(),
|
||||||
onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(),
|
onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ internal fun CallMenuItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (roomCallState) {
|
when (roomCallState) {
|
||||||
|
RoomCallState.Unavailable -> {
|
||||||
|
Box(modifier)
|
||||||
|
}
|
||||||
is RoomCallState.StandBy -> {
|
is RoomCallState.StandBy -> {
|
||||||
StandByCallMenuItem(
|
StandByCallMenuItem(
|
||||||
roomCallState = roomCallState,
|
roomCallState = roomCallState,
|
||||||
|
|
|
||||||
|
|
@ -103,10 +103,12 @@ internal fun TimelineItemCallNotifyView(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineItemCallNotifyViewPreview() {
|
internal fun TimelineItemCallNotifyViewPreview() = ElementPreview {
|
||||||
ElementPreview {
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
RoomCallStateProvider()
|
||||||
RoomCallStateProvider().values.forEach { roomCallState ->
|
.values
|
||||||
|
.filter { it !is RoomCallState.Unavailable }
|
||||||
|
.forEach { roomCallState ->
|
||||||
TimelineItemCallNotifyView(
|
TimelineItemCallNotifyView(
|
||||||
event = aTimelineItemEvent(content = TimelineItemCallNotifyContent()),
|
event = aTimelineItemEvent(content = TimelineItemCallNotifyContent()),
|
||||||
roomCallState = roomCallState,
|
roomCallState = roomCallState,
|
||||||
|
|
@ -114,6 +116,5 @@ internal fun TimelineItemCallNotifyViewPreview() {
|
||||||
onJoinCallClick = {},
|
onJoinCallClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -513,7 +513,17 @@ class AttachmentsPreviewPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - dismissing the progress dialog stops media upload with media queue`() = runTest {
|
fun `present - dismissing the progress dialog stops media upload with media queue`() = runTest {
|
||||||
val onDoneListenerResult = lambdaRecorder<Unit> {}
|
val onDoneListenerResult = lambdaRecorder<Unit> {}
|
||||||
val presenter = createAttachmentsPreviewPresenter(mediaUploadOnSendQueueEnabled = true, onDoneListener = onDoneListenerResult)
|
val presenter = createAttachmentsPreviewPresenter(
|
||||||
|
room = FakeJoinedRoom(
|
||||||
|
liveTimeline = FakeTimeline().apply {
|
||||||
|
sendFileLambda = { _, _, _, _, _, _ ->
|
||||||
|
Result.success(FakeMediaUploadHandler())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
mediaUploadOnSendQueueEnabled = true,
|
||||||
|
onDoneListener = onDoneListenerResult,
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider
|
import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider
|
||||||
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 kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -91,13 +91,12 @@ class ForwardMessagesPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.aForwardMessagesPresenter(
|
private fun TestScope.aForwardMessagesPresenter(
|
||||||
eventId: EventId = AN_EVENT_ID,
|
eventId: EventId = AN_EVENT_ID,
|
||||||
fakeRoom: FakeJoinedRoom = FakeJoinedRoom(),
|
fakeRoom: FakeJoinedRoom = FakeJoinedRoom(),
|
||||||
coroutineScope: CoroutineScope = this,
|
|
||||||
) = ForwardMessagesPresenter(
|
) = ForwardMessagesPresenter(
|
||||||
eventId = eventId.value,
|
eventId = eventId.value,
|
||||||
timelineProvider = LiveTimelineProvider(fakeRoom),
|
timelineProvider = LiveTimelineProvider(fakeRoom),
|
||||||
appCoroutineScope = coroutineScope,
|
appCoroutineScope = this,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
||||||
|
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
|
|
@ -101,8 +102,8 @@ import io.element.android.tests.testutils.waitForPredicate
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -132,7 +133,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createPresenter(this)
|
val presenter = createPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -147,7 +148,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - toggle fullscreen`() = runTest {
|
fun `present - toggle fullscreen`() = runTest {
|
||||||
val presenter = createPresenter(this)
|
val presenter = createPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -163,7 +164,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - change message`() = runTest {
|
fun `present - change message`() = runTest {
|
||||||
val presenter = createPresenter(this)
|
val presenter = createPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -186,7 +187,6 @@ class MessageComposerPresenterTest {
|
||||||
this.saveDraftLambda = updateDraftLambda
|
this.saveDraftLambda = updateDraftLambda
|
||||||
}
|
}
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
draftService = draftService,
|
draftService = draftService,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -229,7 +229,6 @@ class MessageComposerPresenterTest {
|
||||||
this.saveDraftLambda = updateDraftLambda
|
this.saveDraftLambda = updateDraftLambda
|
||||||
}
|
}
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
draftService = draftService,
|
draftService = draftService,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -273,7 +272,6 @@ class MessageComposerPresenterTest {
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
typingNoticeResult = { Result.success(Unit) }
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
room = joinedRoom,
|
room = joinedRoom,
|
||||||
isRichTextEditorEnabled = false,
|
isRichTextEditorEnabled = false,
|
||||||
)
|
)
|
||||||
|
|
@ -314,7 +312,6 @@ class MessageComposerPresenterTest {
|
||||||
this.saveDraftLambda = updateDraftLambda
|
this.saveDraftLambda = updateDraftLambda
|
||||||
}
|
}
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
draftService = draftService,
|
draftService = draftService,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -346,7 +343,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - change mode to reply`() = runTest {
|
fun `present - change mode to reply`() = runTest {
|
||||||
val presenter = createPresenter(this)
|
val presenter = createPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -362,7 +359,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - cancel reply`() = runTest {
|
fun `present - cancel reply`() = runTest {
|
||||||
val presenter = createPresenter(this)
|
val presenter = createPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -382,7 +379,6 @@ class MessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - send message with rich text enabled`() = runTest {
|
fun `present - send message with rich text enabled`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
room = FakeJoinedRoom(
|
room = FakeJoinedRoom(
|
||||||
liveTimeline = FakeTimeline().apply {
|
liveTimeline = FakeTimeline().apply {
|
||||||
sendMessageLambda = { _, _, _ -> Result.success(Unit) }
|
sendMessageLambda = { _, _, _ -> Result.success(Unit) }
|
||||||
|
|
@ -417,7 +413,6 @@ class MessageComposerPresenterTest {
|
||||||
fun `present - send message with plain text enabled`() = runTest {
|
fun `present - send message with plain text enabled`() = runTest {
|
||||||
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("") })
|
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("") })
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
isRichTextEditorEnabled = false,
|
isRichTextEditorEnabled = false,
|
||||||
room = FakeJoinedRoom(
|
room = FakeJoinedRoom(
|
||||||
liveTimeline = FakeTimeline().apply {
|
liveTimeline = FakeTimeline().apply {
|
||||||
|
|
@ -464,7 +459,6 @@ class MessageComposerPresenterTest {
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
typingNoticeResult = { Result.success(Unit) }
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
this,
|
|
||||||
joinedRoom,
|
joinedRoom,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -520,7 +514,6 @@ class MessageComposerPresenterTest {
|
||||||
editMessageLambda = roomEditMessageLambda,
|
editMessageLambda = roomEditMessageLambda,
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
this,
|
|
||||||
joinedRoom,
|
joinedRoom,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -576,7 +569,6 @@ class MessageComposerPresenterTest {
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
this,
|
|
||||||
joinedRoom,
|
joinedRoom,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -628,7 +620,6 @@ class MessageComposerPresenterTest {
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
typingNoticeResult = { Result.success(Unit) }
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
this,
|
|
||||||
joinedRoom,
|
joinedRoom,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -666,7 +657,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Open attachments menu`() = runTest {
|
fun `present - Open attachments menu`() = runTest {
|
||||||
val presenter = createPresenter(this)
|
val presenter = createPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -679,7 +670,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Dismiss attachments menu`() = runTest {
|
fun `present - Dismiss attachments menu`() = runTest {
|
||||||
val presenter = createPresenter(this)
|
val presenter = createPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -702,7 +693,6 @@ class MessageComposerPresenterTest {
|
||||||
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
room = room,
|
room = room,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
)
|
)
|
||||||
|
|
@ -743,7 +733,6 @@ class MessageComposerPresenterTest {
|
||||||
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
room = room,
|
room = room,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
)
|
)
|
||||||
|
|
@ -777,7 +766,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Pick media from gallery & cancel does nothing`() = runTest {
|
fun `present - Pick media from gallery & cancel does nothing`() = runTest {
|
||||||
val presenter = createPresenter(this)
|
val presenter = createPresenter()
|
||||||
with(pickerProvider) {
|
with(pickerProvider) {
|
||||||
givenResult(null) // Simulate a user canceling the flow
|
givenResult(null) // Simulate a user canceling the flow
|
||||||
givenMimeType(MimeTypes.Images)
|
givenMimeType(MimeTypes.Images)
|
||||||
|
|
@ -801,7 +790,6 @@ class MessageComposerPresenterTest {
|
||||||
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
room = room,
|
room = room,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
)
|
)
|
||||||
|
|
@ -819,7 +807,7 @@ class MessageComposerPresenterTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
typingNoticeResult = { Result.success(Unit) }
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(this, room = room)
|
val presenter = createPresenter(room = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -838,7 +826,7 @@ class MessageComposerPresenterTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
typingNoticeResult = { Result.success(Unit) }
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(this, room = room)
|
val presenter = createPresenter(room = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -863,7 +851,6 @@ class MessageComposerPresenterTest {
|
||||||
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
room = room,
|
room = room,
|
||||||
permissionPresenter = permissionPresenter,
|
permissionPresenter = permissionPresenter,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
|
|
@ -888,7 +875,6 @@ class MessageComposerPresenterTest {
|
||||||
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
room = room,
|
room = room,
|
||||||
permissionPresenter = permissionPresenter,
|
permissionPresenter = permissionPresenter,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
|
|
@ -915,7 +901,6 @@ class MessageComposerPresenterTest {
|
||||||
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
room = room,
|
room = room,
|
||||||
permissionPresenter = permissionPresenter,
|
permissionPresenter = permissionPresenter,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
|
|
@ -940,7 +925,6 @@ class MessageComposerPresenterTest {
|
||||||
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
room = room,
|
room = room,
|
||||||
permissionPresenter = permissionPresenter,
|
permissionPresenter = permissionPresenter,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
|
|
@ -961,7 +945,7 @@ class MessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - errors are tracked`() = runTest {
|
fun `present - errors are tracked`() = runTest {
|
||||||
val testException = Exception("Test error")
|
val testException = Exception("Test error")
|
||||||
val presenter = createPresenter(this)
|
val presenter = createPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -973,7 +957,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - ToggleTextFormatting toggles text formatting`() = runTest {
|
fun `present - ToggleTextFormatting toggles text formatting`() = runTest {
|
||||||
val presenter = createPresenter(this, isRichTextEditorEnabled = false)
|
val presenter = createPresenter(isRichTextEditorEnabled = false)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -1017,7 +1001,7 @@ class MessageComposerPresenterTest {
|
||||||
)
|
)
|
||||||
givenRoomInfo(aRoomInfo(isDirect = false))
|
givenRoomInfo(aRoomInfo(isDirect = false))
|
||||||
}
|
}
|
||||||
val presenter = createPresenter(this, room)
|
val presenter = createPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -1078,7 +1062,7 @@ class MessageComposerPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val presenter = createPresenter(this, room)
|
val presenter = createPresenter(room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -1094,7 +1078,6 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
fun `present - InsertSuggestion`() = runTest {
|
fun `present - InsertSuggestion`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
permalinkBuilder = FakePermalinkBuilder(
|
permalinkBuilder = FakePermalinkBuilder(
|
||||||
permalinkForUserLambda = {
|
permalinkForUserLambda = {
|
||||||
Result.success("https://matrix.to/#/${A_USER_ID_2.value}")
|
Result.success("https://matrix.to/#/${A_USER_ID_2.value}")
|
||||||
|
|
@ -1134,7 +1117,7 @@ class MessageComposerPresenterTest {
|
||||||
liveTimeline = timeline,
|
liveTimeline = timeline,
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
typingNoticeResult = { Result.success(Unit) }
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(room = room, coroutineScope = this)
|
val presenter = createPresenter(room = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -1197,7 +1180,16 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - send uri`() = runTest {
|
fun `present - send uri`() = runTest {
|
||||||
val presenter = createPresenter(this)
|
val presenter = createPresenter(
|
||||||
|
room = FakeJoinedRoom(
|
||||||
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
liveTimeline = FakeTimeline().apply {
|
||||||
|
sendFileLambda = { _, _, _, _, _, _ ->
|
||||||
|
Result.success(FakeMediaUploadHandler())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
remember(state, state.textEditorState.messageHtml()) { state }
|
remember(state, state.textEditorState.messageHtml()) { state }
|
||||||
|
|
@ -1214,7 +1206,7 @@ class MessageComposerPresenterTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
typingNoticeResult = typingNoticeResult,
|
typingNoticeResult = typingNoticeResult,
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(room = room, coroutineScope = this)
|
val presenter = createPresenter(room = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -1240,7 +1232,7 @@ class MessageComposerPresenterTest {
|
||||||
val store = InMemorySessionPreferencesStore(
|
val store = InMemorySessionPreferencesStore(
|
||||||
isSendTypingNotificationsEnabled = false
|
isSendTypingNotificationsEnabled = false
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(room = room, sessionPreferencesStore = store, coroutineScope = this)
|
val presenter = createPresenter(room = room, sessionPreferencesStore = store)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -1258,7 +1250,7 @@ class MessageComposerPresenterTest {
|
||||||
val composerDraftService = FakeComposerDraftService().apply {
|
val composerDraftService = FakeComposerDraftService().apply {
|
||||||
this.loadDraftLambda = loadDraftLambda
|
this.loadDraftLambda = loadDraftLambda
|
||||||
}
|
}
|
||||||
val presenter = createPresenter(draftService = composerDraftService, coroutineScope = this)
|
val presenter = createPresenter(draftService = composerDraftService)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -1283,7 +1275,6 @@ class MessageComposerPresenterTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
draftService = composerDraftService,
|
draftService = composerDraftService,
|
||||||
permalinkBuilder = permalinkBuilder,
|
permalinkBuilder = permalinkBuilder,
|
||||||
coroutineScope = this
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -1317,7 +1308,6 @@ class MessageComposerPresenterTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
draftService = composerDraftService,
|
draftService = composerDraftService,
|
||||||
permalinkBuilder = permalinkBuilder,
|
permalinkBuilder = permalinkBuilder,
|
||||||
coroutineScope = this
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -1351,7 +1341,6 @@ class MessageComposerPresenterTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
draftService = composerDraftService,
|
draftService = composerDraftService,
|
||||||
permalinkBuilder = permalinkBuilder,
|
permalinkBuilder = permalinkBuilder,
|
||||||
coroutineScope = this
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -1398,7 +1387,6 @@ class MessageComposerPresenterTest {
|
||||||
room = room,
|
room = room,
|
||||||
draftService = composerDraftService,
|
draftService = composerDraftService,
|
||||||
permalinkBuilder = permalinkBuilder,
|
permalinkBuilder = permalinkBuilder,
|
||||||
coroutineScope = this
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -1428,7 +1416,7 @@ class MessageComposerPresenterTest {
|
||||||
val composerDraftService = FakeComposerDraftService().apply {
|
val composerDraftService = FakeComposerDraftService().apply {
|
||||||
this.saveDraftLambda = saveDraftLambda
|
this.saveDraftLambda = saveDraftLambda
|
||||||
}
|
}
|
||||||
val presenter = createPresenter(draftService = composerDraftService, coroutineScope = this)
|
val presenter = createPresenter(draftService = composerDraftService)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -1452,7 +1440,6 @@ class MessageComposerPresenterTest {
|
||||||
isRichTextEditorEnabled = false,
|
isRichTextEditorEnabled = false,
|
||||||
draftService = composerDraftService,
|
draftService = composerDraftService,
|
||||||
permalinkBuilder = permalinkBuilder,
|
permalinkBuilder = permalinkBuilder,
|
||||||
coroutineScope = this
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
@ -1528,18 +1515,17 @@ class MessageComposerPresenterTest {
|
||||||
return normalState
|
return normalState
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createPresenter(
|
private fun TestScope.createPresenter(
|
||||||
coroutineScope: CoroutineScope,
|
|
||||||
room: JoinedRoom = FakeJoinedRoom(
|
room: JoinedRoom = FakeJoinedRoom(
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
typingNoticeResult = { Result.success(Unit) }
|
||||||
),
|
),
|
||||||
navigator: MessagesNavigator = FakeMessagesNavigator(),
|
navigator: MessagesNavigator = FakeMessagesNavigator(),
|
||||||
pickerProvider: PickerProvider = this.pickerProvider,
|
pickerProvider: PickerProvider = this@MessageComposerPresenterTest.pickerProvider,
|
||||||
featureFlagService: FeatureFlagService = this.featureFlagService,
|
featureFlagService: FeatureFlagService = this@MessageComposerPresenterTest.featureFlagService,
|
||||||
locationService: LocationService = FakeLocationService(true),
|
locationService: LocationService = FakeLocationService(true),
|
||||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor,
|
mediaPreProcessor: MediaPreProcessor = this@MessageComposerPresenterTest.mediaPreProcessor,
|
||||||
snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher = this@MessageComposerPresenterTest.snackbarDispatcher,
|
||||||
permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
||||||
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
|
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
|
||||||
permalinkParser: PermalinkParser = FakePermalinkParser(),
|
permalinkParser: PermalinkParser = FakePermalinkParser(),
|
||||||
|
|
@ -1553,7 +1539,7 @@ class MessageComposerPresenterTest {
|
||||||
draftService: ComposerDraftService = FakeComposerDraftService(),
|
draftService: ComposerDraftService = FakeComposerDraftService(),
|
||||||
) = MessageComposerPresenter(
|
) = MessageComposerPresenter(
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
appCoroutineScope = coroutineScope,
|
appCoroutineScope = this,
|
||||||
room = room,
|
room = room,
|
||||||
mediaPickerProvider = pickerProvider,
|
mediaPickerProvider = pickerProvider,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
|
|
@ -155,7 +154,6 @@ class PollHistoryPresenterTest {
|
||||||
|
|
||||||
private fun TestScope.createPollHistoryPresenter(
|
private fun TestScope.createPollHistoryPresenter(
|
||||||
room: FakeJoinedRoom = FakeJoinedRoom(),
|
room: FakeJoinedRoom = FakeJoinedRoom(),
|
||||||
appCoroutineScope: CoroutineScope = this,
|
|
||||||
endPollAction: EndPollAction = FakeEndPollAction(),
|
endPollAction: EndPollAction = FakeEndPollAction(),
|
||||||
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory(
|
pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory(
|
||||||
|
|
@ -165,7 +163,7 @@ class PollHistoryPresenterTest {
|
||||||
),
|
),
|
||||||
): PollHistoryPresenter {
|
): PollHistoryPresenter {
|
||||||
return PollHistoryPresenter(
|
return PollHistoryPresenter(
|
||||||
appCoroutineScope = appCoroutineScope,
|
appCoroutineScope = this,
|
||||||
sendPollResponseAction = sendPollResponseAction,
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
endPollAction = endPollAction,
|
endPollAction = endPollAction,
|
||||||
pollHistoryItemFactory = pollHistoryItemFactory,
|
pollHistoryItemFactory = pollHistoryItemFactory,
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ dependencies {
|
||||||
implementation(projects.features.roomlist.api)
|
implementation(projects.features.roomlist.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.services.analytics.compose)
|
implementation(projects.services.analytics.compose)
|
||||||
|
implementation(projects.services.appnavstate.api)
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -37,8 +38,11 @@ class DefaultClearCacheUseCase @Inject constructor(
|
||||||
private val ftueService: FtueService,
|
private val ftueService: FtueService,
|
||||||
private val pushService: PushService,
|
private val pushService: PushService,
|
||||||
private val seenInvitesStore: SeenInvitesStore,
|
private val seenInvitesStore: SeenInvitesStore,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
) : ClearCacheUseCase {
|
) : ClearCacheUseCase {
|
||||||
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
||||||
|
// Active rooms should be disposed of before clearing the cache
|
||||||
|
activeRoomsHolder.clear(matrixClient.sessionId)
|
||||||
// Clear Matrix cache
|
// Clear Matrix cache
|
||||||
matrixClient.clearCache()
|
matrixClient.clearCache()
|
||||||
// Clear Coil cache
|
// Clear Coil cache
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,11 @@ import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||||
import io.element.android.features.preferences.impl.DefaultCacheService
|
import io.element.android.features.preferences.impl.DefaultCacheService
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.push.test.FakePushService
|
import io.element.android.libraries.push.test.FakePushService
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
|
@ -31,8 +34,10 @@ import org.robolectric.RobolectricTestRunner
|
||||||
class DefaultClearCacheUseCaseTest {
|
class DefaultClearCacheUseCaseTest {
|
||||||
@Test
|
@Test
|
||||||
fun `execute clear cache should do all the expected tasks`() = runTest {
|
fun `execute clear cache should do all the expected tasks`() = runTest {
|
||||||
|
val activeRoomsHolder = ActiveRoomsHolder().apply { addRoom(FakeJoinedRoom()) }
|
||||||
val clearCacheLambda = lambdaRecorder<Unit> { }
|
val clearCacheLambda = lambdaRecorder<Unit> { }
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
clearCacheLambda = clearCacheLambda,
|
clearCacheLambda = clearCacheLambda,
|
||||||
)
|
)
|
||||||
val defaultCacheService = DefaultCacheService()
|
val defaultCacheService = DefaultCacheService()
|
||||||
|
|
@ -55,6 +60,7 @@ class DefaultClearCacheUseCaseTest {
|
||||||
ftueService = ftueService,
|
ftueService = ftueService,
|
||||||
pushService = pushService,
|
pushService = pushService,
|
||||||
seenInvitesStore = seenInvitesStore,
|
seenInvitesStore = seenInvitesStore,
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
defaultCacheService.clearedCacheEventFlow.test {
|
defaultCacheService.clearedCacheEventFlow.test {
|
||||||
sut.invoke()
|
sut.invoke()
|
||||||
|
|
@ -64,6 +70,7 @@ class DefaultClearCacheUseCaseTest {
|
||||||
.with(value(matrixClient.sessionId), value(false))
|
.with(value(matrixClient.sessionId), value(false))
|
||||||
assertThat(awaitItem()).isEqualTo(matrixClient.sessionId)
|
assertThat(awaitItem()).isEqualTo(matrixClient.sessionId)
|
||||||
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
|
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
|
||||||
|
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import io.element.android.features.roomcall.api.RoomCallState.StandBy
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface RoomCallState {
|
sealed interface RoomCallState {
|
||||||
|
data object Unavailable : RoomCallState
|
||||||
|
|
||||||
data class StandBy(
|
data class StandBy(
|
||||||
val canStartCall: Boolean,
|
val canStartCall: Boolean,
|
||||||
) : RoomCallState
|
) : RoomCallState
|
||||||
|
|
@ -25,6 +27,7 @@ sealed interface RoomCallState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RoomCallState.hasPermissionToJoin() = when (this) {
|
fun RoomCallState.hasPermissionToJoin() = when (this) {
|
||||||
|
RoomCallState.Unavailable -> false
|
||||||
is StandBy -> canStartCall
|
is StandBy -> canStartCall
|
||||||
is OnGoing -> canJoinCall
|
is OnGoing -> canJoinCall
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ open class RoomCallStateProvider : PreviewParameterProvider<RoomCallState> {
|
||||||
anOngoingCallState(),
|
anOngoingCallState(),
|
||||||
anOngoingCallState(canJoinCall = false),
|
anOngoingCallState(canJoinCall = false),
|
||||||
anOngoingCallState(canJoinCall = true, isUserInTheCall = true),
|
anOngoingCallState(canJoinCall = true, isUserInTheCall = true),
|
||||||
|
RoomCallState.Unavailable,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ dependencies {
|
||||||
api(projects.features.roomcall.api)
|
api(projects.features.roomcall.api)
|
||||||
implementation(libs.kotlinx.collections.immutable)
|
implementation(libs.kotlinx.collections.immutable)
|
||||||
implementation(projects.features.call.api)
|
implementation(projects.features.call.api)
|
||||||
|
implementation(projects.features.enterprise.api)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
|
|
@ -32,6 +33,7 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.call.test)
|
testImplementation(projects.features.call.test)
|
||||||
|
testImplementation(projects.features.enterprise.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,11 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import io.element.android.features.call.api.CurrentCall
|
import io.element.android.features.call.api.CurrentCall
|
||||||
import io.element.android.features.call.api.CurrentCallService
|
import io.element.android.features.call.api.CurrentCallService
|
||||||
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.roomcall.api.RoomCallState
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
|
@ -23,9 +25,13 @@ import javax.inject.Inject
|
||||||
class RoomCallStatePresenter @Inject constructor(
|
class RoomCallStatePresenter @Inject constructor(
|
||||||
private val room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
private val currentCallService: CurrentCallService,
|
private val currentCallService: CurrentCallService,
|
||||||
|
private val enterpriseService: EnterpriseService,
|
||||||
) : Presenter<RoomCallState> {
|
) : Presenter<RoomCallState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomCallState {
|
override fun present(): RoomCallState {
|
||||||
|
val isAvailable by produceState(false) {
|
||||||
|
value = enterpriseService.isElementCallAvailable()
|
||||||
|
}
|
||||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value)
|
val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value)
|
||||||
|
|
@ -41,6 +47,7 @@ class RoomCallStatePresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val callState = when {
|
val callState = when {
|
||||||
|
isAvailable.not() -> RoomCallState.Unavailable
|
||||||
roomInfo.hasRoomCall -> RoomCallState.OnGoing(
|
roomInfo.hasRoomCall -> RoomCallState.OnGoing(
|
||||||
canJoinCall = canJoinCall,
|
canJoinCall = canJoinCall,
|
||||||
isUserInTheCall = isUserInTheCall,
|
isUserInTheCall = isUserInTheCall,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.call.api.CurrentCall
|
import io.element.android.features.call.api.CurrentCall
|
||||||
import io.element.android.features.call.api.CurrentCallService
|
import io.element.android.features.call.api.CurrentCallService
|
||||||
import io.element.android.features.call.test.FakeCurrentCallService
|
import io.element.android.features.call.test.FakeCurrentCallService
|
||||||
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.roomcall.api.RoomCallState
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
|
|
@ -25,12 +26,13 @@ class RoomCallStatePresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
canUserJoinCallResult = { Result.success(false) },
|
canUserJoinCallResult = { Result.success(false) },
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val presenter = createRoomCallStatePresenter(joinedRoom = room)
|
val presenter = createRoomCallStatePresenter(joinedRoom = room)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState).isEqualTo(
|
assertThat(initialState).isEqualTo(
|
||||||
RoomCallState.StandBy(
|
RoomCallState.StandBy(
|
||||||
|
|
@ -40,10 +42,29 @@ class RoomCallStatePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - element call not available`() = runTest {
|
||||||
|
val room = FakeJoinedRoom(
|
||||||
|
baseRoom = FakeBaseRoom(
|
||||||
|
canUserJoinCallResult = { Result.success(false) },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val presenter = createRoomCallStatePresenter(
|
||||||
|
joinedRoom = room,
|
||||||
|
isElementCallAvailable = false,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState).isEqualTo(
|
||||||
|
RoomCallState.Unavailable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state - user can join call`() = runTest {
|
fun `present - initial state - user can join call`() = runTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -69,6 +90,7 @@ class RoomCallStatePresenterTest {
|
||||||
)
|
)
|
||||||
val presenter = createRoomCallStatePresenter(joinedRoom = room)
|
val presenter = createRoomCallStatePresenter(joinedRoom = room)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
assertThat(awaitItem()).isEqualTo(
|
assertThat(awaitItem()).isEqualTo(
|
||||||
RoomCallState.OnGoing(
|
RoomCallState.OnGoing(
|
||||||
canJoinCall = false,
|
canJoinCall = false,
|
||||||
|
|
@ -83,15 +105,15 @@ class RoomCallStatePresenterTest {
|
||||||
fun `present - user has joined the call on another session`() = runTest {
|
fun `present - user has joined the call on another session`() = runTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
).apply {
|
).apply {
|
||||||
givenRoomInfo(
|
givenRoomInfo(
|
||||||
aRoomInfo(
|
aRoomInfo(
|
||||||
hasRoomCall = true,
|
hasRoomCall = true,
|
||||||
activeRoomCallParticipants = listOf(sessionId),
|
activeRoomCallParticipants = listOf(sessionId),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
val presenter = createRoomCallStatePresenter(joinedRoom = room)
|
val presenter = createRoomCallStatePresenter(joinedRoom = room)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|
@ -110,15 +132,15 @@ class RoomCallStatePresenterTest {
|
||||||
fun `present - user has joined the call locally`() = runTest {
|
fun `present - user has joined the call locally`() = runTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
).apply {
|
).apply {
|
||||||
givenRoomInfo(
|
givenRoomInfo(
|
||||||
aRoomInfo(
|
aRoomInfo(
|
||||||
hasRoomCall = true,
|
hasRoomCall = true,
|
||||||
activeRoomCallParticipants = listOf(sessionId),
|
activeRoomCallParticipants = listOf(sessionId),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
val presenter = createRoomCallStatePresenter(
|
val presenter = createRoomCallStatePresenter(
|
||||||
joinedRoom = room,
|
joinedRoom = room,
|
||||||
|
|
@ -140,15 +162,15 @@ class RoomCallStatePresenterTest {
|
||||||
fun `present - user leaves the call`() = runTest {
|
fun `present - user leaves the call`() = runTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
).apply {
|
).apply {
|
||||||
givenRoomInfo(
|
givenRoomInfo(
|
||||||
aRoomInfo(
|
aRoomInfo(
|
||||||
hasRoomCall = true,
|
hasRoomCall = true,
|
||||||
activeRoomCallParticipants = listOf(sessionId),
|
activeRoomCallParticipants = listOf(sessionId),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
val currentCall = MutableStateFlow<CurrentCall>(CurrentCall.RoomCall(room.roomId))
|
val currentCall = MutableStateFlow<CurrentCall>(CurrentCall.RoomCall(room.roomId))
|
||||||
val currentCallService = FakeCurrentCallService(currentCall = currentCall)
|
val currentCallService = FakeCurrentCallService(currentCall = currentCall)
|
||||||
|
|
@ -203,10 +225,14 @@ class RoomCallStatePresenterTest {
|
||||||
private fun createRoomCallStatePresenter(
|
private fun createRoomCallStatePresenter(
|
||||||
joinedRoom: JoinedRoom,
|
joinedRoom: JoinedRoom,
|
||||||
currentCallService: CurrentCallService = FakeCurrentCallService(),
|
currentCallService: CurrentCallService = FakeCurrentCallService(),
|
||||||
|
isElementCallAvailable: Boolean = true,
|
||||||
): RoomCallStatePresenter {
|
): RoomCallStatePresenter {
|
||||||
return RoomCallStatePresenter(
|
return RoomCallStatePresenter(
|
||||||
room = joinedRoom,
|
room = joinedRoom,
|
||||||
currentCallService = currentCallService,
|
currentCallService = currentCallService,
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
isElementCallAvailableResult = { isElementCallAvailable },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
|
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||||
|
|
@ -241,6 +242,8 @@ class RoomDetailsPresenterTest {
|
||||||
fun `present - initial state when user can not invite others to room`() = runTest {
|
fun `present - initial state when user can not invite others to room`() = runTest {
|
||||||
val room = aJoinedRoom(
|
val room = aJoinedRoom(
|
||||||
canInviteResult = { Result.success(false) },
|
canInviteResult = { Result.success(false) },
|
||||||
|
canKickResult = { Result.success(false) },
|
||||||
|
canBanResult = { Result.success(false) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
canSendStateResult = { _, _ -> Result.success(true) },
|
canSendStateResult = { _, _ -> Result.success(true) },
|
||||||
)
|
)
|
||||||
|
|
@ -277,6 +280,8 @@ class RoomDetailsPresenterTest {
|
||||||
else -> Result.failure(Throwable("Whelp"))
|
else -> Result.failure(Throwable("Whelp"))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
canBanResult = { Result.success(false) },
|
||||||
|
canKickResult = { Result.success(false) },
|
||||||
canInviteResult = { Result.success(false) },
|
canInviteResult = { Result.success(false) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
|
|
@ -304,6 +309,8 @@ class RoomDetailsPresenterTest {
|
||||||
else -> Result.failure(Throwable("Whelp"))
|
else -> Result.failure(Throwable("Whelp"))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
canKickResult = { Result.success(false) },
|
||||||
|
canBanResult = { Result.success(false) },
|
||||||
canInviteResult = { Result.success(false) },
|
canInviteResult = { Result.success(false) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
getUpdatedMemberResult = { userId ->
|
getUpdatedMemberResult = { userId ->
|
||||||
|
|
@ -353,6 +360,8 @@ class RoomDetailsPresenterTest {
|
||||||
else -> Result.failure(Throwable("Whelp"))
|
else -> Result.failure(Throwable("Whelp"))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
userDisplayNameResult = { Result.success(A_USER_NAME) },
|
||||||
|
userAvatarUrlResult = { Result.success(AN_AVATAR_URL) },
|
||||||
canInviteResult = { Result.success(true) },
|
canInviteResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
getUpdatedMemberResult = { userId ->
|
getUpdatedMemberResult = { userId ->
|
||||||
|
|
@ -397,6 +406,12 @@ class RoomDetailsPresenterTest {
|
||||||
else -> Result.failure(Throwable("Whelp"))
|
else -> Result.failure(Throwable("Whelp"))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
canKickResult = {
|
||||||
|
Result.success(false)
|
||||||
|
},
|
||||||
|
canBanResult = {
|
||||||
|
Result.success(false)
|
||||||
|
},
|
||||||
canInviteResult = {
|
canInviteResult = {
|
||||||
Result.success(false)
|
Result.success(false)
|
||||||
},
|
},
|
||||||
|
|
@ -424,6 +439,12 @@ class RoomDetailsPresenterTest {
|
||||||
else -> Result.failure(Throwable("Whelp"))
|
else -> Result.failure(Throwable("Whelp"))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
canBanResult = {
|
||||||
|
Result.success(false)
|
||||||
|
},
|
||||||
|
canKickResult = {
|
||||||
|
Result.success(false)
|
||||||
|
},
|
||||||
canInviteResult = {
|
canInviteResult = {
|
||||||
Result.success(false)
|
Result.success(false)
|
||||||
},
|
},
|
||||||
|
|
@ -450,6 +471,12 @@ class RoomDetailsPresenterTest {
|
||||||
else -> Result.failure(Throwable("Whelp"))
|
else -> Result.failure(Throwable("Whelp"))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
canKickResult = {
|
||||||
|
Result.success(false)
|
||||||
|
},
|
||||||
|
canBanResult = {
|
||||||
|
Result.success(false)
|
||||||
|
},
|
||||||
canInviteResult = {
|
canInviteResult = {
|
||||||
Result.success(false)
|
Result.success(false)
|
||||||
},
|
},
|
||||||
|
|
@ -476,6 +503,12 @@ class RoomDetailsPresenterTest {
|
||||||
else -> Result.failure(Throwable("Whelp"))
|
else -> Result.failure(Throwable("Whelp"))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
canKickResult = {
|
||||||
|
Result.success(false)
|
||||||
|
},
|
||||||
|
canBanResult = {
|
||||||
|
Result.success(false)
|
||||||
|
},
|
||||||
canInviteResult = {
|
canInviteResult = {
|
||||||
Result.success(false)
|
Result.success(false)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
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
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
|
|
@ -289,7 +290,14 @@ class EditBaseRoomAddressPresenterTest {
|
||||||
val navigator = FakeSecurityAndPrivacyNavigator(
|
val navigator = FakeSecurityAndPrivacyNavigator(
|
||||||
closeEditRoomAddressLambda = closeEditAddressLambda
|
closeEditRoomAddressLambda = closeEditAddressLambda
|
||||||
)
|
)
|
||||||
val presenter = createEditRoomAddressPresenter(navigator = navigator)
|
val presenter = createEditRoomAddressPresenter(
|
||||||
|
navigator = navigator,
|
||||||
|
room = FakeJoinedRoom(
|
||||||
|
publishRoomAliasInRoomDirectoryResult = {
|
||||||
|
Result.failure(AN_EXCEPTION)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
with(awaitItem()) {
|
with(awaitItem()) {
|
||||||
eventSink(EditRoomAddressEvents.RoomAddressChanged("valid"))
|
eventSink(EditRoomAddressEvents.RoomAddressChanged("valid"))
|
||||||
|
|
@ -313,7 +321,13 @@ class EditBaseRoomAddressPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - dismiss error`() = runTest {
|
fun `present - dismiss error`() = runTest {
|
||||||
val presenter = createEditRoomAddressPresenter()
|
val presenter = createEditRoomAddressPresenter(
|
||||||
|
room = FakeJoinedRoom(
|
||||||
|
publishRoomAliasInRoomDirectoryResult = {
|
||||||
|
Result.failure(AN_EXCEPTION)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
with(awaitItem()) {
|
with(awaitItem()) {
|
||||||
eventSink(EditRoomAddressEvents.Save)
|
eventSink(EditRoomAddressEvents.Save)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle
|
import io.element.android.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
|
@ -132,11 +131,10 @@ class ResetIdentityFlowManagerTest {
|
||||||
private fun TestScope.createFlowManager(
|
private fun TestScope.createFlowManager(
|
||||||
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
||||||
client: FakeMatrixClient = FakeMatrixClient(encryptionService = encryptionService),
|
client: FakeMatrixClient = FakeMatrixClient(encryptionService = encryptionService),
|
||||||
sessionCoroutineScope: CoroutineScope = this,
|
|
||||||
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
||||||
) = ResetIdentityFlowManager(
|
) = ResetIdentityFlowManager(
|
||||||
matrixClient = client,
|
matrixClient = client,
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = this,
|
||||||
sessionVerificationService = sessionVerificationService,
|
sessionVerificationService = sessionVerificationService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ dependencies {
|
||||||
implementation(projects.libraries.roomselect.api)
|
implementation(projects.libraries.roomselect.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
|
implementation(projects.services.appnavstate.api)
|
||||||
api(libs.statemachine)
|
api(libs.statemachine)
|
||||||
api(projects.features.share.api)
|
api(projects.features.share.api)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,11 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
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.room.JoinedRoom
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
@ -33,6 +35,7 @@ class SharePresenter @AssistedInject constructor(
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val mediaPreProcessor: MediaPreProcessor,
|
private val mediaPreProcessor: MediaPreProcessor,
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
) : Presenter<ShareState> {
|
) : Presenter<ShareState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -59,6 +62,12 @@ class SharePresenter @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? {
|
||||||
|
return activeRoomsHolder.getActiveRoom(matrixClient.sessionId)
|
||||||
|
?.takeIf { it.roomId == roomId }
|
||||||
|
?: matrixClient.getJoinedRoom(roomId)
|
||||||
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.share(
|
private fun CoroutineScope.share(
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
roomIds: List<RoomId>,
|
roomIds: List<RoomId>,
|
||||||
|
|
@ -72,7 +81,7 @@ class SharePresenter @AssistedInject constructor(
|
||||||
} else {
|
} else {
|
||||||
roomIds
|
roomIds
|
||||||
.map { roomId ->
|
.map { roomId ->
|
||||||
val room = matrixClient.getJoinedRoom(roomId) ?: return@map false
|
val room = getJoinedRoom(roomId) ?: return@map false
|
||||||
val mediaSender = MediaSender(
|
val mediaSender = MediaSender(
|
||||||
preProcessor = mediaPreProcessor,
|
preProcessor = mediaPreProcessor,
|
||||||
room = room,
|
room = room,
|
||||||
|
|
@ -86,7 +95,11 @@ class SharePresenter @AssistedInject constructor(
|
||||||
).isSuccess
|
).isSuccess
|
||||||
}
|
}
|
||||||
.all { it }
|
.all { it }
|
||||||
.also { room.destroy() }
|
.also {
|
||||||
|
if (activeRoomsHolder.getActiveRoomMatching(matrixClient.sessionId, roomId) == null) {
|
||||||
|
room.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.all { it }
|
.all { it }
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +107,7 @@ class SharePresenter @AssistedInject constructor(
|
||||||
onPlainText = { text ->
|
onPlainText = { text ->
|
||||||
roomIds
|
roomIds
|
||||||
.map { roomId ->
|
.map { roomId ->
|
||||||
matrixClient.getJoinedRoom(roomId)?.liveTimeline?.sendMessage(
|
getJoinedRoom(roomId)?.liveTimeline?.sendMessage(
|
||||||
body = text,
|
body = text,
|
||||||
htmlBody = null,
|
htmlBody = null,
|
||||||
intentionalMentions = emptyList(),
|
intentionalMentions = emptyList(),
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
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 kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -163,7 +164,8 @@ class SharePresenterTest {
|
||||||
intent: Intent = Intent(),
|
intent: Intent = Intent(),
|
||||||
shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(),
|
shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(),
|
||||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor()
|
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||||
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
): SharePresenter {
|
): SharePresenter {
|
||||||
return SharePresenter(
|
return SharePresenter(
|
||||||
intent = intent,
|
intent = intent,
|
||||||
|
|
@ -171,7 +173,8 @@ class SharePresenterTest {
|
||||||
shareIntentHandler = shareIntentHandler,
|
shareIntentHandler = shareIntentHandler,
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
mediaPreProcessor = mediaPreProcessor,
|
mediaPreProcessor = mediaPreProcessor,
|
||||||
InMemorySessionPreferencesStore(),
|
sessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ dependencies {
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.mediaviewer.api)
|
implementation(projects.libraries.mediaviewer.api)
|
||||||
implementation(projects.features.call.api)
|
implementation(projects.features.call.api)
|
||||||
|
implementation(projects.features.enterprise.api)
|
||||||
implementation(projects.features.verifysession.api)
|
implementation(projects.features.verifysession.api)
|
||||||
api(projects.features.userprofile.api)
|
api(projects.features.userprofile.api)
|
||||||
api(projects.features.userprofile.shared)
|
api(projects.features.userprofile.shared)
|
||||||
|
|
@ -49,6 +50,7 @@ dependencies {
|
||||||
testImplementation(libs.test.robolectric)
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.createroom.test)
|
testImplementation(projects.features.createroom.test)
|
||||||
|
testImplementation(projects.features.enterprise.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.createroom.api.StartDMAction
|
import io.element.android.features.createroom.api.StartDMAction
|
||||||
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.userprofile.api.UserProfileEvents
|
import io.element.android.features.userprofile.api.UserProfileEvents
|
||||||
import io.element.android.features.userprofile.api.UserProfileState
|
import io.element.android.features.userprofile.api.UserProfileState
|
||||||
import io.element.android.features.userprofile.api.UserProfileState.ConfirmationDialog
|
import io.element.android.features.userprofile.api.UserProfileState.ConfirmationDialog
|
||||||
|
|
@ -44,6 +45,7 @@ class UserProfilePresenter @AssistedInject constructor(
|
||||||
@Assisted private val userId: UserId,
|
@Assisted private val userId: UserId,
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val startDMAction: StartDMAction,
|
private val startDMAction: StartDMAction,
|
||||||
|
private val enterpriseService: EnterpriseService,
|
||||||
) : Presenter<UserProfileState> {
|
) : Presenter<UserProfileState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -59,11 +61,21 @@ class UserProfilePresenter @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getCanCall(roomId: RoomId?): State<Boolean> {
|
private fun getCanCall(roomId: RoomId?): State<Boolean> {
|
||||||
return produceState(initialValue = false, roomId) {
|
val isElementCallAvailable by produceState(initialValue = false, roomId) {
|
||||||
value = if (client.isMe(userId)) {
|
value = enterpriseService.isElementCallAvailable()
|
||||||
false
|
}
|
||||||
} else {
|
|
||||||
roomId?.let { client.getRoom(it)?.canUserJoinCall(client.sessionId)?.getOrNull() == true }.orFalse()
|
return produceState(initialValue = false, isElementCallAvailable, roomId) {
|
||||||
|
value = when {
|
||||||
|
isElementCallAvailable.not() -> false
|
||||||
|
client.isMe(userId) -> false
|
||||||
|
else ->
|
||||||
|
roomId
|
||||||
|
?.let { client.getRoom(it) }
|
||||||
|
?.use { room ->
|
||||||
|
room.canUserJoinCall(client.sessionId).getOrNull()
|
||||||
|
}
|
||||||
|
.orFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
|
import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
|
||||||
import io.element.android.features.createroom.api.StartDMAction
|
import io.element.android.features.createroom.api.StartDMAction
|
||||||
import io.element.android.features.createroom.test.FakeStartDMAction
|
import io.element.android.features.createroom.test.FakeStartDMAction
|
||||||
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.userprofile.api.UserProfileEvents
|
import io.element.android.features.userprofile.api.UserProfileEvents
|
||||||
import io.element.android.features.userprofile.api.UserProfileState
|
import io.element.android.features.userprofile.api.UserProfileState
|
||||||
import io.element.android.features.userprofile.api.UserProfileVerificationState
|
import io.element.android.features.userprofile.api.UserProfileVerificationState
|
||||||
|
|
@ -37,7 +38,6 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
|
||||||
import io.element.android.tests.testutils.lambda.any
|
import io.element.android.tests.testutils.lambda.any
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
|
@ -81,6 +81,8 @@ class UserProfilePresenterTest {
|
||||||
fun `present - canCall is true when all the conditions are met`() {
|
fun `present - canCall is true when all the conditions are met`() {
|
||||||
testCanCall(
|
testCanCall(
|
||||||
expectedResult = true,
|
expectedResult = true,
|
||||||
|
skipItems = 3,
|
||||||
|
checkThatRoomIsDestroyed = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,11 +118,22 @@ class UserProfilePresenterTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - canCall is false when call is not available`() {
|
||||||
|
testCanCall(
|
||||||
|
isElementCallAvailable = false,
|
||||||
|
expectedResult = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun testCanCall(
|
private fun testCanCall(
|
||||||
|
isElementCallAvailable: Boolean = true,
|
||||||
canUserJoinCallResult: Result<Boolean> = Result.success(true),
|
canUserJoinCallResult: Result<Boolean> = Result.success(true),
|
||||||
dmRoom: RoomId? = A_ROOM_ID,
|
dmRoom: RoomId? = A_ROOM_ID,
|
||||||
canFindRoom: Boolean = true,
|
canFindRoom: Boolean = true,
|
||||||
expectedResult: Boolean,
|
expectedResult: Boolean,
|
||||||
|
skipItems: Int = 1,
|
||||||
|
checkThatRoomIsDestroyed: Boolean = false,
|
||||||
) = runTest {
|
) = runTest {
|
||||||
val room = FakeBaseRoom(
|
val room = FakeBaseRoom(
|
||||||
canUserJoinCallResult = { canUserJoinCallResult },
|
canUserJoinCallResult = { canUserJoinCallResult },
|
||||||
|
|
@ -134,11 +147,15 @@ class UserProfilePresenterTest {
|
||||||
val presenter = createUserProfilePresenter(
|
val presenter = createUserProfilePresenter(
|
||||||
userId = A_USER_ID_2,
|
userId = A_USER_ID_2,
|
||||||
client = client,
|
client = client,
|
||||||
|
isElementCallAvailable = isElementCallAvailable,
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitLastSequentialItem()
|
val initialState = awaitFirstItem(skipItems)
|
||||||
assertThat(initialState.canCall).isEqualTo(expectedResult)
|
assertThat(initialState.canCall).isEqualTo(expectedResult)
|
||||||
}
|
}
|
||||||
|
if (checkThatRoomIsDestroyed) {
|
||||||
|
room.assertDestroyed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -202,7 +219,7 @@ class UserProfilePresenterTest {
|
||||||
)
|
)
|
||||||
val presenter = createUserProfilePresenter(client = matrixClient)
|
val presenter = createUserProfilePresenter(client = matrixClient)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem(count = 2)
|
||||||
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
|
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
|
||||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||||
val errorState = awaitItem()
|
val errorState = awaitItem()
|
||||||
|
|
@ -220,7 +237,7 @@ class UserProfilePresenterTest {
|
||||||
)
|
)
|
||||||
val presenter = createUserProfilePresenter(client = matrixClient)
|
val presenter = createUserProfilePresenter(client = matrixClient)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem(count = 2)
|
||||||
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
|
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
|
||||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||||
val errorState = awaitItem()
|
val errorState = awaitItem()
|
||||||
|
|
@ -363,8 +380,8 @@ class UserProfilePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(count: Int = 1): T {
|
||||||
skipItems(1)
|
skipItems(count)
|
||||||
return awaitItem()
|
return awaitItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -387,12 +404,16 @@ class UserProfilePresenterTest {
|
||||||
private fun createUserProfilePresenter(
|
private fun createUserProfilePresenter(
|
||||||
client: MatrixClient = createFakeMatrixClient(),
|
client: MatrixClient = createFakeMatrixClient(),
|
||||||
userId: UserId = UserId("@alice:server.org"),
|
userId: UserId = UserId("@alice:server.org"),
|
||||||
startDMAction: StartDMAction = FakeStartDMAction()
|
startDMAction: StartDMAction = FakeStartDMAction(),
|
||||||
|
isElementCallAvailable: Boolean = true,
|
||||||
): UserProfilePresenter {
|
): UserProfilePresenter {
|
||||||
return UserProfilePresenter(
|
return UserProfilePresenter(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
client = client,
|
client = client,
|
||||||
startDMAction = startDMAction,
|
startDMAction = startDMAction,
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
isElementCallAvailableResult = { isElementCallAvailable },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ constraintlayout = "2.2.1"
|
||||||
constraintlayout_compose = "1.1.1"
|
constraintlayout_compose = "1.1.1"
|
||||||
lifecycle = "2.9.0"
|
lifecycle = "2.9.0"
|
||||||
activity = "1.10.1"
|
activity = "1.10.1"
|
||||||
media3 = "1.6.1"
|
media3 = "1.7.1"
|
||||||
camera = "1.4.2"
|
camera = "1.4.2"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
|
|
@ -40,7 +40,7 @@ serialization_json = "1.8.1"
|
||||||
coil = "3.1.0"
|
coil = "3.1.0"
|
||||||
showkase = "1.0.3"
|
showkase = "1.0.3"
|
||||||
appyx = "1.7.0"
|
appyx = "1.7.0"
|
||||||
sqldelight = "2.0.2"
|
sqldelight = "2.1.0"
|
||||||
wysiwyg = "2.38.3"
|
wysiwyg = "2.38.3"
|
||||||
telephoto = "0.15.1"
|
telephoto = "0.15.1"
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", ve
|
||||||
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
|
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
|
||||||
gms_google_services = "com.google.gms:google-services:4.4.2"
|
gms_google_services = "com.google.gms:google-services:4.4.2"
|
||||||
# https://firebase.google.com/docs/android/setup#available-libraries
|
# https://firebase.google.com/docs/android/setup#available-libraries
|
||||||
google_firebase_bom = "com.google.firebase:firebase-bom:33.13.0"
|
google_firebase_bom = "com.google.firebase:firebase-bom:33.14.0"
|
||||||
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
|
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
|
||||||
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
|
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
|
||||||
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
|
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
|
||||||
|
|
@ -169,7 +169,7 @@ jsoup = "org.jsoup:jsoup:1.20.1"
|
||||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||||
molecule-runtime = "app.cash.molecule:molecule-runtime:2.1.0"
|
molecule-runtime = "app.cash.molecule:molecule-runtime:2.1.0"
|
||||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.5.21"
|
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.5.26"
|
||||||
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
||||||
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
||||||
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||||
|
|
@ -177,7 +177,7 @@ sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.
|
||||||
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:android-database-sqlcipher:4.5.4"
|
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
|
||||||
sqlite = "androidx.sqlite:sqlite-ktx:2.5.1"
|
sqlite = "androidx.sqlite:sqlite-ktx:2.5.1"
|
||||||
unifiedpush = "com.github.UnifiedPush:android-connector:2.4.0"
|
unifiedpush = "org.unifiedpush.android:connector:3.0.9"
|
||||||
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.2"
|
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.2"
|
||||||
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
||||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||||
|
|
@ -190,7 +190,7 @@ opusencoder = "io.element.android:opusencoder:1.1.0"
|
||||||
zxing_cpp = "io.github.zxing-cpp:android:2.3.0"
|
zxing_cpp = "io.github.zxing-cpp:android:2.3.0"
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
posthog = "com.posthog:posthog-android:3.15.0"
|
posthog = "com.posthog:posthog-android:3.15.1"
|
||||||
sentry = "io.sentry:sentry-android:8.12.0"
|
sentry = "io.sentry:sentry-android:8.12.0"
|
||||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
|
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
|
||||||
|
|
@ -232,7 +232,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
|
||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" }
|
anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" }
|
||||||
detekt = "io.gitlab.arturbosch.detekt:1.23.8"
|
detekt = "io.gitlab.arturbosch.detekt:1.23.8"
|
||||||
ktlint = "org.jlleitschuh.gradle.ktlint:12.2.0"
|
ktlint = "org.jlleitschuh.gradle.ktlint:12.3.0"
|
||||||
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
|
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
|
||||||
dependencycheck = "org.owasp.dependencycheck:12.1.1"
|
dependencycheck = "org.owasp.dependencycheck:12.1.1"
|
||||||
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
|
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
|
||||||
|
|
|
||||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa
|
distributionSha256Sum=845952a9d6afa783db70bb3b0effaae45ae5542ca2bb7929619e8af49cb634cf
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@ package io.element.android.libraries.matrix.api.notification
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
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.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||||
|
|
||||||
data class NotificationData(
|
data class NotificationData(
|
||||||
|
val sessionId: SessionId,
|
||||||
val eventId: EventId,
|
val eventId: EventId,
|
||||||
val threadId: ThreadId?,
|
val threadId: ThreadId?,
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,5 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
interface NotificationService {
|
interface NotificationService {
|
||||||
suspend fun getNotification(roomId: RoomId, eventId: EventId): Result<NotificationData?>
|
suspend fun getNotifications(ids: Map<RoomId, List<EventId>>): Result<Map<EventId, NotificationData>>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,10 +120,6 @@ class RustClientSessionDelegate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didRefreshTokens() {
|
|
||||||
// This is done in `saveSessionInKeychain(Session)` instead.
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun retrieveSessionFromKeychain(userId: String): Session {
|
override fun retrieveSessionFromKeychain(userId: String): Session {
|
||||||
// This should never be called, as it's only used for multi-process setups
|
// This should never be called, as it's only used for multi-process setups
|
||||||
error("retrieveSessionFromKeychain should never be called for Android")
|
error("retrieveSessionFromKeychain should never be called for Android")
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ class RustMatrixClient(
|
||||||
)
|
)
|
||||||
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(innerSyncService)
|
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(innerSyncService)
|
||||||
private val innerNotificationClient = runBlocking { innerClient.notificationClient(notificationProcessSetup) }
|
private val innerNotificationClient = runBlocking { innerClient.notificationClient(notificationProcessSetup) }
|
||||||
private val notificationService = RustNotificationService(innerNotificationClient, dispatchers, clock)
|
private val notificationService = RustNotificationService(sessionId, innerNotificationClient, dispatchers, clock)
|
||||||
private val notificationSettingsService = RustNotificationSettingsService(innerClient, sessionCoroutineScope, dispatchers)
|
private val notificationSettingsService = RustNotificationSettingsService(innerClient, sessionCoroutineScope, dispatchers)
|
||||||
private val encryptionService = RustEncryptionService(
|
private val encryptionService = RustEncryptionService(
|
||||||
client = innerClient,
|
client = innerClient,
|
||||||
|
|
@ -189,7 +189,6 @@ class RustMatrixClient(
|
||||||
|
|
||||||
private val roomMembershipObserver = RoomMembershipObserver()
|
private val roomMembershipObserver = RoomMembershipObserver()
|
||||||
private val roomFactory = RustRoomFactory(
|
private val roomFactory = RustRoomFactory(
|
||||||
innerClient = innerClient,
|
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
innerRoomListService = innerRoomListService,
|
innerRoomListService = innerRoomListService,
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,9 @@ class RustMatrixClientFactory @Inject constructor(
|
||||||
suspend fun create(client: Client): RustMatrixClient {
|
suspend fun create(client: Client): RustMatrixClient {
|
||||||
val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()
|
val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()
|
||||||
|
|
||||||
|
client.setUtdDelegate(UtdTracker(analyticsService))
|
||||||
|
|
||||||
val syncService = client.syncService()
|
val syncService = client.syncService()
|
||||||
.withUtdHook(UtdTracker(analyticsService))
|
|
||||||
.withOfflineMode()
|
.withOfflineMode()
|
||||||
.finish()
|
.finish()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.notification
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
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.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||||
|
|
@ -25,6 +26,7 @@ class NotificationMapper(
|
||||||
private val notificationContentMapper = NotificationContentMapper()
|
private val notificationContentMapper = NotificationContentMapper()
|
||||||
|
|
||||||
fun map(
|
fun map(
|
||||||
|
sessionId: SessionId,
|
||||||
eventId: EventId,
|
eventId: EventId,
|
||||||
roomId: RoomId,
|
roomId: RoomId,
|
||||||
notificationItem: NotificationItem
|
notificationItem: NotificationItem
|
||||||
|
|
@ -35,6 +37,7 @@ class NotificationMapper(
|
||||||
activeMembersCount = item.roomInfo.joinedMembersCount.toInt(),
|
activeMembersCount = item.roomInfo.joinedMembersCount.toInt(),
|
||||||
)
|
)
|
||||||
NotificationData(
|
NotificationData(
|
||||||
|
sessionId = sessionId,
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
// FIXME once the `NotificationItem` in the SDK returns the thread id
|
// FIXME once the `NotificationItem` in the SDK returns the thread id
|
||||||
threadId = null,
|
threadId = null,
|
||||||
|
|
|
||||||
|
|
@ -10,28 +10,45 @@ package io.element.android.libraries.matrix.impl.notification
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
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.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||||
import io.element.android.libraries.matrix.api.notification.NotificationService
|
import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.NotificationClient
|
import org.matrix.rustcomponents.sdk.NotificationClient
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.NotificationItemsRequest
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
class RustNotificationService(
|
class RustNotificationService(
|
||||||
|
private val sessionId: SessionId,
|
||||||
private val notificationClient: NotificationClient,
|
private val notificationClient: NotificationClient,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
clock: SystemClock,
|
clock: SystemClock,
|
||||||
) : NotificationService {
|
) : NotificationService {
|
||||||
private val notificationMapper: NotificationMapper = NotificationMapper(clock)
|
private val notificationMapper: NotificationMapper = NotificationMapper(clock)
|
||||||
|
|
||||||
override suspend fun getNotification(
|
override suspend fun getNotifications(
|
||||||
roomId: RoomId,
|
ids: Map<RoomId, List<EventId>>
|
||||||
eventId: EventId,
|
): Result<Map<EventId, NotificationData>> = withContext(dispatchers.io) {
|
||||||
): Result<NotificationData?> = withContext(dispatchers.io) {
|
|
||||||
runCatching {
|
runCatching {
|
||||||
val item = notificationClient.getNotification(roomId.value, eventId.value)
|
val requests = ids.map { (roomId, eventIds) ->
|
||||||
item?.use {
|
NotificationItemsRequest(
|
||||||
notificationMapper.map(eventId, roomId, it)
|
roomId = roomId.value,
|
||||||
|
eventIds = eventIds.map { it.value }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val items = notificationClient.getNotifications(requests)
|
||||||
|
buildMap {
|
||||||
|
val eventIds = requests.flatMap { it.eventIds }
|
||||||
|
for (eventId in eventIds) {
|
||||||
|
val item = items[eventId]
|
||||||
|
if (item != null) {
|
||||||
|
val roomId = RoomId(requests.find { it.eventIds.contains(eventId) }?.roomId!!)
|
||||||
|
put(EventId(eventId), notificationMapper.map(sessionId, EventId(eventId), roomId, item))
|
||||||
|
} else {
|
||||||
|
Timber.e("Could not retrieve event for notification with $eventId")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@
|
||||||
package io.element.android.libraries.matrix.impl.proxy
|
package io.element.android.libraries.matrix.impl.proxy
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
|
@ -32,6 +34,13 @@ class DefaultProxyProvider @Inject constructor(
|
||||||
private val context: Context
|
private val context: Context
|
||||||
) : ProxyProvider {
|
) : ProxyProvider {
|
||||||
override fun provides(): String? {
|
override fun provides(): String? {
|
||||||
|
val defaultProxy = context.getSystemService<ConnectivityManager>()?.defaultProxy
|
||||||
|
if (defaultProxy == null) {
|
||||||
|
// Note: can be tested by running:
|
||||||
|
// adb shell settings put global http_proxy :0
|
||||||
|
Timber.d("No default proxy")
|
||||||
|
return null
|
||||||
|
}
|
||||||
return Settings.Global.getString(context.contentResolver, Settings.Global.HTTP_PROXY)
|
return Settings.Global.getString(context.contentResolver, Settings.Global.HTTP_PROXY)
|
||||||
?.also {
|
?.also {
|
||||||
Timber.d("Using global proxy")
|
Timber.d("Using global proxy")
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,7 @@ class JoinedRustRoom(
|
||||||
internalIdPrefix = internalIdPrefix,
|
internalIdPrefix = internalIdPrefix,
|
||||||
dateDividerMode = dateDividerMode,
|
dateDividerMode = dateDividerMode,
|
||||||
trackReadReceipts = trackReadReceipts,
|
trackReadReceipts = trackReadReceipts,
|
||||||
|
reportUtds = true,
|
||||||
)
|
)
|
||||||
).let { innerTimeline ->
|
).let { innerTimeline ->
|
||||||
val mode = when (createTimelineParams) {
|
val mode = when (createTimelineParams) {
|
||||||
|
|
@ -416,7 +417,6 @@ class JoinedRustRoom(
|
||||||
RustWidgetDriver(
|
RustWidgetDriver(
|
||||||
widgetSettings = widgetSettings,
|
widgetSettings = widgetSettings,
|
||||||
room = innerRoom,
|
room = innerRoom,
|
||||||
joinedRustRoom = this,
|
|
||||||
widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider {
|
widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider {
|
||||||
override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities {
|
override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities {
|
||||||
return getElementCallRequiredPermissions(sessionId.value, baseRoom.deviceId.value)
|
return getElementCallRequiredPermissions(sessionId.value, baseRoom.deviceId.value)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import io.element.android.libraries.core.coroutine.parallelMap
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
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.room.ForwardEventException
|
import io.element.android.libraries.matrix.api.room.ForwardEventException
|
||||||
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
|
|
||||||
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
|
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
|
||||||
import io.element.android.libraries.matrix.impl.timeline.runWithTimelineListenerRegistered
|
import io.element.android.libraries.matrix.impl.timeline.runWithTimelineListenerRegistered
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
@ -49,10 +48,7 @@ class RoomContentForwarder(
|
||||||
val content = (messageLikeContent.kind as? MsgLikeKind.Message)?.content
|
val content = (messageLikeContent.kind as? MsgLikeKind.Message)?.content
|
||||||
?: throw ForwardEventException(toRoomIds)
|
?: throw ForwardEventException(toRoomIds)
|
||||||
|
|
||||||
val targetSlidingSyncRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
|
val targetRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
|
||||||
val targetRooms = targetSlidingSyncRooms.map { slidingSyncRoom ->
|
|
||||||
slidingSyncRoom.use { it.fullRoomWithTimeline(null) }
|
|
||||||
}
|
|
||||||
val failedForwardingTo = mutableSetOf<RoomId>()
|
val failedForwardingTo = mutableSetOf<RoomId>()
|
||||||
targetRooms.parallelMap { room ->
|
targetRooms.parallelMap { room ->
|
||||||
room.use { targetRoom ->
|
room.use { targetRoom ->
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
||||||
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
|
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
|
||||||
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
|
|
||||||
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
|
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -28,10 +27,12 @@ import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.Client
|
import org.matrix.rustcomponents.sdk.DateDividerMode
|
||||||
import org.matrix.rustcomponents.sdk.Membership
|
import org.matrix.rustcomponents.sdk.Membership
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
import org.matrix.rustcomponents.sdk.TimelineConfiguration
|
||||||
|
import org.matrix.rustcomponents.sdk.TimelineFilter
|
||||||
|
import org.matrix.rustcomponents.sdk.TimelineFocus
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
||||||
|
|
@ -39,7 +40,6 @@ import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
||||||
class RustRoomFactory(
|
class RustRoomFactory(
|
||||||
private val sessionId: SessionId,
|
private val sessionId: SessionId,
|
||||||
private val deviceId: DeviceId,
|
private val deviceId: DeviceId,
|
||||||
private val innerClient: Client,
|
|
||||||
private val notificationSettingsService: NotificationSettingsService,
|
private val notificationSettingsService: NotificationSettingsService,
|
||||||
private val sessionCoroutineScope: CoroutineScope,
|
private val sessionCoroutineScope: CoroutineScope,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
|
@ -79,16 +79,11 @@ class RustRoomFactory(
|
||||||
Timber.d("Room factory is destroyed, returning null for $roomId")
|
Timber.d("Room factory is destroyed, returning null for $roomId")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
val roomListItem = awaitRoomListItem(roomId) ?: return@withContext null
|
val room = awaitRoomInRoomList(roomId) ?: return@withContext null
|
||||||
getBaseRoom(roomListItem)
|
getBaseRoom(room)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getBaseRoom(roomListItem: RoomListItem): RustBaseRoom? {
|
|
||||||
val sdkRoom = innerClient.getRoom(roomListItem.id()) ?: return null
|
|
||||||
return getBaseRoom(sdkRoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getBaseRoom(sdkRoom: Room): RustBaseRoom {
|
private suspend fun getBaseRoom(sdkRoom: Room): RustBaseRoom {
|
||||||
val initialRoomInfo = sdkRoom.roomInfo()
|
val initialRoomInfo = sdkRoom.roomInfo()
|
||||||
return RustBaseRoom(
|
return RustBaseRoom(
|
||||||
|
|
@ -110,18 +105,27 @@ class RustRoomFactory(
|
||||||
Timber.d("Room factory is destroyed, returning null for $roomId")
|
Timber.d("Room factory is destroyed, returning null for $roomId")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
val roomListItem = awaitRoomListItem(roomId) ?: return@withContext null
|
val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withContext null
|
||||||
|
|
||||||
if (roomListItem.membership() == Membership.JOINED) {
|
if (sdkRoom.membership() == Membership.JOINED) {
|
||||||
// Init the live timeline in the SDK from the RoomListItem
|
// Init the live timeline in the SDK from the Room
|
||||||
val sdkRoom = roomListItem.fullRoomWithTimeline(eventFilters)
|
val timeline = sdkRoom.timelineWithConfiguration(
|
||||||
|
TimelineConfiguration(
|
||||||
|
focus = TimelineFocus.Live,
|
||||||
|
filter = eventFilters?.let(TimelineFilter::EventTypeFilter) ?: TimelineFilter.All,
|
||||||
|
internalIdPrefix = "live",
|
||||||
|
dateDividerMode = DateDividerMode.DAILY,
|
||||||
|
trackReadReceipts = true,
|
||||||
|
reportUtds = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
GetRoomResult.Joined(
|
GetRoomResult.Joined(
|
||||||
JoinedRustRoom(
|
JoinedRustRoom(
|
||||||
baseRoom = getBaseRoom(sdkRoom),
|
baseRoom = getBaseRoom(sdkRoom),
|
||||||
notificationSettingsService = notificationSettingsService,
|
notificationSettingsService = notificationSettingsService,
|
||||||
roomContentForwarder = roomContentForwarder,
|
roomContentForwarder = roomContentForwarder,
|
||||||
liveInnerTimeline = sdkRoom.timeline(),
|
liveInnerTimeline = timeline,
|
||||||
coroutineDispatchers = dispatchers,
|
coroutineDispatchers = dispatchers,
|
||||||
systemClock = systemClock,
|
systemClock = systemClock,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
|
@ -129,7 +133,7 @@ class RustRoomFactory(
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val preview = try {
|
val preview = try {
|
||||||
roomListItem.previewRoom(via = emptyList())
|
sdkRoom.previewRoom(via = emptyList())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Failed to get room preview for $roomId")
|
Timber.e(e, "Failed to get room preview for $roomId")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
|
|
@ -138,7 +142,7 @@ class RustRoomFactory(
|
||||||
GetRoomResult.NotJoined(
|
GetRoomResult.NotJoined(
|
||||||
NotJoinedRustRoom(
|
NotJoinedRustRoom(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
localRoom = getBaseRoom(roomListItem),
|
localRoom = getBaseRoom(sdkRoom),
|
||||||
previewInfo = RoomPreviewInfoMapper.map(preview.info()),
|
previewInfo = RoomPreviewInfoMapper.map(preview.info()),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -147,22 +151,22 @@ class RustRoomFactory(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Rust room list item for a room, retrying after the room list is loaded if necessary.
|
* Get the Rust room for a room, retrying after the room list is loaded if necessary.
|
||||||
*/
|
*/
|
||||||
private suspend fun awaitRoomListItem(roomId: RoomId): RoomListItem? {
|
private suspend fun awaitRoomInRoomList(roomId: RoomId): Room? {
|
||||||
var roomListItem = innerRoomListService.roomOrNull(roomId.value)
|
var sdkRoom = innerRoomListService.roomOrNull(roomId.value)
|
||||||
if (roomListItem == null) {
|
if (sdkRoom == null) {
|
||||||
// ... otherwise, lets wait for the SS to load all rooms and check again.
|
// ... otherwise, lets wait for the SS to load all rooms and check again.
|
||||||
roomListService.allRooms.awaitLoaded()
|
roomListService.allRooms.awaitLoaded()
|
||||||
roomListItem = innerRoomListService.roomOrNull(roomId.value)
|
sdkRoom = innerRoomListService.roomOrNull(roomId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (roomListItem == null) {
|
if (sdkRoom == null) {
|
||||||
Timber.d("Room not found for $roomId")
|
Timber.d("Room not found for $roomId")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return roomListItem
|
return sdkRoom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
||||||
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
|
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
|
||||||
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
||||||
import org.matrix.rustcomponents.sdk.RoomListInterface
|
import org.matrix.rustcomponents.sdk.RoomListInterface
|
||||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
|
||||||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||||
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
|
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
|
||||||
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
|
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
|
||||||
|
|
@ -114,7 +114,7 @@ internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncI
|
||||||
)
|
)
|
||||||
}.buffer(Channel.UNLIMITED)
|
}.buffer(Channel.UNLIMITED)
|
||||||
|
|
||||||
internal fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? {
|
internal fun RoomListServiceInterface.roomOrNull(roomId: String): Room? {
|
||||||
return tryOrNull(
|
return tryOrNull(
|
||||||
onError = { Timber.e(it, "Failed finding room with id=$roomId.") }
|
onError = { Timber.e(it, "Failed finding room with id=$roomId.") }
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.roomlist
|
|
||||||
|
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
|
||||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
|
||||||
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
|
|
||||||
|
|
||||||
/** Returns a `Room` with an initialized timeline using the given [filter]. */
|
|
||||||
suspend fun RoomListItem.fullRoomWithTimeline(filter: TimelineEventTypeFilter? = null): Room {
|
|
||||||
if (!isTimelineInitialized()) {
|
|
||||||
initTimeline(filter, "live")
|
|
||||||
}
|
|
||||||
return fullRoom()
|
|
||||||
}
|
|
||||||
|
|
@ -10,16 +10,16 @@ package io.element.android.libraries.matrix.impl.roomlist
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import io.element.android.libraries.matrix.impl.room.RoomInfoMapper
|
import io.element.android.libraries.matrix.impl.room.RoomInfoMapper
|
||||||
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
|
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
|
||||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
|
|
||||||
class RoomSummaryFactory(
|
class RoomSummaryFactory(
|
||||||
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
|
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
|
||||||
private val roomInfoMapper: RoomInfoMapper = RoomInfoMapper(),
|
private val roomInfoMapper: RoomInfoMapper = RoomInfoMapper(),
|
||||||
) {
|
) {
|
||||||
suspend fun create(roomListItem: RoomListItem): RoomSummary {
|
suspend fun create(room: Room): RoomSummary {
|
||||||
val roomInfo = roomListItem.roomInfo().let(roomInfoMapper::map)
|
val roomInfo = room.roomInfo().let(roomInfoMapper::map)
|
||||||
val latestRoomMessage = roomListItem.latestEvent().use { event ->
|
val latestRoomMessage = room.latestEvent().use { event ->
|
||||||
roomMessageFactory.create(event)
|
roomMessageFactory.create(event)
|
||||||
}
|
}
|
||||||
return RoomSummary(
|
return RoomSummary(
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
||||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
|
||||||
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
|
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -95,20 +95,16 @@ class RoomSummaryListProcessor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildSummaryForRoomListEntry(entry: RoomListItem): RoomSummary {
|
private suspend fun buildSummaryForRoomListEntry(entry: Room): RoomSummary {
|
||||||
return buildRoomSummaryForRoomListItem(entry)
|
return entry.use { roomSummaryDetailsFactory.create(room = it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildRoomSummaryForIdentifier(identifier: String): RoomSummary? {
|
private suspend fun buildRoomSummaryForIdentifier(identifier: String): RoomSummary? {
|
||||||
return roomListService.roomOrNull(identifier)?.use { roomListItem ->
|
return roomListService.roomOrNull(identifier)?.let { room ->
|
||||||
buildRoomSummaryForRoomListItem(roomListItem)
|
buildSummaryForRoomListEntry(room)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildRoomSummaryForRoomListItem(roomListItem: RoomListItem): RoomSummary {
|
|
||||||
return roomSummaryDetailsFactory.create(roomListItem = roomListItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(coroutineContext) {
|
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(coroutineContext) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val current = roomSummaries.replayCache.lastOrNull()
|
val current = roomSummaries.replayCache.lastOrNull()
|
||||||
|
|
|
||||||
|
|
@ -53,4 +53,5 @@ fun TracingConfiguration.map(): org.matrix.rustcomponents.sdk.TracingConfigurati
|
||||||
extraTargets = extraTargets,
|
extraTargets = extraTargets,
|
||||||
traceLogPacks = traceLogPacks.map(),
|
traceLogPacks = traceLogPacks.map(),
|
||||||
writeToFiles = writesToFilesConfiguration.toTracingFileConfiguration(),
|
writeToFiles = writesToFilesConfiguration.toTracingFileConfiguration(),
|
||||||
|
sentryDsn = null,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ package io.element.android.libraries.matrix.impl.widget
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||||
import io.element.android.libraries.matrix.impl.room.JoinedRustRoom
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -25,7 +24,6 @@ import kotlin.coroutines.coroutineContext
|
||||||
class RustWidgetDriver(
|
class RustWidgetDriver(
|
||||||
widgetSettings: MatrixWidgetSettings,
|
widgetSettings: MatrixWidgetSettings,
|
||||||
private val room: Room,
|
private val room: Room,
|
||||||
private val joinedRustRoom: JoinedRustRoom,
|
|
||||||
private val widgetCapabilitiesProvider: WidgetCapabilitiesProvider,
|
private val widgetCapabilitiesProvider: WidgetCapabilitiesProvider,
|
||||||
) : MatrixWidgetDriver {
|
) : MatrixWidgetDriver {
|
||||||
// It's important to have extra capacity here to make sure we don't drop any messages
|
// It's important to have extra capacity here to make sure we don't drop any messages
|
||||||
|
|
@ -71,6 +69,5 @@ class RustWidgetDriver(
|
||||||
override fun close() {
|
override fun close() {
|
||||||
receiveMessageJob?.cancel()
|
receiveMessageJob?.cancel()
|
||||||
driverAndHandle.driver.close()
|
driverAndHandle.driver.close()
|
||||||
joinedRustRoom.destroy()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import org.matrix.rustcomponents.sdk.RoomDirectorySearch
|
||||||
import org.matrix.rustcomponents.sdk.Session
|
import org.matrix.rustcomponents.sdk.Session
|
||||||
import org.matrix.rustcomponents.sdk.SyncServiceBuilder
|
import org.matrix.rustcomponents.sdk.SyncServiceBuilder
|
||||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||||
|
import org.matrix.rustcomponents.sdk.UnableToDecryptDelegate
|
||||||
|
|
||||||
class FakeRustClient(
|
class FakeRustClient(
|
||||||
private val userId: String = A_USER_ID.value,
|
private val userId: String = A_USER_ID.value,
|
||||||
|
|
@ -34,6 +35,7 @@ class FakeRustClient(
|
||||||
private val encryption: Encryption = FakeRustEncryption(),
|
private val encryption: Encryption = FakeRustEncryption(),
|
||||||
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 closeResult: () -> Unit = {},
|
private val closeResult: () -> Unit = {},
|
||||||
) : Client(NoPointer) {
|
) : Client(NoPointer) {
|
||||||
override fun userId(): String = userId
|
override fun userId(): String = userId
|
||||||
|
|
@ -58,5 +60,6 @@ class FakeRustClient(
|
||||||
|
|
||||||
override suspend fun deletePusher(identifiers: PusherIdentifiers) = Unit
|
override suspend fun deletePusher(identifiers: PusherIdentifiers) = Unit
|
||||||
override suspend fun clearCaches() = simulateLongTask { clearCachesResult() }
|
override suspend fun clearCaches() = simulateLongTask { clearCachesResult() }
|
||||||
|
override suspend fun setUtdDelegate(utdDelegate: UnableToDecryptDelegate) = withUtdHook(utdDelegate)
|
||||||
override fun close() = closeResult()
|
override fun close() = closeResult()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,6 @@ class FakeRustClientBuilder : ClientBuilder(NoPointer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun build(): Client {
|
override suspend fun build(): Client {
|
||||||
return FakeRustClient()
|
return FakeRustClient(withUtdHook = {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,15 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||||
|
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
|
||||||
import org.matrix.rustcomponents.sdk.NoPointer
|
import org.matrix.rustcomponents.sdk.NoPointer
|
||||||
import org.matrix.rustcomponents.sdk.NotificationClient
|
import org.matrix.rustcomponents.sdk.NotificationClient
|
||||||
import org.matrix.rustcomponents.sdk.NotificationItem
|
import org.matrix.rustcomponents.sdk.NotificationItem
|
||||||
|
import org.matrix.rustcomponents.sdk.NotificationItemsRequest
|
||||||
|
|
||||||
class FakeRustNotificationClient(
|
class FakeRustNotificationClient(
|
||||||
var notificationItemResult: NotificationItem? = null
|
var notificationItemResult: Map<String, NotificationItem> = emptyMap(),
|
||||||
) : NotificationClient(NoPointer) {
|
) : NotificationClient(NoPointer) {
|
||||||
override suspend fun getNotification(roomId: String, eventId: String): NotificationItem? = simulateLongTask {
|
override suspend fun getNotifications(requests: List<NotificationItemsRequest>): Map<String, NotificationItem> {
|
||||||
notificationItemResult
|
return notificationItemResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,13 @@
|
||||||
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
||||||
import org.matrix.rustcomponents.sdk.NoPointer
|
import org.matrix.rustcomponents.sdk.NoPointer
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||||
import org.matrix.rustcomponents.sdk.RoomMembersIterator
|
import org.matrix.rustcomponents.sdk.RoomMembersIterator
|
||||||
|
|
||||||
class FakeRustRoom(
|
class FakeRustRoom(
|
||||||
|
|
@ -19,6 +22,8 @@ class FakeRustRoom(
|
||||||
private val getMembers: () -> RoomMembersIterator = { lambdaError() },
|
private val getMembers: () -> RoomMembersIterator = { lambdaError() },
|
||||||
private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() },
|
private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() },
|
||||||
private val leaveLambda: () -> Unit = { lambdaError() },
|
private val leaveLambda: () -> Unit = { lambdaError() },
|
||||||
|
private val latestEventLambda: () -> EventTimelineItem? = { lambdaError() },
|
||||||
|
private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value),
|
||||||
) : Room(NoPointer) {
|
) : Room(NoPointer) {
|
||||||
override fun id(): String {
|
override fun id(): String {
|
||||||
return roomId.value
|
return roomId.value
|
||||||
|
|
@ -36,6 +41,14 @@ class FakeRustRoom(
|
||||||
leaveLambda()
|
leaveLambda()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun roomInfo(): RoomInfo {
|
||||||
|
return roomInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun latestEvent(): EventTimelineItem? {
|
||||||
|
return latestEventLambda()
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
// No-op
|
// No-op
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo
|
|
||||||
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
|
||||||
import org.matrix.rustcomponents.sdk.NoPointer
|
|
||||||
import org.matrix.rustcomponents.sdk.RoomInfo
|
|
||||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
|
||||||
|
|
||||||
class FakeRustRoomListItem(
|
|
||||||
private val roomId: RoomId,
|
|
||||||
private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value),
|
|
||||||
private val latestEvent: EventTimelineItem? = null,
|
|
||||||
) : RoomListItem(NoPointer) {
|
|
||||||
override fun id(): String {
|
|
||||||
return roomId.value
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun roomInfo(): RoomInfo {
|
|
||||||
return roomInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun latestEvent(): EventTimelineItem? {
|
|
||||||
return latestEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,10 +10,8 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||||
import org.matrix.rustcomponents.sdk.NoPointer
|
import org.matrix.rustcomponents.sdk.NoPointer
|
||||||
import org.matrix.rustcomponents.sdk.SyncService
|
import org.matrix.rustcomponents.sdk.SyncService
|
||||||
import org.matrix.rustcomponents.sdk.SyncServiceBuilder
|
import org.matrix.rustcomponents.sdk.SyncServiceBuilder
|
||||||
import org.matrix.rustcomponents.sdk.UnableToDecryptDelegate
|
|
||||||
|
|
||||||
class FakeRustSyncServiceBuilder : SyncServiceBuilder(NoPointer) {
|
class FakeRustSyncServiceBuilder : SyncServiceBuilder(NoPointer) {
|
||||||
override suspend fun withUtdHook(delegate: UnableToDecryptDelegate): SyncServiceBuilder = this
|
|
||||||
override fun withOfflineMode(): SyncServiceBuilder = this
|
override fun withOfflineMode(): SyncServiceBuilder = this
|
||||||
override suspend fun finish(): SyncService = FakeRustSyncService()
|
override suspend fun finish(): SyncService = FakeRustSyncService()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustNotificat
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
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_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||||
|
|
@ -28,12 +29,12 @@ class RustNotificationServiceTest {
|
||||||
@Test
|
@Test
|
||||||
fun test() = runTest {
|
fun test() = runTest {
|
||||||
val notificationClient = FakeRustNotificationClient(
|
val notificationClient = FakeRustNotificationClient(
|
||||||
notificationItemResult = aRustNotificationItem(),
|
notificationItemResult = mapOf(AN_EVENT_ID.value to aRustNotificationItem()),
|
||||||
)
|
)
|
||||||
val sut = createRustNotificationService(
|
val sut = createRustNotificationService(
|
||||||
notificationClient = notificationClient,
|
notificationClient = notificationClient,
|
||||||
)
|
)
|
||||||
val result = sut.getNotification(A_ROOM_ID, AN_EVENT_ID).getOrThrow()!!
|
val result = sut.getNotifications(mapOf(A_ROOM_ID to listOf(AN_EVENT_ID))).getOrThrow()[AN_EVENT_ID]!!
|
||||||
assertThat(result.isEncrypted).isTrue()
|
assertThat(result.isEncrypted).isTrue()
|
||||||
assertThat(result.content).isEqualTo(
|
assertThat(result.content).isEqualTo(
|
||||||
NotificationContent.MessageLike.RoomMessage(
|
NotificationContent.MessageLike.RoomMessage(
|
||||||
|
|
@ -51,6 +52,7 @@ class RustNotificationServiceTest {
|
||||||
clock: SystemClock = FakeSystemClock(),
|
clock: SystemClock = FakeSystemClock(),
|
||||||
) =
|
) =
|
||||||
RustNotificationService(
|
RustNotificationService(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
notificationClient = notificationClient,
|
notificationClient = notificationClient,
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
clock = clock,
|
clock = clock,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.room
|
package io.element.android.libraries.matrix.impl.room
|
||||||
|
|
||||||
|
import app.cash.turbine.TurbineTestContext
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
|
@ -19,7 +20,6 @@ import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
|
|
@ -30,10 +30,7 @@ import org.junit.Test
|
||||||
class RustBaseRoomTest {
|
class RustBaseRoomTest {
|
||||||
@Test
|
@Test
|
||||||
fun `RustBaseRoom should cancel the room coroutine scope when it is destroyed`() = runTest {
|
fun `RustBaseRoom should cancel the room coroutine scope when it is destroyed`() = runTest {
|
||||||
val rustBaseRoom = createRustBaseRoom(
|
val rustBaseRoom = createRustBaseRoom()
|
||||||
// Not using backgroundScope here, but the test scope
|
|
||||||
sessionCoroutineScope = this
|
|
||||||
)
|
|
||||||
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isTrue()
|
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isTrue()
|
||||||
rustBaseRoom.destroy()
|
rustBaseRoom.destroy()
|
||||||
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isFalse()
|
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isFalse()
|
||||||
|
|
@ -43,7 +40,6 @@ class RustBaseRoomTest {
|
||||||
fun `when currentUserMembership=JOINED and user leave room succeed then roomMembershipObserver emits change as LEFT`() = runTest {
|
fun `when currentUserMembership=JOINED and user leave room succeed then roomMembershipObserver emits change as LEFT`() = runTest {
|
||||||
val roomMembershipObserver = RoomMembershipObserver()
|
val roomMembershipObserver = RoomMembershipObserver()
|
||||||
val rustBaseRoom = createRustBaseRoom(
|
val rustBaseRoom = createRustBaseRoom(
|
||||||
sessionCoroutineScope = this,
|
|
||||||
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.JOINED),
|
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.JOINED),
|
||||||
innerRoom = FakeRustRoom(
|
innerRoom = FakeRustRoom(
|
||||||
leaveLambda = {
|
leaveLambda = {
|
||||||
|
|
@ -52,23 +48,18 @@ class RustBaseRoomTest {
|
||||||
),
|
),
|
||||||
roomMembershipObserver = roomMembershipObserver,
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
)
|
)
|
||||||
val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1)
|
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
|
||||||
rustBaseRoom.leave()
|
|
||||||
shared.test {
|
|
||||||
val membershipUpdate = awaitItem()
|
val membershipUpdate = awaitItem()
|
||||||
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
|
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
|
||||||
assertThat(membershipUpdate.isUserInRoom).isFalse()
|
assertThat(membershipUpdate.isUserInRoom).isFalse()
|
||||||
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.LEFT)
|
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.LEFT)
|
||||||
ensureAllEventsConsumed()
|
|
||||||
}
|
}
|
||||||
rustBaseRoom.destroy()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when currentUserMembership=KNOCKED and user leave room succeed then roomMembershipObserver emits change as KNOCK_RETRACTED`() = runTest {
|
fun `when currentUserMembership=KNOCKED and user leave room succeed then roomMembershipObserver emits change as KNOCK_RETRACTED`() = runTest {
|
||||||
val roomMembershipObserver = RoomMembershipObserver()
|
val roomMembershipObserver = RoomMembershipObserver()
|
||||||
val rustBaseRoom = createRustBaseRoom(
|
val rustBaseRoom = createRustBaseRoom(
|
||||||
sessionCoroutineScope = this,
|
|
||||||
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.KNOCKED),
|
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.KNOCKED),
|
||||||
innerRoom = FakeRustRoom(
|
innerRoom = FakeRustRoom(
|
||||||
leaveLambda = {
|
leaveLambda = {
|
||||||
|
|
@ -77,23 +68,18 @@ class RustBaseRoomTest {
|
||||||
),
|
),
|
||||||
roomMembershipObserver = roomMembershipObserver,
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
)
|
)
|
||||||
val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1)
|
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
|
||||||
rustBaseRoom.leave()
|
|
||||||
shared.test {
|
|
||||||
val membershipUpdate = awaitItem()
|
val membershipUpdate = awaitItem()
|
||||||
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
|
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
|
||||||
assertThat(membershipUpdate.isUserInRoom).isFalse()
|
assertThat(membershipUpdate.isUserInRoom).isFalse()
|
||||||
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.KNOCK_RETRACTED)
|
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.KNOCK_RETRACTED)
|
||||||
ensureAllEventsConsumed()
|
|
||||||
}
|
}
|
||||||
rustBaseRoom.destroy()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when currentUserMembership=INVITED and user leave room succeed then roomMembershipObserver emits change as INVITATION_REJECTED`() = runTest {
|
fun `when currentUserMembership=INVITED and user leave room succeed then roomMembershipObserver emits change as INVITATION_REJECTED`() = runTest {
|
||||||
val roomMembershipObserver = RoomMembershipObserver()
|
val roomMembershipObserver = RoomMembershipObserver()
|
||||||
val rustBaseRoom = createRustBaseRoom(
|
val rustBaseRoom = createRustBaseRoom(
|
||||||
sessionCoroutineScope = this,
|
|
||||||
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED),
|
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED),
|
||||||
innerRoom = FakeRustRoom(
|
innerRoom = FakeRustRoom(
|
||||||
leaveLambda = {
|
leaveLambda = {
|
||||||
|
|
@ -102,39 +88,44 @@ class RustBaseRoomTest {
|
||||||
),
|
),
|
||||||
roomMembershipObserver = roomMembershipObserver,
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
)
|
)
|
||||||
val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1)
|
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
|
||||||
rustBaseRoom.leave()
|
|
||||||
shared.test {
|
|
||||||
val membershipUpdate = awaitItem()
|
val membershipUpdate = awaitItem()
|
||||||
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
|
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
|
||||||
assertThat(membershipUpdate.isUserInRoom).isFalse()
|
assertThat(membershipUpdate.isUserInRoom).isFalse()
|
||||||
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.INVITATION_REJECTED)
|
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.INVITATION_REJECTED)
|
||||||
ensureAllEventsConsumed()
|
|
||||||
}
|
}
|
||||||
rustBaseRoom.destroy()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when user leave room fails then roomMembershipObserver emits nothing`() = runTest {
|
fun `when user leave room fails then roomMembershipObserver emits nothing`() = runTest {
|
||||||
val roomMembershipObserver = RoomMembershipObserver()
|
val roomMembershipObserver = RoomMembershipObserver()
|
||||||
val rustBaseRoom = createRustBaseRoom(
|
val rustBaseRoom = createRustBaseRoom(
|
||||||
sessionCoroutineScope = this,
|
|
||||||
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED),
|
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED),
|
||||||
innerRoom = FakeRustRoom(
|
innerRoom = FakeRustRoom(
|
||||||
leaveLambda = { error("Leave failed") }
|
leaveLambda = { error("Leave failed") }
|
||||||
),
|
),
|
||||||
roomMembershipObserver = roomMembershipObserver,
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
)
|
)
|
||||||
|
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
|
||||||
|
// No emit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun TestScope.leaveRoomAndObserveMembershipChange(
|
||||||
|
roomMembershipObserver: RoomMembershipObserver,
|
||||||
|
rustBaseRoom: RustBaseRoom,
|
||||||
|
validate: suspend TurbineTestContext<RoomMembershipObserver.RoomMembershipUpdate>.() -> Unit
|
||||||
|
) {
|
||||||
val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1)
|
val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1)
|
||||||
rustBaseRoom.leave()
|
rustBaseRoom.leave()
|
||||||
shared.test {
|
shared.test {
|
||||||
|
validate()
|
||||||
ensureAllEventsConsumed()
|
ensureAllEventsConsumed()
|
||||||
}
|
}
|
||||||
rustBaseRoom.destroy()
|
rustBaseRoom.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createRustBaseRoom(
|
private fun TestScope.createRustBaseRoom(
|
||||||
sessionCoroutineScope: CoroutineScope,
|
|
||||||
initialRoomInfo: RoomInfo = aRoomInfo(),
|
initialRoomInfo: RoomInfo = aRoomInfo(),
|
||||||
innerRoom: FakeRustRoom = FakeRustRoom(),
|
innerRoom: FakeRustRoom = FakeRustRoom(),
|
||||||
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
||||||
|
|
@ -150,7 +141,8 @@ class RustBaseRoomTest {
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
),
|
),
|
||||||
roomMembershipObserver = roomMembershipObserver,
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
// Not using backgroundScope here, but the test scope
|
||||||
|
sessionCoroutineScope = this,
|
||||||
roomInfoMapper = RoomInfoMapper(),
|
roomInfoMapper = RoomInfoMapper(),
|
||||||
initialRoomInfo = initialRoomInfo,
|
initialRoomInfo = initialRoomInfo,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomDescription
|
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomDescription
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomDirectorySearch
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomDirectorySearch
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -31,7 +30,6 @@ class RustBaseRoomDirectoryListTest {
|
||||||
val mapper = RoomDescriptionMapper()
|
val mapper = RoomDescriptionMapper()
|
||||||
val sut = createRustRoomDirectoryList(
|
val sut = createRustRoomDirectoryList(
|
||||||
roomDirectorySearch = roomDirectorySearch,
|
roomDirectorySearch = roomDirectorySearch,
|
||||||
scope = backgroundScope,
|
|
||||||
)
|
)
|
||||||
// Let the mxCallback be ready
|
// Let the mxCallback be ready
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
@ -81,10 +79,9 @@ class RustBaseRoomDirectoryListTest {
|
||||||
|
|
||||||
private fun TestScope.createRustRoomDirectoryList(
|
private fun TestScope.createRustRoomDirectoryList(
|
||||||
roomDirectorySearch: RoomDirectorySearch = FakeRustRoomDirectorySearch(),
|
roomDirectorySearch: RoomDirectorySearch = FakeRustRoomDirectorySearch(),
|
||||||
scope: CoroutineScope,
|
|
||||||
) = RustRoomDirectoryList(
|
) = RustRoomDirectoryList(
|
||||||
inner = roomDirectorySearch,
|
inner = roomDirectorySearch,
|
||||||
coroutineScope = scope,
|
coroutineScope = backgroundScope,
|
||||||
coroutineContext = StandardTestDispatcher(testScheduler),
|
coroutineContext = StandardTestDispatcher(testScheduler),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@
|
||||||
package io.element.android.libraries.matrix.impl.roomlist
|
package io.element.android.libraries.matrix.impl.roomlist
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListItem
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
|
|
@ -30,7 +31,7 @@ class RoomSummaryListProcessorTest {
|
||||||
summaries.value = listOf(aRoomSummary())
|
summaries.value = listOf(aRoomSummary())
|
||||||
val processor = createProcessor()
|
val processor = createProcessor()
|
||||||
|
|
||||||
val newEntry = FakeRustRoomListItem(A_ROOM_ID_2)
|
val newEntry = aRustRoom(A_ROOM_ID_2)
|
||||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(newEntry, newEntry, newEntry))))
|
processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(newEntry, newEntry, newEntry))))
|
||||||
|
|
||||||
assertThat(summaries.value.count()).isEqualTo(4)
|
assertThat(summaries.value.count()).isEqualTo(4)
|
||||||
|
|
@ -41,7 +42,7 @@ class RoomSummaryListProcessorTest {
|
||||||
fun `PushBack adds a new entry at the end of the list`() = runTest {
|
fun `PushBack adds a new entry at the end of the list`() = runTest {
|
||||||
summaries.value = listOf(aRoomSummary())
|
summaries.value = listOf(aRoomSummary())
|
||||||
val processor = createProcessor()
|
val processor = createProcessor()
|
||||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRustRoomListItem(A_ROOM_ID_2))))
|
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(aRustRoom(A_ROOM_ID_2))))
|
||||||
|
|
||||||
assertThat(summaries.value.count()).isEqualTo(2)
|
assertThat(summaries.value.count()).isEqualTo(2)
|
||||||
assertThat(summaries.value.last().roomId).isEqualTo(A_ROOM_ID_2)
|
assertThat(summaries.value.last().roomId).isEqualTo(A_ROOM_ID_2)
|
||||||
|
|
@ -51,7 +52,7 @@ class RoomSummaryListProcessorTest {
|
||||||
fun `PushFront inserts a new entry at the start of the list`() = runTest {
|
fun `PushFront inserts a new entry at the start of the list`() = runTest {
|
||||||
summaries.value = listOf(aRoomSummary())
|
summaries.value = listOf(aRoomSummary())
|
||||||
val processor = createProcessor()
|
val processor = createProcessor()
|
||||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRustRoomListItem(A_ROOM_ID_2))))
|
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(aRustRoom(A_ROOM_ID_2))))
|
||||||
|
|
||||||
assertThat(summaries.value.count()).isEqualTo(2)
|
assertThat(summaries.value.count()).isEqualTo(2)
|
||||||
assertThat(summaries.value.first().roomId).isEqualTo(A_ROOM_ID_2)
|
assertThat(summaries.value.first().roomId).isEqualTo(A_ROOM_ID_2)
|
||||||
|
|
@ -63,7 +64,7 @@ class RoomSummaryListProcessorTest {
|
||||||
val processor = createProcessor()
|
val processor = createProcessor()
|
||||||
val index = 0
|
val index = 0
|
||||||
|
|
||||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), FakeRustRoomListItem(A_ROOM_ID_2))))
|
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), aRustRoom(A_ROOM_ID_2))))
|
||||||
|
|
||||||
assertThat(summaries.value.count()).isEqualTo(1)
|
assertThat(summaries.value.count()).isEqualTo(1)
|
||||||
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2)
|
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2)
|
||||||
|
|
@ -75,7 +76,7 @@ class RoomSummaryListProcessorTest {
|
||||||
val processor = createProcessor()
|
val processor = createProcessor()
|
||||||
val index = 0
|
val index = 0
|
||||||
|
|
||||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), FakeRustRoomListItem(A_ROOM_ID_2))))
|
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), aRustRoom(A_ROOM_ID_2))))
|
||||||
|
|
||||||
assertThat(summaries.value.count()).isEqualTo(2)
|
assertThat(summaries.value.count()).isEqualTo(2)
|
||||||
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2)
|
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2)
|
||||||
|
|
@ -163,12 +164,17 @@ class RoomSummaryListProcessorTest {
|
||||||
val processor = createProcessor()
|
val processor = createProcessor()
|
||||||
val index = 0
|
val index = 0
|
||||||
|
|
||||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Reset(listOf(FakeRustRoomListItem(A_ROOM_ID_3)))))
|
processor.postUpdate(listOf(RoomListEntriesUpdate.Reset(listOf(aRustRoom(A_ROOM_ID_3)))))
|
||||||
|
|
||||||
assertThat(summaries.value.count()).isEqualTo(1)
|
assertThat(summaries.value.count()).isEqualTo(1)
|
||||||
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_3)
|
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun aRustRoom(roomId: RoomId = A_ROOM_ID) = FakeRustRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
latestEventLambda = { null },
|
||||||
|
)
|
||||||
|
|
||||||
private fun TestScope.createProcessor() = RoomSummaryListProcessor(
|
private fun TestScope.createProcessor() = RoomSummaryListProcessor(
|
||||||
summaries,
|
summaries,
|
||||||
FakeRustRoomListService(),
|
FakeRustRoomListService(),
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
|
||||||
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -28,7 +27,6 @@ class RustBaseRoomListServiceTest {
|
||||||
fun `syncIndicator should emit the expected values`() = runTest {
|
fun `syncIndicator should emit the expected values`() = runTest {
|
||||||
val roomListService = FakeRustRoomListService()
|
val roomListService = FakeRustRoomListService()
|
||||||
val sut = createRustRoomListService(
|
val sut = createRustRoomListService(
|
||||||
sessionCoroutineScope = backgroundScope,
|
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
)
|
)
|
||||||
// Give time for mxCallback to setup
|
// Give time for mxCallback to setup
|
||||||
|
|
@ -44,18 +42,17 @@ class RustBaseRoomListServiceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createRustRoomListService(
|
private fun TestScope.createRustRoomListService(
|
||||||
sessionCoroutineScope: CoroutineScope,
|
|
||||||
roomListService: RustRoomListService = FakeRustRoomListService(),
|
roomListService: RustRoomListService = FakeRustRoomListService(),
|
||||||
) = RustRoomListService(
|
) = RustRoomListService(
|
||||||
innerRoomListService = roomListService,
|
innerRoomListService = roomListService,
|
||||||
sessionDispatcher = StandardTestDispatcher(testScheduler),
|
sessionDispatcher = StandardTestDispatcher(testScheduler),
|
||||||
roomListFactory = RoomListFactory(
|
roomListFactory = RoomListFactory(
|
||||||
innerRoomListService = roomListService,
|
innerRoomListService = roomListService,
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = backgroundScope,
|
||||||
),
|
),
|
||||||
roomSyncSubscriber = RoomSyncSubscriber(
|
roomSyncSubscriber = RoomSyncSubscriber(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
),
|
),
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = backgroundScope,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineI
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -38,7 +37,6 @@ class TimelineItemsSubscriberTest {
|
||||||
MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
|
MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
|
||||||
val timeline = FakeRustTimeline()
|
val timeline = FakeRustTimeline()
|
||||||
val timelineItemsSubscriber = createTimelineItemsSubscriber(
|
val timelineItemsSubscriber = createTimelineItemsSubscriber(
|
||||||
coroutineScope = backgroundScope,
|
|
||||||
timeline = timeline,
|
timeline = timeline,
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
)
|
)
|
||||||
|
|
@ -59,7 +57,6 @@ class TimelineItemsSubscriberTest {
|
||||||
MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
|
MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
|
||||||
val timeline = FakeRustTimeline()
|
val timeline = FakeRustTimeline()
|
||||||
val timelineItemsSubscriber = createTimelineItemsSubscriber(
|
val timelineItemsSubscriber = createTimelineItemsSubscriber(
|
||||||
coroutineScope = backgroundScope,
|
|
||||||
timeline = timeline,
|
timeline = timeline,
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
)
|
)
|
||||||
|
|
@ -81,7 +78,6 @@ class TimelineItemsSubscriberTest {
|
||||||
val timeline = FakeRustTimeline()
|
val timeline = FakeRustTimeline()
|
||||||
val onNewSyncedEventRecorder = lambdaRecorder<Unit> { }
|
val onNewSyncedEventRecorder = lambdaRecorder<Unit> { }
|
||||||
val timelineItemsSubscriber = createTimelineItemsSubscriber(
|
val timelineItemsSubscriber = createTimelineItemsSubscriber(
|
||||||
coroutineScope = backgroundScope,
|
|
||||||
timeline = timeline,
|
timeline = timeline,
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
onNewSyncedEvent = onNewSyncedEventRecorder,
|
onNewSyncedEvent = onNewSyncedEventRecorder,
|
||||||
|
|
@ -109,9 +105,7 @@ class TimelineItemsSubscriberTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `multiple subscriptions does not have side effect`() = runTest {
|
fun `multiple subscriptions does not have side effect`() = runTest {
|
||||||
val timelineItemsSubscriber = createTimelineItemsSubscriber(
|
val timelineItemsSubscriber = createTimelineItemsSubscriber()
|
||||||
coroutineScope = backgroundScope,
|
|
||||||
)
|
|
||||||
timelineItemsSubscriber.subscribeIfNeeded()
|
timelineItemsSubscriber.subscribeIfNeeded()
|
||||||
timelineItemsSubscriber.subscribeIfNeeded()
|
timelineItemsSubscriber.subscribeIfNeeded()
|
||||||
timelineItemsSubscriber.unsubscribeIfNeeded()
|
timelineItemsSubscriber.unsubscribeIfNeeded()
|
||||||
|
|
@ -120,7 +114,6 @@ class TimelineItemsSubscriberTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createTimelineItemsSubscriber(
|
private fun TestScope.createTimelineItemsSubscriber(
|
||||||
coroutineScope: CoroutineScope,
|
|
||||||
timeline: Timeline = FakeRustTimeline(),
|
timeline: Timeline = FakeRustTimeline(),
|
||||||
timelineItems: MutableSharedFlow<List<MatrixTimelineItem>> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE),
|
timelineItems: MutableSharedFlow<List<MatrixTimelineItem>> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE),
|
||||||
initLatch: CompletableDeferred<Unit> = CompletableDeferred(),
|
initLatch: CompletableDeferred<Unit> = CompletableDeferred(),
|
||||||
|
|
@ -128,7 +121,7 @@ private fun TestScope.createTimelineItemsSubscriber(
|
||||||
onNewSyncedEvent: () -> Unit = { lambdaError() },
|
onNewSyncedEvent: () -> Unit = { lambdaError() },
|
||||||
): TimelineItemsSubscriber {
|
): TimelineItemsSubscriber {
|
||||||
return TimelineItemsSubscriber(
|
return TimelineItemsSubscriber(
|
||||||
timelineCoroutineScope = coroutineScope,
|
timelineCoroutineScope = backgroundScope,
|
||||||
dispatcher = StandardTestDispatcher(testScheduler),
|
dispatcher = StandardTestDispatcher(testScheduler),
|
||||||
timeline = timeline,
|
timeline = timeline,
|
||||||
timelineDiffProcessor = createMatrixTimelineDiffProcessor(timelineItems),
|
timelineDiffProcessor = createMatrixTimelineDiffProcessor(timelineItems),
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ const val A_REDACTION_REASON = "A redaction reason"
|
||||||
const val A_HOMESERVER_URL = "matrix.org"
|
const val A_HOMESERVER_URL = "matrix.org"
|
||||||
const val A_HOMESERVER_URL_2 = "matrix-client.org"
|
const val A_HOMESERVER_URL_2 = "matrix-client.org"
|
||||||
|
|
||||||
|
const val AN_ACCOUNT_PROVIDER = "matrix.org"
|
||||||
|
const val AN_ACCOUNT_PROVIDER_2 = "element.io"
|
||||||
|
const val AN_ACCOUNT_PROVIDER_3 = "other.io"
|
||||||
|
|
||||||
val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false)
|
val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false)
|
||||||
val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
|
val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
|
||||||
val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE
|
val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue