Merge branch 'develop' into feature/bma/documentation

This commit is contained in:
Benoit Marty
2023-02-03 11:22:05 +01:00
committed by GitHub
366 changed files with 3066 additions and 1682 deletions

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.2
uses: danger/danger-js@11.2.3
with:
args: "--dangerfile ./tools/danger/dangerfile.js"
env:

View File

@@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@v3
- name: Assemble debug APK
run: ./gradlew assembleDebug $CI_GRADLE_ARG_PROPERTIES
- uses: mobile-dev-inc/action-maestro-cloud@v1.1.1
- uses: mobile-dev-inc/action-maestro-cloud@v1.2.3
with:
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
app-file: app/build/outputs/apk/debug/app-debug.apk

View File

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

View File

@@ -23,6 +23,17 @@ jobs:
- uses: actions/checkout@v3
- name: Run tests
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES
- name: Generate kover report
if: always()
run: ./gradlew koverMergedReport $CI_GRADLE_ARG_PROPERTIES
- name: Archive kover report
if: always()
uses: actions/upload-artifact@v3
with:
name: kover-results
path: |
**/build/reports/kover/merged
- name: Archive test results on error
if: failure()
@@ -32,3 +43,17 @@ jobs:
path: |
**/out/failures/
**/build/reports/tests/*UnitTest/
- name: Publish results to Sonar
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
# https://github.com/codecov/codecov-action
- name: Upload coverage reports to codecov
if: always()
uses: codecov/codecov-action@v3
# with:
# files: build/reports/kover/merged/xml/report.xml

4
.idea/compiler.xml generated
View File

@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
<bytecodeTargetLevel target="1.8">
<module name="ElementX.libraries.composeutils" target="11" />
</bytecodeTargetLevel>
</component>
</project>

2
.idea/misc.xml generated
View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@@ -1,3 +1,11 @@
[![Latest build](https://github.com/vector-im/element-x-android/actions/workflows/build.yml/badge.svg?query=branch%3Adevelop)](https://github.com/vector-im/element-x-android/actions/workflows/build.yml?query=branch%3Adevelop)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-x-android&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-x-android&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-x-android&metric=bugs)](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android)
[![codecov](https://codecov.io/github/vector-im/element-x-android/branch/develop/graph/badge.svg?token=ecwvia7amV)](https://codecov.io/github/vector-im/element-x-android)
[![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org)
[![Weblate](https://translate.element.io/widgets/element-android/-/svg-badge.svg)](https://translate.element.io/engage/element-android/?utm_source=widget)
# element-x-android
ElementX Android is a [Matrix](https://matrix.org/) Android Client provided by [Element](https://element.io/).

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.anvilannotations
package io.element.android.anvilannotations
import kotlin.reflect.KClass

View File

@@ -22,7 +22,7 @@ plugins {
}
dependencies {
implementation(project(":anvilannotations"))
implementation(projects.anvilannotations)
api(libs.anvil.compiler.api)
implementation(libs.anvil.compiler.utils)
implementation("com.squareup:kotlinpoet:1.12.0")

View File

@@ -16,7 +16,7 @@
@file:OptIn(ExperimentalAnvilApi::class)
package io.element.android.x.anvilcodegen
package io.element.android.anvilcodegen
import com.google.auto.service.AutoService
import com.squareup.anvil.annotations.ContributesTo
@@ -46,7 +46,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.multibindings.IntoMap
import io.element.android.x.anvilannotations.ContributesNode
import io.element.android.anvilannotations.ContributesNode
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
@@ -148,7 +148,7 @@ class ContributesNodeCodeGenerator : CodeGenerator {
}
companion object {
private val assistedNodeFactoryFqName = FqName("io.element.android.x.architecture.AssistedNodeFactory")
private val nodeKeyFqName = FqName("io.element.android.x.architecture.NodeKey")
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
}
}

View File

@@ -1,5 +1,3 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
@@ -164,14 +162,14 @@ knit {
dependencies {
allLibraries()
allFeatures()
implementation(project(":tests:uitests"))
implementation(project(":anvilannotations"))
anvil(project(":anvilcodegen"))
implementation(projects.tests.uitests)
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
// https://developer.android.com/studio/write/java8-support#library-desugaring-versions
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
implementation(libs.appyx.core)
implementation(libs.androidx.splash)
implementation(libs.androidx.corektx)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.activity.compose)

View File

@@ -31,7 +31,8 @@
tools:targetApi="33">
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:theme="@style/Theme.ElementX.Splash"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
android:exported="true"
android:windowSoftInputMode="adjustResize"
tools:ignore="LockedOrientationActivity">

View File

@@ -18,9 +18,10 @@ package io.element.android.x
import android.app.Application
import androidx.startup.AppInitializer
import io.element.android.x.di.DaggerComponentOwner
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.x.di.AppComponent
import io.element.android.x.di.DaggerAppComponent
import io.element.android.x.info.logApplicationInfo
import io.element.android.x.initializer.CrashInitializer
import io.element.android.x.initializer.MatrixInitializer
import io.element.android.x.initializer.TimberInitializer
@@ -40,5 +41,6 @@ class ElementXApplication : Application(), DaggerComponentOwner {
initializeComponent(TimberInitializer::class.java)
initializeComponent(MatrixInitializer::class.java)
}
logApplicationInfo()
}
}

View File

@@ -22,20 +22,23 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
import io.element.android.x.architecture.bindings
import io.element.android.x.di.DaggerComponentOwner
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.designsystem.ElementXTheme
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.x.di.AppBindings
import io.element.android.x.node.RootFlowNode
class MainActivity : NodeComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
val appBindings = bindings<AppBindings>()
appBindings.matrixClientsHolder().restore(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
ElementXTheme {
@@ -48,11 +51,17 @@ class MainActivity : NodeComponentActivity() {
buildContext = it,
appComponentOwner = applicationContext as DaggerComponentOwner,
authenticationService = appBindings.authenticationService(),
rootPresenter = appBindings.rootPresenter()
presenter = appBindings.rootPresenter(),
matrixClientsHolder = appBindings.matrixClientsHolder()
)
}
}
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
bindings<AppBindings>().matrixClientsHolder().onSaveInstanceState(outState)
}
}

View File

@@ -17,13 +17,14 @@
package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo
import io.element.android.x.matrix.auth.MatrixAuthenticationService
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import io.element.android.x.root.RootPresenter
import kotlinx.coroutines.CoroutineScope
@ContributesTo(AppScope::class)
interface AppBindings {
fun coroutineScope(): CoroutineScope
fun rootPresenter(): RootPresenter
fun authenticationService(): MatrixAuthenticationService
fun matrixClientsHolder(): MatrixClientsHolder
}

View File

@@ -20,7 +20,10 @@ import android.content.Context
import com.squareup.anvil.annotations.MergeComponent
import dagger.BindsInstance
import dagger.Component
import io.element.android.x.architecture.NodeFactoriesBindings
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
@SingleIn(AppScope::class)
@MergeComponent(AppScope::class)

View File

@@ -20,7 +20,10 @@ import android.content.Context
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.element.android.x.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers

View File

@@ -0,0 +1,79 @@
/*
* 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.x.di
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 kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey"
@SingleIn(AppScope::class)
class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) {
private val sessionIdsToMatrixClient = ConcurrentHashMap<SessionId, MatrixClient>()
fun add(matrixClient: MatrixClient) {
sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient
}
fun removeAll() {
sessionIdsToMatrixClient.clear()
}
fun remove(sessionId: SessionId) {
sessionIdsToMatrixClient.remove(sessionId)
}
fun isEmpty(): Boolean = sessionIdsToMatrixClient.isEmpty()
fun knowSession(sessionId: SessionId): Boolean = sessionIdsToMatrixClient.containsKey(sessionId)
fun getOrNull(sessionId: SessionId): MatrixClient? {
return sessionIdsToMatrixClient[sessionId]
}
@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
// 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)
if (matrixClient != null) {
add(matrixClient)
}
}
}
}
fun onSaveInstanceState(outState: Bundle) {
val sessionKeys = sessionIdsToMatrixClient.keys.toTypedArray()
Timber.v("Save matrix session keys = $sessionKeys")
outState.putSerializable(SAVE_INSTANCE_KEY, sessionKeys)
}
}

View File

@@ -20,8 +20,11 @@ import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance
import dagger.Subcomponent
import io.element.android.x.architecture.NodeFactoriesBindings
import io.element.android.x.matrix.room.MatrixRoom
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
@SingleIn(RoomScope::class)
@MergeSubcomponent(RoomScope::class)

View File

@@ -20,8 +20,11 @@ import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance
import dagger.Subcomponent
import io.element.android.x.architecture.NodeFactoriesBindings
import io.element.android.x.matrix.MatrixClient
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
@SingleIn(SessionScope::class)
@MergeSubcomponent(SessionScope::class)

View File

@@ -0,0 +1,44 @@
/*
* 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.x.info
import io.element.android.x.BuildConfig
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
fun logApplicationInfo() {
val appVersion = buildString {
append(BuildConfig.VERSION_NAME)
append(" (")
append(BuildConfig.VERSION_CODE)
append(") - ")
append(BuildConfig.BUILD_TYPE)
}
// TODO Get SDK version somehow
val sdkVersion = "SDK VERSION (TODO)"
val date = SimpleDateFormat("MM-dd HH:mm:ss.SSSZ", Locale.US).format(Date())
Timber.d("----------------------------------------------------------------")
Timber.d("----------------------------------------------------------------")
Timber.d(" Application version: $appVersion")
Timber.d(" SDK version: $sdkVersion")
Timber.d(" Local time: $date")
Timber.d("----------------------------------------------------------------")
Timber.d("----------------------------------------------------------------\n\n\n\n")
}

View File

@@ -18,7 +18,7 @@ package io.element.android.x.initializer
import android.content.Context
import androidx.startup.Initializer
import io.element.android.x.features.rageshake.crash.VectorUncaughtExceptionHandler
import io.element.android.features.rageshake.crash.VectorUncaughtExceptionHandler
class CrashInitializer : Initializer<Unit> {

View File

@@ -18,9 +18,9 @@ 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.x.BuildConfig
import io.element.android.x.matrix.tracing.TracingConfigurations
import io.element.android.x.matrix.tracing.setupTracing
class MatrixInitializer : Initializer<Unit> {

View File

@@ -18,8 +18,8 @@ package io.element.android.x.initializer
import android.content.Context
import androidx.startup.Initializer
import io.element.android.features.rageshake.logs.VectorFileLogger
import io.element.android.x.BuildConfig
import io.element.android.x.features.rageshake.logs.VectorFileLogger
import timber.log.Timber
class TimberInitializer : Initializer<Unit> {

View File

@@ -0,0 +1,31 @@
/*
* 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.x.node
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.NewRoot
/**
* Don't process NewRoot if the nav target already exists in the stack.
*/
fun <T : Any> BackStack<T>.safeRoot(element: T) {
val containsRoot = elements.value.any {
it.key.navTarget == element
}
if (containsRoot) return
accept(NewRoot(element))
}

View File

@@ -32,16 +32,17 @@ import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.node.node
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.x.architecture.bindings
import io.element.android.x.architecture.createNode
import io.element.android.x.di.DaggerComponentOwner
import io.element.android.features.preferences.PreferencesFlowNode
import io.element.android.features.roomlist.RoomListNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
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.core.RoomId
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
import io.element.android.x.di.SessionComponent
import io.element.android.x.features.preferences.PreferencesFlowNode
import io.element.android.x.features.roomlist.RoomListNode
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.core.RoomId
import io.element.android.x.matrix.core.SessionId
import io.element.android.x.matrix.ui.di.MatrixUIBindings
import kotlinx.parcelize.Parcelize
class LoggedInFlowNode(
@@ -124,6 +125,11 @@ class LoggedInFlowNode(
@Composable
override fun View(modifier: Modifier) {
Children(navModel = backstack)
Children(
navModel = backstack,
modifier = modifier,
// Animate navigation to settings and to a room
transitionHandler = rememberDefaultTransitionHandler(),
)
}
}

View File

@@ -26,9 +26,10 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.node.node
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.replace
import io.element.android.x.features.login.LoginFlowNode
import io.element.android.x.features.onboarding.OnBoardingScreen
import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.features.login.LoginFlowNode
import io.element.android.features.onboarding.OnBoardingScreen
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import kotlinx.parcelize.Parcelize
import timber.log.Timber
@@ -62,7 +63,7 @@ class NotLoggedInFlowNode(
return when (navTarget) {
NavTarget.OnBoarding -> node(buildContext) {
OnBoardingScreen(
onSignIn = { backstack.replace(NavTarget.LoginFlow) }
onSignIn = { backstack.push(NavTarget.LoginFlow) }
)
}
NavTarget.LoginFlow -> LoginFlowNode(buildContext)
@@ -71,6 +72,11 @@ class NotLoggedInFlowNode(
@Composable
override fun View(modifier: Modifier) {
Children(navModel = backstack)
Children(
navModel = backstack,
modifier = modifier,
// Animate navigation to login screen
transitionHandler = rememberDefaultTransitionHandler(),
)
}
}

View File

@@ -25,12 +25,12 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.navmodel.backstack.BackStack
import io.element.android.x.architecture.bindings
import io.element.android.x.architecture.createNode
import io.element.android.x.di.DaggerComponentOwner
import io.element.android.features.messages.MessagesNode
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.room.MatrixRoom
import io.element.android.x.di.RoomComponent
import io.element.android.x.features.messages.MessagesNode
import io.element.android.x.matrix.room.MatrixRoom
import kotlinx.parcelize.Parcelize
import timber.log.Timber
@@ -70,6 +70,9 @@ class RoomFlowNode(
@Composable
override fun View(modifier: Modifier) {
Children(navModel = backstack)
Children(
navModel = backstack,
modifier = modifier,
)
}
}

View File

@@ -21,13 +21,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
@@ -36,13 +33,13 @@ import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.x.architecture.createNode
import io.element.android.x.architecture.presenterConnector
import io.element.android.x.di.DaggerComponentOwner
import io.element.android.x.features.rageshake.bugreport.BugReportNode
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.auth.MatrixAuthenticationService
import io.element.android.x.matrix.core.SessionId
import io.element.android.features.rageshake.bugreport.BugReportNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.x.di.MatrixClientsHolder
import io.element.android.x.root.RootPresenter
import io.element.android.x.root.RootView
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -50,66 +47,96 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
class RootFlowNode(
buildContext: BuildContext,
private val buildContext: BuildContext,
private val backstack: BackStack<NavTarget> = BackStack(
initialElement = NavTarget.SplashScreen,
savedStateMap = buildContext.savedStateMap,
),
private val appComponentOwner: DaggerComponentOwner,
private val authenticationService: MatrixAuthenticationService,
rootPresenter: RootPresenter
private val matrixClientsHolder: MatrixClientsHolder,
private val presenter: RootPresenter
) :
ParentNode<RootFlowNode.NavTarget>(
navModel = backstack,
buildContext = buildContext,
buildContext = buildContext
),
DaggerComponentOwner by appComponentOwner {
private val matrixClientsHolder = ConcurrentHashMap<SessionId, MatrixClient>()
private val presenterConnector = presenterConnector(rootPresenter)
override fun onBuilt() {
super.onBuilt()
whenChildAttached(LoggedInFlowNode::class) { _, child ->
child.lifecycle.subscribe(
onDestroy = { matrixClientsHolder.remove(child.sessionId) }
)
}
observeLoggedInState()
}
private fun observeLoggedInState() {
authenticationService.isLoggedIn()
.distinctUntilChanged()
.onEach { isLoggedIn ->
Timber.v("isLoggedIn=$isLoggedIn")
if (isLoggedIn) {
val matrixClient = authenticationService.restoreSession()
if (matrixClient == null) {
backstack.newRoot(NavTarget.NotLoggedInFlow)
} else {
matrixClientsHolder[matrixClient.sessionId] = matrixClient
backstack.newRoot(NavTarget.LoggedInFlow(matrixClient.sessionId))
}
tryToRestoreLatestSession(
onSuccess = { switchToLoggedInFlow(it) },
onFailure = { switchToLogoutFlow() }
)
} else {
backstack.newRoot(NavTarget.NotLoggedInFlow)
switchToLogoutFlow()
}
}
.launchIn(lifecycleScope)
}
private fun switchToLoggedInFlow(sessionId: SessionId) {
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId = sessionId))
}
private fun switchToLogoutFlow() {
matrixClientsHolder.removeAll()
backstack.safeRoot(NavTarget.NotLoggedInFlow)
}
private suspend fun tryToRestoreLatestSession(
onSuccess: (SessionId) -> Unit = {},
onFailure: () -> Unit = {}
) {
val latestKnownSessionId = authenticationService.getLatestSessionId()
if (latestKnownSessionId == null) {
onFailure()
return
}
if (matrixClientsHolder.knowSession(latestKnownSessionId)) {
onSuccess(latestKnownSessionId)
return
}
val matrixClient = authenticationService.restoreSession(latestKnownSessionId)
if (matrixClient == null) {
Timber.v("Failed to restore session...")
onFailure()
} else {
matrixClientsHolder.add(matrixClient)
onSuccess(matrixClient.sessionId)
}
}
private fun onOpenBugReport() {
backstack.push(NavTarget.BugReport)
}
@Composable
override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState()
val state = presenter.present()
RootView(
state = state,
modifier = modifier,
onOpenBugReport = this::onOpenBugReport,
) {
Children(navModel = backstack)
Children(
navModel = backstack,
// Animate opening the bug report screen
transitionHandler = rememberDefaultTransitionHandler(),
)
}
}
@@ -136,8 +163,10 @@ class RootFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
is NavTarget.LoggedInFlow -> {
val matrixClient =
matrixClientsHolder[navTarget.sessionId] ?: throw IllegalStateException("Makes sure to give a matrixClient with the given sessionId")
val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
Timber.w("Couldn't find any session, go through SplashScreen")
backstack.newRoot(NavTarget.SplashScreen)
}
LoggedInFlowNode(
buildContext = buildContext,
sessionId = navTarget.sessionId,
@@ -146,12 +175,14 @@ class RootFlowNode(
)
}
NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext)
NavTarget.SplashScreen -> node(buildContext) {
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
NavTarget.SplashScreen -> splashNode(buildContext)
NavTarget.BugReport -> createNode<BugReportNode>(buildContext, plugins = listOf(bugReportNodeCallback))
}
}
private fun splashNode(buildContext: BuildContext) = node(buildContext) {
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
}

