Merge branch 'develop' into feature/fga/join_space
This commit is contained in:
commit
5cda6730c4
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.
|
# Android Studio 3 in .gitignore file.
|
||||||
.idea/caches
|
.idea/caches
|
||||||
.idea/copilot
|
.idea/copilot
|
||||||
|
.idea/copilot.*
|
||||||
.idea/inspectionProfiles
|
.idea/inspectionProfiles
|
||||||
# Shelved changes in the IDE
|
# Shelved changes in the IDE
|
||||||
.idea/shelf
|
.idea/shelf
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import extension.koverDependencies
|
||||||
import extension.locales
|
import extension.locales
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
import extension.setupKover
|
import extension.setupKover
|
||||||
|
import extension.testCommonDependencies
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
|
|
@ -290,15 +291,9 @@ dependencies {
|
||||||
|
|
||||||
implementation(libs.matrix.emojibase.bindings)
|
implementation(libs.matrix.emojibase.bindings)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs)
|
||||||
testImplementation(libs.test.robolectric)
|
|
||||||
testImplementation(libs.coroutines.test)
|
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
testImplementation(projects.tests.testutils)
|
|
||||||
|
|
||||||
koverDependencies()
|
koverDependencies()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
import extension.allFeaturesApi
|
import extension.allFeaturesApi
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
|
|
@ -42,18 +43,12 @@ dependencies {
|
||||||
|
|
||||||
implementation(projects.features.ftue.api)
|
implementation(projects.features.ftue.api)
|
||||||
implementation(projects.features.share.api)
|
implementation(projects.features.share.api)
|
||||||
implementation(projects.features.viewfolder.api)
|
|
||||||
|
|
||||||
implementation(projects.services.apperror.impl)
|
implementation(projects.services.apperror.impl)
|
||||||
implementation(projects.services.appnavstate.api)
|
implementation(projects.services.appnavstate.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs)
|
||||||
testImplementation(libs.test.robolectric)
|
|
||||||
testImplementation(libs.coroutines.test)
|
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(projects.features.login.test)
|
testImplementation(projects.features.login.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.oidc.test)
|
testImplementation(projects.libraries.oidc.test)
|
||||||
|
|
@ -61,11 +56,8 @@ dependencies {
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
testImplementation(projects.tests.testutils)
|
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.services.toolbox.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.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.home.api.HomeEntryPoint
|
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.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||||
|
|
@ -119,7 +118,6 @@ class LoggedInFlowNode(
|
||||||
private val shareEntryPoint: ShareEntryPoint,
|
private val shareEntryPoint: ShareEntryPoint,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val sendingQueue: SendQueues,
|
private val sendingQueue: SendQueues,
|
||||||
private val logoutEntryPoint: LogoutEntryPoint,
|
|
||||||
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
|
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
|
||||||
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
|
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
|
||||||
private val sessionEnterpriseService: SessionEnterpriseService,
|
private val sessionEnterpriseService: SessionEnterpriseService,
|
||||||
|
|
@ -277,9 +275,6 @@ class LoggedInFlowNode(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class IncomingShare(val intent: Intent) : NavTarget
|
data class IncomingShare(val intent: Intent) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget
|
data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget
|
||||||
}
|
}
|
||||||
|
|
@ -324,10 +319,6 @@ class LoggedInFlowNode(
|
||||||
override fun onReportBugClick() {
|
override fun onReportBugClick() {
|
||||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLogoutForNativeSlidingSyncMigrationNeeded() {
|
|
||||||
backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
homeEntryPoint
|
homeEntryPoint
|
||||||
.nodeBuilder(this, buildContext)
|
.nodeBuilder(this, buildContext)
|
||||||
|
|
@ -454,8 +445,7 @@ class LoggedInFlowNode(
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
NavTarget.Ftue -> {
|
NavTarget.Ftue -> {
|
||||||
ftueEntryPoint.nodeBuilder(this, buildContext)
|
ftueEntryPoint.createNode(this, buildContext)
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
NavTarget.RoomDirectorySearch -> {
|
NavTarget.RoomDirectorySearch -> {
|
||||||
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
|
@ -486,17 +476,6 @@ class LoggedInFlowNode(
|
||||||
.params(ShareEntryPoint.Params(intent = navTarget.intent))
|
.params(ShareEntryPoint.Params(intent = navTarget.intent))
|
||||||
.build()
|
.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 -> {
|
is NavTarget.IncomingVerificationRequest -> {
|
||||||
incomingVerificationEntryPoint.nodeBuilder(this, buildContext)
|
incomingVerificationEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.params(IncomingVerificationEntryPoint.Params(navTarget.data))
|
.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.bugreport.BugReportEntryPoint
|
||||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||||
import io.element.android.features.signedout.api.SignedOutEntryPoint
|
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.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
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.core.uri.ensureProtocol
|
||||||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
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.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.oidc.api.OidcAction
|
import io.element.android.libraries.oidc.api.OidcAction
|
||||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
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.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -65,13 +64,12 @@ import timber.log.Timber
|
||||||
class RootFlowNode(
|
class RootFlowNode(
|
||||||
@Assisted val buildContext: BuildContext,
|
@Assisted val buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val authenticationService: MatrixAuthenticationService,
|
private val sessionStore: SessionStore,
|
||||||
private val accountProviderAccessControl: AccountProviderAccessControl,
|
private val accountProviderAccessControl: AccountProviderAccessControl,
|
||||||
private val navStateFlowFactory: RootNavStateFlowFactory,
|
private val navStateFlowFactory: RootNavStateFlowFactory,
|
||||||
private val matrixSessionCache: MatrixSessionCache,
|
private val matrixSessionCache: MatrixSessionCache,
|
||||||
private val presenter: RootPresenter,
|
private val presenter: RootPresenter,
|
||||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||||
private val viewFolderEntryPoint: ViewFolderEntryPoint,
|
|
||||||
private val signedOutEntryPoint: SignedOutEntryPoint,
|
private val signedOutEntryPoint: SignedOutEntryPoint,
|
||||||
private val intentResolver: IntentResolver,
|
private val intentResolver: IntentResolver,
|
||||||
private val oidcActionFlow: OidcActionFlow,
|
private val oidcActionFlow: OidcActionFlow,
|
||||||
|
|
@ -154,7 +152,7 @@ class RootFlowNode(
|
||||||
onSuccess: (SessionId) -> Unit,
|
onSuccess: (SessionId) -> Unit,
|
||||||
onFailure: () -> Unit
|
onFailure: () -> Unit
|
||||||
) {
|
) {
|
||||||
val latestSessionId = authenticationService.getLatestSessionId()
|
val latestSessionId = sessionStore.getLatestSessionId()
|
||||||
if (latestSessionId == null) {
|
if (latestSessionId == null) {
|
||||||
onFailure()
|
onFailure()
|
||||||
return
|
return
|
||||||
|
|
@ -200,11 +198,6 @@ class RootFlowNode(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object BugReport : NavTarget
|
data object BugReport : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ViewLogs(
|
|
||||||
val rootPath: String,
|
|
||||||
) : NavTarget
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -244,31 +237,12 @@ class RootFlowNode(
|
||||||
NavTarget.SplashScreen -> splashNode(buildContext)
|
NavTarget.SplashScreen -> splashNode(buildContext)
|
||||||
NavTarget.BugReport -> {
|
NavTarget.BugReport -> {
|
||||||
val callback = object : BugReportEntryPoint.Callback {
|
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() {
|
override fun onDone() {
|
||||||
backstack.pop()
|
backstack.pop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val params = ViewFolderEntryPoint.Params(
|
bugReportEntryPoint
|
||||||
rootPath = navTarget.rootPath,
|
|
||||||
)
|
|
||||||
viewFolderEntryPoint
|
|
||||||
.nodeBuilder(this, buildContext)
|
.nodeBuilder(this, buildContext)
|
||||||
.params(params)
|
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
@ -294,7 +268,7 @@ class RootFlowNode(
|
||||||
|
|
||||||
private suspend fun onLoginLink(params: LoginParams) {
|
private suspend fun onLoginLink(params: LoginParams) {
|
||||||
// Is there a session already?
|
// Is there a session already?
|
||||||
val latestSessionId = authenticationService.getLatestSessionId()
|
val latestSessionId = sessionStore.getLatestSessionId()
|
||||||
if (latestSessionId == null) {
|
if (latestSessionId == null) {
|
||||||
// No session, open login
|
// No session, open login
|
||||||
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) {
|
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) {
|
||||||
|
|
@ -311,7 +285,7 @@ class RootFlowNode(
|
||||||
|
|
||||||
private suspend fun onIncomingShare(intent: Intent) {
|
private suspend fun onIncomingShare(intent: Intent) {
|
||||||
// Is there a session already?
|
// Is there a session already?
|
||||||
val latestSessionId = authenticationService.getLatestSessionId()
|
val latestSessionId = sessionStore.getLatestSessionId()
|
||||||
if (latestSessionId == null) {
|
if (latestSessionId == null) {
|
||||||
// No session, open login
|
// No session, open login
|
||||||
switchToNotLoggedInFlow(null)
|
switchToNotLoggedInFlow(null)
|
||||||
|
|
@ -368,3 +342,5 @@ class RootFlowNode(
|
||||||
.attachSession()
|
.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
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
|
||||||
interface RoomComponentFactory {
|
fun interface RoomComponentFactory {
|
||||||
fun create(room: JoinedRoom): Any
|
fun create(room: JoinedRoom): Any
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ import com.bumble.appyx.core.state.SavedStateMap
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.appnav.di.MatrixSessionCache
|
import io.element.android.appnav.di.MatrixSessionCache
|
||||||
import io.element.android.features.preferences.api.CacheService
|
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.matrix.ui.media.ImageLoaderHolder
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
@ -28,7 +28,7 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFact
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
class RootNavStateFlowFactory(
|
class RootNavStateFlowFactory(
|
||||||
private val authenticationService: MatrixAuthenticationService,
|
private val sessionStore: SessionStore,
|
||||||
private val cacheService: CacheService,
|
private val cacheService: CacheService,
|
||||||
private val matrixSessionCache: MatrixSessionCache,
|
private val matrixSessionCache: MatrixSessionCache,
|
||||||
private val imageLoaderHolder: ImageLoaderHolder,
|
private val imageLoaderHolder: ImageLoaderHolder,
|
||||||
|
|
@ -39,7 +39,7 @@ class RootNavStateFlowFactory(
|
||||||
fun create(savedStateMap: SavedStateMap?): Flow<RootNavState> {
|
fun create(savedStateMap: SavedStateMap?): Flow<RootNavState> {
|
||||||
return combine(
|
return combine(
|
||||||
cacheIndexFlow(savedStateMap),
|
cacheIndexFlow(savedStateMap),
|
||||||
authenticationService.loggedInStateFlow(),
|
sessionStore.loggedInStateFlow(),
|
||||||
) { cacheIndex, loggedInState ->
|
) { cacheIndex, loggedInState ->
|
||||||
RootNavState(
|
RootNavState(
|
||||||
cacheIndex = cacheIndex,
|
cacheIndex = cacheIndex,
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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_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_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>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -501,22 +501,16 @@ class LoggedInPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - CheckSlidingSyncProxyAvailability forces the sliding sync migration under the right circumstances`() = runTest {
|
fun `present - CheckSlidingSyncProxyAvailability forces the sliding sync migration under the right circumstances`() = runTest {
|
||||||
// The migration will be forced if:
|
// The migration will be forced if the user is not using the native sliding sync
|
||||||
// - The user is not using the native sliding sync
|
|
||||||
// - The sliding sync proxy is no longer supported
|
|
||||||
// - The native sliding sync is supported
|
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
currentSlidingSyncVersionLambda = { Result.success(SlidingSyncVersion.Proxy) },
|
currentSlidingSyncVersionLambda = { Result.success(SlidingSyncVersion.Proxy) },
|
||||||
availableSlidingSyncVersionsLambda = { Result.success(listOf(SlidingSyncVersion.Native)) },
|
|
||||||
)
|
)
|
||||||
createLoggedInPresenter(
|
createLoggedInPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
).test {
|
).test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
|
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
|
||||||
|
|
||||||
initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
|
initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
|
||||||
|
|
||||||
assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue()
|
assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,4 @@ package io.element.android.features.analytics.api
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||||
|
|
||||||
interface AnalyticsEntryPoint : SimpleFeatureEntryPoint
|
fun interface AnalyticsEntryPoint : SimpleFeatureEntryPoint
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
|
@ -30,13 +31,7 @@ dependencies {
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(libs.androidx.browser)
|
implementation(libs.androidx.browser)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs)
|
||||||
testImplementation(libs.coroutines.test)
|
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(libs.test.mockk)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.services.analytics.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.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
|
@ -22,8 +23,5 @@ dependencies {
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs)
|
||||||
testImplementation(libs.coroutines.test)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(projects.tests.testutils)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ interface ElementCallEntryPoint {
|
||||||
* @param senderName The name of the sender of the event that started the call.
|
* @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 avatarUrl The avatar url of the room or DM.
|
||||||
* @param timestamp The timestamp of the event that started the call.
|
* @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 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.
|
* @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?,
|
senderName: String?,
|
||||||
avatarUrl: String?,
|
avatarUrl: String?,
|
||||||
timestamp: Long,
|
timestamp: Long,
|
||||||
|
expirationTimestamp: Long,
|
||||||
notificationChannelId: String,
|
notificationChannelId: String,
|
||||||
textContent: String?,
|
textContent: String?,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import extension.buildConfigFieldStr
|
import extension.buildConfigFieldStr
|
||||||
import extension.readLocalProperty
|
import extension.readLocalProperty
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2024 New Vector Ltd.
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
|
@ -87,12 +88,7 @@ dependencies {
|
||||||
implementation(libs.element.call.embedded)
|
implementation(libs.element.call.embedded)
|
||||||
api(projects.features.call.api)
|
api(projects.features.call.api)
|
||||||
|
|
||||||
testImplementation(libs.coroutines.test)
|
testCommonDependencies(libs, true)
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(libs.test.robolectric)
|
|
||||||
testImplementation(libs.test.mockk)
|
|
||||||
testImplementation(projects.features.call.test)
|
testImplementation(projects.features.call.test)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
|
|
@ -100,7 +96,5 @@ dependencies {
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.services.toolbox.test)
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ class DefaultElementCallEntryPoint(
|
||||||
senderName: String?,
|
senderName: String?,
|
||||||
avatarUrl: String?,
|
avatarUrl: String?,
|
||||||
timestamp: Long,
|
timestamp: Long,
|
||||||
|
expirationTimestamp: Long,
|
||||||
notificationChannelId: String,
|
notificationChannelId: String,
|
||||||
textContent: String?,
|
textContent: String?,
|
||||||
) {
|
) {
|
||||||
|
|
@ -55,6 +56,7 @@ class DefaultElementCallEntryPoint(
|
||||||
senderName = senderName,
|
senderName = senderName,
|
||||||
avatarUrl = avatarUrl,
|
avatarUrl = avatarUrl,
|
||||||
timestamp = timestamp,
|
timestamp = timestamp,
|
||||||
|
expirationTimestamp = expirationTimestamp,
|
||||||
notificationChannelId = notificationChannelId,
|
notificationChannelId = notificationChannelId,
|
||||||
textContent = textContent,
|
textContent = textContent,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,6 @@ data class CallNotificationData(
|
||||||
val notificationChannelId: String,
|
val notificationChannelId: String,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
val textContent: String?,
|
val textContent: String?,
|
||||||
|
// Expiration timestamp in millis since epoch
|
||||||
|
val expirationTimestamp: Long,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ class RingingCallNotificationCreator(
|
||||||
roomAvatarUrl: String?,
|
roomAvatarUrl: String?,
|
||||||
notificationChannelId: String,
|
notificationChannelId: String,
|
||||||
timestamp: Long,
|
timestamp: Long,
|
||||||
|
expirationTimestamp: Long,
|
||||||
textContent: String?,
|
textContent: String?,
|
||||||
): Notification? {
|
): Notification? {
|
||||||
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
|
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
|
||||||
|
|
@ -88,6 +89,7 @@ class RingingCallNotificationCreator(
|
||||||
notificationChannelId = notificationChannelId,
|
notificationChannelId = notificationChannelId,
|
||||||
timestamp = timestamp,
|
timestamp = timestamp,
|
||||||
textContent = textContent,
|
textContent = textContent,
|
||||||
|
expirationTimestamp = expirationTimestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
val declineIntent = PendingIntentCompat.getBroadcast(
|
val declineIntent = PendingIntentCompat.getBroadcast(
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ internal fun IncomingCallScreenPreview() = ElementPreview {
|
||||||
notificationChannelId = "incoming_call",
|
notificationChannelId = "incoming_call",
|
||||||
timestamp = 0L,
|
timestamp = 0L,
|
||||||
textContent = null,
|
textContent = null,
|
||||||
|
expirationTimestamp = 1000L,
|
||||||
),
|
),
|
||||||
onAnswer = {},
|
onAnswer = {},
|
||||||
onCancel = {},
|
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.NotificationIdProvider
|
||||||
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
|
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
|
||||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||||
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -53,7 +54,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the active call state.
|
* Manages the active call state.
|
||||||
|
|
@ -98,6 +99,7 @@ class DefaultActiveCallManager(
|
||||||
private val defaultCurrentCallService: DefaultCurrentCallService,
|
private val defaultCurrentCallService: DefaultCurrentCallService,
|
||||||
private val appForegroundStateService: AppForegroundStateService,
|
private val appForegroundStateService: AppForegroundStateService,
|
||||||
private val imageLoaderHolder: ImageLoaderHolder,
|
private val imageLoaderHolder: ImageLoaderHolder,
|
||||||
|
private val systemClock: SystemClock,
|
||||||
) : ActiveCallManager {
|
) : ActiveCallManager {
|
||||||
private val tag = "DefaultActiveCallManager"
|
private val tag = "DefaultActiveCallManager"
|
||||||
private var timedOutCallJob: Job? = null
|
private var timedOutCallJob: Job? = null
|
||||||
|
|
@ -118,8 +120,20 @@ class DefaultActiveCallManager(
|
||||||
|
|
||||||
override suspend fun registerIncomingCall(notificationData: CallNotificationData) {
|
override suspend fun registerIncomingCall(notificationData: CallNotificationData) {
|
||||||
mutex.withLock {
|
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)
|
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) {
|
if (activeCall.value != null) {
|
||||||
displayMissedCallNotification(notificationData)
|
displayMissedCallNotification(notificationData)
|
||||||
Timber.tag(tag).w("Already have an active call, ignoring incoming call: $notificationData")
|
Timber.tag(tag).w("Already have an active call, ignoring incoming call: $notificationData")
|
||||||
|
|
@ -138,14 +152,14 @@ class DefaultActiveCallManager(
|
||||||
showIncomingCallNotification(notificationData)
|
showIncomingCallNotification(notificationData)
|
||||||
|
|
||||||
// Wait for the ringing call to time out
|
// Wait for the ringing call to time out
|
||||||
delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds)
|
delay(timeMillis = ringDuration)
|
||||||
incomingCallTimedOut(displayMissedCallNotification = true)
|
incomingCallTimedOut(displayMissedCallNotification = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire a wake lock to keep the device awake during the incoming call, so we can process the room info data
|
// 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) {
|
if (activeWakeLock?.isHeld == false) {
|
||||||
Timber.tag(tag).d("Acquiring partial wakelock")
|
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 {
|
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")
|
Timber.tag(tag).w("Call type $callType does not match the active call type, ignoring")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (currentActiveCall.callState is CallState.Ringing) {
|
||||||
Timber.tag(tag).d("Hung up call: $callType")
|
// Decline the call
|
||||||
|
val notificationData = currentActiveCall.callState.notificationData
|
||||||
|
matrixClientProvider.getOrRestore(notificationData.sessionId).getOrNull()
|
||||||
|
?.getRoom(notificationData.roomId)
|
||||||
|
?.declineCall(notificationData.eventId)
|
||||||
|
}
|
||||||
|
|
||||||
cancelIncomingCallNotification()
|
cancelIncomingCallNotification()
|
||||||
if (activeWakeLock?.isHeld == true) {
|
if (activeWakeLock?.isHeld == true) {
|
||||||
|
|
@ -226,6 +250,7 @@ class DefaultActiveCallManager(
|
||||||
notificationChannelId = notificationData.notificationChannelId,
|
notificationChannelId = notificationData.notificationChannelId,
|
||||||
timestamp = notificationData.timestamp,
|
timestamp = notificationData.timestamp,
|
||||||
textContent = notificationData.textContent,
|
textContent = notificationData.textContent,
|
||||||
|
expirationTimestamp = notificationData.expirationTimestamp,
|
||||||
) ?: return
|
) ?: return
|
||||||
runCatchingExceptions {
|
runCatchingExceptions {
|
||||||
notificationManagerCompat.notify(
|
notificationManagerCompat.notify(
|
||||||
|
|
@ -256,6 +281,43 @@ class DefaultActiveCallManager(
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private fun observeRingingCall() {
|
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
|
// 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.
|
// has joined the call from another session.
|
||||||
activeCall
|
activeCall
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,14 @@ class DefaultCallWidgetProvider(
|
||||||
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
|
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
|
||||||
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
|
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
|
||||||
|
|
||||||
val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
|
val roomInfo = room.info()
|
||||||
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = isEncrypted, direct = room.isDm())
|
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(
|
val callUrl = room.generateWidgetWebViewUrl(
|
||||||
widgetSettings = widgetSettings,
|
widgetSettings = widgetSettings,
|
||||||
clientId = clientId,
|
clientId = clientId,
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ class DefaultElementCallEntryPointTest {
|
||||||
senderName = "senderName",
|
senderName = "senderName",
|
||||||
avatarUrl = "avatarUrl",
|
avatarUrl = "avatarUrl",
|
||||||
timestamp = 0,
|
timestamp = 0,
|
||||||
|
expirationTimestamp = 0,
|
||||||
notificationChannelId = "notificationChannelId",
|
notificationChannelId = "notificationChannelId",
|
||||||
textContent = "textContent",
|
textContent = "textContent",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ class RingingCallNotificationCreatorTest {
|
||||||
roomAvatarUrl = "https://example.com/avatar.jpg",
|
roomAvatarUrl = "https://example.com/avatar.jpg",
|
||||||
notificationChannelId = "channelId",
|
notificationChannelId = "channelId",
|
||||||
timestamp = 0L,
|
timestamp = 0L,
|
||||||
|
expirationTimestamp = 20L,
|
||||||
textContent = "textContent",
|
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.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.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
|
||||||
|
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
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
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.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
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.ForegroundServiceType
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
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.FakeOnMissedCallNotificationHandler
|
||||||
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
|
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
|
||||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
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.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
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.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
|
@ -164,6 +171,102 @@ class DefaultActiveCallManagerTest {
|
||||||
verify { notificationManagerCompat.cancel(notificationId) }
|
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
|
@Test
|
||||||
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
|
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
|
||||||
setupShadowPowerManager()
|
setupShadowPowerManager()
|
||||||
|
|
@ -267,6 +370,83 @@ class DefaultActiveCallManagerTest {
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
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() {
|
private fun setupShadowPowerManager() {
|
||||||
shadowOf(InstrumentationRegistry.getInstrumentation().targetContext.getSystemService<PowerManager>()).apply {
|
shadowOf(InstrumentationRegistry.getInstrumentation().targetContext.getSystemService<PowerManager>()).apply {
|
||||||
setIsWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK, true)
|
setIsWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK, true)
|
||||||
|
|
@ -277,6 +457,7 @@ class DefaultActiveCallManagerTest {
|
||||||
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
|
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
|
||||||
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
|
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
|
||||||
|
systemClock: FakeSystemClock = FakeSystemClock(),
|
||||||
) = DefaultActiveCallManager(
|
) = DefaultActiveCallManager(
|
||||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
coroutineScope = backgroundScope,
|
coroutineScope = backgroundScope,
|
||||||
|
|
@ -292,5 +473,6 @@ class DefaultActiveCallManagerTest {
|
||||||
defaultCurrentCallService = DefaultCurrentCallService(),
|
defaultCurrentCallService = DefaultCurrentCallService(),
|
||||||
appForegroundStateService = FakeAppForegroundStateService(),
|
appForegroundStateService = FakeAppForegroundStateService(),
|
||||||
imageLoaderHolder = FakeImageLoaderHolder(),
|
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||||
|
systemClock = systemClock,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ fun aCallNotificationData(
|
||||||
avatarUrl: String? = AN_AVATAR_URL,
|
avatarUrl: String? = AN_AVATAR_URL,
|
||||||
notificationChannelId: String = "channel_id",
|
notificationChannelId: String = "channel_id",
|
||||||
timestamp: Long = 0L,
|
timestamp: Long = 0L,
|
||||||
|
expirationTimestamp: Long = 30_000L,
|
||||||
textContent: String? = null,
|
textContent: String? = null,
|
||||||
): CallNotificationData = CallNotificationData(
|
): CallNotificationData = CallNotificationData(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
|
@ -41,5 +42,6 @@ fun aCallNotificationData(
|
||||||
avatarUrl = avatarUrl,
|
avatarUrl = avatarUrl,
|
||||||
notificationChannelId = notificationChannelId,
|
notificationChannelId = notificationChannelId,
|
||||||
timestamp = timestamp,
|
timestamp = timestamp,
|
||||||
|
expirationTimestamp = expirationTimestamp,
|
||||||
textContent = textContent,
|
textContent = textContent,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ class FakeElementCallEntryPoint(
|
||||||
senderName: String?,
|
senderName: String?,
|
||||||
avatarUrl: String?,
|
avatarUrl: String?,
|
||||||
timestamp: Long,
|
timestamp: Long,
|
||||||
|
expirationTimestamp: Long,
|
||||||
notificationChannelId: String,
|
notificationChannelId: String,
|
||||||
textContent: 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.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
|
||||||
interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint {
|
fun interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint {
|
||||||
fun builder(parentNode: Node, buildContext: BuildContext): Builder
|
fun builder(parentNode: Node, buildContext: BuildContext): Builder
|
||||||
|
|
||||||
interface Builder {
|
interface Builder {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2025 New Vector Ltd.
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
|
@ -37,15 +38,7 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
|
|
||||||
|
testCommonDependencies(libs, true)
|
||||||
testImplementation(projects.services.analytics.test)
|
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.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
|
) : NodeInputs
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
|
private val presenter = presenterFactory.create(inputs.listType.toRoomMemberRole())
|
||||||
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 stateFlow = launchMolecule { presenter.present() }
|
private val stateFlow = launchMolecule { presenter.present() }
|
||||||
|
|
||||||
suspend fun waitForRoleChanged() {
|
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,
|
private val analyticsService: AnalyticsService,
|
||||||
) : Presenter<ChangeRolesState> {
|
) : Presenter<ChangeRolesState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
fun interface Factory {
|
||||||
fun create(role: RoomMember.Role): ChangeRolesPresenter
|
fun create(role: RoomMember.Role): ChangeRolesPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,16 +39,13 @@ class ChangeRoomMemberRolesRootNode(
|
||||||
roomComponentFactory: RoomComponentFactory,
|
roomComponentFactory: RoomComponentFactory,
|
||||||
) : ParentNode<ChangeRoomMemberRolesRootNode.NavTarget>(
|
) : ParentNode<ChangeRoomMemberRolesRootNode.NavTarget>(
|
||||||
navModel = PermanentNavModel(
|
navModel = PermanentNavModel(
|
||||||
navTargets = setOf(NavTarget.Root),
|
navTargets = setOf(NavTarget),
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
), DependencyInjectionGraphOwner, ChangeRoomMemberRolesEntryPoint.NodeProxy {
|
), DependencyInjectionGraphOwner, ChangeRoomMemberRolesEntryPoint.NodeProxy {
|
||||||
sealed interface NavTarget : Parcelable {
|
@Parcelize object NavTarget : Parcelable
|
||||||
@Parcelize
|
|
||||||
object Root : NavTarget
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val joinedRoom: JoinedRoom,
|
val joinedRoom: JoinedRoom,
|
||||||
|
|
@ -60,14 +57,10 @@ class ChangeRoomMemberRolesRootNode(
|
||||||
override val graph = roomComponentFactory.create(inputs.joinedRoom)
|
override val graph = roomComponentFactory.create(inputs.joinedRoom)
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return createNode<ChangeRolesNode>(
|
||||||
NavTarget.Root -> {
|
buildContext = buildContext,
|
||||||
createNode<ChangeRolesNode>(
|
plugins = listOf(ChangeRolesNode.Inputs(listType = inputs.listType)),
|
||||||
buildContext = buildContext,
|
)
|
||||||
plugins = listOf(ChangeRolesNode.Inputs(listType = inputs.listType)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.collections.plus
|
|
||||||
|
|
||||||
class ChangeRolesPresenterTest {
|
class ChangeRolesPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -556,18 +555,18 @@ class ChangeRolesPresenterTest {
|
||||||
users = pairs.associate { (userId, role) -> userId to role.powerLevel }.toPersistentMap()
|
users = pairs.associate { (userId, role) -> userId to role.powerLevel }.toPersistentMap()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private fun TestScope.createChangeRolesPresenter(
|
|
||||||
role: RoomMember.Role = RoomMember.Role.Admin,
|
internal fun TestScope.createChangeRolesPresenter(
|
||||||
room: FakeJoinedRoom = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})),
|
role: RoomMember.Role = RoomMember.Role.Admin,
|
||||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
room: FakeJoinedRoom = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})),
|
||||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
): ChangeRolesPresenter {
|
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||||
return ChangeRolesPresenter(
|
): ChangeRolesPresenter {
|
||||||
role = role,
|
return ChangeRolesPresenter(
|
||||||
room = room,
|
role = role,
|
||||||
dispatchers = dispatchers,
|
room = room,
|
||||||
analyticsService = analyticsService,
|
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.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2022-2024 New Vector Ltd.
|
* Copyright 2022-2024 New Vector Ltd.
|
||||||
|
|
@ -43,13 +44,7 @@ dependencies {
|
||||||
implementation(projects.features.invitepeople.api)
|
implementation(projects.features.invitepeople.api)
|
||||||
api(projects.features.createroom.api)
|
api(projects.features.createroom.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs, true)
|
||||||
testImplementation(libs.test.mockk)
|
|
||||||
testImplementation(libs.coroutines.test)
|
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(libs.test.robolectric)
|
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.mediapickers.test)
|
testImplementation(projects.libraries.mediapickers.test)
|
||||||
|
|
@ -58,7 +53,4 @@ dependencies {
|
||||||
testImplementation(projects.libraries.usersearch.test)
|
testImplementation(projects.libraries.usersearch.test)
|
||||||
testImplementation(projects.features.startchat.test)
|
testImplementation(projects.features.startchat.test)
|
||||||
testImplementation(projects.libraries.featureflag.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.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2024 New Vector Ltd.
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
|
@ -34,14 +35,6 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
api(projects.features.deactivation.api)
|
api(projects.features.deactivation.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs, true)
|
||||||
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)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.tests.testutils)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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">"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_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>
|
<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)
|
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.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2024 New Vector Ltd.
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
|
@ -22,8 +23,6 @@ dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
|
||||||
testImplementation(libs.coroutines.test)
|
testCommonDependencies(libs)
|
||||||
testImplementation(libs.test.junit)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,6 @@
|
||||||
|
|
||||||
package io.element.android.features.ftue.api
|
package io.element.android.features.ftue.api
|
||||||
|
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||||
import com.bumble.appyx.core.node.Node
|
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
|
||||||
|
|
||||||
interface FtueEntryPoint : FeatureEntryPoint {
|
interface FtueEntryPoint : SimpleFeatureEntryPoint
|
||||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
|
||||||
|
|
||||||
interface NodeBuilder {
|
|
||||||
fun build(): Node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
|
@ -46,14 +47,7 @@ dependencies {
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
implementation(projects.appconfig)
|
implementation(projects.appconfig)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs, true)
|
||||||
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)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.services.analytics.noop)
|
testImplementation(projects.services.analytics.noop)
|
||||||
|
|
@ -61,5 +55,4 @@ dependencies {
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.features.lockscreen.test)
|
testImplementation(projects.features.lockscreen.test)
|
||||||
testImplementation(projects.services.toolbox.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.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
|
|
@ -19,13 +18,7 @@ import io.element.android.libraries.architecture.createNode
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
@Inject
|
@Inject
|
||||||
class DefaultFtueEntryPoint : FtueEntryPoint {
|
class DefaultFtueEntryPoint : FtueEntryPoint {
|
||||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): FtueEntryPoint.NodeBuilder {
|
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||||
val plugins = ArrayList<Plugin>()
|
return parentNode.createNode<FtueFlowNode>(buildContext)
|
||||||
|
|
||||||
return object : FtueEntryPoint.NodeBuilder {
|
|
||||||
override fun build(): Node {
|
|
||||||
return parentNode.createNode<FtueFlowNode>(buildContext, plugins)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
resetPermissionLambda.assertions().isCalledOnce()
|
||||||
.with(value("android.permission.POST_NOTIFICATIONS"))
|
.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 onSessionConfirmRecoveryKeyClick()
|
||||||
fun onRoomSettingsClick(roomId: RoomId)
|
fun onRoomSettingsClick(roomId: RoomId)
|
||||||
fun onReportBugClick()
|
fun onReportBugClick()
|
||||||
fun onLogoutForNativeSlidingSyncMigrationNeeded()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2022-2024 New Vector Ltd.
|
* Copyright 2022-2024 New Vector Ltd.
|
||||||
|
|
@ -58,14 +59,7 @@ dependencies {
|
||||||
implementation(projects.libraries.previewutils)
|
implementation(projects.libraries.previewutils)
|
||||||
api(projects.features.home.api)
|
api(projects.features.home.api)
|
||||||
|
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
testCommonDependencies(libs, true)
|
||||||
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)
|
|
||||||
testImplementation(projects.features.invite.test)
|
testImplementation(projects.features.invite.test)
|
||||||
testImplementation(projects.features.logout.test)
|
testImplementation(projects.features.logout.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
|
|
@ -80,5 +74,4 @@ dependencies {
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.services.toolbox.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))
|
stateFlow.value.roomListState.eventSink(RoomListEvents.LeaveRoom(roomId, needsConfirmation = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rootNode(buildContext: BuildContext): Node {
|
private fun rootNode(buildContext: BuildContext): Node {
|
||||||
return node(buildContext) { modifier ->
|
return node(buildContext) { modifier ->
|
||||||
val state by stateFlow.collectAsState()
|
val state by stateFlow.collectAsState()
|
||||||
val activity = requireNotNull(LocalActivity.current)
|
val activity = requireNotNull(LocalActivity.current)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<string name="screen_home_tab_spaces">"Espaços"</string>
|
<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_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_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_decline_direct_chat_title">"Rejeitar conversa"</string>
|
||||||
<string name="screen_invites_empty_list">"Sem convites"</string>
|
<string name="screen_invites_empty_list">"Sem convites"</string>
|
||||||
<string name="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</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">"Convites"</string>
|
||||||
<string name="screen_roomlist_filter_invites_empty_state_title">"Não tens nenhum convite pendente."</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">"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_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_mixed_empty_state_title">"Não tens nenhuma conversa selecionada"</string>
|
||||||
<string name="screen_roomlist_filter_people">"Pessoas"</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()
|
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 androidx.compose.ui.Modifier
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
interface AcceptDeclineInviteView {
|
fun interface AcceptDeclineInviteView {
|
||||||
@Composable
|
@Composable
|
||||||
fun Render(
|
fun Render(
|
||||||
state: AcceptDeclineInviteState,
|
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.features.invite.api.InviteData
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
|
||||||
interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
|
fun interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
|
||||||
fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node
|
fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
|
@ -36,17 +37,9 @@ dependencies {
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.libraries.push.api)
|
implementation(projects.libraries.push.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs, true)
|
||||||
testImplementation(libs.coroutines.test)
|
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(libs.test.robolectric)
|
|
||||||
testImplementation(projects.features.invite.test)
|
testImplementation(projects.features.invite.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.services.analytics.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,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : Presenter<DeclineAndBlockState> {
|
) : Presenter<DeclineAndBlockState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
fun interface Factory {
|
||||||
fun create(inviteData: InviteData): DeclineAndBlockPresenter
|
fun create(inviteData: InviteData): DeclineAndBlockPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<string name="screen_decline_and_block_title">"Rejeitar e bloquear"</string>
|
<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_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_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_decline_direct_chat_title">"Rejeitar conversa"</string>
|
||||||
<string name="screen_invites_empty_list">"Sem convites"</string>
|
<string name="screen_invites_empty_list">"Sem convites"</string>
|
||||||
<string name="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</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.api.InviteData
|
||||||
import io.element.android.features.invite.impl.DeclineInvite
|
import io.element.android.features.invite.impl.DeclineInvite
|
||||||
import io.element.android.features.invite.impl.fake.FakeDeclineInvite
|
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.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -148,28 +148,16 @@ class DeclineAndBlockPresenterTest {
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(A_ROOM_ID), value(true), value(false), value(""))
|
.with(value(A_ROOM_ID), value(true), value(false), value(""))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private fun anInviteData(
|
|
||||||
roomId: RoomId = A_ROOM_ID,
|
internal fun createDeclineAndBlockPresenter(
|
||||||
name: String = A_ROOM_NAME,
|
inviteData: InviteData = anInviteData(),
|
||||||
isDm: Boolean = false,
|
declineInvite: DeclineInvite = FakeDeclineInvite(),
|
||||||
): InviteData {
|
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||||
return InviteData(
|
): DeclineAndBlockPresenter {
|
||||||
roomId = roomId,
|
return DeclineAndBlockPresenter(
|
||||||
roomName = name,
|
inviteData = inviteData,
|
||||||
isDm = isDm,
|
declineInvite = declineInvite,
|
||||||
)
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
}
|
)
|
||||||
|
|
||||||
private 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
|
package io.element.android.features.invitepeople.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
interface InvitePeopleState {
|
interface InvitePeopleState {
|
||||||
val canInvite: Boolean
|
val canInvite: Boolean
|
||||||
val isSearchActive: Boolean
|
val isSearchActive: Boolean
|
||||||
|
val sendInvitesAction: AsyncAction<Unit>
|
||||||
val eventSink: (InvitePeopleEvents) -> Unit
|
val eventSink: (InvitePeopleEvents) -> Unit
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,28 +8,33 @@
|
||||||
package io.element.android.features.invitepeople.api
|
package io.element.android.features.invitepeople.api
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
class InvitePeopleStateProvider : PreviewParameterProvider<InvitePeopleState> {
|
class InvitePeopleStateProvider : PreviewParameterProvider<InvitePeopleState> {
|
||||||
override val values: Sequence<InvitePeopleState>
|
override val values: Sequence<InvitePeopleState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aPreviewInvitePeopleState(),
|
aPreviewInvitePeopleState(),
|
||||||
aPreviewInvitePeopleState(canInvite = true),
|
aPreviewInvitePeopleState(canInvite = true),
|
||||||
aPreviewInvitePeopleState(isSearchActive = true)
|
aPreviewInvitePeopleState(isSearchActive = true),
|
||||||
|
aPreviewInvitePeopleState(sendInvitesAction = AsyncAction.Loading),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class PreviewInvitePeopleState(
|
private data class PreviewInvitePeopleState(
|
||||||
override val canInvite: Boolean,
|
override val canInvite: Boolean,
|
||||||
override val isSearchActive: Boolean,
|
override val isSearchActive: Boolean,
|
||||||
|
override val sendInvitesAction: AsyncAction<Unit>,
|
||||||
override val eventSink: (InvitePeopleEvents) -> Unit,
|
override val eventSink: (InvitePeopleEvents) -> Unit,
|
||||||
) : InvitePeopleState
|
) : InvitePeopleState
|
||||||
|
|
||||||
private fun aPreviewInvitePeopleState(
|
private fun aPreviewInvitePeopleState(
|
||||||
canInvite: Boolean = false,
|
canInvite: Boolean = false,
|
||||||
isSearchActive: Boolean = false,
|
isSearchActive: Boolean = false,
|
||||||
|
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
eventSink: (InvitePeopleEvents) -> Unit = {},
|
eventSink: (InvitePeopleEvents) -> Unit = {},
|
||||||
) = PreviewInvitePeopleState(
|
) = PreviewInvitePeopleState(
|
||||||
canInvite = canInvite,
|
canInvite = canInvite,
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
|
sendInvitesAction = sendInvitesAction,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2022-2024 New Vector Ltd.
|
* Copyright 2022-2024 New Vector Ltd.
|
||||||
|
|
@ -37,17 +38,8 @@ dependencies {
|
||||||
implementation(projects.services.apperror.api)
|
implementation(projects.services.apperror.api)
|
||||||
api(projects.features.invitepeople.api)
|
api(projects.features.invitepeople.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs, true)
|
||||||
testImplementation(libs.test.mockk)
|
|
||||||
testImplementation(libs.coroutines.test)
|
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(libs.test.robolectric)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.usersearch.test)
|
testImplementation(projects.libraries.usersearch.test)
|
||||||
testImplementation(projects.services.apperror.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.InvitePeopleEvents
|
||||||
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
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.AsyncData
|
||||||
import io.element.android.libraries.architecture.map
|
import io.element.android.libraries.architecture.map
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
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.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
@ -73,6 +75,8 @@ class DefaultInvitePeoplePresenter(
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val showSearchLoader = 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()) {
|
val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) {
|
||||||
if (joinedRoom == null) {
|
if (joinedRoom == null) {
|
||||||
val result = matrixClient.getJoinedRoom(roomId)
|
val result = matrixClient.getJoinedRoom(roomId)
|
||||||
|
|
@ -116,7 +120,7 @@ class DefaultInvitePeoplePresenter(
|
||||||
}
|
}
|
||||||
is InvitePeopleEvents.SendInvites -> {
|
is InvitePeopleEvents.SendInvites -> {
|
||||||
room.dataOrNull()?.let {
|
room.dataOrNull()?.let {
|
||||||
sessionCoroutineScope.sendInvites(it, selectedUsers.value)
|
sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is InvitePeopleEvents.CloseSearch -> {
|
is InvitePeopleEvents.CloseSearch -> {
|
||||||
|
|
@ -128,12 +132,13 @@ class DefaultInvitePeoplePresenter(
|
||||||
|
|
||||||
return DefaultInvitePeopleState(
|
return DefaultInvitePeopleState(
|
||||||
room = room.map { },
|
room = room.map { },
|
||||||
canInvite = selectedUsers.value.isNotEmpty(),
|
canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(),
|
||||||
selectedUsers = selectedUsers.value,
|
selectedUsers = selectedUsers.value,
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
isSearchActive = searchActive,
|
isSearchActive = searchActive,
|
||||||
searchResults = searchResults.value,
|
searchResults = searchResults.value,
|
||||||
showSearchLoader = showSearchLoader.value,
|
showSearchLoader = showSearchLoader.value,
|
||||||
|
sendInvitesAction = sendInvitesAction.value,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -141,16 +146,21 @@ class DefaultInvitePeoplePresenter(
|
||||||
private fun CoroutineScope.sendInvites(
|
private fun CoroutineScope.sendInvites(
|
||||||
room: JoinedRoom,
|
room: JoinedRoom,
|
||||||
selectedUsers: List<MatrixUser>,
|
selectedUsers: List<MatrixUser>,
|
||||||
|
sendInvitesAction: MutableState<AsyncAction<Unit>>,
|
||||||
) = launch {
|
) = launch {
|
||||||
val anyInviteFailed = selectedUsers
|
sendInvitesAction.runUpdatingState {
|
||||||
.map { room.inviteUserById(it.userId) }
|
val anyInviteFailed = selectedUsers
|
||||||
.any { it.isFailure }
|
.map { room.inviteUserById(it.userId) }
|
||||||
|
.any { it.isFailure }
|
||||||
|
|
||||||
if (anyInviteFailed) {
|
if (anyInviteFailed) {
|
||||||
appErrorStateService.showError(
|
appErrorStateService.showError(
|
||||||
titleRes = CommonStrings.common_unable_to_invite_title,
|
titleRes = CommonStrings.common_unable_to_invite_title,
|
||||||
bodyRes = CommonStrings.common_unable_to_invite_message,
|
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.InvitePeopleEvents
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
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.AsyncData
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
@ -22,5 +23,6 @@ data class DefaultInvitePeopleState(
|
||||||
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||||
val selectedUsers: ImmutableList<MatrixUser>,
|
val selectedUsers: ImmutableList<MatrixUser>,
|
||||||
override val isSearchActive: Boolean,
|
override val isSearchActive: Boolean,
|
||||||
|
override val sendInvitesAction: AsyncAction<Unit>,
|
||||||
override val eventSink: (InvitePeopleEvents) -> Unit
|
override val eventSink: (InvitePeopleEvents) -> Unit
|
||||||
) : InvitePeopleState
|
) : InvitePeopleState
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.invitepeople.impl
|
package io.element.android.features.invitepeople.impl
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
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.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
@ -68,6 +69,11 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider<Defau
|
||||||
showSearchLoader = true,
|
showSearchLoader = true,
|
||||||
),
|
),
|
||||||
aDefaultInvitePeopleState(room = AsyncData.Failure(Exception("Room not found"))),
|
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(),
|
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
||||||
isSearchActive: Boolean = false,
|
isSearchActive: Boolean = false,
|
||||||
showSearchLoader: Boolean = false,
|
showSearchLoader: Boolean = false,
|
||||||
|
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
): DefaultInvitePeopleState {
|
): DefaultInvitePeopleState {
|
||||||
return DefaultInvitePeopleState(
|
return DefaultInvitePeopleState(
|
||||||
room = room,
|
room = room,
|
||||||
|
|
@ -102,6 +109,7 @@ private fun aDefaultInvitePeopleState(
|
||||||
selectedUsers = selectedUsers,
|
selectedUsers = selectedUsers,
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
showSearchLoader = showSearchLoader,
|
showSearchLoader = showSearchLoader,
|
||||||
|
sendInvitesAction = sendInvitesAction,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -409,10 +409,23 @@ internal class DefaultInvitePeoplePresenterTest {
|
||||||
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||||
// Send invites
|
// Send invites
|
||||||
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
||||||
|
|
||||||
|
// Can't invite in the loading state
|
||||||
|
awaitItem().run {
|
||||||
|
assertThat(sendInvitesAction.isLoading()).isTrue()
|
||||||
|
assertThat(canInvite).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
delay(1_000)
|
delay(1_000)
|
||||||
inviteUserResult.assertions().isCalledOnce().with(
|
inviteUserResult.assertions().isCalledOnce().with(
|
||||||
value(selectedUser.userId)
|
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)
|
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||||
// Send invites
|
// Send invites
|
||||||
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
||||||
|
|
||||||
|
// Can't invite in the loading state
|
||||||
|
awaitItem().run {
|
||||||
|
assertThat(sendInvitesAction.isLoading()).isTrue()
|
||||||
|
assertThat(canInvite).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
delay(1_000)
|
delay(1_000)
|
||||||
inviteUserResult.assertions().isCalledOnce().with(
|
inviteUserResult.assertions().isCalledOnce().with(
|
||||||
value(selectedUser.userId)
|
value(selectedUser.userId)
|
||||||
|
|
@ -455,6 +475,12 @@ internal class DefaultInvitePeoplePresenterTest {
|
||||||
value(CommonStrings.common_unable_to_invite_title),
|
value(CommonStrings.common_unable_to_invite_title),
|
||||||
value(CommonStrings.common_unable_to_invite_message)
|
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.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2024 New Vector Ltd.
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
|
@ -38,16 +39,8 @@ dependencies {
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.appconfig)
|
implementation(projects.appconfig)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs, true)
|
||||||
testImplementation(libs.coroutines.test)
|
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.robolectric)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(projects.features.invite.test)
|
testImplementation(projects.features.invite.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.tests.testutils)
|
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ class JoinRoomPresenter(
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val seenInvitesStore: SeenInvitesStore,
|
private val seenInvitesStore: SeenInvitesStore,
|
||||||
) : Presenter<JoinRoomState> {
|
) : Presenter<JoinRoomState> {
|
||||||
interface Factory {
|
fun interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
roomId: RoomId,
|
roomId: RoomId,
|
||||||
roomIdOrAlias: RoomIdOrAlias,
|
roomIdOrAlias: RoomIdOrAlias,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
<string name="screen_join_room_knock_action">"Bater à porta"</string>
|
<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_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_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_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_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>
|
<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(
|
private fun aRoomDescription(
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
name: String? = A_ROOM_NAME,
|
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.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
|
|
@ -33,15 +34,7 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs, true)
|
||||||
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.libraries.matrix.test)
|
||||||
testImplementation(projects.tests.testutils)
|
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
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(acceptFailureLambda).isCalledOnce()
|
||||||
assert(acceptSuccessLambda).isCalledOnce()
|
assert(acceptSuccessLambda).isCalledOnce()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private fun TestScope.createKnockRequestsListPresenter(
|
|
||||||
canAccept: Boolean = true,
|
internal fun TestScope.createKnockRequestsListPresenter(
|
||||||
canDecline: Boolean = true,
|
canAccept: Boolean = true,
|
||||||
canBan: Boolean = true,
|
canDecline: Boolean = true,
|
||||||
knockRequestsFlow: Flow<List<KnockRequest>> = flowOf(emptyList())
|
canBan: Boolean = true,
|
||||||
): KnockRequestsListPresenter {
|
knockRequestsFlow: Flow<List<KnockRequest>> = flowOf(emptyList())
|
||||||
val knockRequestsService = KnockRequestsService(
|
): KnockRequestsListPresenter {
|
||||||
knockRequestsFlow = knockRequestsFlow,
|
val knockRequestsService = KnockRequestsService(
|
||||||
coroutineScope = backgroundScope,
|
knockRequestsFlow = knockRequestsFlow,
|
||||||
isKnockFeatureEnabledFlow = flowOf(true),
|
coroutineScope = backgroundScope,
|
||||||
permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)),
|
isKnockFeatureEnabledFlow = flowOf(true),
|
||||||
)
|
permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)),
|
||||||
return KnockRequestsListPresenter(knockRequestsService = knockRequestsService)
|
)
|
||||||
}
|
return KnockRequestsListPresenter(knockRequestsService = knockRequestsService)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
interface LeaveRoomRenderer {
|
fun interface LeaveRoomRenderer {
|
||||||
@Composable
|
@Composable
|
||||||
fun Render(
|
fun Render(
|
||||||
state: LeaveRoomState,
|
state: LeaveRoomState,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2022-2024 New Vector Ltd.
|
* Copyright 2022-2024 New Vector Ltd.
|
||||||
|
|
@ -26,13 +27,8 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.push.api)
|
implementation(projects.libraries.push.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs)
|
||||||
testImplementation(libs.coroutines.test)
|
|
||||||
testImplementation(libs.coroutines.core)
|
testImplementation(libs.coroutines.core)
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.tests.testutils)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,6 @@
|
||||||
|
|
||||||
package io.element.android.features.licenses.api
|
package io.element.android.features.licenses.api
|
||||||
|
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||||
import com.bumble.appyx.core.node.Node
|
|
||||||
|
|
||||||
interface OpenSourceLicensesEntryPoint {
|
interface OpenSourceLicensesEntryPoint : SimpleFeatureEntryPoint
|
||||||
fun getNode(node: Node, buildContext: BuildContext): Node
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2022-2024 New Vector Ltd.
|
* Copyright 2022-2024 New Vector Ltd.
|
||||||
|
|
@ -26,12 +27,8 @@ dependencies {
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
api(projects.features.licenses.api)
|
api(projects.features.licenses.api)
|
||||||
testImplementation(libs.test.junit)
|
|
||||||
testImplementation(libs.coroutines.test)
|
testCommonDependencies(libs)
|
||||||
testImplementation(libs.coroutines.core)
|
testImplementation(libs.coroutines.core)
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.tests.testutils)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import io.element.android.libraries.architecture.createNode
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
@Inject
|
@Inject
|
||||||
class DefaultOpenSourcesLicensesEntryPoint : OpenSourceLicensesEntryPoint {
|
class DefaultOpenSourcesLicensesEntryPoint : OpenSourceLicensesEntryPoint {
|
||||||
override fun getNode(node: Node, buildContext: BuildContext): Node {
|
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||||
return node.createNode<DependenciesFlowNode>(buildContext)
|
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 config.BuildTimeConfig
|
||||||
import extension.buildConfigFieldStr
|
import extension.buildConfigFieldStr
|
||||||
import extension.readLocalProperty
|
import extension.readLocalProperty
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
|
|
@ -70,6 +71,5 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs)
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2022-2024 New Vector Ltd.
|
* Copyright 2022-2024 New Vector Ltd.
|
||||||
|
|
@ -37,17 +38,10 @@ dependencies {
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(libs.accompanist.permission)
|
implementation(libs.accompanist.permission)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
testImplementation(libs.test.junit)
|
|
||||||
testImplementation(libs.coroutines.test)
|
testCommonDependencies(libs, true)
|
||||||
testImplementation(libs.molecule.runtime)
|
|
||||||
testImplementation(libs.test.robolectric)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.testtags)
|
testImplementation(projects.libraries.testtags)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.features.messages.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
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
||||||
interface PermissionsPresenter : Presenter<PermissionsState> {
|
interface PermissionsPresenter : Presenter<PermissionsState> {
|
||||||
interface Factory {
|
fun interface Factory {
|
||||||
fun create(permissions: List<String>): PermissionsPresenter
|
fun create(permissions: List<String>): PermissionsPresenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class SendLocationPresenter(
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
) : Presenter<SendLocationState> {
|
) : Presenter<SendLocationState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
fun interface Factory {
|
||||||
fun create(timelineMode: Timeline.Mode): SendLocationPresenter
|
fun create(timelineMode: Timeline.Mode): SendLocationPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@ import io.element.android.services.analytics.api.AnalyticsService
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
@Inject
|
@Inject
|
||||||
class ShowLocationNode(
|
class ShowLocationNode(
|
||||||
presenterFactory: ShowLocationPresenter.Factory,
|
|
||||||
analyticsService: AnalyticsService,
|
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
presenterFactory: ShowLocationPresenter.Factory,
|
||||||
|
analyticsService: AnalyticsService,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,14 @@ import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class ShowLocationPresenter(
|
class ShowLocationPresenter(
|
||||||
|
@Assisted private val location: Location,
|
||||||
|
@Assisted private val description: String?,
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
private val locationActions: LocationActions,
|
private val locationActions: LocationActions,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
@Assisted private val location: Location,
|
|
||||||
@Assisted private val description: String?
|
|
||||||
) : Presenter<ShowLocationState> {
|
) : Presenter<ShowLocationState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
fun interface Factory {
|
||||||
fun create(location: Location, description: String?): ShowLocationPresenter
|
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 {
|
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||||
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
|
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
|
||||||
},
|
},
|
||||||
fakeLocationActions,
|
locationActions = fakeLocationActions,
|
||||||
fakeBuildMeta,
|
buildMeta = fakeBuildMeta,
|
||||||
location,
|
location = location,
|
||||||
A_DESCRIPTION,
|
description = A_DESCRIPTION,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ package io.element.android.features.location.test
|
||||||
import io.element.android.features.location.api.LocationService
|
import io.element.android.features.location.api.LocationService
|
||||||
|
|
||||||
class FakeLocationService(
|
class FakeLocationService(
|
||||||
private val isServiceAvailable: Boolean,
|
private val isServiceAvailable: Boolean = false,
|
||||||
) : LocationService {
|
) : LocationService {
|
||||||
override fun isServiceAvailable() = isServiceAvailable
|
override fun isServiceAvailable() = isServiceAvailable
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
|
@ -44,21 +45,12 @@ dependencies {
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(libs.androidx.biometric)
|
implementation(libs.androidx.biometric)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs, true)
|
||||||
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)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.tests.testutils)
|
|
||||||
testImplementation(projects.libraries.cryptography.test)
|
testImplementation(projects.libraries.cryptography.test)
|
||||||
testImplementation(projects.libraries.cryptography.impl)
|
testImplementation(projects.libraries.cryptography.impl)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.libraries.sessionStorage.test)
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
testImplementation(projects.features.logout.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 {
|
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
logoutUseCase.logout(ignoreSdkError = true)
|
logoutUseCase.logoutAll(ignoreSdkError = true)
|
||||||
}.runCatchingUpdatingState(signOutAction)
|
}.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.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2022-2024 New Vector Ltd.
|
* Copyright 2022-2024 New Vector Ltd.
|
||||||
|
|
@ -48,14 +49,7 @@ dependencies {
|
||||||
implementation(libs.serialization.json)
|
implementation(libs.serialization.json)
|
||||||
api(projects.features.login.api)
|
api(projects.features.login.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs, true)
|
||||||
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)
|
|
||||||
testImplementation(projects.features.login.test)
|
testImplementation(projects.features.login.test)
|
||||||
testImplementation(projects.features.enterprise.test)
|
testImplementation(projects.features.enterprise.test)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
|
|
@ -63,6 +57,4 @@ dependencies {
|
||||||
testImplementation(projects.libraries.oidc.test)
|
testImplementation(projects.libraries.oidc.test)
|
||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
testImplementation(projects.libraries.wellknown.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">"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_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_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_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_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>
|
<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
|
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 {
|
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.
|
* @param ignoreSdkError if true, the SDK error will be ignored and the user will be logged out anyway.
|
||||||
*/
|
*/
|
||||||
suspend fun logout(ignoreSdkError: Boolean)
|
suspend fun logoutAll(ignoreSdkError: Boolean)
|
||||||
|
|
||||||
interface Factory {
|
|
||||||
fun create(sessionId: String): LogoutUseCase
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ package io.element.android.features.logout.api.direct
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
interface DirectLogoutView {
|
fun interface DirectLogoutView {
|
||||||
@Composable
|
@Composable
|
||||||
fun Render(state: DirectLogoutState)
|
fun Render(state: DirectLogoutState)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2022-2024 New Vector Ltd.
|
* Copyright 2022-2024 New Vector Ltd.
|
||||||
|
|
@ -35,15 +36,8 @@ dependencies {
|
||||||
implementation(projects.libraries.dateformatter.api)
|
implementation(projects.libraries.dateformatter.api)
|
||||||
api(projects.features.logout.api)
|
api(projects.features.logout.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testCommonDependencies(libs, true)
|
||||||
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)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.featureflag.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