Merge branch 'develop' into feature/fga/improve_node_architecture

This commit is contained in:
ganfra 2023-03-07 12:08:35 +01:00
commit 9d91fb06cc
463 changed files with 3133 additions and 1555 deletions

View file

@ -1,6 +1,7 @@
name: APK Build name: APK Build
on: on:
workflow_dispatch:
pull_request: { } pull_request: { }
push: push:
branches: [ main, develop ] branches: [ main, develop ]
@ -37,11 +38,13 @@ jobs:
app/build/outputs/apk/debug/*.apk app/build/outputs/apk/debug/*.apk
- uses: rnkdsh/action-upload-diawi@v1.3.1 - uses: rnkdsh/action-upload-diawi@v1.3.1
id: diawi id: diawi
if: ${{ github.event_name == 'pull_request' }}
with: with:
token: ${{ secrets.DIAWI_TOKEN }} token: ${{ secrets.DIAWI_TOKEN }}
file: app/build/outputs/apk/debug/app-arm64-v8a-debug.apk file: app/build/outputs/apk/debug/app-arm64-v8a-debug.apk
- name: Add or update PR comment with QR Code to download APK. - name: Add or update PR comment with QR Code to download APK.
uses: NejcZdovc/comment-pr@v1 if: ${{ github.event_name == 'pull_request' }}
uses: NejcZdovc/comment-pr@v2
with: with:
message: | message: |
:iphone: Scan the QR code below to install the build (arm64 only) for this PR. :iphone: Scan the QR code below to install the build (arm64 only) for this PR.

View file

@ -11,7 +11,7 @@ jobs:
- run: | - run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger - name: Danger
uses: danger/danger-js@11.2.3 uses: danger/danger-js@11.2.4
with: with:
args: "--dangerfile ./tools/danger/dangerfile.js" args: "--dangerfile ./tools/danger/dangerfile.js"
env: env:

View file

@ -38,7 +38,7 @@ jobs:
yarn add danger-plugin-lint-report --dev yarn add danger-plugin-lint-report --dev
- name: Danger lint - name: Danger lint
if: always() if: always()
uses: danger/danger-js@11.2.3 uses: danger/danger-js@11.2.4
with: with:
args: "--dangerfile ./tools/danger/dangerfile-lint.js" args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env: env:

View file

@ -1,5 +1,6 @@
appId: ${APP_ID} appId: ${APP_ID}
--- ---
- tapOn: "Change" - tapOn:
id: "login-change_server"
- takeScreenshot: build/maestro/200-ChangeServer - takeScreenshot: build/maestro/200-ChangeServer
- tapOn: "Continue" - tapOn: "Continue"

View file

@ -5,17 +5,14 @@ appId: ${APP_ID}
- takeScreenshot: build/maestro/100-SignIn - takeScreenshot: build/maestro/100-SignIn
- runFlow: changeServer.yaml - runFlow: changeServer.yaml
- runFlow: ../assertions/assertLoginDisplayed.yaml - runFlow: ../assertions/assertLoginDisplayed.yaml
- tapOn: "Username or email" - tapOn:
# ios id: "login-email_username"
# - tapOn:
# id: "usernameTextField"
# index: 0
- inputText: ${USERNAME} - inputText: ${USERNAME}
- pressKey: Enter
- tapOn: "Password" - tapOn: "Password"
# iOS - tapOn:
#- tapOn: id: "login-password"
# id: "passwordTextField"
# index: 0
- inputText: ${PASSWORD} - inputText: ${PASSWORD}
- pressKey: Enter
- tapOn: "Continue" - tapOn: "Continue"
- runFlow: ../assertions/assertHomeDisplayed.yaml - runFlow: ../assertions/assertHomeDisplayed.yaml

View file

@ -10,7 +10,7 @@
ElementX Android is a [Matrix](https://matrix.org/) Android Client provided by [Element](https://element.io/). ElementX Android is a [Matrix](https://matrix.org/) Android Client provided by [Element](https://element.io/).
The application is a total rewrite of [Element-Android](https://github.com/vector-im/element-android) using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running Android 5+. The UI layer is written using Jetpack compose. The application is a total rewrite of [Element-Android](https://github.com/vector-im/element-android) using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running Android 6+. The UI layer is written using Jetpack compose.
<!--- TOC --> <!--- TOC -->

View file

@ -198,6 +198,9 @@ knit {
dependencies { dependencies {
allLibraries() allLibraries()
allFeatures() allFeatures()
implementation(projects.libraries.matrix.impl)
implementation(projects.libraries.dateformatter.impl)
implementation(projects.libraries.sessionStorage.impl)
implementation(projects.tests.uitests) implementation(projects.tests.uitests)
implementation(projects.anvilannotations) implementation(projects.anvilannotations)
implementation(projects.appnav) implementation(projects.appnav)
@ -221,5 +224,5 @@ dependencies {
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest) testImplementation(projects.libraries.matrix.test)
} }

View file

@ -18,8 +18,10 @@ package io.element.android.x
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@ -40,7 +42,7 @@ class MainActivity : NodeComponentActivity() {
setContent { setContent {
ElementTheme { ElementTheme {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
) { ) {
NodeHost(integrationPoint = appyxIntegrationPoint) { NodeHost(integrationPoint = appyxIntegrationPoint) {
MainNode(it, appBindings.mainDaggerComponentOwner()) MainNode(it, appBindings.mainDaggerComponentOwner())

View file

@ -30,8 +30,8 @@ import io.element.android.appnav.RootFlowNode
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.matrix.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.x.di.MainDaggerComponentsOwner import io.element.android.x.di.MainDaggerComponentsOwner
import io.element.android.x.di.RoomComponent import io.element.android.x.di.RoomComponent
import io.element.android.x.di.SessionComponent import io.element.android.x.di.SessionComponent

View file

@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
@SingleIn(RoomScope::class) @SingleIn(RoomScope::class)
@MergeSubcomponent(RoomScope::class) @MergeSubcomponent(RoomScope::class)

View file

@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@SingleIn(SessionScope::class) @SingleIn(SessionScope::class)
@MergeSubcomponent(SessionScope::class) @MergeSubcomponent(SessionScope::class)

View file

@ -18,8 +18,8 @@ package io.element.android.x.initializer
import android.content.Context import android.content.Context
import androidx.startup.Initializer import androidx.startup.Initializer
import io.element.android.libraries.matrix.tracing.TracingConfigurations import io.element.android.libraries.matrix.impl.tracing.setupTracing
import io.element.android.libraries.matrix.tracing.setupTracing import io.element.android.libraries.matrix.api.tracing.TracingConfigurations
import io.element.android.x.BuildConfig import io.element.android.x.BuildConfig
class MatrixInitializer : Initializer<Unit> { class MatrixInitializer : Initializer<Unit> {

View file

@ -28,7 +28,6 @@ plugins {
android { android {
namespace = "io.element.android.appnav" namespace = "io.element.android.appnav"
} }
dependencies { dependencies {
@ -47,7 +46,7 @@ dependencies {
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.tests.uitests) implementation(projects.tests.uitests)
@ -58,5 +57,5 @@ dependencies {
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest) testImplementation(projects.libraries.matrix.test)
} }

View file

@ -46,8 +46,8 @@ import io.element.android.libraries.architecture.nodeInputs
import io.element.android.libraries.architecture.nodeInputsProvider import io.element.android.libraries.architecture.nodeInputsProvider
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize

View file

@ -19,9 +19,10 @@ package io.element.android.appnav
import android.os.Bundle import android.os.Bundle
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -57,13 +58,13 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService:
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun restore(savedInstanceState: Bundle?) { fun restore(savedInstanceState: Bundle?) {
if (savedInstanceState == null || sessionIdsToMatrixClient.isNotEmpty()) return if (savedInstanceState == null || sessionIdsToMatrixClient.isNotEmpty()) return
val sessionIds = savedInstanceState.getSerializable(SAVE_INSTANCE_KEY) as? Array<SessionId> val userIds = savedInstanceState.getSerializable(SAVE_INSTANCE_KEY) as? Array<UserId>
if (sessionIds.isNullOrEmpty()) return if (userIds.isNullOrEmpty()) return
// Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs. // Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs.
runBlocking { runBlocking {
sessionIds.forEach { sessionId -> userIds.forEach { userId ->
Timber.v("Restore matrix session: $sessionId") Timber.v("Restore matrix session: $userId")
val matrixClient = authenticationService.restoreSession(sessionId) val matrixClient = authenticationService.restoreSession(userId)
if (matrixClient != null) { if (matrixClient != null) {
add(matrixClient) add(matrixClient)
} }

View file

@ -34,7 +34,7 @@ import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.nodeInputs import io.element.android.libraries.architecture.nodeInputs
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber

View file

@ -49,8 +49,9 @@ import io.element.android.libraries.architecture.nodeInputsProvider
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.tests.uitests.openShowkase import io.element.android.tests.uitests.openShowkase
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -100,7 +101,7 @@ class RootFlowNode @AssistedInject constructor(
} }
private fun switchToLoggedInFlow(sessionId: SessionId) { private fun switchToLoggedInFlow(sessionId: SessionId) {
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId = sessionId)) backstack.safeRoot(NavTarget.LoggedInFlow(sessionId))
} }
private fun switchToLogoutFlow() { private fun switchToLogoutFlow() {
@ -109,19 +110,19 @@ class RootFlowNode @AssistedInject constructor(
} }
private suspend fun tryToRestoreLatestSession( private suspend fun tryToRestoreLatestSession(
onSuccess: (SessionId) -> Unit = {}, onSuccess: (UserId) -> Unit = {},
onFailure: () -> Unit = {} onFailure: () -> Unit = {}
) { ) {
val latestKnownSessionId = authenticationService.getLatestSessionId() val latestKnownUserId = authenticationService.getLatestSessionId()
if (latestKnownSessionId == null) { if (latestKnownUserId == null) {
onFailure() onFailure()
return return
} }
if (matrixClientsHolder.knowSession(latestKnownSessionId)) { if (matrixClientsHolder.knowSession(latestKnownUserId)) {
onSuccess(latestKnownSessionId) onSuccess(latestKnownUserId)
return return
} }
val matrixClient = authenticationService.restoreSession(latestKnownSessionId) val matrixClient = authenticationService.restoreSession(UserId(latestKnownUserId.value))
if (matrixClient == null) { if (matrixClient == null) {
Timber.v("Failed to restore session...") Timber.v("Failed to restore session...")
onFailure() onFailure()
@ -141,6 +142,7 @@ class RootFlowNode @AssistedInject constructor(
fun openShowkase() { fun openShowkase() {
openShowkase(activity) openShowkase(activity)
} }
val state = presenter.present() val state = presenter.present()
RootView( RootView(
state = state, state = state,

View file

@ -19,7 +19,7 @@ package io.element.android.appnav
import io.element.android.features.rageshake.reporter.BugReporter import io.element.android.features.rageshake.reporter.BugReporter
import io.element.android.features.rageshake.reporter.BugReporterListener import io.element.android.features.rageshake.reporter.BugReporterListener
import io.element.android.features.rageshake.reporter.ReportType import io.element.android.features.rageshake.reporter.ReportType
import io.element.android.libraries.matrixtest.A_FAILURE_REASON import io.element.android.libraries.matrix.test.A_FAILURE_REASON
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View file

@ -60,7 +60,7 @@ allprojects {
config = files("$rootDir/tools/detekt/detekt.yml") config = files("$rootDir/tools/detekt/detekt.yml")
} }
dependencies { dependencies {
detektPlugins("com.twitter.compose.rules:detekt:0.0.26") detektPlugins("io.nlopez.compose.rules:detekt:0.1.2")
} }
// KtLint // KtLint
@ -214,11 +214,11 @@ koverMerged {
name = "Global minimum code coverage." name = "Global minimum code coverage."
target = kotlinx.kover.api.VerificationTarget.ALL target = kotlinx.kover.api.VerificationTarget.ALL
bound { bound {
minValue = 50 minValue = 55
// Setting a max value, so that if coverage is bigger, it means that we have to change minValue. // Setting a max value, so that if coverage is bigger, it means that we have to change minValue.
// For instance if we have minValue = 25 and maxValue = 30, and current code coverage is now 37.32%, update // For instance if we have minValue = 25 and maxValue = 30, and current code coverage is now 37.32%, update
// minValue to 35 and maxValue to 40. // minValue to 35 and maxValue to 40.
maxValue = 55 maxValue = 60
counter = kotlinx.kover.api.CounterType.INSTRUCTION counter = kotlinx.kover.api.CounterType.INSTRUCTION
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
} }
@ -303,3 +303,15 @@ tasks.register("runQualityChecks") {
} }
dependsOn(":app:knitCheck") dependsOn(":app:knitCheck")
} }
// Make sure to delete old screenshots before recording new ones
subprojects {
val snapshotsDir = File("${project.path}/src/test/snapshots")
val removeOldScreenshotsTask = tasks.register("removeOldSnapshots") {
onlyIf { snapshotsDir.exists() }
doFirst {
snapshotsDir.deleteRecursively()
}
}
tasks.findByName("recordPaparazzi")?.dependsOn(removeOldScreenshotsTask)
}

3
changelog.d/138.bugfix Normal file
View file

@ -0,0 +1,3 @@
Add consumer proguard rules for SQLCipher.
Thanks @anoadragon453 for reporting and the tentative fix!

1
changelog.d/84.feature Normal file
View file

@ -0,0 +1 @@
Store session data in a secure storage.

1
changelog.d/88.bugfix Normal file
View file

@ -0,0 +1 @@
Fix designs in sign in and change server flows

View file

@ -36,7 +36,7 @@ dependencies {
anvil(projects.anvilcodegen) anvil(projects.anvilcodegen)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources) implementation(projects.libraries.elementresources)
implementation(projects.libraries.testtags) implementation(projects.libraries.testtags)
@ -48,7 +48,7 @@ dependencies {
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest) testImplementation(projects.libraries.matrix.test)
androidTestImplementation(libs.test.junitext) androidTestImplementation(libs.test.junitext)
} }

View file

@ -19,4 +19,5 @@ package io.element.android.features.login.impl.changeserver
sealed interface ChangeServerEvents { sealed interface ChangeServerEvents {
data class SetServer(val server: String) : ChangeServerEvents data class SetServer(val server: String) : ChangeServerEvents
object Submit : ChangeServerEvents object Submit : ChangeServerEvents
object ClearError : ChangeServerEvents
} }

View file

@ -16,14 +16,20 @@
package io.element.android.features.login.impl.changeserver package io.element.android.features.login.impl.changeserver
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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 com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.util.LoginConstants
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@ -32,18 +38,25 @@ class ChangeServerNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: ChangeServerPresenter, private val presenter: ChangeServerPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private fun onSuccess() { private fun onSuccess() {
navigateUp() navigateUp()
} }
private fun openLearnMorePage(context: Context) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL))
tryOrNull { context.startActivity(intent) }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
val context = LocalContext.current
ChangeServerView( ChangeServerView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onChangeServerSuccess = this::onSuccess, onChangeServerSuccess = this::onSuccess,
onBackPressed = { navigateUp() },
onLearnMoreClicked = { openLearnMorePage(context) },
) )
} }
} }

View file

@ -22,12 +22,15 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.login.util.LoginConstants
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute import io.element.android.libraries.architecture.execute
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URL
import javax.inject.Inject import javax.inject.Inject
class ChangeServerPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : Presenter<ChangeServerState> { class ChangeServerPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : Presenter<ChangeServerState> {
@ -35,8 +38,9 @@ class ChangeServerPresenter @Inject constructor(private val authenticationServic
@Composable @Composable
override fun present(): ChangeServerState { override fun present(): ChangeServerState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val homeserver = rememberSaveable { val homeserver = rememberSaveable {
mutableStateOf(authenticationService.getHomeserverOrDefault()) mutableStateOf(authenticationService.getHomeserverDetails().value?.url ?: LoginConstants.DEFAULT_HOMESERVER_URL)
} }
val changeServerAction: MutableState<Async<Unit>> = remember { val changeServerAction: MutableState<Async<Unit>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(Async.Uninitialized)
@ -45,7 +49,10 @@ class ChangeServerPresenter @Inject constructor(private val authenticationServic
fun handleEvents(event: ChangeServerEvents) { fun handleEvents(event: ChangeServerEvents) {
when (event) { when (event) {
is ChangeServerEvents.SetServer -> homeserver.value = event.server is ChangeServerEvents.SetServer -> homeserver.value = event.server
ChangeServerEvents.Submit -> localCoroutineScope.submit(homeserver.value, changeServerAction) ChangeServerEvents.Submit -> {
localCoroutineScope.submit(homeserver, changeServerAction)
}
ChangeServerEvents.ClearError -> changeServerAction.value = Async.Uninitialized
} }
} }
@ -56,9 +63,11 @@ class ChangeServerPresenter @Inject constructor(private val authenticationServic
) )
} }
private fun CoroutineScope.submit(homeserver: String, changeServerAction: MutableState<Async<Unit>>) = launch { private fun CoroutineScope.submit(homeserverUrl: MutableState<String>, changeServerAction: MutableState<Async<Unit>>) = launch {
suspend { suspend {
authenticationService.setHomeserver(homeserver) val domain = tryOrNull { URL(homeserverUrl.value) }?.host ?: homeserverUrl.value
authenticationService.setHomeserver(domain)
homeserverUrl.value = domain
}.execute(changeServerAction) }.execute(changeServerAction)
} }
} }

View file

@ -19,164 +19,240 @@ package io.element.android.features.login.impl.changeserver
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.login.R import io.element.android.features.login.R
import io.element.android.features.login.impl.error.changeServerError
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.theme.components.BackButton
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import org.matrix.rustcomponents.sdk.AuthenticationException
import io.element.android.libraries.ui.strings.R as StringR
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ChangeServerView( fun ChangeServerView(
state: ChangeServerState, state: ChangeServerState,
onLearnMoreClicked: () -> Unit,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onChangeServerSuccess: () -> Unit = {}, onChangeServerSuccess: () -> Unit = {},
) { ) {
val eventSink = state.eventSink val eventSink = state.eventSink
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Box( val interactionEnabled by remember(state.changeServerAction) {
modifier = modifier derivedStateOf {
.fillMaxSize() state.changeServerAction !is Async.Loading
.systemBarsPadding()
.imePadding()
) {
Column(
modifier = Modifier
.verticalScroll(
state = scrollState,
)
.padding(horizontal = 16.dp)
) {
val isError = state.changeServerAction is Async.Failure
Box(
modifier = Modifier
.padding(top = 99.dp)
.size(width = 81.dp, height = 73.dp)
.align(Alignment.CenterHorizontally)
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(32.dp)
)
) {
Icon(
modifier = Modifier
.align(Alignment.Center)
.size(width = 48.dp, height = 48.dp),
// TODO Update with design input
resourceId = R.drawable.ic_baseline_dataset_24,
contentDescription = "",
)
}
Text(
text = "Your server",
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 56.dp)
.align(Alignment.CenterHorizontally)
.padding(top = 38.dp),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
fontSize = 24.sp,
color = MaterialTheme.colorScheme.primary,
)
Text(
text = "A server is a home for all your data.\n" +
"You choose your server and its easy to make one.", // TODO "Learn more.",
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
.padding(top = 16.dp),
textAlign = TextAlign.Center,
fontSize = 16.sp,
color = MaterialTheme.colorScheme.secondary,
)
var homeserverFieldState by textFieldState(stateValue = state.homeserver)
OutlinedTextField(
value = homeserverFieldState,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.changeServerServer)
.padding(top = 200.dp),
onValueChange = {
homeserverFieldState = it
eventSink(ChangeServerEvents.SetServer(it))
},
label = {
Text(text = "Server")
},
isError = isError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = { eventSink(ChangeServerEvents.Submit) }
)
)
if (state.changeServerAction is Async.Failure) {
Text(
text = changeServerError(
state.homeserver,
state.changeServerAction.error
),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp)
)
}
Button(
onClick = { eventSink(ChangeServerEvents.Submit) },
enabled = state.submitEnabled,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.changeServerContinue)
.padding(top = 44.dp)
) {
Text(text = "Continue")
}
if (state.changeServerAction is Async.Success) {
onChangeServerSuccess()
}
}
if (state.changeServerAction is Async.Loading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
} }
} }
val focusManager = LocalFocusManager.current
Scaffold(
topBar = {
TopAppBar(
title = {},
navigationIcon = { BackButton(action = onBackPressed, enabled = interactionEnabled) }
)
}
) { padding ->
Box(
modifier = modifier
.fillMaxSize()
.imePadding()
.padding(padding)
) {
Column(
modifier = Modifier
.verticalScroll(
state = scrollState,
)
.padding(horizontal = 16.dp)
) {
Spacer(Modifier.height(42.dp))
Box(
modifier = Modifier
.size(width = 70.dp, height = 70.dp)
.align(Alignment.CenterHorizontally)
.background(
color = LocalColors.current.quinary,
shape = RoundedCornerShape(14.dp)
)
) {
Icon(
modifier = Modifier
.align(Alignment.Center)
.size(width = 32.dp, height = 32.dp),
tint = MaterialTheme.colorScheme.secondary,
resourceId = R.drawable.ic_homeserver,
contentDescription = "",
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = StringR.string.ftue_auth_choose_server_title),
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center,
style = ElementTextStyles.Bold.title2,
color = MaterialTheme.colorScheme.primary,
)
Spacer(Modifier.height(8.dp))
Text(
text = stringResource(id = StringR.string.ex_choose_server_subtitle),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textAlign = TextAlign.Center,
style = ElementTextStyles.Regular.subheadline,
color = MaterialTheme.colorScheme.secondary,
)
Spacer(Modifier.height(24.dp))
Text(
stringResource(StringR.string.hs_url),
style = ElementTextStyles.Regular.formHeader,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
var homeserverFieldState by textFieldState(stateValue = state.homeserver)
TextField(
value = homeserverFieldState,
readOnly = !interactionEnabled,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.changeServerServer)
.onTabOrEnterKeyFocusNext(focusManager),
onValueChange = {
homeserverFieldState = it
eventSink(ChangeServerEvents.SetServer(it))
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = { eventSink(ChangeServerEvents.Submit) }
),
singleLine = true,
maxLines = 1,
trailingIcon = if (homeserverFieldState.isNotEmpty()) {
{
IconButton(onClick = {
homeserverFieldState = ""
}, enabled = interactionEnabled) {
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(StringR.string.action_clear))
}
}
} else null,
)
if (state.changeServerAction is Async.Failure) {
if (state.changeServerAction.error is AuthenticationException.SlidingSyncNotAvailable) {
SlidingSyncNotSupportedDialog(onLearnMoreClicked = {
onLearnMoreClicked()
eventSink(ChangeServerEvents.ClearError)
}, onDismiss = {
eventSink(ChangeServerEvents.ClearError)
})
} else {
ChangeServerErrorDialog(
error = state.changeServerAction.error,
onDismiss = {
eventSink(ChangeServerEvents.ClearError)
}
)
}
}
Spacer(Modifier.height(8.dp))
Text(
text = stringResource(StringR.string.server_selection_server_footer),
modifier = Modifier.padding(horizontal = 16.dp),
style = ElementTextStyles.Regular.caption1,
textAlign = TextAlign.Start,
color = MaterialTheme.colorScheme.tertiary,
)
Spacer(Modifier.height(32.dp))
Button(
onClick = { eventSink(ChangeServerEvents.Submit) },
enabled = interactionEnabled && state.submitEnabled,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.changeServerContinue)
) {
Text(text = stringResource(id = StringR.string.login_continue), style = ElementTextStyles.Button)
}
if (state.changeServerAction is Async.Success) {
onChangeServerSuccess()
}
}
if (state.changeServerAction is Async.Loading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
@Composable
internal fun ChangeServerErrorDialog(error: Throwable, onDismiss: () -> Unit) {
ErrorDialog(
content = error.localizedMessage ?: stringResource(id = StringR.string.unknown_error),
onDismiss = onDismiss
)
}
@Composable
internal fun SlidingSyncNotSupportedDialog(onLearnMoreClicked: () -> Unit, onDismiss: () -> Unit) {
ConfirmationDialog(
onDismiss = onDismiss,
submitText = stringResource(StringR.string.action_learn_more),
onSubmitClicked = onLearnMoreClicked,
title = stringResource(StringR.string.server_selection_sliding_sync_alert_title),
content = stringResource(StringR.string.server_selection_sliding_sync_alert_message),
)
} }
@Preview @Preview
@ -191,5 +267,5 @@ internal fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProv
@Composable @Composable
private fun ContentToPreview(state: ChangeServerState) { private fun ContentToPreview(state: ChangeServerState) {
ChangeServerView(state = state) ChangeServerView(state = state, onBackPressed = {}, onLearnMoreClicked = {})
} }

View file

@ -17,8 +17,8 @@
package io.element.android.features.login.impl.root package io.element.android.features.login.impl.root
sealed interface LoginRootEvents { sealed interface LoginRootEvents {
object RefreshHomeServer : LoginRootEvents
data class SetLogin(val login: String) : LoginRootEvents data class SetLogin(val login: String) : LoginRootEvents
data class SetPassword(val password: String) : LoginRootEvents data class SetPassword(val password: String) : LoginRootEvents
object Submit : LoginRootEvents object Submit : LoginRootEvents
object ClearError : LoginRootEvents
} }

View file

@ -47,16 +47,11 @@ class LoginRootNode @AssistedInject constructor(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
OnLifecycleEvent { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> state.eventSink(LoginRootEvents.RefreshHomeServer)
else -> Unit
}
}
LoginRootScreen( LoginRootScreen(
state = state, state = state,
modifier = modifier, modifier = modifier,
onChangeServer = this::onChangeHomeServer, onChangeServer = this::onChangeHomeServer,
onBackPressed = this::navigateUp
) )
} }
} }

View file

@ -18,25 +18,30 @@ package io.element.android.features.login.impl.root
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.login.util.LoginConstants
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class LoginRootPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : Presenter<LoginRootState> { class LoginRootPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : Presenter<LoginRootState> {
private val defaultHomeserver = MatrixHomeServerDetails(LoginConstants.DEFAULT_HOMESERVER_URL, true, null)
@Composable @Composable
override fun present(): LoginRootState { override fun present(): LoginRootState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val homeserver = rememberSaveable { val homeserver = authenticationService.getHomeserverDetails().collectAsState().value ?: defaultHomeserver
mutableStateOf(authenticationService.getHomeserverOrDefault())
}
val loggedInState: MutableState<LoggedInState> = remember { val loggedInState: MutableState<LoggedInState> = remember {
mutableStateOf(LoggedInState.NotLoggedIn) mutableStateOf(LoggedInState.NotLoggedIn)
} }
@ -46,19 +51,19 @@ class LoginRootPresenter @Inject constructor(private val authenticationService:
fun handleEvents(event: LoginRootEvents) { fun handleEvents(event: LoginRootEvents) {
when (event) { when (event) {
LoginRootEvents.RefreshHomeServer -> refreshHomeServer(homeserver)
is LoginRootEvents.SetLogin -> updateFormState(formState) { is LoginRootEvents.SetLogin -> updateFormState(formState) {
copy(login = event.login) copy(login = event.login)
} }
is LoginRootEvents.SetPassword -> updateFormState(formState) { is LoginRootEvents.SetPassword -> updateFormState(formState) {
copy(password = event.password) copy(password = event.password)
} }
LoginRootEvents.Submit -> localCoroutineScope.submit(homeserver.value, formState.value, loggedInState) LoginRootEvents.Submit -> localCoroutineScope.submit(homeserver.url, formState.value, loggedInState)
LoginRootEvents.ClearError -> loggedInState.value = LoggedInState.NotLoggedIn
} }
} }
return LoginRootState( return LoginRootState(
homeserver = homeserver.value, homeserverDetails = homeserver,
loggedInState = loggedInState.value, loggedInState = loggedInState.value,
formState = formState.value, formState = formState.value,
eventSink = ::handleEvents eventSink = ::handleEvents
@ -83,7 +88,4 @@ class LoginRootPresenter @Inject constructor(private val authenticationService:
formState.value = updateLambda(formState.value) formState.value = updateLambda(formState.value)
} }
private fun refreshHomeServer(homeserver: MutableState<String>) {
homeserver.value = authenticationService.getHomeserverOrDefault()
}
} }

View file

@ -16,222 +16,332 @@
package io.element.android.features.login.impl.root package io.element.android.features.login.impl.root
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.features.login.impl.error.loginError import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.BackButton
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.R as StringR import io.element.android.libraries.ui.strings.R as StringR
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LoginRootScreen( fun LoginRootScreen(
state: LoginRootState, state: LoginRootState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onChangeServer: () -> Unit = {}, onChangeServer: () -> Unit = {},
onLoginWithSuccess: (SessionId) -> Unit = {}, onLoginWithSuccess: (SessionId) -> Unit = {},
onBackPressed: () -> Unit,
) { ) {
val eventSink = state.eventSink val interactionEnabled by remember(state.loggedInState) {
Box( derivedStateOf {
modifier = modifier state.loggedInState != LoggedInState.LoggingIn
.fillMaxSize() }
.systemBarsPadding() }
.imePadding() Scaffold(
) { topBar = {
val scrollState = rememberScrollState() TopAppBar(
var loginFieldState by textFieldState(stateValue = state.formState.login) title = {},
var passwordFieldState by textFieldState(stateValue = state.formState.password) navigationIcon = { BackButton(action = onBackPressed, enabled = interactionEnabled) },
Column(
modifier = Modifier
.verticalScroll(
state = scrollState,
)
.padding(horizontal = 16.dp),
) {
val isError = state.loggedInState is LoggedInState.ErrorLoggingIn
// Title
Text(
text = stringResource(id = StringR.string.ftue_auth_welcome_back_title),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 48.dp),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
fontSize = 24.sp,
color = MaterialTheme.colorScheme.primary,
) )
// Form }
Column( ) { padding ->
// modifier = Modifier.weight(1f), Box(
) { modifier = modifier
Box( .fillMaxSize()
modifier = Modifier.fillMaxWidth() .imePadding()
) { .padding(padding)
OutlinedTextField( ) {
value = state.homeserver, val scrollState = rememberScrollState()
modifier = Modifier.fillMaxWidth(),
onValueChange = { /* no op */ },
enabled = false,
label = {
Text(text = "Server")
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Uri,
),
)
Button(
onClick = onChangeServer,
modifier = Modifier
.align(Alignment.CenterEnd)
.testTag(TestTags.loginChangeServer)
.padding(top = 8.dp, end = 8.dp),
content = {
Text(text = "Change")
}
)
}
OutlinedTextField(
value = loginFieldState,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginEmailUsername)
.padding(top = 60.dp),
label = {
Text(text = stringResource(id = StringR.string.login_signin_username_hint))
},
onValueChange = {
loginFieldState = it
eventSink(LoginRootEvents.SetLogin(it))
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
)
var passwordVisible by remember { mutableStateOf(false) }
if (state.loggedInState is LoggedInState.LoggingIn) {
// Ensure password is hidden when user submits the form
passwordVisible = false
}
OutlinedTextField(
value = passwordFieldState,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginPassword)
.padding(top = 24.dp),
onValueChange = {
passwordFieldState = it
eventSink(LoginRootEvents.SetPassword(it))
},
label = {
Text(text = "Password")
},
isError = isError,
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
val image =
if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
val description =
if (passwordVisible) "Hide password" else "Show password"
IconButton(onClick = { passwordVisible = !passwordVisible }) { Column(
Icon(imageVector = image, description)
}
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = { eventSink(LoginRootEvents.Submit) }
),
)
if (state.loggedInState is LoggedInState.ErrorLoggingIn) {
Text(
text = loginError(state.formState, state.loggedInState.failure),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp)
)
}
}
// Submit
Button(
onClick = { eventSink(LoginRootEvents.Submit) },
enabled = state.submitEnabled,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .verticalScroll(state = scrollState)
.testTag(TestTags.loginContinue) .padding(horizontal = 16.dp),
.padding(vertical = 32.dp)
) { ) {
Text(text = "Continue") Spacer(Modifier.height(16.dp))
// Title
Text(
text = stringResource(id = StringR.string.ftue_auth_welcome_back_title),
modifier = Modifier
.fillMaxWidth(),
style = ElementTextStyles.Bold.title1,
color = MaterialTheme.colorScheme.primary,
)
Spacer(Modifier.height(32.dp))
ChangeServerSection(
interactionEnabled = interactionEnabled,
homeserver = state.homeserverDetails.url,
onChangeServer = onChangeServer
)
Spacer(Modifier.height(32.dp))
LoginForm(state = state, interactionEnabled = interactionEnabled)
Spacer(modifier = Modifier.height(32.dp))
} }
when (val loggedInState = state.loggedInState) { when (val loggedInState = state.loggedInState) {
is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId) is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId)
else -> Unit else -> Unit
} }
}
if (state.loggedInState is LoggedInState.LoggingIn) { if (state.loggedInState is LoggedInState.LoggingIn) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
}
} }
} }
} }
@Preview
@Composable @Composable
internal fun LoginRootScreenLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun ChangeServerSection(
interactionEnabled: Boolean,
@Preview homeserver: String,
@Composable onChangeServer: () -> Unit,
internal fun LoginRootScreenDarkPreview() = ElementPreviewDark { ContentToPreview() } modifier: Modifier = Modifier
) {
Column(modifier) {
Text(
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp),
text = stringResource(id = StringR.string.ftue_auth_sign_in_choose_server_header),
style = ElementTextStyles.Regular.formHeader,
)
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(14.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
.testTag(TestTags.loginChangeServer)
.clickable {
if (interactionEnabled) {
onChangeServer()
}
},
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = homeserver,
style = ElementTextStyles.Bold.body,
textAlign = TextAlign.Start,
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp, vertical = 16.dp)
)
IconButton(
modifier = Modifier.size(24.dp),
onClick = {
if (interactionEnabled) {
onChangeServer()
}
}
) {
Icon(imageVector = Icons.Default.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.tertiary)
}
Spacer(Modifier.width(8.dp))
}
}
}
@Composable @Composable
private fun ContentToPreview() { internal fun LoginForm(
LoginRootScreen( state: LoginRootState,
state = aLoginRootState().copy( interactionEnabled: Boolean,
homeserver = "matrix.org", modifier: Modifier = Modifier
), ) {
var loginFieldState by textFieldState(stateValue = state.formState.login)
var passwordFieldState by textFieldState(stateValue = state.formState.password)
val focusManager = LocalFocusManager.current
val eventSink = state.eventSink
Column(modifier) {
Text(
text = stringResource(StringR.string.login_form_title),
modifier = Modifier.padding(start = 16.dp),
style = ElementTextStyles.Regular.formHeader
)
Spacer(modifier = Modifier.height(8.dp))
TextField(
value = loginFieldState,
readOnly = !interactionEnabled,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginEmailUsername)
.onTabOrEnterKeyFocusNext(focusManager),
label = {
Text(text = stringResource(StringR.string.ex_login_username_hint))
},
onValueChange = {
loginFieldState = it
eventSink(LoginRootEvents.SetLogin(it))
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(onNext = {
focusManager.moveFocus(FocusDirection.Down)
}),
singleLine = true,
maxLines = 1,
trailingIcon = if (loginFieldState.isNotEmpty()) {
{
IconButton(onClick = {
loginFieldState = ""
}) {
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(StringR.string.action_clear))
}
}
} else null,
)
var passwordVisible by remember { mutableStateOf(false) }
if (state.loggedInState is LoggedInState.LoggingIn) {
// Ensure password is hidden when user submits the form
passwordVisible = false
}
Spacer(Modifier.height(20.dp))
TextField(
value = passwordFieldState,
readOnly = !interactionEnabled,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginPassword)
.onTabOrEnterKeyFocusNext(focusManager),
onValueChange = {
passwordFieldState = it
eventSink(LoginRootEvents.SetPassword(it))
},
label = {
Text(text = stringResource(StringR.string.login_signup_password_hint))
},
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
val image =
if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
val description =
if (passwordVisible) stringResource(StringR.string.login_hide_password) else stringResource(StringR.string.login_show_password)
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, description)
}
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = { eventSink(LoginRootEvents.Submit) }
),
singleLine = true,
maxLines = 1,
)
if (state.loggedInState is LoggedInState.ErrorLoggingIn) {
LoginErrorDialog(error = state.loggedInState.failure, onDismiss = {
eventSink(LoginRootEvents.ClearError)
})
}
Spacer(Modifier.height(28.dp))
// Submit
Button(
onClick = { eventSink(LoginRootEvents.Submit) },
enabled = interactionEnabled && state.submitEnabled,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginContinue)
) {
Text(text = stringResource(StringR.string.login_continue), style = ElementTextStyles.Button)
}
}
}
@Composable
internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
ErrorDialog(
content = error.localizedMessage ?: stringResource(id = StringR.string.unknown_error),
onDismiss = onDismiss
)
}
@Preview
@Composable
internal fun LoginRootScreenLightPreview(@PreviewParameter(LoginRootStateProvider::class) state: LoginRootState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
internal fun LoginRootScreenDarkPreview(@PreviewParameter(LoginRootStateProvider::class) state: LoginRootState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: LoginRootState) {
LoginRootScreen(
state = state,
onBackPressed = {}
) )
} }

