Nav: First iteration integrating Appyx
This commit is contained in:
parent
c040e18431
commit
8b8b490bb2
28 changed files with 566 additions and 280 deletions
|
|
@ -6,6 +6,7 @@ plugins {
|
||||||
alias(libs.plugins.kapt)
|
alias(libs.plugins.kapt)
|
||||||
id("com.google.firebase.appdistribution") version "3.0.2"
|
id("com.google.firebase.appdistribution") version "3.0.2"
|
||||||
id("org.jetbrains.kotlinx.knit") version "0.4.0"
|
id("org.jetbrains.kotlinx.knit") version "0.4.0"
|
||||||
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -147,6 +148,7 @@ dependencies {
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.0")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.0")
|
||||||
implementation(libs.compose.destinations)
|
implementation(libs.compose.destinations)
|
||||||
ksp(libs.compose.destinations.processor)
|
ksp(libs.compose.destinations.processor)
|
||||||
|
implementation(libs.appyx.core)
|
||||||
|
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
implementation(libs.androidx.lifecycle.runtime)
|
implementation(libs.androidx.lifecycle.runtime)
|
||||||
|
|
|
||||||
|
|
@ -6,177 +6,48 @@
|
||||||
package io.element.android.x
|
package io.element.android.x
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.compose.animation.AnimatedContentScope
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.navigation.NavHostController
|
import com.bumble.appyx.core.integration.NodeHost
|
||||||
import com.airbnb.android.showkase.models.Showkase
|
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
|
||||||
import com.airbnb.mvrx.compose.mavericksActivityViewModel
|
|
||||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
|
||||||
import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi
|
import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi
|
||||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
|
||||||
import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations
|
|
||||||
import com.ramcosta.composedestinations.animations.rememberAnimatedNavHostEngine
|
|
||||||
import com.ramcosta.composedestinations.manualcomposablecalls.animatedComposable
|
|
||||||
import com.ramcosta.composedestinations.navigation.dependency
|
|
||||||
import com.ramcosta.composedestinations.spec.Route
|
|
||||||
import io.element.android.x.core.compose.OnLifecycleEvent
|
|
||||||
import io.element.android.x.core.di.DaggerComponentOwner
|
import io.element.android.x.core.di.DaggerComponentOwner
|
||||||
|
import io.element.android.x.core.di.bindings
|
||||||
import io.element.android.x.designsystem.ElementXTheme
|
import io.element.android.x.designsystem.ElementXTheme
|
||||||
import io.element.android.x.destinations.OnBoardingScreenNavigationDestination
|
import io.element.android.x.di.AppBindings
|
||||||
import kotlinx.coroutines.runBlocking
|
import io.element.android.x.node.RootFlowNode
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
private const val transitionAnimationDuration = 500
|
class MainActivity : NodeComponentActivity(), DaggerComponentOwner {
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
override val daggerComponent: Any
|
||||||
|
get() = listOfNotNull((applicationContext as? DaggerComponentOwner)?.daggerComponent)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
val appBindings = bindings<AppBindings>()
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
setContent {
|
setContent {
|
||||||
ElementXTheme {
|
ElementXTheme {
|
||||||
MainScreen(viewModel = mavericksActivityViewModel())
|
// A surface container using the 'background' color from the theme
|
||||||
}
|
Surface(
|
||||||
}
|
modifier = Modifier.fillMaxSize(),
|
||||||
}
|
color = MaterialTheme.colorScheme.background
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ShowkaseButton(
|
|
||||||
isVisible: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onCloseClicked: () -> Unit
|
|
||||||
) {
|
|
||||||
if (isVisible) {
|
|
||||||
Button(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 32.dp, start = 16.dp),
|
|
||||||
onClick = onClick
|
|
||||||
) {
|
|
||||||
Text(text = "Showkase Browser")
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 8.dp)
|
|
||||||
.size(16.dp),
|
|
||||||
onClick = onCloseClicked,
|
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Filled.Close, contentDescription = "")
|
NodeHost(integrationPoint = appyxIntegrationPoint) {
|
||||||
|
RootFlowNode(
|
||||||
|
buildContext = it,
|
||||||
|
daggerComponentOwner = this,
|
||||||
|
matrix = appBindings.matrix(),
|
||||||
|
sessionComponentsOwner = appBindings.sessionComponentsOwner()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MainScreen(viewModel: MainViewModel) {
|
|
||||||
val startRoute = runBlocking {
|
|
||||||
if (!viewModel.isLoggedIn()) {
|
|
||||||
OnBoardingScreenNavigationDestination
|
|
||||||
} else {
|
|
||||||
viewModel.restoreSession()
|
|
||||||
NavGraphs.root.startRoute
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isShowkaseButtonVisible by remember { mutableStateOf(BuildConfig.DEBUG) }
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
|
||||||
MainContent(
|
|
||||||
startRoute = startRoute
|
|
||||||
)
|
|
||||||
ShowkaseButton(
|
|
||||||
isVisible = isShowkaseButtonVisible,
|
|
||||||
onCloseClicked = { isShowkaseButtonVisible = false },
|
|
||||||
onClick = { startActivity(Showkase.getBrowserIntent(this@MainActivity)) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
OnLifecycleEvent { _, event ->
|
|
||||||
Timber.v("OnLifecycleEvent: $event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MainContent(startRoute: Route) {
|
|
||||||
val engine = rememberAnimatedNavHostEngine(
|
|
||||||
rootDefaultAnimations = RootNavGraphDefaultAnimations(
|
|
||||||
enterTransition = {
|
|
||||||
slideIntoContainer(
|
|
||||||
AnimatedContentScope.SlideDirection.Left,
|
|
||||||
animationSpec = tween(transitionAnimationDuration)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
exitTransition = {
|
|
||||||
slideOutOfContainer(
|
|
||||||
AnimatedContentScope.SlideDirection.Left,
|
|
||||||
animationSpec = tween(transitionAnimationDuration)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
popEnterTransition = {
|
|
||||||
slideIntoContainer(
|
|
||||||
AnimatedContentScope.SlideDirection.Right,
|
|
||||||
animationSpec = tween(transitionAnimationDuration)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
popExitTransition = {
|
|
||||||
slideOutOfContainer(
|
|
||||||
AnimatedContentScope.SlideDirection.Right,
|
|
||||||
animationSpec = tween(transitionAnimationDuration)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val navController = engine.rememberNavController()
|
|
||||||
LogNavigation(navController)
|
|
||||||
|
|
||||||
DestinationsNavHost(
|
|
||||||
modifier = Modifier.background(MaterialTheme.colorScheme.background),
|
|
||||||
engine = engine,
|
|
||||||
navController = navController,
|
|
||||||
navGraph = NavGraphs.root,
|
|
||||||
startRoute = startRoute,
|
|
||||||
dependenciesContainerBuilder = {
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LogNavigation(navController: NavHostController) {
|
|
||||||
LaunchedEffect(key1 = navController) {
|
|
||||||
navController.appCurrentDestinationFlow.collect {
|
|
||||||
Timber.d("Navigating to ${it.route}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@Preview
|
|
||||||
fun MainContentPreview() {
|
|
||||||
MainContent(startRoute = OnBoardingScreenNavigationDestination)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
package io.element.android.x
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import com.ramcosta.composedestinations.navigation.popUpTo
|
|
||||||
import io.element.android.x.core.di.bindings
|
|
||||||
import io.element.android.x.destinations.ChangeServerScreenNavigationDestination
|
|
||||||
import io.element.android.x.destinations.LoginScreenNavigationDestination
|
|
||||||
import io.element.android.x.destinations.MessagesScreenNavigationDestination
|
|
||||||
import io.element.android.x.destinations.OnBoardingScreenNavigationDestination
|
|
||||||
import io.element.android.x.destinations.RoomListScreenNavigationDestination
|
|
||||||
import io.element.android.x.di.AppBindings
|
|
||||||
import io.element.android.x.features.login.LoginScreen
|
|
||||||
import io.element.android.x.features.login.changeserver.ChangeServerScreen
|
|
||||||
import io.element.android.x.features.messages.MessagesScreen
|
|
||||||
import io.element.android.x.features.onboarding.OnBoardingScreen
|
|
||||||
import io.element.android.x.features.roomlist.RoomListScreen
|
|
||||||
import io.element.android.x.matrix.core.RoomId
|
|
||||||
|
|
||||||
@Destination
|
|
||||||
@Composable
|
|
||||||
fun OnBoardingScreenNavigation(navigator: DestinationsNavigator) {
|
|
||||||
OnBoardingScreen(
|
|
||||||
onSignUp = {
|
|
||||||
// TODO
|
|
||||||
},
|
|
||||||
onSignIn = {
|
|
||||||
navigator.navigate(LoginScreenNavigationDestination)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Destination
|
|
||||||
@Composable
|
|
||||||
fun LoginScreenNavigation(navigator: DestinationsNavigator) {
|
|
||||||
val sessionComponentsOwner = LocalContext.current.bindings<AppBindings>().sessionComponentsOwner()
|
|
||||||
LoginScreen(
|
|
||||||
onChangeServer = {
|
|
||||||
navigator.navigate(ChangeServerScreenNavigationDestination)
|
|
||||||
},
|
|
||||||
onLoginWithSuccess = {
|
|
||||||
sessionComponentsOwner.create(it)
|
|
||||||
navigator.navigate(RoomListScreenNavigationDestination) {
|
|
||||||
popUpTo(OnBoardingScreenNavigationDestination) {
|
|
||||||
inclusive = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Create a subgraph in Login module
|
|
||||||
@Destination
|
|
||||||
@Composable
|
|
||||||
fun ChangeServerScreenNavigation(navigator: DestinationsNavigator) {
|
|
||||||
ChangeServerScreen(
|
|
||||||
onChangeServerSuccess = {
|
|
||||||
navigator.popBackStack()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RootNavGraph(start = true)
|
|
||||||
@Destination
|
|
||||||
@Composable
|
|
||||||
fun RoomListScreenNavigation(navigator: DestinationsNavigator) {
|
|
||||||
val sessionComponentsOwner = LocalContext.current.bindings<AppBindings>().sessionComponentsOwner()
|
|
||||||
RoomListScreen(
|
|
||||||
onRoomClicked = { roomId: RoomId ->
|
|
||||||
navigator.navigate(MessagesScreenNavigationDestination(roomId = roomId.value))
|
|
||||||
},
|
|
||||||
onSuccessLogout = {
|
|
||||||
sessionComponentsOwner.releaseActiveSession()
|
|
||||||
navigator.navigate(OnBoardingScreenNavigationDestination) {
|
|
||||||
popUpTo(RoomListScreenNavigationDestination) {
|
|
||||||
inclusive = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Destination
|
|
||||||
@Composable
|
|
||||||
fun MessagesScreenNavigation(roomId: String, navigator: DestinationsNavigator) {
|
|
||||||
MessagesScreen(roomId = roomId, onBackPressed = navigator::navigateUp)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package io.element.android.x.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ShowkaseButton(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isVisible: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onCloseClicked: () -> Unit
|
||||||
|
) {
|
||||||
|
if (isVisible) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 32.dp, start = 16.dp),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
Text(text = "Showkase Browser")
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.size(16.dp),
|
||||||
|
onClick = onCloseClicked,
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.Filled.Close, contentDescription = "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ package io.element.android.x.di
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import io.element.android.x.matrix.Matrix
|
import io.element.android.x.matrix.Matrix
|
||||||
|
import io.element.android.x.node.LoggedInFlowNode
|
||||||
|
import io.element.android.x.node.RootFlowNode
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(AppScope::class)
|
||||||
|
|
@ -9,4 +11,4 @@ interface AppBindings {
|
||||||
fun coroutineScope(): CoroutineScope
|
fun coroutineScope(): CoroutineScope
|
||||||
fun matrix(): Matrix
|
fun matrix(): Matrix
|
||||||
fun sessionComponentsOwner(): SessionComponentsOwner
|
fun sessionComponentsOwner(): SessionComponentsOwner
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,18 @@ package io.element.android.x.di
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import io.element.android.x.core.di.bindings
|
import io.element.android.x.core.di.bindings
|
||||||
import io.element.android.x.matrix.MatrixClient
|
import io.element.android.x.matrix.MatrixClient
|
||||||
|
import io.element.android.x.matrix.core.SessionId
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
class SessionComponentsOwner @Inject constructor(@ApplicationContext private val context: Context) {
|
class SessionComponentsOwner @Inject constructor(@ApplicationContext private val context: Context) {
|
||||||
|
|
||||||
private val sessionComponents = ConcurrentHashMap<String, SessionComponent>()
|
private val sessionComponents = ConcurrentHashMap<SessionId, SessionComponent>()
|
||||||
var activeSessionComponent: SessionComponent? = null
|
var activeSessionComponent: SessionComponent? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun setActive(sessionId: String) {
|
fun setActive(sessionId: SessionId) {
|
||||||
val sessionComponent = sessionComponents[sessionId]
|
val sessionComponent = sessionComponents[sessionId]
|
||||||
if (activeSessionComponent != sessionComponent) {
|
if (activeSessionComponent != sessionComponent) {
|
||||||
activeSessionComponent = sessionComponent
|
activeSessionComponent = sessionComponent
|
||||||
|
|
@ -35,7 +36,7 @@ class SessionComponentsOwner @Inject constructor(@ApplicationContext private val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun release(sessionId: String) {
|
fun release(sessionId: SessionId) {
|
||||||
val sessionComponent = sessionComponents.remove(sessionId)
|
val sessionComponent = sessionComponents.remove(sessionId)
|
||||||
if (activeSessionComponent == sessionComponent) {
|
if (activeSessionComponent == sessionComponent) {
|
||||||
activeSessionComponent = null
|
activeSessionComponent = null
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class MatrixInitializer : Initializer<Unit> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
|
override fun dependencies(): List<Class<out Initializer<*>>> = listOf(TimberInitializer::class.java)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,5 @@ class TimberInitializer : Initializer<Unit> {
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dependencies(): List<Class<out Initializer<*>>> =
|
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
|
||||||
listOf(TimberInitializer::class.java)
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package io.element.android.x.node
|
||||||
|
|
||||||
|
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.node.Node
|
||||||
|
import com.bumble.appyx.core.node.ParentNode
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
|
import io.element.android.x.core.di.viewModelSupportNode
|
||||||
|
import io.element.android.x.features.messages.MessagesScreen
|
||||||
|
import io.element.android.x.features.roomlist.RoomListScreen
|
||||||
|
import io.element.android.x.matrix.core.RoomId
|
||||||
|
import io.element.android.x.matrix.core.SessionId
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
class LoggedInFlowNode(
|
||||||
|
buildContext: BuildContext,
|
||||||
|
val sessionId: SessionId,
|
||||||
|
private val backstack: BackStack<NavTarget> = BackStack(
|
||||||
|
initialElement = NavTarget.RoomList,
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
) : ParentNode<LoggedInFlowNode.NavTarget>(
|
||||||
|
navModel = backstack,
|
||||||
|
buildContext = buildContext
|
||||||
|
) {
|
||||||
|
|
||||||
|
sealed interface NavTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
object RoomList : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Messages(val roomId: RoomId) : NavTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
return when (navTarget) {
|
||||||
|
NavTarget.RoomList -> viewModelSupportNode(buildContext) {
|
||||||
|
RoomListScreen(
|
||||||
|
onRoomClicked = { backstack.push(NavTarget.Messages(it)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is NavTarget.Messages -> viewModelSupportNode(buildContext) {
|
||||||
|
MessagesScreen(
|
||||||
|
roomId = navTarget.roomId.value,
|
||||||
|
onBackPressed = { backstack.pop() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
Children(navModel = backstack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package io.element.android.x.node
|
||||||
|
|
||||||
|
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.node.Node
|
||||||
|
import com.bumble.appyx.core.node.ParentNode
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
|
import io.element.android.x.core.di.viewModelSupportNode
|
||||||
|
import io.element.android.x.features.login.node.LoginFlowNode
|
||||||
|
import io.element.android.x.features.onboarding.OnBoardingScreen
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
class NotLoggedInFlowNode(
|
||||||
|
buildContext: BuildContext,
|
||||||
|
private val backstack: BackStack<NavTarget> = BackStack(
|
||||||
|
initialElement = NavTarget.OnBoarding,
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
) : ParentNode<NotLoggedInFlowNode.NavTarget>(
|
||||||
|
navModel = backstack,
|
||||||
|
buildContext = buildContext
|
||||||
|
) {
|
||||||
|
|
||||||
|
sealed interface NavTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
object OnBoarding : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object LoginFlow : NavTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
return when (navTarget) {
|
||||||
|
NavTarget.OnBoarding -> viewModelSupportNode(buildContext) {
|
||||||
|
OnBoardingScreen(
|
||||||
|
onSignIn = { backstack.replace(NavTarget.LoginFlow) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
NavTarget.LoginFlow -> LoginFlowNode(buildContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
Children(navModel = backstack)
|
||||||
|
}
|
||||||
|
}
|
||||||
136
app/src/main/java/io/element/android/x/node/RootFlowNode.kt
Normal file
136
app/src/main/java/io/element/android/x/node/RootFlowNode.kt
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
package io.element.android.x.node
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
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.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.airbnb.android.showkase.models.Showkase
|
||||||
|
import com.bumble.appyx.core.children.whenChildAttached
|
||||||
|
import com.bumble.appyx.core.clienthelper.interactor.Interactor
|
||||||
|
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.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
|
import io.element.android.x.BuildConfig
|
||||||
|
import io.element.android.x.component.ShowkaseButton
|
||||||
|
import io.element.android.x.core.di.DaggerComponentOwner
|
||||||
|
import io.element.android.x.di.SessionComponentsOwner
|
||||||
|
import io.element.android.x.getBrowserIntent
|
||||||
|
import io.element.android.x.matrix.Matrix
|
||||||
|
import io.element.android.x.matrix.core.SessionId
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class SessionComponentsOwnerInteractor(private val sessionComponentsOwner: SessionComponentsOwner) : Interactor<RootFlowNode>() {
|
||||||
|
override fun onCreate(lifecycle: Lifecycle) {
|
||||||
|
lifecycle.subscribe(onCreate = {
|
||||||
|
whenChildAttached { commonLifecycle: Lifecycle, child: LoggedInFlowNode ->
|
||||||
|
Timber.v("LoggedInFlowNode attached: ${child.sessionId} ")
|
||||||
|
commonLifecycle.subscribe(
|
||||||
|
onDestroy = {
|
||||||
|
Timber.v("LoggedInFlowNode destroyed: ${child.sessionId}")
|
||||||
|
sessionComponentsOwner.release(child.sessionId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RootFlowNode(
|
||||||
|
buildContext: BuildContext,
|
||||||
|
private val backstack: BackStack<NavTarget> = BackStack(
|
||||||
|
initialElement = NavTarget.SplashScreen,
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
private val daggerComponentOwner: DaggerComponentOwner,
|
||||||
|
private val matrix: Matrix,
|
||||||
|
private val sessionComponentsOwner: SessionComponentsOwner,
|
||||||
|
) :
|
||||||
|
ParentNode<RootFlowNode.NavTarget>(
|
||||||
|
navModel = backstack,
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = listOf(SessionComponentsOwnerInteractor(sessionComponentsOwner)),
|
||||||
|
),
|
||||||
|
|
||||||
|
DaggerComponentOwner by daggerComponentOwner {
|
||||||
|
|
||||||
|
init {
|
||||||
|
matrix.isLoggedIn()
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach { isLoggedIn ->
|
||||||
|
if (isLoggedIn) {
|
||||||
|
val matrixClient = matrix.restoreSession()
|
||||||
|
if (matrixClient == null) {
|
||||||
|
backstack.replace(NavTarget.NotLoggedInFlow)
|
||||||
|
} else {
|
||||||
|
matrixClient.startSync()
|
||||||
|
sessionComponentsOwner.create(matrixClient)
|
||||||
|
backstack.replace(NavTarget.LoggedInFlow(matrixClient.sessionId))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
backstack.replace(NavTarget.NotLoggedInFlow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
var isShowkaseButtonVisible by remember { mutableStateOf(BuildConfig.DEBUG) }
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.TopCenter,
|
||||||
|
) {
|
||||||
|
Children(navModel = backstack)
|
||||||
|
val context = LocalContext.current
|
||||||
|
ShowkaseButton(
|
||||||
|
isVisible = isShowkaseButtonVisible,
|
||||||
|
onCloseClicked = { isShowkaseButtonVisible = false },
|
||||||
|
onClick = { startActivity(context, Showkase.getBrowserIntent(context), null) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface NavTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
object SplashScreen : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object NotLoggedInFlow : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class LoggedInFlow(val sessionId: SessionId) : NavTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
return when (navTarget) {
|
||||||
|
is NavTarget.LoggedInFlow -> LoggedInFlowNode(buildContext, navTarget.sessionId)
|
||||||
|
NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext)
|
||||||
|
NavTarget.SplashScreen -> node(buildContext) {
|
||||||
|
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
alias(libs.plugins.anvil)
|
alias(libs.plugins.anvil)
|
||||||
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -21,6 +22,7 @@ dependencies {
|
||||||
implementation(project(":libraries:matrix"))
|
implementation(project(":libraries:matrix"))
|
||||||
implementation(project(":libraries:designsystem"))
|
implementation(project(":libraries:designsystem"))
|
||||||
implementation(project(":libraries:elementresources"))
|
implementation(project(":libraries:elementresources"))
|
||||||
|
implementation(libs.appyx.core)
|
||||||
implementation(libs.mavericks.compose)
|
implementation(libs.mavericks.compose)
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,14 @@ import com.airbnb.mvrx.compose.collectAsState
|
||||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
import com.airbnb.mvrx.compose.mavericksViewModel
|
||||||
import io.element.android.x.designsystem.ElementXTheme
|
import io.element.android.x.designsystem.ElementXTheme
|
||||||
import io.element.android.x.features.login.error.loginError
|
import io.element.android.x.features.login.error.loginError
|
||||||
import io.element.android.x.matrix.MatrixClient
|
import io.element.android.x.matrix.core.SessionId
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
viewModel: LoginViewModel = mavericksViewModel(),
|
viewModel: LoginViewModel = mavericksViewModel(),
|
||||||
onChangeServer: () -> Unit = { },
|
onChangeServer: () -> Unit = { },
|
||||||
onLoginWithSuccess: (MatrixClient) -> Unit = { },
|
onLoginWithSuccess: (SessionId) -> Unit = { },
|
||||||
) {
|
) {
|
||||||
val state: LoginViewState by viewModel.collectAsState()
|
val state: LoginViewState by viewModel.collectAsState()
|
||||||
val formState: LoginFormState by viewModel.formState
|
val formState: LoginFormState by viewModel.formState
|
||||||
|
|
@ -85,7 +85,7 @@ fun LoginContent(
|
||||||
onLoginChanged: (String) -> Unit = {},
|
onLoginChanged: (String) -> Unit = {},
|
||||||
onPasswordChanged: (String) -> Unit = {},
|
onPasswordChanged: (String) -> Unit = {},
|
||||||
onSubmitClicked: () -> Unit = {},
|
onSubmitClicked: () -> Unit = {},
|
||||||
onLoginWithSuccess: (MatrixClient) -> Unit = {},
|
onLoginWithSuccess: (SessionId) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -105,7 +105,7 @@ fun LoginContent(
|
||||||
)
|
)
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
) {
|
) {
|
||||||
val isError = state.loggedInClient is Fail
|
val isError = state.loggedInSessionId is Fail
|
||||||
// Title
|
// Title
|
||||||
Text(
|
Text(
|
||||||
text = "Welcome back",
|
text = "Welcome back",
|
||||||
|
|
@ -160,7 +160,7 @@ fun LoginContent(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
if (state.loggedInClient is Loading) {
|
if (state.loggedInSessionId is Loading) {
|
||||||
// Ensure password is hidden when user submits the form
|
// Ensure password is hidden when user submits the form
|
||||||
passwordVisible = false
|
passwordVisible = false
|
||||||
}
|
}
|
||||||
|
|
@ -193,9 +193,9 @@ fun LoginContent(
|
||||||
onDone = { onSubmitClicked() }
|
onDone = { onSubmitClicked() }
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (state.loggedInClient is Fail) {
|
if (state.loggedInSessionId is Fail) {
|
||||||
Text(
|
Text(
|
||||||
text = loginError(state.formState, state.loggedInClient.error),
|
text = loginError(state.formState, state.loggedInSessionId.error),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
modifier = Modifier.padding(start = 16.dp)
|
modifier = Modifier.padding(start = 16.dp)
|
||||||
|
|
@ -212,12 +212,12 @@ fun LoginContent(
|
||||||
) {
|
) {
|
||||||
Text(text = "Continue")
|
Text(text = "Continue")
|
||||||
}
|
}
|
||||||
when (val loggedInClient = state.loggedInClient) {
|
when (val loggedInSessionId = state.loggedInSessionId) {
|
||||||
is Success -> onLoginWithSuccess(loggedInClient())
|
is Success -> onLoginWithSuccess(loggedInSessionId())
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state.loggedInClient is Loading) {
|
if (state.loggedInSessionId is Loading) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -48,22 +48,20 @@ class LoginViewModel @AssistedInject constructor(
|
||||||
val state = awaitState()
|
val state = awaitState()
|
||||||
// Ensure the server is provided to the Rust SDK
|
// Ensure the server is provided to the Rust SDK
|
||||||
matrix.setHomeserver(state.homeserver)
|
matrix.setHomeserver(state.homeserver)
|
||||||
matrix.login(state.formState.login.trim(), state.formState.password.trim()).also {
|
matrix.login(state.formState.login.trim(), state.formState.password.trim())
|
||||||
it.startSync()
|
|
||||||
}
|
|
||||||
}.execute {
|
}.execute {
|
||||||
copy(loggedInClient = it)
|
copy(loggedInSessionId = it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSetPassword(password: String) {
|
fun onSetPassword(password: String) {
|
||||||
formState.value = formState.value.copy(password = password)
|
formState.value = formState.value.copy(password = password)
|
||||||
setState { copy(loggedInClient = Uninitialized) }
|
setState { copy(loggedInSessionId = Uninitialized) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSetName(name: String) {
|
fun onSetName(name: String) {
|
||||||
formState.value = formState.value.copy(login = name)
|
formState.value = formState.value.copy(login = name)
|
||||||
setState { copy(loggedInClient = Uninitialized) }
|
setState { copy(loggedInSessionId = Uninitialized) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@ import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import io.element.android.x.matrix.MatrixClient
|
import io.element.android.x.matrix.core.SessionId
|
||||||
|
|
||||||
data class LoginViewState(
|
data class LoginViewState(
|
||||||
val homeserver: String = "",
|
val homeserver: String = "",
|
||||||
val loggedInClient: Async<MatrixClient> = Uninitialized,
|
val loggedInSessionId: Async<SessionId> = Uninitialized,
|
||||||
val formState: LoginFormState = LoginFormState.Default,
|
val formState: LoginFormState = LoginFormState.Default,
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
val submitEnabled =
|
val submitEnabled =
|
||||||
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInClient !is Loading
|
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInSessionId !is Loading
|
||||||
}
|
}
|
||||||
|
|
||||||
data class LoginFormState(
|
data class LoginFormState(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package io.element.android.x.features.login.node
|
||||||
|
|
||||||
|
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.node.Node
|
||||||
|
import com.bumble.appyx.core.node.ParentNode
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
|
import io.element.android.x.core.di.viewModelSupportNode
|
||||||
|
import io.element.android.x.features.login.LoginScreen
|
||||||
|
import io.element.android.x.features.login.changeserver.ChangeServerScreen
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
class LoginFlowNode(
|
||||||
|
buildContext: BuildContext,
|
||||||
|
private val backstack: BackStack<NavTarget> = BackStack(
|
||||||
|
initialElement = NavTarget.Root,
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
) : ParentNode<LoginFlowNode.NavTarget>(
|
||||||
|
navModel = backstack,
|
||||||
|
buildContext = buildContext
|
||||||
|
) {
|
||||||
|
|
||||||
|
sealed interface NavTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
object Root : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object ChangeServer : NavTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
return when (navTarget) {
|
||||||
|
NavTarget.Root -> viewModelSupportNode(buildContext) {
|
||||||
|
LoginScreen(
|
||||||
|
onChangeServer = { backstack.push(NavTarget.ChangeServer) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
NavTarget.ChangeServer -> viewModelSupportNode(buildContext) {
|
||||||
|
ChangeServerScreen(
|
||||||
|
onChangeServerSuccess = { backstack.pop() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
Children(navModel = backstack)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ dependencies {
|
||||||
implementation(project(":libraries:matrix"))
|
implementation(project(":libraries:matrix"))
|
||||||
implementation(project(":libraries:designsystem"))
|
implementation(project(":libraries:designsystem"))
|
||||||
implementation(project(":libraries:textcomposer"))
|
implementation(project(":libraries:textcomposer"))
|
||||||
|
implementation(libs.appyx.core)
|
||||||
implementation(libs.mavericks.compose)
|
implementation(libs.mavericks.compose)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ dependencies {
|
||||||
implementation(libs.mavericks.compose)
|
implementation(libs.mavericks.compose)
|
||||||
implementation(libs.accompanist.pager)
|
implementation(libs.accompanist.pager)
|
||||||
implementation(libs.accompanist.pagerindicator)
|
implementation(libs.accompanist.pagerindicator)
|
||||||
|
implementation(libs.appyx.core)
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
androidTestImplementation(libs.test.junitext)
|
androidTestImplementation(libs.test.junitext)
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ class SplashCarouselStateFactory {
|
||||||
|
|
||||||
fun hero(@DrawableRes lightDrawable: Int, @DrawableRes darkDrawable: Int) =
|
fun hero(@DrawableRes lightDrawable: Int, @DrawableRes darkDrawable: Int) =
|
||||||
if (lightTheme) lightDrawable else darkDrawable
|
if (lightTheme) lightDrawable else darkDrawable
|
||||||
|
|
||||||
return SplashCarouselState(
|
return SplashCarouselState(
|
||||||
listOf(
|
listOf(
|
||||||
SplashCarouselState.Item(
|
SplashCarouselState.Item(
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ dependencies {
|
||||||
implementation(project(":libraries:core"))
|
implementation(project(":libraries:core"))
|
||||||
implementation(project(":libraries:matrix"))
|
implementation(project(":libraries:matrix"))
|
||||||
implementation(project(":libraries:designsystem"))
|
implementation(project(":libraries:designsystem"))
|
||||||
|
implementation(libs.appyx.core)
|
||||||
implementation(libs.mavericks.compose)
|
implementation(libs.mavericks.compose)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
implementation(libs.accompanist.placeholder)
|
implementation(libs.accompanist.placeholder)
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ serialization_json = "1.4.1"
|
||||||
showkase = "1.0.0-beta14"
|
showkase = "1.0.0-beta14"
|
||||||
compose_destinations = "1.7.23-beta"
|
compose_destinations = "1.7.23-beta"
|
||||||
jsoup = "1.15.3"
|
jsoup = "1.15.3"
|
||||||
|
appyx = "1.0.1"
|
||||||
|
|
||||||
# DI
|
# DI
|
||||||
dagger = "2.43"
|
dagger = "2.43"
|
||||||
|
|
@ -117,7 +118,7 @@ compose_destinations_processor = { module = "io.github.raamcosta.compose-destina
|
||||||
showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
|
showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
|
||||||
showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
|
showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
|
||||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||||
|
appyx_core = {module = "com.bumble.appyx:core", version.ref = "appyx"}
|
||||||
|
|
||||||
# Di
|
# Di
|
||||||
inject = {module = "javax.inject:javax.inject", version = "1"}
|
inject = {module = "javax.inject:javax.inject", version = "1"}
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,5 @@ dependencies {
|
||||||
api(libs.mavericks.compose)
|
api(libs.mavericks.compose)
|
||||||
api(libs.dagger)
|
api(libs.dagger)
|
||||||
api(libs.androidx.fragment)
|
api(libs.androidx.fragment)
|
||||||
|
api(libs.appyx.core)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package io.element.android.x.core.di
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use this to get the Dagger "Bindings" for your module. Bindings are used if you need to directly interact with a dagger component such as:
|
* Use this to get the Dagger "Bindings" for your module. Bindings are used if you need to directly interact with a dagger component such as:
|
||||||
|
|
@ -20,6 +21,7 @@ import androidx.fragment.app.Fragment
|
||||||
* 2) Contribute your interface to the correct component via `@ContributesTo(AppScope::class)`.
|
* 2) Contribute your interface to the correct component via `@ContributesTo(AppScope::class)`.
|
||||||
* 3) Call bindings<YourModuleBindings>().inject(this).
|
* 3) Call bindings<YourModuleBindings>().inject(this).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
inline fun <reified T : Any> Context.bindings() = bindings(T::class.java)
|
inline fun <reified T : Any> Context.bindings() = bindings(T::class.java)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -27,6 +29,8 @@ inline fun <reified T : Any> Context.bindings() = bindings(T::class.java)
|
||||||
*/
|
*/
|
||||||
inline fun <reified T : Any> Fragment.bindings() = bindings(T::class.java)
|
inline fun <reified T : Any> Fragment.bindings() = bindings(T::class.java)
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Node.bindings() = bindings(T::class.java)
|
||||||
|
|
||||||
/** Use no-arg extension function instead: [Context.bindings] */
|
/** Use no-arg extension function instead: [Context.bindings] */
|
||||||
fun <T : Any> Context.bindings(klass: Class<T>): T {
|
fun <T : Any> Context.bindings(klass: Class<T>): T {
|
||||||
// search dagger components in the context hierarchy
|
// search dagger components in the context hierarchy
|
||||||
|
|
@ -50,4 +54,16 @@ fun <T : Any> Fragment.bindings(klass: Class<T>): T {
|
||||||
.filterIsInstance(klass)
|
.filterIsInstance(klass)
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?: requireActivity().bindings(klass)
|
?: requireActivity().bindings(klass)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Use no-arg extension function instead: [Node.bindings] */
|
||||||
|
fun <T : Any> Node.bindings(klass: Class<T>): T {
|
||||||
|
// search dagger components in node hierarchy
|
||||||
|
return generateSequence(this, Node::parent)
|
||||||
|
.filterIsInstance<DaggerComponentOwner>()
|
||||||
|
.map { it.daggerComponent }
|
||||||
|
.flatMap { if (it is Collection<*>) it else listOf(it) }
|
||||||
|
.filterIsInstance(klass)
|
||||||
|
.firstOrNull()
|
||||||
|
?: error("Unable to find bindings for ${klass.name}")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
package io.element.android.x.core.di
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.DEFAULT_ARGS_KEY
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.HasDefaultViewModelProviderFactory
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.SAVED_STATE_REGISTRY_OWNER_KEY
|
||||||
|
import androidx.lifecycle.SavedStateViewModelFactory
|
||||||
|
import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.ViewModelStore
|
||||||
|
import androidx.lifecycle.ViewModelStoreOwner
|
||||||
|
import androidx.lifecycle.enableSavedStateHandles
|
||||||
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
|
import androidx.lifecycle.viewmodel.MutableCreationExtras
|
||||||
|
import androidx.savedstate.SavedStateRegistry
|
||||||
|
import androidx.savedstate.SavedStateRegistryController
|
||||||
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
|
||||||
|
fun viewModelSupportNode(buildContext: BuildContext, plugins: List<Plugin> = emptyList(), composable: @Composable (Modifier) -> Unit): Node =
|
||||||
|
ViewModelSupportNode(buildContext, plugins, composable)
|
||||||
|
|
||||||
|
class ViewModelSupportNode(
|
||||||
|
buildContext: BuildContext,
|
||||||
|
plugins: List<Plugin> = emptyList(),
|
||||||
|
private val composable: @Composable (Modifier) -> Unit,
|
||||||
|
) : Node(
|
||||||
|
buildContext, plugins = plugins
|
||||||
|
), ViewModelStoreOwner, SavedStateRegistryOwner {
|
||||||
|
|
||||||
|
private val viewModelSupport = ViewModelSupport(
|
||||||
|
lifecycle,
|
||||||
|
buildContext.savedStateMap?.get("SAVED_STATE_REGISTRY") as Bundle?,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getViewModelStore(): ViewModelStore {
|
||||||
|
return viewModelSupport.viewModelStore
|
||||||
|
}
|
||||||
|
|
||||||
|
override val savedStateRegistry: SavedStateRegistry
|
||||||
|
get() = viewModelSupport.savedStateRegistry
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
composable(modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ViewModelSupport(
|
||||||
|
private val lifecycle: Lifecycle,
|
||||||
|
private val initialSavedState: Bundle?,
|
||||||
|
val defaultArgs: Bundle? = null,
|
||||||
|
) : ViewModelStoreOwner, HasDefaultViewModelProviderFactory, SavedStateRegistryOwner {
|
||||||
|
|
||||||
|
private val viewModelStore = ViewModelStore()
|
||||||
|
private val savedStateRegistryController: SavedStateRegistryController =
|
||||||
|
SavedStateRegistryController.create(this)
|
||||||
|
|
||||||
|
//Don't replace the initial saved state until we have at least started
|
||||||
|
private var canSaveState: Boolean = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
savedStateRegistryController.performAttach()
|
||||||
|
|
||||||
|
// We copy the bundle because the `savedStateRegistryController` will modify it.
|
||||||
|
// We don't want to modify `initialSavedState` since we may need to return that as our
|
||||||
|
// state in `saveState`.
|
||||||
|
savedStateRegistryController.performRestore(initialSavedState?.let { Bundle(it) })
|
||||||
|
enableSavedStateHandles()
|
||||||
|
|
||||||
|
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||||
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
|
canSaveState = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
viewModelStore.clear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getViewModelStore(): ViewModelStore {
|
||||||
|
return viewModelStore
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
|
||||||
|
return SavedStateViewModelFactory(null, this, defaultArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDefaultViewModelCreationExtras(): CreationExtras {
|
||||||
|
val extras = MutableCreationExtras()
|
||||||
|
extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
|
||||||
|
extras[VIEW_MODEL_STORE_OWNER_KEY] = this
|
||||||
|
defaultArgs?.let { args ->
|
||||||
|
extras[DEFAULT_ARGS_KEY] = args
|
||||||
|
}
|
||||||
|
return extras
|
||||||
|
}
|
||||||
|
|
||||||
|
override val savedStateRegistry: SavedStateRegistry
|
||||||
|
get() = savedStateRegistryController.savedStateRegistry
|
||||||
|
|
||||||
|
override fun getLifecycle(): Lifecycle {
|
||||||
|
return lifecycle
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveState(): Bundle? {
|
||||||
|
return if (canSaveState) {
|
||||||
|
Bundle().also(savedStateRegistryController::performSave)
|
||||||
|
} else {
|
||||||
|
initialSavedState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,9 +6,11 @@ import io.element.android.x.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.x.di.AppScope
|
import io.element.android.x.di.AppScope
|
||||||
import io.element.android.x.di.ApplicationContext
|
import io.element.android.x.di.ApplicationContext
|
||||||
import io.element.android.x.di.SingleIn
|
import io.element.android.x.di.SingleIn
|
||||||
|
import io.element.android.x.matrix.core.SessionId
|
||||||
import io.element.android.x.matrix.media.MediaFetcher
|
import io.element.android.x.matrix.media.MediaFetcher
|
||||||
import io.element.android.x.matrix.media.MediaKeyer
|
import io.element.android.x.matrix.media.MediaKeyer
|
||||||
import io.element.android.x.matrix.session.SessionStore
|
import io.element.android.x.matrix.session.SessionStore
|
||||||
|
import io.element.android.x.matrix.session.sessionId
|
||||||
import io.element.android.x.matrix.util.logError
|
import io.element.android.x.matrix.util.logError
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -80,7 +82,7 @@ class Matrix @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun login(username: String, password: String): MatrixClient =
|
suspend fun login(username: String, password: String): SessionId =
|
||||||
withContext(coroutineDispatchers.io) {
|
withContext(coroutineDispatchers.io) {
|
||||||
val client = try {
|
val client = try {
|
||||||
authService.login(username, password, "ElementX Android", null)
|
authService.login(username, password, "ElementX Android", null)
|
||||||
|
|
@ -88,8 +90,9 @@ class Matrix @Inject constructor(
|
||||||
Timber.e(failure, "Fail login")
|
Timber.e(failure, "Fail login")
|
||||||
throw failure
|
throw failure
|
||||||
}
|
}
|
||||||
sessionStore.storeData(client.session())
|
val session = client.session()
|
||||||
createMatrixClient(client)
|
sessionStore.storeData(session)
|
||||||
|
session.sessionId()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createMatrixClient(client: Client): MatrixClient {
|
private fun createMatrixClient(client: Client): MatrixClient {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package io.element.android.x.matrix
|
package io.element.android.x.matrix
|
||||||
|
|
||||||
import io.element.android.x.core.coroutine.CoroutineDispatchers
|
import io.element.android.x.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.x.di.SingleIn
|
import io.element.android.x.matrix.core.SessionId
|
||||||
import io.element.android.x.matrix.core.UserId
|
import io.element.android.x.matrix.core.UserId
|
||||||
import io.element.android.x.matrix.media.MediaResolver
|
import io.element.android.x.matrix.media.MediaResolver
|
||||||
import io.element.android.x.matrix.media.RustMediaResolver
|
import io.element.android.x.matrix.media.RustMediaResolver
|
||||||
|
|
@ -9,10 +9,8 @@ import io.element.android.x.matrix.room.MatrixRoom
|
||||||
import io.element.android.x.matrix.room.RoomSummaryDataSource
|
import io.element.android.x.matrix.room.RoomSummaryDataSource
|
||||||
import io.element.android.x.matrix.room.RustRoomSummaryDataSource
|
import io.element.android.x.matrix.room.RustRoomSummaryDataSource
|
||||||
import io.element.android.x.matrix.session.SessionStore
|
import io.element.android.x.matrix.session.SessionStore
|
||||||
|
import io.element.android.x.matrix.session.sessionId
|
||||||
import io.element.android.x.matrix.sync.SlidingSyncObserverProxy
|
import io.element.android.x.matrix.sync.SlidingSyncObserverProxy
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.Client
|
import org.matrix.rustcomponents.sdk.Client
|
||||||
|
|
@ -23,6 +21,9 @@ import org.matrix.rustcomponents.sdk.SlidingSyncMode
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncViewBuilder
|
import org.matrix.rustcomponents.sdk.SlidingSyncViewBuilder
|
||||||
import org.matrix.rustcomponents.sdk.StoppableSpawn
|
import org.matrix.rustcomponents.sdk.StoppableSpawn
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class MatrixClient internal constructor(
|
class MatrixClient internal constructor(
|
||||||
private val client: Client,
|
private val client: Client,
|
||||||
|
|
@ -32,8 +33,7 @@ class MatrixClient internal constructor(
|
||||||
private val baseDirectory: File,
|
private val baseDirectory: File,
|
||||||
) : Closeable {
|
) : Closeable {
|
||||||
|
|
||||||
val sessionId: String
|
val sessionId: SessionId = client.session().sessionId()
|
||||||
get() = "${client.session().userId}_${client.session().deviceId}"
|
|
||||||
|
|
||||||
private val clientDelegate = object : ClientDelegate {
|
private val clientDelegate = object : ClientDelegate {
|
||||||
override fun didReceiveAuthError(isSoftLogout: Boolean) {
|
override fun didReceiveAuthError(isSoftLogout: Boolean) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package io.element.android.x.matrix.core
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class SessionId(val value: String) : Serializable
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package io.element.android.x.matrix.session
|
||||||
|
|
||||||
|
import io.element.android.x.matrix.core.SessionId
|
||||||
|
import org.matrix.rustcomponents.sdk.Session
|
||||||
|
|
||||||
|
fun Session.sessionId() = SessionId("${userId}_${deviceId}")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue