From 8b8b490bb246d1e86bcce98f78528fabcb7c98ea Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 21 Dec 2022 17:56:01 +0100 Subject: [PATCH] Nav: First iteration integrating Appyx --- app/build.gradle.kts | 2 + .../java/io/element/android/x/MainActivity.kt | 173 +++--------------- .../java/io/element/android/x/Navigation.kt | 90 --------- .../android/x/component/ShowkaseButton.kt | 39 ++++ .../io/element/android/x/di/AppBindings.kt | 4 +- .../android/x/di/SessionComponentsOwner.kt | 7 +- .../x/initializer/MatrixInitializer.kt | 4 +- .../x/initializer/TimberInitializer.kt | 5 +- .../android/x/node/LoggedInFlowNode.kt | 60 ++++++ .../android/x/node/NotLoggedInFlowNode.kt | 51 ++++++ .../io/element/android/x/node/RootFlowNode.kt | 136 ++++++++++++++ features/login/build.gradle.kts | 2 + .../android/x/features/login/LoginScreen.kt | 20 +- .../x/features/login/LoginViewModel.kt | 10 +- .../x/features/login/LoginViewState.kt | 6 +- .../x/features/login/node/LoginFlowNode.kt | 57 ++++++ features/messages/build.gradle.kts | 1 + features/onboarding/build.gradle.kts | 1 + .../onboarding/SplashCarouselStateFactory.kt | 1 + features/roomlist/build.gradle.kts | 1 + gradle/libs.versions.toml | 3 +- libraries/core/build.gradle.kts | 1 + .../io/element/android/x/core/di/Bindings.kt | 18 +- .../android/x/core/di/ViewModelSupport.kt | 121 ++++++++++++ .../io/element/android/x/matrix/Matrix.kt | 9 +- .../element/android/x/matrix/MatrixClient.kt | 12 +- .../android/x/matrix/core/SessionId.kt | 6 + .../android/x/matrix/session/Session.kt | 6 + 28 files changed, 566 insertions(+), 280 deletions(-) delete mode 100644 app/src/main/java/io/element/android/x/Navigation.kt create mode 100644 app/src/main/java/io/element/android/x/component/ShowkaseButton.kt create mode 100644 app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt create mode 100644 app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt create mode 100644 app/src/main/java/io/element/android/x/node/RootFlowNode.kt create mode 100644 features/login/src/main/java/io/element/android/x/features/login/node/LoginFlowNode.kt create mode 100644 libraries/core/src/main/java/io/element/android/x/core/di/ViewModelSupport.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/core/SessionId.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/session/Session.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7b26039ea8..e26c262165 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/io/element/android/x/MainActivity.kt b/app/src/main/java/io/element/android/x/MainActivity.kt index d6af9a459a..0ca348698d 100644 --- a/app/src/main/java/io/element/android/x/MainActivity.kt +++ b/app/src/main/java/io/element/android/x/MainActivity.kt @@ -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() 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) - } } diff --git a/app/src/main/java/io/element/android/x/Navigation.kt b/app/src/main/java/io/element/android/x/Navigation.kt deleted file mode 100644 index 3bf0b95d27..0000000000 --- a/app/src/main/java/io/element/android/x/Navigation.kt +++ /dev/null @@ -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().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().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) -} diff --git a/app/src/main/java/io/element/android/x/component/ShowkaseButton.kt b/app/src/main/java/io/element/android/x/component/ShowkaseButton.kt new file mode 100644 index 0000000000..a6656d6084 --- /dev/null +++ b/app/src/main/java/io/element/android/x/component/ShowkaseButton.kt @@ -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 = "") + } + } + } +} diff --git a/app/src/main/java/io/element/android/x/di/AppBindings.kt b/app/src/main/java/io/element/android/x/di/AppBindings.kt index e3141ccf02..73ae3bf846 100644 --- a/app/src/main/java/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/java/io/element/android/x/di/AppBindings.kt @@ -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 -} \ No newline at end of file +} diff --git a/app/src/main/java/io/element/android/x/di/SessionComponentsOwner.kt b/app/src/main/java/io/element/android/x/di/SessionComponentsOwner.kt index ef95205250..ca6f299750 100644 --- a/app/src/main/java/io/element/android/x/di/SessionComponentsOwner.kt +++ b/app/src/main/java/io/element/android/x/di/SessionComponentsOwner.kt @@ -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() + private val sessionComponents = ConcurrentHashMap() 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 diff --git a/app/src/main/java/io/element/android/x/initializer/MatrixInitializer.kt b/app/src/main/java/io/element/android/x/initializer/MatrixInitializer.kt index 800819f1a7..30bf700297 100644 --- a/app/src/main/java/io/element/android/x/initializer/MatrixInitializer.kt +++ b/app/src/main/java/io/element/android/x/initializer/MatrixInitializer.kt @@ -17,7 +17,7 @@ class MatrixInitializer : Initializer { } } - override fun dependencies(): List>> = emptyList() + override fun dependencies(): List>> = listOf(TimberInitializer::class.java) -} \ No newline at end of file +} diff --git a/app/src/main/java/io/element/android/x/initializer/TimberInitializer.kt b/app/src/main/java/io/element/android/x/initializer/TimberInitializer.kt index a7065e482f..ebb101cc59 100644 --- a/app/src/main/java/io/element/android/x/initializer/TimberInitializer.kt +++ b/app/src/main/java/io/element/android/x/initializer/TimberInitializer.kt @@ -10,6 +10,5 @@ class TimberInitializer : Initializer { Timber.plant(Timber.DebugTree()) } - override fun dependencies(): List>> = - listOf(TimberInitializer::class.java) -} \ No newline at end of file + override fun dependencies(): List>> = emptyList() +} diff --git a/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt new file mode 100644 index 0000000000..2e1b7d8c9c --- /dev/null +++ b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt @@ -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 = BackStack( + initialElement = NavTarget.RoomList, + savedStateMap = buildContext.savedStateMap, + ), +) : ParentNode( + 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) + } +} diff --git a/app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt b/app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt new file mode 100644 index 0000000000..4be72a0696 --- /dev/null +++ b/app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt @@ -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 = BackStack( + initialElement = NavTarget.OnBoarding, + savedStateMap = buildContext.savedStateMap, + ), +) : ParentNode( + 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) + } +} diff --git a/app/src/main/java/io/element/android/x/node/RootFlowNode.kt b/app/src/main/java/io/element/android/x/node/RootFlowNode.kt new file mode 100644 index 0000000000..fa80bc5209 --- /dev/null +++ b/app/src/main/java/io/element/android/x/node/RootFlowNode.kt @@ -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() { + 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 = BackStack( + initialElement = NavTarget.SplashScreen, + savedStateMap = buildContext.savedStateMap, + ), + private val daggerComponentOwner: DaggerComponentOwner, + private val matrix: Matrix, + private val sessionComponentsOwner: SessionComponentsOwner, +) : + ParentNode( + 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() + } + } + } + } +} diff --git a/features/login/build.gradle.kts b/features/login/build.gradle.kts index 5717ba632b..0d392a6533 100644 --- a/features/login/build.gradle.kts +++ b/features/login/build.gradle.kts @@ -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) diff --git a/features/login/src/main/java/io/element/android/x/features/login/LoginScreen.kt b/features/login/src/main/java/io/element/android/x/features/login/LoginScreen.kt index d7fe0de593..19c5821a2e 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/LoginScreen.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/LoginScreen.kt @@ -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) ) diff --git a/features/login/src/main/java/io/element/android/x/features/login/LoginViewModel.kt b/features/login/src/main/java/io/element/android/x/features/login/LoginViewModel.kt index 735289edfc..a31016f83e 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/LoginViewModel.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/LoginViewModel.kt @@ -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) } } } diff --git a/features/login/src/main/java/io/element/android/x/features/login/LoginViewState.kt b/features/login/src/main/java/io/element/android/x/features/login/LoginViewState.kt index e7871dc504..27a15945a6 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/LoginViewState.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/LoginViewState.kt @@ -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 = Uninitialized, + val loggedInSessionId: Async = 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( diff --git a/features/login/src/main/java/io/element/android/x/features/login/node/LoginFlowNode.kt b/features/login/src/main/java/io/element/android/x/features/login/node/LoginFlowNode.kt new file mode 100644 index 0000000000..c33e5e970b --- /dev/null +++ b/features/login/src/main/java/io/element/android/x/features/login/node/LoginFlowNode.kt @@ -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 = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), +) : ParentNode( + 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) + } + +} diff --git a/features/messages/build.gradle.kts b/features/messages/build.gradle.kts index 4735cd6530..c2c4754545 100644 --- a/features/messages/build.gradle.kts +++ b/features/messages/build.gradle.kts @@ -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) diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index a808aa7d8d..bdcc5d4e76 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -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) diff --git a/features/onboarding/src/main/java/io/element/android/x/features/onboarding/SplashCarouselStateFactory.kt b/features/onboarding/src/main/java/io/element/android/x/features/onboarding/SplashCarouselStateFactory.kt index 04f605279d..7ed1951ce2 100644 --- a/features/onboarding/src/main/java/io/element/android/x/features/onboarding/SplashCarouselStateFactory.kt +++ b/features/onboarding/src/main/java/io/element/android/x/features/onboarding/SplashCarouselStateFactory.kt @@ -11,6 +11,7 @@ class SplashCarouselStateFactory { fun hero(@DrawableRes lightDrawable: Int, @DrawableRes darkDrawable: Int) = if (lightTheme) lightDrawable else darkDrawable + return SplashCarouselState( listOf( SplashCarouselState.Item( diff --git a/features/roomlist/build.gradle.kts b/features/roomlist/build.gradle.kts index 6eb99fa7a3..15f7bf740d 100644 --- a/features/roomlist/build.gradle.kts +++ b/features/roomlist/build.gradle.kts @@ -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) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6b62ade122..9ff4d4ed0c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"} diff --git a/libraries/core/build.gradle.kts b/libraries/core/build.gradle.kts index 1afadbd396..e308392a8c 100644 --- a/libraries/core/build.gradle.kts +++ b/libraries/core/build.gradle.kts @@ -10,4 +10,5 @@ dependencies { api(libs.mavericks.compose) api(libs.dagger) api(libs.androidx.fragment) + api(libs.appyx.core) } diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/Bindings.kt b/libraries/core/src/main/java/io/element/android/x/core/di/Bindings.kt index 5d808c8c09..b5f1b5c5a4 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/di/Bindings.kt +++ b/libraries/core/src/main/java/io/element/android/x/core/di/Bindings.kt @@ -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().inject(this). */ + inline fun Context.bindings() = bindings(T::class.java) /** @@ -27,6 +29,8 @@ inline fun Context.bindings() = bindings(T::class.java) */ inline fun Fragment.bindings() = bindings(T::class.java) +inline fun Node.bindings() = bindings(T::class.java) + /** Use no-arg extension function instead: [Context.bindings] */ fun Context.bindings(klass: Class): T { // search dagger components in the context hierarchy @@ -50,4 +54,16 @@ fun Fragment.bindings(klass: Class): T { .filterIsInstance(klass) .firstOrNull() ?: requireActivity().bindings(klass) -} \ No newline at end of file +} + +/** Use no-arg extension function instead: [Node.bindings] */ +fun Node.bindings(klass: Class): T { + // search dagger components in node hierarchy + return generateSequence(this, Node::parent) + .filterIsInstance() + .map { it.daggerComponent } + .flatMap { if (it is Collection<*>) it else listOf(it) } + .filterIsInstance(klass) + .firstOrNull() + ?: error("Unable to find bindings for ${klass.name}") +} diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/ViewModelSupport.kt b/libraries/core/src/main/java/io/element/android/x/core/di/ViewModelSupport.kt new file mode 100644 index 0000000000..27801979c7 --- /dev/null +++ b/libraries/core/src/main/java/io/element/android/x/core/di/ViewModelSupport.kt @@ -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 = emptyList(), composable: @Composable (Modifier) -> Unit): Node = + ViewModelSupportNode(buildContext, plugins, composable) + +class ViewModelSupportNode( + buildContext: BuildContext, + plugins: List = 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 + } + } +} diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt index dd59606a15..5e567f87c4 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt @@ -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 { diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt index db16d9f706..9f78514f77 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt @@ -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) { diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/core/SessionId.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/core/SessionId.kt new file mode 100644 index 0000000000..355999105e --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/core/SessionId.kt @@ -0,0 +1,6 @@ +package io.element.android.x.matrix.core + +import java.io.Serializable + +@JvmInline +value class SessionId(val value: String) : Serializable diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/session/Session.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/session/Session.kt new file mode 100644 index 0000000000..6b50a09660 --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/session/Session.kt @@ -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}")