View file

@ -17,11 +17,12 @@
package io.element.android.features.login.impl.root package io.element.android.features.login.impl.root
import android.os.Parcelable import android.os.Parcelable
import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
data class LoginRootState( data class LoginRootState(
val homeserver: String, val homeserverDetails: MatrixHomeServerDetails,
val loggedInState: LoggedInState, val loggedInState: LoggedInState,
val formState: LoginFormState, val formState: LoginFormState,
val eventSink: (LoginRootEvents) -> Unit val eventSink: (LoginRootEvents) -> Unit

View file

@ -16,8 +16,24 @@
package io.element.android.features.login.impl.root package io.element.android.features.login.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.core.SessionId
open class LoginRootStateProvider : PreviewParameterProvider<LoginRootState> {
override val values: Sequence<LoginRootState>
get() = sequenceOf(
aLoginRootState(),
aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("some-custom-server.com", true, null)),
aLoginRootState().copy(formState = LoginFormState("user", "pass")),
aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.LoggingIn),
aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.ErrorLoggingIn(Throwable())),
aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.LoggedIn(SessionId("1234"))),
)
}
fun aLoginRootState() = LoginRootState( fun aLoginRootState() = LoginRootState(
homeserver = "", homeserverDetails = MatrixHomeServerDetails("matrix.org", true, null),
loggedInState = LoggedInState.NotLoggedIn, loggedInState = LoggedInState.NotLoggedIn,
formState = LoginFormState.Default, formState = LoginFormState.Default,
eventSink = {} eventSink = {}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.util
object LoginConstants {
const val DEFAULT_HOMESERVER_URL = "matrix.org"
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
}

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="42dp"
android:height="42dp"
android:viewportWidth="42"
android:viewportHeight="42">
<group>
<clip-path
android:pathData="M0,0h42v42h-42z"/>
<path
android:pathData="M33.25,22.75H8.75C6.825,22.75 5.25,24.325 5.25,26.25V33.25C5.25,35.175 6.825,36.75 8.75,36.75H33.25C35.175,36.75 36.75,35.175 36.75,33.25V26.25C36.75,24.325 35.175,22.75 33.25,22.75ZM12.25,33.25C10.325,33.25 8.75,31.675 8.75,29.75C8.75,27.825 10.325,26.25 12.25,26.25C14.175,26.25 15.75,27.825 15.75,29.75C15.75,31.675 14.175,33.25 12.25,33.25ZM33.25,5.25H8.75C6.825,5.25 5.25,6.825 5.25,8.75V15.75C5.25,17.675 6.825,19.25 8.75,19.25H33.25C35.175,19.25 36.75,17.675 36.75,15.75V8.75C36.75,6.825 35.175,5.25 33.25,5.25ZM12.25,15.75C10.325,15.75 8.75,14.175 8.75,12.25C8.75,10.325 10.325,8.75 12.25,8.75C14.175,8.75 15.75,10.325 15.75,12.25C15.75,14.175 14.175,15.75 12.25,15.75Z"
android:fillColor="#737D8C"/>
</group>
</vector>

View file

@ -25,8 +25,11 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.changeserver.ChangeServerEvents import io.element.android.features.login.impl.changeserver.ChangeServerEvents
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrixtest.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrixtest.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -41,7 +44,23 @@ class ChangeServerPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER) assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER_URL)
assertThat(initialState.submitEnabled).isTrue()
}
}
@Test
fun `present - authentication service can provide a homeserver`() = runTest {
val presenter = ChangeServerPresenter(
FakeAuthenticationService().apply {
givenHomeserver(A_HOMESERVER.copy(url = A_HOMESERVER_URL_2))
},
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER_URL_2)
assertThat(initialState.submitEnabled).isTrue() assertThat(initialState.submitEnabled).isTrue()
} }
} }
@ -80,4 +99,70 @@ class ChangeServerPresenterTest {
assertThat(successState.changeServerAction).isInstanceOf(Async.Success::class.java) assertThat(successState.changeServerAction).isInstanceOf(Async.Success::class.java)
} }
} }
@Test
fun `present - submit parses URL`() = runTest {
val presenter = ChangeServerPresenter(
FakeAuthenticationService(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val longUrl = "https://matrix.org/.well-known/"
val initialState = awaitItem()
initialState.eventSink.invoke(ChangeServerEvents.SetServer(longUrl))
awaitItem()
initialState.eventSink.invoke(ChangeServerEvents.Submit)
val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isFalse()
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
awaitItem() // Skip changing the url to the parsed domain
val successState = awaitItem()
assertThat(successState.submitEnabled).isTrue()
assertThat(successState.changeServerAction).isInstanceOf(Async.Success::class.java)
assertThat(successState.homeserver).isEqualTo("matrix.org")
}
}
@Test
fun `present - submit fails`() = runTest {
val authServer = FakeAuthenticationService()
val presenter = ChangeServerPresenter(authServer)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
authServer.givenChangeServerError(Throwable())
initialState.eventSink.invoke(ChangeServerEvents.Submit)
val failureState = awaitItem()
assertThat(failureState.submitEnabled).isTrue()
assertThat(failureState.changeServerAction).isInstanceOf(Async.Failure::class.java)
}
}
@Test
fun `present - clear error`() = runTest {
val authenticationService = FakeAuthenticationService()
val presenter = ChangeServerPresenter(
authenticationService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
// Submit will return an error
authenticationService.givenChangeServerError(A_THROWABLE)
initialState.eventSink(ChangeServerEvents.Submit)
// Check an error was returned
val submittedState = awaitItem()
assertThat(submittedState.changeServerAction).isEqualTo(Async.Failure<Unit>(A_THROWABLE))
// Assert the error is then cleared
submittedState.eventSink(ChangeServerEvents.ClearError)
val clearedState = awaitItem()
assertThat(clearedState.changeServerAction).isEqualTo(Async.Uninitialized)
}
}
} }