View File

@@ -19,10 +19,10 @@ package io.element.android.x.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Presenter
import io.element.android.x.features.rageshake.bugreport.BugReportPresenter
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionPresenter
import io.element.android.x.features.rageshake.detection.RageshakeDetectionPresenter
import io.element.android.features.rageshake.bugreport.BugReportPresenter
import io.element.android.features.rageshake.crash.ui.CrashDetectionPresenter
import io.element.android.features.rageshake.detection.RageshakeDetectionPresenter
import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject
class RootPresenter @Inject constructor(

View File

@@ -17,9 +17,9 @@
package io.element.android.x.root
import androidx.compose.runtime.Stable
import io.element.android.x.features.rageshake.bugreport.BugReportState
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionState
import io.element.android.x.features.rageshake.detection.RageshakeDetectionState
import io.element.android.features.rageshake.bugreport.BugReportState
import io.element.android.features.rageshake.crash.ui.CrashDetectionState
import io.element.android.features.rageshake.detection.RageshakeDetectionState
@Stable
data class RootState(

View File

@@ -24,12 +24,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import io.element.android.features.rageshake.crash.ui.CrashDetectionEvents
import io.element.android.features.rageshake.crash.ui.CrashDetectionView
import io.element.android.features.rageshake.detection.RageshakeDetectionEvents
import io.element.android.features.rageshake.detection.RageshakeDetectionView
import io.element.android.tests.uitests.openShowkase
import io.element.android.x.component.ShowkaseButton
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionEvents
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionView
import io.element.android.x.features.rageshake.detection.RageshakeDetectionEvents
import io.element.android.x.features.rageshake.detection.RageshakeDetectionView
import io.element.android.x.tests.uitests.openShowkase
@Composable
fun RootView(

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<inset
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@mipmap/ic_launcher_round"
android:insetTop="80dp"
android:insetRight="80dp"
android:insetBottom="80dp"
android:insetLeft="80dp" />

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<resources>
<style name="Theme.ElementX.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/black</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
</style>
<style name="Theme.ElementX" parent="Theme.ElementAndroid" />
</resources>

View File

@@ -16,6 +16,10 @@
-->
<resources>
<style name="Theme.ElementX.Splash" parent="Theme.SplashScreen.IconBackground">
<item name="windowSplashScreenBackground">@color/white</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
</style>
<style name="Theme.ElementX" parent="Theme.ElementAndroid" />
</resources>

View File

@@ -29,6 +29,8 @@ plugins {
alias(libs.plugins.detekt)
alias(libs.plugins.ktlint)
alias(libs.plugins.dependencygraph)
alias(libs.plugins.sonarqube)
alias(libs.plugins.kover)
}
tasks.register<Delete>("clean").configure {
@@ -108,3 +110,68 @@ allprojects {
plugin("org.owasp.dependencycheck")
}
}
// To run a sonar analysis:
// Run './gradlew sonar -Dsonar.login=<SONAR_LOGIN>'
// The SONAR_LOGIN is stored in passbolt as Token Sonar Cloud Bma
// Sonar result can be found here: https://sonarcloud.io/project/overview?id=vector-im_element-x-android
sonar {
properties {
property("sonar.projectName", "element-x-android")
property("sonar.projectKey", "vector-im_element-x-android")
property("sonar.host.url", "https://sonarcloud.io")
property("sonar.projectVersion", "1.0") // TODO project(":app").android.defaultConfig.versionName)
property("sonar.sourceEncoding", "UTF-8")
property("sonar.links.homepage", "https://github.com/vector-im/element-x-android/")
property("sonar.links.ci", "https://github.com/vector-im/element-x-android/actions")
property("sonar.links.scm", "https://github.com/vector-im/element-x-android/")
property("sonar.links.issue", "https://github.com/vector-im/element-x-android/issues")
property("sonar.organization", "new_vector_ltd_organization")
property("sonar.login", if (project.hasProperty("SONAR_LOGIN")) project.property("SONAR_LOGIN")!! else "invalid")
// exclude source code from analyses separated by a colon (:)
// Exclude Java source
property("sonar.exclusions", "**/BugReporterMultipartBody.java")
}
}
allprojects {
val projectDir = projectDir.toString()
sonar {
properties {
// Note: folders `kotlin` are not supported (yet), I asked on their side: https://community.sonarsource.com/t/82824
// As a workaround provide the path in `sonar.sources` property.
if (File("$projectDir/src/main/kotlin").exists()) {
property("sonar.sources", "src/main/kotlin")
}
if (File("$projectDir/src/test/kotlin").exists()) {
property("sonar.tests", "src/test/kotlin")
}
}
}
}
allprojects {
apply(plugin = "kover")
}
// Run `./gradlew koverMergedHtmlReport` to get report at ./build/reports/kover
// Run `./gradlew koverMergedReport` to also get XML report
koverMerged {
enable()
filters {
classes {
excludes.addAll(
listOf(
/*
"*Fragment",
"*Fragment\$*",
"*Activity",
"*Activity\$*",
*/
)
)
}
}
}

View File

@@ -24,7 +24,7 @@ plugins {
}
android {
namespace = "io.element.android.x.features.login"
namespace = "io.element.android.features.login"
}
anvil {
@@ -32,16 +32,15 @@ anvil {
}
dependencies {
implementation(project(":anvilannotations"))
anvil(project(":anvilcodegen"))
implementation(project(":libraries:di"))
implementation(project(":libraries:core"))
implementation(project(":libraries:architecture"))
implementation(project(":libraries:matrix"))
implementation(project(":libraries:designsystem"))
implementation(project(":libraries:elementresources"))
implementation(libs.appyx.core)
implementation(project(":libraries:ui-strings"))
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
ksp(libs.showkase.processor)
testImplementation(libs.test.junit)
androidTestImplementation(libs.test.junitext)

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.login
package io.element.android.features.login
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
@@ -33,6 +33,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.x.features.login.test", appContext.packageName)
assertEquals("io.element.android.features.login.test", appContext.packageName)
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.login
package io.element.android.features.login
import android.os.Parcelable
import androidx.compose.runtime.Composable
@@ -25,9 +25,10 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.x.architecture.createNode
import io.element.android.x.features.login.changeserver.ChangeServerNode
import io.element.android.x.features.login.root.LoginRootNode
import io.element.android.features.login.changeserver.ChangeServerNode
import io.element.android.features.login.root.LoginRootNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import kotlinx.parcelize.Parcelize
class LoginFlowNode(
@@ -64,6 +65,11 @@ class LoginFlowNode(
@Composable
override fun View(modifier: Modifier) {
Children(navModel = backstack)
Children(
navModel = backstack,
modifier = modifier,
// Animate transition to change server screen
transitionHandler = rememberDefaultTransitionHandler(),
)
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.login.changeserver
package io.element.android.features.login.changeserver
sealed interface ChangeServerEvents {
data class SetServer(val server: String) : ChangeServerEvents

View File

@@ -14,20 +14,17 @@
* limitations under the License.
*/
package io.element.android.x.features.login.changeserver
package io.element.android.features.login.changeserver
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
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.x.anvilannotations.ContributesNode
import io.element.android.x.architecture.presenterConnector
import io.element.android.x.di.AppScope
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class)
class ChangeServerNode @AssistedInject constructor(
@@ -36,17 +33,16 @@ class ChangeServerNode @AssistedInject constructor(
private val presenter: ChangeServerPresenter,
) : Node(buildContext, plugins = plugins) {
private val presenterConnector = presenterConnector(presenter)
private fun onSuccess() {
navigateUp()
}
@Composable
override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState()
val state = presenter.present()
ChangeServerView(
state = state,
modifier = modifier,
onChangeServerSuccess = this::onSuccess,
)
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.login.changeserver
package io.element.android.features.login.changeserver
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
@@ -22,10 +22,10 @@ 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.x.architecture.Async
import io.element.android.x.architecture.Presenter
import io.element.android.x.architecture.execute
import io.element.android.x.matrix.auth.MatrixAuthenticationService
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject

View File

@@ -14,9 +14,9 @@
* limitations under the License.
*/
package io.element.android.x.features.login.changeserver
package io.element.android.features.login.changeserver
import io.element.android.x.architecture.Async
import io.element.android.libraries.architecture.Async
data class ChangeServerState(
val homeserver: String = "",

View File

@@ -16,7 +16,7 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.x.features.login.changeserver
package io.element.android.features.login.changeserver
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -52,11 +52,13 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.x.architecture.Async
import io.element.android.x.core.compose.textFieldState
import io.element.android.x.designsystem.components.VectorIcon
import io.element.android.x.features.login.R
import io.element.android.x.features.login.error.changeServerError
import io.element.android.features.login.R
import io.element.android.features.login.error.changeServerError
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.VectorIcon
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
@Composable
fun ChangeServerView(
@@ -129,6 +131,7 @@ fun ChangeServerView(
value = homeserverFieldState,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.changeServerServer)
.padding(top = 200.dp),
onValueChange = {
homeserverFieldState = it
@@ -162,6 +165,7 @@ fun ChangeServerView(
enabled = state.submitEnabled,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.changeServerContinue)
.padding(top = 44.dp)
) {
Text(text = "Continue")

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package io.element.android.x.features.login.error
package io.element.android.features.login.error
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.x.core.uri.isValidUrl
import io.element.android.x.features.login.root.LoginFormState
import io.element.android.x.ui.strings.R as StringR
import io.element.android.features.login.root.LoginFormState
import io.element.android.libraries.core.uri.isValidUrl
import io.element.android.libraries.ui.strings.R as StringR
@Composable
fun loginError(

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.login.root
package io.element.android.features.login.root
sealed interface LoginRootEvents {
object RefreshHomeServer : LoginRootEvents

View File

@@ -14,11 +14,9 @@
* limitations under the License.
*/
package io.element.android.x.features.login.root
package io.element.android.features.login.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import com.bumble.appyx.core.modality.BuildContext
@@ -27,10 +25,9 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesNode
import io.element.android.x.architecture.presenterConnector
import io.element.android.x.core.compose.OnLifecycleEvent
import io.element.android.x.di.AppScope
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class)
class LoginRootNode @AssistedInject constructor(
@@ -39,8 +36,6 @@ class LoginRootNode @AssistedInject constructor(
private val presenter: LoginRootPresenter,
) : Node(buildContext, plugins = plugins) {
private val presenterConnector = presenterConnector(presenter)
interface Callback : Plugin {
fun onChangeHomeServer()
}
@@ -51,7 +46,7 @@ class LoginRootNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState()
val state = presenter.present()
OnLifecycleEvent { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> state.eventSink(LoginRootEvents.RefreshHomeServer)
@@ -60,6 +55,7 @@ class LoginRootNode @AssistedInject constructor(
}
LoginRootScreen(
state = state,
modifier = modifier,
onChangeServer = this::onChangeHomeServer,
)
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.login.root
package io.element.android.features.login.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
@@ -22,8 +22,8 @@ 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.x.architecture.Presenter
import io.element.android.x.matrix.auth.MatrixAuthenticationService
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject

View File

@@ -16,7 +16,7 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.x.features.login.root
package io.element.android.features.login.root
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -58,10 +58,12 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.x.core.compose.textFieldState
import io.element.android.x.features.login.error.loginError
import io.element.android.x.matrix.core.SessionId
import io.element.android.x.ui.strings.R as StringR
import io.element.android.features.login.error.loginError
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.matrix.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
@@ -127,6 +129,7 @@ fun LoginRootScreen(
onClick = onChangeServer,
modifier = Modifier
.align(Alignment.CenterEnd)
.testTag(TestTags.loginChangeServer)
.padding(top = 8.dp, end = 8.dp),
content = {
Text(text = "Change")
@@ -137,6 +140,7 @@ fun LoginRootScreen(
value = loginFieldState,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginEmailUsername)
.padding(top = 60.dp),
label = {
Text(text = stringResource(id = StringR.string.login_signin_username_hint))
@@ -159,6 +163,7 @@ fun LoginRootScreen(
value = passwordFieldState,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginPassword)
.padding(top = 24.dp),
onValueChange = {
passwordFieldState = it
@@ -202,6 +207,7 @@ fun LoginRootScreen(
enabled = state.submitEnabled,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginContinue)
.padding(vertical = 32.dp)
) {
Text(text = "Continue")

View File

@@ -14,10 +14,10 @@
* limitations under the License.
*/
package io.element.android.x.features.login.root
package io.element.android.features.login.root
import android.os.Parcelable
import io.element.android.x.matrix.core.SessionId
import io.element.android.libraries.matrix.core.SessionId
import kotlinx.parcelize.Parcelize
data class LoginRootState(

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.login
package io.element.android.features.login
import org.junit.Assert.assertEquals
import org.junit.Test

View File

@@ -23,7 +23,7 @@ plugins {
}
android {
namespace = "io.element.android.x.features.logout"
namespace = "io.element.android.features.logout"
}
anvil {
@@ -31,15 +31,14 @@ anvil {
}
dependencies {
implementation(project(":anvilannotations"))
anvil(project(":anvilcodegen"))
implementation(project(":libraries:di"))
implementation(project(":libraries:architecture"))
implementation(project(":libraries:core"))
implementation(project(":libraries:matrix"))
implementation(project(":libraries:designsystem"))
implementation(project(":libraries:elementresources"))
implementation(project(":libraries:ui-strings"))
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.matrix)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources)
implementation(projects.libraries.uiStrings)
ksp(libs.showkase.processor)
testImplementation(libs.test.junit)
androidTestImplementation(libs.test.junitext)

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.login
package io.element.android.features.logout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
@@ -33,6 +33,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.x.features.login.test", appContext.packageName)
assertEquals("io.element.android.features.login.test", appContext.packageName)
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.logout
package io.element.android.features.logout
sealed interface LogoutPreferenceEvents {
object Logout : LogoutPreferenceEvents

View File

@@ -14,22 +14,23 @@
* limitations under the License.
*/
package io.element.android.x.features.logout
package io.element.android.features.logout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.x.architecture.Async
import io.element.android.x.architecture.Presenter
import io.element.android.x.architecture.execute
import io.element.android.x.matrix.MatrixClient
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) : Presenter<LogoutPreferenceState> {
class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) :
Presenter<LogoutPreferenceState> {
@Composable
override fun present(): LogoutPreferenceState {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.logout
package io.element.android.features.logout
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Logout
@@ -24,12 +24,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.x.architecture.Async
import io.element.android.x.designsystem.components.ProgressDialog
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.x.designsystem.components.preferences.PreferenceCategory
import io.element.android.x.designsystem.components.preferences.PreferenceText
import io.element.android.x.ui.strings.R as StringR
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.ui.strings.R as StringR
@Composable
fun LogoutPreferenceView(

View File

@@ -14,9 +14,9 @@
* limitations under the License.
*/
package io.element.android.x.features.logout
package io.element.android.features.logout
import io.element.android.x.architecture.Async
import io.element.android.libraries.architecture.Async
data class LogoutPreferenceState(
val logoutAction: Async<Unit> = Async.Uninitialized,

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.login
package io.element.android.features.logout
import org.junit.Assert.assertEquals
import org.junit.Test

View File

@@ -23,7 +23,7 @@ plugins {
}
android {
namespace = "io.element.android.x.features.messages"
namespace = "io.element.android.features.messages"
}
anvil {
@@ -31,16 +31,14 @@ anvil {
}
dependencies {
implementation(project(":anvilannotations"))
anvil(project(":anvilcodegen"))
implementation(project(":libraries:di"))
implementation(project(":libraries:core"))
implementation(project(":libraries:architecture"))
implementation(project(":libraries:matrix"))
implementation(project(":libraries:matrixui"))
implementation(project(":libraries:designsystem"))
implementation(project(":libraries:textcomposer"))
implementation(libs.appyx.core)
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.textcomposer)
implementation(libs.coil.compose)
implementation(libs.datetime)
implementation(libs.accompanist.flowlayout)

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages
package io.element.android.features.messages
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
@@ -33,6 +33,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.x.features.messages.test", appContext.packageName)
assertEquals("io.element.android.features.messages.test", appContext.packageName)
}
}

View File

@@ -14,10 +14,10 @@
* limitations under the License.
*/
package io.element.android.x.features.messages
package io.element.android.features.messages
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.features.messages.timeline.model.TimelineItem
sealed interface MessagesEvents {
data class HandleAction(val action: TimelineItemAction, val messageEvent: TimelineItem.MessageEvent) : MessagesEvents

View File

@@ -14,33 +14,28 @@
* limitations under the License.
*/
package io.element.android.x.features.messages
package io.element.android.features.messages
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
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.x.anvilannotations.ContributesNode
import io.element.android.x.architecture.presenterConnector
import io.element.android.x.di.RoomScope
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.RoomScope
@ContributesNode(RoomScope::class)
class MessagesNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenter: MessagesPresenter,
private val presenter: MessagesPresenter,
) : Node(buildContext, plugins = plugins) {
private val connector = presenterConnector(presenter)
@Composable
override fun View(modifier: Modifier) {
val state by connector.stateFlow.collectAsState()
val state = presenter.present()
MessagesView(
state = state,
onBackPressed = this::navigateUp,

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages
package io.element.android.features.messages
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -24,37 +24,32 @@ 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.x.architecture.Presenter
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.features.messages.actionlist.ActionListPresenter
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.textcomposer.MessageComposerEvents
import io.element.android.x.features.messages.textcomposer.MessageComposerPresenter
import io.element.android.x.features.messages.textcomposer.MessageComposerState
import io.element.android.x.features.messages.timeline.TimelineEvents
import io.element.android.x.features.messages.timeline.TimelinePresenter
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.ui.MatrixItemHelper
import io.element.android.x.textcomposer.MessageComposerMode
import io.element.android.features.messages.actionlist.ActionListPresenter
import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.features.messages.textcomposer.MessageComposerEvents
import io.element.android.features.messages.textcomposer.MessageComposerPresenter
import io.element.android.features.messages.textcomposer.MessageComposerState
import io.element.android.features.messages.timeline.TimelineEvents
import io.element.android.features.messages.timeline.TimelinePresenter
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContent
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.textcomposer.MessageComposerMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class MessagesPresenter @Inject constructor(
private val matrixClient: MatrixClient,
private val room: MatrixRoom,
private val composerPresenter: MessageComposerPresenter,
private val timelinePresenter: TimelinePresenter,
private val actionListPresenter: ActionListPresenter,
) : Presenter<MessagesState> {
private val matrixItemHelper = MatrixItemHelper(matrixClient)
@Composable
override fun present(): MessagesState {
val localCoroutineScope = rememberCoroutineScope()
@@ -71,8 +66,9 @@ class MessagesPresenter @Inject constructor(
}
LaunchedEffect(syncUpdateFlow) {
roomAvatar.value =
matrixItemHelper.loadAvatarData(
room = room,
AvatarData(
name = room.bestName,
url = room.avatarUrl,
size = AvatarSize.SMALL
)
roomName.value = room.name

View File

@@ -14,14 +14,14 @@
* limitations under the License.
*/
package io.element.android.x.features.messages
package io.element.android.features.messages
import androidx.compose.runtime.Immutable
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.messages.actionlist.ActionListState
import io.element.android.x.features.messages.textcomposer.MessageComposerState
import io.element.android.x.features.messages.timeline.TimelineState
import io.element.android.x.matrix.core.RoomId
import io.element.android.features.messages.actionlist.ActionListState
import io.element.android.features.messages.textcomposer.MessageComposerState
import io.element.android.features.messages.timeline.TimelineState
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.RoomId
@Immutable
data class MessagesState(

View File

@@ -19,7 +19,7 @@
ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class,
)
package io.element.android.x.features.messages
package io.element.android.features.messages
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -56,15 +56,15 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.designsystem.components.avatar.Avatar
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.messages.actionlist.ActionListEvents
import io.element.android.x.features.messages.actionlist.ActionListView
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.textcomposer.MessageComposerView
import io.element.android.x.features.messages.timeline.TimelineView
import io.element.android.features.messages.actionlist.ActionListEvents
import io.element.android.features.messages.actionlist.ActionListView
import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.features.messages.textcomposer.MessageComposerView
import io.element.android.features.messages.timeline.TimelineView
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.utils.LogCompositions
import kotlinx.coroutines.launch
import timber.log.Timber

View File

@@ -14,9 +14,9 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.actionlist
package io.element.android.features.messages.actionlist
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.TimelineItem
sealed interface ActionListEvents {
object Clear : ActionListEvents

View File

@@ -14,17 +14,17 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.actionlist
package io.element.android.features.messages.actionlist
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.x.architecture.Presenter
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.libraries.architecture.Presenter
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.actionlist
package io.element.android.features.messages.actionlist
import androidx.compose.runtime.Immutable
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.features.messages.timeline.model.TimelineItem
import kotlinx.collections.immutable.ImmutableList
@Immutable

View File

@@ -16,7 +16,7 @@
@file:OptIn(ExperimentalMaterialApi::class)
package io.element.android.x.features.messages.actionlist
package io.element.android.features.messages.actionlist
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -41,9 +41,9 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import io.element.android.x.designsystem.components.VectorIcon
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.libraries.designsystem.components.VectorIcon
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch

View File

@@ -14,11 +14,10 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.actionlist.model
package io.element.android.features.messages.actionlist.model
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import io.element.android.x.designsystem.VectorIcons
@Immutable
sealed class TimelineItemAction(
@@ -26,9 +25,9 @@ sealed class TimelineItemAction(
@DrawableRes val icon: Int,
val destructive: Boolean = false
) {
object Forward : TimelineItemAction("Forward", VectorIcons.ArrowForward)
object Copy : TimelineItemAction("Copy", VectorIcons.Copy)
object Redact : TimelineItemAction("Redact", VectorIcons.Delete, destructive = true)
object Reply : TimelineItemAction("Reply", VectorIcons.Reply)
object Edit : TimelineItemAction("Edit", VectorIcons.Edit)
object Forward : TimelineItemAction("Forward", io.element.android.libraries.designsystem.VectorIcons.ArrowForward)
object Copy : TimelineItemAction("Copy", io.element.android.libraries.designsystem.VectorIcons.Copy)
object Redact : TimelineItemAction("Redact", io.element.android.libraries.designsystem.VectorIcons.Delete, destructive = true)
object Reply : TimelineItemAction("Reply", io.element.android.libraries.designsystem.VectorIcons.Reply)
object Edit : TimelineItemAction("Edit", io.element.android.libraries.designsystem.VectorIcons.Edit)
}

View File

@@ -14,9 +14,9 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.textcomposer
package io.element.android.features.messages.textcomposer
import io.element.android.x.textcomposer.MessageComposerMode
import io.element.android.libraries.textcomposer.MessageComposerMode
sealed interface MessageComposerEvents {
object ToggleFullScreenState : MessageComposerEvents

View File

@@ -14,18 +14,19 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.textcomposer
package io.element.android.features.messages.textcomposer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Presenter
import io.element.android.x.core.data.StableCharSequence
import io.element.android.x.core.data.toStableCharSequence
import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.textcomposer.MessageComposerMode
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.textcomposer.MessageComposerMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -40,7 +41,7 @@ class MessageComposerPresenter @Inject constructor(
val isFullScreen = rememberSaveable {
mutableStateOf(false)
}
val text: MutableState<StableCharSequence> = rememberSaveable {
val text: MutableState<StableCharSequence> = remember {
mutableStateOf(StableCharSequence(""))
}
val composerMode: MutableState<MessageComposerMode> = rememberSaveable {

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.textcomposer
package io.element.android.features.messages.textcomposer
import androidx.compose.runtime.Immutable
import io.element.android.x.core.data.StableCharSequence
import io.element.android.x.textcomposer.MessageComposerMode
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.textcomposer.MessageComposerMode
@Immutable
data class MessageComposerState(

View File

@@ -14,12 +14,12 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.textcomposer
package io.element.android.features.messages.textcomposer
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.x.designsystem.LocalIsDarkTheme
import io.element.android.x.textcomposer.TextComposer
import io.element.android.libraries.designsystem.LocalIsDarkTheme
import io.element.android.libraries.textcomposer.TextComposer
@Composable
fun MessageComposerView(

View File

@@ -14,9 +14,9 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline
package io.element.android.features.messages.timeline
import io.element.android.x.matrix.core.EventId
import io.element.android.libraries.matrix.core.EventId
sealed interface TimelineEvents {
object LoadMore : TimelineEvents

View File

@@ -14,30 +14,31 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline
package io.element.android.features.messages.timeline
import androidx.recyclerview.widget.DiffUtil
import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.features.messages.timeline.diff.CacheInvalidator
import io.element.android.x.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback
import io.element.android.x.features.messages.timeline.model.AggregatedReaction
import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.timeline.model.TimelineItemReactions
import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEmoteContent
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent
import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent
import io.element.android.x.features.messages.timeline.model.content.TimelineItemNoticeContent
import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextContent
import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent
import io.element.android.x.features.messages.timeline.util.invalidateLast
import io.element.android.x.matrix.core.EventId
import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimelineItem
import io.element.android.x.matrix.ui.MatrixItemHelper
import io.element.android.features.messages.timeline.diff.CacheInvalidator
import io.element.android.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemEmoteContent
import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent
import io.element.android.features.messages.timeline.model.content.TimelineItemNoticeContent
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemTextContent
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
import io.element.android.features.messages.timeline.util.invalidateLast
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.media.MediaResolver
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.ui.MatrixItemHelper
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
@@ -154,12 +155,11 @@ class TimelineItemsFactory @Inject constructor(
computeGroupPosition(currentTimelineItem, timelineItems, index)
val senderDisplayName = room.userDisplayName(currentSender).getOrNull()
val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull()
val senderAvatarData =
matrixItemHelper.loadAvatarData(
name = senderDisplayName ?: currentSender,
url = senderAvatarUrl,
size = AvatarSize.SMALL
)
val senderAvatarData = AvatarData(
name = senderDisplayName ?: currentSender,
url = senderAvatarUrl,
size = AvatarSize.SMALL
)
return TimelineItem.MessageEvent(
id = EventId(currentTimelineItem.uniqueId),
senderId = currentSender,

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline
package io.element.android.features.messages.timeline
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -24,14 +24,14 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Presenter
import io.element.android.x.core.coroutine.CoroutineDispatchers
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.core.EventId
import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimeline
import io.element.android.x.matrix.timeline.MatrixTimelineItem
import io.element.android.x.matrix.ui.MatrixItemHelper
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.MatrixClient
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.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.ui.MatrixItemHelper
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline
package io.element.android.features.messages.timeline
import androidx.compose.runtime.Immutable
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.matrix.core.EventId
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.libraries.matrix.core.EventId
import kotlinx.collections.immutable.ImmutableList
@Immutable

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline
package io.element.android.features.messages.timeline
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@@ -58,29 +58,29 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import io.element.android.x.core.compose.PairCombinedPreviewParameter
import io.element.android.x.designsystem.components.avatar.Avatar
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.messages.timeline.model.AggregatedReaction
import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.x.features.messages.timeline.model.TimelineItemGroupPositionProvider
import io.element.android.x.features.messages.timeline.model.TimelineItemReactions
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.x.features.messages.timeline.model.content.MessagesTimelineItemContentProvider
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent
import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent
import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent
import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent
import io.element.android.x.features.messages.timeline.components.MessageEventBubble
import io.element.android.x.features.messages.timeline.components.TimelineItemReactionsView
import io.element.android.x.features.messages.timeline.components.TimelineItemEncryptedView
import io.element.android.x.features.messages.timeline.components.TimelineItemImageView
import io.element.android.x.features.messages.timeline.components.TimelineItemRedactedView
import io.element.android.x.features.messages.timeline.components.TimelineItemTextView
import io.element.android.x.features.messages.timeline.components.TimelineItemUnknownView
import io.element.android.x.matrix.core.EventId
import io.element.android.features.messages.timeline.components.MessageEventBubble
import io.element.android.features.messages.timeline.components.TimelineItemEncryptedView
import io.element.android.features.messages.timeline.components.TimelineItemImageView
import io.element.android.features.messages.timeline.components.TimelineItemReactionsView
import io.element.android.features.messages.timeline.components.TimelineItemRedactedView
import io.element.android.features.messages.timeline.components.TimelineItemTextView
import io.element.android.features.messages.timeline.components.TimelineItemUnknownView
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.TimelineItemGroupPositionProvider
import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.features.messages.timeline.model.content.MessagesTimelineItemContentProvider
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.utils.PairCombinedPreviewParameter
import io.element.android.libraries.matrix.core.EventId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.distinctUntilChanged

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.components
package io.element.android.features.messages.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
@@ -29,14 +29,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
import io.element.android.x.designsystem.LocalIsDarkTheme
import io.element.android.x.designsystem.MessageHighlightDark
import io.element.android.x.designsystem.MessageHighlightLight
import io.element.android.x.designsystem.SystemGrey5Dark
import io.element.android.x.designsystem.SystemGrey5Light
import io.element.android.x.designsystem.SystemGrey6Dark
import io.element.android.x.designsystem.SystemGrey6Light
import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.libraries.designsystem.LocalIsDarkTheme
import io.element.android.libraries.designsystem.MessageHighlightDark
import io.element.android.libraries.designsystem.MessageHighlightLight
import io.element.android.libraries.designsystem.SystemGrey5Dark
import io.element.android.libraries.designsystem.SystemGrey5Light
import io.element.android.libraries.designsystem.SystemGrey6Dark
import io.element.android.libraries.designsystem.SystemGrey6Light
private val BUBBLE_RADIUS = 16.dp

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.components
package io.element.android.features.messages.timeline.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent
@Composable
fun TimelineItemEncryptedView(

View File

@@ -16,7 +16,7 @@
@file:OptIn(ExperimentalFoundationApi::class)
package io.element.android.x.features.messages.timeline.components
package io.element.android.features.messages.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
@@ -33,7 +33,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil.compose.AsyncImage
import coil.request.ImageRequest
import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent
@Composable
fun TimelineItemImageView(

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.components
package io.element.android.features.messages.timeline.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.components
package io.element.android.features.messages.timeline.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Row
@@ -32,8 +32,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import io.element.android.x.features.messages.timeline.model.AggregatedReaction
import io.element.android.x.features.messages.timeline.model.TimelineItemReactions
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.TimelineItemReactions
@Composable
fun TimelineItemReactionsView(

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.components
package io.element.android.features.messages.timeline.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
@Composable
fun TimelineItemRedactedView(

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.components
package io.element.android.features.messages.timeline.components
import android.text.SpannableString
import android.text.style.URLSpan
@@ -28,10 +28,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.core.text.util.LinkifyCompat
import io.element.android.x.designsystem.LinkColor
import io.element.android.x.designsystem.components.ClickableLinkText
import io.element.android.x.features.messages.timeline.components.html.HtmlDocument
import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent
import io.element.android.features.messages.timeline.components.html.HtmlDocument
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContent
import io.element.android.libraries.designsystem.LinkColor
import io.element.android.libraries.designsystem.components.ClickableLinkText
@Composable
fun TimelineItemTextView(

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.components
package io.element.android.features.messages.timeline.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
@Composable
fun TimelineItemUnknownView(

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.components.html
package io.element.android.features.messages.timeline.components.html
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -47,10 +47,10 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import io.element.android.x.designsystem.LinkColor
import io.element.android.x.designsystem.components.ClickableLinkText
import io.element.android.x.matrix.permalink.PermalinkData
import io.element.android.x.matrix.permalink.PermalinkParser
import io.element.android.libraries.designsystem.LinkColor
import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.matrix.permalink.PermalinkData
import io.element.android.libraries.matrix.permalink.PermalinkParser
import kotlinx.collections.immutable.persistentMapOf
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.diff
package io.element.android.features.messages.timeline.diff
import androidx.recyclerview.widget.ListUpdateCallback
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.timeline.util.invalidateLast
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.util.invalidateLast
import timber.log.Timber
internal class CacheInvalidator(private val itemStatesCache: MutableList<TimelineItem?>) :

View File

@@ -14,10 +14,10 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.diff
package io.element.android.features.messages.timeline.diff
import androidx.recyclerview.widget.DiffUtil
import io.element.android.x.matrix.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
internal class MatrixTimelineItemsDiffCallback(
private val oldList: List<MatrixTimelineItem>,

View File

@@ -14,12 +14,12 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.model
package io.element.android.features.messages.timeline.model
import androidx.compose.runtime.Immutable
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.x.matrix.core.EventId
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.EventId
@Immutable
sealed interface TimelineItem {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.model
package io.element.android.features.messages.timeline.model
import androidx.compose.runtime.Immutable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.model
package io.element.android.features.messages.timeline.model
import kotlinx.collections.immutable.ImmutableList

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.model.content
package io.element.android.features.messages.timeline.model.content
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import org.matrix.rustcomponents.sdk.EncryptedMessage

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.model.content
package io.element.android.features.messages.timeline.model.content
import org.jsoup.nodes.Document

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.model.content
package io.element.android.features.messages.timeline.model.content
import org.matrix.rustcomponents.sdk.EncryptedMessage

View File

@@ -14,9 +14,9 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.model.content
package io.element.android.features.messages.timeline.model.content
import io.element.android.x.matrix.media.MediaResolver
import io.element.android.libraries.matrix.media.MediaResolver
data class TimelineItemImageContent(
val body: String,

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.model.content
package io.element.android.features.messages.timeline.model.content
import org.jsoup.nodes.Document

View File

@@ -14,6 +14,6 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.model.content
package io.element.android.features.messages.timeline.model.content
object TimelineItemRedactedContent : TimelineItemContent

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.features.messages.timeline.model.content
package io.element.android.features.messages.timeline.model.content
import org.jsoup.nodes.Document

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