Merge pull request #173 from vector-im/feature/fga/improve_node_architecture

Feature/fga/improve node architecture
This commit is contained in:
ganfra
2023-03-09 17:55:17 +01:00
committed by GitHub
557 changed files with 2189 additions and 1743 deletions

View File

@@ -17,7 +17,7 @@
@file:Suppress("UnstableApiUsage")
import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
import extension.allFeatures
import extension.allFeaturesImpl
import extension.allLibraries
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@@ -26,8 +26,8 @@ plugins {
id("io.element.android-compose-application")
alias(libs.plugins.stem)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp)
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
alias(libs.plugins.kapt)
id("com.google.firebase.appdistribution") version "4.0.0"
id("org.jetbrains.kotlinx.knit") version "0.4.0"
@@ -201,12 +201,13 @@ knit {
dependencies {
allLibraries()
allFeatures()
allFeaturesImpl()
implementation(projects.libraries.matrix.impl)
implementation(projects.libraries.dateformatter.impl)
implementation(projects.libraries.sessionStorage.impl)
implementation(projects.tests.uitests)
implementation(projects.anvilannotations)
implementation(projects.appnav)
anvil(projects.anvilcodegen)
// https://developer.android.com/studio/write/java8-support#library-desugaring-versions

View File

@@ -18,9 +18,9 @@ package io.element.android.x
import android.app.Application
import androidx.startup.AppInitializer
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.libraries.di.DaggerComponentOwner
import io.element.android.x.info.logApplicationInfo
import io.element.android.x.initializer.CrashInitializer
import io.element.android.x.initializer.MatrixInitializer

View File

@@ -29,9 +29,7 @@ import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.x.di.AppBindings
import io.element.android.x.node.RootFlowNode
class MainActivity : NodeComponentActivity() {
@@ -47,13 +45,7 @@ class MainActivity : NodeComponentActivity() {
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
) {
NodeHost(integrationPoint = appyxIntegrationPoint) {
RootFlowNode(
buildContext = it,
appComponentOwner = applicationContext as DaggerComponentOwner,
authenticationService = appBindings.authenticationService(),
presenter = appBindings.rootPresenter(),
matrixClientsHolder = appBindings.matrixClientsHolder()
)
MainNode(it, appBindings.mainDaggerComponentOwner())
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import io.element.android.appnav.LoggedInFlowNode
import io.element.android.appnav.RoomFlowNode
import io.element.android.appnav.RootFlowNode
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.x.di.MainDaggerComponentsOwner
import io.element.android.x.di.RoomComponent
import io.element.android.x.di.SessionComponent
import kotlinx.parcelize.Parcelize
class MainNode(
buildContext: BuildContext,
private val mainDaggerComponentOwner: MainDaggerComponentsOwner,
) :
ParentNode<MainNode.RootNavTarget>(
navModel = PermanentNavModel(
navTargets = setOf(RootNavTarget),
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
),
DaggerComponentOwner by mainDaggerComponentOwner {
private val loggedInFlowNodeCallback = object : LoggedInFlowNode.LifecycleCallback {
override fun onFlowCreated(client: MatrixClient) {
val component = bindings<SessionComponent.ParentBindings>().sessionComponentBuilder().client(client).build()
mainDaggerComponentOwner.addComponent(client.sessionId.value, component)
}
override fun onFlowReleased(client: MatrixClient) {
mainDaggerComponentOwner.removeComponent(client.sessionId.value)
}
}
private val roomFlowNodeCallback = object : RoomFlowNode.LifecycleCallback {
override fun onFlowCreated(room: MatrixRoom) {
val component = bindings<RoomComponent.ParentBindings>().roomComponentBuilder().room(room).build()
mainDaggerComponentOwner.addComponent(room.roomId.value, component)
}
override fun onFlowReleased(room: MatrixRoom) {
mainDaggerComponentOwner.removeComponent(room.roomId.value)
}
}
override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node {
return createNode<RootFlowNode>(buildContext, plugins = listOf(loggedInFlowNodeCallback, roomFlowNodeCallback))
}
@Composable
override fun View(modifier: Modifier) {
Children(navModel = navModel)
}
@Parcelize
object RootNavTarget : Parcelable
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* 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.
@@ -17,13 +17,11 @@
package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo
import io.element.android.appnav.di.MatrixClientsHolder
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.x.root.RootPresenter
@ContributesTo(AppScope::class)
interface AppBindings {
fun rootPresenter(): RootPresenter
fun authenticationService(): MatrixAuthenticationService
fun matrixClientsHolder(): MatrixClientsHolder
fun mainDaggerComponentOwner(): MainDaggerComponentsOwner
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* 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.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* 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.
@@ -20,8 +20,6 @@ import android.content.Context
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.element.android.features.rageshake.reporter.BugReporter
import io.element.android.features.rageshake.reporter.DefaultBugReporter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
@@ -60,7 +58,4 @@ object AppModule {
diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
)
}
@Provides
fun providesBugReporter(bugReporter: DefaultBugReporter): BugReporter = bugReporter
}

View File

@@ -0,0 +1,43 @@
/*
* 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.content.Context
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.di.SingleIn
import javax.inject.Inject
@SingleIn(AppScope::class)
class MainDaggerComponentsOwner @Inject constructor(@ApplicationContext context: Context) : DaggerComponentOwner {
private val daggerComponents = LinkedHashMap<String, Any>().apply {
put("app", (context as DaggerComponentOwner).daggerComponent)
}
fun addComponent(identifier: String, component: Any) {
daggerComponents[identifier] = component
}
fun removeComponent(identifier: String) {
daggerComponents.remove(identifier)
}
override val daggerComponent: Any
get() = daggerComponents.values.reversed()
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* 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.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* 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.
@@ -28,7 +28,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
@SingleIn(SessionScope::class)
@MergeSubcomponent(SessionScope::class)
interface SessionComponent : NodeFactoriesBindings, RoomComponent.ParentBindings {
interface SessionComponent : NodeFactoriesBindings {
@Subcomponent.Builder
interface Builder {

View File

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

View File

@@ -18,7 +18,7 @@ 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.features.rageshake.impl.logs.VectorFileLogger
import io.element.android.x.BuildConfig
import timber.log.Timber

View File

@@ -1,70 +0,0 @@
/*
* 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.root
import io.element.android.features.rageshake.reporter.BugReporter
import io.element.android.features.rageshake.reporter.BugReporterListener
import io.element.android.features.rageshake.reporter.ReportType
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
// TODO Remove this duplicated class when we will rework modules.
class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Success) : BugReporter {
override fun sendBugReport(
coroutineScope: CoroutineScope,
reportType: ReportType,
withDevicesLogs: Boolean,
withCrashLogs: Boolean,
withKeyRequestHistory: Boolean,
withScreenshot: Boolean,
theBugDescription: String,
serverVersion: String,
canContact: Boolean,
customFields: Map<String, String>?,
listener: BugReporterListener?,
) {
coroutineScope.launch {
delay(100)
listener?.onProgress(0)
delay(100)
listener?.onProgress(50)
delay(100)
when (mode) {
FakeBugReporterMode.Success -> Unit
FakeBugReporterMode.Failure -> {
listener?.onUploadFailed(A_FAILURE_REASON)
return@launch
}
FakeBugReporterMode.Cancel -> {
listener?.onUploadCancelled()
return@launch
}
}
listener?.onProgress(100)
delay(100)
listener?.onUploadSucceed(null)
}
}
}
enum class FakeBugReporterMode {
Success,
Failure,
Cancel
}

View File

@@ -1,50 +0,0 @@
/*
* 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.root
import io.element.android.features.rageshake.crash.CrashDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
const val A_CRASH_DATA = "Some crash data"
// TODO Remove this duplicated class when we will rework modules.
class FakeCrashDataStore(
crashData: String = "",
appHasCrashed: Boolean = false,
) : CrashDataStore {
private val appHasCrashedFlow = MutableStateFlow(appHasCrashed)
private val crashDataFlow = MutableStateFlow(crashData)
override fun setCrashData(crashData: String) {
crashDataFlow.value = crashData
}
override suspend fun resetAppHasCrashed() {
appHasCrashedFlow.value = false
}
override fun appHasCrashed(): Flow<Boolean> = appHasCrashedFlow
override fun crashInfo(): Flow<String> = crashDataFlow
override suspend fun reset() {
appHasCrashedFlow.value = false
crashDataFlow.value = ""
}
}

View File

@@ -1,44 +0,0 @@
/*
* 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.root
import io.element.android.features.rageshake.rageshake.RageShake
// TODO Remove this duplicated class when we will rework modules.
class FakeRageShake(
private var isAvailableValue: Boolean = true
) : RageShake {
private var interceptor: (() -> Unit)? = null
override fun isAvailable() = isAvailableValue
override fun start(sensitivity: Float) {
}
override fun stop() {
}
override fun setSensitivity(sensitivity: Float) {
}
override fun setInterceptor(interceptor: (() -> Unit)?) {
this.interceptor = interceptor
}
fun triggerPhoneRageshake() = interceptor?.invoke()
}

View File

@@ -1,46 +0,0 @@
/*
* 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.root
import io.element.android.features.rageshake.rageshake.RageshakeDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
const val A_SENSITIVITY = 1f
// TODO Remove this duplicated class when we will rework modules.
class FakeRageshakeDataStore(
isEnabled: Boolean = true,
sensitivity: Float = A_SENSITIVITY,
) : RageshakeDataStore {
private val isEnabledFlow = MutableStateFlow(isEnabled)
override fun isEnabled(): Flow<Boolean> = isEnabledFlow
override suspend fun setIsEnabled(isEnabled: Boolean) {
isEnabledFlow.value = isEnabled
}
private val sensitivityFlow = MutableStateFlow(sensitivity)
override fun sensitivity(): Flow<Float> = sensitivityFlow
override suspend fun setSensitivity(sensitivity: Float) {
sensitivityFlow.value = sensitivity
}
override suspend fun reset() = Unit
}

59
appnav/build.gradle.kts Normal file
View File

@@ -0,0 +1,59 @@
/*
* Copyright (c) 2022 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.
*/
@file:Suppress("UnstableApiUsage")
import extension.allFeaturesApi
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
alias(libs.plugins.kapt)
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.appnav"
}
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(libs.dagger)
kapt(libs.dagger.compiler)
allFeaturesApi()
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrixui)
implementation(projects.tests.uitests)
implementation(libs.coil)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.rageshake.impl)
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.node
package io.element.android.appnav
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.NewRoot

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.node
package io.element.android.appnav
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
@@ -27,72 +27,79 @@ 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
import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.features.createroom.CreateRoomFlowNode
import io.element.android.features.preferences.PreferencesFlowNode
import io.element.android.features.roomlist.RoomListNode
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.NodeInputs
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.architecture.inputs
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
import io.element.android.x.di.SessionComponent
import kotlinx.parcelize.Parcelize
class LoggedInFlowNode(
buildContext: BuildContext,
val sessionId: SessionId,
private val matrixClient: MatrixClient,
private val onOpenBugReport: () -> Unit,
private val backstack: BackStack<NavTarget> = BackStack(
@ContributesNode(AppScope::class)
class LoggedInFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val roomListEntryPoint: RoomListEntryPoint,
private val preferencesEntryPoint: PreferencesEntryPoint,
private val createRoomEntryPoint: CreateRoomEntryPoint,
) : BackstackNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.RoomList,
savedStateMap = buildContext.savedStateMap,
),
) : ParentNode<LoggedInFlowNode.NavTarget>(
navModel = backstack,
buildContext = buildContext
), DaggerComponentOwner {
buildContext = buildContext,
plugins = plugins
) {
override val daggerComponent: Any by lazy {
parent!!.bindings<SessionComponent.ParentBindings>().sessionComponentBuilder().client(matrixClient).build()
interface Callback : Plugin {
fun onOpenBugReport() = Unit
}
interface LifecycleCallback : NodeLifecycleCallback {
fun onFlowCreated(client: MatrixClient) = Unit
fun onFlowReleased(client: MatrixClient) = Unit
}
data class Inputs(
val matrixClient: MatrixClient
) : NodeInputs
private val inputs: Inputs = inputs()
override fun onBuilt() {
super.onBuilt()
lifecycle.subscribe(
onCreate = {
plugins<LifecycleCallback>().forEach { it.onFlowCreated(inputs.matrixClient) }
val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory()
Coil.setImageLoader(imageLoaderFactory)
matrixClient.startSync()
inputs.matrixClient.startSync()
},
onDestroy = {
val imageLoaderFactory = bindings<MatrixUIBindings>().notLoggedInImageLoaderFactory()
Coil.setImageLoader(imageLoaderFactory)
plugins<LifecycleCallback>().forEach { it.onFlowReleased(inputs.matrixClient) }
}
)
}
private val roomListCallback = object : RoomListNode.Callback {
override fun onRoomClicked(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId))
}
override fun onSettingsClicked() {
backstack.push(NavTarget.Settings)
}
override fun onCreateRoomClicked() {
backstack.push(NavTarget.CreateRoom)
}
}
sealed interface NavTarget : Parcelable {
@Parcelize
object RoomList : NavTarget
@@ -110,10 +117,26 @@ class LoggedInFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.RoomList -> {
createNode<RoomListNode>(buildContext, plugins = listOf(roomListCallback))
val callback = object : RoomListEntryPoint.Callback {
override fun onRoomClicked(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId))
}
override fun onSettingsClicked() {
backstack.push(NavTarget.Settings)
}
override fun onCreateRoomClicked() {
backstack.push(NavTarget.CreateRoom)
}
}
roomListEntryPoint
.nodeBuilder(this, buildContext)
.callback(callback)
.build()
}
is NavTarget.Room -> {
val room = matrixClient.getRoom(roomId = navTarget.roomId)
val room = inputs.matrixClient.getRoom(roomId = navTarget.roomId)
if (room == null) {
// TODO CREATE UNKNOWN ROOM NODE
node(buildContext) {
@@ -122,14 +145,23 @@ class LoggedInFlowNode(
}
}
} else {
RoomFlowNode(buildContext, room)
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
val inputs = RoomFlowNode.Inputs(room)
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs) + nodeLifecycleCallbacks)
}
}
NavTarget.Settings -> {
PreferencesFlowNode(buildContext, onOpenBugReport)
val callback = object : PreferencesEntryPoint.Callback {
override fun onOpenBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() }
}
}
preferencesEntryPoint.nodeBuilder(this, buildContext)
.callback(callback)
.build()
}
NavTarget.CreateRoom -> {
CreateRoomFlowNode(buildContext)
createRoomEntryPoint.createNode(this, buildContext)
}
}
}