View file

@ -26,14 +26,12 @@ import io.element.android.features.login.impl.root.LoggedInState
import io.element.android.features.login.impl.root.LoginFormState import io.element.android.features.login.impl.root.LoginFormState
import io.element.android.features.login.impl.root.LoginRootEvents import io.element.android.features.login.impl.root.LoginRootEvents
import io.element.android.features.login.impl.root.LoginRootPresenter import io.element.android.features.login.impl.root.LoginRootPresenter
import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrixtest.A_HOMESERVER import io.element.android.libraries.matrix.test.A_PASSWORD
import io.element.android.libraries.matrixtest.A_HOMESERVER_2 import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrixtest.A_PASSWORD import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrixtest.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrixtest.A_THROWABLE import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
import io.element.android.libraries.matrixtest.A_USER_NAME
import io.element.android.libraries.matrixtest.auth.FakeAuthenticationService
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -48,7 +46,7 @@ class LoginRootPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER) assertThat(initialState.homeserverDetails).isEqualTo(A_HOMESERVER)
assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
assertThat(initialState.formState).isEqualTo(LoginFormState.Default) assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
assertThat(initialState.submitEnabled).isFalse() assertThat(initialState.submitEnabled).isFalse()
@ -92,7 +90,7 @@ class LoginRootPresenterTest {
val submitState = awaitItem() val submitState = awaitItem()
assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn) assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn)
val loggedInState = awaitItem() val loggedInState = awaitItem()
assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.LoggedIn(SessionId(A_SESSION_ID))) assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.LoggedIn(A_SESSION_ID))
} }
} }
@ -120,7 +118,7 @@ class LoginRootPresenterTest {
} }
@Test @Test
fun `present - refresh server`() = runTest { fun `present - clear error`() = runTest {
val authenticationService = FakeAuthenticationService() val authenticationService = FakeAuthenticationService()
val presenter = LoginRootPresenter( val presenter = LoginRootPresenter(
authenticationService, authenticationService,
@ -129,11 +127,20 @@ class LoginRootPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER)
authenticationService.givenHomeserver(A_HOMESERVER_2) // Submit will return an error
initialState.eventSink.invoke(LoginRootEvents.RefreshHomeServer) authenticationService.givenLoginError(A_THROWABLE)
val refreshedState = awaitItem() initialState.eventSink(LoginRootEvents.Submit)
assertThat(refreshedState.homeserver).isEqualTo(A_HOMESERVER_2) awaitItem() // Skip LoggingIn state
// Check an error was returned
val submittedState = awaitItem()
assertThat(submittedState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
// Assert the error is then cleared
submittedState.eventSink(LoginRootEvents.ClearError)
val clearedState = awaitItem()
assertThat(clearedState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
} }
} }
} }

