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
commit 9f252ad621
366 changed files with 3066 additions and 1682 deletions

View file

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

View file

@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Assemble debug APK - name: Assemble debug APK
run: ./gradlew assembleDebug $CI_GRADLE_ARG_PROPERTIES 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: with:
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
app-file: app/build/outputs/apk/debug/app-debug.apk app-file: app/build/outputs/apk/debug/app-debug.apk

View file

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

View file

@ -23,6 +23,17 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Run tests - name: Run tests
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES 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 - name: Archive test results on error
if: failure() if: failure()
@ -32,3 +43,17 @@ jobs:
path: | path: |
**/out/failures/ **/out/failures/
**/build/reports/tests/*UnitTest/ **/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"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="CompilerConfiguration"> <component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" /> <bytecodeTargetLevel target="1.8">
<module name="ElementX.libraries.composeutils" target="11" />
</bytecodeTargetLevel>
</component> </component>
</project> </project>

2
.idea/misc.xml generated
View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <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" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <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 # element-x-android
ElementX Android is a [Matrix](https://matrix.org/) Android Client provided by [Element](https://element.io/). ElementX Android is a [Matrix](https://matrix.org/) Android Client provided by [Element](https://element.io/).

View file

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

View file

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

View file

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

View file

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

View file

@ -31,7 +31,8 @@
tools:targetApi="33"> tools:targetApi="33">
<activity <activity
android:name=".MainActivity" 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:exported="true"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
tools:ignore="LockedOrientationActivity"> tools:ignore="LockedOrientationActivity">

View file

@ -18,9 +18,10 @@ package io.element.android.x
import android.app.Application import android.app.Application
import androidx.startup.AppInitializer 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.AppComponent
import io.element.android.x.di.DaggerAppComponent 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.CrashInitializer
import io.element.android.x.initializer.MatrixInitializer import io.element.android.x.initializer.MatrixInitializer
import io.element.android.x.initializer.TimberInitializer import io.element.android.x.initializer.TimberInitializer
@ -40,5 +41,6 @@ class ElementXApplication : Application(), DaggerComponentOwner {
initializeComponent(TimberInitializer::class.java) initializeComponent(TimberInitializer::class.java)
initializeComponent(MatrixInitializer::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.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.bumble.appyx.core.integration.NodeHost import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
import io.element.android.x.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.x.di.DaggerComponentOwner import io.element.android.libraries.designsystem.ElementXTheme
import io.element.android.x.designsystem.ElementXTheme import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.x.di.AppBindings import io.element.android.x.di.AppBindings
import io.element.android.x.node.RootFlowNode import io.element.android.x.node.RootFlowNode
class MainActivity : NodeComponentActivity() { class MainActivity : NodeComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val appBindings = bindings<AppBindings>() val appBindings = bindings<AppBindings>()
appBindings.matrixClientsHolder().restore(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
setContent { setContent {
ElementXTheme { ElementXTheme {
@ -48,11 +51,17 @@ class MainActivity : NodeComponentActivity() {
buildContext = it, buildContext = it,
appComponentOwner = applicationContext as DaggerComponentOwner, appComponentOwner = applicationContext as DaggerComponentOwner,
authenticationService = appBindings.authenticationService(), 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 package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo 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 io.element.android.x.root.RootPresenter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
interface AppBindings { interface AppBindings {
fun coroutineScope(): CoroutineScope
fun rootPresenter(): RootPresenter fun rootPresenter(): RootPresenter
fun authenticationService(): MatrixAuthenticationService fun authenticationService(): MatrixAuthenticationService
fun matrixClientsHolder(): MatrixClientsHolder
} }

View file

@ -20,7 +20,10 @@ import android.content.Context
import com.squareup.anvil.annotations.MergeComponent import com.squareup.anvil.annotations.MergeComponent
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Component 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) @SingleIn(AppScope::class)
@MergeComponent(AppScope::class) @MergeComponent(AppScope::class)

View file

@ -20,7 +20,10 @@ import android.content.Context
import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.annotations.ContributesTo
import dagger.Module import dagger.Module
import dagger.Provides 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.CoroutineName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers 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 com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Subcomponent import dagger.Subcomponent
import io.element.android.x.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.x.matrix.room.MatrixRoom 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) @SingleIn(RoomScope::class)
@MergeSubcomponent(RoomScope::class) @MergeSubcomponent(RoomScope::class)

View file

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

View file

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

View file

@ -18,8 +18,8 @@ package io.element.android.x.initializer
import android.content.Context import android.content.Context
import androidx.startup.Initializer import androidx.startup.Initializer
import io.element.android.features.rageshake.logs.VectorFileLogger
import io.element.android.x.BuildConfig import io.element.android.x.BuildConfig
import io.element.android.x.features.rageshake.logs.VectorFileLogger
import timber.log.Timber import timber.log.Timber
class TimberInitializer : Initializer<Unit> { 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.core.node.node
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.x.architecture.bindings import io.element.android.features.preferences.PreferencesFlowNode
import io.element.android.x.architecture.createNode import io.element.android.features.roomlist.RoomListNode
import io.element.android.x.di.DaggerComponentOwner 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.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 import kotlinx.parcelize.Parcelize
class LoggedInFlowNode( class LoggedInFlowNode(
@ -124,6 +125,11 @@ class LoggedInFlowNode(
@Composable @Composable
override fun View(modifier: Modifier) { 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.ParentNode
import com.bumble.appyx.core.node.node import com.bumble.appyx.core.node.node
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.replace import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.x.features.login.LoginFlowNode import io.element.android.features.login.LoginFlowNode
import io.element.android.x.features.onboarding.OnBoardingScreen import io.element.android.features.onboarding.OnBoardingScreen
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
@ -62,7 +63,7 @@ class NotLoggedInFlowNode(
return when (navTarget) { return when (navTarget) {
NavTarget.OnBoarding -> node(buildContext) { NavTarget.OnBoarding -> node(buildContext) {
OnBoardingScreen( OnBoardingScreen(
onSignIn = { backstack.replace(NavTarget.LoginFlow) } onSignIn = { backstack.push(NavTarget.LoginFlow) }
) )
} }
NavTarget.LoginFlow -> LoginFlowNode(buildContext) NavTarget.LoginFlow -> LoginFlowNode(buildContext)
@ -71,6 +72,11 @@ class NotLoggedInFlowNode(
@Composable @Composable
override fun View(modifier: Modifier) { 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.Node
import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import io.element.android.x.architecture.bindings import io.element.android.features.messages.MessagesNode
import io.element.android.x.architecture.createNode import io.element.android.libraries.architecture.bindings
import io.element.android.x.di.DaggerComponentOwner 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.di.RoomComponent
import io.element.android.x.features.messages.MessagesNode
import io.element.android.x.matrix.room.MatrixRoom
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
@ -70,6 +70,9 @@ class RoomFlowNode(
@Composable @Composable
override fun View(modifier: Modifier) { 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.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.Children 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.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode 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.newRoot
import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.x.architecture.createNode import io.element.android.features.rageshake.bugreport.BugReportNode
import io.element.android.x.architecture.presenterConnector import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.x.di.DaggerComponentOwner import io.element.android.libraries.architecture.createNode
import io.element.android.x.features.rageshake.bugreport.BugReportNode import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.x.matrix.MatrixClient import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import io.element.android.x.matrix.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.core.SessionId
import io.element.android.x.matrix.core.SessionId import io.element.android.x.di.MatrixClientsHolder
import io.element.android.x.root.RootPresenter import io.element.android.x.root.RootPresenter
import io.element.android.x.root.RootView import io.element.android.x.root.RootView
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@ -50,66 +47,96 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
class RootFlowNode( class RootFlowNode(
buildContext: BuildContext, private val buildContext: BuildContext,
private val backstack: BackStack<NavTarget> = BackStack( private val backstack: BackStack<NavTarget> = BackStack(
initialElement = NavTarget.SplashScreen, initialElement = NavTarget.SplashScreen,
savedStateMap = buildContext.savedStateMap, savedStateMap = buildContext.savedStateMap,
), ),
private val appComponentOwner: DaggerComponentOwner, private val appComponentOwner: DaggerComponentOwner,
private val authenticationService: MatrixAuthenticationService, private val authenticationService: MatrixAuthenticationService,
rootPresenter: RootPresenter private val matrixClientsHolder: MatrixClientsHolder,
private val presenter: RootPresenter
) : ) :
ParentNode<RootFlowNode.NavTarget>( ParentNode<RootFlowNode.NavTarget>(
navModel = backstack, navModel = backstack,
buildContext = buildContext, buildContext = buildContext
), ),
DaggerComponentOwner by appComponentOwner { DaggerComponentOwner by appComponentOwner {
private val matrixClientsHolder = ConcurrentHashMap<SessionId, MatrixClient>()
private val presenterConnector = presenterConnector(rootPresenter)
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
whenChildAttached(LoggedInFlowNode::class) { _, child -> observeLoggedInState()
child.lifecycle.subscribe( }
onDestroy = { matrixClientsHolder.remove(child.sessionId) }
) private fun observeLoggedInState() {
}
authenticationService.isLoggedIn() authenticationService.isLoggedIn()
.distinctUntilChanged() .distinctUntilChanged()
.onEach { isLoggedIn -> .onEach { isLoggedIn ->
Timber.v("isLoggedIn=$isLoggedIn") Timber.v("isLoggedIn=$isLoggedIn")
if (isLoggedIn) { if (isLoggedIn) {
val matrixClient = authenticationService.restoreSession() tryToRestoreLatestSession(
if (matrixClient == null) { onSuccess = { switchToLoggedInFlow(it) },
backstack.newRoot(NavTarget.NotLoggedInFlow) onFailure = { switchToLogoutFlow() }
} else { )
matrixClientsHolder[matrixClient.sessionId] = matrixClient
backstack.newRoot(NavTarget.LoggedInFlow(matrixClient.sessionId))
}
} else { } else {
backstack.newRoot(NavTarget.NotLoggedInFlow) switchToLogoutFlow()
} }
} }
.launchIn(lifecycleScope) .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() { private fun onOpenBugReport() {
backstack.push(NavTarget.BugReport) backstack.push(NavTarget.BugReport)
} }
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState() val state = presenter.present()
RootView( RootView(
state = state, state = state,
modifier = modifier,
onOpenBugReport = this::onOpenBugReport, 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 { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
is NavTarget.LoggedInFlow -> { is NavTarget.LoggedInFlow -> {
val matrixClient = val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
matrixClientsHolder[navTarget.sessionId] ?: throw IllegalStateException("Makes sure to give a matrixClient with the given sessionId") Timber.w("Couldn't find any session, go through SplashScreen")
backstack.newRoot(NavTarget.SplashScreen)
}
LoggedInFlowNode( LoggedInFlowNode(
buildContext = buildContext, buildContext = buildContext,
sessionId = navTarget.sessionId, sessionId = navTarget.sessionId,
@ -146,12 +175,14 @@ class RootFlowNode(
) )
} }
NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext) NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext)
NavTarget.SplashScreen -> node(buildContext) { NavTarget.SplashScreen -> splashNode(buildContext)
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
NavTarget.BugReport -> createNode<BugReportNode>(buildContext, plugins = listOf(bugReportNodeCallback)) 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.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Presenter import io.element.android.features.rageshake.bugreport.BugReportPresenter
import io.element.android.x.features.rageshake.bugreport.BugReportPresenter import io.element.android.features.rageshake.crash.ui.CrashDetectionPresenter
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionPresenter import io.element.android.features.rageshake.detection.RageshakeDetectionPresenter
import io.element.android.x.features.rageshake.detection.RageshakeDetectionPresenter import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject import javax.inject.Inject
class RootPresenter @Inject constructor( class RootPresenter @Inject constructor(

View file

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

View file

@ -24,12 +24,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.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 @Composable
fun RootView( 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> <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" /> <style name="Theme.ElementX" parent="Theme.ElementAndroid" />
</resources> </resources>

View file

@ -29,6 +29,8 @@ plugins {
alias(libs.plugins.detekt) alias(libs.plugins.detekt)
alias(libs.plugins.ktlint) alias(libs.plugins.ktlint)
alias(libs.plugins.dependencygraph) alias(libs.plugins.dependencygraph)
alias(libs.plugins.sonarqube)
alias(libs.plugins.kover)
} }
tasks.register<Delete>("clean").configure { tasks.register<Delete>("clean").configure {
@ -108,3 +110,68 @@ allprojects {
plugin("org.owasp.dependencycheck") 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 { android {
namespace = "io.element.android.x.features.login" namespace = "io.element.android.features.login"
} }
anvil { anvil {
@ -32,16 +32,15 @@ anvil {
} }
dependencies { dependencies {
implementation(project(":anvilannotations")) implementation(projects.anvilannotations)
anvil(project(":anvilcodegen")) anvil(projects.anvilcodegen)
implementation(project(":libraries:di")) implementation(projects.libraries.core)
implementation(project(":libraries:core")) implementation(projects.libraries.architecture)
implementation(project(":libraries:architecture")) implementation(projects.libraries.matrix)
implementation(project(":libraries:matrix")) implementation(projects.libraries.designsystem)
implementation(project(":libraries:designsystem")) implementation(projects.libraries.elementresources)
implementation(project(":libraries:elementresources")) implementation(projects.libraries.testtags)
implementation(libs.appyx.core) implementation(projects.libraries.uiStrings)
implementation(project(":libraries:ui-strings"))
ksp(libs.showkase.processor) ksp(libs.showkase.processor)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
androidTestImplementation(libs.test.junitext) androidTestImplementation(libs.test.junitext)

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
@ -33,6 +33,6 @@ class ExampleInstrumentedTest {
fun useAppContext() { fun useAppContext() {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext 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. * limitations under the License.
*/ */
package io.element.android.x.features.login package io.element.android.features.login
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable 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.core.node.ParentNode
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.x.architecture.createNode import io.element.android.features.login.changeserver.ChangeServerNode
import io.element.android.x.features.login.changeserver.ChangeServerNode import io.element.android.features.login.root.LoginRootNode
import io.element.android.x.features.login.root.LoginRootNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
class LoginFlowNode( class LoginFlowNode(
@ -64,6 +65,11 @@ class LoginFlowNode(
@Composable @Composable
override fun View(modifier: Modifier) { 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. * limitations under the License.
*/ */
package io.element.android.x.features.login.changeserver package io.element.android.features.login.changeserver
sealed interface ChangeServerEvents { sealed interface ChangeServerEvents {
data class SetServer(val server: String) : ChangeServerEvents data class SetServer(val server: String) : ChangeServerEvents

View file

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

View file

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

View file

@ -14,9 +14,9 @@
* limitations under the License. * 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( data class ChangeServerState(
val homeserver: String = "", val homeserver: String = "",

View file

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

View file

@ -14,13 +14,13 @@
* limitations under the License. * 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.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.element.android.x.core.uri.isValidUrl import io.element.android.features.login.root.LoginFormState
import io.element.android.x.features.login.root.LoginFormState import io.element.android.libraries.core.uri.isValidUrl
import io.element.android.x.ui.strings.R as StringR import io.element.android.libraries.ui.strings.R as StringR
@Composable @Composable
fun loginError( fun loginError(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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.Assert.assertEquals
import org.junit.Test import org.junit.Test

View file

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

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
@ -33,6 +33,6 @@ class ExampleInstrumentedTest {
fun useAppContext() { fun useAppContext() {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext 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. * limitations under the License.
*/ */
package io.element.android.x.features.logout package io.element.android.features.logout
sealed interface LogoutPreferenceEvents { sealed interface LogoutPreferenceEvents {
object Logout : LogoutPreferenceEvents object Logout : LogoutPreferenceEvents

View file

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

View file

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

View file

@ -14,9 +14,9 @@
* limitations under the License. * 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( data class LogoutPreferenceState(
val logoutAction: Async<Unit> = Async.Uninitialized, val logoutAction: Async<Unit> = Async.Uninitialized,

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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.Assert.assertEquals
import org.junit.Test import org.junit.Test

View file

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

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
@ -33,6 +33,6 @@ class ExampleInstrumentedTest {
fun useAppContext() { fun useAppContext() {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext 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. * 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.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.timeline.model.TimelineItem import io.element.android.features.messages.timeline.model.TimelineItem
sealed interface MessagesEvents { sealed interface MessagesEvents {
data class HandleAction(val action: TimelineItemAction, val messageEvent: TimelineItem.MessageEvent) : MessagesEvents data class HandleAction(val action: TimelineItemAction, val messageEvent: TimelineItem.MessageEvent) : MessagesEvents

View file

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

View file

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

View file

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

View file

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

View file

@ -14,9 +14,9 @@
* limitations under the License. * 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 { sealed interface ActionListEvents {
object Clear : ActionListEvents object Clear : ActionListEvents

View file

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

View file

@ -14,11 +14,11 @@
* limitations under the License. * 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 androidx.compose.runtime.Immutable
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.timeline.model.TimelineItem import io.element.android.features.messages.timeline.model.TimelineItem
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable @Immutable

View file

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

View file

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

View file

@ -14,9 +14,9 @@
* limitations under the License. * 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 { sealed interface MessageComposerEvents {
object ToggleFullScreenState : MessageComposerEvents object ToggleFullScreenState : MessageComposerEvents

View file

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

View file

@ -14,11 +14,11 @@
* limitations under the License. * 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 androidx.compose.runtime.Immutable
import io.element.android.x.core.data.StableCharSequence import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.x.textcomposer.MessageComposerMode import io.element.android.libraries.textcomposer.MessageComposerMode
@Immutable @Immutable
data class MessageComposerState( data class MessageComposerState(

View file

@ -14,12 +14,12 @@
* limitations under the License. * 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.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import io.element.android.x.designsystem.LocalIsDarkTheme import io.element.android.libraries.designsystem.LocalIsDarkTheme
import io.element.android.x.textcomposer.TextComposer import io.element.android.libraries.textcomposer.TextComposer
@Composable @Composable
fun MessageComposerView( fun MessageComposerView(

View file

@ -14,9 +14,9 @@
* limitations under the License. * 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 { sealed interface TimelineEvents {
object LoadMore : TimelineEvents object LoadMore : TimelineEvents

View file

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

View file

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

View file

@ -14,11 +14,11 @@
* limitations under the License. * 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 androidx.compose.runtime.Immutable
import io.element.android.x.features.messages.timeline.model.TimelineItem import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.x.matrix.core.EventId import io.element.android.libraries.matrix.core.EventId
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable @Immutable

View file

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

View file

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

View file

@ -14,13 +14,13 @@
* limitations under the License. * 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.Icons
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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 @Composable
fun TimelineItemEncryptedView( fun TimelineItemEncryptedView(

View file

@ -16,7 +16,7 @@
@file:OptIn(ExperimentalFoundationApi::class) @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.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -33,7 +33,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest 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 @Composable
fun TimelineItemImageView( fun TimelineItemImageView(

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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.BorderStroke
import androidx.compose.foundation.layout.Row 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import io.element.android.x.features.messages.timeline.model.AggregatedReaction import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.x.features.messages.timeline.model.TimelineItemReactions import io.element.android.features.messages.timeline.model.TimelineItemReactions
@Composable @Composable
fun TimelineItemReactionsView( fun TimelineItemReactionsView(

View file

@ -14,13 +14,13 @@
* limitations under the License. * 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.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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 @Composable
fun TimelineItemRedactedView( fun TimelineItemRedactedView(

View file

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

View file

@ -14,13 +14,13 @@
* limitations under the License. * 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.Icons
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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 @Composable
fun TimelineItemUnknownView( fun TimelineItemUnknownView(

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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.background
import androidx.compose.foundation.interaction.MutableInteractionSource 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import io.element.android.x.designsystem.LinkColor import io.element.android.libraries.designsystem.LinkColor
import io.element.android.x.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.x.matrix.permalink.PermalinkData import io.element.android.libraries.matrix.permalink.PermalinkData
import io.element.android.x.matrix.permalink.PermalinkParser import io.element.android.libraries.matrix.permalink.PermalinkParser
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element

View file

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

View file

@ -14,10 +14,10 @@
* limitations under the License. * 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 androidx.recyclerview.widget.DiffUtil
import io.element.android.x.matrix.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
internal class MatrixTimelineItemsDiffCallback( internal class MatrixTimelineItemsDiffCallback(
private val oldList: List<MatrixTimelineItem>, private val oldList: List<MatrixTimelineItem>,

View file

@ -14,12 +14,12 @@
* limitations under the License. * 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.runtime.Immutable
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.x.matrix.core.EventId import io.element.android.libraries.matrix.core.EventId
@Immutable @Immutable
sealed interface TimelineItem { sealed interface TimelineItem {

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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.runtime.Immutable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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 import kotlinx.collections.immutable.ImmutableList

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
import org.matrix.rustcomponents.sdk.EncryptedMessage import org.matrix.rustcomponents.sdk.EncryptedMessage

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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 import org.jsoup.nodes.Document

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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 import org.matrix.rustcomponents.sdk.EncryptedMessage

View file

@ -14,9 +14,9 @@
* limitations under the License. * 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( data class TimelineItemImageContent(
val body: String, val body: String,

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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 import org.jsoup.nodes.Document

View file

@ -14,6 +14,6 @@
* limitations under the License. * 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 object TimelineItemRedactedContent : TimelineItemContent

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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 import org.jsoup.nodes.Document

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