Merge branch 'develop' into feature/fga/improve_node_architecture

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -198,6 +198,9 @@ knit {
dependencies {
allLibraries()
allFeatures()
implementation(projects.libraries.matrix.impl)
implementation(projects.libraries.dateformatter.impl)
implementation(projects.libraries.sessionStorage.impl)
implementation(projects.tests.uitests)
implementation(projects.anvilannotations)
implementation(projects.appnav)
@@ -221,5 +224,5 @@ dependencies {
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
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 androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
@@ -40,7 +42,7 @@ class MainActivity : NodeComponentActivity() {
setContent {
ElementTheme {
Box(
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
) {
NodeHost(integrationPoint = appyxIntegrationPoint) {
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.createNode
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.x.di.MainDaggerComponentsOwner
import io.element.android.x.di.RoomComponent
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.SessionScope
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)
@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.SessionScope
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)
@MergeSubcomponent(SessionScope::class)

View File

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

View File

@@ -28,7 +28,6 @@ plugins {
android {
namespace = "io.element.android.appnav"
}
dependencies {
@@ -47,7 +46,7 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrixui)
implementation(projects.tests.uitests)
@@ -58,5 +57,5 @@ dependencies {
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
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.designsystem.theme.components.Text
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.core.RoomId
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
import kotlinx.parcelize.Parcelize

View File

@@ -19,9 +19,10 @@ package io.element.android.appnav
import android.os.Bundle
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
@@ -57,13 +58,13 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService:
@Suppress("DEPRECATION")
fun restore(savedInstanceState: Bundle?) {
if (savedInstanceState == null || sessionIdsToMatrixClient.isNotEmpty()) return
val sessionIds = savedInstanceState.getSerializable(SAVE_INSTANCE_KEY) as? Array<SessionId>
if (sessionIds.isNullOrEmpty()) return
val userIds = savedInstanceState.getSerializable(SAVE_INSTANCE_KEY) as? Array<UserId>
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.
runBlocking {
sessionIds.forEach { sessionId ->
Timber.v("Restore matrix session: $sessionId")
val matrixClient = authenticationService.restoreSession(sessionId)
userIds.forEach { userId ->
Timber.v("Restore matrix session: $userId")
val matrixClient = authenticationService.restoreSession(userId)
if (matrixClient != null) {
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.di.SessionScope
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom
import kotlinx.parcelize.Parcelize
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.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.tests.uitests.openShowkase
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
@@ -100,7 +101,7 @@ class RootFlowNode @AssistedInject constructor(
}
private fun switchToLoggedInFlow(sessionId: SessionId) {
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId = sessionId))
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId))
}
private fun switchToLogoutFlow() {
@@ -109,19 +110,19 @@ class RootFlowNode @AssistedInject constructor(
}
private suspend fun tryToRestoreLatestSession(
onSuccess: (SessionId) -> Unit = {},
onSuccess: (UserId) -> Unit = {},
onFailure: () -> Unit = {}
) {
val latestKnownSessionId = authenticationService.getLatestSessionId()
if (latestKnownSessionId == null) {
val latestKnownUserId = authenticationService.getLatestSessionId()
if (latestKnownUserId == null) {
onFailure()
return
}
if (matrixClientsHolder.knowSession(latestKnownSessionId)) {
onSuccess(latestKnownSessionId)
if (matrixClientsHolder.knowSession(latestKnownUserId)) {
onSuccess(latestKnownUserId)
return
}
val matrixClient = authenticationService.restoreSession(latestKnownSessionId)
val matrixClient = authenticationService.restoreSession(UserId(latestKnownUserId.value))
if (matrixClient == null) {
Timber.v("Failed to restore session...")
onFailure()
@@ -141,6 +142,7 @@ class RootFlowNode @AssistedInject constructor(
fun openShowkase() {
openShowkase(activity)
}
val state = presenter.present()
RootView(
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.BugReporterListener
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.delay
import kotlinx.coroutines.launch

View File

@@ -60,7 +60,7 @@ allprojects {
config = files("$rootDir/tools/detekt/detekt.yml")
}
dependencies {
detektPlugins("com.twitter.compose.rules:detekt:0.0.26")
detektPlugins("io.nlopez.compose.rules:detekt:0.1.2")
}
// KtLint
@@ -214,11 +214,11 @@ koverMerged {
name = "Global minimum code coverage."
target = kotlinx.kover.api.VerificationTarget.ALL
bound {
minValue = 50
minValue = 55
// 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
// minValue to 35 and maxValue to 40.
maxValue = 55
maxValue = 60
counter = kotlinx.kover.api.CounterType.INSTRUCTION
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
}
@@ -303,3 +303,15 @@ tasks.register("runQualityChecks") {
}
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)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources)
implementation(projects.libraries.testtags)
@@ -48,7 +48,7 @@ dependencies {
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest)
testImplementation(projects.libraries.matrix.test)
androidTestImplementation(libs.test.junitext)
}

View File

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

View File

@@ -16,14 +16,20 @@
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.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
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
@ContributesNode(AppScope::class)
@@ -32,18 +38,25 @@ class ChangeServerNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>,
private val presenter: ChangeServerPresenter,
) : Node(buildContext, plugins = plugins) {
private fun onSuccess() {
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
override fun View(modifier: Modifier) {
val state = presenter.present()
val context = LocalContext.current
ChangeServerView(
state = state,
modifier = modifier,
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.rememberCoroutineScope
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.Presenter
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.launch
import java.net.URL
import javax.inject.Inject
class ChangeServerPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : Presenter<ChangeServerState> {
@@ -35,8 +38,9 @@ class ChangeServerPresenter @Inject constructor(private val authenticationServic
@Composable
override fun present(): ChangeServerState {
val localCoroutineScope = rememberCoroutineScope()
val homeserver = rememberSaveable {
mutableStateOf(authenticationService.getHomeserverOrDefault())
mutableStateOf(authenticationService.getHomeserverDetails().value?.url ?: LoginConstants.DEFAULT_HOMESERVER_URL)
}
val changeServerAction: MutableState<Async<Unit>> = remember {
mutableStateOf(Async.Uninitialized)
@@ -45,7 +49,10 @@ class ChangeServerPresenter @Inject constructor(private val authenticationServic
fun handleEvents(event: ChangeServerEvents) {
when (event) {
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 {
authenticationService.setHomeserver(homeserver)
val domain = tryOrNull { URL(homeserverUrl.value) }?.host ?: homeserverUrl.value
authenticationService.setHomeserver(domain)
homeserverUrl.value = domain
}.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.layout.Box
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
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.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.KeyboardType
import androidx.compose.ui.text.style.TextAlign
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.sp
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.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.preview.ElementPreviewDark
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.CircularProgressIndicator
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.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.testTag
import org.matrix.rustcomponents.sdk.AuthenticationException
import io.element.android.libraries.ui.strings.R as StringR
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChangeServerView(
state: ChangeServerState,
onLearnMoreClicked: () -> Unit,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
onChangeServerSuccess: () -> Unit = {},
) {
val eventSink = state.eventSink
val scrollState = rememberScrollState()
Box(
modifier = modifier
.fillMaxSize()
.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 interactionEnabled by remember(state.changeServerAction) {
derivedStateOf {
state.changeServerAction !is Async.Loading
}
}
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
@@ -191,5 +267,5 @@ internal fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProv
@Composable
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
sealed interface LoginRootEvents {
object RefreshHomeServer : LoginRootEvents
data class SetLogin(val login: String) : LoginRootEvents
data class SetPassword(val password: String) : LoginRootEvents
object Submit : LoginRootEvents
object ClearError : LoginRootEvents
}

View File

@@ -47,16 +47,11 @@ class LoginRootNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
OnLifecycleEvent { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> state.eventSink(LoginRootEvents.RefreshHomeServer)
else -> Unit
}
}
LoginRootScreen(
state = state,
modifier = modifier,
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.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.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.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
class LoginRootPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : Presenter<LoginRootState> {
private val defaultHomeserver = MatrixHomeServerDetails(LoginConstants.DEFAULT_HOMESERVER_URL, true, null)
@Composable
override fun present(): LoginRootState {
val localCoroutineScope = rememberCoroutineScope()
val homeserver = rememberSaveable {
mutableStateOf(authenticationService.getHomeserverOrDefault())
}
val homeserver = authenticationService.getHomeserverDetails().collectAsState().value ?: defaultHomeserver
val loggedInState: MutableState<LoggedInState> = remember {
mutableStateOf(LoggedInState.NotLoggedIn)
}
@@ -46,19 +51,19 @@ class LoginRootPresenter @Inject constructor(private val authenticationService:
fun handleEvents(event: LoginRootEvents) {
when (event) {
LoginRootEvents.RefreshHomeServer -> refreshHomeServer(homeserver)
is LoginRootEvents.SetLogin -> updateFormState(formState) {
copy(login = event.login)
}
is LoginRootEvents.SetPassword -> updateFormState(formState) {
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(
homeserver = homeserver.value,
homeserverDetails = homeserver,
loggedInState = loggedInState.value,
formState = formState.value,
eventSink = ::handleEvents
@@ -83,7 +88,4 @@ class LoginRootPresenter @Inject constructor(private val authenticationService:
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
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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
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.VisibilityOff
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
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.sp
import io.element.android.features.login.impl.error.loginError
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
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.CircularProgressIndicator
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.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Scaffold
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.testTag
import io.element.android.libraries.ui.strings.R as StringR
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginRootScreen(
state: LoginRootState,
modifier: Modifier = Modifier,
onChangeServer: () -> Unit = {},
onLoginWithSuccess: (SessionId) -> Unit = {},
onBackPressed: () -> Unit,
) {
val eventSink = state.eventSink
Box(
modifier = modifier
.fillMaxSize()
.systemBarsPadding()
.imePadding()
) {
val scrollState = rememberScrollState()
var loginFieldState by textFieldState(stateValue = state.formState.login)
var passwordFieldState by textFieldState(stateValue = state.formState.password)
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,
val interactionEnabled by remember(state.loggedInState) {
derivedStateOf {
state.loggedInState != LoggedInState.LoggingIn
}
}
Scaffold(
topBar = {
TopAppBar(
title = {},
navigationIcon = { BackButton(action = onBackPressed, enabled = interactionEnabled) },
)
// Form
Column(
// modifier = Modifier.weight(1f),
) {
Box(
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = state.homeserver,
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"
}
) { padding ->
Box(
modifier = modifier
.fillMaxSize()
.imePadding()
.padding(padding)
) {
val scrollState = rememberScrollState()
IconButton(onClick = { passwordVisible = !passwordVisible }) {
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,
Column(
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginContinue)
.padding(vertical = 32.dp)
.verticalScroll(state = scrollState)
.padding(horizontal = 16.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) {
is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId)
else -> Unit
}
}
if (state.loggedInState is LoggedInState.LoggingIn) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
if (state.loggedInState is LoggedInState.LoggingIn) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
@Preview
@Composable
internal fun LoginRootScreenLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun LoginRootScreenDarkPreview() = ElementPreviewDark { ContentToPreview() }
internal fun ChangeServerSection(
interactionEnabled: Boolean,
homeserver: String,
onChangeServer: () -> Unit,
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
private fun ContentToPreview() {
LoginRootScreen(
state = aLoginRootState().copy(
homeserver = "matrix.org",
),
internal fun LoginForm(
state: LoginRootState,
interactionEnabled: Boolean,
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
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
data class LoginRootState(
val homeserver: String,
val homeserverDetails: MatrixHomeServerDetails,
val loggedInState: LoggedInState,
val formState: LoginFormState,
val eventSink: (LoginRootEvents) -> Unit

View File

@@ -16,8 +16,24 @@
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(
homeserver = "",
homeserverDetails = MatrixHomeServerDetails("matrix.org", true, null),
loggedInState = LoggedInState.NotLoggedIn,
formState = LoginFormState.Default,
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.ChangeServerPresenter
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrixtest.A_HOMESERVER
import io.element.android.libraries.matrixtest.auth.FakeAuthenticationService
import io.element.android.libraries.matrix.test.A_HOMESERVER
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.test.runTest
import org.junit.Test
@@ -41,7 +44,23 @@ class ChangeServerPresenterTest {
presenter.present()
}.test {
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()
}
}
@@ -80,4 +99,70 @@ class ChangeServerPresenterTest {
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.LoginRootEvents
import io.element.android.features.login.impl.root.LoginRootPresenter
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrixtest.A_HOMESERVER
import io.element.android.libraries.matrixtest.A_HOMESERVER_2
import io.element.android.libraries.matrixtest.A_PASSWORD
import io.element.android.libraries.matrixtest.A_SESSION_ID
import io.element.android.libraries.matrixtest.A_THROWABLE
import io.element.android.libraries.matrixtest.A_USER_NAME
import io.element.android.libraries.matrixtest.auth.FakeAuthenticationService
import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_PASSWORD
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -48,7 +46,7 @@ class LoginRootPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER)
assertThat(initialState.homeserverDetails).isEqualTo(A_HOMESERVER)
assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
assertThat(initialState.submitEnabled).isFalse()
@@ -92,7 +90,7 @@ class LoginRootPresenterTest {
val submitState = awaitItem()
assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn)
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
fun `present - refresh server`() = runTest {
fun `present - clear error`() = runTest {
val authenticationService = FakeAuthenticationService()
val presenter = LoginRootPresenter(
authenticationService,
@@ -129,11 +127,20 @@ class LoginRootPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER)
authenticationService.givenHomeserver(A_HOMESERVER_2)
initialState.eventSink.invoke(LoginRootEvents.RefreshHomeServer)
val refreshedState = awaitItem()
assertThat(refreshedState.homeserver).isEqualTo(A_HOMESERVER_2)
// Submit will return an error
authenticationService.givenLoginError(A_THROWABLE)
initialState.eventSink(LoginRootEvents.Submit)
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)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.matrix)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources)
implementation(projects.libraries.uiStrings)
@@ -46,7 +46,7 @@ dependencies {
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest)
testImplementation(projects.libraries.matrix.test)
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.Presenter
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.launch
import javax.inject.Inject

View File

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

View File

@@ -25,16 +25,5 @@ android {
}
dependencies {
implementation(projects.libraries.core)
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)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.textcomposer)
implementation(projects.libraries.dateformatter)
implementation(projects.libraries.dateformatter.api)
implementation(libs.coil.compose)
implementation(libs.datetime)
implementation(libs.accompanist.flowlayout)
@@ -52,7 +52,8 @@ dependencies {
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.dateformatter.test)
androidTestImplementation(libs.test.junitext)
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.designsystem.components.avatar.AvatarData
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 kotlinx.coroutines.CoroutineScope
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.timeline.TimelineState
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
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.libraries.core.data.StableCharSequence
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
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.core.data.StableCharSequence
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

View File

@@ -16,7 +16,7 @@
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 {
object LoadMore : TimelineEvents

View File

@@ -26,9 +26,9 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
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 io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import kotlinx.collections.immutable.ImmutableList
@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.TimelineItemTextContent
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import kotlinx.collections.immutable.ImmutableList
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.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.permalink.PermalinkData
import io.element.android.libraries.matrix.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import kotlinx.collections.immutable.persistentMapOf
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element

View File

@@ -17,7 +17,7 @@
package io.element.android.features.messages.impl.timeline.diff
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(
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.model.TimelineItem
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.StateFlow
import kotlinx.coroutines.flow.asStateFlow

View File

@@ -16,14 +16,14 @@
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.TimelineItemEventContent
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.TimelineItemTextContent
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.libraries.matrix.media.MediaResolver
import io.element.android.libraries.matrix.api.media.MediaResolver
import org.matrix.rustcomponents.sdk.Message
import org.matrix.rustcomponents.sdk.MessageType
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.libraries.designsystem.components.avatar.AvatarData
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 org.matrix.rustcomponents.sdk.ProfileTimelineDetails
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.TimelineItemVirtualModel
import io.element.android.libraries.dateformatter.DaySeparatorFormatter
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
import org.matrix.rustcomponents.sdk.VirtualTimelineItem
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.TimelineItemUnknownVirtualModel
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 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.virtual.TimelineItemVirtualModel
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
sealed interface TimelineItem {

View File

@@ -16,7 +16,7 @@
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(
val body: String,

View File

@@ -17,7 +17,7 @@
package io.element.android.features.messages.impl.timeline.model.event
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> {
override val values: Sequence<TimelineItemImageContent>

View File

@@ -22,18 +22,18 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
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.aTimelineItemsFactory
import io.element.android.features.messages.impl.MessagesEvents
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.timeline.TimelinePresenter
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrixtest.AN_EVENT_ID
import io.element.android.libraries.matrixtest.A_ROOM_ID
import io.element.android.libraries.matrixtest.room.FakeMatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.TimelineItemTextContent
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrixtest.AN_EVENT_ID
import io.element.android.libraries.matrixtest.A_MESSAGE
import io.element.android.libraries.matrixtest.A_USER_ID
import io.element.android.libraries.matrixtest.A_USER_NAME
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.TimelineItemTextContent
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrixtest.AN_EVENT_ID
import io.element.android.libraries.matrixtest.A_MESSAGE
import io.element.android.libraries.matrixtest.A_USER_ID
import io.element.android.libraries.matrixtest.A_USER_NAME
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import kotlinx.collections.immutable.persistentListOf
internal fun aMessageEvent(

View File

@@ -16,7 +16,6 @@
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.event.TimelineItemContentFactory
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.virtual.TimelineItemDaySeparatorFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
internal fun aTimelineItemsFactory() = TimelineItemsFactory(
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.MessageComposerState
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.matrixtest.ANOTHER_MESSAGE
import io.element.android.libraries.matrixtest.AN_EVENT_ID
import io.element.android.libraries.matrixtest.A_MESSAGE
import io.element.android.libraries.matrixtest.A_REPLY
import io.element.android.libraries.matrixtest.A_USER_NAME
import io.element.android.libraries.matrixtest.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_REPLY
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.libraries.matrixtest.AN_EVENT_ID
import io.element.android.libraries.matrixtest.room.FakeMatrixRoom
import io.element.android.libraries.matrixtest.timeline.FakeMatrixTimeline
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test

View File

@@ -50,7 +50,7 @@ dependencies {
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest)
testImplementation(projects.libraries.matrix.test)
androidTestImplementation(libs.test.junitext)
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.rageshake.preferences.RageshakePreferencesPresenter
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.test.runTest
import org.junit.Test

View File

@@ -51,7 +51,7 @@ dependencies {
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest)
testImplementation(projects.libraries.matrix.test)
testImplementation(libs.test.mockk)
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.FakeCrashDataStore
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.test.runTest
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.BugReporterListener
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.delay
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.RageshakePreferencesPresenter
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 kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first

View File

@@ -36,12 +36,12 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.dateformatter)
implementation(projects.libraries.dateformatter.api)
implementation(libs.accompanist.placeholder)
testImplementation(libs.test.junit)
@@ -49,9 +49,8 @@ dependencies {
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrixtest)
testImplementation(testFixtures(projects.libraries.matrix))
testImplementation(projects.libraries.matrix.test)
implementation(projects.libraries.dateformatter.test)
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 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 Callback : Plugin {

View File

@@ -27,7 +27,7 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.roomlist.api.RoomListEntryPoint
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)
class RoomListNode @AssistedInject constructor(

View File

@@ -27,11 +27,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Presenter
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.AvatarSize
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.room.RoomSummary
import io.element.android.libraries.matrix.api.MatrixClient
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -103,14 +104,14 @@ class RoomListPresenter @Inject constructor(
val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData =
AvatarData(
id = client.userId().value,
id = client.sessionId.value,
name = userDisplayName,
url = userAvatarUrl,
size = AvatarSize.SMALL
)
matrixUser.value = MatrixUser(
id = client.userId(),
username = userDisplayName ?: client.userId().value,
id = UserId(client.sessionId.value),
username = userDisplayName ?: client.sessionId.value,
avatarData = avatarData,
)
}

View File

@@ -18,7 +18,7 @@ package io.element.android.features.roomlist.impl
import androidx.compose.runtime.Immutable
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
data class RoomListRoomSummary(

View File

@@ -18,7 +18,7 @@ package io.element.android.features.roomlist.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
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> {
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 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 kotlinx.collections.immutable.ImmutableList
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.theme.components.Scaffold
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 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.TopAppBar
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.ui.strings.R as StringR

View File

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

View File

@@ -22,22 +22,22 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
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.RoomListPresenter
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.matrix.core.SessionId
import io.element.android.libraries.matrixtest.AN_AVATAR_URL
import io.element.android.libraries.matrixtest.AN_EXCEPTION
import io.element.android.libraries.matrixtest.A_MESSAGE
import io.element.android.libraries.matrixtest.A_ROOM_ID
import io.element.android.libraries.matrixtest.A_ROOM_NAME
import io.element.android.libraries.matrixtest.A_USER_ID
import io.element.android.libraries.matrixtest.A_USER_NAME
import io.element.android.libraries.matrixtest.FakeMatrixClient
import io.element.android.libraries.matrixtest.room.FakeRoomSummaryDataSource
import io.element.android.libraries.matrixtest.room.aRoomSummaryFilled
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -47,9 +47,7 @@ class RoomListPresenterTests {
@Test
fun `present - should start with no user and then load user with success`() = runTest {
val presenter = RoomListPresenter(
FakeMatrixClient(
SessionId("sessionId")
),
FakeMatrixClient(A_SESSION_ID),
createDateFormatter()
)
moleculeFlow(RecompositionClock.Immediate) {
@@ -70,7 +68,7 @@ class RoomListPresenterTests {
fun `present - should start with no user and then load user with error`() = runTest {
val presenter = RoomListPresenter(
FakeMatrixClient(
SessionId("sessionId"),
A_SESSION_ID,
userDisplayName = Result.failure(AN_EXCEPTION),
userAvatarURLString = Result.failure(AN_EXCEPTION),
),
@@ -91,9 +89,7 @@ class RoomListPresenterTests {
@Test
fun `present - should filter room with success`() = runTest {
val presenter = RoomListPresenter(
FakeMatrixClient(
SessionId("sessionId")
),
FakeMatrixClient(A_SESSION_ID),
createDateFormatter()
)
moleculeFlow(RecompositionClock.Immediate) {
@@ -113,7 +109,7 @@ class RoomListPresenterTests {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val presenter = RoomListPresenter(
FakeMatrixClient(
sessionId = SessionId("sessionId"),
sessionId = A_SESSION_ID,
roomSummaryDataSource = roomSummaryDataSource
),
createDateFormatter()
@@ -139,7 +135,7 @@ class RoomListPresenterTests {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val presenter = RoomListPresenter(
FakeMatrixClient(
sessionId = SessionId("sessionId"),
sessionId = A_SESSION_ID,
roomSummaryDataSource = roomSummaryDataSource
),
createDateFormatter()
@@ -170,7 +166,7 @@ class RoomListPresenterTests {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val presenter = RoomListPresenter(
FakeMatrixClient(
sessionId = SessionId("sessionId"),
sessionId = A_SESSION_ID,
roomSummaryDataSource = roomSummaryDataSource
),
createDateFormatter()

View File

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

View File

@@ -42,6 +42,7 @@ jsoup = "1.15.3"
appyx = "1.0.3"
dependencycheck = "7.4.4"
stem = "2.2.3"
sqldelight = "1.5.5"
# DI
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_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
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_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" }
timber = "com.jakewharton.timber:timber:5.0.1"
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
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"
sonarqube = "org.sonarqube:3.5.0.2730"
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.
*/
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.
*/
package io.element.android.libraries.dateformatter
package io.element.android.libraries.dateformatter.api
interface DaySeparatorFormatter {
fun format(timestamp: Long): String

View File

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

View File

@@ -0,0 +1 @@
/build

View File

@@ -27,17 +27,19 @@ anvil {
}
android {
namespace = "io.element.android.libraries.dateformatter"
namespace = "io.element.android.libraries.dateformatter.impl"
dependencies {
anvil(projects.anvilcodegen)
implementation(libs.dagger)
implementation(projects.libraries.di)
implementation(projects.anvilannotations)
implementation(projects.libraries.dateformatter.api)
api(libs.datetime)
ksp(libs.showkase.processor)
testImplementation(libs.test.junit)
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
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,5 +13,4 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest/>
<manifest />

View File

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

View File

@@ -17,7 +17,7 @@
package io.element.android.libraries.dateformatter.impl
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 javax.inject.Inject

View File

@@ -17,7 +17,7 @@
package io.element.android.libraries.dateformatter.impl
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 javax.inject.Inject

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