View file

@ -35,7 +35,7 @@ dependencies {
anvil(projects.anvilcodegen) anvil(projects.anvilcodegen)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.matrix) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources) implementation(projects.libraries.elementresources)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
@ -46,7 +46,7 @@ dependencies {
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest) testImplementation(projects.libraries.matrix.test)
androidTestImplementation(libs.test.junitext) androidTestImplementation(libs.test.junitext)
} }

View file

@ -24,7 +24,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute import io.element.android.libraries.architecture.execute
import io.element.android.libraries.matrix.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject

View file

@ -23,9 +23,9 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrixtest.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrixtest.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -34,7 +34,7 @@ class LogoutPreferencePresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = LogoutPreferencePresenter( val presenter = LogoutPreferencePresenter(
FakeMatrixClient(SessionId("sessionId")), FakeMatrixClient(A_SESSION_ID),
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
@ -47,7 +47,7 @@ class LogoutPreferencePresenterTest {
@Test @Test
fun `present - logout`() = runTest { fun `present - logout`() = runTest {
val presenter = LogoutPreferencePresenter( val presenter = LogoutPreferencePresenter(
FakeMatrixClient(SessionId("sessionId")), FakeMatrixClient(A_SESSION_ID),
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
@ -63,7 +63,7 @@ class LogoutPreferencePresenterTest {
@Test @Test
fun `present - logout with error`() = runTest { fun `present - logout with error`() = runTest {
val matrixClient = FakeMatrixClient(SessionId("sessionId")) val matrixClient = FakeMatrixClient(A_SESSION_ID)
val presenter = LogoutPreferencePresenter( val presenter = LogoutPreferencePresenter(
matrixClient, matrixClient,
) )

View file

@ -25,16 +25,5 @@ android {
} }
dependencies { dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest)
androidTestImplementation(libs.test.junitext)
} }

View file

@ -36,11 +36,11 @@ dependencies {
api(projects.features.messages.api) api(projects.features.messages.api)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.textcomposer) implementation(projects.libraries.textcomposer)
implementation(projects.libraries.dateformatter) implementation(projects.libraries.dateformatter.api)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.datetime) implementation(libs.datetime)
implementation(libs.accompanist.flowlayout) implementation(libs.accompanist.flowlayout)
@ -52,7 +52,8 @@ dependencies {
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.dateformatter.test)
androidTestImplementation(libs.test.junitext) androidTestImplementation(libs.test.junitext)
ksp(libs.showkase.processor) ksp(libs.showkase.processor)

View file

@ -36,7 +36,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View file

@ -21,7 +21,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.textcomposer.MessageComposerState import io.element.android.features.messages.impl.textcomposer.MessageComposerState
import io.element.android.features.messages.impl.timeline.TimelineState import io.element.android.features.messages.impl.timeline.TimelineState
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@Immutable @Immutable
data class MessagesState( data class MessagesState(

View file

@ -24,7 +24,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemList
import io.element.android.features.messages.impl.timeline.aTimelineState import io.element.android.features.messages.impl.timeline.aTimelineState
import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.libraries.textcomposer.MessageComposerMode
open class MessagesStateProvider : PreviewParameterProvider<MessagesState> { open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {

View file

@ -25,7 +25,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.core.data.toStableCharSequence import io.element.android.libraries.core.data.toStableCharSequence
import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View file

@ -16,7 +16,7 @@
package io.element.android.features.messages.impl.timeline package io.element.android.features.messages.impl.timeline
import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.api.core.EventId
sealed interface TimelineEvents { sealed interface TimelineEvents {
object LoadMore : TimelineEvents object LoadMore : TimelineEvents

View file

@ -26,9 +26,9 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn

View file

@ -18,8 +18,8 @@ package io.element.android.features.messages.impl.timeline
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable @Immutable

View file

@ -23,8 +23,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemReac
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf

View file

@ -53,8 +53,8 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element

View file

@ -17,7 +17,7 @@
package io.element.android.features.messages.impl.timeline.diff package io.element.android.features.messages.impl.timeline.diff
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
internal class MatrixTimelineItemsDiffCallback( internal class MatrixTimelineItemsDiffCallback(
private val oldList: List<MatrixTimelineItem>, private val oldList: List<MatrixTimelineItem>,

View file

@ -23,7 +23,7 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow

View file

@ -16,14 +16,14 @@
package io.element.android.features.messages.impl.timeline.factories.event package io.element.android.features.messages.impl.timeline.factories.event
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.util.toHtmlDocument import io.element.android.features.messages.impl.timeline.util.toHtmlDocument
import io.element.android.libraries.matrix.media.MediaResolver import io.element.android.libraries.matrix.api.media.MediaResolver
import org.matrix.rustcomponents.sdk.Message import org.matrix.rustcomponents.sdk.Message
import org.matrix.rustcomponents.sdk.MessageType import org.matrix.rustcomponents.sdk.MessageType
import javax.inject.Inject import javax.inject.Inject

View file

@ -22,7 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.ProfileTimelineDetails import org.matrix.rustcomponents.sdk.ProfileTimelineDetails
import javax.inject.Inject import javax.inject.Inject

View file

@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.timeline.factories.virtual
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
import io.element.android.libraries.dateformatter.DaySeparatorFormatter import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
import org.matrix.rustcomponents.sdk.VirtualTimelineItem import org.matrix.rustcomponents.sdk.VirtualTimelineItem
import javax.inject.Inject import javax.inject.Inject

View file

@ -21,7 +21,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemUnknownVirtualModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemUnknownVirtualModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import org.matrix.rustcomponents.sdk.VirtualTimelineItem import org.matrix.rustcomponents.sdk.VirtualTimelineItem
import javax.inject.Inject import javax.inject.Inject

View file

@ -20,7 +20,7 @@ import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@Immutable @Immutable
sealed interface TimelineItem { sealed interface TimelineItem {

View file

@ -16,7 +16,7 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.media.MediaResolver import io.element.android.libraries.matrix.api.media.MediaResolver
data class TimelineItemImageContent( data class TimelineItemImageContent(
val body: String, val body: String,

View file

@ -17,7 +17,7 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.media.MediaResolver import io.element.android.libraries.matrix.api.media.MediaResolver
open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineItemImageContent> { open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineItemImageContent> {
override val values: Sequence<TimelineItemImageContent> override val values: Sequence<TimelineItemImageContent>

View file

@ -22,18 +22,18 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.fixtures.aMessageEvent import io.element.android.features.messages.fixtures.aMessageEvent
import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.fixtures.aTimelineItemsFactory
import io.element.android.features.messages.impl.MessagesEvents import io.element.android.features.messages.impl.MessagesEvents
import io.element.android.features.messages.impl.MessagesPresenter import io.element.android.features.messages.impl.MessagesPresenter
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrixtest.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrixtest.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrixtest.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope

View file

@ -32,10 +32,10 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrixtest.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrixtest.A_MESSAGE import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrixtest.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrixtest.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest

View file

@ -21,10 +21,10 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemReac
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrixtest.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrixtest.A_MESSAGE import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrixtest.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrixtest.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
internal fun aMessageEvent( internal fun aMessageEvent(

View file

@ -16,7 +16,6 @@
package io.element.android.features.messages.fixtures package io.element.android.features.messages.fixtures
import io.element.android.features.messages.fakes.FakeDaySeparatorFormatter
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseMessageFactory import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseMessageFactory
@ -31,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemDaySeparatorFactory import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemDaySeparatorFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
internal fun aTimelineItemsFactory() = TimelineItemsFactory( internal fun aTimelineItemsFactory() = TimelineItemsFactory(
dispatchers = testCoroutineDispatchers(), dispatchers = testCoroutineDispatchers(),

View file

@ -27,12 +27,12 @@ import io.element.android.features.messages.impl.textcomposer.MessageComposerEve
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.textcomposer.MessageComposerState import io.element.android.features.messages.impl.textcomposer.MessageComposerState
import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.matrixtest.ANOTHER_MESSAGE import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE
import io.element.android.libraries.matrixtest.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrixtest.A_MESSAGE import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrixtest.A_REPLY import io.element.android.libraries.matrix.test.A_REPLY
import io.element.android.libraries.matrixtest.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrixtest.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest

View file

@ -25,9 +25,9 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.fixtures.aTimelineItemsFactory
import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.libraries.matrixtest.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrixtest.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrixtest.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test

View file

@ -50,7 +50,7 @@ dependencies {
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest) testImplementation(projects.libraries.matrix.test)
androidTestImplementation(libs.test.junitext) androidTestImplementation(libs.test.junitext)
ksp(libs.showkase.processor) ksp(libs.showkase.processor)

View file

@ -26,7 +26,7 @@ import io.element.android.features.logout.LogoutPreferencePresenter
import io.element.android.features.preferences.impl.root.PreferencesRootPresenter import io.element.android.features.preferences.impl.root.PreferencesRootPresenter
import io.element.android.features.rageshake.preferences.RageshakePreferencesPresenter import io.element.android.features.rageshake.preferences.RageshakePreferencesPresenter
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrixtest.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test

View file

@ -51,7 +51,7 @@ dependencies {
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest) testImplementation(projects.libraries.matrix.test)
testImplementation(libs.test.mockk) testImplementation(libs.test.mockk)
androidTestImplementation(libs.test.junitext) androidTestImplementation(libs.test.junitext)

View file

@ -25,7 +25,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.rageshake.crash.ui.A_CRASH_DATA import io.element.android.features.rageshake.crash.ui.A_CRASH_DATA
import io.element.android.features.rageshake.crash.ui.FakeCrashDataStore import io.element.android.features.rageshake.crash.ui.FakeCrashDataStore
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrixtest.A_FAILURE_REASON import io.element.android.libraries.matrix.test.A_FAILURE_REASON
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test

View file

@ -19,7 +19,7 @@ package io.element.android.features.rageshake.bugreport
import io.element.android.features.rageshake.reporter.BugReporter import io.element.android.features.rageshake.reporter.BugReporter
import io.element.android.features.rageshake.reporter.BugReporterListener import io.element.android.features.rageshake.reporter.BugReporterListener
import io.element.android.features.rageshake.reporter.ReportType import io.element.android.features.rageshake.reporter.ReportType
import io.element.android.libraries.matrixtest.A_FAILURE_REASON import io.element.android.libraries.matrix.test.A_FAILURE_REASON
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View file

@ -28,7 +28,7 @@ import io.element.android.features.rageshake.preferences.FakeRageShake
import io.element.android.features.rageshake.preferences.FakeRageshakeDataStore import io.element.android.features.rageshake.preferences.FakeRageshakeDataStore
import io.element.android.features.rageshake.preferences.RageshakePreferencesPresenter import io.element.android.features.rageshake.preferences.RageshakePreferencesPresenter
import io.element.android.features.rageshake.screenshot.ImageResult import io.element.android.features.rageshake.screenshot.ImageResult
import io.element.android.libraries.matrixtest.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first

View file

@ -36,12 +36,12 @@ dependencies {
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources) implementation(projects.libraries.elementresources)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.dateformatter) implementation(projects.libraries.dateformatter.api)
implementation(libs.accompanist.placeholder) implementation(libs.accompanist.placeholder)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
@ -49,9 +49,8 @@ dependencies {
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest) testImplementation(projects.libraries.matrix.test)
implementation(projects.libraries.dateformatter.test)
testImplementation(testFixtures(projects.libraries.matrix))
androidTestImplementation(libs.test.junitext) androidTestImplementation(libs.test.junitext)

View file

@ -18,7 +18,7 @@ package io.element.android.features.roomlist.api
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
interface RoomListEntryPoint : FeatureEntryPoint { interface RoomListEntryPoint : FeatureEntryPoint {
interface Callback : Plugin { interface Callback : Plugin {

View file

@ -27,7 +27,7 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
class RoomListNode @AssistedInject constructor( class RoomListNode @AssistedInject constructor(

View file

@ -27,11 +27,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.parallelMap import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.dateformatter.LastMessageFormatter import io.element.android.libraries.dateformatter.api.LastMessageFormatter
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.room.RoomSummary import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomSummary
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -103,14 +104,14 @@ class RoomListPresenter @Inject constructor(
val userDisplayName = client.loadUserDisplayName().getOrNull() val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData = val avatarData =
AvatarData( AvatarData(
id = client.userId().value, id = client.sessionId.value,
name = userDisplayName, name = userDisplayName,
url = userAvatarUrl, url = userAvatarUrl,
size = AvatarSize.SMALL size = AvatarSize.SMALL
) )
matrixUser.value = MatrixUser( matrixUser.value = MatrixUser(
id = client.userId(), id = UserId(client.sessionId.value),
username = userDisplayName ?: client.userId().value, username = userDisplayName ?: client.sessionId.value,
avatarData = avatarData, avatarData = avatarData,
) )
} }

View file

@ -18,7 +18,7 @@ package io.element.android.features.roomlist.impl
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@Immutable @Immutable
data class RoomListRoomSummary( data class RoomListRoomSummary(

View file

@ -18,7 +18,7 @@ package io.element.android.features.roomlist.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> { open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> {
override val values: Sequence<RoomListRoomSummary> override val values: Sequence<RoomListRoomSummary>

View file

@ -18,7 +18,7 @@ package io.element.android.features.roomlist.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf

View file

@ -40,7 +40,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList

View file

@ -59,7 +59,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
import io.element.android.libraries.ui.strings.R as StringR import io.element.android.libraries.ui.strings.R as StringR

View file

@ -16,7 +16,7 @@
package io.element.android.features.roomlist package io.element.android.features.roomlist
import io.element.android.libraries.dateformatter.LastMessageFormatter import io.element.android.libraries.dateformatter.api.LastMessageFormatter
class FakeLastMessageFormatter : LastMessageFormatter { class FakeLastMessageFormatter : LastMessageFormatter {
private var format = "" private var format = ""

View file

@ -22,22 +22,22 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.roomlist.impl.RoomListPresenter
import io.element.android.features.roomlist.impl.RoomListEvents import io.element.android.features.roomlist.impl.RoomListEvents
import io.element.android.features.roomlist.impl.RoomListPresenter
import io.element.android.features.roomlist.impl.RoomListRoomSummary import io.element.android.features.roomlist.impl.RoomListRoomSummary
import io.element.android.libraries.dateformatter.LastMessageFormatter import io.element.android.libraries.dateformatter.api.LastMessageFormatter
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrixtest.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrixtest.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrixtest.A_MESSAGE import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrixtest.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrixtest.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrixtest.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrixtest.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrixtest.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrixtest.room.FakeRoomSummaryDataSource import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import io.element.android.libraries.matrixtest.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -47,9 +47,7 @@ class RoomListPresenterTests {
@Test @Test
fun `present - should start with no user and then load user with success`() = runTest { fun `present - should start with no user and then load user with success`() = runTest {
val presenter = RoomListPresenter( val presenter = RoomListPresenter(
FakeMatrixClient( FakeMatrixClient(A_SESSION_ID),
SessionId("sessionId")
),
createDateFormatter() createDateFormatter()
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
@ -70,7 +68,7 @@ class RoomListPresenterTests {
fun `present - should start with no user and then load user with error`() = runTest { fun `present - should start with no user and then load user with error`() = runTest {
val presenter = RoomListPresenter( val presenter = RoomListPresenter(
FakeMatrixClient( FakeMatrixClient(
SessionId("sessionId"), A_SESSION_ID,
userDisplayName = Result.failure(AN_EXCEPTION), userDisplayName = Result.failure(AN_EXCEPTION),
userAvatarURLString = Result.failure(AN_EXCEPTION), userAvatarURLString = Result.failure(AN_EXCEPTION),
), ),
@ -91,9 +89,7 @@ class RoomListPresenterTests {
@Test @Test
fun `present - should filter room with success`() = runTest { fun `present - should filter room with success`() = runTest {
val presenter = RoomListPresenter( val presenter = RoomListPresenter(
FakeMatrixClient( FakeMatrixClient(A_SESSION_ID),
SessionId("sessionId")
),
createDateFormatter() createDateFormatter()
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
@ -113,7 +109,7 @@ class RoomListPresenterTests {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomSummaryDataSource = FakeRoomSummaryDataSource()
val presenter = RoomListPresenter( val presenter = RoomListPresenter(
FakeMatrixClient( FakeMatrixClient(
sessionId = SessionId("sessionId"), sessionId = A_SESSION_ID,
roomSummaryDataSource = roomSummaryDataSource roomSummaryDataSource = roomSummaryDataSource
), ),
createDateFormatter() createDateFormatter()
@ -139,7 +135,7 @@ class RoomListPresenterTests {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomSummaryDataSource = FakeRoomSummaryDataSource()
val presenter = RoomListPresenter( val presenter = RoomListPresenter(
FakeMatrixClient( FakeMatrixClient(
sessionId = SessionId("sessionId"), sessionId = A_SESSION_ID,
roomSummaryDataSource = roomSummaryDataSource roomSummaryDataSource = roomSummaryDataSource
), ),
createDateFormatter() createDateFormatter()
@ -170,7 +166,7 @@ class RoomListPresenterTests {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomSummaryDataSource = FakeRoomSummaryDataSource()
val presenter = RoomListPresenter( val presenter = RoomListPresenter(
FakeMatrixClient( FakeMatrixClient(
sessionId = SessionId("sessionId"), sessionId = A_SESSION_ID,
roomSummaryDataSource = roomSummaryDataSource roomSummaryDataSource = roomSummaryDataSource
), ),
createDateFormatter() createDateFormatter()

View file

@ -37,7 +37,7 @@ dependencies {
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources) implementation(projects.libraries.elementresources)
@ -48,7 +48,7 @@ dependencies {
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest) testImplementation(projects.libraries.matrix.test)
androidTestImplementation(libs.test.junitext) androidTestImplementation(libs.test.junitext)

View file

@ -42,6 +42,7 @@ jsoup = "1.15.3"
appyx = "1.0.3" appyx = "1.0.3"
dependencycheck = "7.4.4" dependencycheck = "7.4.4"
stem = "2.2.3" stem = "2.2.3"
sqldelight = "1.5.5"
# DI # DI
dagger = "2.44.2" dagger = "2.44.2"
@ -69,6 +70,7 @@ androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-kt
androidx_lifecycle_compose = { module = "androidx.lifecycle:compose", version.ref = "lifecycle" } androidx_lifecycle_compose = { module = "androidx.lifecycle:compose", version.ref = "lifecycle" }
androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
androidx_splash = "androidx.core:core-splashscreen:1.0.0" androidx_splash = "androidx.core:core-splashscreen:1.0.0"
androidx_security_crypto = "androidx.security:security-crypto:1.0.0"
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity_compose" } androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity_compose" }
androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" } androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" }
@ -119,6 +121,11 @@ appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.2" matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.2"
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.3"
sqlite = "androidx.sqlite:sqlite:2.3.0"
# Di # Di
inject = "javax.inject:javax.inject:1" inject = "javax.inject:javax.inject:1"
@ -149,3 +156,4 @@ stemlibrary = { id = "com.likethesalad.stem-library", version.ref = "stem" }
paparazzi = "app.cash.paparazzi:1.2.0" paparazzi = "app.cash.paparazzi:1.2.0"
sonarqube = "org.sonarqube:3.5.0.2730" sonarqube = "org.sonarqube:3.5.0.2730"
kover = "org.jetbrains.kotlinx.kover:0.6.1" kover = "org.jetbrains.kotlinx.kover:0.6.1"
sqldelight = { id = "com.squareup.sqldelight", version.ref = "sqldelight" }

View file

@ -14,6 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.matrix // TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-library")
}
internal const val LOG_TAG = "Matrix" android {
namespace = "io.element.android.libraries.dateformatter.api"
}

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.dateformatter package io.element.android.libraries.dateformatter.api
interface DaySeparatorFormatter { interface DaySeparatorFormatter {
fun format(timestamp: Long): String fun format(timestamp: Long): String

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.dateformatter package io.element.android.libraries.dateformatter.api
interface LastMessageFormatter { interface LastMessageFormatter {
fun format(timestamp: Long?): String fun format(timestamp: Long?): String

View file

@ -0,0 +1 @@
/build

View file

@ -27,17 +27,19 @@ anvil {
} }
android { android {
namespace = "io.element.android.libraries.dateformatter" namespace = "io.element.android.libraries.dateformatter.impl"
dependencies { dependencies {
anvil(projects.anvilcodegen) anvil(projects.anvilcodegen)
implementation(libs.dagger) implementation(libs.dagger)
implementation(projects.libraries.di) implementation(projects.libraries.di)
implementation(projects.anvilannotations) implementation(projects.anvilannotations)
implementation(projects.libraries.dateformatter.api)
api(libs.datetime) api(libs.datetime)
ksp(libs.showkase.processor)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(projects.libraries.dateformatter.test)
} }
} }

View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!--
<!--
~ Copyright (c) 2023 New Vector Ltd ~ Copyright (c) 2023 New Vector Ltd
~ ~
~ Licensed under the Apache License, Version 2.0 (the "License"); ~ Licensed under the Apache License, Version 2.0 (the "License");
@ -14,5 +13,4 @@
~ See the License for the specific language governing permissions and ~ See the License for the specific language governing permissions and
~ limitations under the License. ~ limitations under the License.
--> -->
<manifest />
<manifest/>

View file

@ -39,17 +39,17 @@ class DateFormatters @Inject constructor(
private val onlyTimeFormatter: DateTimeFormatter by lazy { private val onlyTimeFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") ?: "HH:mm" val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") ?: "HH:mm"
DateTimeFormatter.ofPattern(pattern) DateTimeFormatter.ofPattern(pattern, locale)
} }
private val dateWithMonthFormatter: DateTimeFormatter by lazy { private val dateWithMonthFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") ?: "d MMM" val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") ?: "d MMM"
DateTimeFormatter.ofPattern(pattern) DateTimeFormatter.ofPattern(pattern, locale)
} }
private val dateWithYearFormatter: DateTimeFormatter by lazy { private val dateWithYearFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") ?: "dd.MM.yyyy" val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") ?: "dd.MM.yyyy"
DateTimeFormatter.ofPattern(pattern) DateTimeFormatter.ofPattern(pattern, locale)
} }
internal fun formatTime(localDateTime: LocalDateTime): String { internal fun formatTime(localDateTime: LocalDateTime): String {

View file

@ -17,7 +17,7 @@
package io.element.android.libraries.dateformatter.impl package io.element.android.libraries.dateformatter.impl
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.dateformatter.DaySeparatorFormatter import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import javax.inject.Inject import javax.inject.Inject

View file

@ -17,7 +17,7 @@
package io.element.android.libraries.dateformatter.impl package io.element.android.libraries.dateformatter.impl
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.dateformatter.LastMessageFormatter import io.element.android.libraries.dateformatter.api.LastMessageFormatter
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import javax.inject.Inject import javax.inject.Inject

Some files were not shown because too many files have changed in this diff Show more