Merge branch 'develop' into feature/fga/join_space
This commit is contained in:
commit
c4308e9810
446 changed files with 5669 additions and 2617 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -62,6 +62,7 @@ captures/
|
|||
# Android Studio 3 in .gitignore file.
|
||||
.idea/caches
|
||||
.idea/copilot
|
||||
.idea/copilot.*
|
||||
.idea/inspectionProfiles
|
||||
# Shelved changes in the IDE
|
||||
.idea/shelf
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import extension.koverDependencies
|
|||
import extension.locales
|
||||
import extension.setupDependencyInjection
|
||||
import extension.setupKover
|
||||
import extension.testCommonDependencies
|
||||
import java.util.Locale
|
||||
|
||||
plugins {
|
||||
|
|
@ -290,15 +291,9 @@ dependencies {
|
|||
|
||||
implementation(libs.matrix.emojibase.bindings)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
|
||||
koverDependencies()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import extension.allFeaturesApi
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
|
|
@ -42,18 +43,12 @@ dependencies {
|
|||
|
||||
implementation(projects.features.ftue.api)
|
||||
implementation(projects.features.share.api)
|
||||
implementation(projects.features.viewfolder.api)
|
||||
|
||||
implementation(projects.services.apperror.impl)
|
||||
implementation(projects.services.appnavstate.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.features.login.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.oidc.test)
|
||||
|
|
@ -61,11 +56,8 @@ dependencies {
|
|||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.features.rageshake.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(libs.test.appyx.junit)
|
||||
testImplementation(libs.test.arch.core)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ import io.element.android.features.ftue.api.FtueEntryPoint
|
|||
import io.element.android.features.ftue.api.state.FtueService
|
||||
import io.element.android.features.ftue.api.state.FtueState
|
||||
import io.element.android.features.home.api.HomeEntryPoint
|
||||
import io.element.android.features.logout.api.LogoutEntryPoint
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||
|
|
@ -119,7 +118,6 @@ class LoggedInFlowNode(
|
|||
private val shareEntryPoint: ShareEntryPoint,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val sendingQueue: SendQueues,
|
||||
private val logoutEntryPoint: LogoutEntryPoint,
|
||||
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
|
||||
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
|
||||
private val sessionEnterpriseService: SessionEnterpriseService,
|
||||
|
|
@ -277,9 +275,6 @@ class LoggedInFlowNode(
|
|||
@Parcelize
|
||||
data class IncomingShare(val intent: Intent) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget
|
||||
}
|
||||
|
|
@ -324,10 +319,6 @@ class LoggedInFlowNode(
|
|||
override fun onReportBugClick() {
|
||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||
}
|
||||
|
||||
override fun onLogoutForNativeSlidingSyncMigrationNeeded() {
|
||||
backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded)
|
||||
}
|
||||
}
|
||||
homeEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
|
|
@ -454,8 +445,7 @@ class LoggedInFlowNode(
|
|||
.build()
|
||||
}
|
||||
NavTarget.Ftue -> {
|
||||
ftueEntryPoint.nodeBuilder(this, buildContext)
|
||||
.build()
|
||||
ftueEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
NavTarget.RoomDirectorySearch -> {
|
||||
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
||||
|
|
@ -486,17 +476,6 @@ class LoggedInFlowNode(
|
|||
.params(ShareEntryPoint.Params(intent = navTarget.intent))
|
||||
.build()
|
||||
}
|
||||
is NavTarget.LogoutForNativeSlidingSyncMigrationNeeded -> {
|
||||
val callback = object : LogoutEntryPoint.Callback {
|
||||
override fun onChangeRecoveryKeyClick() {
|
||||
backstack.push(NavTarget.SecureBackup())
|
||||
}
|
||||
}
|
||||
|
||||
logoutEntryPoint.nodeBuilder(this, buildContext)
|
||||
.callback(callback)
|
||||
.build()
|
||||
}
|
||||
is NavTarget.IncomingVerificationRequest -> {
|
||||
incomingVerificationEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(IncomingVerificationEntryPoint.Params(navTarget.data))
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import io.element.android.features.login.api.accesscontrol.AccountProviderAccess
|
|||
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||
import io.element.android.features.signedout.api.SignedOutEntryPoint
|
||||
import io.element.android.features.viewfolder.api.ViewFolderEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -47,13 +46,13 @@ import io.element.android.libraries.architecture.waitForChildAttached
|
|||
import io.element.android.libraries.core.uri.ensureProtocol
|
||||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -65,13 +64,12 @@ import timber.log.Timber
|
|||
class RootFlowNode(
|
||||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val sessionStore: SessionStore,
|
||||
private val accountProviderAccessControl: AccountProviderAccessControl,
|
||||
private val navStateFlowFactory: RootNavStateFlowFactory,
|
||||
private val matrixSessionCache: MatrixSessionCache,
|
||||
private val presenter: RootPresenter,
|
||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||
private val viewFolderEntryPoint: ViewFolderEntryPoint,
|
||||
private val signedOutEntryPoint: SignedOutEntryPoint,
|
||||
private val intentResolver: IntentResolver,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
|
|
@ -154,7 +152,7 @@ class RootFlowNode(
|
|||
onSuccess: (SessionId) -> Unit,
|
||||
onFailure: () -> Unit
|
||||
) {
|
||||
val latestSessionId = authenticationService.getLatestSessionId()
|
||||
val latestSessionId = sessionStore.getLatestSessionId()
|
||||
if (latestSessionId == null) {
|
||||
onFailure()
|
||||
return
|
||||
|
|
@ -200,11 +198,6 @@ class RootFlowNode(
|
|||
|
||||
@Parcelize
|
||||
data object BugReport : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class ViewLogs(
|
||||
val rootPath: String,
|
||||
) : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -244,31 +237,12 @@ class RootFlowNode(
|
|||
NavTarget.SplashScreen -> splashNode(buildContext)
|
||||
NavTarget.BugReport -> {
|
||||
val callback = object : BugReportEntryPoint.Callback {
|
||||
override fun onBugReportSent() {
|
||||
backstack.pop()
|
||||
}
|
||||
|
||||
override fun onViewLogs(basePath: String) {
|
||||
backstack.push(NavTarget.ViewLogs(rootPath = basePath))
|
||||
}
|
||||
}
|
||||
bugReportEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
.callback(callback)
|
||||
.build()
|
||||
}
|
||||
is NavTarget.ViewLogs -> {
|
||||
val callback = object : ViewFolderEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
val params = ViewFolderEntryPoint.Params(
|
||||
rootPath = navTarget.rootPath,
|
||||
)
|
||||
viewFolderEntryPoint
|
||||
bugReportEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
.params(params)
|
||||
.callback(callback)
|
||||
.build()
|
||||
}
|
||||
|
|
@ -294,7 +268,7 @@ class RootFlowNode(
|
|||
|
||||
private suspend fun onLoginLink(params: LoginParams) {
|
||||
// Is there a session already?
|
||||
val latestSessionId = authenticationService.getLatestSessionId()
|
||||
val latestSessionId = sessionStore.getLatestSessionId()
|
||||
if (latestSessionId == null) {
|
||||
// No session, open login
|
||||
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) {
|
||||
|
|
@ -311,7 +285,7 @@ class RootFlowNode(
|
|||
|
||||
private suspend fun onIncomingShare(intent: Intent) {
|
||||
// Is there a session already?
|
||||
val latestSessionId = authenticationService.getLatestSessionId()
|
||||
val latestSessionId = sessionStore.getLatestSessionId()
|
||||
if (latestSessionId == null) {
|
||||
// No session, open login
|
||||
switchToNotLoggedInFlow(null)
|
||||
|
|
@ -368,3 +342,5 @@ class RootFlowNode(
|
|||
.attachSession()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun SessionStore.getLatestSessionId() = getLatestSession()?.userId?.let(::SessionId)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ package io.element.android.appnav.di
|
|||
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
interface RoomComponentFactory {
|
||||
fun interface RoomComponentFactory {
|
||||
fun create(room: JoinedRoom): Any
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import com.bumble.appyx.core.state.SavedStateMap
|
|||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.appnav.di.MatrixSessionCache
|
||||
import io.element.android.features.preferences.api.CacheService
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
|
@ -28,7 +28,7 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFact
|
|||
*/
|
||||
@Inject
|
||||
class RootNavStateFlowFactory(
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val sessionStore: SessionStore,
|
||||
private val cacheService: CacheService,
|
||||
private val matrixSessionCache: MatrixSessionCache,
|
||||
private val imageLoaderHolder: ImageLoaderHolder,
|
||||
|
|
@ -39,7 +39,7 @@ class RootNavStateFlowFactory(
|
|||
fun create(savedStateMap: SavedStateMap?): Flow<RootNavState> {
|
||||
return combine(
|
||||
cacheIndexFlow(savedStateMap),
|
||||
authenticationService.loggedInStateFlow(),
|
||||
sessionStore.loggedInStateFlow(),
|
||||
) { cacheIndex, loggedInState ->
|
||||
RootNavState(
|
||||
cacheIndex = cacheIndex,
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Sair & Atualizar"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s já não suporta o protocolo antigo. Termina a sessão e volta a iniciar sessão para continuares a utilizar a aplicação."</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Seu homeserver não suporta mais o protocolo antigo. Termine sessão e volte a iniciar sessão para continuar a utilizar a aplicação."</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"O teu servidor já não permite o protocolo antigo. Termine sessão e volte a iniciá-la para continuar a utilizar a aplicação."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -501,22 +501,16 @@ class LoggedInPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - CheckSlidingSyncProxyAvailability forces the sliding sync migration under the right circumstances`() = runTest {
|
||||
// The migration will be forced if:
|
||||
// - The user is not using the native sliding sync
|
||||
// - The sliding sync proxy is no longer supported
|
||||
// - The native sliding sync is supported
|
||||
// The migration will be forced if the user is not using the native sliding sync
|
||||
val matrixClient = FakeMatrixClient(
|
||||
currentSlidingSyncVersionLambda = { Result.success(SlidingSyncVersion.Proxy) },
|
||||
availableSlidingSyncVersionsLambda = { Result.success(listOf(SlidingSyncVersion.Native)) },
|
||||
)
|
||||
createLoggedInPresenter(
|
||||
matrixClient = matrixClient,
|
||||
).test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
|
||||
|
||||
initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
|
||||
|
||||
assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ package io.element.android.features.analytics.api
|
|||
|
||||
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||
|
||||
interface AnalyticsEntryPoint : SimpleFeatureEntryPoint
|
||||
fun interface AnalyticsEntryPoint : SimpleFeatureEntryPoint
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -30,13 +31,7 @@ dependencies {
|
|||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.browser)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.mockk)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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.analytics.impl
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultAnalyticsEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Test
|
||||
fun `test node creation`() {
|
||||
val entryPoint = DefaultAnalyticsEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
AnalyticsOptInNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
AnalyticsOptInPresenter(
|
||||
buildMeta = aBuildMeta(),
|
||||
analyticsService = FakeAnalyticsService()
|
||||
)
|
||||
)
|
||||
}
|
||||
val result = entryPoint.createNode(parentNode, BuildContext.root(null))
|
||||
assertThat(result).isInstanceOf(AnalyticsOptInNode::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -22,8 +23,5 @@ dependencies {
|
|||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ interface ElementCallEntryPoint {
|
|||
* @param senderName The name of the sender of the event that started the call.
|
||||
* @param avatarUrl The avatar url of the room or DM.
|
||||
* @param timestamp The timestamp of the event that started the call.
|
||||
* @param expirationTimestamp The timestamp at which the call should stop ringing.
|
||||
* @param notificationChannelId The id of the notification channel to use for the call notification.
|
||||
* @param textContent The text content of the notification. If null the default content from the system will be used.
|
||||
*/
|
||||
|
|
@ -40,6 +41,7 @@ interface ElementCallEntryPoint {
|
|||
senderName: String?,
|
||||
avatarUrl: String?,
|
||||
timestamp: Long,
|
||||
expirationTimestamp: Long,
|
||||
notificationChannelId: String,
|
||||
textContent: String?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import extension.buildConfigFieldStr
|
||||
import extension.readLocalProperty
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
|
|
@ -87,12 +88,7 @@ dependencies {
|
|||
implementation(libs.element.call.embedded)
|
||||
api(projects.features.call.api)
|
||||
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.mockk)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.features.call.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
|
|
@ -100,7 +96,5 @@ dependencies {
|
|||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class DefaultElementCallEntryPoint(
|
|||
senderName: String?,
|
||||
avatarUrl: String?,
|
||||
timestamp: Long,
|
||||
expirationTimestamp: Long,
|
||||
notificationChannelId: String,
|
||||
textContent: String?,
|
||||
) {
|
||||
|
|
@ -55,6 +56,7 @@ class DefaultElementCallEntryPoint(
|
|||
senderName = senderName,
|
||||
avatarUrl = avatarUrl,
|
||||
timestamp = timestamp,
|
||||
expirationTimestamp = expirationTimestamp,
|
||||
notificationChannelId = notificationChannelId,
|
||||
textContent = textContent,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,4 +26,6 @@ data class CallNotificationData(
|
|||
val notificationChannelId: String,
|
||||
val timestamp: Long,
|
||||
val textContent: String?,
|
||||
// Expiration timestamp in millis since epoch
|
||||
val expirationTimestamp: Long,
|
||||
) : Parcelable
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ class RingingCallNotificationCreator(
|
|||
roomAvatarUrl: String?,
|
||||
notificationChannelId: String,
|
||||
timestamp: Long,
|
||||
expirationTimestamp: Long,
|
||||
textContent: String?,
|
||||
): Notification? {
|
||||
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
|
||||
|
|
@ -88,6 +89,7 @@ class RingingCallNotificationCreator(
|
|||
notificationChannelId = notificationChannelId,
|
||||
timestamp = timestamp,
|
||||
textContent = textContent,
|
||||
expirationTimestamp = expirationTimestamp,
|
||||
)
|
||||
|
||||
val declineIntent = PendingIntentCompat.getBroadcast(
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ internal fun IncomingCallScreenPreview() = ElementPreview {
|
|||
notificationChannelId = "incoming_call",
|
||||
timestamp = 0L,
|
||||
textContent = null,
|
||||
expirationTimestamp = 1000L,
|
||||
),
|
||||
onAnswer = {},
|
||||
onCancel = {},
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import io.element.android.libraries.push.api.notifications.ForegroundServiceType
|
|||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
|
||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -53,7 +54,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Manages the active call state.
|
||||
|
|
@ -98,6 +99,7 @@ class DefaultActiveCallManager(
|
|||
private val defaultCurrentCallService: DefaultCurrentCallService,
|
||||
private val appForegroundStateService: AppForegroundStateService,
|
||||
private val imageLoaderHolder: ImageLoaderHolder,
|
||||
private val systemClock: SystemClock,
|
||||
) : ActiveCallManager {
|
||||
private val tag = "DefaultActiveCallManager"
|
||||
private var timedOutCallJob: Job? = null
|
||||
|
|
@ -118,8 +120,20 @@ class DefaultActiveCallManager(
|
|||
|
||||
override suspend fun registerIncomingCall(notificationData: CallNotificationData) {
|
||||
mutex.withLock {
|
||||
val ringDuration =
|
||||
min(
|
||||
notificationData.expirationTimestamp - systemClock.epochMillis(),
|
||||
ElementCallConfig.RINGING_CALL_DURATION_SECONDS * 1000L
|
||||
)
|
||||
|
||||
if (ringDuration < 0) {
|
||||
// Should already have stopped ringing, ignore.
|
||||
Timber.tag(tag).d("Received timed-out incoming ringing call for room id: ${notificationData.roomId}, cancel ringing")
|
||||
return
|
||||
}
|
||||
|
||||
appForegroundStateService.updateHasRingingCall(true)
|
||||
Timber.tag(tag).d("Received incoming call for room id: ${notificationData.roomId}")
|
||||
Timber.tag(tag).d("Received incoming call for room id: ${notificationData.roomId}, ringDuration(ms): $ringDuration")
|
||||
if (activeCall.value != null) {
|
||||
displayMissedCallNotification(notificationData)
|
||||
Timber.tag(tag).w("Already have an active call, ignoring incoming call: $notificationData")
|
||||
|
|
@ -138,14 +152,14 @@ class DefaultActiveCallManager(
|
|||
showIncomingCallNotification(notificationData)
|
||||
|
||||
// Wait for the ringing call to time out
|
||||
delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds)
|
||||
delay(timeMillis = ringDuration)
|
||||
incomingCallTimedOut(displayMissedCallNotification = true)
|
||||
}
|
||||
|
||||
// Acquire a wake lock to keep the device awake during the incoming call, so we can process the room info data
|
||||
if (activeWakeLock?.isHeld == false) {
|
||||
Timber.tag(tag).d("Acquiring partial wakelock")
|
||||
activeWakeLock.acquire(ElementCallConfig.RINGING_CALL_DURATION_SECONDS * 1000L)
|
||||
activeWakeLock.acquire(ringDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -180,12 +194,22 @@ class DefaultActiveCallManager(
|
|||
}
|
||||
|
||||
override suspend fun hungUpCall(callType: CallType) = mutex.withLock {
|
||||
if (activeCall.value?.callType != callType) {
|
||||
Timber.tag(tag).d("Hung up call: $callType")
|
||||
val currentActiveCall = activeCall.value ?: run {
|
||||
Timber.tag(tag).w("No active call, ignoring hang up")
|
||||
return
|
||||
}
|
||||
if (currentActiveCall.callType != callType) {
|
||||
Timber.tag(tag).w("Call type $callType does not match the active call type, ignoring")
|
||||
return
|
||||
}
|
||||
|
||||
Timber.tag(tag).d("Hung up call: $callType")
|
||||
if (currentActiveCall.callState is CallState.Ringing) {
|
||||
// Decline the call
|
||||
val notificationData = currentActiveCall.callState.notificationData
|
||||
matrixClientProvider.getOrRestore(notificationData.sessionId).getOrNull()
|
||||
?.getRoom(notificationData.roomId)
|
||||
?.declineCall(notificationData.eventId)
|
||||
}
|
||||
|
||||
cancelIncomingCallNotification()
|
||||
if (activeWakeLock?.isHeld == true) {
|
||||
|
|
@ -226,6 +250,7 @@ class DefaultActiveCallManager(
|
|||
notificationChannelId = notificationData.notificationChannelId,
|
||||
timestamp = notificationData.timestamp,
|
||||
textContent = notificationData.textContent,
|
||||
expirationTimestamp = notificationData.expirationTimestamp,
|
||||
) ?: return
|
||||
runCatchingExceptions {
|
||||
notificationManagerCompat.notify(
|
||||
|
|
@ -256,6 +281,43 @@ class DefaultActiveCallManager(
|
|||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun observeRingingCall() {
|
||||
activeCall
|
||||
.filterNotNull()
|
||||
.filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall }
|
||||
.flatMapLatest { activeCall ->
|
||||
val callType = activeCall.callType as CallType.RoomCall
|
||||
val ringingInfo = activeCall.callState as CallState.Ringing
|
||||
val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run {
|
||||
Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall")
|
||||
return@flatMapLatest flowOf()
|
||||
}
|
||||
val room = client.getRoom(callType.roomId) ?: run {
|
||||
Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall")
|
||||
return@flatMapLatest flowOf()
|
||||
}
|
||||
|
||||
Timber.tag(tag).d("Found room for ringing call: ${room.roomId}")
|
||||
|
||||
// If we have declined from another phone we want to stop ringing.
|
||||
room.subscribeToCallDecline(ringingInfo.notificationData.eventId)
|
||||
.filter { decliner ->
|
||||
Timber.tag(tag).d("Call: $activeCall was declined by $decliner")
|
||||
// only want to listen if the call was declined from another of my sessions,
|
||||
// (we are ringing for an incoming call in a DM)
|
||||
decliner == client.sessionId
|
||||
}
|
||||
}
|
||||
.onEach { decliner ->
|
||||
Timber.tag(tag).d("Call: $activeCall was declined by user from another session")
|
||||
// Remove the active call and cancel the notification
|
||||
activeCall.value = null
|
||||
if (activeWakeLock?.isHeld == true) {
|
||||
Timber.tag(tag).d("Releasing partial wakelock after call declined from another session")
|
||||
activeWakeLock.release()
|
||||
}
|
||||
cancelIncomingCallNotification()
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
// This will observe ringing calls and ensure they're terminated if the room call is cancelled or if the user
|
||||
// has joined the call from another session.
|
||||
activeCall
|
||||
|
|
|
|||
|
|
@ -45,8 +45,14 @@ class DefaultCallWidgetProvider(
|
|||
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
|
||||
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
|
||||
|
||||
val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
|
||||
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = isEncrypted, direct = room.isDm())
|
||||
val roomInfo = room.info()
|
||||
val isEncrypted = roomInfo.isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
|
||||
val widgetSettings = callWidgetSettingsProvider.provide(
|
||||
baseUrl = baseUrl,
|
||||
encrypted = isEncrypted,
|
||||
direct = room.isDm(),
|
||||
hasActiveCall = roomInfo.hasRoomCall,
|
||||
)
|
||||
val callUrl = room.generateWidgetWebViewUrl(
|
||||
widgetSettings = widgetSettings,
|
||||
clientId = clientId,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class DefaultElementCallEntryPointTest {
|
|||
senderName = "senderName",
|
||||
avatarUrl = "avatarUrl",
|
||||
timestamp = 0,
|
||||
expirationTimestamp = 0,
|
||||
notificationChannelId = "notificationChannelId",
|
||||
textContent = "textContent",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ class RingingCallNotificationCreatorTest {
|
|||
roomAvatarUrl = "https://example.com/avatar.jpg",
|
||||
notificationChannelId = "channelId",
|
||||
timestamp = 0L,
|
||||
expirationTimestamp = 20L,
|
||||
textContent = "textContent",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,13 +22,16 @@ import io.element.android.features.call.test.aCallNotificationData
|
|||
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.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
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_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
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.aRoomInfo
|
||||
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
|
|
@ -36,8 +39,12 @@ import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolde
|
|||
import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler
|
||||
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
|
||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.plantTestTimber
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -164,6 +171,102 @@ class DefaultActiveCallManagerTest {
|
|||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Decline event - Hangup on a ringing call should send a decline event`() = runTest {
|
||||
setupShadowPowerManager()
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
|
||||
val room = mockk<JoinedRoom>(relaxed = true)
|
||||
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) })
|
||||
|
||||
val manager = createActiveCallManager(
|
||||
matrixClientProvider = clientProvider,
|
||||
notificationManagerCompat = notificationManagerCompat
|
||||
)
|
||||
|
||||
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
|
||||
manager.registerIncomingCall(notificationData)
|
||||
|
||||
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
||||
|
||||
coVerify {
|
||||
room.declineCall(notificationEventId = notificationData.eventId)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `Decline event - Declining from another session should stop ringing`() = runTest {
|
||||
setupShadowPowerManager()
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
|
||||
val room = FakeJoinedRoom()
|
||||
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) })
|
||||
|
||||
val manager = createActiveCallManager(
|
||||
matrixClientProvider = clientProvider,
|
||||
notificationManagerCompat = notificationManagerCompat
|
||||
)
|
||||
|
||||
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
|
||||
manager.registerIncomingCall(notificationData)
|
||||
|
||||
runCurrent()
|
||||
|
||||
// Simulate declined from other session
|
||||
room.baseRoom.givenDecliner(matrixClient.sessionId, notificationData.eventId)
|
||||
|
||||
runCurrent()
|
||||
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `Decline event - Should ignore decline for other notification events`() = runTest {
|
||||
plantTestTimber()
|
||||
setupShadowPowerManager()
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
|
||||
val room = FakeJoinedRoom()
|
||||
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) })
|
||||
|
||||
val manager = createActiveCallManager(
|
||||
matrixClientProvider = clientProvider,
|
||||
notificationManagerCompat = notificationManagerCompat
|
||||
)
|
||||
|
||||
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
|
||||
manager.registerIncomingCall(notificationData)
|
||||
|
||||
runCurrent()
|
||||
|
||||
// Simulate declined for another notification event
|
||||
room.baseRoom.givenDecliner(matrixClient.sessionId, AN_EVENT_ID_2)
|
||||
|
||||
runCurrent()
|
||||
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||
|
||||
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
|
||||
setupShadowPowerManager()
|
||||
|
|
@ -267,6 +370,83 @@ class DefaultActiveCallManagerTest {
|
|||
assertThat(manager.activeCall.value).isNotNull()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `IncomingCall - rings no longer than expiration time`() = runTest {
|
||||
setupShadowPowerManager()
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
val clock = FakeSystemClock()
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat, systemClock = clock)
|
||||
|
||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
|
||||
val eventTimestamp = A_FAKE_TIMESTAMP
|
||||
// The call should not ring more than 30 seconds after the initial event was sent
|
||||
val expirationTimestamp = eventTimestamp + 30_000
|
||||
|
||||
val callNotificationData = aCallNotificationData(
|
||||
timestamp = eventTimestamp,
|
||||
expirationTimestamp = expirationTimestamp,
|
||||
)
|
||||
|
||||
// suppose it took 10s to be notified
|
||||
clock.epochMillisResult = eventTimestamp + 10_000
|
||||
manager.registerIncomingCall(callNotificationData)
|
||||
|
||||
assertThat(manager.activeCall.value).isEqualTo(
|
||||
ActiveCall(
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = callNotificationData.sessionId,
|
||||
roomId = callNotificationData.roomId,
|
||||
),
|
||||
callState = CallState.Ringing(callNotificationData)
|
||||
)
|
||||
)
|
||||
|
||||
runCurrent()
|
||||
|
||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||
verify { notificationManagerCompat.notify(notificationId, any()) }
|
||||
|
||||
// advance by 21s it should have stopped ringing
|
||||
advanceTimeBy(21_000)
|
||||
runCurrent()
|
||||
|
||||
verify { notificationManagerCompat.cancel(any()) }
|
||||
}
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `IncomingCall - ignore expired ring lifetime`() = runTest {
|
||||
setupShadowPowerManager()
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
val clock = FakeSystemClock()
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat, systemClock = clock)
|
||||
|
||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
|
||||
val eventTimestamp = A_FAKE_TIMESTAMP
|
||||
// The call should not ring more than 30 seconds after the initial event was sent
|
||||
val expirationTimestamp = eventTimestamp + 30_000
|
||||
|
||||
val callNotificationData = aCallNotificationData(
|
||||
timestamp = eventTimestamp,
|
||||
expirationTimestamp = expirationTimestamp,
|
||||
)
|
||||
|
||||
// suppose it took 35s to be notified
|
||||
clock.epochMillisResult = eventTimestamp + 35_000
|
||||
manager.registerIncomingCall(callNotificationData)
|
||||
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
|
||||
runCurrent()
|
||||
|
||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||
verify(exactly = 0) { notificationManagerCompat.notify(notificationId, any()) }
|
||||
}
|
||||
|
||||
private fun setupShadowPowerManager() {
|
||||
shadowOf(InstrumentationRegistry.getInstrumentation().targetContext.getSystemService<PowerManager>()).apply {
|
||||
setIsWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK, true)
|
||||
|
|
@ -277,6 +457,7 @@ class DefaultActiveCallManagerTest {
|
|||
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
|
||||
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
|
||||
systemClock: FakeSystemClock = FakeSystemClock(),
|
||||
) = DefaultActiveCallManager(
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
coroutineScope = backgroundScope,
|
||||
|
|
@ -292,5 +473,6 @@ class DefaultActiveCallManagerTest {
|
|||
defaultCurrentCallService = DefaultCurrentCallService(),
|
||||
appForegroundStateService = FakeAppForegroundStateService(),
|
||||
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||
systemClock = systemClock,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ fun aCallNotificationData(
|
|||
avatarUrl: String? = AN_AVATAR_URL,
|
||||
notificationChannelId: String = "channel_id",
|
||||
timestamp: Long = 0L,
|
||||
expirationTimestamp: Long = 30_000L,
|
||||
textContent: String? = null,
|
||||
): CallNotificationData = CallNotificationData(
|
||||
sessionId = sessionId,
|
||||
|
|
@ -41,5 +42,6 @@ fun aCallNotificationData(
|
|||
avatarUrl = avatarUrl,
|
||||
notificationChannelId = notificationChannelId,
|
||||
timestamp = timestamp,
|
||||
expirationTimestamp = expirationTimestamp,
|
||||
textContent = textContent,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class FakeElementCallEntryPoint(
|
|||
senderName: String?,
|
||||
avatarUrl: String?,
|
||||
timestamp: Long,
|
||||
expirationTimestamp: Long,
|
||||
notificationChannelId: String,
|
||||
textContent: String?,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import io.element.android.libraries.architecture.NodeInputs
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint {
|
||||
fun interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint {
|
||||
fun builder(parentNode: Node, buildContext: BuildContext): Builder
|
||||
|
||||
interface Builder {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
|
|
@ -37,15 +38,7 @@ dependencies {
|
|||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.services.analytics.api)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,16 +37,7 @@ class ChangeRolesNode(
|
|||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
||||
private val presenter = presenterFactory.run {
|
||||
val role = when (inputs.listType) {
|
||||
ChangeRoomMemberRolesListType.Admins -> RoomMember.Role.Admin
|
||||
ChangeRoomMemberRolesListType.Moderators -> RoomMember.Role.Moderator
|
||||
ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving -> RoomMember.Role.Owner(isCreator = false)
|
||||
}
|
||||
create(role)
|
||||
}
|
||||
|
||||
private val presenter = presenterFactory.create(inputs.listType.toRoomMemberRole())
|
||||
private val stateFlow = launchMolecule { presenter.present() }
|
||||
|
||||
suspend fun waitForRoleChanged() {
|
||||
|
|
@ -63,3 +54,9 @@ class ChangeRolesNode(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ChangeRoomMemberRolesListType.toRoomMemberRole() = when (this) {
|
||||
ChangeRoomMemberRolesListType.Admins -> RoomMember.Role.Admin
|
||||
ChangeRoomMemberRolesListType.Moderators -> RoomMember.Role.Moderator
|
||||
ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving -> RoomMember.Role.Owner(isCreator = false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class ChangeRolesPresenter(
|
|||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<ChangeRolesState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun interface Factory {
|
||||
fun create(role: RoomMember.Role): ChangeRolesPresenter
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,16 +39,13 @@ class ChangeRoomMemberRolesRootNode(
|
|||
roomComponentFactory: RoomComponentFactory,
|
||||
) : ParentNode<ChangeRoomMemberRolesRootNode.NavTarget>(
|
||||
navModel = PermanentNavModel(
|
||||
navTargets = setOf(NavTarget.Root),
|
||||
navTargets = setOf(NavTarget),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
), DependencyInjectionGraphOwner, ChangeRoomMemberRolesEntryPoint.NodeProxy {
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Root : NavTarget
|
||||
}
|
||||
@Parcelize object NavTarget : Parcelable
|
||||
|
||||
data class Inputs(
|
||||
val joinedRoom: JoinedRoom,
|
||||
|
|
@ -60,14 +57,10 @@ class ChangeRoomMemberRolesRootNode(
|
|||
override val graph = roomComponentFactory.create(inputs.joinedRoom)
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Root -> {
|
||||
createNode<ChangeRolesNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(ChangeRolesNode.Inputs(listType = inputs.listType)),
|
||||
)
|
||||
}
|
||||
}
|
||||
return createNode<ChangeRolesNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(ChangeRolesNode.Inputs(listType = inputs.listType)),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.changeroommemberroles.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import org.junit.Test
|
||||
|
||||
class ChangeRolesNodeTest {
|
||||
@Test
|
||||
fun `test toRoomMemberRole`() {
|
||||
assertThat(ChangeRoomMemberRolesListType.Admins.toRoomMemberRole())
|
||||
.isEqualTo(RoomMember.Role.Admin)
|
||||
assertThat(ChangeRoomMemberRolesListType.Moderators.toRoomMemberRole())
|
||||
.isEqualTo(RoomMember.Role.Moderator)
|
||||
assertThat(ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving.toRoomMemberRole())
|
||||
.isEqualTo(RoomMember.Role.Owner(false))
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,6 @@ import kotlinx.collections.immutable.toPersistentMap
|
|||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.collections.plus
|
||||
|
||||
class ChangeRolesPresenterTest {
|
||||
@Test
|
||||
|
|
@ -556,18 +555,18 @@ class ChangeRolesPresenterTest {
|
|||
users = pairs.associate { (userId, role) -> userId to role.powerLevel }.toPersistentMap()
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.createChangeRolesPresenter(
|
||||
role: RoomMember.Role = RoomMember.Role.Admin,
|
||||
room: FakeJoinedRoom = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})),
|
||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
): ChangeRolesPresenter {
|
||||
return ChangeRolesPresenter(
|
||||
role = role,
|
||||
room = room,
|
||||
dispatchers = dispatchers,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TestScope.createChangeRolesPresenter(
|
||||
role: RoomMember.Role = RoomMember.Role.Admin,
|
||||
room: FakeJoinedRoom = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})),
|
||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
): ChangeRolesPresenter {
|
||||
return ChangeRolesPresenter(
|
||||
role = role,
|
||||
room = room,
|
||||
dispatchers = dispatchers,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.changeroommemberroles.impl
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DefaultChangeRoomMemberRolesEntyPointTest {
|
||||
@Test
|
||||
fun `test node builder`() = runTest {
|
||||
val entryPoint = DefaultChangeRoomMemberRolesEntyPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
ChangeRoomMemberRolesRootNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
roomComponentFactory = { },
|
||||
)
|
||||
}
|
||||
val room = FakeJoinedRoom()
|
||||
val listType = ChangeRoomMemberRolesListType.Admins
|
||||
val result = entryPoint.builder(parentNode, BuildContext.root(null))
|
||||
.room(FakeJoinedRoom())
|
||||
.listType(listType)
|
||||
.build()
|
||||
assertThat(result).isInstanceOf(ChangeRoomMemberRolesRootNode::class.java)
|
||||
// Search for the Inputs plugin
|
||||
val input = result.plugins.filterIsInstance<ChangeRoomMemberRolesRootNode.Inputs>().single()
|
||||
assertThat(input.joinedRoom.roomId).isEqualTo(room.roomId)
|
||||
assertThat(input.listType).isEqualTo(listType)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -43,13 +44,7 @@ dependencies {
|
|||
implementation(projects.features.invitepeople.api)
|
||||
api(projects.features.createroom.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.mediapickers.test)
|
||||
|
|
@ -58,7 +53,4 @@ dependencies {
|
|||
testImplementation(projects.libraries.usersearch.test)
|
||||
testImplementation(projects.features.startchat.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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.createroom.impl
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultCreateRoomEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultCreateRoomEntryPoint()
|
||||
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
CreateRoomFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
)
|
||||
}
|
||||
val callback = object : CreateRoomEntryPoint.Callback {
|
||||
override fun onRoomCreated(roomId: RoomId) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.callback(callback)
|
||||
.build()
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
|
|
@ -34,14 +35,6 @@ dependencies {
|
|||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.features.deactivation.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Confirme que pretende desativar a sua conta. Esta ação não pode ser desfeita."</string>
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Confirma que pretendes desativar a tua conta. Esta ação não pode ser desfeita."</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"Eliminar todas as minhas mensagens"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Aviso: futuros usuários podem ver conversas incompletas."</string>
|
||||
<string name="screen_deactivate_account_description">"A desativação da sua conta é %1$s, irá:"</string>
|
||||
|
|
|
|||
|
|
@ -148,10 +148,10 @@ class AccountDeactivationPresenterTest {
|
|||
assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
) = AccountDeactivationPresenter(
|
||||
matrixClient = matrixClient,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun createPresenter(
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
) = AccountDeactivationPresenter(
|
||||
matrixClient = matrixClient,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.logout.impl
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultAccountDeactivationEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultAccountDeactivationEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
AccountDeactivationNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenter = createPresenter(),
|
||||
)
|
||||
}
|
||||
val result = entryPoint.createNode(parentNode, BuildContext.root(null))
|
||||
assertThat(result).isInstanceOf(AccountDeactivationNode::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
|
|
@ -22,8 +23,6 @@ dependencies {
|
|||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,6 @@
|
|||
|
||||
package io.element.android.features.ftue.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||
|
||||
interface FtueEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun build(): Node
|
||||
}
|
||||
}
|
||||
interface FtueEntryPoint : SimpleFeatureEntryPoint
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -46,14 +47,7 @@ dependencies {
|
|||
implementation(projects.services.toolbox.api)
|
||||
implementation(projects.appconfig)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.analytics.noop)
|
||||
|
|
@ -61,5 +55,4 @@ dependencies {
|
|||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.features.lockscreen.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package io.element.android.features.ftue.impl
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
|
@ -19,13 +18,7 @@ import io.element.android.libraries.architecture.createNode
|
|||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultFtueEntryPoint : FtueEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): FtueEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : FtueEntryPoint.NodeBuilder {
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<FtueFlowNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<FtueFlowNode>(buildContext)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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.ftue.impl
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultFtueEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() = runTest {
|
||||
val entryPoint = DefaultFtueEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
FtueFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
analyticsEntryPoint = { _, _ -> lambdaError() },
|
||||
ftueState = createDefaultFtueService(),
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
lockScreenEntryPoint = object : LockScreenEntryPoint {
|
||||
override fun nodeBuilder(
|
||||
parentNode: com.bumble.appyx.core.node.Node,
|
||||
buildContext: BuildContext,
|
||||
navTarget: LockScreenEntryPoint.Target
|
||||
): LockScreenEntryPoint.NodeBuilder {
|
||||
lambdaError()
|
||||
}
|
||||
|
||||
override fun pinUnlockIntent(context: Context): Intent {
|
||||
lambdaError()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
val result = entryPoint.createNode(parentNode, BuildContext.root(null))
|
||||
assertThat(result).isInstanceOf(FtueFlowNode::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -226,22 +226,22 @@ class DefaultFtueServiceTest {
|
|||
resetPermissionLambda.assertions().isCalledOnce()
|
||||
.with(value("android.permission.POST_NOTIFICATIONS"))
|
||||
}
|
||||
|
||||
private fun TestScope.createDefaultFtueService(
|
||||
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
||||
lockScreenService: LockScreenService = FakeLockScreenService(),
|
||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
// First version where notification permission is required
|
||||
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
||||
) = DefaultFtueService(
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
|
||||
analyticsService = analyticsService,
|
||||
permissionStateProvider = permissionStateProvider,
|
||||
lockScreenService = lockScreenService,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun TestScope.createDefaultFtueService(
|
||||
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
||||
lockScreenService: LockScreenService = FakeLockScreenService(),
|
||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
// First version where notification permission is required
|
||||
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
||||
) = DefaultFtueService(
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
|
||||
analyticsService = analyticsService,
|
||||
permissionStateProvider = permissionStateProvider,
|
||||
lockScreenService = lockScreenService,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,5 @@ interface HomeEntryPoint : FeatureEntryPoint {
|
|||
fun onSessionConfirmRecoveryKeyClick()
|
||||
fun onRoomSettingsClick(roomId: RoomId)
|
||||
fun onReportBugClick()
|
||||
fun onLogoutForNativeSlidingSyncMigrationNeeded()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -58,14 +59,7 @@ dependencies {
|
|||
implementation(projects.libraries.previewutils)
|
||||
api(projects.features.home.api)
|
||||
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.features.invite.test)
|
||||
testImplementation(projects.features.logout.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
|
|
@ -80,5 +74,4 @@ dependencies {
|
|||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ class HomeFlowNode(
|
|||
stateFlow.value.roomListState.eventSink(RoomListEvents.LeaveRoom(roomId, needsConfirmation = false))
|
||||
}
|
||||
|
||||
fun rootNode(buildContext: BuildContext): Node {
|
||||
private fun rootNode(buildContext: BuildContext): Node {
|
||||
return node(buildContext) { modifier ->
|
||||
val state by stateFlow.collectAsState()
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<string name="screen_home_tab_spaces">"Espaços"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Tens a certeza que queres rejeitar o convite para entra em %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Rejeitar convite"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Tem a certeza que queres rejeitar esta conversa privada com %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Tens a certeza que queres rejeitar esta conversa privada com %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Rejeitar conversa"</string>
|
||||
<string name="screen_invites_empty_list">"Sem convites"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</string>
|
||||
|
|
@ -33,6 +33,7 @@ Por enquanto, podes anular a seleção dos filtros para veres as tuas outras con
|
|||
<string name="screen_roomlist_filter_invites">"Convites"</string>
|
||||
<string name="screen_roomlist_filter_invites_empty_state_title">"Não tens nenhum convite pendente."</string>
|
||||
<string name="screen_roomlist_filter_low_priority">"Prioridade baixa"</string>
|
||||
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Ainda não tens conversas de prioridade baixa"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Podes anular a seleção dos filtros para veres as tuas outras conversas"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_title">"Não tens nenhuma conversa selecionada"</string>
|
||||
<string name="screen_roomlist_filter_people">"Pessoas"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.home.impl
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.home.api.HomeEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DefaultHomeEntryPointTest {
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultHomeEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
HomeFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
matrixClient = FakeMatrixClient(),
|
||||
presenter = createHomePresenter(),
|
||||
inviteFriendsUseCase = { lambdaError() },
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
|
||||
directLogoutView = { _ -> lambdaError() },
|
||||
reportRoomEntryPoint = { _, _, _ -> lambdaError() },
|
||||
declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() },
|
||||
changeRoomMemberRolesEntryPoint = { _, _ -> lambdaError() },
|
||||
leaveRoomRenderer = { _, _, _ -> lambdaError() },
|
||||
)
|
||||
}
|
||||
val callback = object : HomeEntryPoint.Callback {
|
||||
override fun onRoomClick(roomId: RoomId) = lambdaError()
|
||||
override fun onStartChatClick() = lambdaError()
|
||||
override fun onSettingsClick() = lambdaError()
|
||||
override fun onSetUpRecoveryClick() = lambdaError()
|
||||
override fun onSessionConfirmRecoveryKeyClick() = lambdaError()
|
||||
override fun onRoomSettingsClick(roomId: RoomId) = lambdaError()
|
||||
override fun onReportBugClick() = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.callback(callback)
|
||||
.build()
|
||||
assertThat(result).isInstanceOf(HomeFlowNode::class.java)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
|
|
@ -175,24 +175,24 @@ class HomePresenterTest {
|
|||
assertThat(finalState.showNavigationBar).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHomePresenter(
|
||||
client: MatrixClient = FakeMatrixClient(),
|
||||
syncService: SyncService = FakeSyncService(),
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(false) },
|
||||
indicatorService: IndicatorService = FakeIndicatorService(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
homeSpacesPresenter: Presenter<HomeSpacesState> = Presenter { aHomeSpacesState() },
|
||||
) = HomePresenter(
|
||||
client = client,
|
||||
syncService = syncService,
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
indicatorService = indicatorService,
|
||||
logoutPresenter = { aDirectLogoutState() },
|
||||
roomListPresenter = { aRoomListState() },
|
||||
homeSpacesPresenter = homeSpacesPresenter,
|
||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun createHomePresenter(
|
||||
client: MatrixClient = FakeMatrixClient(),
|
||||
syncService: SyncService = FakeSyncService(),
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(false) },
|
||||
indicatorService: IndicatorService = FakeIndicatorService(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
homeSpacesPresenter: Presenter<HomeSpacesState> = Presenter { aHomeSpacesState() },
|
||||
) = HomePresenter(
|
||||
client = client,
|
||||
syncService = syncService,
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
indicatorService = indicatorService,
|
||||
logoutPresenter = { aDirectLogoutState() },
|
||||
roomListPresenter = { aRoomListState() },
|
||||
homeSpacesPresenter = homeSpacesPresenter,
|
||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface AcceptDeclineInviteView {
|
||||
fun interface AcceptDeclineInviteView {
|
||||
@Composable
|
||||
fun Render(
|
||||
state: AcceptDeclineInviteState,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@ import com.bumble.appyx.core.node.Node
|
|||
import io.element.android.features.invite.api.InviteData
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
|
||||
interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
|
||||
fun interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
|
||||
fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -36,17 +37,9 @@ dependencies {
|
|||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.features.invite.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class DeclineAndBlockPresenter(
|
|||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
) : Presenter<DeclineAndBlockState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun interface Factory {
|
||||
fun create(inviteData: InviteData): DeclineAndBlockPresenter
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<string name="screen_decline_and_block_title">"Rejeitar e bloquear"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Tens a certeza que queres rejeitar o convite para entra em %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Rejeitar convite"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Tem a certeza que queres rejeitar esta conversa privada com %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Tens a certeza que queres rejeitar esta conversa privada com %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Rejeitar conversa"</string>
|
||||
<string name="screen_invites_empty_list">"Sem convites"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</string>
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.invite.api.InviteData
|
||||
import io.element.android.features.invite.impl.DeclineInvite
|
||||
import io.element.android.features.invite.impl.fake.FakeDeclineInvite
|
||||
import io.element.android.features.invite.test.anInviteData
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
|
|
@ -148,28 +148,16 @@ class DeclineAndBlockPresenterTest {
|
|||
.isCalledOnce()
|
||||
.with(value(A_ROOM_ID), value(true), value(false), value(""))
|
||||
}
|
||||
|
||||
private fun anInviteData(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
name: String = A_ROOM_NAME,
|
||||
isDm: Boolean = false,
|
||||
): InviteData {
|
||||
return InviteData(
|
||||
roomId = roomId,
|
||||
roomName = name,
|
||||
isDm = isDm,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createDeclineAndBlockPresenter(
|
||||
inviteData: InviteData = anInviteData(),
|
||||
declineInvite: DeclineInvite = FakeDeclineInvite(),
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
): DeclineAndBlockPresenter {
|
||||
return DeclineAndBlockPresenter(
|
||||
inviteData = inviteData,
|
||||
declineInvite = declineInvite,
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createDeclineAndBlockPresenter(
|
||||
inviteData: InviteData = anInviteData(),
|
||||
declineInvite: DeclineInvite = FakeDeclineInvite(),
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
): DeclineAndBlockPresenter {
|
||||
return DeclineAndBlockPresenter(
|
||||
inviteData = inviteData,
|
||||
declineInvite = declineInvite,
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.invite.impl.declineandblock
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.invite.test.anInviteData
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultDeclineAndBlockEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultDeclineAndBlockEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
DeclineAndBlockNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenterFactory = { inviteData -> createDeclineAndBlockPresenter() }
|
||||
)
|
||||
}
|
||||
val inviteData = anInviteData()
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
inviteData = inviteData
|
||||
)
|
||||
assertThat(result).isInstanceOf(DeclineAndBlockNode::class.java)
|
||||
assertThat(result.plugins).contains(DeclineAndBlockNode.Inputs(inviteData))
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,11 @@
|
|||
|
||||
package io.element.android.features.invitepeople.api
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
interface InvitePeopleState {
|
||||
val canInvite: Boolean
|
||||
val isSearchActive: Boolean
|
||||
val sendInvitesAction: AsyncAction<Unit>
|
||||
val eventSink: (InvitePeopleEvents) -> Unit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,28 +8,33 @@
|
|||
package io.element.android.features.invitepeople.api
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
class InvitePeopleStateProvider : PreviewParameterProvider<InvitePeopleState> {
|
||||
override val values: Sequence<InvitePeopleState>
|
||||
get() = sequenceOf(
|
||||
aPreviewInvitePeopleState(),
|
||||
aPreviewInvitePeopleState(canInvite = true),
|
||||
aPreviewInvitePeopleState(isSearchActive = true)
|
||||
aPreviewInvitePeopleState(isSearchActive = true),
|
||||
aPreviewInvitePeopleState(sendInvitesAction = AsyncAction.Loading),
|
||||
)
|
||||
}
|
||||
|
||||
private data class PreviewInvitePeopleState(
|
||||
override val canInvite: Boolean,
|
||||
override val isSearchActive: Boolean,
|
||||
override val sendInvitesAction: AsyncAction<Unit>,
|
||||
override val eventSink: (InvitePeopleEvents) -> Unit,
|
||||
) : InvitePeopleState
|
||||
|
||||
private fun aPreviewInvitePeopleState(
|
||||
canInvite: Boolean = false,
|
||||
isSearchActive: Boolean = false,
|
||||
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (InvitePeopleEvents) -> Unit = {},
|
||||
) = PreviewInvitePeopleState(
|
||||
canInvite = canInvite,
|
||||
isSearchActive = isSearchActive,
|
||||
sendInvitesAction = sendInvitesAction,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -37,17 +38,8 @@ dependencies {
|
|||
implementation(projects.services.apperror.api)
|
||||
api(projects.features.invitepeople.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.usersearch.test)
|
||||
testImplementation(projects.services.apperror.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ import dev.zacsweers.metro.Inject
|
|||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.map
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
|
@ -73,6 +75,8 @@ class DefaultInvitePeoplePresenter(
|
|||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
||||
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) {
|
||||
if (joinedRoom == null) {
|
||||
val result = matrixClient.getJoinedRoom(roomId)
|
||||
|
|
@ -116,7 +120,7 @@ class DefaultInvitePeoplePresenter(
|
|||
}
|
||||
is InvitePeopleEvents.SendInvites -> {
|
||||
room.dataOrNull()?.let {
|
||||
sessionCoroutineScope.sendInvites(it, selectedUsers.value)
|
||||
sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction)
|
||||
}
|
||||
}
|
||||
is InvitePeopleEvents.CloseSearch -> {
|
||||
|
|
@ -128,12 +132,13 @@ class DefaultInvitePeoplePresenter(
|
|||
|
||||
return DefaultInvitePeopleState(
|
||||
room = room.map { },
|
||||
canInvite = selectedUsers.value.isNotEmpty(),
|
||||
canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(),
|
||||
selectedUsers = selectedUsers.value,
|
||||
searchQuery = searchQuery,
|
||||
isSearchActive = searchActive,
|
||||
searchResults = searchResults.value,
|
||||
showSearchLoader = showSearchLoader.value,
|
||||
sendInvitesAction = sendInvitesAction.value,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
|
@ -141,16 +146,21 @@ class DefaultInvitePeoplePresenter(
|
|||
private fun CoroutineScope.sendInvites(
|
||||
room: JoinedRoom,
|
||||
selectedUsers: List<MatrixUser>,
|
||||
sendInvitesAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch {
|
||||
val anyInviteFailed = selectedUsers
|
||||
.map { room.inviteUserById(it.userId) }
|
||||
.any { it.isFailure }
|
||||
sendInvitesAction.runUpdatingState {
|
||||
val anyInviteFailed = selectedUsers
|
||||
.map { room.inviteUserById(it.userId) }
|
||||
.any { it.isFailure }
|
||||
|
||||
if (anyInviteFailed) {
|
||||
appErrorStateService.showError(
|
||||
titleRes = CommonStrings.common_unable_to_invite_title,
|
||||
bodyRes = CommonStrings.common_unable_to_invite_message,
|
||||
)
|
||||
if (anyInviteFailed) {
|
||||
appErrorStateService.showError(
|
||||
titleRes = CommonStrings.common_unable_to_invite_title,
|
||||
bodyRes = CommonStrings.common_unable_to_invite_message,
|
||||
)
|
||||
}
|
||||
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.invitepeople.impl
|
|||
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
|
@ -22,5 +23,6 @@ data class DefaultInvitePeopleState(
|
|||
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
override val isSearchActive: Boolean,
|
||||
override val sendInvitesAction: AsyncAction<Unit>,
|
||||
override val eventSink: (InvitePeopleEvents) -> Unit
|
||||
) : InvitePeopleState
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.features.invitepeople.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
|
@ -68,6 +69,11 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider<Defau
|
|||
showSearchLoader = true,
|
||||
),
|
||||
aDefaultInvitePeopleState(room = AsyncData.Failure(Exception("Room not found"))),
|
||||
aDefaultInvitePeopleState(
|
||||
canInvite = false,
|
||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||
sendInvitesAction = AsyncAction.Loading,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -93,6 +99,7 @@ private fun aDefaultInvitePeopleState(
|
|||
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
isSearchActive: Boolean = false,
|
||||
showSearchLoader: Boolean = false,
|
||||
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
): DefaultInvitePeopleState {
|
||||
return DefaultInvitePeopleState(
|
||||
room = room,
|
||||
|
|
@ -102,6 +109,7 @@ private fun aDefaultInvitePeopleState(
|
|||
selectedUsers = selectedUsers,
|
||||
isSearchActive = isSearchActive,
|
||||
showSearchLoader = showSearchLoader,
|
||||
sendInvitesAction = sendInvitesAction,
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -409,10 +409,23 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||
// Send invites
|
||||
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
||||
|
||||
// Can't invite in the loading state
|
||||
awaitItem().run {
|
||||
assertThat(sendInvitesAction.isLoading()).isTrue()
|
||||
assertThat(canInvite).isFalse()
|
||||
}
|
||||
|
||||
delay(1_000)
|
||||
inviteUserResult.assertions().isCalledOnce().with(
|
||||
value(selectedUser.userId)
|
||||
)
|
||||
|
||||
// Can invite again once the action is finished
|
||||
awaitItem().run {
|
||||
assertThat(sendInvitesAction.isReady()).isTrue()
|
||||
assertThat(canInvite).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -445,6 +458,13 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||
// Send invites
|
||||
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
||||
|
||||
// Can't invite in the loading state
|
||||
awaitItem().run {
|
||||
assertThat(sendInvitesAction.isLoading()).isTrue()
|
||||
assertThat(canInvite).isFalse()
|
||||
}
|
||||
|
||||
delay(1_000)
|
||||
inviteUserResult.assertions().isCalledOnce().with(
|
||||
value(selectedUser.userId)
|
||||
|
|
@ -455,6 +475,12 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||
value(CommonStrings.common_unable_to_invite_title),
|
||||
value(CommonStrings.common_unable_to_invite_message)
|
||||
)
|
||||
|
||||
// Can invite again once the action is finished
|
||||
awaitItem().run {
|
||||
assertThat(sendInvitesAction.isReady()).isTrue()
|
||||
assertThat(canInvite).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
|
|
@ -38,16 +39,8 @@ dependencies {
|
|||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.appconfig)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.features.invite.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class JoinRoomPresenter(
|
|||
private val buildMeta: BuildMeta,
|
||||
private val seenInvitesStore: SeenInvitesStore,
|
||||
) : Presenter<JoinRoomState> {
|
||||
interface Factory {
|
||||
fun interface Factory {
|
||||
fun create(
|
||||
roomId: RoomId,
|
||||
roomIdOrAlias: RoomIdOrAlias,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<string name="screen_join_room_knock_action">"Bater à porta"</string>
|
||||
<string name="screen_join_room_knock_message_characters_count">"%1$d de %2$d caracteres permitidos"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Mensagem (opcional)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Irá receber um convite para participar na sala se seu pedido for aceite."</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Irás receber um convite para participar na sala se o pedido for aceite."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Pedido de adesão enviado"</string>
|
||||
<string name="screen_join_room_loading_alert_message">"Não conseguimos exibir a pré-visualização da sala. Isso pode ser devido a problemas de rede ou servidor."</string>
|
||||
<string name="screen_join_room_loading_alert_title">"Não foi possível exibir a pré-visualização desta sala"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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.joinroom.impl
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.features.invite.api.InviteData
|
||||
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
|
||||
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.Optional
|
||||
|
||||
class DefaultJoinRoomEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultJoinRoomEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
JoinRoomFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenterFactory = { _, _, _, _, _ -> createJoinRoomPresenter() },
|
||||
acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
|
||||
declineAndBlockEntryPoint = object : DeclineInviteAndBlockEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData) = lambdaError()
|
||||
}
|
||||
)
|
||||
}
|
||||
val inputs = JoinRoomEntryPoint.Inputs(
|
||||
roomId = A_ROOM_ID,
|
||||
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
|
||||
roomDescription = Optional.ofNullable(null),
|
||||
serverNames = emptyList(),
|
||||
trigger = JoinedRoom.Trigger.RoomDirectory,
|
||||
)
|
||||
val result = entryPoint.createNode(parentNode, BuildContext.root(null), inputs)
|
||||
assertThat(result).isInstanceOf(JoinRoomFlowNode::class.java)
|
||||
assertThat(result.plugins).contains(inputs)
|
||||
}
|
||||
}
|
||||
|
|
@ -1034,39 +1034,6 @@ class JoinRoomPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createJoinRoomPresenter(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
roomDescription: Optional<RoomDescription> = Optional.empty(),
|
||||
serverNames: List<String> = emptyList(),
|
||||
trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite,
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
joinRoomLambda: (RoomIdOrAlias, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
},
|
||||
knockRoom: KnockRoom = FakeKnockRoom(),
|
||||
cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(),
|
||||
forgetRoom: ForgetRoom = FakeForgetRoom(),
|
||||
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
|
||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||
): JoinRoomPresenter {
|
||||
return JoinRoomPresenter(
|
||||
roomId = roomId,
|
||||
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
||||
roomDescription = roomDescription,
|
||||
serverNames = serverNames,
|
||||
trigger = trigger,
|
||||
matrixClient = matrixClient,
|
||||
joinRoom = FakeJoinRoom(joinRoomLambda),
|
||||
knockRoom = knockRoom,
|
||||
cancelKnockRoom = cancelKnockRoom,
|
||||
forgetRoom = forgetRoom,
|
||||
buildMeta = buildMeta,
|
||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||
seenInvitesStore = seenInvitesStore,
|
||||
)
|
||||
}
|
||||
|
||||
private fun aRoomDescription(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
name: String? = A_ROOM_NAME,
|
||||
|
|
@ -1087,3 +1054,36 @@ class JoinRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createJoinRoomPresenter(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
roomDescription: Optional<RoomDescription> = Optional.empty(),
|
||||
serverNames: List<String> = emptyList(),
|
||||
trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite,
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
joinRoomLambda: (RoomIdOrAlias, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
},
|
||||
knockRoom: KnockRoom = FakeKnockRoom(),
|
||||
cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(),
|
||||
forgetRoom: ForgetRoom = FakeForgetRoom(),
|
||||
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
|
||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||
): JoinRoomPresenter {
|
||||
return JoinRoomPresenter(
|
||||
roomId = roomId,
|
||||
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
||||
roomDescription = roomDescription,
|
||||
serverNames = serverNames,
|
||||
trigger = trigger,
|
||||
matrixClient = matrixClient,
|
||||
joinRoom = FakeJoinRoom(joinRoomLambda),
|
||||
knockRoom = knockRoom,
|
||||
cancelKnockRoom = cancelKnockRoom,
|
||||
forgetRoom = forgetRoom,
|
||||
buildMeta = buildMeta,
|
||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||
seenInvitesStore = seenInvitesStore,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
|
|
@ -33,15 +34,7 @@ dependencies {
|
|||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.knockrequests.impl.list
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultKnockRequestsListEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() = runTest {
|
||||
val entryPoint = DefaultKnockRequestsListEntryPoint()
|
||||
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
KnockRequestsListNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenter = createKnockRequestsListPresenter(),
|
||||
)
|
||||
}
|
||||
val result = entryPoint.createNode(parentNode, BuildContext.root(null))
|
||||
assertThat(result).isInstanceOf(KnockRequestsListNode::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -286,19 +286,19 @@ class KnockRequestsListPresenterTest {
|
|||
assert(acceptFailureLambda).isCalledOnce()
|
||||
assert(acceptSuccessLambda).isCalledOnce()
|
||||
}
|
||||
|
||||
private fun TestScope.createKnockRequestsListPresenter(
|
||||
canAccept: Boolean = true,
|
||||
canDecline: Boolean = true,
|
||||
canBan: Boolean = true,
|
||||
knockRequestsFlow: Flow<List<KnockRequest>> = flowOf(emptyList())
|
||||
): KnockRequestsListPresenter {
|
||||
val knockRequestsService = KnockRequestsService(
|
||||
knockRequestsFlow = knockRequestsFlow,
|
||||
coroutineScope = backgroundScope,
|
||||
isKnockFeatureEnabledFlow = flowOf(true),
|
||||
permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)),
|
||||
)
|
||||
return KnockRequestsListPresenter(knockRequestsService = knockRequestsService)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TestScope.createKnockRequestsListPresenter(
|
||||
canAccept: Boolean = true,
|
||||
canDecline: Boolean = true,
|
||||
canBan: Boolean = true,
|
||||
knockRequestsFlow: Flow<List<KnockRequest>> = flowOf(emptyList())
|
||||
): KnockRequestsListPresenter {
|
||||
val knockRequestsService = KnockRequestsService(
|
||||
knockRequestsFlow = knockRequestsFlow,
|
||||
coroutineScope = backgroundScope,
|
||||
isKnockFeatureEnabledFlow = flowOf(true),
|
||||
permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)),
|
||||
)
|
||||
return KnockRequestsListPresenter(knockRequestsService = knockRequestsService)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface LeaveRoomRenderer {
|
||||
fun interface LeaveRoomRenderer {
|
||||
@Composable
|
||||
fun Render(
|
||||
state: LeaveRoomState,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -26,13 +27,8 @@ dependencies {
|
|||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.push.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.coroutines.core)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@
|
|||
|
||||
package io.element.android.features.licenses.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||
|
||||
interface OpenSourceLicensesEntryPoint {
|
||||
fun getNode(node: Node, buildContext: BuildContext): Node
|
||||
}
|
||||
interface OpenSourceLicensesEntryPoint : SimpleFeatureEntryPoint
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -26,12 +27,8 @@ dependencies {
|
|||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.features.licenses.api)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.coroutines.core)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import io.element.android.libraries.architecture.createNode
|
|||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultOpenSourcesLicensesEntryPoint : OpenSourceLicensesEntryPoint {
|
||||
override fun getNode(node: Node, buildContext: BuildContext): Node {
|
||||
return node.createNode<DependenciesFlowNode>(buildContext)
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<DependenciesFlowNode>(buildContext)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.licenses.impl
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultOpenSourcesLicensesEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultOpenSourcesLicensesEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
DependenciesFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
)
|
||||
}
|
||||
val result = entryPoint.createNode(parentNode, BuildContext.root(null))
|
||||
assertThat(result).isInstanceOf(DependenciesFlowNode::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
import config.BuildTimeConfig
|
||||
import extension.buildConfigFieldStr
|
||||
import extension.readLocalProperty
|
||||
import extension.testCommonDependencies
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
|
|
@ -70,6 +71,5 @@ dependencies {
|
|||
implementation(projects.libraries.uiStrings)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -37,17 +38,10 @@ dependencies {
|
|||
implementation(projects.services.analytics.api)
|
||||
implementation(libs.accompanist.permission)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.testtags)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.features.messages.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ package io.element.android.features.location.impl.common.permissions
|
|||
import io.element.android.libraries.architecture.Presenter
|
||||
|
||||
interface PermissionsPresenter : Presenter<PermissionsState> {
|
||||
interface Factory {
|
||||
fun interface Factory {
|
||||
fun create(permissions: List<String>): PermissionsPresenter
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class SendLocationPresenter(
|
|||
private val buildMeta: BuildMeta,
|
||||
) : Presenter<SendLocationState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun interface Factory {
|
||||
fun create(timelineMode: Timeline.Mode): SendLocationPresenter
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ import io.element.android.services.analytics.api.AnalyticsService
|
|||
@ContributesNode(RoomScope::class)
|
||||
@Inject
|
||||
class ShowLocationNode(
|
||||
presenterFactory: ShowLocationPresenter.Factory,
|
||||
analyticsService: AnalyticsService,
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: ShowLocationPresenter.Factory,
|
||||
analyticsService: AnalyticsService,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
|
|
|
|||
|
|
@ -28,14 +28,14 @@ import io.element.android.libraries.core.meta.BuildMeta
|
|||
|
||||
@Inject
|
||||
class ShowLocationPresenter(
|
||||
@Assisted private val location: Location,
|
||||
@Assisted private val description: String?,
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
private val locationActions: LocationActions,
|
||||
private val buildMeta: BuildMeta,
|
||||
@Assisted private val location: Location,
|
||||
@Assisted private val description: String?
|
||||
) : Presenter<ShowLocationState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun interface Factory {
|
||||
fun create(location: Location, description: String?): ShowLocationPresenter
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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.location.impl.send
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultSendLocationEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultSendLocationEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
SendLocationNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenterFactory = { timelineMode: Timeline.Mode ->
|
||||
SendLocationPresenter(
|
||||
permissionsPresenterFactory = { FakePermissionsPresenter() },
|
||||
room = FakeJoinedRoom(),
|
||||
timelineMode = timelineMode,
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
messageComposerContext = FakeMessageComposerContext(),
|
||||
locationActions = FakeLocationActions(),
|
||||
buildMeta = aBuildMeta(),
|
||||
)
|
||||
},
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
)
|
||||
}
|
||||
val timelineMode = Timeline.Mode.Live
|
||||
val result = entryPoint.builder(timelineMode)
|
||||
.build(parentNode, BuildContext.root(null))
|
||||
assertThat(result).isInstanceOf(SendLocationNode::class.java)
|
||||
assertThat(result.plugins).contains(SendLocationNode.Inputs(timelineMode))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.location.impl.show
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultShowLocationEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultShowLocationEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
ShowLocationNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenterFactory = { location: Location, description: String? ->
|
||||
ShowLocationPresenter(
|
||||
permissionsPresenterFactory = { FakePermissionsPresenter() },
|
||||
locationActions = FakeLocationActions(),
|
||||
buildMeta = aBuildMeta(),
|
||||
location = location,
|
||||
description = description,
|
||||
)
|
||||
},
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
)
|
||||
}
|
||||
val inputs = ShowLocationEntryPoint.Inputs(
|
||||
location = Location(37.4219983, -122.084, 10f),
|
||||
description = "My location",
|
||||
)
|
||||
val result = entryPoint.createNode(
|
||||
parentNode,
|
||||
BuildContext.root(null),
|
||||
inputs = inputs,
|
||||
)
|
||||
assertThat(result).isInstanceOf(ShowLocationNode::class.java)
|
||||
assertThat(result.plugins).contains(inputs)
|
||||
}
|
||||
}
|
||||
|
|
@ -37,10 +37,10 @@ class ShowLocationPresenterTest {
|
|||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
|
||||
},
|
||||
fakeLocationActions,
|
||||
fakeBuildMeta,
|
||||
location,
|
||||
A_DESCRIPTION,
|
||||
locationActions = fakeLocationActions,
|
||||
buildMeta = fakeBuildMeta,
|
||||
location = location,
|
||||
description = A_DESCRIPTION,
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ package io.element.android.features.location.test
|
|||
import io.element.android.features.location.api.LocationService
|
||||
|
||||
class FakeLocationService(
|
||||
private val isServiceAvailable: Boolean,
|
||||
private val isServiceAvailable: Boolean = false,
|
||||
) : LocationService {
|
||||
override fun isServiceAvailable() = isServiceAvailable
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -44,21 +45,12 @@ dependencies {
|
|||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.biometric)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.libraries.cryptography.test)
|
||||
testImplementation(projects.libraries.cryptography.impl)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
testImplementation(projects.features.logout.test)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ class PinUnlockPresenter(
|
|||
|
||||
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||
suspend {
|
||||
logoutUseCase.logout(ignoreSdkError = true)
|
||||
logoutUseCase.logoutAll(ignoreSdkError = true)
|
||||
}.runCatchingUpdatingState(signOutAction)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.lockscreen.impl
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DefaultLockScreenEntryPointIntentTest {
|
||||
@Test
|
||||
fun `test pin unlock intent`() {
|
||||
val entryPoint = DefaultLockScreenEntryPoint()
|
||||
val result = entryPoint.pinUnlockIntent(InstrumentationRegistry.getInstrumentation().context)
|
||||
assertThat(result.component?.className).isEqualTo(PinUnlockActivity::class.qualifiedName)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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.lockscreen.impl
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultLockScreenEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder Setup`() {
|
||||
val entryPoint = DefaultLockScreenEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
LockScreenFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
)
|
||||
}
|
||||
val callback = object : LockScreenEntryPoint.Callback {
|
||||
override fun onSetupDone() = lambdaError()
|
||||
}
|
||||
val navTarget = LockScreenEntryPoint.Target.Setup
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null), navTarget)
|
||||
.callback(callback)
|
||||
.build()
|
||||
assertThat(result).isInstanceOf(LockScreenFlowNode::class.java)
|
||||
assertThat(result.plugins).contains(LockScreenFlowNode.Inputs(LockScreenFlowNode.NavTarget.Setup))
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test node builder Settings`() {
|
||||
val entryPoint = DefaultLockScreenEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
LockScreenFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
)
|
||||
}
|
||||
val callback = object : LockScreenEntryPoint.Callback {
|
||||
override fun onSetupDone() = lambdaError()
|
||||
}
|
||||
val navTarget = LockScreenEntryPoint.Target.Settings
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null), navTarget)
|
||||
.callback(callback)
|
||||
.build()
|
||||
assertThat(result).isInstanceOf(LockScreenFlowNode::class.java)
|
||||
assertThat(result.plugins).contains(LockScreenFlowNode.Inputs(LockScreenFlowNode.NavTarget.Settings))
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -48,14 +49,7 @@ dependencies {
|
|||
implementation(libs.serialization.json)
|
||||
api(projects.features.login.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.features.login.test)
|
||||
testImplementation(projects.features.enterprise.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
|
|
@ -63,6 +57,4 @@ dependencies {
|
|||
testImplementation(projects.libraries.oidc.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.wellknown.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ Tenta iniciar a sessão manualmente ou digitaliza o código QR com outro disposi
|
|||
<string name="screen_qr_code_login_initial_state_item_3">"Seleciona %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Ligar novo dispositivo”"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Lê o código QR com este dispositivo"</string>
|
||||
<string name="screen_qr_code_login_initial_state_subtitle">"Disponível apenas se o seu fornecedor de conta o suportar."</string>
|
||||
<string name="screen_qr_code_login_initial_state_subtitle">"Disponível apenas se o teu operador de conta o permitir."</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Abre a %1$s noutro dispositivo para obteres o código QR"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Lê o código QR apresentado no outro dispositivo."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Tentar novamente"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||
import io.element.android.features.login.api.LoginEntryPoint
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultLoginEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultLoginEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
LoginFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
||||
oidcActionFlow = FakeOidcActionFlow(),
|
||||
)
|
||||
}
|
||||
val callback = object : LoginEntryPoint.Callback {
|
||||
override fun onReportProblem() = lambdaError()
|
||||
}
|
||||
val params = LoginEntryPoint.Params(
|
||||
accountProvider = "ac",
|
||||
loginHint = "lh",
|
||||
)
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.params(params)
|
||||
.callback(callback)
|
||||
.build()
|
||||
assertThat(result).isInstanceOf(LoginFlowNode::class.java)
|
||||
assertThat(result.plugins).contains(LoginFlowNode.Params(params.accountProvider, params.loginHint))
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,16 +8,12 @@
|
|||
package io.element.android.features.logout.api
|
||||
|
||||
/**
|
||||
* Used to trigger a log out of the current user from any part of the app.
|
||||
* Used to trigger a log out of the current user(s) from any part of the app.
|
||||
*/
|
||||
interface LogoutUseCase {
|
||||
/**
|
||||
* Log out the current user and then perform any needed cleanup tasks.
|
||||
* Log out the current user(s) and then perform any needed cleanup tasks.
|
||||
* @param ignoreSdkError if true, the SDK error will be ignored and the user will be logged out anyway.
|
||||
*/
|
||||
suspend fun logout(ignoreSdkError: Boolean)
|
||||
|
||||
interface Factory {
|
||||
fun create(sessionId: String): LogoutUseCase
|
||||
}
|
||||
suspend fun logoutAll(ignoreSdkError: Boolean)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ package io.element.android.features.logout.api.direct
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
interface DirectLogoutView {
|
||||
fun interface DirectLogoutView {
|
||||
@Composable
|
||||
fun Render(state: DirectLogoutState)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -35,15 +36,8 @@ dependencies {
|
|||
implementation(projects.libraries.dateformatter.api)
|
||||
api(projects.features.logout.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
}
|
||||
|
|
|
|||
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