View File

@@ -0,0 +1,21 @@
/*
* 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.appnav
import com.bumble.appyx.core.plugin.Plugin
interface NodeLifecycleCallback : Plugin

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.node
package io.element.android.appnav
import android.os.Parcelable
import androidx.compose.runtime.Composable
@@ -23,27 +23,34 @@ 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
import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.features.login.LoginFlowNode
import io.element.android.features.onboarding.OnBoardingScreen
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.onboarding.api.OnBoardingEntryPoint
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.di.AppScope
import kotlinx.parcelize.Parcelize
import timber.log.Timber
class NotLoggedInFlowNode(
buildContext: BuildContext,
private val backstack: BackStack<NavTarget> = BackStack(
@ContributesNode(AppScope::class)
class NotLoggedInFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val onBoardingEntryPoint: OnBoardingEntryPoint,
private val loginEntryPoint: LoginEntryPoint,
) : BackstackNode<NotLoggedInFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.OnBoarding,
savedStateMap = buildContext.savedStateMap,
savedStateMap = buildContext.savedStateMap
),
) : ParentNode<NotLoggedInFlowNode.NavTarget>(
navModel = backstack,
buildContext = buildContext
buildContext = buildContext,
plugins = plugins,
) {
init {
lifecycle.subscribe(
onCreate = { Timber.v("OnCreate") },
@@ -61,12 +68,24 @@ class NotLoggedInFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.OnBoarding -> node(buildContext) {
OnBoardingScreen(
onSignIn = { backstack.push(NavTarget.LoginFlow) }
)
NavTarget.OnBoarding -> {
val callback = object : OnBoardingEntryPoint.Callback {
override fun onSignUp() {
//NOOP
}
override fun onSignIn() {
backstack.push(NavTarget.LoginFlow)
}
}
onBoardingEntryPoint
.nodeBuilder(this, buildContext)
.callback(callback)
.build()
}
NavTarget.LoginFlow -> {
loginEntryPoint.createNode(this, buildContext)
}
NavTarget.LoginFlow -> LoginFlowNode(buildContext)
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.node
package io.element.android.appnav
import android.os.Parcelable
import androidx.compose.runtime.Composable
@@ -23,43 +23,64 @@ 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
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
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 dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.x.di.RoomComponent
import kotlinx.parcelize.Parcelize
import timber.log.Timber
class RoomFlowNode(
buildContext: BuildContext,
private val room: MatrixRoom,
private val backstack: BackStack<NavTarget> = BackStack(
@ContributesNode(SessionScope::class)
class RoomFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val messagesEntryPoint: MessagesEntryPoint,
) : BackstackNode<RoomFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Messages,
savedStateMap = buildContext.savedStateMap,
),
) : ParentNode<RoomFlowNode.NavTarget>(
navModel = backstack,
buildContext = buildContext
), DaggerComponentOwner {
buildContext = buildContext,
plugins = plugins,
) {
override val daggerComponent: Any by lazy {
parent!!.bindings<RoomComponent.ParentBindings>().roomComponentBuilder().room(room).build()
interface LifecycleCallback : NodeLifecycleCallback {
fun onFlowCreated(room: MatrixRoom) = Unit
fun onFlowReleased(room: MatrixRoom) = Unit
}
data class Inputs(
val room: MatrixRoom,
) : NodeInputs
private val inputs: Inputs = inputs()
init {
lifecycle.subscribe(
onCreate = { Timber.v("OnCreate") },
onDestroy = { Timber.v("OnDestroy") }
onCreate = {
Timber.v("OnCreate")
plugins<LifecycleCallback>().forEach { it.onFlowCreated(inputs.room) }
},
onDestroy = {
Timber.v("OnDestroy")
plugins<LifecycleCallback>().forEach { it.onFlowReleased(inputs.room) }
}
)
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Messages -> createNode<MessagesNode>(buildContext)
NavTarget.Messages -> {
messagesEntryPoint.createNode(this, buildContext)
}
}
}

View File

@@ -14,58 +14,66 @@
* limitations under the License.
*/
package io.element.android.x.node
package io.element.android.appnav
import android.app.Activity
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.Children
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.core.node.node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
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.features.rageshake.bugreport.BugReportNode
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.di.MatrixClientsHolder
import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.x.di.MatrixClientsHolder
import io.element.android.x.root.RootPresenter
import io.element.android.x.root.RootView
import io.element.android.tests.uitests.openShowkase
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
import timber.log.Timber
class RootFlowNode(
private val buildContext: BuildContext,
private val backstack: BackStack<NavTarget> = BackStack(
initialElement = NavTarget.SplashScreen,
savedStateMap = buildContext.savedStateMap,
),
private val appComponentOwner: DaggerComponentOwner,
@ContributesNode(AppScope::class)
class RootFlowNode @AssistedInject constructor(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val authenticationService: MatrixAuthenticationService,
private val matrixClientsHolder: MatrixClientsHolder,
private val presenter: RootPresenter
private val presenter: RootPresenter,
private val bugReportEntryPoint: BugReportEntryPoint,
) :
ParentNode<RootFlowNode.NavTarget>(
navModel = backstack,
buildContext = buildContext
),
DaggerComponentOwner by appComponentOwner {
BackstackNode<RootFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.SplashScreen,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
) {
override fun onBuilt() {
super.onBuilt()
@@ -127,11 +135,17 @@ class RootFlowNode(
@Composable
override fun View(modifier: Modifier) {
val activity = LocalContext.current as Activity
fun openShowkase() {
openShowkase(activity)
}
val state = presenter.present()
RootView(
state = state,
modifier = modifier,
onOpenBugReport = this::onOpenBugReport,
onOpenShowkase = ::openShowkase
) {
Children(
navModel = backstack,
@@ -141,12 +155,6 @@ class RootFlowNode(
}
}
private val bugReportNodeCallback = object : BugReportNode.Callback {
override fun onBugReportSent() {
backstack.pop()
}
}
sealed interface NavTarget : Parcelable {
@Parcelize
object SplashScreen : NavTarget
@@ -168,16 +176,28 @@ class RootFlowNode(
Timber.w("Couldn't find any session, go through SplashScreen")
backstack.newRoot(NavTarget.SplashScreen)
}
LoggedInFlowNode(
buildContext = buildContext,
sessionId = navTarget.sessionId,
matrixClient = matrixClient,
onOpenBugReport = this::onOpenBugReport
)
val inputs = LoggedInFlowNode.Inputs(matrixClient)
val callback = object : LoggedInFlowNode.Callback {
override fun onOpenBugReport() {
backstack.push(NavTarget.BugReport)
}
}
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
createNode<LoggedInFlowNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
}
NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext)
NavTarget.NotLoggedInFlow -> createNode<NotLoggedInFlowNode>(buildContext)
NavTarget.SplashScreen -> splashNode(buildContext)
NavTarget.BugReport -> createNode<BugReportNode>(buildContext, plugins = listOf(bugReportNodeCallback))
NavTarget.BugReport -> {
val callback = object : BugReportEntryPoint.Callback {
override fun onBugReportSent() {
backstack.pop()
}
}
bugReportEntryPoint
.nodeBuilder(this, buildContext)
.callback(callback)
.build()
}
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.di
package io.element.android.appnav.di
import android.os.Bundle
import io.element.android.libraries.di.AppScope

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.root
package io.element.android.appnav.root
sealed interface RootEvents {
object HideShowkaseButton : RootEvents

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package io.element.android.x.root
package io.element.android.appnav.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.rageshake.crash.ui.CrashDetectionPresenter
import io.element.android.features.rageshake.detection.RageshakeDetectionPresenter
import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter
import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter
import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package io.element.android.x.root
package io.element.android.appnav.root
import androidx.compose.runtime.Stable
import io.element.android.features.rageshake.crash.ui.CrashDetectionState
import io.element.android.features.rageshake.detection.RageshakeDetectionState
import androidx.compose.runtime.Immutable
import io.element.android.features.rageshake.api.crash.CrashDetectionState
import io.element.android.features.rageshake.api.detection.RageshakeDetectionState
@Stable
@Immutable
data class RootState(
val isShowkaseButtonVisible: Boolean,
val rageshakeDetectionState: RageshakeDetectionState,

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package io.element.android.x.root
package io.element.android.appnav.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.rageshake.crash.ui.aCrashDetectionState
import io.element.android.features.rageshake.detection.aRageshakeDetectionState
import io.element.android.features.rageshake.api.crash.aCrashDetectionState
import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState
open class RootStateProvider : PreviewParameterProvider<RootState> {
override val values: Sequence<RootState>

View File

@@ -14,33 +14,30 @@
* limitations under the License.
*/
package io.element.android.x.root
package io.element.android.appnav.root
import android.app.Activity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
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.features.rageshake.api.crash.CrashDetectionEvents
import io.element.android.features.rageshake.api.crash.CrashDetectionView
import io.element.android.features.rageshake.api.detection.RageshakeDetectionEvents
import io.element.android.features.rageshake.api.detection.RageshakeDetectionView
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.tests.uitests.openShowkase
import io.element.android.x.component.ShowkaseButton
@Composable
fun RootView(
state: RootState,
modifier: Modifier = Modifier,
onOpenBugReport: () -> Unit = {},
onOpenShowkase: () -> Unit = {},
children: @Composable BoxScope.() -> Unit,
) {
Box(
@@ -50,7 +47,6 @@ fun RootView(
) {
children()
val eventSink = state.eventSink
val context = LocalContext.current
fun onOpenBugReport() {
state.crashDetectionState.eventSink(CrashDetectionEvents.ResetAppHasCrashed)
@@ -61,7 +57,7 @@ fun RootView(
ShowkaseButton(
isVisible = state.isShowkaseButtonVisible,
onCloseClicked = { eventSink(RootEvents.HideShowkaseButton) },
onClick = { openShowkase(context as Activity) }
onClick = onOpenShowkase
)
RageshakeDetectionView(
state = state.rageshakeDetectionState,

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.x.component
package io.element.android.appnav.root
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size

View File

@@ -16,15 +16,21 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.x.root
package io.element.android.appnav
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.rageshake.crash.ui.CrashDetectionPresenter
import io.element.android.features.rageshake.detection.RageshakeDetectionPresenter
import io.element.android.features.rageshake.preferences.RageshakePreferencesPresenter
import io.element.android.appnav.root.RootEvents
import io.element.android.appnav.root.RootPresenter
import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter
import io.element.android.features.rageshake.impl.detection.DefaultRageshakeDetectionPresenter
import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -61,13 +67,13 @@ class RootPresenterTest {
val rageshakeDataStore = FakeRageshakeDataStore()
val rageshake = FakeRageShake()
val screenshotHolder = FakeScreenshotHolder()
val crashDetectionPresenter = CrashDetectionPresenter(
val crashDetectionPresenter = DefaultCrashDetectionPresenter(
crashDataStore = crashDataStore
)
val rageshakeDetectionPresenter = RageshakeDetectionPresenter(
val rageshakeDetectionPresenter = DefaultRageshakeDetectionPresenter(
screenshotHolder = screenshotHolder,
rageShake = rageshake,
preferencesPresenter = RageshakePreferencesPresenter(
preferencesPresenter = DefaultRageshakePreferencesPresenter(
rageshake = rageshake,
rageshakeDataStore = rageshakeDataStore,
)

View File

@@ -1,6 +1,12 @@
import kotlinx.kover.api.KoverTaskExtension
import org.jetbrains.kotlin.cli.common.toBooleanLenient
buildscript {
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
}
}
/*
* Copyright (c) 2022 New Vector Ltd
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* 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.
@@ -14,19 +14,14 @@
* limitations under the License.
*/
package io.element.android.features.login
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.createroom.api"
}
dependencies {
implementation(projects.libraries.architecture)
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.createroom.api
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
interface CreateRoomEntryPoint : SimpleFeatureEntryPoint

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2023 New Vector Ltd
* Copyright (c) 2022 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.
@@ -18,13 +18,19 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.ksp)
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.createroom"
namespace = "io.element.android.features.createroom.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
anvil {
@@ -42,6 +48,7 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources)
implementation(projects.libraries.uiStrings)
api(projects.features.createroom.api)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.createroom
package io.element.android.features.createroom.impl
import android.os.Parcelable
import androidx.compose.runtime.Composable
@@ -22,22 +22,29 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.composable.Children
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.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import io.element.android.features.createroom.root.CreateRoomRootNode
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.createroom.impl.root.CreateRoomRootNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
class CreateRoomFlowNode(
buildContext: BuildContext,
private val backstack: BackStack<NavTarget> = BackStack(
@ContributesNode(SessionScope::class)
class CreateRoomFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : BackstackNode<CreateRoomFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
),
) : ParentNode<CreateRoomFlowNode.NavTarget>(
navModel = backstack,
buildContext = buildContext
buildContext = buildContext,
plugins = plugins
) {
sealed interface NavTarget : Parcelable {

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.createroom.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode<CreateRoomFlowNode>(buildContext)
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.createroom.root
package io.element.android.features.createroom.impl.root
sealed interface CreateRoomRootEvents {
object CreateRoom : CreateRoomRootEvents

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.createroom.root
package io.element.android.features.createroom.impl.root
import android.os.Parcelable
import androidx.compose.runtime.Composable
@@ -43,7 +43,7 @@ class CreateRoomRootNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
CreateRoomRootScreen(
CreateRoomRootView(
state = state,
modifier = modifier,
onClosePressed = this::navigateUp,

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.createroom.root
package io.element.android.features.createroom.impl.root
import androidx.compose.runtime.Composable
import io.element.android.libraries.architecture.Presenter

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.createroom.root
package io.element.android.features.createroom.impl.root
// TODO add your ui models. Remove the eventSink if you don't have events.
// Do not use default value, so no member get forgotten in the presenters.

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.createroom.root
package io.element.android.features.createroom.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.createroom.root
package io.element.android.features.createroom.impl.root
import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
@@ -59,7 +59,7 @@ import io.element.android.libraries.ui.strings.R as StringR
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateRoomRootScreen(
fun CreateRoomRootView(
state: CreateRoomRootState,
modifier: Modifier = Modifier,
onClosePressed: () -> Unit = {},
@@ -230,7 +230,7 @@ fun CreateRoomRootViewDarkPreview(@PreviewParameter(CreateRoomRootStateProvider:
@Composable
private fun ContentToPreview(state: CreateRoomRootState) {
CreateRoomRootScreen(
CreateRoomRootView(
state = state,
)
}

View File

@@ -16,12 +16,14 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.createroom.root
package io.element.android.features.createroom.impl.root
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.createroom.impl.root.CreateRoomRootEvents
import io.element.android.features.createroom.impl.root.CreateRoomRootPresenter
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* 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.
@@ -14,19 +14,14 @@
* limitations under the License.
*/
package io.element.android.features.logout
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.login.api"
}
dependencies {
implementation(projects.libraries.architecture)
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.api
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
interface LoginEntryPoint : SimpleFeatureEntryPoint

View File

@@ -18,13 +18,13 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.ksp)
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.login"
namespace = "io.element.android.features.login.impl"
testOptions {
unitTests {
@@ -47,6 +47,7 @@ dependencies {
implementation(projects.libraries.elementresources)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
api(projects.features.login.api)
ksp(libs.showkase.processor)
testImplementation(libs.test.junit)

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultLoginEntryPoint @Inject constructor() : LoginEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode<LoginFlowNode>(buildContext)
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login
package io.element.android.features.login.impl
import android.os.Parcelable
import androidx.compose.runtime.Composable
@@ -22,32 +22,33 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.composable.Children
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.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.features.login.changeserver.ChangeServerNode
import io.element.android.features.login.root.LoginRootNode
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.impl.changeserver.ChangeServerNode
import io.element.android.features.login.impl.root.LoginRootNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import kotlinx.parcelize.Parcelize
class LoginFlowNode(
buildContext: BuildContext,
private val backstack: BackStack<NavTarget> = BackStack(
@ContributesNode(AppScope::class)
class LoginFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : BackstackNode<LoginFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
),
) : ParentNode<LoginFlowNode.NavTarget>(
navModel = backstack,
buildContext = buildContext
buildContext = buildContext,
plugins = plugins,
) {
private val loginRootCallback = object : LoginRootNode.Callback {
override fun onChangeHomeServer() {
backstack.push(NavTarget.ChangeServer)
}
}
sealed interface NavTarget : Parcelable {
@Parcelize
object Root : NavTarget
@@ -58,7 +59,14 @@ class LoginFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Root -> createNode<LoginRootNode>(buildContext, plugins = listOf(loginRootCallback))
NavTarget.Root -> {
val callback = object : LoginRootNode.Callback {
override fun onChangeHomeServer() {
backstack.push(NavTarget.ChangeServer)
}
}
createNode<LoginRootNode>(buildContext, plugins = listOf(callback))
}
NavTarget.ChangeServer -> createNode<ChangeServerNode>(buildContext)
}
}

View File

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

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.changeserver
package io.element.android.features.login.impl.changeserver
import android.content.Context
import android.content.Intent
@@ -28,7 +28,7 @@ import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.util.LoginConstants
import io.element.android.features.login.impl.util.LoginConstants
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.di.AppScope

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.changeserver
package io.element.android.features.login.impl.changeserver
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
@@ -22,7 +22,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.login.util.LoginConstants
import io.element.android.features.login.impl.util.LoginConstants
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.changeserver
package io.element.android.features.login.impl.changeserver
import io.element.android.libraries.architecture.Async

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.changeserver
package io.element.android.features.login.impl.changeserver
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.changeserver
package io.element.android.features.login.impl.changeserver
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -45,7 +45,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
@@ -58,20 +57,20 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.login.R
import io.element.android.features.login.error.changeServerError
import io.element.android.features.login.util.LoginConstants
import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.error.changeServerError
import io.element.android.features.login.impl.util.LoginConstants
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.LinkColor
import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon
@@ -81,7 +80,6 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
import io.element.android.libraries.designsystem.theme.roomListRoomName
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import org.matrix.rustcomponents.sdk.AuthenticationException

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.error
package io.element.android.features.login.impl.error
import io.element.android.libraries.matrix.api.auth.AuthErrorCode
import io.element.android.libraries.matrix.api.auth.errorCode

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.root
package io.element.android.features.login.impl.root
sealed interface LoginRootEvents {
data class SetLogin(val login: String) : LoginRootEvents

View File

@@ -14,11 +14,10 @@
* limitations under the License.
*/
package io.element.android.features.login.root
package io.element.android.features.login.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@@ -26,7 +25,6 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class)
@@ -47,7 +45,7 @@ class LoginRootNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
LoginRootScreen(
LoginRootView(
state = state,
modifier = modifier,
onChangeServer = this::onChangeHomeServer,

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.root
package io.element.android.features.login.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
@@ -23,14 +23,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.login.util.LoginConstants
import io.element.android.features.login.impl.util.LoginConstants
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -87,5 +85,4 @@ class LoginRootPresenter @Inject constructor(private val authenticationService:
private fun updateFormState(formState: MutableState<LoginFormState>, updateLambda: LoginFormState.() -> LoginFormState) {
formState.value = updateLambda(formState.value)
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.root
package io.element.android.features.login.impl.root
import android.os.Parcelable
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.root
package io.element.android.features.login.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.root
package io.element.android.features.login.impl.root
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -62,7 +62,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.login.error.loginError
import io.element.android.features.login.impl.error.loginError
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState
@@ -85,7 +85,7 @@ import io.element.android.libraries.ui.strings.R as StringR
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginRootScreen(
fun LoginRootView(
state: LoginRootState,
modifier: Modifier = Modifier,
onChangeServer: () -> Unit = {},
@@ -349,7 +349,7 @@ internal fun LoginRootScreenDarkPreview(@PreviewParameter(LoginRootStateProvider
@Composable
private fun ContentToPreview(state: LoginRootState) {
LoginRootScreen(
LoginRootView(
state = state,
onBackPressed = {}
)

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.util
package io.element.android.features.login.impl.util
object LoginConstants {

View File

@@ -16,12 +16,14 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.login.changeserver
package io.element.android.features.login.impl.changeserver
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package io.element.android.features.login.error
package io.element.android.features.login.impl.error
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import io.element.android.libraries.ui.strings.R
import org.junit.Test
import org.matrix.rustcomponents.sdk.AuthenticationException
class ErrorFormatterTests {

View File

@@ -16,12 +16,16 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.login.root
package io.element.android.features.login.impl.root
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.root.LoggedInState
import io.element.android.features.login.impl.root.LoginFormState
import io.element.android.features.login.impl.root.LoginRootEvents
import io.element.android.features.login.impl.root.LoginRootPresenter
import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_PASSWORD
import io.element.android.libraries.matrix.test.A_SESSION_ID

View File

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

View File

@@ -1,38 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.features.login.test", appContext.packageName)
}
}

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 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.
-->
<manifest>
</manifest>

View File

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

View File

@@ -0,0 +1,29 @@
/*
* 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.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.features.logout.api"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
}

View File

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

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.logout.api
import io.element.android.libraries.architecture.Presenter
interface LogoutPreferencePresenter : Presenter<LogoutPreferenceState>

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.logout
package io.element.android.features.logout.api
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Logout

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.logout
package io.element.android.features.logout.api
import io.element.android.libraries.architecture.Async

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.logout
package io.element.android.features.logout.api
import io.element.android.libraries.architecture.Async

View File

@@ -18,12 +18,12 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.ksp)
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
}
android {
namespace = "io.element.android.features.logout"
namespace = "io.element.android.features.logout.impl"
}
anvil {
@@ -33,12 +33,14 @@ anvil {
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
api(projects.features.logout.api)
ksp(libs.showkase.processor)
testImplementation(libs.test.junit)

View File

@@ -15,6 +15,6 @@
~ limitations under the License.
-->
<manifest>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -14,23 +14,28 @@
* limitations under the License.
*/
package io.element.android.features.logout
package io.element.android.features.logout.impl
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 com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.logout.api.LogoutPreferenceEvents
import io.element.android.features.logout.api.LogoutPreferencePresenter
import io.element.android.features.logout.api.LogoutPreferenceState
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.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) :
Presenter<LogoutPreferenceState> {
@ContributesBinding(SessionScope::class)
class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) :
LogoutPreferencePresenter {
@Composable
override fun present(): LogoutPreferenceState {

View File

@@ -16,12 +16,14 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.logout
package io.element.android.features.logout.impl
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.LogoutPreferenceEvents
import io.element.android.features.logout.api.LogoutPreferenceState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THROWABLE
@@ -33,7 +35,7 @@ import org.junit.Test
class LogoutPreferencePresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = LogoutPreferencePresenter(
val presenter = DefaultLogoutPreferencePresenter(
FakeMatrixClient(A_SESSION_ID),
)
moleculeFlow(RecompositionClock.Immediate) {
@@ -46,7 +48,7 @@ class LogoutPreferencePresenterTest {
@Test
fun `present - logout`() = runTest {
val presenter = LogoutPreferencePresenter(
val presenter = DefaultLogoutPreferencePresenter(
FakeMatrixClient(A_SESSION_ID),
)
moleculeFlow(RecompositionClock.Immediate) {
@@ -64,7 +66,7 @@ class LogoutPreferencePresenterTest {
@Test
fun `present - logout with error`() = runTest {
val matrixClient = FakeMatrixClient(A_SESSION_ID)
val presenter = LogoutPreferencePresenter(
val presenter = DefaultLogoutPreferencePresenter(
matrixClient,
)
moleculeFlow(RecompositionClock.Immediate) {

View File

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

View File

@@ -1,38 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.logout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.features.login.test", appContext.packageName)
}
}

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 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.
-->
<manifest>
</manifest>

View File

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

View File

@@ -14,19 +14,16 @@
* limitations under the License.
*/
package io.element.android.features.login
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.messages.api"
}
dependencies {
implementation(projects.libraries.architecture)
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.api
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
interface MessagesEntryPoint : SimpleFeatureEntryPoint

View File

@@ -18,12 +18,12 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.ksp)
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
}
android {
namespace = "io.element.android.features.messages"
namespace = "io.element.android.features.messages.impl"
}
anvil {
@@ -33,6 +33,7 @@ anvil {
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.messages.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode<MessagesNode>(buildContext)
}
}

View File

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

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages
package io.element.android.features.messages.impl
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages
package io.element.android.features.messages.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -24,15 +24,15 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.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.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.textcomposer.MessageComposerState
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.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

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