Nav: First iteration integrating Appyx
This commit is contained in:
@@ -6,6 +6,7 @@ plugins {
|
||||
alias(libs.plugins.kapt)
|
||||
id("com.google.firebase.appdistribution") version "3.0.2"
|
||||
id("org.jetbrains.kotlinx.knit") version "0.4.0"
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -147,6 +148,7 @@ dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.0")
|
||||
implementation(libs.compose.destinations)
|
||||
ksp(libs.compose.destinations.processor)
|
||||
implementation(libs.appyx.core)
|
||||
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
|
||||
@@ -6,177 +6,48 @@
|
||||
package io.element.android.x
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.animation.AnimatedContentScope
|
||||
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.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.Text
|
||||
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.material3.Surface
|
||||
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.navigation.NavHostController
|
||||
import com.airbnb.android.showkase.models.Showkase
|
||||
import com.airbnb.mvrx.compose.mavericksActivityViewModel
|
||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
||||
import com.bumble.appyx.core.integration.NodeHost
|
||||
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
|
||||
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.bindings
|
||||
import io.element.android.x.designsystem.ElementXTheme
|
||||
import io.element.android.x.destinations.OnBoardingScreenNavigationDestination
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import io.element.android.x.di.AppBindings
|
||||
import io.element.android.x.node.RootFlowNode
|
||||
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val appBindings = bindings<AppBindings>()
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
setContent {
|
||||
ElementXTheme {
|
||||
MainScreen(viewModel = mavericksActivityViewModel())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
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 io.element.android.x.matrix.Matrix
|
||||
import io.element.android.x.node.LoggedInFlowNode
|
||||
import io.element.android.x.node.RootFlowNode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
@@ -9,4 +11,4 @@ interface AppBindings {
|
||||
fun coroutineScope(): CoroutineScope
|
||||
fun matrix(): Matrix
|
||||
fun sessionComponentsOwner(): SessionComponentsOwner
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,18 @@ package io.element.android.x.di
|
||||
import android.content.Context
|
||||
import io.element.android.x.core.di.bindings
|
||||
import io.element.android.x.matrix.MatrixClient
|
||||
import io.element.android.x.matrix.core.SessionId
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
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
|
||||
private set
|
||||
|
||||
fun setActive(sessionId: String) {
|
||||
fun setActive(sessionId: SessionId) {
|
||||
val sessionComponent = sessionComponents[sessionId]
|
||||
if (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)
|
||||
if (activeSessionComponent == sessionComponent) {
|
||||
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())
|
||||
}
|
||||
|
||||
override fun dependencies(): List<Class<out Initializer<*>>> =
|
||||
listOf(TimberInitializer::class.java)
|
||||
}
|
||||
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.anvil)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -21,6 +22,7 @@ dependencies {
|
||||
implementation(project(":libraries:matrix"))
|
||||
implementation(project(":libraries:designsystem"))
|
||||
implementation(project(":libraries:elementresources"))
|
||||
implementation(libs.appyx.core)
|
||||
implementation(libs.mavericks.compose)
|
||||
ksp(libs.showkase.processor)
|
||||
testImplementation(libs.test.junit)
|
||||
|
||||
@@ -49,14 +49,14 @@ import com.airbnb.mvrx.compose.collectAsState
|
||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
||||
import io.element.android.x.designsystem.ElementXTheme
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
viewModel: LoginViewModel = mavericksViewModel(),
|
||||
onChangeServer: () -> Unit = { },
|
||||
onLoginWithSuccess: (MatrixClient) -> Unit = { },
|
||||
onLoginWithSuccess: (SessionId) -> Unit = { },
|
||||
) {
|
||||
val state: LoginViewState by viewModel.collectAsState()
|
||||
val formState: LoginFormState by viewModel.formState
|
||||
@@ -85,7 +85,7 @@ fun LoginContent(
|
||||
onLoginChanged: (String) -> Unit = {},
|
||||
onPasswordChanged: (String) -> Unit = {},
|
||||
onSubmitClicked: () -> Unit = {},
|
||||
onLoginWithSuccess: (MatrixClient) -> Unit = {},
|
||||
onLoginWithSuccess: (SessionId) -> Unit = {},
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
@@ -105,7 +105,7 @@ fun LoginContent(
|
||||
)
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
val isError = state.loggedInClient is Fail
|
||||
val isError = state.loggedInSessionId is Fail
|
||||
// Title
|
||||
Text(
|
||||
text = "Welcome back",
|
||||
@@ -160,7 +160,7 @@ fun LoginContent(
|
||||
),
|
||||
)
|
||||
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
|
||||
passwordVisible = false
|
||||
}
|
||||
@@ -193,9 +193,9 @@ fun LoginContent(
|
||||
onDone = { onSubmitClicked() }
|
||||
),
|
||||
)
|
||||
if (state.loggedInClient is Fail) {
|
||||
if (state.loggedInSessionId is Fail) {
|
||||
Text(
|
||||
text = loginError(state.formState, state.loggedInClient.error),
|
||||
text = loginError(state.formState, state.loggedInSessionId.error),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
@@ -212,12 +212,12 @@ fun LoginContent(
|
||||
) {
|
||||
Text(text = "Continue")
|
||||
}
|
||||
when (val loggedInClient = state.loggedInClient) {
|
||||
is Success -> onLoginWithSuccess(loggedInClient())
|
||||
when (val loggedInSessionId = state.loggedInSessionId) {
|
||||
is Success -> onLoginWithSuccess(loggedInSessionId())
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
if (state.loggedInClient is Loading) {
|
||||
if (state.loggedInSessionId is Loading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
|
||||
@@ -48,22 +48,20 @@ class LoginViewModel @AssistedInject constructor(
|
||||
val state = awaitState()
|
||||
// Ensure the server is provided to the Rust SDK
|
||||
matrix.setHomeserver(state.homeserver)
|
||||
matrix.login(state.formState.login.trim(), state.formState.password.trim()).also {
|
||||
it.startSync()
|
||||
}
|
||||
matrix.login(state.formState.login.trim(), state.formState.password.trim())
|
||||
}.execute {
|
||||
copy(loggedInClient = it)
|
||||
copy(loggedInSessionId = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSetPassword(password: String) {
|
||||
formState.value = formState.value.copy(password = password)
|
||||
setState { copy(loggedInClient = Uninitialized) }
|
||||
setState { copy(loggedInSessionId = Uninitialized) }
|
||||
}
|
||||
|
||||
fun onSetName(name: String) {
|
||||
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.MavericksState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import io.element.android.x.matrix.MatrixClient
|
||||
import io.element.android.x.matrix.core.SessionId
|
||||
|
||||
data class LoginViewState(
|
||||
val homeserver: String = "",
|
||||
val loggedInClient: Async<MatrixClient> = Uninitialized,
|
||||
val loggedInSessionId: Async<SessionId> = Uninitialized,
|
||||
val formState: LoginFormState = LoginFormState.Default,
|
||||
) : MavericksState {
|
||||
val submitEnabled =
|
||||
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInClient !is Loading
|
||||
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInSessionId !is Loading
|
||||
}
|
||||
|
||||
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:designsystem"))
|
||||
implementation(project(":libraries:textcomposer"))
|
||||
implementation(libs.appyx.core)
|
||||
implementation(libs.mavericks.compose)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.datetime)
|
||||
|
||||
@@ -14,6 +14,7 @@ dependencies {
|
||||
implementation(libs.mavericks.compose)
|
||||
implementation(libs.accompanist.pager)
|
||||
implementation(libs.accompanist.pagerindicator)
|
||||
implementation(libs.appyx.core)
|
||||
testImplementation(libs.test.junit)
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
@@ -11,6 +11,7 @@ class SplashCarouselStateFactory {
|
||||
|
||||
fun hero(@DrawableRes lightDrawable: Int, @DrawableRes darkDrawable: Int) =
|
||||
if (lightTheme) lightDrawable else darkDrawable
|
||||
|
||||
return SplashCarouselState(
|
||||
listOf(
|
||||
SplashCarouselState.Item(
|
||||
|
||||
@@ -19,6 +19,7 @@ dependencies {
|
||||
implementation(project(":libraries:core"))
|
||||
implementation(project(":libraries:matrix"))
|
||||
implementation(project(":libraries:designsystem"))
|
||||
implementation(libs.appyx.core)
|
||||
implementation(libs.mavericks.compose)
|
||||
implementation(libs.datetime)
|
||||
implementation(libs.accompanist.placeholder)
|
||||
|
||||
@@ -49,6 +49,7 @@ serialization_json = "1.4.1"
|
||||
showkase = "1.0.0-beta14"
|
||||
compose_destinations = "1.7.23-beta"
|
||||
jsoup = "1.15.3"
|
||||
appyx = "1.0.1"
|
||||
|
||||
# DI
|
||||
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_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
|
||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||
|
||||
appyx_core = {module = "com.bumble.appyx:core", version.ref = "appyx"}
|
||||
|
||||
# Di
|
||||
inject = {module = "javax.inject:javax.inject", version = "1"}
|
||||
|
||||
@@ -10,4 +10,5 @@ dependencies {
|
||||
api(libs.mavericks.compose)
|
||||
api(libs.dagger)
|
||||
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.ContextWrapper
|
||||
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:
|
||||
@@ -20,6 +21,7 @@ import androidx.fragment.app.Fragment
|
||||
* 2) Contribute your interface to the correct component via `@ContributesTo(AppScope::class)`.
|
||||
* 3) Call bindings<YourModuleBindings>().inject(this).
|
||||
*/
|
||||
|
||||
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> Node.bindings() = bindings(T::class.java)
|
||||
|
||||
/** Use no-arg extension function instead: [Context.bindings] */
|
||||
fun <T : Any> Context.bindings(klass: Class<T>): T {
|
||||
// search dagger components in the context hierarchy
|
||||
@@ -50,4 +54,16 @@ fun <T : Any> Fragment.bindings(klass: Class<T>): T {
|
||||
.filterIsInstance(klass)
|
||||
.firstOrNull()
|
||||
?: 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.ApplicationContext
|
||||
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.MediaKeyer
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
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) {
|
||||
val client = try {
|
||||
authService.login(username, password, "ElementX Android", null)
|
||||
@@ -88,8 +90,9 @@ class Matrix @Inject constructor(
|
||||
Timber.e(failure, "Fail login")
|
||||
throw failure
|
||||
}
|
||||
sessionStore.storeData(client.session())
|
||||
createMatrixClient(client)
|
||||
val session = client.session()
|
||||
sessionStore.storeData(session)
|
||||
session.sessionId()
|
||||
}
|
||||
|
||||
private fun createMatrixClient(client: Client): MatrixClient {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.element.android.x.matrix
|
||||
|
||||
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.media.MediaResolver
|
||||
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.RustRoomSummaryDataSource
|
||||
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 java.io.Closeable
|
||||
import java.io.File
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
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.StoppableSpawn
|
||||
import timber.log.Timber
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class MatrixClient internal constructor(
|
||||
private val client: Client,
|
||||
@@ -32,8 +33,7 @@ class MatrixClient internal constructor(
|
||||
private val baseDirectory: File,
|
||||
) : Closeable {
|
||||
|
||||
val sessionId: String
|
||||
get() = "${client.session().userId}_${client.session().deviceId}"
|
||||
val sessionId: SessionId = client.session().sessionId()
|
||||
|
||||
private val clientDelegate = object : ClientDelegate {
|
||||
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}")
|
||||
Reference in New Issue
Block a user