Merge branch 'develop' into feature/bma/documentation
This commit is contained in:
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
- run: |
|
||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||
- name: Danger
|
||||
uses: danger/danger-js@11.2.2
|
||||
uses: danger/danger-js@11.2.3
|
||||
with:
|
||||
args: "--dangerfile ./tools/danger/dangerfile.js"
|
||||
env:
|
||||
|
||||
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Assemble debug APK
|
||||
run: ./gradlew assembleDebug $CI_GRADLE_ARG_PROPERTIES
|
||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.1.1
|
||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.2.3
|
||||
with:
|
||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||
app-file: app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
yarn add danger-plugin-lint-report --dev
|
||||
- name: Danger lint
|
||||
if: always()
|
||||
uses: danger/danger-js@11.2.2
|
||||
uses: danger/danger-js@11.2.3
|
||||
with:
|
||||
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
||||
env:
|
||||
|
||||
25
.github/workflows/tests.yml
vendored
25
.github/workflows/tests.yml
vendored
@@ -23,6 +23,17 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run tests
|
||||
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Generate kover report
|
||||
if: always()
|
||||
run: ./gradlew koverMergedReport $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
- name: Archive kover report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: kover-results
|
||||
path: |
|
||||
**/build/reports/kover/merged
|
||||
|
||||
- name: Archive test results on error
|
||||
if: failure()
|
||||
@@ -32,3 +43,17 @@ jobs:
|
||||
path: |
|
||||
**/out/failures/
|
||||
**/build/reports/tests/*UnitTest/
|
||||
|
||||
- name: Publish results to Sonar
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
|
||||
if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
|
||||
run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
# https://github.com/codecov/codecov-action
|
||||
- name: Upload coverage reports to codecov
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v3
|
||||
# with:
|
||||
# files: build/reports/kover/merged/xml/report.xml
|
||||
|
||||
4
.idea/compiler.xml
generated
4
.idea/compiler.xml
generated
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
<bytecodeTargetLevel target="1.8">
|
||||
<module name="ElementX.libraries.composeutils" target="11" />
|
||||
</bytecodeTargetLevel>
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="11" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
[](https://github.com/vector-im/element-x-android/actions/workflows/build.yml?query=branch%3Adevelop)
|
||||
[](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android)
|
||||
[](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android)
|
||||
[](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android)
|
||||
[](https://codecov.io/github/vector-im/element-x-android)
|
||||
[](https://matrix.to/#/#element-android:matrix.org)
|
||||
[](https://translate.element.io/engage/element-android/?utm_source=widget)
|
||||
|
||||
# element-x-android
|
||||
|
||||
ElementX Android is a [Matrix](https://matrix.org/) Android Client provided by [Element](https://element.io/).
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.anvilannotations
|
||||
package io.element.android.anvilannotations
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@@ -22,7 +22,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":anvilannotations"))
|
||||
implementation(projects.anvilannotations)
|
||||
api(libs.anvil.compiler.api)
|
||||
implementation(libs.anvil.compiler.utils)
|
||||
implementation("com.squareup:kotlinpoet:1.12.0")
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
@file:OptIn(ExperimentalAnvilApi::class)
|
||||
|
||||
package io.element.android.x.anvilcodegen
|
||||
package io.element.android.anvilcodegen
|
||||
|
||||
import com.google.auto.service.AutoService
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
@@ -46,7 +46,7 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.element.android.x.anvilannotations.ContributesNode
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
@@ -148,7 +148,7 @@ class ContributesNodeCodeGenerator : CodeGenerator {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val assistedNodeFactoryFqName = FqName("io.element.android.x.architecture.AssistedNodeFactory")
|
||||
private val nodeKeyFqName = FqName("io.element.android.x.architecture.NodeKey")
|
||||
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
|
||||
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
@@ -164,14 +162,14 @@ knit {
|
||||
dependencies {
|
||||
allLibraries()
|
||||
allFeatures()
|
||||
implementation(project(":tests:uitests"))
|
||||
implementation(project(":anvilannotations"))
|
||||
anvil(project(":anvilcodegen"))
|
||||
implementation(projects.tests.uitests)
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
|
||||
// https://developer.android.com/studio/write/java8-support#library-desugaring-versions
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
|
||||
implementation(libs.appyx.core)
|
||||
|
||||
implementation(libs.androidx.splash)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
tools:targetApi="33">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:theme="@style/Theme.ElementX.Splash"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
|
||||
@@ -18,9 +18,10 @@ package io.element.android.x
|
||||
|
||||
import android.app.Application
|
||||
import androidx.startup.AppInitializer
|
||||
import io.element.android.x.di.DaggerComponentOwner
|
||||
import io.element.android.libraries.di.DaggerComponentOwner
|
||||
import io.element.android.x.di.AppComponent
|
||||
import io.element.android.x.di.DaggerAppComponent
|
||||
import io.element.android.x.info.logApplicationInfo
|
||||
import io.element.android.x.initializer.CrashInitializer
|
||||
import io.element.android.x.initializer.MatrixInitializer
|
||||
import io.element.android.x.initializer.TimberInitializer
|
||||
@@ -40,5 +41,6 @@ class ElementXApplication : Application(), DaggerComponentOwner {
|
||||
initializeComponent(TimberInitializer::class.java)
|
||||
initializeComponent(MatrixInitializer::class.java)
|
||||
}
|
||||
logApplicationInfo()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,20 +22,23 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.bumble.appyx.core.integration.NodeHost
|
||||
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
|
||||
import io.element.android.x.architecture.bindings
|
||||
import io.element.android.x.di.DaggerComponentOwner
|
||||
import io.element.android.x.designsystem.ElementXTheme
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.designsystem.ElementXTheme
|
||||
import io.element.android.libraries.di.DaggerComponentOwner
|
||||
import io.element.android.x.di.AppBindings
|
||||
import io.element.android.x.node.RootFlowNode
|
||||
|
||||
class MainActivity : NodeComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
val appBindings = bindings<AppBindings>()
|
||||
appBindings.matrixClientsHolder().restore(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
setContent {
|
||||
ElementXTheme {
|
||||
@@ -48,11 +51,17 @@ class MainActivity : NodeComponentActivity() {
|
||||
buildContext = it,
|
||||
appComponentOwner = applicationContext as DaggerComponentOwner,
|
||||
authenticationService = appBindings.authenticationService(),
|
||||
rootPresenter = appBindings.rootPresenter()
|
||||
presenter = appBindings.rootPresenter(),
|
||||
matrixClientsHolder = appBindings.matrixClientsHolder()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
bindings<AppBindings>().matrixClientsHolder().onSaveInstanceState(outState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,14 @@
|
||||
package io.element.android.x.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import io.element.android.x.matrix.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
|
||||
import io.element.android.x.root.RootPresenter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
interface AppBindings {
|
||||
fun coroutineScope(): CoroutineScope
|
||||
fun rootPresenter(): RootPresenter
|
||||
fun authenticationService(): MatrixAuthenticationService
|
||||
fun matrixClientsHolder(): MatrixClientsHolder
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@ import android.content.Context
|
||||
import com.squareup.anvil.annotations.MergeComponent
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import io.element.android.x.architecture.NodeFactoriesBindings
|
||||
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
@MergeComponent(AppScope::class)
|
||||
|
||||
@@ -20,7 +20,10 @@ import android.content.Context
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.x.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,11 @@ import com.squareup.anvil.annotations.ContributesTo
|
||||
import com.squareup.anvil.annotations.MergeSubcomponent
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.element.android.x.architecture.NodeFactoriesBindings
|
||||
import io.element.android.x.matrix.room.MatrixRoom
|
||||
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.room.MatrixRoom
|
||||
|
||||
@SingleIn(RoomScope::class)
|
||||
@MergeSubcomponent(RoomScope::class)
|
||||
|
||||
@@ -20,8 +20,11 @@ import com.squareup.anvil.annotations.ContributesTo
|
||||
import com.squareup.anvil.annotations.MergeSubcomponent
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.element.android.x.architecture.NodeFactoriesBindings
|
||||
import io.element.android.x.matrix.MatrixClient
|
||||
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.MatrixClient
|
||||
|
||||
@SingleIn(SessionScope::class)
|
||||
@MergeSubcomponent(SessionScope::class)
|
||||
|
||||
44
app/src/main/kotlin/io/element/android/x/info/Logs.kt
Normal file
44
app/src/main/kotlin/io/element/android/x/info/Logs.kt
Normal 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")
|
||||
}
|
||||
@@ -18,7 +18,7 @@ package io.element.android.x.initializer
|
||||
|
||||
import android.content.Context
|
||||
import androidx.startup.Initializer
|
||||
import io.element.android.x.features.rageshake.crash.VectorUncaughtExceptionHandler
|
||||
import io.element.android.features.rageshake.crash.VectorUncaughtExceptionHandler
|
||||
|
||||
class CrashInitializer : Initializer<Unit> {
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ package io.element.android.x.initializer
|
||||
|
||||
import android.content.Context
|
||||
import androidx.startup.Initializer
|
||||
import io.element.android.libraries.matrix.tracing.TracingConfigurations
|
||||
import io.element.android.libraries.matrix.tracing.setupTracing
|
||||
import io.element.android.x.BuildConfig
|
||||
import io.element.android.x.matrix.tracing.TracingConfigurations
|
||||
import io.element.android.x.matrix.tracing.setupTracing
|
||||
|
||||
class MatrixInitializer : Initializer<Unit> {
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ package io.element.android.x.initializer
|
||||
|
||||
import android.content.Context
|
||||
import androidx.startup.Initializer
|
||||
import io.element.android.features.rageshake.logs.VectorFileLogger
|
||||
import io.element.android.x.BuildConfig
|
||||
import io.element.android.x.features.rageshake.logs.VectorFileLogger
|
||||
import timber.log.Timber
|
||||
|
||||
class TimberInitializer : Initializer<Unit> {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -32,16 +32,17 @@ import com.bumble.appyx.core.node.ParentNode
|
||||
import com.bumble.appyx.core.node.node
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import io.element.android.x.architecture.bindings
|
||||
import io.element.android.x.architecture.createNode
|
||||
import io.element.android.x.di.DaggerComponentOwner
|
||||
import io.element.android.features.preferences.PreferencesFlowNode
|
||||
import io.element.android.features.roomlist.RoomListNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.DaggerComponentOwner
|
||||
import io.element.android.libraries.matrix.MatrixClient
|
||||
import io.element.android.libraries.matrix.core.RoomId
|
||||
import io.element.android.libraries.matrix.core.SessionId
|
||||
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
|
||||
import io.element.android.x.di.SessionComponent
|
||||
import io.element.android.x.features.preferences.PreferencesFlowNode
|
||||
import io.element.android.x.features.roomlist.RoomListNode
|
||||
import io.element.android.x.matrix.MatrixClient
|
||||
import io.element.android.x.matrix.core.RoomId
|
||||
import io.element.android.x.matrix.core.SessionId
|
||||
import io.element.android.x.matrix.ui.di.MatrixUIBindings
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
class LoggedInFlowNode(
|
||||
@@ -124,6 +125,11 @@ class LoggedInFlowNode(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Children(navModel = backstack)
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
// Animate navigation to settings and to a room
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,10 @@ import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.ParentNode
|
||||
import com.bumble.appyx.core.node.node
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import io.element.android.x.features.login.LoginFlowNode
|
||||
import io.element.android.x.features.onboarding.OnBoardingScreen
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import io.element.android.features.login.LoginFlowNode
|
||||
import io.element.android.features.onboarding.OnBoardingScreen
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -62,7 +63,7 @@ class NotLoggedInFlowNode(
|
||||
return when (navTarget) {
|
||||
NavTarget.OnBoarding -> node(buildContext) {
|
||||
OnBoardingScreen(
|
||||
onSignIn = { backstack.replace(NavTarget.LoginFlow) }
|
||||
onSignIn = { backstack.push(NavTarget.LoginFlow) }
|
||||
)
|
||||
}
|
||||
NavTarget.LoginFlow -> LoginFlowNode(buildContext)
|
||||
@@ -71,6 +72,11 @@ class NotLoggedInFlowNode(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Children(navModel = backstack)
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
// Animate navigation to login screen
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,12 @@ import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.ParentNode
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import io.element.android.x.architecture.bindings
|
||||
import io.element.android.x.architecture.createNode
|
||||
import io.element.android.x.di.DaggerComponentOwner
|
||||
import io.element.android.features.messages.MessagesNode
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.DaggerComponentOwner
|
||||
import io.element.android.libraries.matrix.room.MatrixRoom
|
||||
import io.element.android.x.di.RoomComponent
|
||||
import io.element.android.x.features.messages.MessagesNode
|
||||
import io.element.android.x.matrix.room.MatrixRoom
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -70,6 +70,9 @@ class RoomFlowNode(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Children(navModel = backstack)
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,10 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.ParentNode
|
||||
@@ -36,13 +33,13 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import io.element.android.x.architecture.createNode
|
||||
import io.element.android.x.architecture.presenterConnector
|
||||
import io.element.android.x.di.DaggerComponentOwner
|
||||
import io.element.android.x.features.rageshake.bugreport.BugReportNode
|
||||
import io.element.android.x.matrix.MatrixClient
|
||||
import io.element.android.x.matrix.auth.MatrixAuthenticationService
|
||||
import io.element.android.x.matrix.core.SessionId
|
||||
import io.element.android.features.rageshake.bugreport.BugReportNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.DaggerComponentOwner
|
||||
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.core.SessionId
|
||||
import io.element.android.x.di.MatrixClientsHolder
|
||||
import io.element.android.x.root.RootPresenter
|
||||
import io.element.android.x.root.RootView
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -50,66 +47,96 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class RootFlowNode(
|
||||
buildContext: BuildContext,
|
||||
private val buildContext: BuildContext,
|
||||
private val backstack: BackStack<NavTarget> = BackStack(
|
||||
initialElement = NavTarget.SplashScreen,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
private val appComponentOwner: DaggerComponentOwner,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
rootPresenter: RootPresenter
|
||||
private val matrixClientsHolder: MatrixClientsHolder,
|
||||
private val presenter: RootPresenter
|
||||
) :
|
||||
ParentNode<RootFlowNode.NavTarget>(
|
||||
navModel = backstack,
|
||||
buildContext = buildContext,
|
||||
buildContext = buildContext
|
||||
),
|
||||
|
||||
DaggerComponentOwner by appComponentOwner {
|
||||
|
||||
private val matrixClientsHolder = ConcurrentHashMap<SessionId, MatrixClient>()
|
||||
private val presenterConnector = presenterConnector(rootPresenter)
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
whenChildAttached(LoggedInFlowNode::class) { _, child ->
|
||||
child.lifecycle.subscribe(
|
||||
onDestroy = { matrixClientsHolder.remove(child.sessionId) }
|
||||
)
|
||||
}
|
||||
observeLoggedInState()
|
||||
}
|
||||
|
||||
private fun observeLoggedInState() {
|
||||
authenticationService.isLoggedIn()
|
||||
.distinctUntilChanged()
|
||||
.onEach { isLoggedIn ->
|
||||
Timber.v("isLoggedIn=$isLoggedIn")
|
||||
if (isLoggedIn) {
|
||||
val matrixClient = authenticationService.restoreSession()
|
||||
if (matrixClient == null) {
|
||||
backstack.newRoot(NavTarget.NotLoggedInFlow)
|
||||
} else {
|
||||
matrixClientsHolder[matrixClient.sessionId] = matrixClient
|
||||
backstack.newRoot(NavTarget.LoggedInFlow(matrixClient.sessionId))
|
||||
}
|
||||
tryToRestoreLatestSession(
|
||||
onSuccess = { switchToLoggedInFlow(it) },
|
||||
onFailure = { switchToLogoutFlow() }
|
||||
)
|
||||
} else {
|
||||
backstack.newRoot(NavTarget.NotLoggedInFlow)
|
||||
switchToLogoutFlow()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun switchToLoggedInFlow(sessionId: SessionId) {
|
||||
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId = sessionId))
|
||||
}
|
||||
|
||||
private fun switchToLogoutFlow() {
|
||||
matrixClientsHolder.removeAll()
|
||||
backstack.safeRoot(NavTarget.NotLoggedInFlow)
|
||||
}
|
||||
|
||||
private suspend fun tryToRestoreLatestSession(
|
||||
onSuccess: (SessionId) -> Unit = {},
|
||||
onFailure: () -> Unit = {}
|
||||
) {
|
||||
val latestKnownSessionId = authenticationService.getLatestSessionId()
|
||||
if (latestKnownSessionId == null) {
|
||||
onFailure()
|
||||
return
|
||||
}
|
||||
if (matrixClientsHolder.knowSession(latestKnownSessionId)) {
|
||||
onSuccess(latestKnownSessionId)
|
||||
return
|
||||
}
|
||||
val matrixClient = authenticationService.restoreSession(latestKnownSessionId)
|
||||
if (matrixClient == null) {
|
||||
Timber.v("Failed to restore session...")
|
||||
onFailure()
|
||||
} else {
|
||||
matrixClientsHolder.add(matrixClient)
|
||||
onSuccess(matrixClient.sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOpenBugReport() {
|
||||
backstack.push(NavTarget.BugReport)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state by presenterConnector.stateFlow.collectAsState()
|
||||
val state = presenter.present()
|
||||
RootView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onOpenBugReport = this::onOpenBugReport,
|
||||
) {
|
||||
Children(navModel = backstack)
|
||||
Children(
|
||||
navModel = backstack,
|
||||
// Animate opening the bug report screen
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,8 +163,10 @@ class RootFlowNode(
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
is NavTarget.LoggedInFlow -> {
|
||||
val matrixClient =
|
||||
matrixClientsHolder[navTarget.sessionId] ?: throw IllegalStateException("Makes sure to give a matrixClient with the given sessionId")
|
||||
val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
|
||||
Timber.w("Couldn't find any session, go through SplashScreen")
|
||||
backstack.newRoot(NavTarget.SplashScreen)
|
||||
}
|
||||
LoggedInFlowNode(
|
||||
buildContext = buildContext,
|
||||
sessionId = navTarget.sessionId,
|
||||
@@ -146,12 +175,14 @@ class RootFlowNode(
|
||||
)
|
||||
}
|
||||
NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext)
|
||||
NavTarget.SplashScreen -> node(buildContext) {
|
||||
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
NavTarget.SplashScreen -> splashNode(buildContext)
|
||||
NavTarget.BugReport -> createNode<BugReportNode>(buildContext, plugins = listOf(bugReportNodeCallback))
|
||||
}
|
||||
}
|
||||
|
||||
private fun splashNode(buildContext: BuildContext) = node(buildContext) {
|
||||
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ package io.element.android.x.root
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import io.element.android.x.architecture.Presenter
|
||||
import io.element.android.x.features.rageshake.bugreport.BugReportPresenter
|
||||
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionPresenter
|
||||
import io.element.android.x.features.rageshake.detection.RageshakeDetectionPresenter
|
||||
import io.element.android.features.rageshake.bugreport.BugReportPresenter
|
||||
import io.element.android.features.rageshake.crash.ui.CrashDetectionPresenter
|
||||
import io.element.android.features.rageshake.detection.RageshakeDetectionPresenter
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import javax.inject.Inject
|
||||
|
||||
class RootPresenter @Inject constructor(
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
package io.element.android.x.root
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.element.android.x.features.rageshake.bugreport.BugReportState
|
||||
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionState
|
||||
import io.element.android.x.features.rageshake.detection.RageshakeDetectionState
|
||||
import io.element.android.features.rageshake.bugreport.BugReportState
|
||||
import io.element.android.features.rageshake.crash.ui.CrashDetectionState
|
||||
import io.element.android.features.rageshake.detection.RageshakeDetectionState
|
||||
|
||||
@Stable
|
||||
data class RootState(
|
||||
|
||||
@@ -24,12 +24,12 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import io.element.android.features.rageshake.crash.ui.CrashDetectionEvents
|
||||
import io.element.android.features.rageshake.crash.ui.CrashDetectionView
|
||||
import io.element.android.features.rageshake.detection.RageshakeDetectionEvents
|
||||
import io.element.android.features.rageshake.detection.RageshakeDetectionView
|
||||
import io.element.android.tests.uitests.openShowkase
|
||||
import io.element.android.x.component.ShowkaseButton
|
||||
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionEvents
|
||||
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionView
|
||||
import io.element.android.x.features.rageshake.detection.RageshakeDetectionEvents
|
||||
import io.element.android.x.features.rageshake.detection.RageshakeDetectionView
|
||||
import io.element.android.x.tests.uitests.openShowkase
|
||||
|
||||
@Composable
|
||||
fun RootView(
|
||||
|
||||
8
app/src/main/res/drawable/splash_icon.xml
Normal file
8
app/src/main/res/drawable/splash_icon.xml
Normal 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" />
|
||||
26
app/src/main/res/values-night/themes.xml
Normal file
26
app/src/main/res/values-night/themes.xml
Normal 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>
|
||||
@@ -16,6 +16,10 @@
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="Theme.ElementX.Splash" parent="Theme.SplashScreen.IconBackground">
|
||||
<item name="windowSplashScreenBackground">@color/white</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
|
||||
</style>
|
||||
<style name="Theme.ElementX" parent="Theme.ElementAndroid" />
|
||||
</resources>
|
||||
|
||||
@@ -29,6 +29,8 @@ plugins {
|
||||
alias(libs.plugins.detekt)
|
||||
alias(libs.plugins.ktlint)
|
||||
alias(libs.plugins.dependencygraph)
|
||||
alias(libs.plugins.sonarqube)
|
||||
alias(libs.plugins.kover)
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean").configure {
|
||||
@@ -108,3 +110,68 @@ allprojects {
|
||||
plugin("org.owasp.dependencycheck")
|
||||
}
|
||||
}
|
||||
|
||||
// To run a sonar analysis:
|
||||
// Run './gradlew sonar -Dsonar.login=<SONAR_LOGIN>'
|
||||
// The SONAR_LOGIN is stored in passbolt as Token Sonar Cloud Bma
|
||||
// Sonar result can be found here: https://sonarcloud.io/project/overview?id=vector-im_element-x-android
|
||||
sonar {
|
||||
properties {
|
||||
property("sonar.projectName", "element-x-android")
|
||||
property("sonar.projectKey", "vector-im_element-x-android")
|
||||
property("sonar.host.url", "https://sonarcloud.io")
|
||||
property("sonar.projectVersion", "1.0") // TODO project(":app").android.defaultConfig.versionName)
|
||||
property("sonar.sourceEncoding", "UTF-8")
|
||||
property("sonar.links.homepage", "https://github.com/vector-im/element-x-android/")
|
||||
property("sonar.links.ci", "https://github.com/vector-im/element-x-android/actions")
|
||||
property("sonar.links.scm", "https://github.com/vector-im/element-x-android/")
|
||||
property("sonar.links.issue", "https://github.com/vector-im/element-x-android/issues")
|
||||
property("sonar.organization", "new_vector_ltd_organization")
|
||||
property("sonar.login", if (project.hasProperty("SONAR_LOGIN")) project.property("SONAR_LOGIN")!! else "invalid")
|
||||
|
||||
// exclude source code from analyses separated by a colon (:)
|
||||
// Exclude Java source
|
||||
property("sonar.exclusions", "**/BugReporterMultipartBody.java")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
val projectDir = projectDir.toString()
|
||||
sonar {
|
||||
properties {
|
||||
// Note: folders `kotlin` are not supported (yet), I asked on their side: https://community.sonarsource.com/t/82824
|
||||
// As a workaround provide the path in `sonar.sources` property.
|
||||
if (File("$projectDir/src/main/kotlin").exists()) {
|
||||
property("sonar.sources", "src/main/kotlin")
|
||||
}
|
||||
if (File("$projectDir/src/test/kotlin").exists()) {
|
||||
property("sonar.tests", "src/test/kotlin")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
apply(plugin = "kover")
|
||||
}
|
||||
|
||||
// Run `./gradlew koverMergedHtmlReport` to get report at ./build/reports/kover
|
||||
// Run `./gradlew koverMergedReport` to also get XML report
|
||||
koverMerged {
|
||||
enable()
|
||||
|
||||
filters {
|
||||
classes {
|
||||
excludes.addAll(
|
||||
listOf(
|
||||
/*
|
||||
"*Fragment",
|
||||
"*Fragment\$*",
|
||||
"*Activity",
|
||||
"*Activity\$*",
|
||||
*/
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.x.features.login"
|
||||
namespace = "io.element.android.features.login"
|
||||
}
|
||||
|
||||
anvil {
|
||||
@@ -32,16 +32,15 @@ anvil {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":anvilannotations"))
|
||||
anvil(project(":anvilcodegen"))
|
||||
implementation(project(":libraries:di"))
|
||||
implementation(project(":libraries:core"))
|
||||
implementation(project(":libraries:architecture"))
|
||||
implementation(project(":libraries:matrix"))
|
||||
implementation(project(":libraries:designsystem"))
|
||||
implementation(project(":libraries:elementresources"))
|
||||
implementation(libs.appyx.core)
|
||||
implementation(project(":libraries:ui-strings"))
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
ksp(libs.showkase.processor)
|
||||
testImplementation(libs.test.junit)
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login
|
||||
package io.element.android.features.login
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
@@ -33,6 +33,6 @@ class ExampleInstrumentedTest {
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("io.element.android.x.features.login.test", appContext.packageName)
|
||||
assertEquals("io.element.android.features.login.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login
|
||||
package io.element.android.features.login
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -25,9 +25,10 @@ import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.ParentNode
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import io.element.android.x.architecture.createNode
|
||||
import io.element.android.x.features.login.changeserver.ChangeServerNode
|
||||
import io.element.android.x.features.login.root.LoginRootNode
|
||||
import io.element.android.features.login.changeserver.ChangeServerNode
|
||||
import io.element.android.features.login.root.LoginRootNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
class LoginFlowNode(
|
||||
@@ -64,6 +65,11 @@ class LoginFlowNode(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Children(navModel = backstack)
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
// Animate transition to change server screen
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login.changeserver
|
||||
package io.element.android.features.login.changeserver
|
||||
|
||||
sealed interface ChangeServerEvents {
|
||||
data class SetServer(val server: String) : ChangeServerEvents
|
||||
@@ -14,20 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login.changeserver
|
||||
package io.element.android.features.login.changeserver
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.x.anvilannotations.ContributesNode
|
||||
import io.element.android.x.architecture.presenterConnector
|
||||
import io.element.android.x.di.AppScope
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class ChangeServerNode @AssistedInject constructor(
|
||||
@@ -36,17 +33,16 @@ class ChangeServerNode @AssistedInject constructor(
|
||||
private val presenter: ChangeServerPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
private val presenterConnector = presenterConnector(presenter)
|
||||
|
||||
private fun onSuccess() {
|
||||
navigateUp()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state by presenterConnector.stateFlow.collectAsState()
|
||||
val state = presenter.present()
|
||||
ChangeServerView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onChangeServerSuccess = this::onSuccess,
|
||||
)
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login.changeserver
|
||||
package io.element.android.features.login.changeserver
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -22,10 +22,10 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import io.element.android.x.architecture.Async
|
||||
import io.element.android.x.architecture.Presenter
|
||||
import io.element.android.x.architecture.execute
|
||||
import io.element.android.x.matrix.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.execute
|
||||
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login.changeserver
|
||||
package io.element.android.features.login.changeserver
|
||||
|
||||
import io.element.android.x.architecture.Async
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
||||
data class ChangeServerState(
|
||||
val homeserver: String = "",
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.x.features.login.changeserver
|
||||
package io.element.android.features.login.changeserver
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -52,11 +52,13 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.x.architecture.Async
|
||||
import io.element.android.x.core.compose.textFieldState
|
||||
import io.element.android.x.designsystem.components.VectorIcon
|
||||
import io.element.android.x.features.login.R
|
||||
import io.element.android.x.features.login.error.changeServerError
|
||||
import io.element.android.features.login.R
|
||||
import io.element.android.features.login.error.changeServerError
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.VectorIcon
|
||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
|
||||
@Composable
|
||||
fun ChangeServerView(
|
||||
@@ -129,6 +131,7 @@ fun ChangeServerView(
|
||||
value = homeserverFieldState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.changeServerServer)
|
||||
.padding(top = 200.dp),
|
||||
onValueChange = {
|
||||
homeserverFieldState = it
|
||||
@@ -162,6 +165,7 @@ fun ChangeServerView(
|
||||
enabled = state.submitEnabled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.changeServerContinue)
|
||||
.padding(top = 44.dp)
|
||||
) {
|
||||
Text(text = "Continue")
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login.error
|
||||
package io.element.android.features.login.error
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.x.core.uri.isValidUrl
|
||||
import io.element.android.x.features.login.root.LoginFormState
|
||||
import io.element.android.x.ui.strings.R as StringR
|
||||
import io.element.android.features.login.root.LoginFormState
|
||||
import io.element.android.libraries.core.uri.isValidUrl
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
fun loginError(
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login.root
|
||||
package io.element.android.features.login.root
|
||||
|
||||
sealed interface LoginRootEvents {
|
||||
object RefreshHomeServer : LoginRootEvents
|
||||
@@ -14,11 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login.root
|
||||
package io.element.android.features.login.root
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
@@ -27,10 +25,9 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.x.anvilannotations.ContributesNode
|
||||
import io.element.android.x.architecture.presenterConnector
|
||||
import io.element.android.x.core.compose.OnLifecycleEvent
|
||||
import io.element.android.x.di.AppScope
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class LoginRootNode @AssistedInject constructor(
|
||||
@@ -39,8 +36,6 @@ class LoginRootNode @AssistedInject constructor(
|
||||
private val presenter: LoginRootPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
private val presenterConnector = presenterConnector(presenter)
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onChangeHomeServer()
|
||||
}
|
||||
@@ -51,7 +46,7 @@ class LoginRootNode @AssistedInject constructor(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state by presenterConnector.stateFlow.collectAsState()
|
||||
val state = presenter.present()
|
||||
OnLifecycleEvent { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_RESUME -> state.eventSink(LoginRootEvents.RefreshHomeServer)
|
||||
@@ -60,6 +55,7 @@ class LoginRootNode @AssistedInject constructor(
|
||||
}
|
||||
LoginRootScreen(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onChangeServer = this::onChangeHomeServer,
|
||||
)
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login.root
|
||||
package io.element.android.features.login.root
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -22,8 +22,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import io.element.android.x.architecture.Presenter
|
||||
import io.element.android.x.matrix.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.x.features.login.root
|
||||
package io.element.android.features.login.root
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -58,10 +58,12 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.x.core.compose.textFieldState
|
||||
import io.element.android.x.features.login.error.loginError
|
||||
import io.element.android.x.matrix.core.SessionId
|
||||
import io.element.android.x.ui.strings.R as StringR
|
||||
import io.element.android.features.login.error.loginError
|
||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||
import io.element.android.libraries.matrix.core.SessionId
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -127,6 +129,7 @@ fun LoginRootScreen(
|
||||
onClick = onChangeServer,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.testTag(TestTags.loginChangeServer)
|
||||
.padding(top = 8.dp, end = 8.dp),
|
||||
content = {
|
||||
Text(text = "Change")
|
||||
@@ -137,6 +140,7 @@ fun LoginRootScreen(
|
||||
value = loginFieldState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.loginEmailUsername)
|
||||
.padding(top = 60.dp),
|
||||
label = {
|
||||
Text(text = stringResource(id = StringR.string.login_signin_username_hint))
|
||||
@@ -159,6 +163,7 @@ fun LoginRootScreen(
|
||||
value = passwordFieldState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.loginPassword)
|
||||
.padding(top = 24.dp),
|
||||
onValueChange = {
|
||||
passwordFieldState = it
|
||||
@@ -202,6 +207,7 @@ fun LoginRootScreen(
|
||||
enabled = state.submitEnabled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.loginContinue)
|
||||
.padding(vertical = 32.dp)
|
||||
) {
|
||||
Text(text = "Continue")
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login.root
|
||||
package io.element.android.features.login.root
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.element.android.x.matrix.core.SessionId
|
||||
import io.element.android.libraries.matrix.core.SessionId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
data class LoginRootState(
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login
|
||||
package io.element.android.features.login
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
@@ -23,7 +23,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.x.features.logout"
|
||||
namespace = "io.element.android.features.logout"
|
||||
}
|
||||
|
||||
anvil {
|
||||
@@ -31,15 +31,14 @@ anvil {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":anvilannotations"))
|
||||
anvil(project(":anvilcodegen"))
|
||||
implementation(project(":libraries:di"))
|
||||
implementation(project(":libraries:architecture"))
|
||||
implementation(project(":libraries:core"))
|
||||
implementation(project(":libraries:matrix"))
|
||||
implementation(project(":libraries:designsystem"))
|
||||
implementation(project(":libraries:elementresources"))
|
||||
implementation(project(":libraries:ui-strings"))
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
ksp(libs.showkase.processor)
|
||||
testImplementation(libs.test.junit)
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login
|
||||
package io.element.android.features.logout
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
@@ -33,6 +33,6 @@ class ExampleInstrumentedTest {
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("io.element.android.x.features.login.test", appContext.packageName)
|
||||
assertEquals("io.element.android.features.login.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.logout
|
||||
package io.element.android.features.logout
|
||||
|
||||
sealed interface LogoutPreferenceEvents {
|
||||
object Logout : LogoutPreferenceEvents
|
||||
@@ -14,22 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.logout
|
||||
package io.element.android.features.logout
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.x.architecture.Async
|
||||
import io.element.android.x.architecture.Presenter
|
||||
import io.element.android.x.architecture.execute
|
||||
import io.element.android.x.matrix.MatrixClient
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.execute
|
||||
import io.element.android.libraries.matrix.MatrixClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) : Presenter<LogoutPreferenceState> {
|
||||
class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) :
|
||||
Presenter<LogoutPreferenceState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): LogoutPreferenceState {
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.logout
|
||||
package io.element.android.features.logout
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Logout
|
||||
@@ -24,12 +24,12 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.x.architecture.Async
|
||||
import io.element.android.x.designsystem.components.ProgressDialog
|
||||
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.x.designsystem.components.preferences.PreferenceCategory
|
||||
import io.element.android.x.designsystem.components.preferences.PreferenceText
|
||||
import io.element.android.x.ui.strings.R as StringR
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
fun LogoutPreferenceView(
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.logout
|
||||
package io.element.android.features.logout
|
||||
|
||||
import io.element.android.x.architecture.Async
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
||||
data class LogoutPreferenceState(
|
||||
val logoutAction: Async<Unit> = Async.Uninitialized,
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.login
|
||||
package io.element.android.features.logout
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
@@ -23,7 +23,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.x.features.messages"
|
||||
namespace = "io.element.android.features.messages"
|
||||
}
|
||||
|
||||
anvil {
|
||||
@@ -31,16 +31,14 @@ anvil {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":anvilannotations"))
|
||||
anvil(project(":anvilcodegen"))
|
||||
implementation(project(":libraries:di"))
|
||||
implementation(project(":libraries:core"))
|
||||
implementation(project(":libraries:architecture"))
|
||||
implementation(project(":libraries:matrix"))
|
||||
implementation(project(":libraries:matrixui"))
|
||||
implementation(project(":libraries:designsystem"))
|
||||
implementation(project(":libraries:textcomposer"))
|
||||
implementation(libs.appyx.core)
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.textcomposer)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.datetime)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages
|
||||
package io.element.android.features.messages
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
@@ -33,6 +33,6 @@ class ExampleInstrumentedTest {
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("io.element.android.x.features.messages.test", appContext.packageName)
|
||||
assertEquals("io.element.android.features.messages.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages
|
||||
package io.element.android.features.messages
|
||||
|
||||
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
|
||||
sealed interface MessagesEvents {
|
||||
data class HandleAction(val action: TimelineItemAction, val messageEvent: TimelineItem.MessageEvent) : MessagesEvents
|
||||
@@ -14,33 +14,28 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages
|
||||
package io.element.android.features.messages
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.x.anvilannotations.ContributesNode
|
||||
import io.element.android.x.architecture.presenterConnector
|
||||
import io.element.android.x.di.RoomScope
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class MessagesNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenter: MessagesPresenter,
|
||||
private val presenter: MessagesPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
private val connector = presenterConnector(presenter)
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state by connector.stateFlow.collectAsState()
|
||||
val state = presenter.present()
|
||||
MessagesView(
|
||||
state = state,
|
||||
onBackPressed = this::navigateUp,
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages
|
||||
package io.element.android.features.messages
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -24,37 +24,32 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import io.element.android.x.architecture.Presenter
|
||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.x.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.x.features.messages.actionlist.ActionListPresenter
|
||||
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.x.features.messages.textcomposer.MessageComposerEvents
|
||||
import io.element.android.x.features.messages.textcomposer.MessageComposerPresenter
|
||||
import io.element.android.x.features.messages.textcomposer.MessageComposerState
|
||||
import io.element.android.x.features.messages.timeline.TimelineEvents
|
||||
import io.element.android.x.features.messages.timeline.TimelinePresenter
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent
|
||||
import io.element.android.x.matrix.MatrixClient
|
||||
import io.element.android.x.matrix.room.MatrixRoom
|
||||
import io.element.android.x.matrix.ui.MatrixItemHelper
|
||||
import io.element.android.x.textcomposer.MessageComposerMode
|
||||
import io.element.android.features.messages.actionlist.ActionListPresenter
|
||||
import io.element.android.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.textcomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.textcomposer.MessageComposerPresenter
|
||||
import io.element.android.features.messages.textcomposer.MessageComposerState
|
||||
import io.element.android.features.messages.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.timeline.TimelinePresenter
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContent
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.room.MatrixRoom
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class MessagesPresenter @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val room: MatrixRoom,
|
||||
private val composerPresenter: MessageComposerPresenter,
|
||||
private val timelinePresenter: TimelinePresenter,
|
||||
private val actionListPresenter: ActionListPresenter,
|
||||
) : Presenter<MessagesState> {
|
||||
|
||||
private val matrixItemHelper = MatrixItemHelper(matrixClient)
|
||||
|
||||
@Composable
|
||||
override fun present(): MessagesState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
@@ -71,8 +66,9 @@ class MessagesPresenter @Inject constructor(
|
||||
}
|
||||
LaunchedEffect(syncUpdateFlow) {
|
||||
roomAvatar.value =
|
||||
matrixItemHelper.loadAvatarData(
|
||||
room = room,
|
||||
AvatarData(
|
||||
name = room.bestName,
|
||||
url = room.avatarUrl,
|
||||
size = AvatarSize.SMALL
|
||||
)
|
||||
roomName.value = room.name
|
||||
@@ -14,14 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages
|
||||
package io.element.android.features.messages
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.x.features.messages.actionlist.ActionListState
|
||||
import io.element.android.x.features.messages.textcomposer.MessageComposerState
|
||||
import io.element.android.x.features.messages.timeline.TimelineState
|
||||
import io.element.android.x.matrix.core.RoomId
|
||||
import io.element.android.features.messages.actionlist.ActionListState
|
||||
import io.element.android.features.messages.textcomposer.MessageComposerState
|
||||
import io.element.android.features.messages.timeline.TimelineState
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.core.RoomId
|
||||
|
||||
@Immutable
|
||||
data class MessagesState(
|
||||
@@ -19,7 +19,7 @@
|
||||
ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class,
|
||||
)
|
||||
|
||||
package io.element.android.x.features.messages
|
||||
package io.element.android.features.messages
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -56,15 +56,15 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.x.core.compose.LogCompositions
|
||||
import io.element.android.x.designsystem.components.avatar.Avatar
|
||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.x.features.messages.actionlist.ActionListEvents
|
||||
import io.element.android.x.features.messages.actionlist.ActionListView
|
||||
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.x.features.messages.textcomposer.MessageComposerView
|
||||
import io.element.android.x.features.messages.timeline.TimelineView
|
||||
import io.element.android.features.messages.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.actionlist.ActionListView
|
||||
import io.element.android.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.textcomposer.MessageComposerView
|
||||
import io.element.android.features.messages.timeline.TimelineView
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.actionlist
|
||||
package io.element.android.features.messages.actionlist
|
||||
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
|
||||
sealed interface ActionListEvents {
|
||||
object Clear : ActionListEvents
|
||||
@@ -14,17 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.actionlist
|
||||
package io.element.android.features.messages.actionlist
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.x.architecture.Presenter
|
||||
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.actionlist
|
||||
package io.element.android.features.messages.actionlist
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
@file:OptIn(ExperimentalMaterialApi::class)
|
||||
|
||||
package io.element.android.x.features.messages.actionlist
|
||||
package io.element.android.features.messages.actionlist
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -41,9 +41,9 @@ import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.x.designsystem.components.VectorIcon
|
||||
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.designsystem.components.VectorIcon
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -14,11 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.actionlist.model
|
||||
package io.element.android.features.messages.actionlist.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.x.designsystem.VectorIcons
|
||||
|
||||
@Immutable
|
||||
sealed class TimelineItemAction(
|
||||
@@ -26,9 +25,9 @@ sealed class TimelineItemAction(
|
||||
@DrawableRes val icon: Int,
|
||||
val destructive: Boolean = false
|
||||
) {
|
||||
object Forward : TimelineItemAction("Forward", VectorIcons.ArrowForward)
|
||||
object Copy : TimelineItemAction("Copy", VectorIcons.Copy)
|
||||
object Redact : TimelineItemAction("Redact", VectorIcons.Delete, destructive = true)
|
||||
object Reply : TimelineItemAction("Reply", VectorIcons.Reply)
|
||||
object Edit : TimelineItemAction("Edit", VectorIcons.Edit)
|
||||
object Forward : TimelineItemAction("Forward", io.element.android.libraries.designsystem.VectorIcons.ArrowForward)
|
||||
object Copy : TimelineItemAction("Copy", io.element.android.libraries.designsystem.VectorIcons.Copy)
|
||||
object Redact : TimelineItemAction("Redact", io.element.android.libraries.designsystem.VectorIcons.Delete, destructive = true)
|
||||
object Reply : TimelineItemAction("Reply", io.element.android.libraries.designsystem.VectorIcons.Reply)
|
||||
object Edit : TimelineItemAction("Edit", io.element.android.libraries.designsystem.VectorIcons.Edit)
|
||||
}
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.textcomposer
|
||||
package io.element.android.features.messages.textcomposer
|
||||
|
||||
import io.element.android.x.textcomposer.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
|
||||
sealed interface MessageComposerEvents {
|
||||
object ToggleFullScreenState : MessageComposerEvents
|
||||
@@ -14,18 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.textcomposer
|
||||
package io.element.android.features.messages.textcomposer
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import io.element.android.x.architecture.Presenter
|
||||
import io.element.android.x.core.data.StableCharSequence
|
||||
import io.element.android.x.core.data.toStableCharSequence
|
||||
import io.element.android.x.matrix.room.MatrixRoom
|
||||
import io.element.android.x.textcomposer.MessageComposerMode
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
import io.element.android.libraries.core.data.toStableCharSequence
|
||||
import io.element.android.libraries.matrix.room.MatrixRoom
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -40,7 +41,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||
val isFullScreen = rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val text: MutableState<StableCharSequence> = rememberSaveable {
|
||||
val text: MutableState<StableCharSequence> = remember {
|
||||
mutableStateOf(StableCharSequence(""))
|
||||
}
|
||||
val composerMode: MutableState<MessageComposerMode> = rememberSaveable {
|
||||
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.textcomposer
|
||||
package io.element.android.features.messages.textcomposer
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.x.core.data.StableCharSequence
|
||||
import io.element.android.x.textcomposer.MessageComposerMode
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
|
||||
@Immutable
|
||||
data class MessageComposerState(
|
||||
@@ -14,12 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.textcomposer
|
||||
package io.element.android.features.messages.textcomposer
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.x.designsystem.LocalIsDarkTheme
|
||||
import io.element.android.x.textcomposer.TextComposer
|
||||
import io.element.android.libraries.designsystem.LocalIsDarkTheme
|
||||
import io.element.android.libraries.textcomposer.TextComposer
|
||||
|
||||
@Composable
|
||||
fun MessageComposerView(
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline
|
||||
package io.element.android.features.messages.timeline
|
||||
|
||||
import io.element.android.x.matrix.core.EventId
|
||||
import io.element.android.libraries.matrix.core.EventId
|
||||
|
||||
sealed interface TimelineEvents {
|
||||
object LoadMore : TimelineEvents
|
||||
@@ -14,30 +14,31 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline
|
||||
package io.element.android.features.messages.timeline
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import io.element.android.x.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.x.features.messages.timeline.diff.CacheInvalidator
|
||||
import io.element.android.x.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback
|
||||
import io.element.android.x.features.messages.timeline.model.AggregatedReaction
|
||||
import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItemReactions
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEmoteContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemNoticeContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent
|
||||
import io.element.android.x.features.messages.timeline.util.invalidateLast
|
||||
import io.element.android.x.matrix.core.EventId
|
||||
import io.element.android.x.matrix.media.MediaResolver
|
||||
import io.element.android.x.matrix.room.MatrixRoom
|
||||
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
||||
import io.element.android.x.matrix.ui.MatrixItemHelper
|
||||
import io.element.android.features.messages.timeline.diff.CacheInvalidator
|
||||
import io.element.android.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback
|
||||
import io.element.android.features.messages.timeline.model.AggregatedReaction
|
||||
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemReactions
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemEmoteContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemNoticeContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemTextContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
|
||||
import io.element.android.features.messages.timeline.util.invalidateLast
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.core.EventId
|
||||
import io.element.android.libraries.matrix.media.MediaResolver
|
||||
import io.element.android.libraries.matrix.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.ui.MatrixItemHelper
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -154,12 +155,11 @@ class TimelineItemsFactory @Inject constructor(
|
||||
computeGroupPosition(currentTimelineItem, timelineItems, index)
|
||||
val senderDisplayName = room.userDisplayName(currentSender).getOrNull()
|
||||
val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull()
|
||||
val senderAvatarData =
|
||||
matrixItemHelper.loadAvatarData(
|
||||
name = senderDisplayName ?: currentSender,
|
||||
url = senderAvatarUrl,
|
||||
size = AvatarSize.SMALL
|
||||
)
|
||||
val senderAvatarData = AvatarData(
|
||||
name = senderDisplayName ?: currentSender,
|
||||
url = senderAvatarUrl,
|
||||
size = AvatarSize.SMALL
|
||||
)
|
||||
return TimelineItem.MessageEvent(
|
||||
id = EventId(currentTimelineItem.uniqueId),
|
||||
senderId = currentSender,
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline
|
||||
package io.element.android.features.messages.timeline
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
@@ -24,14 +24,14 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import io.element.android.x.architecture.Presenter
|
||||
import io.element.android.x.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.x.matrix.MatrixClient
|
||||
import io.element.android.x.matrix.core.EventId
|
||||
import io.element.android.x.matrix.room.MatrixRoom
|
||||
import io.element.android.x.matrix.timeline.MatrixTimeline
|
||||
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
||||
import io.element.android.x.matrix.ui.MatrixItemHelper
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.MatrixClient
|
||||
import io.element.android.libraries.matrix.core.EventId
|
||||
import io.element.android.libraries.matrix.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.ui.MatrixItemHelper
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline
|
||||
package io.element.android.features.messages.timeline
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.x.matrix.core.EventId
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.matrix.core.EventId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline
|
||||
package io.element.android.features.messages.timeline
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -58,29 +58,29 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import io.element.android.x.core.compose.PairCombinedPreviewParameter
|
||||
import io.element.android.x.designsystem.components.avatar.Avatar
|
||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.x.features.messages.timeline.model.AggregatedReaction
|
||||
import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItemGroupPositionProvider
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItemReactions
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.MessagesTimelineItemContentProvider
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent
|
||||
import io.element.android.x.features.messages.timeline.components.MessageEventBubble
|
||||
import io.element.android.x.features.messages.timeline.components.TimelineItemReactionsView
|
||||
import io.element.android.x.features.messages.timeline.components.TimelineItemEncryptedView
|
||||
import io.element.android.x.features.messages.timeline.components.TimelineItemImageView
|
||||
import io.element.android.x.features.messages.timeline.components.TimelineItemRedactedView
|
||||
import io.element.android.x.features.messages.timeline.components.TimelineItemTextView
|
||||
import io.element.android.x.features.messages.timeline.components.TimelineItemUnknownView
|
||||
import io.element.android.x.matrix.core.EventId
|
||||
import io.element.android.features.messages.timeline.components.MessageEventBubble
|
||||
import io.element.android.features.messages.timeline.components.TimelineItemEncryptedView
|
||||
import io.element.android.features.messages.timeline.components.TimelineItemImageView
|
||||
import io.element.android.features.messages.timeline.components.TimelineItemReactionsView
|
||||
import io.element.android.features.messages.timeline.components.TimelineItemRedactedView
|
||||
import io.element.android.features.messages.timeline.components.TimelineItemTextView
|
||||
import io.element.android.features.messages.timeline.components.TimelineItemUnknownView
|
||||
import io.element.android.features.messages.timeline.model.AggregatedReaction
|
||||
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemGroupPositionProvider
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemReactions
|
||||
import io.element.android.features.messages.timeline.model.content.MessagesTimelineItemContentProvider
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.utils.PairCombinedPreviewParameter
|
||||
import io.element.android.libraries.matrix.core.EventId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
@@ -29,14 +29,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.x.designsystem.LocalIsDarkTheme
|
||||
import io.element.android.x.designsystem.MessageHighlightDark
|
||||
import io.element.android.x.designsystem.MessageHighlightLight
|
||||
import io.element.android.x.designsystem.SystemGrey5Dark
|
||||
import io.element.android.x.designsystem.SystemGrey5Light
|
||||
import io.element.android.x.designsystem.SystemGrey6Dark
|
||||
import io.element.android.x.designsystem.SystemGrey6Light
|
||||
import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition
|
||||
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition
|
||||
import io.element.android.libraries.designsystem.LocalIsDarkTheme
|
||||
import io.element.android.libraries.designsystem.MessageHighlightDark
|
||||
import io.element.android.libraries.designsystem.MessageHighlightLight
|
||||
import io.element.android.libraries.designsystem.SystemGrey5Dark
|
||||
import io.element.android.libraries.designsystem.SystemGrey5Light
|
||||
import io.element.android.libraries.designsystem.SystemGrey6Dark
|
||||
import io.element.android.libraries.designsystem.SystemGrey6Light
|
||||
|
||||
private val BUBBLE_RADIUS = 16.dp
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent
|
||||
|
||||
@Composable
|
||||
fun TimelineItemEncryptedView(
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
@file:OptIn(ExperimentalFoundationApi::class)
|
||||
|
||||
package io.element.android.x.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -33,7 +33,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent
|
||||
|
||||
@Composable
|
||||
fun TimelineItemImageView(
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -32,8 +32,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import io.element.android.x.features.messages.timeline.model.AggregatedReaction
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItemReactions
|
||||
import io.element.android.features.messages.timeline.model.AggregatedReaction
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemReactions
|
||||
|
||||
@Composable
|
||||
fun TimelineItemReactionsView(
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
|
||||
|
||||
@Composable
|
||||
fun TimelineItemRedactedView(
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components
|
||||
|
||||
import android.text.SpannableString
|
||||
import android.text.style.URLSpan
|
||||
@@ -28,10 +28,10 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.core.text.util.LinkifyCompat
|
||||
import io.element.android.x.designsystem.LinkColor
|
||||
import io.element.android.x.designsystem.components.ClickableLinkText
|
||||
import io.element.android.x.features.messages.timeline.components.html.HtmlDocument
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.timeline.components.html.HtmlDocument
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContent
|
||||
import io.element.android.libraries.designsystem.LinkColor
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
|
||||
@Composable
|
||||
fun TimelineItemTextView(
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
|
||||
|
||||
@Composable
|
||||
fun TimelineItemUnknownView(
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.components.html
|
||||
package io.element.android.features.messages.timeline.components.html
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -47,10 +47,10 @@ import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import io.element.android.x.designsystem.LinkColor
|
||||
import io.element.android.x.designsystem.components.ClickableLinkText
|
||||
import io.element.android.x.matrix.permalink.PermalinkData
|
||||
import io.element.android.x.matrix.permalink.PermalinkParser
|
||||
import io.element.android.libraries.designsystem.LinkColor
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.matrix.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.permalink.PermalinkParser
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.diff
|
||||
package io.element.android.features.messages.timeline.diff
|
||||
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.x.features.messages.timeline.util.invalidateLast
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.timeline.util.invalidateLast
|
||||
import timber.log.Timber
|
||||
|
||||
internal class CacheInvalidator(private val itemStatesCache: MutableList<TimelineItem?>) :
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.diff
|
||||
package io.element.android.features.messages.timeline.diff
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
|
||||
|
||||
internal class MatrixTimelineItemsDiffCallback(
|
||||
private val oldList: List<MatrixTimelineItem>,
|
||||
@@ -14,12 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.model
|
||||
package io.element.android.features.messages.timeline.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent
|
||||
import io.element.android.x.matrix.core.EventId
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.core.EventId
|
||||
|
||||
@Immutable
|
||||
sealed interface TimelineItem {
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.model
|
||||
package io.element.android.features.messages.timeline.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.model
|
||||
package io.element.android.features.messages.timeline.model
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import org.matrix.rustcomponents.sdk.EncryptedMessage
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
|
||||
import org.matrix.rustcomponents.sdk.EncryptedMessage
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
|
||||
import io.element.android.x.matrix.media.MediaResolver
|
||||
import io.element.android.libraries.matrix.media.MediaResolver
|
||||
|
||||
data class TimelineItemImageContent(
|
||||
val body: String,
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
|
||||
object TimelineItemRedactedContent : TimelineItemContent
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user