From 8b8b490bb246d1e86bcce98f78528fabcb7c98ea Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 21 Dec 2022 17:56:01 +0100 Subject: [PATCH 01/30] 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}") From ebfb3bec51260e4217a0b9a71660a21d5588e13d Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 3 Jan 2023 19:49:47 +0100 Subject: [PATCH 02/30] Let compose handle some config changes --- app/src/main/AndroidManifest.xml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b82495751d..d32f0dca75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,11 +13,10 @@ android:supportsRtl="true" android:theme="@style/Theme.ElementX" tools:targetApi="33"> - @@ -31,10 +30,10 @@ android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup" android:exported="false" - tools:node="remove"/> + tools:node="remove" /> - \ No newline at end of file + From df13ec744538368ae2383e3dd86da2f310e987a7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 3 Jan 2023 19:50:23 +0100 Subject: [PATCH 03/30] Introduce molecule --- build.gradle.kts | 3 +++ features/roomlist/build.gradle.kts | 1 + gradle/libs.versions.toml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index ab8ea28281..b2946b17cc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,12 +5,15 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.anvil) apply false + alias(libs.plugins.molecule) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kapt) apply false alias(libs.plugins.detekt) alias(libs.plugins.ktlint) + } + tasks.register("clean").configure { delete(rootProject.buildDir) } diff --git a/features/roomlist/build.gradle.kts b/features/roomlist/build.gradle.kts index 15f7bf740d..bf81ff34a3 100644 --- a/features/roomlist/build.gradle.kts +++ b/features/roomlist/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.ksp) alias(libs.plugins.anvil) + alias(libs.plugins.molecule) } android { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ff4d4ed0c..b71792afba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ android_gradle_plugin = "7.3.1" firebase_gradle_plugin = "3.0.2" kotlin = "1.7.20" ksp = "1.7.20-1.0.7" +molecule = "0.6.1" # AndroidX material = "1.6.1" @@ -142,3 +143,4 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } anvil = {id = "com.squareup.anvil", version.ref = "anvil"} detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +molecule = {id = "app.cash.molecule", version.ref = "molecule"} From 9153231a65388fb75599d91ee027af4d6f6040b2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 3 Jan 2023 19:51:04 +0100 Subject: [PATCH 04/30] First implementation of using Node/Presenter/UI on RoomList (no DI) --- .../java/io/element/android/x/MainActivity.kt | 2 +- .../android/x/node/LoggedInFlowNode.kt | 25 ++- .../android/x/node/NotLoggedInFlowNode.kt | 9 + .../io/element/android/x/node/RootFlowNode.kt | 25 ++- .../features/roomlist/LastMessageFormatter.kt | 11 +- .../roomlist/NodePresenterConnector.kt | 29 ++++ .../x/features/roomlist/RoomListNode.kt | 43 +++++ .../x/features/roomlist/RoomListPresenter.kt | 148 ++++++++++++++++ .../{RoomListScreen.kt => RoomListView.kt} | 59 +++---- .../x/features/roomlist/RoomListViewModel.kt | 159 ------------------ .../features/roomlist/model/RoomListScreen.kt | 21 +++ .../roomlist/model/RoomListViewState.kt | 16 -- .../android/x/core/architecture/Presenter.kt | 9 + 13 files changed, 326 insertions(+), 230 deletions(-) create mode 100644 features/roomlist/src/main/java/io/element/android/x/features/roomlist/NodePresenterConnector.kt create mode 100644 features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt create mode 100644 features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt rename features/roomlist/src/main/java/io/element/android/x/features/roomlist/{RoomListScreen.kt => RoomListView.kt} (76%) delete mode 100644 features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt create mode 100644 features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListScreen.kt delete mode 100644 features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt create mode 100644 libraries/core/src/main/java/io/element/android/x/core/architecture/Presenter.kt 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 0ca348698d..4fdb426392 100644 --- a/app/src/main/java/io/element/android/x/MainActivity.kt +++ b/app/src/main/java/io/element/android/x/MainActivity.kt @@ -41,7 +41,7 @@ class MainActivity : NodeComponentActivity(), DaggerComponentOwner { NodeHost(integrationPoint = appyxIntegrationPoint) { RootFlowNode( buildContext = it, - daggerComponentOwner = this, + appComponentOwner = this, matrix = appBindings.matrix(), sessionComponentsOwner = appBindings.sessionComponentsOwner() ) 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 index 2e1b7d8c9c..28914b78c0 100644 --- a/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt @@ -4,6 +4,7 @@ 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.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode @@ -12,14 +13,18 @@ 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.features.roomlist.RoomListNode +import io.element.android.x.features.roomlist.RoomListPresenter +import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.core.SessionId import kotlinx.parcelize.Parcelize +import timber.log.Timber class LoggedInFlowNode( buildContext: BuildContext, val sessionId: SessionId, + private val matrixClient: MatrixClient, private val backstack: BackStack = BackStack( initialElement = NavTarget.RoomList, savedStateMap = buildContext.savedStateMap, @@ -29,6 +34,13 @@ class LoggedInFlowNode( buildContext = buildContext ) { + init { + lifecycle.subscribe( + onCreate = { Timber.v("OnCreate") }, + onDestroy = { Timber.v("OnDestroy") } + ) + } + sealed interface NavTarget : Parcelable { @Parcelize object RoomList : NavTarget @@ -39,11 +51,12 @@ class LoggedInFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.RoomList -> viewModelSupportNode(buildContext) { - RoomListScreen( - onRoomClicked = { backstack.push(NavTarget.Messages(it)) } - ) - } + NavTarget.RoomList -> RoomListNode( + buildContext = buildContext, + presenter = RoomListPresenter(matrixClient), + onRoomClicked = { + backstack.push(NavTarget.Messages(it)) + }) is NavTarget.Messages -> viewModelSupportNode(buildContext) { MessagesScreen( roomId = navTarget.roomId.value, 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 index 4be72a0696..e9c848539e 100644 --- a/app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt @@ -4,6 +4,7 @@ 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.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode @@ -13,6 +14,7 @@ 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 +import timber.log.Timber class NotLoggedInFlowNode( buildContext: BuildContext, @@ -25,6 +27,13 @@ class NotLoggedInFlowNode( buildContext = buildContext ) { + init { + lifecycle.subscribe( + onCreate = { Timber.v("OnCreate") }, + onDestroy = { Timber.v("OnDestroy") } + ) + } + sealed interface NavTarget : Parcelable { @Parcelize object OnBoarding : NavTarget 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 index fa80bc5209..643763e5ad 100644 --- a/app/src/main/java/io/element/android/x/node/RootFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/RootFlowNode.kt @@ -25,6 +25,7 @@ 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.newRoot import com.bumble.appyx.navmodel.backstack.operation.replace import io.element.android.x.BuildConfig import io.element.android.x.component.ShowkaseButton @@ -61,7 +62,7 @@ class RootFlowNode( initialElement = NavTarget.SplashScreen, savedStateMap = buildContext.savedStateMap, ), - private val daggerComponentOwner: DaggerComponentOwner, + private val appComponentOwner: DaggerComponentOwner, private val matrix: Matrix, private val sessionComponentsOwner: SessionComponentsOwner, ) : @@ -71,23 +72,32 @@ class RootFlowNode( plugins = listOf(SessionComponentsOwnerInteractor(sessionComponentsOwner)), ), - DaggerComponentOwner by daggerComponentOwner { + DaggerComponentOwner by appComponentOwner { + + init { + Timber.v("Init") + lifecycle.subscribe( + onCreate = { Timber.v("OnCreate") }, + onDestroy = { Timber.v("OnDestroy") } + ) + } init { matrix.isLoggedIn() .distinctUntilChanged() .onEach { isLoggedIn -> + Timber.v("IsLoggedIn") if (isLoggedIn) { val matrixClient = matrix.restoreSession() if (matrixClient == null) { - backstack.replace(NavTarget.NotLoggedInFlow) + backstack.newRoot(NavTarget.NotLoggedInFlow) } else { matrixClient.startSync() sessionComponentsOwner.create(matrixClient) - backstack.replace(NavTarget.LoggedInFlow(matrixClient.sessionId)) + backstack.newRoot(NavTarget.LoggedInFlow(matrixClient.sessionId)) } } else { - backstack.replace(NavTarget.NotLoggedInFlow) + backstack.newRoot(NavTarget.NotLoggedInFlow) } } .launchIn(lifecycleScope) @@ -124,7 +134,10 @@ class RootFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - is NavTarget.LoggedInFlow -> LoggedInFlowNode(buildContext, navTarget.sessionId) + is NavTarget.LoggedInFlow -> { + val matrixClient = sessionComponentsOwner.activeSessionComponent!!.matrixClient() + LoggedInFlowNode(buildContext, navTarget.sessionId, matrixClient) + } NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext) NavTarget.SplashScreen -> node(buildContext) { Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/LastMessageFormatter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/LastMessageFormatter.kt index d06eb20fbc..bc59f0544c 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/LastMessageFormatter.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/LastMessageFormatter.kt @@ -2,10 +2,6 @@ package io.element.android.x.features.roomlist import android.text.format.DateFormat import android.text.format.DateUtils -import java.time.Period -import java.time.format.DateTimeFormatter -import java.util.Locale -import kotlin.math.absoluteValue import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime @@ -14,8 +10,13 @@ import kotlinx.datetime.toInstant import kotlinx.datetime.toJavaLocalDate import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toLocalDateTime +import java.time.Period +import java.time.format.DateTimeFormatter +import java.util.Locale +import javax.inject.Inject +import kotlin.math.absoluteValue -class LastMessageFormatter( +class LastMessageFormatter @Inject constructor( private val clock: Clock = Clock.System, private val locale: Locale = Locale.getDefault() ) { diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/NodePresenterConnector.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/NodePresenterConnector.kt new file mode 100644 index 0000000000..7012714324 --- /dev/null +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/NodePresenterConnector.kt @@ -0,0 +1,29 @@ +package io.element.android.x.features.roomlist + +import androidx.compose.ui.platform.AndroidUiDispatcher +import androidx.lifecycle.lifecycleScope +import app.cash.molecule.RecompositionClock +import app.cash.molecule.launchMolecule +import com.bumble.appyx.core.node.Node +import io.element.android.x.core.architecture.Presenter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.StateFlow + +inline fun Node.presenterConnector(presenter: Presenter): NodePresenterConnector { + return NodePresenterConnector(node = this, presenter = presenter) +} + +class NodePresenterConnector(private val node: Node, presenter: Presenter) { + + private val moleculeScope = CoroutineScope(node.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main) + private val eventFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 64) + + val stateFlow: StateFlow = moleculeScope.launchMolecule(RecompositionClock.ContextClock) { + presenter.present(events = eventFlow) + } + + fun emitEvent(event: Event) { + eventFlow.tryEmit(event) + } +} diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt new file mode 100644 index 0000000000..48b5cfd6eb --- /dev/null +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -0,0 +1,43 @@ +package io.element.android.x.features.roomlist + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.x.features.roomlist.model.RoomListScreen +import io.element.android.x.matrix.core.RoomId + +class RoomListNode( + buildContext: BuildContext, + presenter: RoomListPresenter, + private val onRoomClicked: (RoomId) -> Unit +) : Node(buildContext) { + + private val connector = presenterConnector(presenter) + + private fun updateFilter(filter: String) { + connector.emitEvent(RoomListScreen.Event.UpdateFilter(filter)) + } + + private fun updateVisibleRange(range: IntRange) { + connector.emitEvent((RoomListScreen.Event.UpdateVisibleRange(range))) + } + + private fun logout() { + connector.emitEvent(RoomListScreen.Event.Logout) + } + + @Composable + override fun View(modifier: Modifier) { + val state by connector.stateFlow.collectAsState() + RoomListView( + state = state, + onRoomClicked = onRoomClicked, + onFilterChanged = this::updateFilter, + onScrollOver = this::updateVisibleRange, + onLogoutClicked = this::logout + ) + } +} diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt new file mode 100644 index 0000000000..3fad20ab7f --- /dev/null +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt @@ -0,0 +1,148 @@ +package io.element.android.x.features.roomlist + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.x.core.architecture.Presenter +import io.element.android.x.core.coroutine.parallelMap +import io.element.android.x.designsystem.components.avatar.AvatarData +import io.element.android.x.designsystem.components.avatar.AvatarSize +import io.element.android.x.features.roomlist.model.MatrixUser +import io.element.android.x.features.roomlist.model.RoomListRoomSummary +import io.element.android.x.features.roomlist.model.RoomListRoomSummaryPlaceholders +import io.element.android.x.features.roomlist.model.RoomListScreen +import io.element.android.x.matrix.MatrixClient +import io.element.android.x.matrix.media.MediaResolver +import io.element.android.x.matrix.room.RoomSummary +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +private const val extendedRangeSize = 40 + +class RoomListPresenter @Inject constructor( + private val client: MatrixClient, + private val lastMessageFormatter: LastMessageFormatter = LastMessageFormatter(), +) : Presenter { + + @Composable + override fun present(events: Flow): RoomListScreen.State { + val matrixUser: MutableState = remember { + mutableStateOf(null) + } + var filter by rememberSaveable { mutableStateOf("") } + val isLoginOut = rememberSaveable { mutableStateOf(false) } + val roomSummaries by client + .roomSummaryDataSource() + .roomSummaries() + .collectAsState(initial = null) + + val filteredRoomSummaries: MutableState> = remember { + mutableStateOf(persistentListOf()) + } + LaunchedEffect(Unit) { + initialLoad(matrixUser) + events.collect { event -> + when (event) { + RoomListScreen.Event.Logout -> logout(isLoginOut) + is RoomListScreen.Event.UpdateFilter -> filter = event.newFilter + is RoomListScreen.Event.UpdateVisibleRange -> updateVisibleRange(event.range) + } + } + } + LaunchedEffect(roomSummaries, filter) { + filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter) + } + return RoomListScreen.State( + matrixUser = matrixUser.value, + roomList = filteredRoomSummaries.value, + filter = filter, + isLoginOut = isLoginOut.value + ) + } + + private suspend fun updateFilteredRoomSummaries(roomSummaries: List?, filter: String): ImmutableList { + val mappedRoomSummaries = mapRoomSummaries(roomSummaries.orEmpty()) + return if (filter.isEmpty()) { + mappedRoomSummaries + } else { + mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) } + }.toImmutableList() + } + + private suspend fun initialLoad(matrixUser: MutableState) { + val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() + val userDisplayName = client.loadUserDisplayName().getOrNull() + val avatarData = + loadAvatarData( + userDisplayName ?: client.userId().value, + userAvatarUrl, + AvatarSize.SMALL + ) + matrixUser.value = MatrixUser( + username = userDisplayName ?: client.userId().value, + avatarUrl = userAvatarUrl, + avatarData = avatarData, + ) + } + + private suspend fun logout(isLoginOut: MutableState) { + isLoginOut.value = true + delay(2000) + client.logout() + isLoginOut.value = false + } + + private fun updateVisibleRange(range: IntRange) { + if (range.isEmpty()) return + val midExtendedRangeSize = extendedRangeSize / 2 + val extendedRangeStart = (range.first - midExtendedRangeSize).coerceAtLeast(0) + // Safe to give bigger size than room list + val extendedRangeEnd = range.last + midExtendedRangeSize + val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd) + client.roomSummaryDataSource().setSlidingSyncRange(extendedRange) + } + + private suspend fun mapRoomSummaries( + roomSummaries: List + ): List { + return roomSummaries.parallelMap { roomSummary -> + when (roomSummary) { + is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier) + is RoomSummary.Filled -> { + val avatarData = loadAvatarData( + roomSummary.details.name, + roomSummary.details.avatarURLString + ) + RoomListRoomSummary( + id = roomSummary.identifier(), + name = roomSummary.details.name, + hasUnread = roomSummary.details.unreadNotificationCount > 0, + timestamp = lastMessageFormatter.format(roomSummary.details.lastMessageTimestamp), + lastMessage = roomSummary.details.lastMessage, + avatarData = avatarData, + ) + } + } + } + } + + private suspend fun loadAvatarData( + name: String, + url: String?, + size: AvatarSize = AvatarSize.MEDIUM + ): AvatarData { + val model = client.mediaResolver() + .resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value)) + return AvatarData(name, model, size) + } +} diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt similarity index 76% rename from features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt rename to features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt index dff4871efe..e6a8e0e5fa 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt @@ -5,7 +5,6 @@ package io.element.android.x.features.roomlist import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api @@ -21,10 +20,6 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Velocity -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.compose.collectAsState -import com.airbnb.mvrx.compose.mavericksViewModel import io.element.android.x.core.compose.LogCompositions import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.components.ProgressDialog @@ -33,41 +28,35 @@ import io.element.android.x.features.roomlist.components.RoomListTopBar import io.element.android.x.features.roomlist.components.RoomSummaryRow import io.element.android.x.features.roomlist.model.MatrixUser import io.element.android.x.features.roomlist.model.RoomListRoomSummary -import io.element.android.x.features.roomlist.model.RoomListViewState +import io.element.android.x.features.roomlist.model.RoomListScreen import io.element.android.x.features.roomlist.model.stubbedRoomSummaries import io.element.android.x.matrix.core.RoomId import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList @Composable -fun RoomListScreen( - viewModel: RoomListViewModel = mavericksViewModel(), - onSuccessLogout: () -> Unit = { }, - onRoomClicked: (RoomId) -> Unit = { } +fun RoomListView( + state: RoomListScreen.State, + modifier: Modifier = Modifier, + onRoomClicked: (RoomId) -> Unit = {}, + onFilterChanged: (String) -> Unit = {}, + onLogoutClicked: () -> Unit = {}, + onScrollOver: (IntRange) -> Unit = {}, ) { - val logoutAction by viewModel.collectAsState(RoomListViewState::logoutAction) - val filter by viewModel.collectAsState(RoomListViewState::filter) - if (logoutAction is Success) { - onSuccessLogout() - return - } - LogCompositions(tag = "RoomListScreen", msg = "Root") - val roomSummaries by viewModel.collectAsState(RoomListViewState::rooms) - val matrixUser by viewModel.collectAsState(RoomListViewState::user) - RoomListContent( - roomSummaries = roomSummaries().orEmpty().toImmutableList(), - matrixUser = matrixUser(), + RoomListView( + roomSummaries = state.roomList, + matrixUser = state.matrixUser, + filter = state.filter, + isLoginOut = state.isLoginOut, + modifier = modifier, onRoomClicked = onRoomClicked, - onLogoutClicked = viewModel::logout, - isLoginOut = logoutAction is Loading, - filter = filter, - onFilterChanged = viewModel::filterRoom, - onScrollOver = viewModel::updateVisibleRange + onFilterChanged = onFilterChanged, + onLogoutClicked = onLogoutClicked, + onScrollOver = onScrollOver ) } @Composable -fun RoomListContent( +fun RoomListView( roomSummaries: ImmutableList, matrixUser: MatrixUser?, filter: String, @@ -141,15 +130,11 @@ fun RoomListContent( private fun RoomListRoomSummary.contentType() = isPlaceholder -private fun LazyListState.isScrolled(): Boolean { - return firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0 -} - @Preview @Composable -fun PreviewableRoomListContent() { +fun PreviewableRoomListView() { ElementXTheme(darkTheme = false) { - RoomListContent( + RoomListView( roomSummaries = stubbedRoomSummaries(), matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")), onRoomClicked = {}, @@ -164,9 +149,9 @@ fun PreviewableRoomListContent() { @Preview @Composable -fun PreviewableDarkRoomListContent() { +fun PreviewableDarkRoomListView() { ElementXTheme(darkTheme = true) { - RoomListContent( + RoomListView( roomSummaries = stubbedRoomSummaries(), matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")), onRoomClicked = {}, diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt deleted file mode 100644 index 5137f83761..0000000000 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt +++ /dev/null @@ -1,159 +0,0 @@ -package io.element.android.x.features.roomlist - -import com.airbnb.mvrx.* -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.core.coroutine.parallelMap -import io.element.android.x.core.di.daggerMavericksViewModelFactory -import io.element.android.x.designsystem.components.avatar.AvatarData -import io.element.android.x.designsystem.components.avatar.AvatarSize -import io.element.android.x.di.SessionScope -import io.element.android.x.features.roomlist.model.MatrixUser -import io.element.android.x.features.roomlist.model.RoomListRoomSummary -import io.element.android.x.features.roomlist.model.RoomListRoomSummaryPlaceholders -import io.element.android.x.features.roomlist.model.RoomListViewState -import io.element.android.x.matrix.MatrixClient -import io.element.android.x.matrix.media.MediaResolver -import io.element.android.x.matrix.room.RoomSummary -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch - -private const val extendedRangeSize = 40 - -@ContributesViewModel(SessionScope::class) -class RoomListViewModel @AssistedInject constructor( - private val client: MatrixClient, - @Assisted initialState: RoomListViewState -) : - MavericksViewModel(initialState) { - - companion object : MavericksViewModelFactory by daggerMavericksViewModelFactory() - - private val lastMessageFormatter = LastMessageFormatter() - - init { - handleInit() - } - - fun logout() { - viewModelScope.launch { - suspend { - delay(2000) - client.logout() - }.execute { - copy(logoutAction = it) - } - } - } - - fun filterRoom(filter: String) { - setState { - copy( - filter = filter - ) - } - } - - fun updateVisibleRange(range: IntRange) { - viewModelScope.launch { - if (range.isEmpty()) return@launch - val midExtendedRangeSize = extendedRangeSize / 2 - val extendedRangeStart = (range.first - midExtendedRangeSize).coerceAtLeast(0) - // Safe to give bigger size than room list - val extendedRangeEnd = range.last + midExtendedRangeSize - val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd) - client.roomSummaryDataSource().setSlidingSyncRange(extendedRange) - } - } - - private fun handleInit() { - suspend { - val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() - val userDisplayName = client.loadUserDisplayName().getOrNull() - val avatarData = - loadAvatarData( - userDisplayName ?: client.userId().value, - userAvatarUrl, - AvatarSize.SMALL - ) - MatrixUser( - username = userDisplayName ?: client.userId().value, - avatarUrl = userAvatarUrl, - avatarData = avatarData, - ) - }.execute { - copy(user = it) - } - - // Observe the room list and the filter - combine( - client.roomSummaryDataSource().roomSummaries() - .map(::mapRoomSummaries) - .flowOn(Dispatchers.Default), - stateFlow - .map { it.filter } - .distinctUntilChanged(), - ) { list, filter -> - if (filter.isEmpty()) { - list - } else { - list.filter { it.name.contains(filter, ignoreCase = true) } - } - } - .execute { - copy( - rooms = when { - it is Loading || - // Note: this second case will prevent to handle correctly the empty case - (it is Success && it().isEmpty() && filter.isEmpty()) -> { - // Show fake placeholders to avoid having empty screen - Loading(RoomListRoomSummaryPlaceholders.createFakeList(size = 16)) - } - else -> { - it - } - } - ) - } - } - - private suspend fun mapRoomSummaries( - roomSummaries: List - ): List { - return roomSummaries.parallelMap { roomSummary -> - when (roomSummary) { - is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier) - is RoomSummary.Filled -> { - val avatarData = loadAvatarData( - roomSummary.details.name, - roomSummary.details.avatarURLString - ) - RoomListRoomSummary( - id = roomSummary.identifier(), - name = roomSummary.details.name, - hasUnread = roomSummary.details.unreadNotificationCount > 0, - timestamp = lastMessageFormatter.format(roomSummary.details.lastMessageTimestamp), - lastMessage = roomSummary.details.lastMessage, - avatarData = avatarData, - ) - } - } - } - } - - private suspend fun loadAvatarData( - name: String, - url: String?, - size: AvatarSize = AvatarSize.MEDIUM - ): AvatarData { - val model = client.mediaResolver() - .resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value)) - return AvatarData(name, model, size) - } -} diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListScreen.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListScreen.kt new file mode 100644 index 0000000000..1db974bd7b --- /dev/null +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListScreen.kt @@ -0,0 +1,21 @@ +package io.element.android.x.features.roomlist.model + +import androidx.compose.runtime.Stable +import kotlinx.collections.immutable.ImmutableList + +object RoomListScreen { + + @Stable + data class State( + val matrixUser: MatrixUser?, + val roomList: ImmutableList, + val filter: String, + val isLoginOut: Boolean, + ) + + sealed interface Event { + object Logout : Event + data class UpdateFilter(val newFilter: String) : Event + data class UpdateVisibleRange(val range: IntRange): Event + } +} diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt deleted file mode 100644 index 86414c4a0e..0000000000 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.element.android.x.features.roomlist.model - -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MavericksState -import com.airbnb.mvrx.Uninitialized -import io.element.android.x.matrix.core.RoomId - -data class RoomListViewState( - val user: Async = Uninitialized, - // Will contain the filtered rooms, using ::filter (if filter is not empty) - val rooms: Async> = Uninitialized, - val filter: String = "", - val canLoadMore: Boolean = false, - val logoutAction: Async = Uninitialized, - val roomsById: Map = emptyMap() -) : MavericksState diff --git a/libraries/core/src/main/java/io/element/android/x/core/architecture/Presenter.kt b/libraries/core/src/main/java/io/element/android/x/core/architecture/Presenter.kt new file mode 100644 index 0000000000..a48fa24b7c --- /dev/null +++ b/libraries/core/src/main/java/io/element/android/x/core/architecture/Presenter.kt @@ -0,0 +1,9 @@ +package io.element.android.x.core.architecture + +import androidx.compose.runtime.Composable +import kotlinx.coroutines.flow.Flow + +interface Presenter { + @Composable + fun present(events: Flow): State +} From 9868654c7889b707ba7dad5799c0f99540f6c1c5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 3 Jan 2023 19:57:43 +0100 Subject: [PATCH 05/30] RoomList: re-add placeholders --- .../element/android/x/features/roomlist/RoomListPresenter.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt index 3fad20ab7f..e4f52dce3f 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt @@ -71,7 +71,10 @@ class RoomListPresenter @Inject constructor( } private suspend fun updateFilteredRoomSummaries(roomSummaries: List?, filter: String): ImmutableList { - val mappedRoomSummaries = mapRoomSummaries(roomSummaries.orEmpty()) + if (roomSummaries.isNullOrEmpty()) { + return RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList() + } + val mappedRoomSummaries = mapRoomSummaries(roomSummaries) return if (filter.isEmpty()) { mappedRoomSummaries } else { From a53aae132d91bb04aadd081d25d94dc306dbea5e Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Jan 2023 11:36:32 +0100 Subject: [PATCH 06/30] Roomlist: Extract State and Events --- .../x/features/roomlist/RoomListNode.kt | 8 +++---- .../x/features/roomlist/RoomListPresenter.kt | 15 ++++++------- .../x/features/roomlist/RoomListView.kt | 4 ++-- .../features/roomlist/model/RoomListEvents.kt | 7 +++++++ .../features/roomlist/model/RoomListScreen.kt | 21 ------------------- .../features/roomlist/model/RoomListState.kt | 12 +++++++++++ 6 files changed, 33 insertions(+), 34 deletions(-) create mode 100644 features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt delete mode 100644 features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListScreen.kt create mode 100644 features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt index 48b5cfd6eb..d91b962476 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import io.element.android.x.features.roomlist.model.RoomListScreen +import io.element.android.x.features.roomlist.model.RoomListEvents import io.element.android.x.matrix.core.RoomId class RoomListNode( @@ -18,15 +18,15 @@ class RoomListNode( private val connector = presenterConnector(presenter) private fun updateFilter(filter: String) { - connector.emitEvent(RoomListScreen.Event.UpdateFilter(filter)) + connector.emitEvent(RoomListEvents.UpdateFilter(filter)) } private fun updateVisibleRange(range: IntRange) { - connector.emitEvent((RoomListScreen.Event.UpdateVisibleRange(range))) + connector.emitEvent((RoomListEvents.UpdateVisibleRange(range))) } private fun logout() { - connector.emitEvent(RoomListScreen.Event.Logout) + connector.emitEvent(RoomListEvents.Logout) } @Composable diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt index e4f52dce3f..a13f6fcd4b 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt @@ -14,9 +14,10 @@ import io.element.android.x.core.coroutine.parallelMap import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarSize import io.element.android.x.features.roomlist.model.MatrixUser +import io.element.android.x.features.roomlist.model.RoomListEvents import io.element.android.x.features.roomlist.model.RoomListRoomSummary import io.element.android.x.features.roomlist.model.RoomListRoomSummaryPlaceholders -import io.element.android.x.features.roomlist.model.RoomListScreen +import io.element.android.x.features.roomlist.model.RoomListState import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.room.RoomSummary @@ -32,10 +33,10 @@ private const val extendedRangeSize = 40 class RoomListPresenter @Inject constructor( private val client: MatrixClient, private val lastMessageFormatter: LastMessageFormatter = LastMessageFormatter(), -) : Presenter { +) : Presenter { @Composable - override fun present(events: Flow): RoomListScreen.State { + override fun present(events: Flow): RoomListState { val matrixUser: MutableState = remember { mutableStateOf(null) } @@ -53,16 +54,16 @@ class RoomListPresenter @Inject constructor( initialLoad(matrixUser) events.collect { event -> when (event) { - RoomListScreen.Event.Logout -> logout(isLoginOut) - is RoomListScreen.Event.UpdateFilter -> filter = event.newFilter - is RoomListScreen.Event.UpdateVisibleRange -> updateVisibleRange(event.range) + RoomListEvents.Logout -> logout(isLoginOut) + is RoomListEvents.UpdateFilter -> filter = event.newFilter + is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range) } } } LaunchedEffect(roomSummaries, filter) { filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter) } - return RoomListScreen.State( + return RoomListState( matrixUser = matrixUser.value, roomList = filteredRoomSummaries.value, filter = filter, diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt index e6a8e0e5fa..8b860e0d66 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt @@ -28,14 +28,14 @@ import io.element.android.x.features.roomlist.components.RoomListTopBar import io.element.android.x.features.roomlist.components.RoomSummaryRow import io.element.android.x.features.roomlist.model.MatrixUser import io.element.android.x.features.roomlist.model.RoomListRoomSummary -import io.element.android.x.features.roomlist.model.RoomListScreen +import io.element.android.x.features.roomlist.model.RoomListState import io.element.android.x.features.roomlist.model.stubbedRoomSummaries import io.element.android.x.matrix.core.RoomId import kotlinx.collections.immutable.ImmutableList @Composable fun RoomListView( - state: RoomListScreen.State, + state: RoomListState, modifier: Modifier = Modifier, onRoomClicked: (RoomId) -> Unit = {}, onFilterChanged: (String) -> Unit = {}, diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt new file mode 100644 index 0000000000..ff8b80decf --- /dev/null +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt @@ -0,0 +1,7 @@ +package io.element.android.x.features.roomlist.model + +sealed interface RoomListEvents { + object Logout : RoomListEvents + data class UpdateFilter(val newFilter: String) : RoomListEvents + data class UpdateVisibleRange(val range: IntRange): RoomListEvents +} diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListScreen.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListScreen.kt deleted file mode 100644 index 1db974bd7b..0000000000 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListScreen.kt +++ /dev/null @@ -1,21 +0,0 @@ -package io.element.android.x.features.roomlist.model - -import androidx.compose.runtime.Stable -import kotlinx.collections.immutable.ImmutableList - -object RoomListScreen { - - @Stable - data class State( - val matrixUser: MatrixUser?, - val roomList: ImmutableList, - val filter: String, - val isLoginOut: Boolean, - ) - - sealed interface Event { - object Logout : Event - data class UpdateFilter(val newFilter: String) : Event - data class UpdateVisibleRange(val range: IntRange): Event - } -} diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt new file mode 100644 index 0000000000..a71b2535c1 --- /dev/null +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt @@ -0,0 +1,12 @@ +package io.element.android.x.features.roomlist.model + +import androidx.compose.runtime.Stable +import kotlinx.collections.immutable.ImmutableList + +@Stable +data class RoomListState( + val matrixUser: MatrixUser?, + val roomList: ImmutableList, + val filter: String, + val isLoginOut: Boolean, +) From 1893e6866edc3ccba1b6b862633fb4aec9083b76 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Jan 2023 12:11:12 +0100 Subject: [PATCH 07/30] Create Presentation module and remove Presenter from core --- features/roomlist/build.gradle.kts | 3 +-- .../android/x/features/roomlist/RoomListNode.kt | 1 + .../x/features/roomlist/RoomListPresenter.kt | 2 +- libraries/presentation/.gitignore | 1 + libraries/presentation/build.gradle.kts | 14 ++++++++++++++ .../x/presentation}/NodePresenterConnector.kt | 5 ++--- .../element/android/x/presentation}/Presenter.kt | 2 +- settings.gradle.kts | 1 + 8 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 libraries/presentation/.gitignore create mode 100644 libraries/presentation/build.gradle.kts rename {features/roomlist/src/main/java/io/element/android/x/features/roomlist => libraries/presentation/src/main/java/io/element/android/x/presentation}/NodePresenterConnector.kt (86%) rename libraries/{core/src/main/java/io/element/android/x/core/architecture => presentation/src/main/java/io/element/android/x/presentation}/Presenter.kt (79%) diff --git a/features/roomlist/build.gradle.kts b/features/roomlist/build.gradle.kts index bf81ff34a3..62de2c6549 100644 --- a/features/roomlist/build.gradle.kts +++ b/features/roomlist/build.gradle.kts @@ -2,7 +2,6 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.ksp) alias(libs.plugins.anvil) - alias(libs.plugins.molecule) } android { @@ -18,10 +17,10 @@ dependencies { anvil(project(":anvilcodegen")) implementation(project(":libraries:di")) implementation(project(":libraries:core")) + implementation(project(":libraries:presentation")) implementation(project(":libraries:matrix")) implementation(project(":libraries:designsystem")) implementation(libs.appyx.core) - implementation(libs.mavericks.compose) implementation(libs.datetime) implementation(libs.accompanist.placeholder) testImplementation(libs.test.junit) diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt index d91b962476..cf3ab985c5 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -8,6 +8,7 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import io.element.android.x.features.roomlist.model.RoomListEvents import io.element.android.x.matrix.core.RoomId +import io.element.android.x.presentation.presenterConnector class RoomListNode( buildContext: BuildContext, diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt index a13f6fcd4b..1906dced95 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import io.element.android.x.core.architecture.Presenter import io.element.android.x.core.coroutine.parallelMap import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarSize @@ -21,6 +20,7 @@ import io.element.android.x.features.roomlist.model.RoomListState import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.room.RoomSummary +import io.element.android.x.presentation.Presenter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/libraries/presentation/.gitignore b/libraries/presentation/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/libraries/presentation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libraries/presentation/build.gradle.kts b/libraries/presentation/build.gradle.kts new file mode 100644 index 0000000000..829df0138f --- /dev/null +++ b/libraries/presentation/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("io.element.android-library") + alias(libs.plugins.molecule) +} + +android { + namespace = "io.element.android.x.libraries.presentation" +} + +dependencies { + api(libs.dagger) + api(libs.appyx.core) + api(libs.androidx.lifecycle.runtime) +} diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/NodePresenterConnector.kt b/libraries/presentation/src/main/java/io/element/android/x/presentation/NodePresenterConnector.kt similarity index 86% rename from features/roomlist/src/main/java/io/element/android/x/features/roomlist/NodePresenterConnector.kt rename to libraries/presentation/src/main/java/io/element/android/x/presentation/NodePresenterConnector.kt index 7012714324..1976e3a84b 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/NodePresenterConnector.kt +++ b/libraries/presentation/src/main/java/io/element/android/x/presentation/NodePresenterConnector.kt @@ -1,11 +1,10 @@ -package io.element.android.x.features.roomlist +package io.element.android.x.presentation -import androidx.compose.ui.platform.AndroidUiDispatcher import androidx.lifecycle.lifecycleScope +import app.cash.molecule.AndroidUiDispatcher import app.cash.molecule.RecompositionClock import app.cash.molecule.launchMolecule import com.bumble.appyx.core.node.Node -import io.element.android.x.core.architecture.Presenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.StateFlow diff --git a/libraries/core/src/main/java/io/element/android/x/core/architecture/Presenter.kt b/libraries/presentation/src/main/java/io/element/android/x/presentation/Presenter.kt similarity index 79% rename from libraries/core/src/main/java/io/element/android/x/core/architecture/Presenter.kt rename to libraries/presentation/src/main/java/io/element/android/x/presentation/Presenter.kt index a48fa24b7c..682032ec28 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/architecture/Presenter.kt +++ b/libraries/presentation/src/main/java/io/element/android/x/presentation/Presenter.kt @@ -1,4 +1,4 @@ -package io.element.android.x.core.architecture +package io.element.android.x.presentation import androidx.compose.runtime.Composable import kotlinx.coroutines.flow.Flow diff --git a/settings.gradle.kts b/settings.gradle.kts index a135a4269a..c9bad7e2ed 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,3 +31,4 @@ include(":libraries:designsystem") include(":libraries:di") include(":anvilannotations") include(":anvilcodegen") +include(":libraries:presentation") From 2c19f97e15763e4a94852d503d169a4816ebbac6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Jan 2023 20:19:01 +0100 Subject: [PATCH 08/30] RoomList: setup dagger for node (remove fragment bindings) --- .../java/io/element/android/x/MainActivity.kt | 7 ++--- .../io/element/android/x/di/AppComponent.kt | 4 +-- .../element/android/x/di/SessionComponent.kt | 3 +- .../android/x/node/LoggedInFlowNode.kt | 24 +++++---------- .../io/element/android/x/node/RootFlowNode.kt | 3 +- .../features/roomlist/LastMessageFormatter.kt | 6 ++-- .../x/features/roomlist/RoomListModule.kt | 19 ++++++++++++ .../x/features/roomlist/RoomListNode.kt | 29 +++++++++++++++---- .../x/features/roomlist/RoomListPresenter.kt | 2 +- libraries/core/build.gradle.kts | 1 - .../android/x/core/di/AssistedNodeFactory.kt | 9 ++++++ .../io/element/android/x/core/di/Bindings.kt | 21 ++------------ .../di/DaggerMavericksViewModelFactory.kt | 7 ++--- .../android/x/core/di/NodeFactories.kt | 21 ++++++++++++++ .../io/element/android/x/core/di/NodeKey.kt | 26 +++++++++++++++++ 15 files changed, 122 insertions(+), 60 deletions(-) create mode 100644 features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt create mode 100644 libraries/core/src/main/java/io/element/android/x/core/di/AssistedNodeFactory.kt create mode 100644 libraries/core/src/main/java/io/element/android/x/core/di/NodeFactories.kt create mode 100644 libraries/core/src/main/java/io/element/android/x/core/di/NodeKey.kt 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 4fdb426392..5bac04cab9 100644 --- a/app/src/main/java/io/element/android/x/MainActivity.kt +++ b/app/src/main/java/io/element/android/x/MainActivity.kt @@ -22,10 +22,7 @@ import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.di.AppBindings import io.element.android.x.node.RootFlowNode -class MainActivity : NodeComponentActivity(), DaggerComponentOwner { - - override val daggerComponent: Any - get() = listOfNotNull((applicationContext as? DaggerComponentOwner)?.daggerComponent) +class MainActivity : NodeComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -41,7 +38,7 @@ class MainActivity : NodeComponentActivity(), DaggerComponentOwner { NodeHost(integrationPoint = appyxIntegrationPoint) { RootFlowNode( buildContext = it, - appComponentOwner = this, + appComponentOwner = applicationContext as DaggerComponentOwner, matrix = appBindings.matrix(), sessionComponentsOwner = appBindings.sessionComponentsOwner() ) diff --git a/app/src/main/java/io/element/android/x/di/AppComponent.kt b/app/src/main/java/io/element/android/x/di/AppComponent.kt index 2c170f202b..e68a58eac3 100644 --- a/app/src/main/java/io/element/android/x/di/AppComponent.kt +++ b/app/src/main/java/io/element/android/x/di/AppComponent.kt @@ -8,10 +8,10 @@ import io.element.android.x.core.di.DaggerMavericksBindings @SingleIn(AppScope::class) @MergeComponent(AppScope::class) -interface AppComponent: DaggerMavericksBindings { +interface AppComponent : DaggerMavericksBindings { @Component.Factory interface Factory { fun create(@ApplicationContext @BindsInstance context: Context): AppComponent } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/element/android/x/di/SessionComponent.kt b/app/src/main/java/io/element/android/x/di/SessionComponent.kt index 5acc18d384..e24dd193dc 100644 --- a/app/src/main/java/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/java/io/element/android/x/di/SessionComponent.kt @@ -5,11 +5,12 @@ import com.squareup.anvil.annotations.MergeSubcomponent import dagger.BindsInstance import dagger.Subcomponent import io.element.android.x.core.di.DaggerMavericksBindings +import io.element.android.x.core.di.NodeFactoriesBindings import io.element.android.x.matrix.MatrixClient @SingleIn(SessionScope::class) @MergeSubcomponent(SessionScope::class) -interface SessionComponent: DaggerMavericksBindings { +interface SessionComponent: DaggerMavericksBindings, NodeFactoriesBindings { fun matrixClient(): MatrixClient 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 index 28914b78c0..bf1047ae52 100644 --- a/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt @@ -4,27 +4,23 @@ 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.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.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.createNode import io.element.android.x.core.di.viewModelSupportNode import io.element.android.x.features.messages.MessagesScreen import io.element.android.x.features.roomlist.RoomListNode -import io.element.android.x.features.roomlist.RoomListPresenter -import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.core.SessionId import kotlinx.parcelize.Parcelize -import timber.log.Timber class LoggedInFlowNode( buildContext: BuildContext, val sessionId: SessionId, - private val matrixClient: MatrixClient, private val backstack: BackStack = BackStack( initialElement = NavTarget.RoomList, savedStateMap = buildContext.savedStateMap, @@ -34,11 +30,10 @@ class LoggedInFlowNode( buildContext = buildContext ) { - init { - lifecycle.subscribe( - onCreate = { Timber.v("OnCreate") }, - onDestroy = { Timber.v("OnDestroy") } - ) + private val roomListCallback = object : RoomListNode.Callback { + override fun onRoomClicked(roomId: RoomId) { + backstack.push(NavTarget.Messages(roomId)) + } } sealed interface NavTarget : Parcelable { @@ -51,12 +46,9 @@ class LoggedInFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.RoomList -> RoomListNode( - buildContext = buildContext, - presenter = RoomListPresenter(matrixClient), - onRoomClicked = { - backstack.push(NavTarget.Messages(it)) - }) + NavTarget.RoomList -> { + createNode(buildContext, plugins = listOf(roomListCallback)) + } is NavTarget.Messages -> viewModelSupportNode(buildContext) { MessagesScreen( roomId = navTarget.roomId.value, 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 index 643763e5ad..a19e0f5a53 100644 --- a/app/src/main/java/io/element/android/x/node/RootFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/RootFlowNode.kt @@ -135,8 +135,7 @@ class RootFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.LoggedInFlow -> { - val matrixClient = sessionComponentsOwner.activeSessionComponent!!.matrixClient() - LoggedInFlowNode(buildContext, navTarget.sessionId, matrixClient) + LoggedInFlowNode(buildContext, navTarget.sessionId) } NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext) NavTarget.SplashScreen -> node(buildContext) { diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/LastMessageFormatter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/LastMessageFormatter.kt index bc59f0544c..62b08b6fcf 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/LastMessageFormatter.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/LastMessageFormatter.kt @@ -16,10 +16,10 @@ import java.util.Locale import javax.inject.Inject import kotlin.math.absoluteValue -class LastMessageFormatter @Inject constructor( - private val clock: Clock = Clock.System, +class LastMessageFormatter @Inject constructor() { + + private val clock: Clock = Clock.System private val locale: Locale = Locale.getDefault() -) { private val onlyTimeFormatter: DateTimeFormatter by lazy { val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt new file mode 100644 index 0000000000..d4fc2d25bb --- /dev/null +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt @@ -0,0 +1,19 @@ +package io.element.android.x.features.roomlist + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import io.element.android.x.core.di.AssistedNodeFactory +import io.element.android.x.core.di.NodeKey +import io.element.android.x.di.SessionScope + +@Module +@ContributesTo(SessionScope::class) +abstract class RoomListModule { + + @Binds + @IntoMap + @NodeKey(RoomListNode::class) + abstract fun bindRoomListNodeFactory(factory: RoomListNode.Factory): AssistedNodeFactory<*> +} diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt index cf3ab985c5..1e4f0a0c27 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -6,15 +6,30 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.x.core.di.AssistedNodeFactory import io.element.android.x.features.roomlist.model.RoomListEvents import io.element.android.x.matrix.core.RoomId import io.element.android.x.presentation.presenterConnector -class RoomListNode( - buildContext: BuildContext, +class RoomListNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, presenter: RoomListPresenter, - private val onRoomClicked: (RoomId) -> Unit -) : Node(buildContext) { +) : Node(buildContext, plugins = plugins) { + + @AssistedFactory + interface Factory : AssistedNodeFactory { + override fun create(buildContext: BuildContext, plugins: List): RoomListNode + } + + interface Callback : Plugin { + fun onRoomClicked(roomId: RoomId) + } private val connector = presenterConnector(presenter) @@ -30,12 +45,16 @@ class RoomListNode( connector.emitEvent(RoomListEvents.Logout) } + private fun onRoomClicked(roomId: RoomId) { + plugins().forEach { it.onRoomClicked(roomId) } + } + @Composable override fun View(modifier: Modifier) { val state by connector.stateFlow.collectAsState() RoomListView( state = state, - onRoomClicked = onRoomClicked, + onRoomClicked = this::onRoomClicked, onFilterChanged = this::updateFilter, onScrollOver = this::updateVisibleRange, onLogoutClicked = this::logout diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt index 1906dced95..3760f2f02c 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt @@ -32,7 +32,7 @@ private const val extendedRangeSize = 40 class RoomListPresenter @Inject constructor( private val client: MatrixClient, - private val lastMessageFormatter: LastMessageFormatter = LastMessageFormatter(), + private val lastMessageFormatter: LastMessageFormatter, ) : Presenter { @Composable diff --git a/libraries/core/build.gradle.kts b/libraries/core/build.gradle.kts index e308392a8c..29f4ec2274 100644 --- a/libraries/core/build.gradle.kts +++ b/libraries/core/build.gradle.kts @@ -9,6 +9,5 @@ android { 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/AssistedNodeFactory.kt b/libraries/core/src/main/java/io/element/android/x/core/di/AssistedNodeFactory.kt new file mode 100644 index 0000000000..022fe03720 --- /dev/null +++ b/libraries/core/src/main/java/io/element/android/x/core/di/AssistedNodeFactory.kt @@ -0,0 +1,9 @@ +package io.element.android.x.core.di + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin + +interface AssistedNodeFactory { + fun create(buildContext: BuildContext, plugins: List): NODE +} 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 b5f1b5c5a4..9fe69bc112 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 @@ -7,12 +7,12 @@ 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: - * * an inject function: `inject(MyFragment frag)` + * * an inject function: `inject(node: MyNode)` * * an explicit getter: `fun myClass(): MyClass` * * Anvil will make your Dagger component implement these bindings so that you can call any of these functions on an instance of your component. * - * [bindings] will walk up the Fragment/Activity hierarchy and check for [DaggerComponentOwner] to see if any of its components implement the + * [bindings] will walk up the Node/Activity hierarchy and check for [DaggerComponentOwner] to see if any of its components implement the * specified bindings. Most of the time this will "just work" and you don't have to think about it. * * For example, if your class has @Inject properties: @@ -24,11 +24,6 @@ import com.bumble.appyx.core.node.Node inline fun Context.bindings() = bindings(T::class.java) -/** - * @see bindings - */ -inline fun Fragment.bindings() = bindings(T::class.java) - inline fun Node.bindings() = bindings(T::class.java) /** Use no-arg extension function instead: [Context.bindings] */ @@ -44,18 +39,6 @@ fun Context.bindings(klass: Class): T { ?: error("Unable to find bindings for ${klass.name}") } -/** Use no-arg extension function instead: [Fragment.bindings] */ -fun Fragment.bindings(klass: Class): T { - // search dagger components in fragment hierarchy, then fallback to activity and application - return generateSequence(this, Fragment::getParentFragment) - .filterIsInstance() - .map { it.daggerComponent } - .flatMap { if (it is Collection<*>) it else listOf(it) } - .filterIsInstance(klass) - .firstOrNull() - ?: requireActivity().bindings(klass) -} - /** Use no-arg extension function instead: [Node.bindings] */ fun Node.bindings(klass: Class): T { // search dagger components in node hierarchy diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/DaggerMavericksViewModelFactory.kt b/libraries/core/src/main/java/io/element/android/x/core/di/DaggerMavericksViewModelFactory.kt index 48793e9823..a67c9b204d 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/di/DaggerMavericksViewModelFactory.kt +++ b/libraries/core/src/main/java/io/element/android/x/core/di/DaggerMavericksViewModelFactory.kt @@ -41,10 +41,7 @@ class DaggerMavericksViewModelFactory, S : MavericksS ) : MavericksViewModelFactory { override fun create(viewModelContext: ViewModelContext, state: S): VM { - val bindings: DaggerMavericksBindings = when (viewModelContext) { - is FragmentViewModelContext -> viewModelContext.fragment.bindings() - else -> viewModelContext.activity.bindings() - } + val bindings: DaggerMavericksBindings = viewModelContext.activity.bindings() val viewModelFactoryMap = bindings.viewModelFactories() val viewModelFactory = viewModelFactoryMap[viewModelClass] ?: error("Cannot find ViewModelFactory for ${viewModelClass.name}.") @@ -57,4 +54,4 @@ class DaggerMavericksViewModelFactory, S : MavericksS interface DaggerMavericksBindings { fun viewModelFactories(): Map>, AssistedViewModelFactory<*, *>> -} \ No newline at end of file +} diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/NodeFactories.kt b/libraries/core/src/main/java/io/element/android/x/core/di/NodeFactories.kt new file mode 100644 index 0000000000..94605d4a6a --- /dev/null +++ b/libraries/core/src/main/java/io/element/android/x/core/di/NodeFactories.kt @@ -0,0 +1,21 @@ +package io.element.android.x.core.di + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin + +inline fun Node.createNode(context: BuildContext, plugins: List = emptyList()): NODE { + val nodeClass = NODE::class.java + val bindings: NodeFactoriesBindings = bindings() + val nodeFactoryMap = bindings.nodeFactories() + val nodeFactory = nodeFactoryMap[nodeClass] ?: error("Cannot find NodeFactory for ${nodeClass.name}.") + + @Suppress("UNCHECKED_CAST") + val castedNodeFactory = nodeFactory as? AssistedNodeFactory + val node = castedNodeFactory?.create(context, plugins) + return node as NODE +} + +interface NodeFactoriesBindings { + fun nodeFactories(): Map, AssistedNodeFactory<*>> +} diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/NodeKey.kt b/libraries/core/src/main/java/io/element/android/x/core/di/NodeKey.kt new file mode 100644 index 0000000000..b6541a4688 --- /dev/null +++ b/libraries/core/src/main/java/io/element/android/x/core/di/NodeKey.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.core.di + +import com.bumble.appyx.core.node.Node +import dagger.MapKey +import kotlin.reflect.KClass + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@MapKey +annotation class NodeKey(val value: KClass) From 7fc69a70da8c422155bd981b89a816028e7c9768 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Jan 2023 21:09:59 +0100 Subject: [PATCH 09/30] Move some code to 'architecture' module --- .../ContributesViewModelCodeGenerator.kt | 6 ++--- app/build.gradle.kts | 1 + .../element/android/x/ElementXApplication.kt | 2 +- .../java/io/element/android/x/MainActivity.kt | 2 +- .../io/element/android/x/MainViewModel.kt | 2 +- .../io/element/android/x/di/AppComponent.kt | 2 +- .../element/android/x/di/SessionComponent.kt | 4 ++-- .../android/x/di/SessionComponentsOwner.kt | 2 +- .../android/x/initializer/CoilInitializer.kt | 4 ++-- .../android/x/node/LoggedInFlowNode.kt | 4 ++-- .../android/x/node/NotLoggedInFlowNode.kt | 2 +- features/login/build.gradle.kts | 1 + .../x/features/login/LoginViewModel.kt | 2 +- .../changeserver/ChangeServerViewModel.kt | 2 +- .../x/features/login/node/LoginFlowNode.kt | 2 +- features/messages/build.gradle.kts | 1 + .../x/features/messages/MessagesViewModel.kt | 4 +--- .../textcomposer/MessageComposerViewModel.kt | 2 +- features/onboarding/build.gradle.kts | 1 + features/roomlist/build.gradle.kts | 2 +- .../x/features/roomlist/RoomListModule.kt | 4 ++-- .../x/features/roomlist/RoomListNode.kt | 4 ++-- .../x/features/roomlist/RoomListPresenter.kt | 2 +- .../{presentation => architecture}/.gitignore | 0 .../build.gradle.kts | 4 +++- .../x/architecture}/AssistedNodeFactory.kt | 2 +- .../android/x/architecture}/Bindings.kt | 24 +++---------------- .../android/x/architecture}/NodeFactories.kt | 2 +- .../android/x/architecture}/NodeKey.kt | 2 +- .../x/architecture}/NodePresenterConnector.kt | 2 +- .../android/x/architecture}/Presenter.kt | 2 +- .../viewmodel}/AssistedViewModelFactory.kt | 2 +- .../DaggerMavericksViewModelFactory.kt | 4 ++-- .../x/architecture/viewmodel}/ViewModelKey.kt | 2 +- .../viewmodel}/ViewModelSupport.kt | 2 +- libraries/core/build.gradle.kts | 6 ----- libraries/matrix/build.gradle.kts | 1 + settings.gradle.kts | 2 +- 38 files changed, 48 insertions(+), 67 deletions(-) rename libraries/{presentation => architecture}/.gitignore (100%) rename libraries/{presentation => architecture}/build.gradle.kts (66%) rename libraries/{core/src/main/java/io/element/android/x/core/di => architecture/src/main/java/io/element/android/x/architecture}/AssistedNodeFactory.kt (85%) rename libraries/{core/src/main/java/io/element/android/x/core/di => architecture/src/main/java/io/element/android/x/architecture}/Bindings.kt (55%) rename libraries/{core/src/main/java/io/element/android/x/core/di => architecture/src/main/java/io/element/android/x/architecture}/NodeFactories.kt (94%) rename libraries/{core/src/main/java/io/element/android/x/core/di => architecture/src/main/java/io/element/android/x/architecture}/NodeKey.kt (95%) rename libraries/{presentation/src/main/java/io/element/android/x/presentation => architecture/src/main/java/io/element/android/x/architecture}/NodePresenterConnector.kt (96%) rename libraries/{presentation/src/main/java/io/element/android/x/presentation => architecture/src/main/java/io/element/android/x/architecture}/Presenter.kt (80%) rename libraries/{core/src/main/java/io/element/android/x/core/di => architecture/src/main/java/io/element/android/x/architecture/viewmodel}/AssistedViewModelFactory.kt (79%) rename libraries/{core/src/main/java/io/element/android/x/core/di => architecture/src/main/java/io/element/android/x/architecture/viewmodel}/DaggerMavericksViewModelFactory.kt (95%) rename libraries/{core/src/main/java/io/element/android/x/core/di => architecture/src/main/java/io/element/android/x/architecture/viewmodel}/ViewModelKey.kt (94%) rename libraries/{core/src/main/java/io/element/android/x/core/di => architecture/src/main/java/io/element/android/x/architecture/viewmodel}/ViewModelSupport.kt (98%) diff --git a/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesViewModelCodeGenerator.kt b/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesViewModelCodeGenerator.kt index 544b6b4b11..a72d10ab84 100644 --- a/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesViewModelCodeGenerator.kt +++ b/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesViewModelCodeGenerator.kt @@ -116,7 +116,7 @@ class ContributesViewModelCodeGenerator : CodeGenerator { } companion object { - private val assistedViewModelFactoryFqName = FqName("io.element.android.x.core.di.AssistedViewModelFactory") - private val viewModelKeyFqName = FqName("io.element.android.x.core.di.ViewModelKey") + private val assistedViewModelFactoryFqName = FqName("io.element.android.x.architecture.viewmodel.AssistedViewModelFactory") + private val viewModelKeyFqName = FqName("io.element.android.x.architecture.viewmodel.ViewModelKey") } -} \ No newline at end of file +} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e26c262165..0c4d93816c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -137,6 +137,7 @@ dependencies { implementation(project(":libraries:designsystem")) implementation(project(":libraries:matrix")) implementation(project(":libraries:core")) + implementation(project(":libraries:architecture")) implementation(project(":features:onboarding")) implementation(project(":features:login")) implementation(project(":features:roomlist")) diff --git a/app/src/main/java/io/element/android/x/ElementXApplication.kt b/app/src/main/java/io/element/android/x/ElementXApplication.kt index fc6c1b8b8c..cc97438f31 100644 --- a/app/src/main/java/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/java/io/element/android/x/ElementXApplication.kt @@ -2,8 +2,8 @@ package io.element.android.x import android.app.Application import androidx.startup.AppInitializer +import io.element.android.x.architecture.bindings import io.element.android.x.core.di.DaggerComponentOwner -import io.element.android.x.core.di.bindings import io.element.android.x.di.AppBindings import io.element.android.x.di.AppComponent import io.element.android.x.di.DaggerAppComponent 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 5bac04cab9..0baf53fafd 100644 --- a/app/src/main/java/io/element/android/x/MainActivity.kt +++ b/app/src/main/java/io/element/android/x/MainActivity.kt @@ -16,8 +16,8 @@ import androidx.core.view.WindowCompat import com.bumble.appyx.core.integration.NodeHost import com.bumble.appyx.core.integrationpoint.NodeComponentActivity import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi +import io.element.android.x.architecture.bindings 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.di.AppBindings import io.element.android.x.node.RootFlowNode diff --git a/app/src/main/java/io/element/android/x/MainViewModel.kt b/app/src/main/java/io/element/android/x/MainViewModel.kt index 506431e593..0c498f3ab6 100644 --- a/app/src/main/java/io/element/android/x/MainViewModel.kt +++ b/app/src/main/java/io/element/android/x/MainViewModel.kt @@ -6,7 +6,7 @@ import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.core.di.daggerMavericksViewModelFactory +import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory import io.element.android.x.di.AppScope import io.element.android.x.di.SessionComponentsOwner import io.element.android.x.matrix.Matrix diff --git a/app/src/main/java/io/element/android/x/di/AppComponent.kt b/app/src/main/java/io/element/android/x/di/AppComponent.kt index e68a58eac3..9b9443b3fc 100644 --- a/app/src/main/java/io/element/android/x/di/AppComponent.kt +++ b/app/src/main/java/io/element/android/x/di/AppComponent.kt @@ -4,7 +4,7 @@ import android.content.Context import com.squareup.anvil.annotations.MergeComponent import dagger.BindsInstance import dagger.Component -import io.element.android.x.core.di.DaggerMavericksBindings +import io.element.android.x.architecture.viewmodel.DaggerMavericksBindings @SingleIn(AppScope::class) @MergeComponent(AppScope::class) diff --git a/app/src/main/java/io/element/android/x/di/SessionComponent.kt b/app/src/main/java/io/element/android/x/di/SessionComponent.kt index e24dd193dc..fd9ffb5078 100644 --- a/app/src/main/java/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/java/io/element/android/x/di/SessionComponent.kt @@ -4,8 +4,8 @@ import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.annotations.MergeSubcomponent import dagger.BindsInstance import dagger.Subcomponent -import io.element.android.x.core.di.DaggerMavericksBindings -import io.element.android.x.core.di.NodeFactoriesBindings +import io.element.android.x.architecture.viewmodel.DaggerMavericksBindings +import io.element.android.x.architecture.NodeFactoriesBindings import io.element.android.x.matrix.MatrixClient @SingleIn(SessionScope::class) 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 ca6f299750..f289573821 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 @@ -1,7 +1,7 @@ package io.element.android.x.di import android.content.Context -import io.element.android.x.core.di.bindings +import io.element.android.x.architecture.bindings import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.core.SessionId import java.util.concurrent.ConcurrentHashMap diff --git a/app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt b/app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt index 1b99716606..9594328881 100644 --- a/app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt +++ b/app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt @@ -5,7 +5,7 @@ import androidx.startup.Initializer import coil.Coil import coil.ImageLoader import coil.ImageLoaderFactory -import io.element.android.x.core.di.bindings +import io.element.android.x.architecture.bindings import io.element.android.x.di.AppBindings class CoilInitializer : Initializer { @@ -37,4 +37,4 @@ private class ElementImageLoaderFactory( } -} \ No newline at end of file +} 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 index bf1047ae52..a22c9d58c7 100644 --- a/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt @@ -10,8 +10,8 @@ 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.createNode -import io.element.android.x.core.di.viewModelSupportNode +import io.element.android.x.architecture.createNode +import io.element.android.x.architecture.viewmodel.viewModelSupportNode import io.element.android.x.features.messages.MessagesScreen import io.element.android.x.features.roomlist.RoomListNode import io.element.android.x.matrix.core.RoomId 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 index e9c848539e..65d830dd99 100644 --- a/app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt @@ -10,7 +10,7 @@ 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.architecture.viewmodel.viewModelSupportNode import io.element.android.x.features.login.node.LoginFlowNode import io.element.android.x.features.onboarding.OnBoardingScreen import kotlinx.parcelize.Parcelize diff --git a/features/login/build.gradle.kts b/features/login/build.gradle.kts index 0d392a6533..b483712e4d 100644 --- a/features/login/build.gradle.kts +++ b/features/login/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { anvil(project(":anvilcodegen")) implementation(project(":libraries:di")) implementation(project(":libraries:core")) + implementation(project(":libraries:architecture")) implementation(project(":libraries:matrix")) implementation(project(":libraries:designsystem")) implementation(project(":libraries:elementresources")) 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 a31016f83e..3900a27c49 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 @@ -8,7 +8,7 @@ import com.airbnb.mvrx.Uninitialized import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.core.di.daggerMavericksViewModelFactory +import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory import io.element.android.x.di.AppScope import io.element.android.x.matrix.Matrix import kotlinx.coroutines.flow.launchIn diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewModel.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewModel.kt index e4007dcb4b..91eeb2b589 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewModel.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewModel.kt @@ -6,7 +6,7 @@ import com.airbnb.mvrx.Uninitialized import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.core.di.daggerMavericksViewModelFactory +import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory import io.element.android.x.di.AppScope import io.element.android.x.matrix.Matrix import kotlinx.coroutines.launch 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 index c33e5e970b..703f2f7aec 100644 --- 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 @@ -10,7 +10,7 @@ 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.architecture.viewmodel.viewModelSupportNode import io.element.android.x.features.login.LoginScreen import io.element.android.x.features.login.changeserver.ChangeServerScreen import kotlinx.parcelize.Parcelize diff --git a/features/messages/build.gradle.kts b/features/messages/build.gradle.kts index c2c4754545..74662c13f3 100644 --- a/features/messages/build.gradle.kts +++ b/features/messages/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { anvil(project(":anvilcodegen")) implementation(project(":libraries:di")) implementation(project(":libraries:core")) + implementation(project(":libraries:architecture")) implementation(project(":libraries:matrix")) implementation(project(":libraries:designsystem")) implementation(project(":libraries:textcomposer")) diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt index 98165305f1..35ad154666 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt @@ -6,10 +6,9 @@ import com.airbnb.mvrx.Uninitialized import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.core.di.daggerMavericksViewModelFactory +import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarSize -import io.element.android.x.di.AppScope import io.element.android.x.di.SessionScope import io.element.android.x.features.messages.model.MessagesItemAction import io.element.android.x.features.messages.model.MessagesItemActionsSheetState @@ -17,7 +16,6 @@ import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.model.MessagesViewState import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent -import io.element.android.x.matrix.Matrix import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.timeline.MatrixTimeline diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerViewModel.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerViewModel.kt index cddb9eeae0..cf8c2f4f9c 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerViewModel.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerViewModel.kt @@ -5,8 +5,8 @@ import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.x.anvilannotations.ContributesViewModel +import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory import io.element.android.x.core.data.StableCharSequence -import io.element.android.x.core.di.daggerMavericksViewModelFactory import io.element.android.x.di.SessionScope import io.element.android.x.matrix.MatrixClient diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index bdcc5d4e76..a686b4cdbd 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(project(":libraries:core")) implementation(project(":libraries:elementresources")) implementation(project(":libraries:designsystem")) + implementation(project(":libraries:architecture")) implementation(libs.mavericks.compose) implementation(libs.accompanist.pager) implementation(libs.accompanist.pagerindicator) diff --git a/features/roomlist/build.gradle.kts b/features/roomlist/build.gradle.kts index 62de2c6549..fae8ad97d2 100644 --- a/features/roomlist/build.gradle.kts +++ b/features/roomlist/build.gradle.kts @@ -17,7 +17,7 @@ dependencies { anvil(project(":anvilcodegen")) implementation(project(":libraries:di")) implementation(project(":libraries:core")) - implementation(project(":libraries:presentation")) + implementation(project(":libraries:architecture")) implementation(project(":libraries:matrix")) implementation(project(":libraries:designsystem")) implementation(libs.appyx.core) diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt index d4fc2d25bb..698aa4b083 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt @@ -4,8 +4,8 @@ import com.squareup.anvil.annotations.ContributesTo import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap -import io.element.android.x.core.di.AssistedNodeFactory -import io.element.android.x.core.di.NodeKey +import io.element.android.x.architecture.AssistedNodeFactory +import io.element.android.x.architecture.NodeKey import io.element.android.x.di.SessionScope @Module diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt index 1e4f0a0c27..64ed13bf96 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -11,10 +11,10 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.x.core.di.AssistedNodeFactory +import io.element.android.x.architecture.AssistedNodeFactory import io.element.android.x.features.roomlist.model.RoomListEvents import io.element.android.x.matrix.core.RoomId -import io.element.android.x.presentation.presenterConnector +import io.element.android.x.architecture.presenterConnector class RoomListNode @AssistedInject constructor( @Assisted buildContext: BuildContext, diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt index 3760f2f02c..492d098d6d 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt @@ -20,7 +20,7 @@ import io.element.android.x.features.roomlist.model.RoomListState import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.room.RoomSummary -import io.element.android.x.presentation.Presenter +import io.element.android.x.architecture.Presenter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/libraries/presentation/.gitignore b/libraries/architecture/.gitignore similarity index 100% rename from libraries/presentation/.gitignore rename to libraries/architecture/.gitignore diff --git a/libraries/presentation/build.gradle.kts b/libraries/architecture/build.gradle.kts similarity index 66% rename from libraries/presentation/build.gradle.kts rename to libraries/architecture/build.gradle.kts index 829df0138f..5ac492f37b 100644 --- a/libraries/presentation/build.gradle.kts +++ b/libraries/architecture/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") alias(libs.plugins.molecule) } @@ -8,7 +8,9 @@ android { } dependencies { + api(project(":libraries:core")) api(libs.dagger) api(libs.appyx.core) api(libs.androidx.lifecycle.runtime) + api(libs.mavericks.compose) } diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/AssistedNodeFactory.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/AssistedNodeFactory.kt similarity index 85% rename from libraries/core/src/main/java/io/element/android/x/core/di/AssistedNodeFactory.kt rename to libraries/architecture/src/main/java/io/element/android/x/architecture/AssistedNodeFactory.kt index 022fe03720..06b8e108c5 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/di/AssistedNodeFactory.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/AssistedNodeFactory.kt @@ -1,4 +1,4 @@ -package io.element.android.x.core.di +package io.element.android.x.architecture import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/Bindings.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/Bindings.kt similarity index 55% rename from libraries/core/src/main/java/io/element/android/x/core/di/Bindings.kt rename to libraries/architecture/src/main/java/io/element/android/x/architecture/Bindings.kt index 9fe69bc112..df6231485a 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/di/Bindings.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/Bindings.kt @@ -1,30 +1,12 @@ -package io.element.android.x.core.di +package io.element.android.x.architecture 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: - * * an inject function: `inject(node: MyNode)` - * * an explicit getter: `fun myClass(): MyClass` - * - * Anvil will make your Dagger component implement these bindings so that you can call any of these functions on an instance of your component. - * - * [bindings] will walk up the Node/Activity hierarchy and check for [DaggerComponentOwner] to see if any of its components implement the - * specified bindings. Most of the time this will "just work" and you don't have to think about it. - * - * For example, if your class has @Inject properties: - * 1) Create an bindings interface such as `YourModuleBindings` - * 1) Add an inject function like `fun inject(yourClass: YourClass)` - * 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) +import io.element.android.x.core.di.DaggerComponentOwner inline fun Node.bindings() = bindings(T::class.java) +inline fun Context.bindings() = bindings(T::class.java) /** Use no-arg extension function instead: [Context.bindings] */ fun Context.bindings(klass: Class): T { diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/NodeFactories.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/NodeFactories.kt similarity index 94% rename from libraries/core/src/main/java/io/element/android/x/core/di/NodeFactories.kt rename to libraries/architecture/src/main/java/io/element/android/x/architecture/NodeFactories.kt index 94605d4a6a..efccaafb25 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/di/NodeFactories.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/NodeFactories.kt @@ -1,4 +1,4 @@ -package io.element.android.x.core.di +package io.element.android.x.architecture import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/NodeKey.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/NodeKey.kt similarity index 95% rename from libraries/core/src/main/java/io/element/android/x/core/di/NodeKey.kt rename to libraries/architecture/src/main/java/io/element/android/x/architecture/NodeKey.kt index b6541a4688..63e178552e 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/di/NodeKey.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/NodeKey.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.core.di +package io.element.android.x.architecture import com.bumble.appyx.core.node.Node import dagger.MapKey diff --git a/libraries/presentation/src/main/java/io/element/android/x/presentation/NodePresenterConnector.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/NodePresenterConnector.kt similarity index 96% rename from libraries/presentation/src/main/java/io/element/android/x/presentation/NodePresenterConnector.kt rename to libraries/architecture/src/main/java/io/element/android/x/architecture/NodePresenterConnector.kt index 1976e3a84b..e451d7c2e4 100644 --- a/libraries/presentation/src/main/java/io/element/android/x/presentation/NodePresenterConnector.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/NodePresenterConnector.kt @@ -1,4 +1,4 @@ -package io.element.android.x.presentation +package io.element.android.x.architecture import androidx.lifecycle.lifecycleScope import app.cash.molecule.AndroidUiDispatcher diff --git a/libraries/presentation/src/main/java/io/element/android/x/presentation/Presenter.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt similarity index 80% rename from libraries/presentation/src/main/java/io/element/android/x/presentation/Presenter.kt rename to libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt index 682032ec28..a0addbfe1a 100644 --- a/libraries/presentation/src/main/java/io/element/android/x/presentation/Presenter.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt @@ -1,4 +1,4 @@ -package io.element.android.x.presentation +package io.element.android.x.architecture import androidx.compose.runtime.Composable import kotlinx.coroutines.flow.Flow diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/AssistedViewModelFactory.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/AssistedViewModelFactory.kt similarity index 79% rename from libraries/core/src/main/java/io/element/android/x/core/di/AssistedViewModelFactory.kt rename to libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/AssistedViewModelFactory.kt index 239ce4ac62..6dee6da6d3 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/di/AssistedViewModelFactory.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/AssistedViewModelFactory.kt @@ -1,4 +1,4 @@ -package io.element.android.x.core.di +package io.element.android.x.architecture.viewmodel import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksViewModel diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/DaggerMavericksViewModelFactory.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/DaggerMavericksViewModelFactory.kt similarity index 95% rename from libraries/core/src/main/java/io/element/android/x/core/di/DaggerMavericksViewModelFactory.kt rename to libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/DaggerMavericksViewModelFactory.kt index a67c9b204d..30fd38a3ee 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/di/DaggerMavericksViewModelFactory.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/DaggerMavericksViewModelFactory.kt @@ -1,10 +1,10 @@ -package io.element.android.x.core.di +package io.element.android.x.architecture.viewmodel -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext +import io.element.android.x.architecture.bindings /** * To connect Mavericks ViewModel creation with Anvil's dependency injection, add the following to your MavericksViewModel. diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/ViewModelKey.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelKey.kt similarity index 94% rename from libraries/core/src/main/java/io/element/android/x/core/di/ViewModelKey.kt rename to libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelKey.kt index 67a259a879..0d4f718caf 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/di/ViewModelKey.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelKey.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.core.di +package io.element.android.x.architecture.viewmodel import com.airbnb.mvrx.MavericksViewModel import dagger.MapKey diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/ViewModelSupport.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelSupport.kt similarity index 98% rename from libraries/core/src/main/java/io/element/android/x/core/di/ViewModelSupport.kt rename to libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelSupport.kt index 27801979c7..7a7fc576c2 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/di/ViewModelSupport.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelSupport.kt @@ -1,4 +1,4 @@ -package io.element.android.x.core.di +package io.element.android.x.architecture.viewmodel import android.os.Bundle import androidx.compose.runtime.Composable diff --git a/libraries/core/build.gradle.kts b/libraries/core/build.gradle.kts index 29f4ec2274..6706fcdfa8 100644 --- a/libraries/core/build.gradle.kts +++ b/libraries/core/build.gradle.kts @@ -5,9 +5,3 @@ plugins { android { namespace = "io.element.android.x.core" } - -dependencies { - api(libs.mavericks.compose) - api(libs.dagger) - api(libs.appyx.core) -} diff --git a/libraries/matrix/build.gradle.kts b/libraries/matrix/build.gradle.kts index fa9d9802e1..e515e7e752 100644 --- a/libraries/matrix/build.gradle.kts +++ b/libraries/matrix/build.gradle.kts @@ -15,6 +15,7 @@ anvil { dependencies { api(project(":libraries:rustsdk")) implementation(project(":libraries:di")) + implementation(libs.dagger) implementation(project(":libraries:core")) implementation("net.java.dev.jna:jna:5.12.1@aar") implementation(libs.coil.compose) diff --git a/settings.gradle.kts b/settings.gradle.kts index c9bad7e2ed..0faeb54c2a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,4 +31,4 @@ include(":libraries:designsystem") include(":libraries:di") include(":anvilannotations") include(":anvilcodegen") -include(":libraries:presentation") +include(":libraries:architecture") From 9365dc0e74ac8d3ee828b4ef6e4f6cb3c3e348e6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 5 Jan 2023 10:59:26 +0100 Subject: [PATCH 10/30] Use LifecycleOwner instead of explicit Node on presenterConnector --- .../android/x/features/roomlist/RoomListNode.kt | 2 +- ...nterConnector.kt => LifecyclePresenterConnector.kt} | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename libraries/architecture/src/main/java/io/element/android/x/architecture/{NodePresenterConnector.kt => LifecyclePresenterConnector.kt} (56%) diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt index 64ed13bf96..72983da1a8 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -31,7 +31,7 @@ class RoomListNode @AssistedInject constructor( fun onRoomClicked(roomId: RoomId) } - private val connector = presenterConnector(presenter) + private val connector by presenterConnector(presenter) private fun updateFilter(filter: String) { connector.emitEvent(RoomListEvents.UpdateFilter(filter)) diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/NodePresenterConnector.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/LifecyclePresenterConnector.kt similarity index 56% rename from libraries/architecture/src/main/java/io/element/android/x/architecture/NodePresenterConnector.kt rename to libraries/architecture/src/main/java/io/element/android/x/architecture/LifecyclePresenterConnector.kt index e451d7c2e4..dcca215f07 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/NodePresenterConnector.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/LifecyclePresenterConnector.kt @@ -1,21 +1,21 @@ package io.element.android.x.architecture +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import app.cash.molecule.AndroidUiDispatcher import app.cash.molecule.RecompositionClock import app.cash.molecule.launchMolecule -import com.bumble.appyx.core.node.Node import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.StateFlow -inline fun Node.presenterConnector(presenter: Presenter): NodePresenterConnector { - return NodePresenterConnector(node = this, presenter = presenter) +inline fun LifecycleOwner.presenterConnector(presenter: Presenter): Lazy> = lazy { + LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter) } -class NodePresenterConnector(private val node: Node, presenter: Presenter) { +class LifecyclePresenterConnector(lifecycleOwner: LifecycleOwner, presenter: Presenter) { - private val moleculeScope = CoroutineScope(node.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main) + private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main) private val eventFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 64) val stateFlow: StateFlow = moleculeScope.launchMolecule(RecompositionClock.ContextClock) { From f4ac368d07d040b94fc5d6e9307a1f4d36fe6c5f Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 5 Jan 2023 11:22:38 +0100 Subject: [PATCH 11/30] Create ContributesNode annotation to generate module and assistedFactory for node. --- .../x/anvilannotations/ContributesNode.kt | 23 +++ .../ContributesNodeCodeGenerator.kt | 138 ++++++++++++++++++ .../x/features/roomlist/RoomListModule.kt | 19 --- .../x/features/roomlist/RoomListNode.kt | 12 +- 4 files changed, 165 insertions(+), 27 deletions(-) create mode 100644 anvilannotations/src/main/java/io/element/android/x/anvilannotations/ContributesNode.kt create mode 100644 anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesNodeCodeGenerator.kt delete mode 100644 features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt diff --git a/anvilannotations/src/main/java/io/element/android/x/anvilannotations/ContributesNode.kt b/anvilannotations/src/main/java/io/element/android/x/anvilannotations/ContributesNode.kt new file mode 100644 index 0000000000..9a858e51a8 --- /dev/null +++ b/anvilannotations/src/main/java/io/element/android/x/anvilannotations/ContributesNode.kt @@ -0,0 +1,23 @@ +package io.element.android.x.anvilannotations + +import kotlin.reflect.KClass + +/** + * Adds Node to the specified component graph. + * Equivalent to the following declaration: + * + * @Module + * @ContributesTo(Scope::class) + * abstract class YourNodeModule { + + * @Binds + * @IntoMap + * @NodeKey(YourNode::class) + * abstract fun bindYourNodeFactory(factory: YourNode.Factory): AssistedNodeFactory<*> + *} + + */ +@Target(AnnotationTarget.CLASS) +annotation class ContributesNode( + val scope: KClass<*>, +) diff --git a/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesNodeCodeGenerator.kt b/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesNodeCodeGenerator.kt new file mode 100644 index 0000000000..6a2d0df036 --- /dev/null +++ b/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesNodeCodeGenerator.kt @@ -0,0 +1,138 @@ +@file:OptIn(ExperimentalAnvilApi::class) + +package io.element.android.x.anvilcodegen + +import com.google.auto.service.AutoService +import com.squareup.anvil.annotations.ContributesTo +import com.squareup.anvil.annotations.ExperimentalAnvilApi +import com.squareup.anvil.compiler.api.AnvilCompilationException +import com.squareup.anvil.compiler.api.AnvilContext +import com.squareup.anvil.compiler.api.CodeGenerator +import com.squareup.anvil.compiler.api.GeneratedFile +import com.squareup.anvil.compiler.api.createGeneratedFile +import com.squareup.anvil.compiler.internal.asClassName +import com.squareup.anvil.compiler.internal.buildFile +import com.squareup.anvil.compiler.internal.fqName +import com.squareup.anvil.compiler.internal.reference.ClassReference +import com.squareup.anvil.compiler.internal.reference.asClassName +import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.STAR +import com.squareup.kotlinpoet.TypeSpec +import dagger.Binds +import dagger.Module +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.multibindings.IntoMap +import io.element.android.x.anvilannotations.ContributesNode +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtFile +import java.io.File + +/** + * This is an anvil plugin that allows Node to use [ContributesNode] alone and let this plugin automatically + * handle the rest of the Dagger wiring required for constructor injection. + */ +@AutoService(CodeGenerator::class) +class ContributesNodeCodeGenerator : CodeGenerator { + + override fun isApplicable(context: AnvilContext): Boolean = true + + override fun generateCode(codeGenDir: File, module: ModuleDescriptor, projectFiles: Collection): Collection { + return projectFiles.classAndInnerClassReferences(module) + .filter { it.isAnnotatedWith(ContributesNode::class.fqName) } + .flatMap { listOf(generateModule(it, codeGenDir, module), generateAssistedFactory(it, codeGenDir, module)) } + .toList() + } + + private fun generateModule(nodeClass: ClassReference.Psi, codeGenDir: File, module: ModuleDescriptor): GeneratedFile { + val generatedPackage = nodeClass.packageFqName.toString() + val moduleClassName = "${nodeClass.shortName}_Module" + val scope = nodeClass.annotations.single { it.fqName == ContributesNode::class.fqName }.scope() + val content = FileSpec.buildFile(generatedPackage, moduleClassName) { + addType( + TypeSpec.classBuilder(moduleClassName) + .addModifiers(KModifier.ABSTRACT) + .addAnnotation(Module::class) + .addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.asClassName()).build()) + .addFunction( + FunSpec.builder("bind${nodeClass.shortName}Factory") + .addModifiers(KModifier.ABSTRACT) + .addParameter("factory", ClassName(generatedPackage, "${nodeClass.shortName}_AssistedFactory")) + .returns(assistedNodeFactoryFqName.asClassName(module).parameterizedBy(STAR)) + .addAnnotation(Binds::class) + .addAnnotation(IntoMap::class) + .addAnnotation( + AnnotationSpec.Companion.builder(nodeKeyFqName.asClassName(module)).addMember( + "%T::class", + nodeClass.asClassName() + ).build() + ) + .build(), + ) + .build(), + ) + } + return createGeneratedFile(codeGenDir, generatedPackage, moduleClassName, content) + } + + private fun generateAssistedFactory(nodeClass: ClassReference.Psi, codeGenDir: File, module: ModuleDescriptor): GeneratedFile { + val generatedPackage = nodeClass.packageFqName.toString() + val assistedFactoryClassName = "${nodeClass.shortName}_AssistedFactory" + val constructor = nodeClass.constructors.singleOrNull { it.isAnnotatedWith(AssistedInject::class.fqName) } + val assistedParameters = constructor?.parameters?.filter { it.isAnnotatedWith(Assisted::class.fqName) }.orEmpty() + if (constructor == null || assistedParameters.size != 2) { + throw AnvilCompilationException( + "${nodeClass.fqName} must have an @AssistedInject constructor with 2 @Assisted parameters", + element = nodeClass.clazz, + ) + } + val contextAssistedParam = assistedParameters[0] + if (contextAssistedParam.name != "buildContext") { + throw AnvilCompilationException( + "${nodeClass.fqName} @Assisted parameter must be named buildContext", + element = contextAssistedParam.parameter, + ) + } + val pluginsAssistedParam = assistedParameters[1] + if (pluginsAssistedParam.name != "plugins") { + throw AnvilCompilationException( + "${nodeClass.fqName} @Assisted parameter must be named plugins", + element = pluginsAssistedParam.parameter, + ) + } + + val nodeClassName = nodeClass.asClassName() + val buildContextClassName = contextAssistedParam.type().asTypeName() + val pluginsClassName = pluginsAssistedParam.type().asTypeName() + val content = FileSpec.buildFile(generatedPackage, assistedFactoryClassName) { + addType( + TypeSpec.interfaceBuilder(assistedFactoryClassName) + .addSuperinterface(assistedNodeFactoryFqName.asClassName(module).parameterizedBy(nodeClassName)) + .addAnnotation(AssistedFactory::class) + .addFunction( + FunSpec.builder("create") + .addModifiers(KModifier.OVERRIDE, KModifier.ABSTRACT) + .addParameter("buildContext", buildContextClassName) + .addParameter("plugins", pluginsClassName) + .returns(nodeClassName) + .build(), + ) + .build(), + ) + } + return createGeneratedFile(codeGenDir, generatedPackage, assistedFactoryClassName, content) + } + + companion object { + private val assistedNodeFactoryFqName = FqName("io.element.android.x.architecture.AssistedNodeFactory") + private val nodeKeyFqName = FqName("io.element.android.x.architecture.NodeKey") + } +} diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt deleted file mode 100644 index 698aa4b083..0000000000 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.element.android.x.features.roomlist - -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module -import dagger.multibindings.IntoMap -import io.element.android.x.architecture.AssistedNodeFactory -import io.element.android.x.architecture.NodeKey -import io.element.android.x.di.SessionScope - -@Module -@ContributesTo(SessionScope::class) -abstract class RoomListModule { - - @Binds - @IntoMap - @NodeKey(RoomListNode::class) - abstract fun bindRoomListNodeFactory(factory: RoomListNode.Factory): AssistedNodeFactory<*> -} diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt index 72983da1a8..8a994fcd07 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -9,24 +9,20 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.x.architecture.AssistedNodeFactory +import io.element.android.x.anvilannotations.ContributesNode +import io.element.android.x.architecture.presenterConnector +import io.element.android.x.di.SessionScope import io.element.android.x.features.roomlist.model.RoomListEvents import io.element.android.x.matrix.core.RoomId -import io.element.android.x.architecture.presenterConnector +@ContributesNode(SessionScope::class) class RoomListNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenter: RoomListPresenter, ) : Node(buildContext, plugins = plugins) { - @AssistedFactory - interface Factory : AssistedNodeFactory { - override fun create(buildContext: BuildContext, plugins: List): RoomListNode - } - interface Callback : Plugin { fun onRoomClicked(roomId: RoomId) } From 9ed208950e6f820eb2c8df2522a8727aa98521c1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 6 Jan 2023 15:15:45 +0100 Subject: [PATCH 12/30] Migrate Login to new architecture and make some adjustments --- .../io/element/android/x/di/AppComponent.kt | 3 +- .../android/x/node/NotLoggedInFlowNode.kt | 6 +- .../login/{node => }/LoginFlowNode.kt | 28 +++---- .../x/features/login/LoginViewModel.kt | 67 ---------------- .../x/features/login/LoginViewState.kt | 26 ------- .../login/changeserver/ChangeServerEvents.kt | 6 ++ .../login/changeserver/ChangeServerNode.kt | 47 ++++++++++++ .../changeserver/ChangeServerPresenter.kt | 47 ++++++++++++ .../login/changeserver/ChangeServerState.kt | 10 +++ ...ngeServerScreen.kt => ChangeServerView.kt} | 45 +++++------ .../changeserver/ChangeServerViewModel.kt | 51 ------------- .../changeserver/ChangeServerViewState.kt | 13 ---- .../x/features/login/error/ErrorFormatter.kt | 2 +- .../x/features/login/root/LoginRootEvents.kt | 8 ++ .../x/features/login/root/LoginRootNode.kt | 64 ++++++++++++++++ .../features/login/root/LoginRootPresenter.kt | 69 +++++++++++++++++ .../LoginRootScreen.kt} | 76 +++++++------------ .../x/features/login/root/LoginRootState.kt | 32 ++++++++ .../x/features/onboarding/OnBoardingScreen.kt | 20 +---- .../onboarding/OnBoardingViewModel.kt | 15 ---- .../onboarding/OnBoardingViewState.kt | 7 -- .../onboarding/SplashCarouselStateFactory.kt | 1 + .../x/features/roomlist/RoomListNode.kt | 2 +- .../element/android/x/architecture/Async.kt | 31 ++++++++ ...nterConnector.kt => PresenterConnector.kt} | 13 ++-- 25 files changed, 385 insertions(+), 304 deletions(-) rename features/login/src/main/java/io/element/android/x/features/login/{node => }/LoginFlowNode.kt (65%) delete mode 100644 features/login/src/main/java/io/element/android/x/features/login/LoginViewModel.kt delete mode 100644 features/login/src/main/java/io/element/android/x/features/login/LoginViewState.kt create mode 100644 features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt create mode 100644 features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt create mode 100644 features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt create mode 100644 features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt rename features/login/src/main/java/io/element/android/x/features/login/changeserver/{ChangeServerScreen.kt => ChangeServerView.kt} (85%) delete mode 100644 features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewModel.kt delete mode 100644 features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewState.kt create mode 100644 features/login/src/main/java/io/element/android/x/features/login/root/LoginRootEvents.kt create mode 100644 features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt create mode 100644 features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt rename features/login/src/main/java/io/element/android/x/features/login/{LoginScreen.kt => root/LoginRootScreen.kt} (80%) create mode 100644 features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt delete mode 100644 features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewModel.kt delete mode 100644 features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewState.kt create mode 100644 libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt rename libraries/architecture/src/main/java/io/element/android/x/architecture/{LifecyclePresenterConnector.kt => PresenterConnector.kt} (70%) diff --git a/app/src/main/java/io/element/android/x/di/AppComponent.kt b/app/src/main/java/io/element/android/x/di/AppComponent.kt index 9b9443b3fc..fb6e924672 100644 --- a/app/src/main/java/io/element/android/x/di/AppComponent.kt +++ b/app/src/main/java/io/element/android/x/di/AppComponent.kt @@ -4,11 +4,12 @@ import android.content.Context import com.squareup.anvil.annotations.MergeComponent import dagger.BindsInstance import dagger.Component +import io.element.android.x.architecture.NodeFactoriesBindings import io.element.android.x.architecture.viewmodel.DaggerMavericksBindings @SingleIn(AppScope::class) @MergeComponent(AppScope::class) -interface AppComponent : DaggerMavericksBindings { +interface AppComponent : DaggerMavericksBindings, NodeFactoriesBindings { @Component.Factory interface Factory { 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 index 65d830dd99..4f346bdbdd 100644 --- a/app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt @@ -8,10 +8,10 @@ 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.architecture.viewmodel.viewModelSupportNode -import io.element.android.x.features.login.node.LoginFlowNode +import io.element.android.x.features.login.LoginFlowNode import io.element.android.x.features.onboarding.OnBoardingScreen import kotlinx.parcelize.Parcelize import timber.log.Timber @@ -44,7 +44,7 @@ class NotLoggedInFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.OnBoarding -> viewModelSupportNode(buildContext) { + NavTarget.OnBoarding -> node(buildContext) { OnBoardingScreen( onSignIn = { backstack.replace(NavTarget.LoginFlow) } ) 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/LoginFlowNode.kt similarity index 65% rename from features/login/src/main/java/io/element/android/x/features/login/node/LoginFlowNode.kt rename to features/login/src/main/java/io/element/android/x/features/login/LoginFlowNode.kt index 703f2f7aec..5f1fcbfea9 100644 --- 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/LoginFlowNode.kt @@ -1,4 +1,4 @@ -package io.element.android.x.features.login.node +package io.element.android.x.features.login import android.os.Parcelable import androidx.compose.runtime.Composable @@ -8,11 +8,10 @@ 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.architecture.viewmodel.viewModelSupportNode -import io.element.android.x.features.login.LoginScreen -import io.element.android.x.features.login.changeserver.ChangeServerScreen +import io.element.android.x.architecture.createNode +import io.element.android.x.features.login.changeserver.ChangeServerNode +import io.element.android.x.features.login.root.LoginRootNode import kotlinx.parcelize.Parcelize class LoginFlowNode( @@ -26,6 +25,12 @@ class LoginFlowNode( buildContext = buildContext ) { + private val loginRootCallback = object : LoginRootNode.Callback { + override fun onChangeHomeServer() { + backstack.push(NavTarget.ChangeServer) + } + } + sealed interface NavTarget : Parcelable { @Parcelize object Root : NavTarget @@ -36,16 +41,8 @@ class LoginFlowNode( 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() } - ) - } + NavTarget.Root -> createNode(buildContext, plugins = listOf(loginRootCallback)) + NavTarget.ChangeServer -> createNode(buildContext) } } @@ -53,5 +50,4 @@ class LoginFlowNode( override fun View(modifier: Modifier) { Children(navModel = backstack) } - } 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 deleted file mode 100644 index 3900a27c49..0000000000 --- a/features/login/src/main/java/io/element/android/x/features/login/LoginViewModel.kt +++ /dev/null @@ -1,67 +0,0 @@ -package io.element.android.x.features.login - -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.snapshotFlow -import com.airbnb.mvrx.MavericksViewModel -import com.airbnb.mvrx.MavericksViewModelFactory -import com.airbnb.mvrx.Uninitialized -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory -import io.element.android.x.di.AppScope -import io.element.android.x.matrix.Matrix -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch - -@ContributesViewModel(AppScope::class) -class LoginViewModel @AssistedInject constructor( - private val matrix: Matrix, - @Assisted initialState: LoginViewState) : - MavericksViewModel(initialState) { - - companion object : MavericksViewModelFactory by daggerMavericksViewModelFactory() - - var formState = mutableStateOf(LoginFormState.Default) - private set - - init { - snapshotFlow { formState.value } - .onEach { - setState { copy(formState = it) } - }.launchIn(viewModelScope) - } - - fun onResume() { - val currentHomeserver = matrix.getHomeserverOrDefault() - setState { - copy( - homeserver = currentHomeserver - ) - } - } - - fun onSubmit() { - viewModelScope.launch { - suspend { - 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()) - }.execute { - copy(loggedInSessionId = it) - } - } - } - - fun onSetPassword(password: String) { - formState.value = formState.value.copy(password = password) - setState { copy(loggedInSessionId = Uninitialized) } - } - - fun onSetName(name: String) { - formState.value = formState.value.copy(login = name) - 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 deleted file mode 100644 index 27a15945a6..0000000000 --- a/features/login/src/main/java/io/element/android/x/features/login/LoginViewState.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.element.android.x.features.login - -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.core.SessionId - -data class LoginViewState( - val homeserver: String = "", - val loggedInSessionId: Async = Uninitialized, - val formState: LoginFormState = LoginFormState.Default, -) : MavericksState { - val submitEnabled = - formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInSessionId !is Loading -} - -data class LoginFormState( - val login: String, - val password: String -) { - - companion object { - val Default = LoginFormState("", "") - } -} diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt new file mode 100644 index 0000000000..893459b7cc --- /dev/null +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt @@ -0,0 +1,6 @@ +package io.element.android.x.features.login.changeserver + +sealed interface ChangeServerEvents { + data class SetServer(val server: String) : ChangeServerEvents + object Submit: ChangeServerEvents +} diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt new file mode 100644 index 0000000000..f9f0bc4bbd --- /dev/null +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt @@ -0,0 +1,47 @@ +package io.element.android.x.features.login.changeserver + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.x.anvilannotations.ContributesNode +import io.element.android.x.architecture.presenterConnector +import io.element.android.x.di.AppScope + +@ContributesNode(AppScope::class) +class ChangeServerNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: ChangeServerPresenter, +) : Node(buildContext, plugins = plugins) { + + private val presenterConnector = presenterConnector(presenter) + + private fun onChangeServer(server: String) { + presenterConnector.emitEvent(ChangeServerEvents.SetServer(server)) + } + + private fun onSubmit() { + presenterConnector.emitEvent(ChangeServerEvents.Submit) + } + + private fun onSuccess() { + navigateUp() + } + + @Composable + override fun View(modifier: Modifier) { + val state by presenterConnector.stateFlow.collectAsState() + ChangeServerView( + state = state, + onChangeServer = this::onChangeServer, + onChangeServerSubmit = this::onSubmit, + onChangeServerSuccess = this::onSuccess, + ) + } +} diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt new file mode 100644 index 0000000000..c5d9891c7c --- /dev/null +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt @@ -0,0 +1,47 @@ +package io.element.android.x.features.login.changeserver + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.x.architecture.Async +import io.element.android.x.architecture.Presenter +import io.element.android.x.architecture.execute +import io.element.android.x.matrix.Matrix +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ChangeServerPresenter @Inject constructor(private val matrix: Matrix) : Presenter { + + @Composable + override fun present(events: Flow): ChangeServerState { + val homeserver = rememberSaveable { + mutableStateOf(matrix.getHomeserverOrDefault()) + } + val changeServerAction: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } + LaunchedEffect(Unit) { + events.collect { event -> + when (event) { + is ChangeServerEvents.SetServer -> homeserver.value = event.server + ChangeServerEvents.Submit -> submit(homeserver.value, changeServerAction) + } + } + } + return ChangeServerState( + homeserver = homeserver.value, + changeServerAction = changeServerAction.value + ) + } + + private fun CoroutineScope.submit(homeserver: String, changeServerAction: MutableState>) = launch { + suspend { + matrix.setHomeserver(homeserver) + }.execute(changeServerAction) + } +} diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt new file mode 100644 index 0000000000..dabd7a09bf --- /dev/null +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt @@ -0,0 +1,10 @@ +package io.element.android.x.features.login.changeserver + +import io.element.android.x.architecture.Async + +data class ChangeServerState( + val homeserver: String = "", + val changeServerAction: Async = Async.Uninitialized, +) { + val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Async.Loading +} diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerScreen.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt similarity index 85% rename from features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerScreen.kt rename to features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt index 3918d2f6c5..6c7b63f06c 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerScreen.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -35,33 +36,17 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.compose.collectAsState -import com.airbnb.mvrx.compose.mavericksViewModel +import io.element.android.x.architecture.Async +import io.element.android.x.core.compose.textFieldState import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.components.VectorIcon import io.element.android.x.features.login.R import io.element.android.x.features.login.error.changeServerError -@Composable -fun ChangeServerScreen( - viewModel: ChangeServerViewModel = mavericksViewModel(), - onChangeServerSuccess: () -> Unit = { } -) { - val state: ChangeServerViewState by viewModel.collectAsState() - ChangeServerContent( - state = state, - onChangeServer = viewModel::setServer, - onChangeServerSubmit = viewModel::setServerSubmit, - onChangeServerSuccess = onChangeServerSuccess - ) -} @Composable -fun ChangeServerContent( - state: ChangeServerViewState, +fun ChangeServerView( + state: ChangeServerState, modifier: Modifier = Modifier, onChangeServer: (String) -> Unit = {}, onChangeServerSubmit: () -> Unit = {}, @@ -85,7 +70,7 @@ fun ChangeServerContent( ) .padding(horizontal = 16.dp) ) { - val isError = state.changeServerAction is Fail + val isError = state.changeServerAction is Async.Failure Box( modifier = Modifier .padding(top = 99.dp) @@ -126,12 +111,16 @@ fun ChangeServerContent( fontSize = 16.sp, color = MaterialTheme.colorScheme.secondary ) + var homeserverFieldState by textFieldState(stateValue = state.homeserver) OutlinedTextField( - value = state.homeserver, + value = homeserverFieldState, modifier = Modifier .fillMaxWidth() .padding(top = 200.dp), - onValueChange = onChangeServer, + onValueChange = { + homeserverFieldState = it + onChangeServer(it) + }, label = { Text(text = "Server") }, @@ -144,7 +133,7 @@ fun ChangeServerContent( onDone = { onChangeServerSubmit() } ) ) - if (state.changeServerAction is Fail) { + if (state.changeServerAction is Async.Failure) { Text( text = changeServerError( state.homeserver, @@ -164,11 +153,11 @@ fun ChangeServerContent( ) { Text(text = "Continue") } - if (state.changeServerAction is Success) { + if (state.changeServerAction is Async.Success) { onChangeServerSuccess() } } - if (state.changeServerAction is Loading) { + if (state.changeServerAction is Async.Loading) { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center) ) @@ -181,8 +170,8 @@ fun ChangeServerContent( @Preview fun ChangeServerContentPreview() { ElementXTheme { - ChangeServerContent( - state = ChangeServerViewState(homeserver = "matrix.org"), + ChangeServerView( + state = ChangeServerState(homeserver = "matrix.org"), ) } } diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewModel.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewModel.kt deleted file mode 100644 index 91eeb2b589..0000000000 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewModel.kt +++ /dev/null @@ -1,51 +0,0 @@ -package io.element.android.x.features.login.changeserver - -import com.airbnb.mvrx.MavericksViewModel -import com.airbnb.mvrx.MavericksViewModelFactory -import com.airbnb.mvrx.Uninitialized -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory -import io.element.android.x.di.AppScope -import io.element.android.x.matrix.Matrix -import kotlinx.coroutines.launch - -@ContributesViewModel(AppScope::class) -class ChangeServerViewModel @AssistedInject constructor( - private val matrix: Matrix, - @Assisted initialState: ChangeServerViewState -) : - MavericksViewModel(initialState) { - - companion object : - MavericksViewModelFactory by daggerMavericksViewModelFactory() - - init { - setState { - copy( - homeserver = matrix.getHomeserverOrDefault() - ) - } - } - - fun setServer(server: String) { - setState { - copy( - homeserver = server, - changeServerAction = Uninitialized, - ) - } - } - - fun setServerSubmit() { - viewModelScope.launch { - suspend { - val state = awaitState() - matrix.setHomeserver(state.homeserver) - }.execute { - copy(changeServerAction = it) - } - } - } -} diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewState.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewState.kt deleted file mode 100644 index 8e1f907c31..0000000000 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerViewState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.element.android.x.features.login.changeserver - -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MavericksState -import com.airbnb.mvrx.Uninitialized - -data class ChangeServerViewState( - val homeserver: String = "", - val changeServerAction: Async = Uninitialized, -) : MavericksState { - val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Loading -} diff --git a/features/login/src/main/java/io/element/android/x/features/login/error/ErrorFormatter.kt b/features/login/src/main/java/io/element/android/x/features/login/error/ErrorFormatter.kt index 6c26a9aff1..32b801d95a 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/error/ErrorFormatter.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/error/ErrorFormatter.kt @@ -3,8 +3,8 @@ package io.element.android.x.features.login.error import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import io.element.android.x.core.uri.isValidUrl +import io.element.android.x.features.login.root.LoginFormState import io.element.android.x.element.resources.R as ElementR -import io.element.android.x.features.login.LoginFormState @Composable fun loginError( diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootEvents.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootEvents.kt new file mode 100644 index 0000000000..9c7f401060 --- /dev/null +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootEvents.kt @@ -0,0 +1,8 @@ +package io.element.android.x.features.login.root + +sealed interface LoginRootEvents { + object RefreshHomeServer : LoginRootEvents + data class SetLogin(val login: String) : LoginRootEvents + data class SetPassword(val password: String) : LoginRootEvents + object Submit : LoginRootEvents +} diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt new file mode 100644 index 0000000000..096fb3939a --- /dev/null +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt @@ -0,0 +1,64 @@ +package io.element.android.x.features.login.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.lifecycle.subscribe +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.x.anvilannotations.ContributesNode +import io.element.android.x.architecture.presenterConnector +import io.element.android.x.di.AppScope + +@ContributesNode(AppScope::class) +class LoginRootNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: LoginRootPresenter, +) : Node(buildContext, plugins = plugins) { + + private val presenterConnector = presenterConnector(presenter) + + init { + lifecycle.subscribe( + onResume = { presenterConnector.emitEvent(LoginRootEvents.RefreshHomeServer) } + ) + } + + interface Callback : Plugin { + fun onChangeHomeServer() + } + + private fun onChangeHomeServer() { + plugins().forEach { it.onChangeHomeServer() } + } + + private fun onLoginChanged(login: String) { + presenterConnector.emitEvent(LoginRootEvents.SetLogin(login)) + } + + private fun onPasswordChanged(password: String) { + presenterConnector.emitEvent(LoginRootEvents.SetPassword(password)) + } + + private fun onSubmit() { + presenterConnector.emitEvent(LoginRootEvents.Submit) + } + + @Composable + override fun View(modifier: Modifier) { + val state by presenterConnector.stateFlow.collectAsState() + LoginRootScreen( + state = state, + onChangeServer = this::onChangeHomeServer, + onLoginChanged = this::onLoginChanged, + onPasswordChanged = this::onPasswordChanged, + onSubmitClicked = this::onSubmit + ) + } +} diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt new file mode 100644 index 0000000000..1d254c47d3 --- /dev/null +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt @@ -0,0 +1,69 @@ +package io.element.android.x.features.login.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.x.architecture.Presenter +import io.element.android.x.matrix.Matrix +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class LoginRootPresenter @Inject constructor(private val matrix: Matrix) : Presenter { + + @Composable + override fun present(events: Flow): LoginRootState { + val homeserver = rememberSaveable { + mutableStateOf(matrix.getHomeserverOrDefault()) + } + val loggedInState: MutableState = remember { + mutableStateOf(LoggedInState.NotLoggedIn) + } + val formState = rememberSaveable { + mutableStateOf(LoginFormState.Default) + } + + LaunchedEffect(Unit) { + events.collect { event -> + when (event) { + LoginRootEvents.RefreshHomeServer -> refreshHomeServer(homeserver) + is LoginRootEvents.SetLogin -> updateFormState(formState) { + copy(login = event.login) + } + is LoginRootEvents.SetPassword -> updateFormState(formState) { + copy(password = event.password) + } + LoginRootEvents.Submit -> submit(homeserver.value, formState.value, loggedInState) + } + } + } + return LoginRootState( + homeserver = homeserver.value, + loggedInState = loggedInState.value, + formState = formState.value + ) + } + + private fun CoroutineScope.submit(homeserver: String, formState: LoginFormState, loggedInState: MutableState) = launch { + loggedInState.value = LoggedInState.LoggingIn + try { + matrix.setHomeserver(homeserver) + val sessionId = matrix.login(formState.login.trim(), formState.password.trim()) + loggedInState.value = LoggedInState.LoggedIn(sessionId) + } catch (failure: Throwable) { + loggedInState.value = LoggedInState.ErrorLoggingIn(failure) + } + } + + private fun updateFormState(formState: MutableState, updateLambda: LoginFormState.() -> LoginFormState) { + formState.value = updateLambda(formState.value) + } + + private fun refreshHomeServer(homeserver: MutableState) { + homeserver.value = matrix.getHomeserverOrDefault() + } +} 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/root/LoginRootScreen.kt similarity index 80% rename from features/login/src/main/java/io/element/android/x/features/login/LoginScreen.kt rename to features/login/src/main/java/io/element/android/x/features/login/root/LoginRootScreen.kt index 19c5821a2e..4403c2c972 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/root/LoginRootScreen.kt @@ -1,6 +1,4 @@ -@file:OptIn(ExperimentalMaterial3Api::class) - -package io.element.android.x.features.login +package io.element.android.x.features.login.root import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -26,7 +24,6 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface 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 @@ -42,44 +39,15 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.compose.collectAsState -import com.airbnb.mvrx.compose.mavericksViewModel +import io.element.android.x.core.compose.textFieldState import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.features.login.error.loginError import io.element.android.x.matrix.core.SessionId -import timber.log.Timber - -@Composable -fun LoginScreen( - viewModel: LoginViewModel = mavericksViewModel(), - onChangeServer: () -> Unit = { }, - onLoginWithSuccess: (SessionId) -> Unit = { }, -) { - val state: LoginViewState by viewModel.collectAsState() - val formState: LoginFormState by viewModel.formState - LaunchedEffect(key1 = Unit) { - Timber.d("resume") - viewModel.onResume() - } - LoginContent( - state = state, - formState = formState, - onChangeServer = onChangeServer, - onLoginChanged = viewModel::onSetName, - onPasswordChanged = viewModel::onSetPassword, - onSubmitClicked = viewModel::onSubmit, - onLoginWithSuccess = onLoginWithSuccess - ) -} @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LoginContent( - state: LoginViewState, - formState: LoginFormState, +fun LoginRootScreen( + state: LoginRootState, modifier: Modifier = Modifier, onChangeServer: () -> Unit = {}, onLoginChanged: (String) -> Unit = {}, @@ -98,6 +66,9 @@ fun LoginContent( .imePadding() ) { val scrollState = rememberScrollState() + var loginFieldState by textFieldState(stateValue = state.formState.login) + var passwordFieldState by textFieldState(stateValue = state.formState.password) + Column( modifier = Modifier .verticalScroll( @@ -105,7 +76,7 @@ fun LoginContent( ) .padding(horizontal = 16.dp), ) { - val isError = state.loggedInSessionId is Fail + val isError = state.loggedInState is LoggedInState.ErrorLoggingIn // Title Text( text = "Welcome back", @@ -146,30 +117,36 @@ fun LoginContent( ) } OutlinedTextField( - value = formState.login, + value = loginFieldState, modifier = Modifier .fillMaxWidth() .padding(top = 60.dp), label = { Text(text = "Email or username") }, - onValueChange = onLoginChanged, + onValueChange = { + loginFieldState = it + onLoginChanged(it) + }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Email, imeAction = ImeAction.Next ), ) var passwordVisible by remember { mutableStateOf(false) } - if (state.loggedInSessionId is Loading) { + if (state.loggedInState is LoggedInState.LoggingIn) { // Ensure password is hidden when user submits the form passwordVisible = false } OutlinedTextField( - value = formState.password, + value = passwordFieldState, modifier = Modifier .fillMaxWidth() .padding(top = 24.dp), - onValueChange = onPasswordChanged, + onValueChange = { + passwordFieldState = it + onPasswordChanged(it) + }, label = { Text(text = "Password") }, @@ -193,9 +170,9 @@ fun LoginContent( onDone = { onSubmitClicked() } ), ) - if (state.loggedInSessionId is Fail) { + if (state.loggedInState is LoggedInState.ErrorLoggingIn) { Text( - text = loginError(state.formState, state.loggedInSessionId.error), + text = loginError(state.formState, state.loggedInState.failure), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(start = 16.dp) @@ -212,12 +189,12 @@ fun LoginContent( ) { Text(text = "Continue") } - when (val loggedInSessionId = state.loggedInSessionId) { - is Success -> onLoginWithSuccess(loggedInSessionId()) + when (val loggedInState = state.loggedInState) { + is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId) else -> Unit } } - if (state.loggedInSessionId is Loading) { + if (state.loggedInState is LoggedInState.LoggingIn) { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center) ) @@ -230,11 +207,10 @@ fun LoginContent( @Preview fun LoginContentPreview() { ElementXTheme(darkTheme = false) { - LoginContent( - state = LoginViewState( + LoginRootScreen( + state = LoginRootState( homeserver = "matrix.org", ), - formState = LoginFormState("", "") ) } } diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt new file mode 100644 index 0000000000..38b8f67af2 --- /dev/null +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt @@ -0,0 +1,32 @@ +package io.element.android.x.features.login.root + +import android.os.Parcelable +import io.element.android.x.matrix.core.SessionId +import kotlinx.parcelize.Parcelize + +data class LoginRootState( + val homeserver: String = "", + val loggedInState: LoggedInState = LoggedInState.NotLoggedIn, + val formState: LoginFormState = LoginFormState.Default, +) { + val submitEnabled = + formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState != LoggedInState.LoggingIn +} + +sealed interface LoggedInState { + object NotLoggedIn : LoggedInState + object LoggingIn : LoggedInState + data class ErrorLoggingIn(val failure: Throwable) : LoggedInState + data class LoggedIn(val sessionId: SessionId) : LoggedInState +} + +@Parcelize +data class LoginFormState( + val login: String, + val password: String +) : Parcelable { + + companion object { + val Default = LoginFormState("", "") + } +} diff --git a/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingScreen.kt b/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingScreen.kt index 55d3375a4a..a1907929d9 100644 --- a/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingScreen.kt +++ b/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingScreen.kt @@ -29,8 +29,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.airbnb.mvrx.compose.collectAsState -import com.airbnb.mvrx.compose.mavericksViewModel import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPagerIndicator @@ -39,25 +37,9 @@ import io.element.android.x.designsystem.components.VectorButton import kotlinx.coroutines.delay import kotlinx.coroutines.launch -@Composable -fun OnBoardingScreen( - viewModel: OnBoardingViewModel = mavericksViewModel(), - onSignUp: () -> Unit = { }, - onSignIn: () -> Unit = { }, -) { - val state: OnBoardingViewState by viewModel.collectAsState() - OnBoardingContent( - state, - onPageChanged = viewModel::onPageChanged, - onSignUp = onSignUp, - onSignIn = onSignIn, - ) -} - @OptIn(ExperimentalPagerApi::class) @Composable -fun OnBoardingContent( - state: OnBoardingViewState, +fun OnBoardingScreen( modifier: Modifier = Modifier, onPageChanged: (Int) -> Unit = {}, onSignUp: () -> Unit = {}, diff --git a/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewModel.kt b/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewModel.kt deleted file mode 100644 index b1f708b126..0000000000 --- a/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewModel.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.element.android.x.features.onboarding - -import com.airbnb.mvrx.MavericksViewModel - -class OnBoardingViewModel(initialState: OnBoardingViewState) : - MavericksViewModel(initialState) { - - fun onPageChanged(page: Int) { - setState { - copy( - currentPage = page, - ) - } - } -} diff --git a/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewState.kt b/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewState.kt deleted file mode 100644 index 0262bdd339..0000000000 --- a/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.element.android.x.features.onboarding - -import com.airbnb.mvrx.MavericksState - -data class OnBoardingViewState( - val currentPage: Int = 0, -) : MavericksState 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 7ed1951ce2..ddefc79cc1 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 @@ -6,6 +6,7 @@ import io.element.android.x.element.resources.R as ElementR class SplashCarouselStateFactory { fun create(): SplashCarouselState { val lightTheme = true + fun background(@DrawableRes lightDrawable: Int) = if (lightTheme) lightDrawable else R.drawable.bg_color_background diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt index 8a994fcd07..06b089b807 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -27,7 +27,7 @@ class RoomListNode @AssistedInject constructor( fun onRoomClicked(roomId: RoomId) } - private val connector by presenterConnector(presenter) + private val connector = presenterConnector(presenter) private fun updateFilter(filter: String) { connector.emitEvent(RoomListEvents.UpdateFilter(filter)) diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt new file mode 100644 index 0000000000..206386630d --- /dev/null +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt @@ -0,0 +1,31 @@ +package io.element.android.x.architecture + +import androidx.compose.runtime.MutableState + +sealed interface Async { + object Uninitialized : Async + data class Loading(val prevState: T? = null) : Async + data class Failure(val error: Throwable) : Async + data class Success(val state: T) : Async +} + +suspend fun (suspend () -> T).execute(state: MutableState>) { + try { + state.value = Async.Loading() + state.value = Async.Success(this()) + } catch (error: Throwable) { + state.value = Async.Failure(error) + } +} + +suspend fun (suspend () -> Result).executeResult(state: MutableState>) { + state.value = Async.Loading() + this().fold( + onSuccess = { + state.value = Async.Success(it) + }, + onFailure = { + state.value = Async.Failure(it) + } + ) +} diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/LifecyclePresenterConnector.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt similarity index 70% rename from libraries/architecture/src/main/java/io/element/android/x/architecture/LifecyclePresenterConnector.kt rename to libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt index dcca215f07..022a0a1558 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/LifecyclePresenterConnector.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt @@ -8,21 +8,22 @@ import app.cash.molecule.launchMolecule import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow -inline fun LifecycleOwner.presenterConnector(presenter: Presenter): Lazy> = lazy { +inline fun LifecycleOwner.presenterConnector(presenter: Presenter): LifecyclePresenterConnector = LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter) -} + class LifecyclePresenterConnector(lifecycleOwner: LifecycleOwner, presenter: Presenter) { private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main) - private val eventFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 64) + private val mutableEventFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 64) - val stateFlow: StateFlow = moleculeScope.launchMolecule(RecompositionClock.ContextClock) { - presenter.present(events = eventFlow) + val stateFlow: StateFlow = moleculeScope.launchMolecule(RecompositionClock.Immediate) { + presenter.present(events = mutableEventFlow) } fun emitEvent(event: Event) { - eventFlow.tryEmit(event) + mutableEventFlow.tryEmit(event) } } From 7edfcac62b21362ae3427f12163e5739d3ca7092 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 9 Jan 2023 19:27:28 +0100 Subject: [PATCH 13/30] Migrate Preferences to new architecture --- .../android/x/node/LoggedInFlowNode.kt | 11 ++++ features/logout/build.gradle.kts | 1 - .../features/logout/LogoutPreferenceEvents.kt | 5 ++ .../logout/LogoutPreferencePresenter.kt | 41 ++++++++++++ ...outScreen.kt => LogoutPreferenceScreen.kt} | 22 +++---- ...tViewState.kt => LogoutPreferenceState.kt} | 10 ++- .../x/features/logout/LogoutViewModel.kt | 46 ------------- features/preferences/build.gradle.kts | 1 + .../preferences/PreferencesFlowNode.kt | 41 ++++++++++++ .../features/preferences/PreferencesScreen.kt | 66 ------------------- .../preferences/root/PreferencesRootEvents.kt | 7 ++ .../preferences/root/PreferencesRootNode.kt | 48 ++++++++++++++ .../root/PreferencesRootPresenter.kt | 42 ++++++++++++ .../preferences/root/PreferencesRootState.kt | 12 ++++ .../preferences/root/PreferencesRootView.kt | 56 ++++++++++++++++ .../preferences/user/UserPreferences.kt | 14 ++-- .../preferences/RageshakePreferencesEvents.kt | 6 ++ .../RageshakePreferencesPresenter.kt | 59 +++++++++++++++++ .../preferences/RageshakePreferencesState.kt | 7 ++ ...ategory.kt => RageshakePreferencesView.kt} | 33 +++------- .../x/features/roomlist/RoomListNode.kt | 7 +- libraries/architecture/build.gradle.kts | 2 + .../element/android/x/architecture/Async.kt | 11 +++- .../x/architecture/PresenterConnector.kt | 9 +-- .../x/architecture/SharedFlowHolder.kt | 12 ++++ .../preferences/PreferenceScreen.kt | 4 +- 26 files changed, 399 insertions(+), 174 deletions(-) create mode 100644 features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt create mode 100644 features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt rename features/logout/src/main/java/io/element/android/x/features/logout/{LogoutScreen.kt => LogoutPreferenceScreen.kt} (84%) rename features/logout/src/main/java/io/element/android/x/features/logout/{LogoutViewState.kt => LogoutPreferenceState.kt} (76%) delete mode 100644 features/logout/src/main/java/io/element/android/x/features/logout/LogoutViewModel.kt create mode 100644 features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt delete mode 100644 features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesScreen.kt create mode 100644 features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootEvents.kt create mode 100644 features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt create mode 100644 features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt create mode 100644 features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootState.kt create mode 100644 features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt create mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesEvents.kt create mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt create mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt rename features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/{RageshakePreferenceCategory.kt => RageshakePreferencesView.kt} (74%) create mode 100644 libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt 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 index a22c9d58c7..92fe956141 100644 --- a/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt @@ -13,6 +13,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import io.element.android.x.architecture.createNode import io.element.android.x.architecture.viewmodel.viewModelSupportNode import io.element.android.x.features.messages.MessagesScreen +import io.element.android.x.features.preferences.PreferencesFlowNode import io.element.android.x.features.roomlist.RoomListNode import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.core.SessionId @@ -34,6 +35,10 @@ class LoggedInFlowNode( override fun onRoomClicked(roomId: RoomId) { backstack.push(NavTarget.Messages(roomId)) } + + override fun onSettingsClicked() { + backstack.push(NavTarget.Settings) + } } sealed interface NavTarget : Parcelable { @@ -42,6 +47,9 @@ class LoggedInFlowNode( @Parcelize data class Messages(val roomId: RoomId) : NavTarget + + @Parcelize + object Settings : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -55,6 +63,9 @@ class LoggedInFlowNode( onBackPressed = { backstack.pop() } ) } + NavTarget.Settings -> { + PreferencesFlowNode(buildContext) + } } } diff --git a/features/logout/build.gradle.kts b/features/logout/build.gradle.kts index 4bef1392ec..c7a28ef2d7 100644 --- a/features/logout/build.gradle.kts +++ b/features/logout/build.gradle.kts @@ -39,7 +39,6 @@ dependencies { implementation(project(":libraries:matrix")) implementation(project(":libraries:designsystem")) implementation(project(":libraries:elementresources")) - implementation(libs.mavericks.compose) ksp(libs.showkase.processor) testImplementation(libs.test.junit) androidTestImplementation(libs.test.junitext) diff --git a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt new file mode 100644 index 0000000000..acf280422e --- /dev/null +++ b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt @@ -0,0 +1,5 @@ +package io.element.android.x.features.logout + +sealed interface LogoutPreferenceEvents { + object Logout: LogoutPreferenceEvents +} diff --git a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt new file mode 100644 index 0000000000..800aebf077 --- /dev/null +++ b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt @@ -0,0 +1,41 @@ +package io.element.android.x.features.logout + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import io.element.android.x.architecture.Async +import io.element.android.x.architecture.Presenter +import io.element.android.x.architecture.execute +import io.element.android.x.matrix.MatrixClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) : Presenter { + + @Composable + override fun present(events: Flow): LogoutPreferenceState { + val logoutAction: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } + LaunchedEffect(Unit) { + events.collect { event -> + when (event) { + LogoutPreferenceEvents.Logout -> logout(logoutAction) + } + } + } + return LogoutPreferenceState( + logoutAction = logoutAction.value + ) + } + + private fun CoroutineScope.logout(logoutAction: MutableState>) = launch { + suspend { + matrixClient.logout() + }.execute(logoutAction) + } +} diff --git a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutScreen.kt b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceScreen.kt similarity index 84% rename from features/logout/src/main/java/io/element/android/x/features/logout/LogoutScreen.kt rename to features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceScreen.kt index 029b2dd1d8..dc0a8acea0 100644 --- a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutScreen.kt +++ b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceScreen.kt @@ -19,15 +19,11 @@ package io.element.android.x.features.logout import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Logout import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.compose.collectAsState -import com.airbnb.mvrx.compose.mavericksViewModel +import io.element.android.x.architecture.Async import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.components.ProgressDialog import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog @@ -36,12 +32,12 @@ import io.element.android.x.designsystem.components.preferences.PreferenceText import io.element.android.x.element.resources.R as ElementR @Composable -fun LogoutPreference( - viewModel: LogoutViewModel = mavericksViewModel(), - onSuccessLogout: () -> Unit = { }, +fun LogoutPreferenceView( + state: LogoutPreferenceState, + onLogoutClicked: () -> Unit = {}, + onSuccessLogout: () -> Unit = {}, ) { - val state: LogoutViewState by viewModel.collectAsState() - if (state.logoutAction is Success) { + if (state.logoutAction is Async.Success) { onSuccessLogout() return } @@ -65,7 +61,7 @@ fun LogoutPreference( }, onSubmitClicked = { openDialog.value = false - viewModel.logout() + onLogoutClicked() }, onDismiss = { openDialog.value = false @@ -73,7 +69,7 @@ fun LogoutPreference( ) } - if (state.logoutAction is Loading) { + if (state.logoutAction is Async.Loading) { ProgressDialog(text = "Login out...") } } @@ -95,6 +91,6 @@ fun LogoutPreferenceContent( @Preview fun LogoutContentPreview() { ElementXTheme(darkTheme = false) { - LogoutPreference() + LogoutPreferenceView(LogoutPreferenceState()) } } diff --git a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutViewState.kt b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceState.kt similarity index 76% rename from features/logout/src/main/java/io/element/android/x/features/logout/LogoutViewState.kt rename to features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceState.kt index e72442af05..b7b08578bd 100644 --- a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutViewState.kt +++ b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceState.kt @@ -16,10 +16,8 @@ package io.element.android.x.features.logout -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MavericksState -import com.airbnb.mvrx.Uninitialized +import io.element.android.x.architecture.Async -data class LogoutViewState( - val logoutAction: Async = Uninitialized, -) : MavericksState +data class LogoutPreferenceState( + val logoutAction: Async = Async.Uninitialized, +) diff --git a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutViewModel.kt b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutViewModel.kt deleted file mode 100644 index 32a4ad9bce..0000000000 --- a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutViewModel.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.features.logout - -import com.airbnb.mvrx.MavericksViewModel -import com.airbnb.mvrx.MavericksViewModelFactory -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory -import io.element.android.x.di.SessionScope -import io.element.android.x.matrix.MatrixClient -import kotlinx.coroutines.launch - -@ContributesViewModel(SessionScope::class) -class LogoutViewModel @AssistedInject constructor( - private val client: MatrixClient, - @Assisted initialState: LogoutViewState -) : MavericksViewModel(initialState) { - - companion object : MavericksViewModelFactory by daggerMavericksViewModelFactory() - - fun logout() { - viewModelScope.launch { - suspend { - client.logout() - }.execute { - copy(logoutAction = it) - } - } - } -} diff --git a/features/preferences/build.gradle.kts b/features/preferences/build.gradle.kts index 4eacb51268..e8c4b9abb3 100644 --- a/features/preferences/build.gradle.kts +++ b/features/preferences/build.gradle.kts @@ -20,6 +20,7 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.ksp) alias(libs.plugins.anvil) + id("kotlin-parcelize") } android { diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt new file mode 100644 index 0000000000..10b6c141c1 --- /dev/null +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt @@ -0,0 +1,41 @@ +package io.element.android.x.features.preferences + +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 io.element.android.x.architecture.createNode +import io.element.android.x.features.preferences.root.PreferencesRootNode +import kotlinx.parcelize.Parcelize + +class PreferencesFlowNode( + 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 + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Root -> createNode(buildContext) + } + } + + @Composable + override fun View(modifier: Modifier) { + Children(navModel = backstack) + } +} diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesScreen.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesScreen.kt deleted file mode 100644 index 61f38f4a14..0000000000 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesScreen.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.features.preferences - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import io.element.android.x.designsystem.components.preferences.PreferenceScreen -import io.element.android.x.element.resources.R as ElementR -import io.element.android.x.features.logout.LogoutPreference -import io.element.android.x.features.preferences.user.UserPreferences -import io.element.android.x.features.rageshake.preferences.RageshakePreferences - -@Composable -fun PreferencesScreen( - onBackPressed: () -> Unit = {}, - onOpenRageShake: () -> Unit = {}, - onSuccessLogout: () -> Unit = {}, -) { - // TODO Hierarchy! - // Include pref from other modules - PreferencesContent( - onBackPressed = onBackPressed, - onOpenRageShake = onOpenRageShake, - onSuccessLogout = onSuccessLogout, - ) -} - -@Composable -fun PreferencesContent( - modifier: Modifier = Modifier, - onBackPressed: () -> Unit = {}, - onOpenRageShake: () -> Unit = {}, - onSuccessLogout: () -> Unit = {}, -) { - PreferenceScreen( - modifier = modifier, - onBackPressed = onBackPressed, - title = stringResource(id = ElementR.string.settings) - ) { - UserPreferences() - RageshakePreferences(onOpenRageShake = onOpenRageShake) - LogoutPreference(onSuccessLogout = onSuccessLogout) - } -} - -@Preview -@Composable -fun PreferencesContentPreview() { - PreferencesContent() -} diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootEvents.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootEvents.kt new file mode 100644 index 0000000000..80496c695b --- /dev/null +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootEvents.kt @@ -0,0 +1,7 @@ +package io.element.android.x.features.preferences.root + +sealed interface PreferencesRootEvents { + object Logout : PreferencesRootEvents + data class SetRageshakeSensitivity(val sensitivity: Float) : PreferencesRootEvents + data class SetRageshakeEnabled(val enabled: Boolean) : PreferencesRootEvents +} diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt new file mode 100644 index 0000000000..04dc1a6fb0 --- /dev/null +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt @@ -0,0 +1,48 @@ +package io.element.android.x.features.preferences.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.x.anvilannotations.ContributesNode +import io.element.android.x.architecture.presenterConnector +import io.element.android.x.di.SessionScope + +@ContributesNode(SessionScope::class) +class PreferencesRootNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: PreferencesRootPresenter, +) : Node(buildContext, plugins = plugins) { + + private val presenterConnector = presenterConnector(presenter) + + private fun onLogoutClicked() { + presenterConnector.emitEvent(PreferencesRootEvents.Logout) + } + + private fun onRageshakeEnabledChanged(isEnabled: Boolean) { + presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeEnabled(isEnabled)) + } + + private fun onRageshakeSensitivityChanged(sensitivity: Float) { + presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeSensitivity(sensitivity)) + } + + @Composable + override fun View(modifier: Modifier) { + val state by presenterConnector.stateFlow.collectAsState() + PreferencesRootView( + state = state, + onLogoutClicked = this::onLogoutClicked, + onBackPressed = this::navigateUp, + onRageshakeEnabledChanged = this::onRageshakeEnabledChanged, + onRageshakeSensitivityChanged = this::onRageshakeSensitivityChanged + ) + } +} diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt new file mode 100644 index 0000000000..8927b220ed --- /dev/null +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt @@ -0,0 +1,42 @@ +package io.element.android.x.features.preferences.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import io.element.android.x.architecture.Async +import io.element.android.x.architecture.Presenter +import io.element.android.x.architecture.SharedFlowHolder +import io.element.android.x.features.logout.LogoutPreferenceEvents +import io.element.android.x.features.logout.LogoutPreferencePresenter +import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents +import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class PreferencesRootPresenter @Inject constructor( + private val logoutPresenter: LogoutPreferencePresenter, + private val rageshakePresenter: RageshakePreferencesPresenter, +) : Presenter { + + private val logoutEventsFlow = SharedFlowHolder() + private val rageshakeEventsFlow = SharedFlowHolder() + + @Composable + override fun present(events: Flow): PreferencesRootState { + val logoutState = logoutPresenter.present(events = logoutEventsFlow.asSharedFlow()) + val rageshakeState = rageshakePresenter.present(events = rageshakeEventsFlow.asSharedFlow()) + LaunchedEffect(Unit) { + events.collect { event -> + when (event) { + PreferencesRootEvents.Logout -> logoutEventsFlow.emit(LogoutPreferenceEvents.Logout) + is PreferencesRootEvents.SetRageshakeEnabled -> rageshakeEventsFlow.emit(RageshakePreferencesEvents.SetIsEnabled(event.enabled)) + is PreferencesRootEvents.SetRageshakeSensitivity -> rageshakeEventsFlow.emit(RageshakePreferencesEvents.SetSensitivity(event.sensitivity)) + } + } + } + return PreferencesRootState( + logoutState = logoutState, + rageshakeState = rageshakeState, + myUser = Async.Uninitialized + ) + } +} diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootState.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootState.kt new file mode 100644 index 0000000000..e832582b45 --- /dev/null +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootState.kt @@ -0,0 +1,12 @@ +package io.element.android.x.features.preferences.root + +import io.element.android.x.architecture.Async +import io.element.android.x.features.logout.LogoutPreferenceState +import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState +import io.element.android.x.matrix.ui.model.MatrixUser + +data class PreferencesRootState( + val logoutState: LogoutPreferenceState, + val rageshakeState: RageshakePreferencesState, + val myUser: Async, +) diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt new file mode 100644 index 0000000000..30a116ce8d --- /dev/null +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt @@ -0,0 +1,56 @@ +package io.element.android.x.features.preferences.root + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.x.architecture.Async +import io.element.android.x.designsystem.components.preferences.PreferenceView +import io.element.android.x.element.resources.R +import io.element.android.x.features.logout.LogoutPreferenceState +import io.element.android.x.features.logout.LogoutPreferenceView +import io.element.android.x.features.preferences.user.UserPreferences +import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState +import io.element.android.x.features.rageshake.preferences.RageshakePreferencesView + +@Composable +fun PreferencesRootView( + state: PreferencesRootState, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, + onLogoutClicked: () -> Unit = {}, + onOpenRageShake: () -> Unit = {}, + onRageshakeEnabledChanged: (Boolean) -> Unit = {}, + onRageshakeSensitivityChanged: (Float) -> Unit = {}, +) { + // TODO Hierarchy! + // Include pref from other modules + PreferenceView( + modifier = modifier, + onBackPressed = onBackPressed, + title = stringResource(id = R.string.settings) + ) { + UserPreferences(state.myUser) + RageshakePreferencesView( + state = state.rageshakeState, + onOpenRageshake = onOpenRageShake, + onSensitivityChanged = onRageshakeSensitivityChanged, + onIsEnabledChanged = onRageshakeEnabledChanged, + ) + LogoutPreferenceView( + state = state.logoutState, + onLogoutClicked = onLogoutClicked, + ) + } +} + +@Preview +@Composable +fun PreferencesContentPreview() { + val state = PreferencesRootState( + logoutState = LogoutPreferenceState(), + rageshakeState = RageshakePreferencesState(), + myUser = Async.Uninitialized + ) + PreferencesRootView(state) +} diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/user/UserPreferences.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/user/UserPreferences.kt index e3a58c8c46..5422a3f3f5 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/user/UserPreferences.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/user/UserPreferences.kt @@ -19,26 +19,22 @@ package io.element.android.x.features.preferences.user import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.airbnb.mvrx.compose.collectAsState -import com.airbnb.mvrx.compose.mavericksViewModel +import io.element.android.x.architecture.Async import io.element.android.x.matrix.ui.components.MatrixUserHeader -import io.element.android.x.matrix.ui.viewmodels.user.UserViewModel -import io.element.android.x.matrix.ui.viewmodels.user.UserViewState +import io.element.android.x.matrix.ui.model.MatrixUser @Composable fun UserPreferences( + user: Async, modifier: Modifier = Modifier, - viewModel: UserViewModel = mavericksViewModel(), ) { - val user by viewModel.collectAsState(UserViewState::user) - when (user()) { + when (val userData = user.dataOrNull()) { null -> Spacer(modifier = modifier.height(1.dp)) else -> MatrixUserHeader( modifier = modifier, - matrixUser = user.invoke()!! + matrixUser = userData ) } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesEvents.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesEvents.kt new file mode 100644 index 0000000000..f3152086d1 --- /dev/null +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesEvents.kt @@ -0,0 +1,6 @@ +package io.element.android.x.features.rageshake.preferences + +sealed interface RageshakePreferencesEvents { + data class SetSensitivity(val sensitivity: Float) : RageshakePreferencesEvents + data class SetIsEnabled(val isEnabled: Boolean) : RageshakePreferencesEvents +} diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt new file mode 100644 index 0000000000..1c56b42198 --- /dev/null +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt @@ -0,0 +1,59 @@ +package io.element.android.x.features.rageshake.preferences + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.x.architecture.Presenter +import io.element.android.x.features.rageshake.rageshake.RageShake +import io.element.android.x.features.rageshake.rageshake.RageshakeDataStore +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class RageshakePreferencesPresenter @Inject constructor( + private val rageshake: RageShake, + private val rageshakeDataStore: RageshakeDataStore, + + ) : Presenter { + + @Composable + override fun present(events: Flow): RageshakePreferencesState { + val isSupported: MutableState = rememberSaveable { + mutableStateOf(rageshake.isAvailable()) + } + val isEnabled = rageshakeDataStore + .isEnabled() + .collectAsState(initial = false) + + val sensitivity = rageshakeDataStore + .sensitivity() + .collectAsState(initial = 0f) + + LaunchedEffect(Unit) { + events.collect { event -> + when (event) { + is RageshakePreferencesEvents.SetIsEnabled -> setIsEnabled(event.isEnabled) + is RageshakePreferencesEvents.SetSensitivity -> setSensitivity(event.sensitivity) + } + } + } + + return RageshakePreferencesState( + isEnabled = isEnabled.value, + isSupported = isSupported.value, + sensitivity = sensitivity.value + ) + } + + private fun CoroutineScope.setSensitivity(sensitivity: Float) = launch { + rageshakeDataStore.setSensitivity(sensitivity) + } + + private fun CoroutineScope.setIsEnabled(enabled: Boolean) = launch { + rageshakeDataStore.setIsEnabled(enabled) + } +} diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt new file mode 100644 index 0000000000..d58457f620 --- /dev/null +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt @@ -0,0 +1,7 @@ +package io.element.android.x.features.rageshake.preferences + +data class RageshakePreferencesState( + val isEnabled: Boolean = false, + val isSupported: Boolean = true, + val sensitivity: Float = 0.3f, +) diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferenceCategory.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt similarity index 74% rename from features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferenceCategory.kt rename to features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt index 050689ad44..7bd29c7580 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferenceCategory.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt @@ -20,42 +20,29 @@ import androidx.compose.foundation.layout.Column import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BugReport import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.airbnb.mvrx.compose.collectAsState -import com.airbnb.mvrx.compose.mavericksViewModel import io.element.android.x.designsystem.components.preferences.PreferenceCategory import io.element.android.x.designsystem.components.preferences.PreferenceSlide import io.element.android.x.designsystem.components.preferences.PreferenceSwitch import io.element.android.x.designsystem.components.preferences.PreferenceText import io.element.android.x.element.resources.R as ElementR -import io.element.android.x.features.rageshake.detection.RageshakeDetectionViewModel -import io.element.android.x.features.rageshake.detection.RageshakeDetectionViewState @Composable -fun RageshakePreferences( - onOpenRageShake: () -> Unit = {}, -) { - RageshakePreferencesContent( - onOpenRageShake = onOpenRageShake, - ) -} - -@Composable -fun RageshakePreferencesContent( +fun RageshakePreferencesView( + state: RageshakePreferencesState, modifier: Modifier = Modifier, - viewModel: RageshakeDetectionViewModel = mavericksViewModel(), - onOpenRageShake: () -> Unit = {}, + onOpenRageshake: () -> Unit = {}, + onIsEnabledChanged: (Boolean) -> Unit = {}, + onSensitivityChanged: (Float) -> Unit = {} ) { - val state: RageshakeDetectionViewState by viewModel.collectAsState() Column(modifier = modifier) { PreferenceCategory(title = stringResource(id = ElementR.string.send_bug_report)) { PreferenceText( title = stringResource(id = ElementR.string.send_bug_report), icon = Icons.Default.BugReport, - onClick = onOpenRageShake + onClick = onOpenRageshake ) } PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) { @@ -63,7 +50,7 @@ fun RageshakePreferencesContent( PreferenceSwitch( title = stringResource(id = ElementR.string.send_bug_report_rage_shake), isChecked = state.isEnabled, - onCheckedChange = viewModel::onEnableClicked + onCheckedChange = onIsEnabledChanged ) PreferenceSlide( title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold), @@ -71,7 +58,7 @@ fun RageshakePreferencesContent( value = state.sensitivity, enabled = state.isEnabled, steps = 3 /* 5 possible values - steps are in ]0, 1[ */, - onValueChange = viewModel::onSensitivityChange + onValueChange = onSensitivityChanged ) } else { PreferenceText(title = "Rageshaking is not supported by your device") @@ -82,6 +69,6 @@ fun RageshakePreferencesContent( @Composable @Preview -fun RageshakePreferencePreview() { - RageshakePreferences() +fun RageshakePreferencesPreview() { + RageshakePreferencesView(RageshakePreferencesState(isEnabled = true, isSupported = true, sensitivity = 0.5f)) } diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt index 66f17a2f64..41bccdcb7b 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -25,6 +25,7 @@ class RoomListNode @AssistedInject constructor( interface Callback : Plugin { fun onRoomClicked(roomId: RoomId) + fun onSettingsClicked() } private val connector = presenterConnector(presenter) @@ -45,6 +46,10 @@ class RoomListNode @AssistedInject constructor( plugins().forEach { it.onRoomClicked(roomId) } } + private fun onOpenSettings() { + plugins().forEach { it.onSettingsClicked() } + } + @Composable override fun View(modifier: Modifier) { val state by connector.stateFlow.collectAsState() @@ -53,7 +58,7 @@ class RoomListNode @AssistedInject constructor( onRoomClicked = this::onRoomClicked, onFilterChanged = this::updateFilter, onScrollOver = this::updateVisibleRange, - onOpenSettings = this::logout + onOpenSettings = this::onOpenSettings ) } } diff --git a/libraries/architecture/build.gradle.kts b/libraries/architecture/build.gradle.kts index 5ac492f37b..5efea85ebf 100644 --- a/libraries/architecture/build.gradle.kts +++ b/libraries/architecture/build.gradle.kts @@ -1,3 +1,5 @@ +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") plugins { id("io.element.android-compose-library") alias(libs.plugins.molecule) diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt index 206386630d..73c0a81e3a 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt @@ -5,8 +5,17 @@ import androidx.compose.runtime.MutableState sealed interface Async { object Uninitialized : Async data class Loading(val prevState: T? = null) : Async - data class Failure(val error: Throwable) : Async + data class Failure(val error: Throwable, val prevState: T? = null) : Async data class Success(val state: T) : Async + + fun dataOrNull(): T? { + return when (this) { + is Failure -> prevState + is Loading -> prevState + is Success -> state + Uninitialized -> null + } + } } suspend fun (suspend () -> T).execute(state: MutableState>) { diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt index 022a0a1558..d30b5047f0 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt @@ -6,24 +6,21 @@ import app.cash.molecule.AndroidUiDispatcher import app.cash.molecule.RecompositionClock import app.cash.molecule.launchMolecule import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow inline fun LifecycleOwner.presenterConnector(presenter: Presenter): LifecyclePresenterConnector = LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter) - class LifecyclePresenterConnector(lifecycleOwner: LifecycleOwner, presenter: Presenter) { private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main) - private val mutableEventFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 64) + private val eventFlow = SharedFlowHolder() val stateFlow: StateFlow = moleculeScope.launchMolecule(RecompositionClock.Immediate) { - presenter.present(events = mutableEventFlow) + presenter.present(events = eventFlow.asSharedFlow()) } fun emitEvent(event: Event) { - mutableEventFlow.tryEmit(event) + eventFlow.emit(event) } } diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt new file mode 100644 index 0000000000..7d41e261a5 --- /dev/null +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt @@ -0,0 +1,12 @@ +package io.element.android.x.architecture + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +class SharedFlowHolder(capacity: Int = 64) { + private val mutableFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = capacity) + + fun asSharedFlow() = mutableFlow.asSharedFlow() + + fun emit(data: Data) = mutableFlow.tryEmit(data) +} diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceScreen.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceScreen.kt index 6fca5f5504..8fd26193a3 100644 --- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceScreen.kt +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceScreen.kt @@ -45,7 +45,7 @@ import androidx.compose.ui.unit.sp @OptIn(ExperimentalMaterial3Api::class) @Composable -fun PreferenceScreen( +fun PreferenceView( title: String, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, @@ -113,7 +113,7 @@ fun PreferenceTopAppBar( @Composable @Preview(showBackground = false) fun PreferenceScreenPreview() { - PreferenceScreen( + PreferenceView( title = "Preference screen" ) { PreferenceCategoryPreview() From acc091ef5f068dbfb6d8fdc93dc7a7e294b28828 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 9 Jan 2023 20:39:58 +0100 Subject: [PATCH 14/30] Migrate BugReport and CrashDetection to new architecture --- .../io/element/android/x/node/RootFlowNode.kt | 5 - features/rageshake/build.gradle.kts | 1 + .../rageshake/bugreport/BugReportEvents.kt | 11 ++ .../rageshake/bugreport/BugReportPresenter.kt | 127 +++++++++++++ ...ugReportViewState.kt => BugReportState.kt} | 35 ++-- .../{BugReportScreen.kt => BugReportView.kt} | 79 +++----- .../rageshake/bugreport/BugReportViewModel.kt | 175 ------------------ .../crash/ui/CrashDetectionEvents.kt | 6 + .../crash/ui/CrashDetectionPresenter.kt | 38 ++++ .../crash/ui/CrashDetectionScreen.kt | 23 +-- ...ionViewState.kt => CrashDetectionState.kt} | 6 +- .../crash/ui/CrashDetectionViewModel.kt | 65 ------- 12 files changed, 242 insertions(+), 329 deletions(-) create mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportEvents.kt create mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt rename features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/{BugReportViewState.kt => BugReportState.kt} (60%) rename features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/{BugReportScreen.kt => BugReportView.kt} (81%) delete mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportViewModel.kt create mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionEvents.kt create mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt rename features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/{CrashDetectionViewState.kt => CrashDetectionState.kt} (88%) delete mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionViewModel.kt 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 index 376f88cdb2..aea7eb1a68 100644 --- a/app/src/main/java/io/element/android/x/node/RootFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/RootFlowNode.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -27,14 +26,10 @@ 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.newRoot -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.features.rageshake.bugreport.BugReportScreen -import io.element.android.x.features.rageshake.crash.ui.CrashDetectionScreen -import io.element.android.x.features.rageshake.detection.RageshakeDetectionScreen import io.element.android.x.getBrowserIntent import io.element.android.x.matrix.Matrix import io.element.android.x.matrix.core.SessionId diff --git a/features/rageshake/build.gradle.kts b/features/rageshake/build.gradle.kts index 653e0e55ef..2cd99b6dea 100644 --- a/features/rageshake/build.gradle.kts +++ b/features/rageshake/build.gradle.kts @@ -20,6 +20,7 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.ksp) alias(libs.plugins.anvil) + id("kotlin-parcelize") } android { diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportEvents.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportEvents.kt new file mode 100644 index 0000000000..05d150d12c --- /dev/null +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportEvents.kt @@ -0,0 +1,11 @@ +package io.element.android.x.features.rageshake.bugreport + +sealed interface BugReportEvents { + object SendBugReport : BugReportEvents + object ResetAll: BugReportEvents + data class SetDescription(val description: String): BugReportEvents + data class SetSendLog(val sendLog: Boolean): BugReportEvents + data class SetSendCrashLog(val sendCrashlog: Boolean): BugReportEvents + data class SetCanContact(val canContact: Boolean): BugReportEvents + data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvents +} diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt new file mode 100644 index 0000000000..a4cbf027d8 --- /dev/null +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt @@ -0,0 +1,127 @@ +package io.element.android.x.features.rageshake.bugreport + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.x.architecture.Async +import io.element.android.x.architecture.Presenter +import io.element.android.x.features.rageshake.crash.CrashDataStore +import io.element.android.x.features.rageshake.logs.VectorFileLogger +import io.element.android.x.features.rageshake.reporter.BugReporter +import io.element.android.x.features.rageshake.reporter.ReportType +import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class BugReportPresenter @Inject constructor( + private val bugReporter: BugReporter, + private val crashDataStore: CrashDataStore, + private val screenshotHolder: ScreenshotHolder, + private val appCoroutineScope: CoroutineScope, +) : Presenter { + + private class BugReporterUploadListener( + private val sendingProgress: MutableState, + private val sendingAction: MutableState> + ) : BugReporter.IMXBugReportListener { + override fun onUploadCancelled() { + sendingProgress.value = 0f + sendingAction.value = Async.Uninitialized + } + + override fun onUploadFailed(reason: String?) { + sendingProgress.value = 0f + sendingAction.value = Async.Failure(Exception(reason)) + } + + override fun onProgress(progress: Int) { + sendingProgress.value = progress.toFloat() / 100 + sendingAction.value = Async.Loading() + } + + override fun onUploadSucceed(reportUrl: String?) { + sendingProgress.value = 0f + sendingAction.value = Async.Success(Unit) + } + } + + @Composable + override fun present(events: Flow): BugReportState { + val crashInfo: String by crashDataStore + .crashInfo() + .collectAsState(initial = "") + + val sendingProgress = remember { + mutableStateOf(0f) + } + val sendingAction: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } + val formState: MutableState = rememberSaveable { + mutableStateOf(BugReportFormState.Default) + } + val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction) + val state = BugReportState( + hasCrashLogs = crashInfo.isNotEmpty(), + sendingProgress = sendingProgress.value, + sending = sendingAction.value + ) + LaunchedEffect(Unit) { + events.collect { event -> + when (event) { + BugReportEvents.SendBugReport -> appCoroutineScope.sendBugReport(state, uploadListener) + BugReportEvents.ResetAll -> appCoroutineScope.resetAll() + is BugReportEvents.SetDescription -> updateFormState(formState) { + copy(description = event.description) + } + is BugReportEvents.SetCanContact -> updateFormState(formState) { + copy(canContact = event.canContact) + } + is BugReportEvents.SetSendCrashLog -> updateFormState(formState) { + copy(sendCrashLogs = event.sendCrashlog) + } + is BugReportEvents.SetSendLog -> updateFormState(formState) { + copy(sendLogs = event.sendLog) + } + is BugReportEvents.SetSendScreenshot -> updateFormState(formState) { + copy(sendScreenshot = event.sendScreenshot) + } + } + } + } + return state + } + + private fun updateFormState(formState: MutableState, operation: BugReportFormState.() -> BugReportFormState) { + formState.value = operation(formState.value) + } + + private fun CoroutineScope.sendBugReport(state: BugReportState, listener: BugReporter.IMXBugReportListener) = launch { + bugReporter.sendBugReport( + coroutineScope = this, + reportType = ReportType.BUG_REPORT, + withDevicesLogs = state.formState.sendLogs, + withCrashLogs = state.hasCrashLogs && state.formState.sendCrashLogs, + withKeyRequestHistory = false, + withScreenshot = state.formState.sendScreenshot, + theBugDescription = state.formState.description, + serverVersion = "", + canContact = state.formState.canContact, + customFields = emptyMap(), + listener = listener + ) + } + + private fun CoroutineScope.resetAll() = launch { + screenshotHolder.reset() + crashDataStore.reset() + VectorFileLogger.getFromTimber().reset() + } +} diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportViewState.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportState.kt similarity index 60% rename from features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportViewState.kt rename to features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportState.kt index 813a25dec4..e32f0c3abc 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportViewState.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportState.kt @@ -16,30 +16,37 @@ package io.element.android.x.features.rageshake.bugreport -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MavericksState -import com.airbnb.mvrx.Uninitialized +import android.os.Parcelable +import io.element.android.x.architecture.Async +import kotlinx.parcelize.Parcelize -data class BugReportViewState( +data class BugReportState( val formState: BugReportFormState = BugReportFormState.Default, - val sendLogs: Boolean = true, val hasCrashLogs: Boolean = false, - val sendCrashLogs: Boolean = true, - val canContact: Boolean = false, - val sendScreenshot: Boolean = false, val screenshotUri: String? = null, val sendingProgress: Float = 0F, - val sending: Async = Uninitialized, -) : MavericksState { + val sending: Async = Async.Uninitialized, +) { val submitEnabled = - formState.description.length > 10 && sending !is Loading + formState.description.length > 10 && sending !is Async.Loading } +@Parcelize data class BugReportFormState( val description: String, -) { + val sendLogs: Boolean, + val sendCrashLogs: Boolean, + val canContact: Boolean, + val sendScreenshot: Boolean + +): Parcelable { companion object { - val Default = BugReportFormState("") + val Default = BugReportFormState( + description = "", + sendLogs = true, + sendCrashLogs = true, + canContact = false, + sendScreenshot = false + ) } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportScreen.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportView.kt similarity index 81% rename from features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportScreen.kt rename to features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportView.kt index cbd9ae8be4..0b5ab39365 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportScreen.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportView.kt @@ -37,6 +37,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -50,48 +51,17 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import coil.request.ImageRequest -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.compose.collectAsState -import com.airbnb.mvrx.compose.mavericksViewModel +import io.element.android.x.architecture.Async import io.element.android.x.core.compose.LogCompositions +import io.element.android.x.core.compose.textFieldState import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.components.LabelledCheckbox import io.element.android.x.designsystem.components.dialogs.ErrorDialog import io.element.android.x.element.resources.R as ElementR @Composable -fun BugReportScreen( - viewModel: BugReportViewModel = mavericksViewModel(), - onDone: () -> Unit = { }, -) { - val state: BugReportViewState by viewModel.collectAsState() - val formState: BugReportFormState by viewModel.formState - LogCompositions(tag = "Rageshake", msg = "Root") - if (state.sending is Success) { - onDone() - } - BugReportContent( - state = state, - formState = formState, - onDescriptionChanged = viewModel::onSetDescription, - onSetSendLog = viewModel::onSetSendLog, - onSetSendCrashLog = viewModel::onSetSendCrashLog, - onSetCanContact = viewModel::onSetCanContact, - onSetSendScreenshot = viewModel::onSetSendScreenshot, - onSubmit = viewModel::onSubmit, - onFailureDialogClosed = viewModel::onFailureDialogClosed, - onDone = onDone, - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun BugReportContent( - state: BugReportViewState, - formState: BugReportFormState, +fun BugReportView( + state: BugReportState, modifier: Modifier = Modifier, onDescriptionChanged: (String) -> Unit = {}, onSetSendLog: (Boolean) -> Unit = {}, @@ -102,6 +72,10 @@ fun BugReportContent( onFailureDialogClosed: () -> Unit = { }, onDone: () -> Unit = { }, ) { + LogCompositions(tag = "Rageshake", msg = "Root") + if (state.sending is Async.Success) { + onDone() + } Surface( modifier = modifier, color = MaterialTheme.colorScheme.background, @@ -120,8 +94,8 @@ fun BugReportContent( ) .padding(horizontal = 16.dp), ) { - val isError = state.sending is Fail - val isFormEnabled = state.sending !is Loading + val isError = state.sending is Async.Failure + val isFormEnabled = state.sending !is Async.Loading // Title Text( text = stringResource(id = ElementR.string.send_bug_report), @@ -140,11 +114,12 @@ fun BugReportContent( .padding(horizontal = 16.dp, vertical = 16.dp), fontSize = 16.sp, ) + var descriptionFieldState by textFieldState(stateValue = state.formState.description) Column( // modifier = Modifier.weight(1f), ) { OutlinedTextField( - value = formState.description, + value = descriptionFieldState, modifier = Modifier .fillMaxWidth() .padding(top = 16.dp), @@ -155,7 +130,10 @@ fun BugReportContent( supportingText = { Text(text = stringResource(id = ElementR.string.send_bug_report_description_in_english)) }, - onValueChange = onDescriptionChanged, + onValueChange = { + descriptionFieldState = it + onDescriptionChanged(it) + }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Next @@ -164,33 +142,33 @@ fun BugReportContent( ) } LabelledCheckbox( - checked = state.sendLogs, + checked = state.formState.sendLogs, onCheckedChange = onSetSendLog, enabled = isFormEnabled, text = stringResource(id = ElementR.string.send_bug_report_include_logs) ) if (state.hasCrashLogs) { LabelledCheckbox( - checked = state.sendCrashLogs, + checked = state.formState.sendCrashLogs, onCheckedChange = onSetSendCrashLog, enabled = isFormEnabled, text = stringResource(id = ElementR.string.send_bug_report_include_crash_logs) ) } LabelledCheckbox( - checked = state.canContact, + checked = state.formState.canContact, onCheckedChange = onSetCanContact, enabled = isFormEnabled, text = stringResource(id = ElementR.string.you_may_contact_me) ) if (state.screenshotUri != null) { LabelledCheckbox( - checked = state.sendScreenshot, + checked = state.formState.sendScreenshot, onCheckedChange = onSetSendScreenshot, enabled = isFormEnabled, text = stringResource(id = ElementR.string.send_bug_report_include_screenshot) ) - if (state.sendScreenshot) { + if (state.formState.sendScreenshot) { Box( modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center @@ -219,18 +197,18 @@ fun BugReportContent( } } when (state.sending) { - Uninitialized -> Unit - is Loading -> { + Async.Uninitialized -> Unit + is Async.Loading -> { CircularProgressIndicator( progress = state.sendingProgress, modifier = Modifier.align(Alignment.Center) ) } - is Fail -> ErrorDialog( + is Async.Failure -> ErrorDialog( content = state.sending.error.toString(), onDismiss = onFailureDialogClosed, ) - is Success -> onDone() + is Async.Success -> onDone() } } } @@ -240,9 +218,8 @@ fun BugReportContent( @Preview fun BugReportContentPreview() { ElementXTheme(darkTheme = false) { - BugReportContent( - state = BugReportViewState(), - formState = BugReportFormState.Default + BugReportView( + state = BugReportState(), ) } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportViewModel.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportViewModel.kt deleted file mode 100644 index 588dcffaa9..0000000000 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportViewModel.kt +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.features.rageshake.bugreport - -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.snapshotFlow -import androidx.core.net.toUri -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MavericksViewModel -import com.airbnb.mvrx.MavericksViewModelFactory -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory -import io.element.android.x.di.AppScope -import io.element.android.x.features.rageshake.crash.CrashDataStore -import io.element.android.x.features.rageshake.logs.VectorFileLogger -import io.element.android.x.features.rageshake.reporter.BugReporter -import io.element.android.x.features.rageshake.reporter.ReportType -import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch - -@ContributesViewModel(AppScope::class) -class BugReportViewModel @AssistedInject constructor( - @Assisted initialState: BugReportViewState, - private val bugReporter: BugReporter, - private val crashDataStore: CrashDataStore, - private val screenshotHolder: ScreenshotHolder, - private val appCoroutineScope: CoroutineScope -) : - MavericksViewModel(initialState) { - - companion object : - MavericksViewModelFactory by daggerMavericksViewModelFactory() - - var formState = mutableStateOf(BugReportFormState.Default) - private set - - init { - snapshotFlow { formState.value } - .onEach { - setState { copy(formState = it) } - }.launchIn(viewModelScope) - observerCrashDataStore() - setState { - copy( - screenshotUri = screenshotHolder.getFile()?.toUri()?.toString() - ) - } - } - - private fun observerCrashDataStore() { - viewModelScope.launch { - crashDataStore.crashInfo().collect { - setState { - copy( - hasCrashLogs = it.isNotEmpty() - ) - } - } - } - } - - private val listener: BugReporter.IMXBugReportListener = object : BugReporter.IMXBugReportListener { - override fun onUploadCancelled() { - setState { - copy( - sendingProgress = 0F, - sending = Uninitialized - ) - } - } - - override fun onUploadFailed(reason: String?) { - setState { - copy( - sendingProgress = 0F, - sending = Fail(Exception(reason)) - ) - } - } - - override fun onProgress(progress: Int) { - setState { - copy( - sendingProgress = progress.toFloat() / 100, - sending = Loading() - ) - } - } - - override fun onUploadSucceed(reportUrl: String?) { - setState { - copy( - sendingProgress = 1F, - sending = Success(Unit) - ) - } - } - } - - override fun onCleared() { - // Use appCoroutineScope because we don't want this coroutine to be cancelled - appCoroutineScope.launch(Dispatchers.IO) { - screenshotHolder.reset() - crashDataStore.reset() - VectorFileLogger.getFromTimber().reset() - } - super.onCleared() - } - - fun onSubmit() { - setState { - copy( - sendingProgress = 0F, - sending = Loading() - ) - } - withState { state -> - bugReporter.sendBugReport( - coroutineScope = viewModelScope, - reportType = ReportType.BUG_REPORT, - withDevicesLogs = state.sendLogs, - withCrashLogs = state.hasCrashLogs && state.sendCrashLogs, - withKeyRequestHistory = false, - withScreenshot = state.sendScreenshot, - theBugDescription = state.formState.description, - serverVersion = "", - canContact = state.canContact, - customFields = emptyMap(), - listener = listener - ) - } - } - - fun onFailureDialogClosed() { - setState { - copy( - sendingProgress = 0F, - sending = Uninitialized - ) - } - } - - fun onSetDescription(str: String) { - formState.value = formState.value.copy(description = str) - setState { copy(sending = Uninitialized) } - } - - fun onSetSendLog(value: Boolean) = setState { copy(sendLogs = value) } - fun onSetSendCrashLog(value: Boolean) = setState { copy(sendCrashLogs = value) } - fun onSetCanContact(value: Boolean) = setState { copy(canContact = value) } - fun onSetSendScreenshot(value: Boolean) = setState { copy(sendScreenshot = value) } -} diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionEvents.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionEvents.kt new file mode 100644 index 0000000000..e57a6de9bc --- /dev/null +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionEvents.kt @@ -0,0 +1,6 @@ +package io.element.android.x.features.rageshake.crash.ui + +sealed interface CrashDetectionEvents { + object ResetAll : CrashDetectionEvents + object ResetAppHasCrashed : CrashDetectionEvents +} diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt new file mode 100644 index 0000000000..ae22cec08c --- /dev/null +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt @@ -0,0 +1,38 @@ +package io.element.android.x.features.rageshake.crash.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import io.element.android.x.architecture.Presenter +import io.element.android.x.features.rageshake.crash.CrashDataStore +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class CrashDetectionPresenter @Inject constructor(private val crashDataStore: CrashDataStore) : Presenter { + + @Composable + override fun present(events: Flow): CrashDetectionState { + val crashDetected = crashDataStore.appHasCrashed().collectAsState(initial = false) + LaunchedEffect(Unit) { + events.collect { event -> + when (event) { + CrashDetectionEvents.ResetAll -> resetAll() + CrashDetectionEvents.ResetAppHasCrashed -> resetAppHasCrashed() + } + } + } + return CrashDetectionState( + crashDetected = crashDetected.value + ) + } + + private fun CoroutineScope.resetAppHasCrashed() = launch { + crashDataStore.resetAppHasCrashed() + } + + fun CoroutineScope.resetAll() = launch { + crashDataStore.reset() + } +} diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionScreen.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionScreen.kt index 7c5cc36acb..e7da8ba4ab 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionScreen.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionScreen.kt @@ -17,40 +17,33 @@ package io.element.android.x.features.rageshake.crash.ui import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.airbnb.mvrx.compose.collectAsState -import com.airbnb.mvrx.compose.mavericksViewModel import io.element.android.x.core.compose.LogCompositions import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog import io.element.android.x.element.resources.R as ElementR @Composable -fun CrashDetectionScreen( - viewModel: CrashDetectionViewModel = mavericksViewModel(), +fun CrashDetectionView( + state: CrashDetectionState, onOpenBugReport: () -> Unit = { }, + onPopupDismissed: () -> Unit = {} ) { - val state: CrashDetectionViewState by viewModel.collectAsState() LogCompositions(tag = "Crash", msg = "CrashDetectionScreen") - if (state.crashDetected) { CrashDetectionContent( state, - onYesClicked = { - viewModel.onYes() - onOpenBugReport() - }, - onNoClicked = viewModel::onPopupDismissed, - onDismiss = viewModel::onPopupDismissed, + onYesClicked = onOpenBugReport, + onNoClicked = onPopupDismissed, + onDismiss = onPopupDismissed, ) } } @Composable fun CrashDetectionContent( - state: CrashDetectionViewState, + state: CrashDetectionState, onNoClicked: () -> Unit = { }, onYesClicked: () -> Unit = { }, onDismiss: () -> Unit = { }, @@ -71,7 +64,7 @@ fun CrashDetectionContent( fun CrashDetectionContentPreview() { ElementXTheme { CrashDetectionContent( - state = CrashDetectionViewState() + state = CrashDetectionState() ) } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionViewState.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionState.kt similarity index 88% rename from features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionViewState.kt rename to features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionState.kt index e7a50645f9..1ce7142735 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionViewState.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionState.kt @@ -16,8 +16,6 @@ package io.element.android.x.features.rageshake.crash.ui -import com.airbnb.mvrx.MavericksState - -data class CrashDetectionViewState( +data class CrashDetectionState( val crashDetected: Boolean = false, -) : MavericksState +) diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionViewModel.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionViewModel.kt deleted file mode 100644 index cd387bfa1f..0000000000 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionViewModel.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.features.rageshake.crash.ui - -import com.airbnb.mvrx.MavericksViewModel -import com.airbnb.mvrx.MavericksViewModelFactory -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory -import io.element.android.x.di.AppScope -import io.element.android.x.features.rageshake.crash.CrashDataStore -import kotlinx.coroutines.launch - -@ContributesViewModel(AppScope::class) -class CrashDetectionViewModel @AssistedInject constructor( - @Assisted initialState: CrashDetectionViewState, - private val crashDataStore: CrashDataStore, -) : MavericksViewModel(initialState) { - - companion object : - MavericksViewModelFactory by daggerMavericksViewModelFactory() - - init { - observeDataStore() - } - - private fun observeDataStore() { - viewModelScope.launch { - crashDataStore.appHasCrashed().collect { appHasCrashed -> - setState { - copy( - crashDetected = appHasCrashed - ) - } - } - } - } - - fun onYes() { - viewModelScope.launch { - crashDataStore.resetAppHasCrashed() - } - } - - fun onPopupDismissed() { - viewModelScope.launch { - crashDataStore.reset() - } - } -} From 6d2e5edc82c68af6085e0b46de38ce657ed82c9c Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 10 Jan 2023 10:01:23 +0100 Subject: [PATCH 15/30] Migrate RageshakeDetectionView to new architecture --- .../io/element/android/x/di/AppComponent.kt | 2 +- .../detection/RageshakeDetectionEvents.kt | 11 + .../detection/RageshakeDetectionPresenter.kt | 106 ++++++++++ ...iewState.kt => RageshakeDetectionState.kt} | 10 +- ...ionScreen.kt => RageshakeDetectionView.kt} | 38 +--- .../detection/RageshakeDetectionViewModel.kt | 190 ------------------ 6 files changed, 133 insertions(+), 224 deletions(-) create mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt create mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt rename features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/{RageshakeDetectionViewState.kt => RageshakeDetectionState.kt} (79%) rename features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/{RageshakeDetectionScreen.kt => RageshakeDetectionView.kt} (72%) delete mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewModel.kt diff --git a/app/src/main/java/io/element/android/x/di/AppComponent.kt b/app/src/main/java/io/element/android/x/di/AppComponent.kt index 3320a88218..30f801e31d 100644 --- a/app/src/main/java/io/element/android/x/di/AppComponent.kt +++ b/app/src/main/java/io/element/android/x/di/AppComponent.kt @@ -25,7 +25,7 @@ import io.element.android.x.architecture.viewmodel.DaggerMavericksBindings @SingleIn(AppScope::class) @MergeComponent(AppScope::class) -interface AppComponent : DaggerMavericksBindings, NodeFactoriesBindings { +interface AppComponent : NodeFactoriesBindings { @Component.Factory interface Factory { diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt new file mode 100644 index 0000000000..e47c8040fa --- /dev/null +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt @@ -0,0 +1,11 @@ +package io.element.android.x.features.rageshake.detection + +import io.element.android.x.core.screenshot.ImageResult + +sealed interface RageshakeDetectionEvents { + object Dismiss: RageshakeDetectionEvents + object Disable : RageshakeDetectionEvents + object StartDetection : RageshakeDetectionEvents + object StopDetection : RageshakeDetectionEvents + data class ProcessScreenshot(val imageResult: ImageResult) : RageshakeDetectionEvents +} diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt new file mode 100644 index 0000000000..a9e14a476b --- /dev/null +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt @@ -0,0 +1,106 @@ +package io.element.android.x.features.rageshake.detection + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.x.architecture.Presenter +import io.element.android.x.architecture.SharedFlowHolder +import io.element.android.x.core.screenshot.ImageResult +import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents +import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter +import io.element.android.x.features.rageshake.rageshake.RageShake +import io.element.android.x.features.rageshake.rageshake.RageshakeDataStore +import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +class RageshakeDetectionPresenter @Inject constructor( + private val rageshakeDataStore: RageshakeDataStore, + private val screenshotHolder: ScreenshotHolder, + private val rageShake: RageShake, + private val preferencesPresenter: RageshakePreferencesPresenter, +) : Presenter { + + private val preferencesEventsFlow = SharedFlowHolder() + + @Composable + override fun present(events: Flow): RageshakeDetectionState { + val preferencesState = preferencesPresenter.present(events = preferencesEventsFlow.asSharedFlow()) + val isStarted = rememberSaveable { + mutableStateOf(false) + } + val takeScreenshot = rememberSaveable { + mutableStateOf(false) + } + val showDialog = rememberSaveable { + mutableStateOf(false) + } + val state = RageshakeDetectionState( + isStarted = isStarted.value, + takeScreenshot = takeScreenshot.value, + showDialog = showDialog.value, + preferenceState = preferencesState + ) + LaunchedEffect(Unit) { + events.collect { event -> + when (event) { + RageshakeDetectionEvents.Disable -> preferencesEventsFlow.emit(RageshakePreferencesEvents.SetIsEnabled(false)) + RageshakeDetectionEvents.StartDetection -> isStarted.value = true + RageshakeDetectionEvents.StopDetection -> isStarted.value = false + is RageshakeDetectionEvents.ProcessScreenshot -> processScreenshot(takeScreenshot, showDialog, event.imageResult) + RageshakeDetectionEvents.Dismiss -> showDialog.value = false + } + } + } + LaunchedEffect(preferencesState.sensitivity) { + rageShake.setSensitivity(preferencesState.sensitivity) + } + val shouldStart = remember { + derivedStateOf { + preferencesState.isEnabled && + preferencesState.isSupported && + isStarted.value && + !takeScreenshot.value && + !showDialog.value + } + } + LaunchedEffect(shouldStart) { + handleRageShake(shouldStart.value, state, takeScreenshot) + } + return state + } + + + private fun handleRageShake(start: Boolean, state: RageshakeDetectionState, takeScreenshot: MutableState) { + if (start) { + rageShake.start(state.preferenceState.sensitivity) + rageShake.interceptor = { + takeScreenshot.value = true + } + } else { + rageShake.stop() + rageShake.interceptor = null + } + } + + private fun CoroutineScope.processScreenshot(takeScreenshot: MutableState, showDialog: MutableState, imageResult: ImageResult) = launch { + screenshotHolder.reset() + when (imageResult) { + is ImageResult.Error -> { + Timber.e(imageResult.exception, "Unable to write screenshot") + } + is ImageResult.Success -> { + screenshotHolder.writeBitmap(imageResult.data) + } + } + takeScreenshot.value = false + showDialog.value = true + } +} diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewState.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt similarity index 79% rename from features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewState.kt rename to features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt index 4e426a079e..247c81a2dc 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewState.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt @@ -16,13 +16,11 @@ package io.element.android.x.features.rageshake.detection -import com.airbnb.mvrx.MavericksState +import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState -data class RageshakeDetectionViewState( +data class RageshakeDetectionState( val takeScreenshot: Boolean = false, val showDialog: Boolean = false, - val isEnabled: Boolean = true, val isStarted: Boolean = false, - val isSupported: Boolean = false, - val sensitivity: Float = 0.5f, -) : MavericksState + val preferenceState: RageshakePreferencesState = RageshakePreferencesState() +) diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionScreen.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt similarity index 72% rename from features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionScreen.kt rename to features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt index 576f4e7a92..02273df57e 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionScreen.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt @@ -18,16 +18,11 @@ package io.element.android.x.features.rageshake.detection import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.Lifecycle -import com.airbnb.mvrx.compose.collectAsState -import com.airbnb.mvrx.compose.mavericksViewModel import io.element.android.x.core.compose.LogCompositions -import io.element.android.x.core.compose.OnLifecycleEvent import io.element.android.x.core.hardware.vibrate import io.element.android.x.core.screenshot.ImageResult import io.element.android.x.core.screenshot.screenshot @@ -36,24 +31,18 @@ import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog import io.element.android.x.element.resources.R as ElementR @Composable -fun RageshakeDetectionScreen( - viewModel: RageshakeDetectionViewModel = mavericksViewModel(), +fun RageshakeDetectionView( + state: RageshakeDetectionState, onOpenBugReport: () -> Unit = { }, + onScreenshotTaken: (ImageResult) -> Unit, + onDisableClicked: () -> Unit, + onNoClicked: () -> Unit ) { - val state: RageshakeDetectionViewState by viewModel.collectAsState() LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen") val context = LocalContext.current - OnLifecycleEvent { _, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> viewModel.start() - Lifecycle.Event.ON_PAUSE -> viewModel.stop() - else -> Unit - } - } - when { state.takeScreenshot -> TakeScreenshot( - onScreenshotTaken = viewModel::onScreenshotTaken + onScreenshotTaken = onScreenshotTaken ) state.showDialog -> { LaunchedEffect(key1 = "RS_diag") { @@ -61,14 +50,9 @@ fun RageshakeDetectionScreen( } RageshakeDialogContent( state, - onNoClicked = viewModel::onNo, - onDisableClicked = { - viewModel.onEnableClicked(false) - }, - onYesClicked = { - onOpenBugReport() - viewModel.onYes() - } + onNoClicked = onNoClicked, + onDisableClicked = onDisableClicked, + onYesClicked = onOpenBugReport ) } } @@ -86,7 +70,7 @@ private fun TakeScreenshot( @Composable fun RageshakeDialogContent( - state: RageshakeDetectionViewState, + state: RageshakeDetectionState, onNoClicked: () -> Unit = { }, onDisableClicked: () -> Unit = { }, onYesClicked: () -> Unit = { }, @@ -108,7 +92,7 @@ fun RageshakeDialogContent( fun RageshakeDialogContentPreview() { ElementXTheme { RageshakeDialogContent( - state = RageshakeDetectionViewState() + state = RageshakeDetectionState() ) } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewModel.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewModel.kt deleted file mode 100644 index e3ccde755d..0000000000 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewModel.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.features.rageshake.detection - -import com.airbnb.mvrx.MavericksViewModel -import com.airbnb.mvrx.MavericksViewModelFactory -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory -import io.element.android.x.core.screenshot.ImageResult -import io.element.android.x.di.AppScope -import io.element.android.x.features.rageshake.rageshake.RageShake -import io.element.android.x.features.rageshake.rageshake.RageshakeDataStore -import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import timber.log.Timber - -@ContributesViewModel(AppScope::class) -class RageshakeDetectionViewModel @AssistedInject constructor( - @Assisted initialState: RageshakeDetectionViewState, - private val rageshakeDataStore: RageshakeDataStore, - private val screenshotHolder: ScreenshotHolder, - private val rageShake: RageShake, -) : MavericksViewModel(initialState) { - - companion object : - MavericksViewModelFactory by daggerMavericksViewModelFactory() - - init { - setState { - copy( - isSupported = rageShake.isAvailable() - ) - } - observeDataStore() - observeState() - } - - private fun observeDataStore() { - viewModelScope.launch { - rageshakeDataStore.isEnabled().collect { isEnabled -> - setState { - copy( - isEnabled = isEnabled - ) - } - } - } - viewModelScope.launch { - rageshakeDataStore.sensitivity().collect { sensitivity -> - setState { - copy( - sensitivity = sensitivity - ) - } - } - } - } - - private fun observeState() { - viewModelScope.launch { - stateFlow - .map { - it.isSupported && - it.isEnabled && - it.isStarted && - !it.takeScreenshot && - !it.showDialog - } - .distinctUntilChanged() - .collect(::handleRageShake) - } - viewModelScope.launch { - stateFlow - .map { - it.sensitivity - } - .distinctUntilChanged() - .collect { - rageShake.setSensitivity(it) - } - } - } - - private fun handleRageShake(shouldStart: Boolean) { - if (shouldStart) { - withState { - rageShake.start(it.sensitivity) - } - rageShake.interceptor = { - setState { - copy( - takeScreenshot = true - ) - } - } - } else { - rageShake.stop() - rageShake.interceptor = null - } - } - - fun onScreenshotTaken(imageResult: ImageResult) { - viewModelScope.launch(Dispatchers.IO) { - screenshotHolder.reset() - when (imageResult) { - is ImageResult.Error -> { - Timber.e(imageResult.exception, "Unable to write screenshot") - } - is ImageResult.Success -> { - screenshotHolder.writeBitmap(imageResult.data) - } - } - setState { - copy( - takeScreenshot = false, - showDialog = true, - ) - } - } - } - - fun start() { - setState { - copy(isStarted = true) - } - } - - private fun onPopupDismissed() { - setState { - copy( - showDialog = false - ) - } - } - - fun onNo() { - onPopupDismissed() - } - - fun onYes() { - onPopupDismissed() - } - - fun onEnableClicked(enabled: Boolean) { - viewModelScope.launch { - rageshakeDataStore.setIsEnabled(enabled) - } - if (!enabled) { - onPopupDismissed() - } - } - - fun onSensitivityChange(sensitivity: Float) { - viewModelScope.launch { - rageshakeDataStore.setSensitivity(sensitivity) - } - rageShake.setSensitivity(sensitivity) - } - - fun stop() { - setState { - copy(isStarted = false) - } - } - - override fun onCleared() { - super.onCleared() - stop() - handleRageShake(false) - } -} From 56e54bb172112f485ea42370fa72cb3252c29352 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 10 Jan 2023 21:18:16 +0100 Subject: [PATCH 16/30] Continue migrating BugReport/Rageshake/Crash screens --- .../java/io/element/android/x/MainActivity.kt | 3 +- .../io/element/android/x/di/AppBindings.kt | 2 + .../android/x/node/LoggedInFlowNode.kt | 3 +- .../io/element/android/x/node/RootFlowNode.kt | 113 +++++++++++------- .../io/element/android/x/root/RootEvents.kt | 14 +++ .../element/android/x/root/RootPresenter.kt | 62 ++++++++++ .../io/element/android/x/root/RootState.kt | 15 +++ .../io/element/android/x/root/RootView.kt | 55 +++++++++ .../preferences/PreferencesFlowNode.kt | 9 +- .../preferences/root/PreferencesRootNode.kt | 12 +- .../rageshake/bugreport/BugReportNode.kt | 75 ++++++++++++ .../rageshake/bugreport/BugReportPresenter.kt | 26 +++- .../rageshake/bugreport/BugReportState.kt | 3 +- .../crash/ui/CrashDetectionEvents.kt | 2 +- .../crash/ui/CrashDetectionPresenter.kt | 4 +- .../detection/RageshakeDetectionPresenter.kt | 26 ++-- .../detection/RageshakeDetectionState.kt | 2 + .../detection/RageshakeDetectionView.kt | 12 +- .../RageshakePreferencesPresenter.kt | 3 +- .../rageshake/screenshot/ScreenshotHolder.kt | 3 + .../element/android/x/architecture/Async.kt | 2 + 21 files changed, 366 insertions(+), 80 deletions(-) create mode 100644 app/src/main/java/io/element/android/x/root/RootEvents.kt create mode 100644 app/src/main/java/io/element/android/x/root/RootPresenter.kt create mode 100644 app/src/main/java/io/element/android/x/root/RootState.kt create mode 100644 app/src/main/java/io/element/android/x/root/RootView.kt create mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt 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 33eac75262..1fd827be85 100644 --- a/app/src/main/java/io/element/android/x/MainActivity.kt +++ b/app/src/main/java/io/element/android/x/MainActivity.kt @@ -49,7 +49,8 @@ class MainActivity : NodeComponentActivity() { buildContext = it, appComponentOwner = applicationContext as DaggerComponentOwner, matrix = appBindings.matrix(), - sessionComponentsOwner = appBindings.sessionComponentsOwner() + sessionComponentsOwner = appBindings.sessionComponentsOwner(), + rootPresenter = appBindings.rootPresenter() ) } } 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 f330346e80..28efc08ba8 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 @@ -19,11 +19,13 @@ package io.element.android.x.di import com.squareup.anvil.annotations.ContributesTo import io.element.android.x.matrix.Matrix import io.element.android.x.matrix.ui.MatrixUi +import io.element.android.x.root.RootPresenter import kotlinx.coroutines.CoroutineScope @ContributesTo(AppScope::class) interface AppBindings { fun coroutineScope(): CoroutineScope + fun rootPresenter(): RootPresenter fun matrix(): Matrix fun matrixUi(): MatrixUi fun sessionComponentsOwner(): SessionComponentsOwner 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 index 92fe956141..94fd29807b 100644 --- a/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt @@ -22,6 +22,7 @@ import kotlinx.parcelize.Parcelize class LoggedInFlowNode( buildContext: BuildContext, val sessionId: SessionId, + private val onOpenBugReport: () -> Unit, private val backstack: BackStack = BackStack( initialElement = NavTarget.RoomList, savedStateMap = buildContext.savedStateMap, @@ -64,7 +65,7 @@ class LoggedInFlowNode( ) } NavTarget.Settings -> { - PreferencesFlowNode(buildContext) + PreferencesFlowNode(buildContext, onOpenBugReport) } } } 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 index aea7eb1a68..78ae309341 100644 --- a/app/src/main/java/io/element/android/x/node/RootFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/RootFlowNode.kt @@ -5,17 +5,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.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 @@ -26,13 +21,19 @@ 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.newRoot -import io.element.android.x.BuildConfig -import io.element.android.x.component.ShowkaseButton +import com.bumble.appyx.navmodel.backstack.operation.pop +import com.bumble.appyx.navmodel.backstack.operation.push +import io.element.android.x.architecture.createNode +import io.element.android.x.architecture.presenterConnector import io.element.android.x.core.di.DaggerComponentOwner +import io.element.android.x.core.screenshot.ImageResult import io.element.android.x.di.SessionComponentsOwner -import io.element.android.x.getBrowserIntent +import io.element.android.x.features.rageshake.bugreport.BugReportNode import io.element.android.x.matrix.Matrix import io.element.android.x.matrix.core.SessionId +import io.element.android.x.root.RootEvents +import io.element.android.x.root.RootPresenter +import io.element.android.x.root.RootView import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -64,6 +65,7 @@ class RootFlowNode( private val appComponentOwner: DaggerComponentOwner, private val matrix: Matrix, private val sessionComponentsOwner: SessionComponentsOwner, + rootPresenter: RootPresenter ) : ParentNode( navModel = backstack, @@ -73,10 +75,20 @@ class RootFlowNode( DaggerComponentOwner by appComponentOwner { + private val presenterConnector = presenterConnector(rootPresenter) + init { Timber.v("Init") lifecycle.subscribe( onCreate = { Timber.v("OnCreate") }, + onResume = { + Timber.v("OnResume") + presenterConnector.emitEvent(RootEvents.StartRageshakeDetection) + }, + onPause = { + Timber.v("OnPause") + presenterConnector.emitEvent(RootEvents.StopRageshakeDetection) + }, onDestroy = { Timber.v("OnDestroy") } ) } @@ -85,7 +97,7 @@ class RootFlowNode( matrix.isLoggedIn() .distinctUntilChanged() .onEach { isLoggedIn -> - Timber.v("IsLoggedIn") + Timber.v("isLoggedIn=$isLoggedIn") if (isLoggedIn) { val matrixClient = matrix.restoreSession() if (matrixClient == null) { @@ -102,43 +114,50 @@ class RootFlowNode( .launchIn(lifecycleScope) } + private fun hideShowkaseButton() { + presenterConnector.emitEvent(RootEvents.HideShowkaseButton) + } + + private fun onOpenBugReport() { + presenterConnector.emitEvent(RootEvents.ResetAppHasCrashed) + backstack.push(NavTarget.BugReport) + } + + private fun onCrashDetectedDismissed() { + presenterConnector.emitEvent(RootEvents.ResetAllCrashData) + } + + private fun onDismissRageshake() { + presenterConnector.emitEvent(RootEvents.DismissRageshake) + } + + private fun onDisableRageshake() { + presenterConnector.emitEvent(RootEvents.DisableRageshake) + } + @Composable override fun View(modifier: Modifier) { - var isShowkaseButtonVisible by remember { mutableStateOf(BuildConfig.DEBUG) } - Box( - modifier = modifier - .fillMaxSize(), - contentAlignment = Alignment.TopCenter, + val state by presenterConnector.stateFlow.collectAsState() + RootView( + state = state, + onHideShowkaseClicked = this::hideShowkaseButton, + onOpenBugReport = this::onOpenBugReport, + onCrashDetectedDismissed = this::onCrashDetectedDismissed, + onDisableRageshake = this::onDisableRageshake, + onDismissRageshake = this::onDismissRageshake, + onScreenshotTaken = this::onScreenshotTaken ) { Children(navModel = backstack) - val context = LocalContext.current - ShowkaseButton( - isVisible = isShowkaseButtonVisible, - onCloseClicked = { isShowkaseButtonVisible = false }, - onClick = { startActivity(context, Showkase.getBrowserIntent(context), null) } - ) + } + } - /* - var isBugReportVisible by rememberSaveable { - mutableStateOf(false) - } - RageshakeDetectionScreen( - onOpenBugReport = { - isBugReportVisible = true - } - ) - CrashDetectionScreen( - onOpenBugReport = { - isBugReportVisible = true - } - ) - if (isBugReportVisible) { - // TODO Improve the navigation, when pressing back here, it closes the app. - BugReportScreen( - onDone = { isBugReportVisible = false } - ) - } - */ + private fun onScreenshotTaken(imageResult: ImageResult) { + presenterConnector.emitEvent(RootEvents.ProcessScreenshot(imageResult)) + } + + private val bugReportNodeCallback = object : BugReportNode.Callback { + override fun onBugReportSent() { + backstack.pop() } } @@ -151,12 +170,19 @@ class RootFlowNode( @Parcelize data class LoggedInFlow(val sessionId: SessionId) : NavTarget + + @Parcelize + object BugReport : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.LoggedInFlow -> { - LoggedInFlowNode(buildContext, navTarget.sessionId) + LoggedInFlowNode( + buildContext = buildContext, + sessionId = navTarget.sessionId, + onOpenBugReport = this::onOpenBugReport + ) } NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext) NavTarget.SplashScreen -> node(buildContext) { @@ -164,6 +190,7 @@ class RootFlowNode( CircularProgressIndicator() } } + NavTarget.BugReport -> createNode(buildContext, plugins = listOf(bugReportNodeCallback)) } } } diff --git a/app/src/main/java/io/element/android/x/root/RootEvents.kt b/app/src/main/java/io/element/android/x/root/RootEvents.kt new file mode 100644 index 0000000000..7a88722ce6 --- /dev/null +++ b/app/src/main/java/io/element/android/x/root/RootEvents.kt @@ -0,0 +1,14 @@ +package io.element.android.x.root + +import io.element.android.x.core.screenshot.ImageResult + +sealed interface RootEvents { + data class ProcessScreenshot(val imageResult: ImageResult) : RootEvents + object HideShowkaseButton: RootEvents + object ResetAllCrashData : RootEvents + object ResetAppHasCrashed: RootEvents + object DisableRageshake: RootEvents + object DismissRageshake: RootEvents + object StartRageshakeDetection: RootEvents + object StopRageshakeDetection: RootEvents +} diff --git a/app/src/main/java/io/element/android/x/root/RootPresenter.kt b/app/src/main/java/io/element/android/x/root/RootPresenter.kt new file mode 100644 index 0000000000..45a28f2ba4 --- /dev/null +++ b/app/src/main/java/io/element/android/x/root/RootPresenter.kt @@ -0,0 +1,62 @@ +package io.element.android.x.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.x.architecture.Presenter +import io.element.android.x.architecture.SharedFlowHolder +import io.element.android.x.features.rageshake.bugreport.BugReportEvents +import io.element.android.x.features.rageshake.bugreport.BugReportPresenter +import io.element.android.x.features.rageshake.crash.ui.CrashDetectionEvents +import io.element.android.x.features.rageshake.crash.ui.CrashDetectionPresenter +import io.element.android.x.features.rageshake.detection.RageshakeDetectionEvents +import io.element.android.x.features.rageshake.detection.RageshakeDetectionPresenter +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class RootPresenter @Inject constructor( + private val bugReportPresenter: BugReportPresenter, + private val crashDetectionPresenter: CrashDetectionPresenter, + private val rageshakeDetectionPresenter: RageshakeDetectionPresenter, +) : Presenter { + + private val rageshakeDetectionEventsFlow = SharedFlowHolder() + private val bugReporterEventsFlow = SharedFlowHolder() + private val crashDetectionEventsFlow = SharedFlowHolder() + + @Composable + override fun present(events: Flow): RootState { + val isBugReportVisible = rememberSaveable { + mutableStateOf(false) + } + val isShowkaseButtonVisible = rememberSaveable { + mutableStateOf(true) + } + val rageshakeDetectionState = rageshakeDetectionPresenter.present(events = rageshakeDetectionEventsFlow.asSharedFlow()) + val crashDetectionState = crashDetectionPresenter.present(events = crashDetectionEventsFlow.asSharedFlow()) + val bugReportState = bugReportPresenter.present(events = bugReporterEventsFlow.asSharedFlow()) + + LaunchedEffect(Unit) { + events.collect { event -> + when (event) { + RootEvents.HideShowkaseButton -> isShowkaseButtonVisible.value = false + RootEvents.ResetAllCrashData -> crashDetectionEventsFlow.emit(CrashDetectionEvents.ResetAllCrashData) + RootEvents.ResetAppHasCrashed -> crashDetectionEventsFlow.emit(CrashDetectionEvents.ResetAppHasCrashed) + RootEvents.DisableRageshake -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.Disable) + RootEvents.DismissRageshake -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.Dismiss) + RootEvents.StartRageshakeDetection -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.StartDetection) + RootEvents.StopRageshakeDetection -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.StopDetection) + is RootEvents.ProcessScreenshot -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.ProcessScreenshot(event.imageResult)) + } + } + } + return RootState( + isBugReportVisible = isBugReportVisible.value, + isShowkaseButtonVisible = isShowkaseButtonVisible.value, + rageshakeDetectionState = rageshakeDetectionState, + crashDetectionState = crashDetectionState, + bugReportState = bugReportState + ) + } +} diff --git a/app/src/main/java/io/element/android/x/root/RootState.kt b/app/src/main/java/io/element/android/x/root/RootState.kt new file mode 100644 index 0000000000..a146b93322 --- /dev/null +++ b/app/src/main/java/io/element/android/x/root/RootState.kt @@ -0,0 +1,15 @@ +package io.element.android.x.root + +import androidx.compose.runtime.Stable +import io.element.android.x.features.rageshake.bugreport.BugReportState +import io.element.android.x.features.rageshake.crash.ui.CrashDetectionState +import io.element.android.x.features.rageshake.detection.RageshakeDetectionState + +@Stable +data class RootState( + val isBugReportVisible: Boolean, + val isShowkaseButtonVisible: Boolean, + val rageshakeDetectionState: RageshakeDetectionState, + val crashDetectionState: CrashDetectionState, + val bugReportState: BugReportState +) diff --git a/app/src/main/java/io/element/android/x/root/RootView.kt b/app/src/main/java/io/element/android/x/root/RootView.kt new file mode 100644 index 0000000000..ff0effef57 --- /dev/null +++ b/app/src/main/java/io/element/android/x/root/RootView.kt @@ -0,0 +1,55 @@ +package io.element.android.x.root + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat +import com.airbnb.android.showkase.models.Showkase +import io.element.android.x.component.ShowkaseButton +import io.element.android.x.core.screenshot.ImageResult +import io.element.android.x.features.rageshake.crash.ui.CrashDetectionView +import io.element.android.x.features.rageshake.detection.RageshakeDetectionView +import io.element.android.x.getBrowserIntent + +@Composable +fun RootView( + state: RootState, + modifier: Modifier = Modifier, + onHideShowkaseClicked: () -> Unit = { }, + onOpenBugReport: () -> Unit = {}, + onCrashDetectedDismissed: () -> Unit = {}, + onDisableRageshake: () -> Unit = {}, + onDismissRageshake: () -> Unit = {}, + onScreenshotTaken: (ImageResult) -> Unit = {}, + children: @Composable BoxScope.() -> Unit, +) { + Box( + modifier = modifier + .fillMaxSize(), + contentAlignment = Alignment.TopCenter, + ) { + children() + val context = LocalContext.current + ShowkaseButton( + isVisible = state.isShowkaseButtonVisible, + onCloseClicked = onHideShowkaseClicked, + onClick = { ContextCompat.startActivity(context, Showkase.getBrowserIntent(context), null) } + ) + RageshakeDetectionView( + state = state.rageshakeDetectionState, + onOpenBugReport = onOpenBugReport, + onDisableClicked = onDisableRageshake, + onNoClicked = onDismissRageshake, + onScreenshotTaken = onScreenshotTaken + ) + CrashDetectionView( + state = state.crashDetectionState, + onOpenBugReport = onOpenBugReport, + onPopupDismissed = onCrashDetectedDismissed + ) + } +} diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt index 10b6c141c1..9a69294f3b 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt @@ -14,6 +14,7 @@ import kotlinx.parcelize.Parcelize class PreferencesFlowNode( buildContext: BuildContext, + private val onOpenBugReport: () -> Unit, private val backstack: BackStack = BackStack( initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap, @@ -23,6 +24,12 @@ class PreferencesFlowNode( buildContext = buildContext ) { + private val preferencesRootNodeCallback = object : PreferencesRootNode.Callback { + override fun onOpenBugReport() { + onOpenBugReport.invoke() + } + } + sealed interface NavTarget : Parcelable { @Parcelize object Root : NavTarget @@ -30,7 +37,7 @@ class PreferencesFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Root -> createNode(buildContext) + NavTarget.Root -> createNode(buildContext, plugins = listOf(preferencesRootNodeCallback)) } } diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt index 04dc1a6fb0..78db0459a2 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.x.anvilannotations.ContributesNode @@ -20,6 +21,10 @@ class PreferencesRootNode @AssistedInject constructor( private val presenter: PreferencesRootPresenter, ) : Node(buildContext, plugins = plugins) { + public interface Callback : Plugin { + fun onOpenBugReport() + } + private val presenterConnector = presenterConnector(presenter) private fun onLogoutClicked() { @@ -34,6 +39,10 @@ class PreferencesRootNode @AssistedInject constructor( presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeSensitivity(sensitivity)) } + private fun onOpenBugReport() { + plugins().forEach { it.onOpenBugReport() } + } + @Composable override fun View(modifier: Modifier) { val state by presenterConnector.stateFlow.collectAsState() @@ -42,7 +51,8 @@ class PreferencesRootNode @AssistedInject constructor( onLogoutClicked = this::onLogoutClicked, onBackPressed = this::navigateUp, onRageshakeEnabledChanged = this::onRageshakeEnabledChanged, - onRageshakeSensitivityChanged = this::onRageshakeSensitivityChanged + onRageshakeSensitivityChanged = this::onRageshakeSensitivityChanged, + onOpenRageShake = this::onOpenBugReport ) } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt new file mode 100644 index 0000000000..088179f939 --- /dev/null +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt @@ -0,0 +1,75 @@ +package io.element.android.x.features.rageshake.bugreport + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.x.anvilannotations.ContributesNode +import io.element.android.x.architecture.presenterConnector +import io.element.android.x.di.AppScope + +@ContributesNode(AppScope::class) +class BugReportNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenter: BugReportPresenter, +) : Node(buildContext, plugins = plugins) { + + private val presenterConnector = presenterConnector(presenter) + + interface Callback : Plugin { + fun onBugReportSent() + } + + @Composable + override fun View(modifier: Modifier) { + val state by presenterConnector.stateFlow.collectAsState() + BugReportView( + state = state, + modifier = modifier, + onDescriptionChanged = this::onDescriptionChanged, + onSetSendLog = this::onSetSendLog, + onSetSendCrashLog = this::onSetSendCrashLog, + onSetCanContact = this::onSetCanContact, + onSetSendScreenshot = this::onSetSendScreenshot, + onSubmit = this::onSubmit, + onDone = this::onDone + ) + } + + private fun onDone() { + presenterConnector.emitEvent(BugReportEvents.ResetAll) + plugins().forEach { it.onBugReportSent() } + } + + private fun onSubmit() { + presenterConnector.emitEvent(BugReportEvents.SendBugReport) + } + + private fun onSetSendLog(sendLog: Boolean) { + presenterConnector.emitEvent(BugReportEvents.SetSendLog(sendLog)) + } + + private fun onSetSendCrashLog(sendCrashLog: Boolean) { + presenterConnector.emitEvent(BugReportEvents.SetSendCrashLog(sendCrashLog)) + } + + private fun onSetSendScreenshot(sendScreenshot: Boolean) { + presenterConnector.emitEvent(BugReportEvents.SetSendScreenshot(sendScreenshot)) + } + + private fun onSetCanContact(canContact: Boolean) { + presenterConnector.emitEvent(BugReportEvents.SetCanContact(canContact)) + } + + private fun onDescriptionChanged(description: String) { + presenterConnector.emitEvent(BugReportEvents.SetDescription(description)) + } +} + diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt index a4cbf027d8..4127395ce5 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt @@ -4,10 +4,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.core.net.toUri import io.element.android.x.architecture.Async import io.element.android.x.architecture.Presenter import io.element.android.x.features.rageshake.crash.CrashDataStore @@ -31,6 +33,7 @@ class BugReportPresenter @Inject constructor( private val sendingProgress: MutableState, private val sendingAction: MutableState> ) : BugReporter.IMXBugReportListener { + override fun onUploadCancelled() { sendingProgress.value = 0f sendingAction.value = Async.Uninitialized @@ -54,6 +57,11 @@ class BugReportPresenter @Inject constructor( @Composable override fun present(events: Flow): BugReportState { + val screenshotUri = rememberSaveable { + mutableStateOf( + screenshotHolder.getFile()?.toUri()?.toString() + ) + } val crashInfo: String by crashDataStore .crashInfo() .collectAsState(initial = "") @@ -64,15 +72,21 @@ class BugReportPresenter @Inject constructor( val sendingAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } - val formState: MutableState = rememberSaveable { + val formState: MutableState = remember { mutableStateOf(BugReportFormState.Default) } val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction) - val state = BugReportState( - hasCrashLogs = crashInfo.isNotEmpty(), - sendingProgress = sendingProgress.value, - sending = sendingAction.value - ) + val state by remember { + derivedStateOf { + BugReportState( + hasCrashLogs = crashInfo.isNotEmpty(), + sendingProgress = sendingProgress.value, + sending = sendingAction.value, + formState = formState.value, + screenshotUri = screenshotUri.value + ) + } + } LaunchedEffect(Unit) { events.collect { event -> when (event) { diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportState.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportState.kt index e32f0c3abc..cc7fad74ca 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportState.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportState.kt @@ -38,8 +38,7 @@ data class BugReportFormState( val sendCrashLogs: Boolean, val canContact: Boolean, val sendScreenshot: Boolean - -): Parcelable { +) : Parcelable { companion object { val Default = BugReportFormState( description = "", diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionEvents.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionEvents.kt index e57a6de9bc..0017421c7f 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionEvents.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionEvents.kt @@ -1,6 +1,6 @@ package io.element.android.x.features.rageshake.crash.ui sealed interface CrashDetectionEvents { - object ResetAll : CrashDetectionEvents + object ResetAllCrashData : CrashDetectionEvents object ResetAppHasCrashed : CrashDetectionEvents } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt index ae22cec08c..716bc19bf5 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt @@ -18,7 +18,7 @@ class CrashDetectionPresenter @Inject constructor(private val crashDataStore: Cr LaunchedEffect(Unit) { events.collect { event -> when (event) { - CrashDetectionEvents.ResetAll -> resetAll() + CrashDetectionEvents.ResetAllCrashData -> resetAll() CrashDetectionEvents.ResetAppHasCrashed -> resetAppHasCrashed() } } @@ -32,7 +32,7 @@ class CrashDetectionPresenter @Inject constructor(private val crashDataStore: Cr crashDataStore.resetAppHasCrashed() } - fun CoroutineScope.resetAll() = launch { + private fun CoroutineScope.resetAll() = launch { crashDataStore.reset() } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt index a9e14a476b..c3bd6b9ca7 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt @@ -13,7 +13,6 @@ import io.element.android.x.core.screenshot.ImageResult import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter import io.element.android.x.features.rageshake.rageshake.RageShake -import io.element.android.x.features.rageshake.rageshake.RageshakeDataStore import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -22,7 +21,6 @@ import timber.log.Timber import javax.inject.Inject class RageshakeDetectionPresenter @Inject constructor( - private val rageshakeDataStore: RageshakeDataStore, private val screenshotHolder: ScreenshotHolder, private val rageShake: RageShake, private val preferencesPresenter: RageshakePreferencesPresenter, @@ -42,12 +40,14 @@ class RageshakeDetectionPresenter @Inject constructor( val showDialog = rememberSaveable { mutableStateOf(false) } - val state = RageshakeDetectionState( - isStarted = isStarted.value, - takeScreenshot = takeScreenshot.value, - showDialog = showDialog.value, - preferenceState = preferencesState - ) + val state = remember(preferencesState, isStarted.value, takeScreenshot.value, showDialog.value) { + RageshakeDetectionState( + isStarted = isStarted.value, + takeScreenshot = takeScreenshot.value, + showDialog = showDialog.value, + preferenceState = preferencesState + ) + } LaunchedEffect(Unit) { events.collect { event -> when (event) { @@ -62,22 +62,18 @@ class RageshakeDetectionPresenter @Inject constructor( LaunchedEffect(preferencesState.sensitivity) { rageShake.setSensitivity(preferencesState.sensitivity) } - val shouldStart = remember { - derivedStateOf { - preferencesState.isEnabled && + val shouldStart = preferencesState.isEnabled && preferencesState.isSupported && isStarted.value && !takeScreenshot.value && !showDialog.value - } - } + LaunchedEffect(shouldStart) { - handleRageShake(shouldStart.value, state, takeScreenshot) + handleRageShake(shouldStart, state, takeScreenshot) } return state } - private fun handleRageShake(start: Boolean, state: RageshakeDetectionState, takeScreenshot: MutableState) { if (start) { rageShake.start(state.preferenceState.sensitivity) diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt index 247c81a2dc..f1a3f656cd 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt @@ -16,8 +16,10 @@ package io.element.android.x.features.rageshake.detection +import androidx.compose.runtime.Stable import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState +@Stable data class RageshakeDetectionState( val takeScreenshot: Boolean = false, val showDialog: Boolean = false, diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt index 02273df57e..7de08d158c 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt @@ -34,9 +34,9 @@ import io.element.android.x.element.resources.R as ElementR fun RageshakeDetectionView( state: RageshakeDetectionState, onOpenBugReport: () -> Unit = { }, - onScreenshotTaken: (ImageResult) -> Unit, - onDisableClicked: () -> Unit, - onNoClicked: () -> Unit + onScreenshotTaken: (ImageResult) -> Unit = {}, + onDisableClicked: () -> Unit = {}, + onNoClicked: () -> Unit = {} ) { LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen") val context = LocalContext.current @@ -63,8 +63,10 @@ private fun TakeScreenshot( onScreenshotTaken: (ImageResult) -> Unit = {} ) { val view = LocalView.current - view.screenshot { - onScreenshotTaken(it) + LaunchedEffect(Unit) { + view.screenshot { + onScreenshotTaken(it) + } } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt index 1c56b42198..3012a6c32f 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt @@ -17,8 +17,7 @@ import javax.inject.Inject class RageshakePreferencesPresenter @Inject constructor( private val rageshake: RageShake, private val rageshakeDataStore: RageshakeDataStore, - - ) : Presenter { +) : Presenter { @Composable override fun present(events: Flow): RageshakePreferencesState { diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/screenshot/ScreenshotHolder.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/screenshot/ScreenshotHolder.kt index 1b028ffb2b..00f9d74e96 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/screenshot/ScreenshotHolder.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/screenshot/ScreenshotHolder.kt @@ -19,10 +19,13 @@ package io.element.android.x.features.rageshake.screenshot import android.content.Context import android.graphics.Bitmap import io.element.android.x.core.bitmap.writeBitmap +import io.element.android.x.di.AppScope import io.element.android.x.di.ApplicationContext +import io.element.android.x.di.SingleIn import java.io.File import javax.inject.Inject +@SingleIn(AppScope::class) class ScreenshotHolder @Inject constructor( @ApplicationContext private val context: Context, ) { diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt index 73c0a81e3a..1e63d127fb 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt @@ -1,7 +1,9 @@ package io.element.android.x.architecture import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +@Stable sealed interface Async { object Uninitialized : Async data class Loading(val prevState: T? = null) : Async From 1a0c9df1da6c68c1dc46cf72211bb073e76d317c Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Jan 2023 15:53:52 +0100 Subject: [PATCH 17/30] Use EventSink lambda in state instead of Flow in Presenter --- .../io/element/android/x/node/RootFlowNode.kt | 44 +---------- .../io/element/android/x/root/RootEvents.kt | 11 +-- .../element/android/x/root/RootPresenter.kt | 39 +++------ .../io/element/android/x/root/RootState.kt | 3 +- .../io/element/android/x/root/RootView.kt | 26 +++--- .../login/changeserver/ChangeServerNode.kt | 10 --- .../changeserver/ChangeServerPresenter.kt | 23 +++--- .../login/changeserver/ChangeServerState.kt | 1 + .../login/changeserver/ChangeServerView.kt | 9 +-- .../x/features/login/root/LoginRootNode.kt | 29 ++----- .../features/login/root/LoginRootPresenter.kt | 30 +++---- .../x/features/login/root/LoginRootScreen.kt | 12 ++- .../x/features/login/root/LoginRootState.kt | 1 + .../logout/LogoutPreferencePresenter.kt | 21 ++--- .../features/logout/LogoutPreferenceScreen.kt | 12 +-- .../features/logout/LogoutPreferenceState.kt | 1 + .../onboarding/OnBoardingViewModel.kt | 31 -------- .../onboarding/OnBoardingViewState.kt | 23 ------ .../preferences/root/PreferencesRootEvents.kt | 7 -- .../preferences/root/PreferencesRootNode.kt | 17 +--- .../root/PreferencesRootPresenter.kt | 28 ++----- .../preferences/root/PreferencesRootView.kt | 7 +- .../rageshake/bugreport/BugReportNode.kt | 31 -------- .../rageshake/bugreport/BugReportPresenter.kt | 79 +++++++++---------- .../rageshake/bugreport/BugReportState.kt | 1 + .../rageshake/bugreport/BugReportView.kt | 33 ++++---- .../crash/ui/CrashDetectionPresenter.kt | 23 +++--- .../crash/ui/CrashDetectionScreen.kt | 10 ++- .../rageshake/crash/ui/CrashDetectionState.kt | 1 + .../detection/RageshakeDetectionPresenter.kt | 50 ++++++------ .../detection/RageshakeDetectionState.kt | 3 +- .../detection/RageshakeDetectionView.kt | 28 ++++--- .../RageshakePreferencesPresenter.kt | 21 +++-- .../preferences/RageshakePreferencesState.kt | 1 + .../preferences/RageshakePreferencesView.kt | 16 +++- .../x/features/roomlist/RoomListNode.kt | 14 ---- .../x/features/roomlist/RoomListPresenter.kt | 36 ++++----- .../x/features/roomlist/RoomListView.kt | 16 +++- .../features/roomlist/model/RoomListEvents.kt | 1 - .../features/roomlist/model/RoomListState.kt | 1 + .../android/x/architecture/Presenter.kt | 4 +- .../x/architecture/PresenterConnector.kt | 11 +-- .../x/architecture/SharedFlowHolder.kt | 2 + 43 files changed, 277 insertions(+), 490 deletions(-) delete mode 100644 features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewModel.kt delete mode 100644 features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewState.kt delete mode 100644 features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootEvents.kt 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 index 78ae309341..115d6fb4de 100644 --- a/app/src/main/java/io/element/android/x/node/RootFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/RootFlowNode.kt @@ -25,8 +25,8 @@ import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import io.element.android.x.architecture.createNode import io.element.android.x.architecture.presenterConnector +import io.element.android.x.core.compose.OnLifecycleEvent import io.element.android.x.core.di.DaggerComponentOwner -import io.element.android.x.core.screenshot.ImageResult import io.element.android.x.di.SessionComponentsOwner import io.element.android.x.features.rageshake.bugreport.BugReportNode import io.element.android.x.matrix.Matrix @@ -77,22 +77,6 @@ class RootFlowNode( private val presenterConnector = presenterConnector(rootPresenter) - init { - Timber.v("Init") - lifecycle.subscribe( - onCreate = { Timber.v("OnCreate") }, - onResume = { - Timber.v("OnResume") - presenterConnector.emitEvent(RootEvents.StartRageshakeDetection) - }, - onPause = { - Timber.v("OnPause") - presenterConnector.emitEvent(RootEvents.StopRageshakeDetection) - }, - onDestroy = { Timber.v("OnDestroy") } - ) - } - init { matrix.isLoggedIn() .distinctUntilChanged() @@ -114,47 +98,21 @@ class RootFlowNode( .launchIn(lifecycleScope) } - private fun hideShowkaseButton() { - presenterConnector.emitEvent(RootEvents.HideShowkaseButton) - } - private fun onOpenBugReport() { - presenterConnector.emitEvent(RootEvents.ResetAppHasCrashed) backstack.push(NavTarget.BugReport) } - private fun onCrashDetectedDismissed() { - presenterConnector.emitEvent(RootEvents.ResetAllCrashData) - } - - private fun onDismissRageshake() { - presenterConnector.emitEvent(RootEvents.DismissRageshake) - } - - private fun onDisableRageshake() { - presenterConnector.emitEvent(RootEvents.DisableRageshake) - } - @Composable override fun View(modifier: Modifier) { val state by presenterConnector.stateFlow.collectAsState() RootView( state = state, - onHideShowkaseClicked = this::hideShowkaseButton, onOpenBugReport = this::onOpenBugReport, - onCrashDetectedDismissed = this::onCrashDetectedDismissed, - onDisableRageshake = this::onDisableRageshake, - onDismissRageshake = this::onDismissRageshake, - onScreenshotTaken = this::onScreenshotTaken ) { Children(navModel = backstack) } } - private fun onScreenshotTaken(imageResult: ImageResult) { - presenterConnector.emitEvent(RootEvents.ProcessScreenshot(imageResult)) - } - private val bugReportNodeCallback = object : BugReportNode.Callback { override fun onBugReportSent() { backstack.pop() diff --git a/app/src/main/java/io/element/android/x/root/RootEvents.kt b/app/src/main/java/io/element/android/x/root/RootEvents.kt index 7a88722ce6..174852ad2f 100644 --- a/app/src/main/java/io/element/android/x/root/RootEvents.kt +++ b/app/src/main/java/io/element/android/x/root/RootEvents.kt @@ -1,14 +1,5 @@ package io.element.android.x.root -import io.element.android.x.core.screenshot.ImageResult - sealed interface RootEvents { - data class ProcessScreenshot(val imageResult: ImageResult) : RootEvents - object HideShowkaseButton: RootEvents - object ResetAllCrashData : RootEvents - object ResetAppHasCrashed: RootEvents - object DisableRageshake: RootEvents - object DismissRageshake: RootEvents - object StartRageshakeDetection: RootEvents - object StopRageshakeDetection: RootEvents + object HideShowkaseButton : RootEvents } diff --git a/app/src/main/java/io/element/android/x/root/RootPresenter.kt b/app/src/main/java/io/element/android/x/root/RootPresenter.kt index 45a28f2ba4..3563fafb4f 100644 --- a/app/src/main/java/io/element/android/x/root/RootPresenter.kt +++ b/app/src/main/java/io/element/android/x/root/RootPresenter.kt @@ -1,62 +1,45 @@ package io.element.android.x.root import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.x.architecture.Presenter -import io.element.android.x.architecture.SharedFlowHolder -import io.element.android.x.features.rageshake.bugreport.BugReportEvents import io.element.android.x.features.rageshake.bugreport.BugReportPresenter -import io.element.android.x.features.rageshake.crash.ui.CrashDetectionEvents import io.element.android.x.features.rageshake.crash.ui.CrashDetectionPresenter -import io.element.android.x.features.rageshake.detection.RageshakeDetectionEvents import io.element.android.x.features.rageshake.detection.RageshakeDetectionPresenter -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class RootPresenter @Inject constructor( private val bugReportPresenter: BugReportPresenter, private val crashDetectionPresenter: CrashDetectionPresenter, private val rageshakeDetectionPresenter: RageshakeDetectionPresenter, -) : Presenter { - - private val rageshakeDetectionEventsFlow = SharedFlowHolder() - private val bugReporterEventsFlow = SharedFlowHolder() - private val crashDetectionEventsFlow = SharedFlowHolder() +) : Presenter { @Composable - override fun present(events: Flow): RootState { + override fun present(): RootState { val isBugReportVisible = rememberSaveable { mutableStateOf(false) } val isShowkaseButtonVisible = rememberSaveable { mutableStateOf(true) } - val rageshakeDetectionState = rageshakeDetectionPresenter.present(events = rageshakeDetectionEventsFlow.asSharedFlow()) - val crashDetectionState = crashDetectionPresenter.present(events = crashDetectionEventsFlow.asSharedFlow()) - val bugReportState = bugReportPresenter.present(events = bugReporterEventsFlow.asSharedFlow()) + val rageshakeDetectionState = rageshakeDetectionPresenter.present() + val crashDetectionState = crashDetectionPresenter.present() + val bugReportState = bugReportPresenter.present() - LaunchedEffect(Unit) { - events.collect { event -> - when (event) { - RootEvents.HideShowkaseButton -> isShowkaseButtonVisible.value = false - RootEvents.ResetAllCrashData -> crashDetectionEventsFlow.emit(CrashDetectionEvents.ResetAllCrashData) - RootEvents.ResetAppHasCrashed -> crashDetectionEventsFlow.emit(CrashDetectionEvents.ResetAppHasCrashed) - RootEvents.DisableRageshake -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.Disable) - RootEvents.DismissRageshake -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.Dismiss) - RootEvents.StartRageshakeDetection -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.StartDetection) - RootEvents.StopRageshakeDetection -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.StopDetection) - is RootEvents.ProcessScreenshot -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.ProcessScreenshot(event.imageResult)) - } + fun handleEvent(event: RootEvents) { + when (event) { + RootEvents.HideShowkaseButton -> isShowkaseButtonVisible.value = false } } + return RootState( isBugReportVisible = isBugReportVisible.value, isShowkaseButtonVisible = isShowkaseButtonVisible.value, rageshakeDetectionState = rageshakeDetectionState, crashDetectionState = crashDetectionState, - bugReportState = bugReportState + bugReportState = bugReportState, + eventSink = ::handleEvent ) } } diff --git a/app/src/main/java/io/element/android/x/root/RootState.kt b/app/src/main/java/io/element/android/x/root/RootState.kt index a146b93322..cfcbb29499 100644 --- a/app/src/main/java/io/element/android/x/root/RootState.kt +++ b/app/src/main/java/io/element/android/x/root/RootState.kt @@ -11,5 +11,6 @@ data class RootState( val isShowkaseButtonVisible: Boolean, val rageshakeDetectionState: RageshakeDetectionState, val crashDetectionState: CrashDetectionState, - val bugReportState: BugReportState + val bugReportState: BugReportState, + val eventSink: (RootEvents) -> Unit ) diff --git a/app/src/main/java/io/element/android/x/root/RootView.kt b/app/src/main/java/io/element/android/x/root/RootView.kt index ff0effef57..fb0ac5c296 100644 --- a/app/src/main/java/io/element/android/x/root/RootView.kt +++ b/app/src/main/java/io/element/android/x/root/RootView.kt @@ -10,8 +10,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat import com.airbnb.android.showkase.models.Showkase import io.element.android.x.component.ShowkaseButton -import io.element.android.x.core.screenshot.ImageResult +import io.element.android.x.features.rageshake.crash.ui.CrashDetectionEvents import io.element.android.x.features.rageshake.crash.ui.CrashDetectionView +import io.element.android.x.features.rageshake.detection.RageshakeDetectionEvents import io.element.android.x.features.rageshake.detection.RageshakeDetectionView import io.element.android.x.getBrowserIntent @@ -19,12 +20,7 @@ import io.element.android.x.getBrowserIntent fun RootView( state: RootState, modifier: Modifier = Modifier, - onHideShowkaseClicked: () -> Unit = { }, onOpenBugReport: () -> Unit = {}, - onCrashDetectedDismissed: () -> Unit = {}, - onDisableRageshake: () -> Unit = {}, - onDismissRageshake: () -> Unit = {}, - onScreenshotTaken: (ImageResult) -> Unit = {}, children: @Composable BoxScope.() -> Unit, ) { Box( @@ -33,23 +29,27 @@ fun RootView( contentAlignment = Alignment.TopCenter, ) { children() + val eventSink = state.eventSink val context = LocalContext.current + + fun onOpenBugReport() { + state.crashDetectionState.eventSink(CrashDetectionEvents.ResetAppHasCrashed) + state.rageshakeDetectionState.eventSink(RageshakeDetectionEvents.Dismiss) + onOpenBugReport.invoke() + } + ShowkaseButton( isVisible = state.isShowkaseButtonVisible, - onCloseClicked = onHideShowkaseClicked, + onCloseClicked = { eventSink(RootEvents.HideShowkaseButton) }, onClick = { ContextCompat.startActivity(context, Showkase.getBrowserIntent(context), null) } ) RageshakeDetectionView( state = state.rageshakeDetectionState, - onOpenBugReport = onOpenBugReport, - onDisableClicked = onDisableRageshake, - onNoClicked = onDismissRageshake, - onScreenshotTaken = onScreenshotTaken + onOpenBugReport = ::onOpenBugReport, ) CrashDetectionView( state = state.crashDetectionState, - onOpenBugReport = onOpenBugReport, - onPopupDismissed = onCrashDetectedDismissed + onOpenBugReport = ::onOpenBugReport, ) } } diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt index f9f0bc4bbd..a6dc59a703 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt @@ -22,14 +22,6 @@ class ChangeServerNode @AssistedInject constructor( private val presenterConnector = presenterConnector(presenter) - private fun onChangeServer(server: String) { - presenterConnector.emitEvent(ChangeServerEvents.SetServer(server)) - } - - private fun onSubmit() { - presenterConnector.emitEvent(ChangeServerEvents.Submit) - } - private fun onSuccess() { navigateUp() } @@ -39,8 +31,6 @@ class ChangeServerNode @AssistedInject constructor( val state by presenterConnector.stateFlow.collectAsState() ChangeServerView( state = state, - onChangeServer = this::onChangeServer, - onChangeServerSubmit = this::onSubmit, onChangeServerSuccess = this::onSuccess, ) } diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt index c5d9891c7c..84bb601259 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt @@ -1,41 +1,42 @@ package io.element.android.x.features.login.changeserver import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.x.architecture.Async import io.element.android.x.architecture.Presenter import io.element.android.x.architecture.execute import io.element.android.x.matrix.Matrix import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import javax.inject.Inject -class ChangeServerPresenter @Inject constructor(private val matrix: Matrix) : Presenter { +class ChangeServerPresenter @Inject constructor(private val matrix: Matrix) : Presenter { @Composable - override fun present(events: Flow): ChangeServerState { + override fun present(): ChangeServerState { + val localCoroutineScope = rememberCoroutineScope() val homeserver = rememberSaveable { mutableStateOf(matrix.getHomeserverOrDefault()) } val changeServerAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } - LaunchedEffect(Unit) { - events.collect { event -> - when (event) { - is ChangeServerEvents.SetServer -> homeserver.value = event.server - ChangeServerEvents.Submit -> submit(homeserver.value, changeServerAction) - } + + fun handleEvents(event: ChangeServerEvents) { + when (event) { + is ChangeServerEvents.SetServer -> homeserver.value = event.server + ChangeServerEvents.Submit -> localCoroutineScope.submit(homeserver.value, changeServerAction) } } + return ChangeServerState( homeserver = homeserver.value, - changeServerAction = changeServerAction.value + changeServerAction = changeServerAction.value, + eventSink = ::handleEvents ) } diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt index dabd7a09bf..1cf98d788f 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt @@ -5,6 +5,7 @@ import io.element.android.x.architecture.Async data class ChangeServerState( val homeserver: String = "", val changeServerAction: Async = Async.Uninitialized, + val eventSink: (ChangeServerEvents) -> Unit = {}, ) { val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Async.Loading } diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt index 3bcf580c28..e08fb73e52 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt @@ -64,14 +64,13 @@ import io.element.android.x.features.login.error.changeServerError fun ChangeServerView( state: ChangeServerState, modifier: Modifier = Modifier, - onChangeServer: (String) -> Unit = {}, - onChangeServerSubmit: () -> Unit = {}, onChangeServerSuccess: () -> Unit = {}, ) { Surface( modifier = modifier, color = MaterialTheme.colorScheme.background, ) { + val eventSink = state.eventSink val scrollState = rememberScrollState() Box( modifier = Modifier @@ -135,7 +134,7 @@ fun ChangeServerView( .padding(top = 200.dp), onValueChange = { homeserverFieldState = it - onChangeServer(it) + eventSink(ChangeServerEvents.SetServer(it)) }, label = { Text(text = "Server") @@ -146,7 +145,7 @@ fun ChangeServerView( imeAction = ImeAction.Done, ), keyboardActions = KeyboardActions( - onDone = { onChangeServerSubmit() } + onDone = { eventSink(ChangeServerEvents.Submit) } ) ) if (state.changeServerAction is Async.Failure) { @@ -161,7 +160,7 @@ fun ChangeServerView( ) } Button( - onClick = onChangeServerSubmit, + onClick = { eventSink(ChangeServerEvents.Submit) }, enabled = state.submitEnabled, modifier = Modifier .fillMaxWidth() diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt index 096fb3939a..a0d809b9ad 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.Lifecycle import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -13,6 +14,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.x.anvilannotations.ContributesNode import io.element.android.x.architecture.presenterConnector +import io.element.android.x.core.compose.OnLifecycleEvent import io.element.android.x.di.AppScope @ContributesNode(AppScope::class) @@ -24,12 +26,6 @@ class LoginRootNode @AssistedInject constructor( private val presenterConnector = presenterConnector(presenter) - init { - lifecycle.subscribe( - onResume = { presenterConnector.emitEvent(LoginRootEvents.RefreshHomeServer) } - ) - } - interface Callback : Plugin { fun onChangeHomeServer() } @@ -38,27 +34,18 @@ class LoginRootNode @AssistedInject constructor( plugins().forEach { it.onChangeHomeServer() } } - private fun onLoginChanged(login: String) { - presenterConnector.emitEvent(LoginRootEvents.SetLogin(login)) - } - - private fun onPasswordChanged(password: String) { - presenterConnector.emitEvent(LoginRootEvents.SetPassword(password)) - } - - private fun onSubmit() { - presenterConnector.emitEvent(LoginRootEvents.Submit) - } - @Composable override fun View(modifier: Modifier) { val state by presenterConnector.stateFlow.collectAsState() + OnLifecycleEvent { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> state.eventSink(LoginRootEvents.RefreshHomeServer) + else -> Unit + } + } LoginRootScreen( state = state, onChangeServer = this::onChangeHomeServer, - onLoginChanged = this::onLoginChanged, - onPasswordChanged = this::onPasswordChanged, - onSubmitClicked = this::onSubmit ) } } diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt index 1d254c47d3..79ac8a3d95 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.x.architecture.Presenter import io.element.android.x.matrix.Matrix @@ -13,10 +14,11 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import javax.inject.Inject -class LoginRootPresenter @Inject constructor(private val matrix: Matrix) : Presenter { +class LoginRootPresenter @Inject constructor(private val matrix: Matrix) : Presenter { @Composable - override fun present(events: Flow): LoginRootState { + override fun present(): LoginRootState { + val localCoroutineScope = rememberCoroutineScope() val homeserver = rememberSaveable { mutableStateOf(matrix.getHomeserverOrDefault()) } @@ -27,24 +29,24 @@ class LoginRootPresenter @Inject constructor(private val matrix: Matrix) : Prese mutableStateOf(LoginFormState.Default) } - LaunchedEffect(Unit) { - events.collect { event -> - when (event) { - LoginRootEvents.RefreshHomeServer -> refreshHomeServer(homeserver) - is LoginRootEvents.SetLogin -> updateFormState(formState) { - copy(login = event.login) - } - is LoginRootEvents.SetPassword -> updateFormState(formState) { - copy(password = event.password) - } - LoginRootEvents.Submit -> submit(homeserver.value, formState.value, loggedInState) + fun handleEvents(event: LoginRootEvents){ + when (event) { + LoginRootEvents.RefreshHomeServer -> refreshHomeServer(homeserver) + is LoginRootEvents.SetLogin -> updateFormState(formState) { + copy(login = event.login) } + is LoginRootEvents.SetPassword -> updateFormState(formState) { + copy(password = event.password) + } + LoginRootEvents.Submit -> localCoroutineScope.submit(homeserver.value, formState.value, loggedInState) } } + return LoginRootState( homeserver = homeserver.value, loggedInState = loggedInState.value, - formState = formState.value + formState = formState.value, + eventSink = ::handleEvents ) } diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootScreen.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootScreen.kt index f746965ddb..d71cb909c3 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootScreen.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootScreen.kt @@ -68,11 +68,9 @@ fun LoginRootScreen( state: LoginRootState, modifier: Modifier = Modifier, onChangeServer: () -> Unit = {}, - onLoginChanged: (String) -> Unit = {}, - onPasswordChanged: (String) -> Unit = {}, - onSubmitClicked: () -> Unit = {}, onLoginWithSuccess: (SessionId) -> Unit = {}, ) { + val eventSink = state.eventSink Surface( modifier = modifier, color = MaterialTheme.colorScheme.background, @@ -144,7 +142,7 @@ fun LoginRootScreen( }, onValueChange = { loginFieldState = it - onLoginChanged(it) + eventSink(LoginRootEvents.SetLogin(it)) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Email, @@ -163,7 +161,7 @@ fun LoginRootScreen( .padding(top = 24.dp), onValueChange = { passwordFieldState = it - onPasswordChanged(it) + eventSink(LoginRootEvents.SetPassword(it)) }, label = { Text(text = "Password") @@ -185,7 +183,7 @@ fun LoginRootScreen( imeAction = ImeAction.Done, ), keyboardActions = KeyboardActions( - onDone = { onSubmitClicked() } + onDone = { eventSink(LoginRootEvents.Submit) } ), ) if (state.loggedInState is LoggedInState.ErrorLoggingIn) { @@ -199,7 +197,7 @@ fun LoginRootScreen( } // Submit Button( - onClick = onSubmitClicked, + onClick = { eventSink(LoginRootEvents.Submit) }, enabled = state.submitEnabled, modifier = Modifier .fillMaxWidth() diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt index 38b8f67af2..53fc6262f1 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt @@ -8,6 +8,7 @@ data class LoginRootState( val homeserver: String = "", val loggedInState: LoggedInState = LoggedInState.NotLoggedIn, val formState: LoginFormState = LoginFormState.Default, + val eventSink: (LoginRootEvents) -> Unit = {} ) { val submitEnabled = formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState != LoggedInState.LoggingIn diff --git a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt index 800aebf077..82a56c6bf8 100644 --- a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt +++ b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt @@ -1,35 +1,36 @@ package io.element.android.x.features.logout import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import io.element.android.x.architecture.Async import io.element.android.x.architecture.Presenter import io.element.android.x.architecture.execute import io.element.android.x.matrix.MatrixClient import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import javax.inject.Inject -class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) : Presenter { +class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) : Presenter { @Composable - override fun present(events: Flow): LogoutPreferenceState { + override fun present(): LogoutPreferenceState { + val localCoroutineScope = rememberCoroutineScope() val logoutAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } - LaunchedEffect(Unit) { - events.collect { event -> - when (event) { - LogoutPreferenceEvents.Logout -> logout(logoutAction) - } + + fun handleEvents(event: LogoutPreferenceEvents) { + when (event) { + LogoutPreferenceEvents.Logout -> localCoroutineScope.logout(logoutAction) } } + return LogoutPreferenceState( - logoutAction = logoutAction.value + logoutAction = logoutAction.value, + eventSink = ::handleEvents ) } diff --git a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceScreen.kt b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceScreen.kt index dc0a8acea0..75f29319dd 100644 --- a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceScreen.kt +++ b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceScreen.kt @@ -19,6 +19,7 @@ package io.element.android.x.features.logout import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Logout import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource @@ -34,14 +35,15 @@ import io.element.android.x.element.resources.R as ElementR @Composable fun LogoutPreferenceView( state: LogoutPreferenceState, - onLogoutClicked: () -> Unit = {}, - onSuccessLogout: () -> Unit = {}, + onSuccessLogout: () -> Unit = {} ) { + val eventSink = state.eventSink if (state.logoutAction is Async.Success) { - onSuccessLogout() + LaunchedEffect(state.logoutAction) { + onSuccessLogout() + } return } - val openDialog = remember { mutableStateOf(false) } LogoutPreferenceContent( @@ -61,7 +63,7 @@ fun LogoutPreferenceView( }, onSubmitClicked = { openDialog.value = false - onLogoutClicked() + eventSink(LogoutPreferenceEvents.Logout) }, onDismiss = { openDialog.value = false diff --git a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceState.kt b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceState.kt index b7b08578bd..70e637899b 100644 --- a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceState.kt +++ b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceState.kt @@ -20,4 +20,5 @@ import io.element.android.x.architecture.Async data class LogoutPreferenceState( val logoutAction: Async = Async.Uninitialized, + val eventSink: (LogoutPreferenceEvents) -> Unit = {}, ) diff --git a/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewModel.kt b/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewModel.kt deleted file mode 100644 index a4a170515c..0000000000 --- a/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewModel.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.features.onboarding - -import com.airbnb.mvrx.MavericksViewModel - -class OnBoardingViewModel(initialState: OnBoardingViewState) : - MavericksViewModel(initialState) { - - fun onPageChanged(page: Int) { - setState { - copy( - currentPage = page, - ) - } - } -} diff --git a/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewState.kt b/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewState.kt deleted file mode 100644 index e728e8345c..0000000000 --- a/features/onboarding/src/main/java/io/element/android/x/features/onboarding/OnBoardingViewState.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.features.onboarding - -import com.airbnb.mvrx.MavericksState - -data class OnBoardingViewState( - val currentPage: Int = 0, -) : MavericksState diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootEvents.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootEvents.kt deleted file mode 100644 index 80496c695b..0000000000 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootEvents.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.element.android.x.features.preferences.root - -sealed interface PreferencesRootEvents { - object Logout : PreferencesRootEvents - data class SetRageshakeSensitivity(val sensitivity: Float) : PreferencesRootEvents - data class SetRageshakeEnabled(val enabled: Boolean) : PreferencesRootEvents -} diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt index 78db0459a2..f8afbd9461 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt @@ -21,24 +21,12 @@ class PreferencesRootNode @AssistedInject constructor( private val presenter: PreferencesRootPresenter, ) : Node(buildContext, plugins = plugins) { - public interface Callback : Plugin { + interface Callback : Plugin { fun onOpenBugReport() } private val presenterConnector = presenterConnector(presenter) - private fun onLogoutClicked() { - presenterConnector.emitEvent(PreferencesRootEvents.Logout) - } - - private fun onRageshakeEnabledChanged(isEnabled: Boolean) { - presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeEnabled(isEnabled)) - } - - private fun onRageshakeSensitivityChanged(sensitivity: Float) { - presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeSensitivity(sensitivity)) - } - private fun onOpenBugReport() { plugins().forEach { it.onOpenBugReport() } } @@ -48,10 +36,7 @@ class PreferencesRootNode @AssistedInject constructor( val state by presenterConnector.stateFlow.collectAsState() PreferencesRootView( state = state, - onLogoutClicked = this::onLogoutClicked, onBackPressed = this::navigateUp, - onRageshakeEnabledChanged = this::onRageshakeEnabledChanged, - onRageshakeSensitivityChanged = this::onRageshakeSensitivityChanged, onOpenRageShake = this::onOpenBugReport ) } diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt index 8927b220ed..fd0fe26297 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt @@ -1,42 +1,26 @@ package io.element.android.x.features.preferences.root import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import io.element.android.x.architecture.Async import io.element.android.x.architecture.Presenter -import io.element.android.x.architecture.SharedFlowHolder -import io.element.android.x.features.logout.LogoutPreferenceEvents import io.element.android.x.features.logout.LogoutPreferencePresenter -import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class PreferencesRootPresenter @Inject constructor( private val logoutPresenter: LogoutPreferencePresenter, private val rageshakePresenter: RageshakePreferencesPresenter, -) : Presenter { - - private val logoutEventsFlow = SharedFlowHolder() - private val rageshakeEventsFlow = SharedFlowHolder() +) : Presenter { @Composable - override fun present(events: Flow): PreferencesRootState { - val logoutState = logoutPresenter.present(events = logoutEventsFlow.asSharedFlow()) - val rageshakeState = rageshakePresenter.present(events = rageshakeEventsFlow.asSharedFlow()) - LaunchedEffect(Unit) { - events.collect { event -> - when (event) { - PreferencesRootEvents.Logout -> logoutEventsFlow.emit(LogoutPreferenceEvents.Logout) - is PreferencesRootEvents.SetRageshakeEnabled -> rageshakeEventsFlow.emit(RageshakePreferencesEvents.SetIsEnabled(event.enabled)) - is PreferencesRootEvents.SetRageshakeSensitivity -> rageshakeEventsFlow.emit(RageshakePreferencesEvents.SetSensitivity(event.sensitivity)) - } - } - } + override fun present(): PreferencesRootState { + val logoutState = logoutPresenter.present() + val rageshakeState = rageshakePresenter.present() + return PreferencesRootState( logoutState = logoutState, rageshakeState = rageshakeState, - myUser = Async.Uninitialized + myUser = Async.Uninitialized, ) } } diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt index 30a116ce8d..fc0cbd3331 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt @@ -10,6 +10,7 @@ import io.element.android.x.element.resources.R import io.element.android.x.features.logout.LogoutPreferenceState import io.element.android.x.features.logout.LogoutPreferenceView import io.element.android.x.features.preferences.user.UserPreferences +import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState import io.element.android.x.features.rageshake.preferences.RageshakePreferencesView @@ -18,10 +19,7 @@ fun PreferencesRootView( state: PreferencesRootState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onLogoutClicked: () -> Unit = {}, onOpenRageShake: () -> Unit = {}, - onRageshakeEnabledChanged: (Boolean) -> Unit = {}, - onRageshakeSensitivityChanged: (Float) -> Unit = {}, ) { // TODO Hierarchy! // Include pref from other modules @@ -34,12 +32,9 @@ fun PreferencesRootView( RageshakePreferencesView( state = state.rageshakeState, onOpenRageshake = onOpenRageShake, - onSensitivityChanged = onRageshakeSensitivityChanged, - onIsEnabledChanged = onRageshakeEnabledChanged, ) LogoutPreferenceView( state = state.logoutState, - onLogoutClicked = onLogoutClicked, ) } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt index 088179f939..c7bf7114c3 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt @@ -33,43 +33,12 @@ class BugReportNode @AssistedInject constructor( BugReportView( state = state, modifier = modifier, - onDescriptionChanged = this::onDescriptionChanged, - onSetSendLog = this::onSetSendLog, - onSetSendCrashLog = this::onSetSendCrashLog, - onSetCanContact = this::onSetCanContact, - onSetSendScreenshot = this::onSetSendScreenshot, - onSubmit = this::onSubmit, onDone = this::onDone ) } private fun onDone() { - presenterConnector.emitEvent(BugReportEvents.ResetAll) plugins().forEach { it.onBugReportSent() } } - - private fun onSubmit() { - presenterConnector.emitEvent(BugReportEvents.SendBugReport) - } - - private fun onSetSendLog(sendLog: Boolean) { - presenterConnector.emitEvent(BugReportEvents.SetSendLog(sendLog)) - } - - private fun onSetSendCrashLog(sendCrashLog: Boolean) { - presenterConnector.emitEvent(BugReportEvents.SetSendCrashLog(sendCrashLog)) - } - - private fun onSetSendScreenshot(sendScreenshot: Boolean) { - presenterConnector.emitEvent(BugReportEvents.SetSendScreenshot(sendScreenshot)) - } - - private fun onSetCanContact(canContact: Boolean) { - presenterConnector.emitEvent(BugReportEvents.SetCanContact(canContact)) - } - - private fun onDescriptionChanged(description: String) { - presenterConnector.emitEvent(BugReportEvents.SetDescription(description)) - } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt index 4127395ce5..16a95ad6f0 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt @@ -1,10 +1,8 @@ package io.element.android.x.features.rageshake.bugreport import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -18,7 +16,6 @@ import io.element.android.x.features.rageshake.reporter.BugReporter import io.element.android.x.features.rageshake.reporter.ReportType import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import javax.inject.Inject @@ -27,7 +24,7 @@ class BugReportPresenter @Inject constructor( private val crashDataStore: CrashDataStore, private val screenshotHolder: ScreenshotHolder, private val appCoroutineScope: CoroutineScope, -) : Presenter { +) : Presenter { private class BugReporterUploadListener( private val sendingProgress: MutableState, @@ -56,7 +53,7 @@ class BugReportPresenter @Inject constructor( } @Composable - override fun present(events: Flow): BugReportState { + override fun present(): BugReportState { val screenshotUri = rememberSaveable { mutableStateOf( screenshotHolder.getFile()?.toUri()?.toString() @@ -76,58 +73,54 @@ class BugReportPresenter @Inject constructor( mutableStateOf(BugReportFormState.Default) } val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction) - val state by remember { - derivedStateOf { - BugReportState( - hasCrashLogs = crashInfo.isNotEmpty(), - sendingProgress = sendingProgress.value, - sending = sendingAction.value, - formState = formState.value, - screenshotUri = screenshotUri.value - ) - } - } - LaunchedEffect(Unit) { - events.collect { event -> - when (event) { - BugReportEvents.SendBugReport -> appCoroutineScope.sendBugReport(state, uploadListener) - BugReportEvents.ResetAll -> appCoroutineScope.resetAll() - is BugReportEvents.SetDescription -> updateFormState(formState) { - copy(description = event.description) - } - is BugReportEvents.SetCanContact -> updateFormState(formState) { - copy(canContact = event.canContact) - } - is BugReportEvents.SetSendCrashLog -> updateFormState(formState) { - copy(sendCrashLogs = event.sendCrashlog) - } - is BugReportEvents.SetSendLog -> updateFormState(formState) { - copy(sendLogs = event.sendLog) - } - is BugReportEvents.SetSendScreenshot -> updateFormState(formState) { - copy(sendScreenshot = event.sendScreenshot) - } + + fun handleEvents(event: BugReportEvents) { + when (event) { + BugReportEvents.SendBugReport -> appCoroutineScope.sendBugReport(formState.value, crashInfo.isNotEmpty(), uploadListener) + BugReportEvents.ResetAll -> appCoroutineScope.resetAll() + is BugReportEvents.SetDescription -> updateFormState(formState) { + copy(description = event.description) + } + is BugReportEvents.SetCanContact -> updateFormState(formState) { + copy(canContact = event.canContact) + } + is BugReportEvents.SetSendCrashLog -> updateFormState(formState) { + copy(sendCrashLogs = event.sendCrashlog) + } + is BugReportEvents.SetSendLog -> updateFormState(formState) { + copy(sendLogs = event.sendLog) + } + is BugReportEvents.SetSendScreenshot -> updateFormState(formState) { + copy(sendScreenshot = event.sendScreenshot) } } } - return state + + return BugReportState( + hasCrashLogs = crashInfo.isNotEmpty(), + sendingProgress = sendingProgress.value, + sending = sendingAction.value, + formState = formState.value, + screenshotUri = screenshotUri.value, + eventSink = ::handleEvents + ) } private fun updateFormState(formState: MutableState, operation: BugReportFormState.() -> BugReportFormState) { formState.value = operation(formState.value) } - private fun CoroutineScope.sendBugReport(state: BugReportState, listener: BugReporter.IMXBugReportListener) = launch { + private fun CoroutineScope.sendBugReport(formState: BugReportFormState, hasCrashLogs: Boolean, listener: BugReporter.IMXBugReportListener) = launch { bugReporter.sendBugReport( coroutineScope = this, reportType = ReportType.BUG_REPORT, - withDevicesLogs = state.formState.sendLogs, - withCrashLogs = state.hasCrashLogs && state.formState.sendCrashLogs, + withDevicesLogs = formState.sendLogs, + withCrashLogs = hasCrashLogs && formState.sendCrashLogs, withKeyRequestHistory = false, - withScreenshot = state.formState.sendScreenshot, - theBugDescription = state.formState.description, + withScreenshot = formState.sendScreenshot, + theBugDescription = formState.description, serverVersion = "", - canContact = state.formState.canContact, + canContact = formState.canContact, customFields = emptyMap(), listener = listener ) diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportState.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportState.kt index cc7fad74ca..80912a7923 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportState.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportState.kt @@ -26,6 +26,7 @@ data class BugReportState( val screenshotUri: String? = null, val sendingProgress: Float = 0F, val sending: Async = Async.Uninitialized, + val eventSink: (BugReportEvents) -> Unit = {} ) { val submitEnabled = formState.description.length > 10 && sending !is Async.Loading diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportView.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportView.kt index 0b5ab39365..06fa9dd539 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportView.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportView.kt @@ -14,7 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class) package io.element.android.x.features.rageshake.bugreport @@ -36,6 +35,7 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -59,22 +59,21 @@ import io.element.android.x.designsystem.components.LabelledCheckbox import io.element.android.x.designsystem.components.dialogs.ErrorDialog import io.element.android.x.element.resources.R as ElementR +@OptIn(ExperimentalMaterial3Api::class) @Composable fun BugReportView( state: BugReportState, modifier: Modifier = Modifier, - onDescriptionChanged: (String) -> Unit = {}, - onSetSendLog: (Boolean) -> Unit = {}, - onSetSendCrashLog: (Boolean) -> Unit = {}, - onSetCanContact: (Boolean) -> Unit = {}, - onSetSendScreenshot: (Boolean) -> Unit = {}, - onSubmit: () -> Unit = {}, - onFailureDialogClosed: () -> Unit = { }, onDone: () -> Unit = { }, ) { LogCompositions(tag = "Rageshake", msg = "Root") + val eventSink = state.eventSink if (state.sending is Async.Success) { - onDone() + LaunchedEffect(state.sending) { + eventSink(BugReportEvents.ResetAll) + onDone() + } + return } Surface( modifier = modifier, @@ -132,7 +131,7 @@ fun BugReportView( }, onValueChange = { descriptionFieldState = it - onDescriptionChanged(it) + eventSink(BugReportEvents.SetDescription(it)) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, @@ -143,28 +142,28 @@ fun BugReportView( } LabelledCheckbox( checked = state.formState.sendLogs, - onCheckedChange = onSetSendLog, + onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) }, enabled = isFormEnabled, text = stringResource(id = ElementR.string.send_bug_report_include_logs) ) if (state.hasCrashLogs) { LabelledCheckbox( checked = state.formState.sendCrashLogs, - onCheckedChange = onSetSendCrashLog, + onCheckedChange = { eventSink(BugReportEvents.SetSendCrashLog(it)) }, enabled = isFormEnabled, text = stringResource(id = ElementR.string.send_bug_report_include_crash_logs) ) } LabelledCheckbox( checked = state.formState.canContact, - onCheckedChange = onSetCanContact, + onCheckedChange = { eventSink(BugReportEvents.SetCanContact(it)) }, enabled = isFormEnabled, text = stringResource(id = ElementR.string.you_may_contact_me) ) if (state.screenshotUri != null) { LabelledCheckbox( checked = state.formState.sendScreenshot, - onCheckedChange = onSetSendScreenshot, + onCheckedChange = { eventSink(BugReportEvents.SetSendScreenshot(it)) }, enabled = isFormEnabled, text = stringResource(id = ElementR.string.send_bug_report_include_screenshot) ) @@ -187,7 +186,7 @@ fun BugReportView( } // Submit Button( - onClick = onSubmit, + onClick = { eventSink(BugReportEvents.SendBugReport) }, enabled = state.submitEnabled, modifier = Modifier .fillMaxWidth() @@ -197,7 +196,6 @@ fun BugReportView( } } when (state.sending) { - Async.Uninitialized -> Unit is Async.Loading -> { CircularProgressIndicator( progress = state.sendingProgress, @@ -206,9 +204,8 @@ fun BugReportView( } is Async.Failure -> ErrorDialog( content = state.sending.error.toString(), - onDismiss = onFailureDialogClosed, ) - is Async.Success -> onDone() + else -> Unit } } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt index 716bc19bf5..0153227c5e 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt @@ -1,30 +1,31 @@ package io.element.android.x.features.rageshake.crash.ui import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope import io.element.android.x.architecture.Presenter import io.element.android.x.features.rageshake.crash.CrashDataStore import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import javax.inject.Inject -class CrashDetectionPresenter @Inject constructor(private val crashDataStore: CrashDataStore) : Presenter { +class CrashDetectionPresenter @Inject constructor(private val crashDataStore: CrashDataStore) : Presenter { @Composable - override fun present(events: Flow): CrashDetectionState { + override fun present(): CrashDetectionState { + val localCoroutineScope = rememberCoroutineScope() val crashDetected = crashDataStore.appHasCrashed().collectAsState(initial = false) - LaunchedEffect(Unit) { - events.collect { event -> - when (event) { - CrashDetectionEvents.ResetAllCrashData -> resetAll() - CrashDetectionEvents.ResetAppHasCrashed -> resetAppHasCrashed() - } + + fun handleEvents(event: CrashDetectionEvents) { + when (event) { + CrashDetectionEvents.ResetAllCrashData -> localCoroutineScope.resetAll() + CrashDetectionEvents.ResetAppHasCrashed -> localCoroutineScope.resetAppHasCrashed() } } + return CrashDetectionState( - crashDetected = crashDetected.value + crashDetected = crashDetected.value, + eventSink = ::handleEvents ) } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionScreen.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionScreen.kt index e7da8ba4ab..a0d764a286 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionScreen.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionScreen.kt @@ -28,15 +28,19 @@ import io.element.android.x.element.resources.R as ElementR fun CrashDetectionView( state: CrashDetectionState, onOpenBugReport: () -> Unit = { }, - onPopupDismissed: () -> Unit = {} ) { LogCompositions(tag = "Crash", msg = "CrashDetectionScreen") + + fun onPopupDismissed(){ + state.eventSink(CrashDetectionEvents.ResetAllCrashData) + } + if (state.crashDetected) { CrashDetectionContent( state, onYesClicked = onOpenBugReport, - onNoClicked = onPopupDismissed, - onDismiss = onPopupDismissed, + onNoClicked = ::onPopupDismissed, + onDismiss = ::onPopupDismissed, ) } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionState.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionState.kt index 1ce7142735..52774c4cc9 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionState.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionState.kt @@ -18,4 +18,5 @@ package io.element.android.x.features.rageshake.crash.ui data class CrashDetectionState( val crashDetected: Boolean = false, + val eventSink: (CrashDetectionEvents) -> Unit = {} ) diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt index c3bd6b9ca7..49d6097ed8 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt @@ -3,19 +3,17 @@ package io.element.android.x.features.rageshake.detection import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.x.architecture.Presenter -import io.element.android.x.architecture.SharedFlowHolder import io.element.android.x.core.screenshot.ImageResult import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter import io.element.android.x.features.rageshake.rageshake.RageShake import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -24,13 +22,12 @@ class RageshakeDetectionPresenter @Inject constructor( private val screenshotHolder: ScreenshotHolder, private val rageShake: RageShake, private val preferencesPresenter: RageshakePreferencesPresenter, -) : Presenter { - - private val preferencesEventsFlow = SharedFlowHolder() +) : Presenter { @Composable - override fun present(events: Flow): RageshakeDetectionState { - val preferencesState = preferencesPresenter.present(events = preferencesEventsFlow.asSharedFlow()) + override fun present(): RageshakeDetectionState { + val localCoroutineScope = rememberCoroutineScope() + val preferencesState = preferencesPresenter.present() val isStarted = rememberSaveable { mutableStateOf(false) } @@ -40,33 +37,38 @@ class RageshakeDetectionPresenter @Inject constructor( val showDialog = rememberSaveable { mutableStateOf(false) } + + fun handleEvents(event: RageshakeDetectionEvents) { + when (event) { + RageshakeDetectionEvents.Disable -> { + preferencesState.eventSink(RageshakePreferencesEvents.SetIsEnabled(false)) + showDialog.value = false + } + RageshakeDetectionEvents.StartDetection -> isStarted.value = true + RageshakeDetectionEvents.StopDetection -> isStarted.value = false + is RageshakeDetectionEvents.ProcessScreenshot -> localCoroutineScope.processScreenshot(takeScreenshot, showDialog, event.imageResult) + RageshakeDetectionEvents.Dismiss -> showDialog.value = false + } + } + val state = remember(preferencesState, isStarted.value, takeScreenshot.value, showDialog.value) { RageshakeDetectionState( isStarted = isStarted.value, takeScreenshot = takeScreenshot.value, showDialog = showDialog.value, - preferenceState = preferencesState + preferenceState = preferencesState, + eventSink = ::handleEvents ) } - LaunchedEffect(Unit) { - events.collect { event -> - when (event) { - RageshakeDetectionEvents.Disable -> preferencesEventsFlow.emit(RageshakePreferencesEvents.SetIsEnabled(false)) - RageshakeDetectionEvents.StartDetection -> isStarted.value = true - RageshakeDetectionEvents.StopDetection -> isStarted.value = false - is RageshakeDetectionEvents.ProcessScreenshot -> processScreenshot(takeScreenshot, showDialog, event.imageResult) - RageshakeDetectionEvents.Dismiss -> showDialog.value = false - } - } - } + LaunchedEffect(preferencesState.sensitivity) { rageShake.setSensitivity(preferencesState.sensitivity) } val shouldStart = preferencesState.isEnabled && - preferencesState.isSupported && - isStarted.value && - !takeScreenshot.value && - !showDialog.value + preferencesState.isSupported && + isStarted.value && + !takeScreenshot.value && + !showDialog.value LaunchedEffect(shouldStart) { handleRageShake(shouldStart, state, takeScreenshot) diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt index f1a3f656cd..d32b326dd3 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt @@ -24,5 +24,6 @@ data class RageshakeDetectionState( val takeScreenshot: Boolean = false, val showDialog: Boolean = false, val isStarted: Boolean = false, - val preferenceState: RageshakePreferencesState = RageshakePreferencesState() + val preferenceState: RageshakePreferencesState = RageshakePreferencesState(), + val eventSink: (RageshakeDetectionEvents) -> Unit = {} ) diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt index 7de08d158c..61e9f32150 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt @@ -22,7 +22,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.Lifecycle import io.element.android.x.core.compose.LogCompositions +import io.element.android.x.core.compose.OnLifecycleEvent import io.element.android.x.core.hardware.vibrate import io.element.android.x.core.screenshot.ImageResult import io.element.android.x.core.screenshot.screenshot @@ -34,24 +36,28 @@ import io.element.android.x.element.resources.R as ElementR fun RageshakeDetectionView( state: RageshakeDetectionState, onOpenBugReport: () -> Unit = { }, - onScreenshotTaken: (ImageResult) -> Unit = {}, - onDisableClicked: () -> Unit = {}, - onNoClicked: () -> Unit = {} ) { LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen") + val eventSink = state.eventSink val context = LocalContext.current + OnLifecycleEvent { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> eventSink(RageshakeDetectionEvents.StartDetection) + Lifecycle.Event.ON_PAUSE -> eventSink(RageshakeDetectionEvents.StopDetection) + else -> Unit + } + } when { state.takeScreenshot -> TakeScreenshot( - onScreenshotTaken = onScreenshotTaken + onScreenshotTaken = { eventSink(RageshakeDetectionEvents.ProcessScreenshot(it)) } ) state.showDialog -> { - LaunchedEffect(key1 = "RS_diag") { + LaunchedEffect(Unit) { context.vibrate() } RageshakeDialogContent( - state, - onNoClicked = onNoClicked, - onDisableClicked = onDisableClicked, + onNoClicked = { eventSink(RageshakeDetectionEvents.Dismiss) }, + onDisableClicked = { eventSink(RageshakeDetectionEvents.Disable) }, onYesClicked = onOpenBugReport ) } @@ -72,7 +78,6 @@ private fun TakeScreenshot( @Composable fun RageshakeDialogContent( - state: RageshakeDetectionState, onNoClicked: () -> Unit = { }, onDisableClicked: () -> Unit = { }, onYesClicked: () -> Unit = { }, @@ -83,6 +88,7 @@ fun RageshakeDialogContent( thirdButtonText = stringResource(id = ElementR.string.action_disable), submitText = stringResource(id = ElementR.string.yes), cancelText = stringResource(id = ElementR.string.no), + onCancelClicked = onNoClicked, onThirdButtonClicked = onDisableClicked, onSubmitClicked = onYesClicked, onDismiss = onNoClicked, @@ -93,8 +99,6 @@ fun RageshakeDialogContent( @Composable fun RageshakeDialogContentPreview() { ElementXTheme { - RageshakeDialogContent( - state = RageshakeDetectionState() - ) + RageshakeDialogContent() } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt index 3012a6c32f..6a0ed8416a 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt @@ -1,26 +1,26 @@ package io.element.android.x.features.rageshake.preferences import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.x.architecture.Presenter import io.element.android.x.features.rageshake.rageshake.RageShake import io.element.android.x.features.rageshake.rageshake.RageshakeDataStore import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import javax.inject.Inject class RageshakePreferencesPresenter @Inject constructor( private val rageshake: RageShake, private val rageshakeDataStore: RageshakeDataStore, -) : Presenter { +) : Presenter { @Composable - override fun present(events: Flow): RageshakePreferencesState { + override fun present(): RageshakePreferencesState { + val localCoroutineScope = rememberCoroutineScope() val isSupported: MutableState = rememberSaveable { mutableStateOf(rageshake.isAvailable()) } @@ -32,19 +32,18 @@ class RageshakePreferencesPresenter @Inject constructor( .sensitivity() .collectAsState(initial = 0f) - LaunchedEffect(Unit) { - events.collect { event -> - when (event) { - is RageshakePreferencesEvents.SetIsEnabled -> setIsEnabled(event.isEnabled) - is RageshakePreferencesEvents.SetSensitivity -> setSensitivity(event.sensitivity) - } + fun handleEvents(event: RageshakePreferencesEvents) { + when (event) { + is RageshakePreferencesEvents.SetIsEnabled -> localCoroutineScope.setIsEnabled(event.isEnabled) + is RageshakePreferencesEvents.SetSensitivity -> localCoroutineScope.setSensitivity(event.sensitivity) } } return RageshakePreferencesState( isEnabled = isEnabled.value, isSupported = isSupported.value, - sensitivity = sensitivity.value + sensitivity = sensitivity.value, + eventSink = ::handleEvents ) } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt index d58457f620..216a386d5f 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt @@ -4,4 +4,5 @@ data class RageshakePreferencesState( val isEnabled: Boolean = false, val isSupported: Boolean = true, val sensitivity: Float = 0.3f, + val eventSink: (RageshakePreferencesEvents) -> Unit = {}, ) diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt index 7bd29c7580..ac7b2478fc 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt @@ -34,9 +34,16 @@ fun RageshakePreferencesView( state: RageshakePreferencesState, modifier: Modifier = Modifier, onOpenRageshake: () -> Unit = {}, - onIsEnabledChanged: (Boolean) -> Unit = {}, - onSensitivityChanged: (Float) -> Unit = {} ) { + + fun onSensitivityChanged(sensitivity: Float){ + state.eventSink(RageshakePreferencesEvents.SetSensitivity(sensitivity = sensitivity)) + } + + fun onEnabledChanged(isEnabled: Boolean){ + state.eventSink(RageshakePreferencesEvents.SetIsEnabled(isEnabled = isEnabled)) + } + Column(modifier = modifier) { PreferenceCategory(title = stringResource(id = ElementR.string.send_bug_report)) { PreferenceText( @@ -45,12 +52,13 @@ fun RageshakePreferencesView( onClick = onOpenRageshake ) } + PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) { if (state.isSupported) { PreferenceSwitch( title = stringResource(id = ElementR.string.send_bug_report_rage_shake), isChecked = state.isEnabled, - onCheckedChange = onIsEnabledChanged + onCheckedChange = ::onEnabledChanged ) PreferenceSlide( title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold), @@ -58,7 +66,7 @@ fun RageshakePreferencesView( value = state.sensitivity, enabled = state.isEnabled, steps = 3 /* 5 possible values - steps are in ]0, 1[ */, - onValueChange = onSensitivityChanged + onValueChange = ::onSensitivityChanged ) } else { PreferenceText(title = "Rageshaking is not supported by your device") diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt index 41bccdcb7b..a35acb7345 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -30,18 +30,6 @@ class RoomListNode @AssistedInject constructor( private val connector = presenterConnector(presenter) - private fun updateFilter(filter: String) { - connector.emitEvent(RoomListEvents.UpdateFilter(filter)) - } - - private fun updateVisibleRange(range: IntRange) { - connector.emitEvent((RoomListEvents.UpdateVisibleRange(range))) - } - - private fun logout() { - connector.emitEvent(RoomListEvents.Logout) - } - private fun onRoomClicked(roomId: RoomId) { plugins().forEach { it.onRoomClicked(roomId) } } @@ -56,8 +44,6 @@ class RoomListNode @AssistedInject constructor( RoomListView( state = state, onRoomClicked = this::onRoomClicked, - onFilterChanged = this::updateFilter, - onScrollOver = this::updateVisibleRange, onOpenSettings = this::onOpenSettings ) } diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt index ee3aa7b6fb..7727adf348 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt @@ -7,8 +7,10 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import io.element.android.x.architecture.Presenter import io.element.android.x.core.coroutine.parallelMap import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarSize @@ -19,13 +21,12 @@ import io.element.android.x.features.roomlist.model.RoomListState import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.room.RoomSummary -import io.element.android.x.architecture.Presenter import io.element.android.x.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject private const val extendedRangeSize = 40 @@ -33,10 +34,10 @@ private const val extendedRangeSize = 40 class RoomListPresenter @Inject constructor( private val client: MatrixClient, private val lastMessageFormatter: LastMessageFormatter, -) : Presenter { +) : Presenter { @Composable - override fun present(events: Flow): RoomListState { + override fun present(): RoomListState { val matrixUser: MutableState = remember { mutableStateOf(null) } @@ -52,14 +53,15 @@ class RoomListPresenter @Inject constructor( } LaunchedEffect(Unit) { initialLoad(matrixUser) - events.collect { event -> - when (event) { - RoomListEvents.Logout -> logout(isLoginOut) - is RoomListEvents.UpdateFilter -> filter = event.newFilter - is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range) - } + } + + fun handleEvents(event: RoomListEvents) { + when (event) { + is RoomListEvents.UpdateFilter -> filter = event.newFilter + is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range) } } + LaunchedEffect(roomSummaries, filter) { filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter) } @@ -67,7 +69,8 @@ class RoomListPresenter @Inject constructor( matrixUser = matrixUser.value, roomList = filteredRoomSummaries.value, filter = filter, - isLoginOut = isLoginOut.value + isLoginOut = isLoginOut.value, + eventSink = ::handleEvents ) } @@ -83,7 +86,7 @@ class RoomListPresenter @Inject constructor( }.toImmutableList() } - private suspend fun initialLoad(matrixUser: MutableState) { + private fun CoroutineScope.initialLoad(matrixUser: MutableState) = launch { val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() val userDisplayName = client.loadUserDisplayName().getOrNull() val avatarData = @@ -100,13 +103,6 @@ class RoomListPresenter @Inject constructor( ) } - private suspend fun logout(isLoginOut: MutableState) { - isLoginOut.value = true - delay(2000) - client.logout() - isLoginOut.value = false - } - private fun updateVisibleRange(range: IntRange) { if (range.isEmpty()) return val midExtendedRangeSize = extendedRangeSize / 2 diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt index 97c42f7eeb..4eb09e52b2 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt @@ -41,6 +41,7 @@ import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.features.roomlist.components.RoomListTopBar import io.element.android.x.features.roomlist.components.RoomSummaryRow +import io.element.android.x.features.roomlist.model.RoomListEvents import io.element.android.x.features.roomlist.model.RoomListRoomSummary import io.element.android.x.features.roomlist.model.RoomListState import io.element.android.x.features.roomlist.model.stubbedRoomSummaries @@ -54,19 +55,26 @@ fun RoomListView( state: RoomListState, modifier: Modifier = Modifier, onRoomClicked: (RoomId) -> Unit = {}, - onFilterChanged: (String) -> Unit = {}, onOpenSettings: () -> Unit = {}, - onScrollOver: (IntRange) -> Unit = {}, ) { + + fun onFilterChanged(filter: String){ + state.eventSink(RoomListEvents.UpdateFilter(filter)) + } + + fun onVisibleRangedChanged(range: IntRange){ + state.eventSink(RoomListEvents.UpdateVisibleRange(range)) + } + RoomListView( roomSummaries = state.roomList, matrixUser = state.matrixUser, filter = state.filter, modifier = modifier, onRoomClicked = onRoomClicked, - onFilterChanged = onFilterChanged, + onFilterChanged = ::onFilterChanged, onOpenSettings = onOpenSettings, - onScrollOver = onScrollOver, + onScrollOver = ::onVisibleRangedChanged, ) } diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt index ff8b80decf..c89a87280d 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt @@ -1,7 +1,6 @@ package io.element.android.x.features.roomlist.model sealed interface RoomListEvents { - object Logout : RoomListEvents data class UpdateFilter(val newFilter: String) : RoomListEvents data class UpdateVisibleRange(val range: IntRange): RoomListEvents } diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt index 8e5b159676..97ce9b4023 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt @@ -10,4 +10,5 @@ data class RoomListState( val roomList: ImmutableList, val filter: String, val isLoginOut: Boolean, + val eventSink: (RoomListEvents) -> Unit = {} ) diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt index a0addbfe1a..6926a5f475 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt @@ -3,7 +3,7 @@ package io.element.android.x.architecture import androidx.compose.runtime.Composable import kotlinx.coroutines.flow.Flow -interface Presenter { +interface Presenter { @Composable - fun present(events: Flow): State + fun present(): State } diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt index d30b5047f0..b183c324e8 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt @@ -8,19 +8,14 @@ import app.cash.molecule.launchMolecule import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow -inline fun LifecycleOwner.presenterConnector(presenter: Presenter): LifecyclePresenterConnector = +inline fun LifecycleOwner.presenterConnector(presenter: Presenter): LifecyclePresenterConnector = LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter) -class LifecyclePresenterConnector(lifecycleOwner: LifecycleOwner, presenter: Presenter) { +class LifecyclePresenterConnector(lifecycleOwner: LifecycleOwner, presenter: Presenter) { private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main) - private val eventFlow = SharedFlowHolder() val stateFlow: StateFlow = moleculeScope.launchMolecule(RecompositionClock.Immediate) { - presenter.present(events = eventFlow.asSharedFlow()) - } - - fun emitEvent(event: Event) { - eventFlow.emit(event) + presenter.present() } } diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt index 7d41e261a5..c0933b933e 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt @@ -9,4 +9,6 @@ class SharedFlowHolder(capacity: Int = 64) { fun asSharedFlow() = mutableFlow.asSharedFlow() fun emit(data: Data) = mutableFlow.tryEmit(data) + + suspend fun awaitEmit(data: Data) = mutableFlow.emit(data) } From f470f2a6affce5aa309bfca30c4ba328d4580d8f Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Jan 2023 17:08:56 +0100 Subject: [PATCH 18/30] Make some data class @Immutable when needed --- .../roomlist/model/RoomListRoomSummary.kt | 4 +-- .../features/roomlist/model/RoomListState.kt | 4 +-- .../roomlist/model/RoomListViewState.kt | 30 ------------------- .../components/avatar/AvatarData.kt | 4 +-- .../android/x/matrix/ui/model/MatrixUser.kt | 3 +- 5 files changed, 8 insertions(+), 37 deletions(-) delete mode 100644 features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListRoomSummary.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListRoomSummary.kt index 2f17cc0b25..4bac53544c 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListRoomSummary.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListRoomSummary.kt @@ -16,11 +16,11 @@ package io.element.android.x.features.roomlist.model -import androidx.compose.runtime.Stable +import androidx.compose.runtime.Immutable import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.matrix.core.RoomId -@Stable +@Immutable data class RoomListRoomSummary( val id: String, val roomId: RoomId = RoomId(id), diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt index 97ce9b4023..e6480a1677 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt @@ -1,10 +1,10 @@ package io.element.android.x.features.roomlist.model -import androidx.compose.runtime.Stable +import androidx.compose.runtime.Immutable import io.element.android.x.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList -@Stable +@Immutable data class RoomListState( val matrixUser: MatrixUser?, val roomList: ImmutableList, diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt deleted file mode 100644 index 4fb02b054c..0000000000 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.features.roomlist.model - -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MavericksState -import com.airbnb.mvrx.Uninitialized -import io.element.android.x.matrix.core.RoomId - -data class RoomListViewState( - // Will contain the filtered rooms, using ::filter (if filter is not empty) - val rooms: Async> = Uninitialized, - val filter: String = "", - val canLoadMore: Boolean = false, - val roomsById: Map = emptyMap() -) : MavericksState diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarData.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarData.kt index 346aeca336..d4fb492901 100644 --- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarData.kt +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarData.kt @@ -16,9 +16,9 @@ package io.element.android.x.designsystem.components.avatar -import androidx.compose.runtime.Stable +import androidx.compose.runtime.Immutable -@Stable +@Immutable data class AvatarData( val name: String = "", val model: ByteArray? = null, diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt index 5aaa69e6e6..3dfebaf16b 100644 --- a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt @@ -16,11 +16,12 @@ package io.element.android.x.matrix.ui.model +import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.matrix.core.UserId -@Stable +@Immutable data class MatrixUser( val id: UserId, val username: String? = null, From c2643d096768f8b074a0105309175faf95a86386 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Jan 2023 17:09:14 +0100 Subject: [PATCH 19/30] Remove SharedFlowHolder --- .../android/x/architecture/SharedFlowHolder.kt | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt deleted file mode 100644 index c0933b933e..0000000000 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.element.android.x.architecture - -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow - -class SharedFlowHolder(capacity: Int = 64) { - private val mutableFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = capacity) - - fun asSharedFlow() = mutableFlow.asSharedFlow() - - fun emit(data: Data) = mutableFlow.tryEmit(data) - - suspend fun awaitEmit(data: Data) = mutableFlow.emit(data) -} From 8718bc4cde6c3b32d1ce54e2e06c8870cc6eac12 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 12 Jan 2023 18:19:27 +0100 Subject: [PATCH 20/30] Start migrating messages screen --- .../android/x/node/LoggedInFlowNode.kt | 14 +- .../io/element/android/x/node/RoomFlowNode.kt | 52 ++ .../x/features/messages/MessagesEvents.kt | 8 + .../x/features/messages/MessagesNode.kt | 40 + .../x/features/messages/MessagesPresenter.kt | 91 +++ .../x/features/messages/MessagesScreen.kt | 689 ------------------ .../x/features/messages/MessagesState.kt | 19 + .../x/features/messages/MessagesView.kt | 193 +++++ .../x/features/messages/MessagesViewModel.kt | 228 ------ .../messages/actionlist/ActionListEvents.kt | 8 + .../actionlist/ActionListPresenter.kt | 59 ++ .../messages/actionlist/ActionListState.kt | 37 + .../messages/actionlist/ActionListView.kt | 116 +++ .../TimelineItemAction.kt} | 18 +- .../MessagesTimelineItemActionsSheet.kt | 145 ---- .../messages/model/MessagesViewState.kt | 45 -- .../textcomposer/MessageComposerEvents.kt | 11 + .../textcomposer/MessageComposerPresenter.kt | 73 ++ ...erViewState.kt => MessageComposerState.kt} | 12 +- .../textcomposer/MessageComposerView.kt | 40 + .../textcomposer/MessageComposerViewModel.kt | 54 -- .../messages/timeline/TimelineEvents.kt | 8 + .../messages/timeline/TimelinePresenter.kt | 97 +++ .../messages/timeline/TimelineState.kt | 30 + .../messages/timeline/TimelineView.kt | 412 +++++++++++ .../components/MessageEventBubble.kt | 2 +- .../components/MessagesReactionsView.kt | 2 +- .../MessagesTimelineItemEncryptedView.kt | 2 +- .../MessagesTimelineItemImageView.kt | 2 +- .../MessagesTimelineItemInformativeView.kt | 2 +- .../MessagesTimelineItemRedactedView.kt | 3 +- .../MessagesTimelineItemTextView.kt | 4 +- .../MessagesTimelineItemUnknownView.kt | 2 +- .../components/html/HtmlDocument.kt | 2 +- .../java/io/element/android/x/di/RoomScope.kt | 10 +- .../x/matrix/timeline/MatrixTimeline.kt | 2 +- libraries/textcomposer/build.gradle.kts | 1 + .../x/textcomposer/MessageComposerMode.kt | 9 +- 38 files changed, 1337 insertions(+), 1205 deletions(-) create mode 100644 app/src/main/java/io/element/android/x/node/RoomFlowNode.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/MessagesEvents.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt delete mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/MessagesState.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/MessagesView.kt delete mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListEvents.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListState.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt rename features/messages/src/main/java/io/element/android/x/features/messages/{model/MessagesItemAction.kt => actionlist/TimelineItemAction.kt} (64%) delete mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemActionsSheet.kt delete mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerEvents.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt rename features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/{MessageComposerViewState.kt => MessageComposerState.kt} (82%) create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt delete mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerViewModel.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineEvents.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineState.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineView.kt rename features/messages/src/main/java/io/element/android/x/features/messages/{ => timeline}/components/MessageEventBubble.kt (98%) rename features/messages/src/main/java/io/element/android/x/features/messages/{ => timeline}/components/MessagesReactionsView.kt (97%) rename features/messages/src/main/java/io/element/android/x/features/messages/{ => timeline}/components/MessagesTimelineItemEncryptedView.kt (94%) rename features/messages/src/main/java/io/element/android/x/features/messages/{ => timeline}/components/MessagesTimelineItemImageView.kt (97%) rename features/messages/src/main/java/io/element/android/x/features/messages/{ => timeline}/components/MessagesTimelineItemInformativeView.kt (96%) rename features/messages/src/main/java/io/element/android/x/features/messages/{ => timeline}/components/MessagesTimelineItemRedactedView.kt (87%) rename features/messages/src/main/java/io/element/android/x/features/messages/{ => timeline}/components/MessagesTimelineItemTextView.kt (95%) rename features/messages/src/main/java/io/element/android/x/features/messages/{ => timeline}/components/MessagesTimelineItemUnknownView.kt (94%) rename features/messages/src/main/java/io/element/android/x/features/messages/{ => timeline}/components/html/HtmlDocument.kt (99%) rename features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemActionsSheetState.kt => libraries/di/src/main/java/io/element/android/x/di/RoomScope.kt (70%) 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 index 94fd29807b..ad8ad5743a 100644 --- a/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt @@ -8,11 +8,8 @@ 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.architecture.createNode -import io.element.android.x.architecture.viewmodel.viewModelSupportNode -import io.element.android.x.features.messages.MessagesScreen import io.element.android.x.features.preferences.PreferencesFlowNode import io.element.android.x.features.roomlist.RoomListNode import io.element.android.x.matrix.core.RoomId @@ -34,7 +31,7 @@ class LoggedInFlowNode( private val roomListCallback = object : RoomListNode.Callback { override fun onRoomClicked(roomId: RoomId) { - backstack.push(NavTarget.Messages(roomId)) + backstack.push(NavTarget.Room(roomId)) } override fun onSettingsClicked() { @@ -47,7 +44,7 @@ class LoggedInFlowNode( object RoomList : NavTarget @Parcelize - data class Messages(val roomId: RoomId) : NavTarget + data class Room(val roomId: RoomId) : NavTarget @Parcelize object Settings : NavTarget @@ -58,11 +55,8 @@ class LoggedInFlowNode( NavTarget.RoomList -> { createNode(buildContext, plugins = listOf(roomListCallback)) } - is NavTarget.Messages -> viewModelSupportNode(buildContext) { - MessagesScreen( - roomId = navTarget.roomId.value, - onBackPressed = { backstack.pop() } - ) + is NavTarget.Room -> { + RoomFlowNode(buildContext, navTarget.roomId) } NavTarget.Settings -> { PreferencesFlowNode(buildContext, onOpenBugReport) diff --git a/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt b/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt new file mode 100644 index 0000000000..c7b5f21585 --- /dev/null +++ b/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt @@ -0,0 +1,52 @@ +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.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.navmodel.backstack.BackStack +import io.element.android.x.architecture.createNode +import io.element.android.x.features.messages.MessagesNode +import io.element.android.x.matrix.core.RoomId +import kotlinx.parcelize.Parcelize +import timber.log.Timber + +class RoomFlowNode( + buildContext: BuildContext, + private val roomId: RoomId, + private val backstack: BackStack = BackStack( + initialElement = NavTarget.Messages, + savedStateMap = buildContext.savedStateMap, + ), +) : ParentNode( + navModel = backstack, + buildContext = buildContext +) { + + init { + lifecycle.subscribe( + onCreate = { Timber.v("OnCreate") }, + onDestroy = { Timber.v("OnDestroy") } + ) + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Messages -> createNode(buildContext) + } + } + + sealed interface NavTarget : Parcelable { + @Parcelize + object Messages : NavTarget + } + + @Composable + override fun View(modifier: Modifier) { + Children(navModel = backstack) + } +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesEvents.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesEvents.kt new file mode 100644 index 0000000000..b70c896e9f --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesEvents.kt @@ -0,0 +1,8 @@ +package io.element.android.x.features.messages + +import io.element.android.x.features.messages.actionlist.TimelineItemAction +import io.element.android.x.features.messages.model.MessagesTimelineItemState + +sealed interface MessagesEvents { + data class HandleAction(val action: TimelineItemAction, val messageEvent: MessagesTimelineItemState.MessageEvent) : MessagesEvents +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt new file mode 100644 index 0000000000..25bd74ec97 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt @@ -0,0 +1,40 @@ +package io.element.android.x.features.messages + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.x.anvilannotations.ContributesNode +import io.element.android.x.di.SessionScope + +@ContributesNode(SessionScope::class) +class MessagesNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + //presenter: MessagesPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + /* + val state by connector.stateFlow.collectAsState() + MessagesView( + state = state, + onBackPressed = this::navigateUp, + ) + */ + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "MESSAGES NODE") + } + } +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt new file mode 100644 index 0000000000..253bc3c1c1 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt @@ -0,0 +1,91 @@ +package io.element.android.x.features.messages + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.x.architecture.Presenter +import io.element.android.x.features.messages.actionlist.ActionListPresenter +import io.element.android.x.features.messages.actionlist.TimelineItemAction +import io.element.android.x.features.messages.model.MessagesTimelineItemState +import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent +import io.element.android.x.features.messages.textcomposer.MessageComposerEvents +import io.element.android.x.features.messages.textcomposer.MessageComposerPresenter +import io.element.android.x.features.messages.textcomposer.MessageComposerState +import io.element.android.x.features.messages.timeline.TimelinePresenter +import io.element.android.x.matrix.MatrixClient +import io.element.android.x.matrix.core.RoomId +import io.element.android.x.matrix.room.MatrixRoom +import io.element.android.x.textcomposer.MessageComposerMode +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +class MessagesPresenter @Inject constructor( + private val client: MatrixClient, + private val roomId: RoomId, + private val room: MatrixRoom, + private val composerPresenter: MessageComposerPresenter, + private val timelinePresenter: TimelinePresenter, + private val actionListPresenter: ActionListPresenter, +) : Presenter { + + @Composable + override fun present(): MessagesState { + val localCoroutineScope = rememberCoroutineScope() + val composerState = composerPresenter.present() + val timelineState = timelinePresenter.present() + val actionListState = actionListPresenter.present() + + fun handleEvents(event: MessagesEvents) { + when (event) { + is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.messageEvent, composerState) + } + } + return MessagesState( + roomId = roomId, + composerState = composerState, + timelineState = timelineState, + actionListState = actionListState, + eventSink = ::handleEvents + ) + } + + fun CoroutineScope.handleTimelineAction( + action: TimelineItemAction, + targetEvent: MessagesTimelineItemState.MessageEvent, + composerState: MessageComposerState, + ) = launch { + when (action) { + TimelineItemAction.Copy -> notImplementedYet() + TimelineItemAction.Forward -> notImplementedYet() + TimelineItemAction.Redact -> handleActionRedact(targetEvent) + TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState) + TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState) + } + } + + private fun notImplementedYet() { + Timber.v("NotImplementedYet") + } + + private suspend fun handleActionRedact(event: MessagesTimelineItemState.MessageEvent) { + room.redactEvent(event.id) + } + + private fun handleActionEdit(targetEvent: MessagesTimelineItemState.MessageEvent, composerState: MessageComposerState) { + val composerMode = MessageComposerMode.Edit( + targetEvent.id, + (targetEvent.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty() + ) + composerState.eventSink( + MessageComposerEvents.SetMode(composerMode) + ) + } + + private fun handleActionReply(targetEvent: MessagesTimelineItemState.MessageEvent, composerState: MessageComposerState) { + val composerMode = MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, "") + composerState.eventSink( + MessageComposerEvents.SetMode(composerMode) + ) + } +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt deleted file mode 100644 index 7d9b8146ae..0000000000 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt +++ /dev/null @@ -1,689 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn( - ExperimentalMaterial3Api::class, - ExperimentalMaterialApi::class, - ExperimentalComposeUiApi::class -) - -package io.element.android.x.features.messages - -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBars -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.ArrowDownward -import androidx.compose.material.rememberModalBottomSheetState -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.End -import androidx.compose.ui.Alignment.Companion.Start -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.LastBaseline -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.zIndex -import com.airbnb.mvrx.compose.collectAsState -import com.airbnb.mvrx.compose.mavericksViewModel -import io.element.android.x.core.compose.LogCompositions -import io.element.android.x.core.compose.PairCombinedPreviewParameter -import io.element.android.x.core.data.StableCharSequence -import io.element.android.x.designsystem.components.avatar.Avatar -import io.element.android.x.designsystem.components.avatar.AvatarData -import io.element.android.x.features.messages.components.MessageEventBubble -import io.element.android.x.features.messages.components.MessagesReactionsView -import io.element.android.x.features.messages.components.MessagesTimelineItemEncryptedView -import io.element.android.x.features.messages.components.MessagesTimelineItemImageView -import io.element.android.x.features.messages.components.MessagesTimelineItemRedactedView -import io.element.android.x.features.messages.components.MessagesTimelineItemTextView -import io.element.android.x.features.messages.components.MessagesTimelineItemUnknownView -import io.element.android.x.features.messages.components.TimelineItemActionsScreen -import io.element.android.x.features.messages.model.AggregatedReaction -import io.element.android.x.features.messages.model.MessagesItemGroupPosition -import io.element.android.x.features.messages.model.MessagesItemGroupPositionProvider -import io.element.android.x.features.messages.model.MessagesItemReactionState -import io.element.android.x.features.messages.model.MessagesTimelineItemState -import io.element.android.x.features.messages.model.MessagesViewState -import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemContentProvider -import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent -import io.element.android.x.features.messages.textcomposer.MessageComposerViewModel -import io.element.android.x.features.messages.textcomposer.MessageComposerViewState -import io.element.android.x.textcomposer.MessageComposerMode -import io.element.android.x.textcomposer.TextComposer -import java.lang.Math.random -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.launch -import timber.log.Timber - -@Composable -fun MessagesScreen( - roomId: String, - onBackPressed: () -> Unit, - viewModel: MessagesViewModel = mavericksViewModel(argsFactory = { roomId }), - composerViewModel: MessageComposerViewModel = mavericksViewModel(argsFactory = { roomId }) -) { - fun onSendMessage(textMessage: String) { - viewModel.sendMessage(textMessage) - composerViewModel.updateText("") - } - - LogCompositions(tag = "MessagesScreen", msg = "Root") - val itemActionsBottomSheetState = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Hidden, - ) - val snackbarHostState = remember { SnackbarHostState() } - val coroutineScope = rememberCoroutineScope() - val focusManager = LocalFocusManager.current - val roomTitle by viewModel.collectAsState(MessagesViewState::roomName) - val roomAvatar by viewModel.collectAsState(MessagesViewState::roomAvatar) - val timelineItems by viewModel.collectAsState(MessagesViewState::timelineItems) - val hasMoreToLoad by viewModel.collectAsState(MessagesViewState::hasMoreToLoad) - val snackBarContent by viewModel.collectAsState(MessagesViewState::snackbarContent) - val composerMode by viewModel.collectAsState(MessagesViewState::composerMode) - val highlightedEventId by viewModel.collectAsState(MessagesViewState::highlightedEventId) - val composerFullScreen by composerViewModel.collectAsState(MessageComposerViewState::isFullScreen) - val composerCanSendMessage by composerViewModel.collectAsState(MessageComposerViewState::isSendButtonVisible) - val composerText by composerViewModel.collectAsState(MessageComposerViewState::text) - - MessagesScreenContent( - roomTitle = roomTitle, - roomAvatar = roomAvatar, - timelineItems = timelineItems().orEmpty().toImmutableList(), - hasMoreToLoad = hasMoreToLoad, - onReachedLoadMore = viewModel::loadMore, - onBackPressed = onBackPressed, - onSendMessage = ::onSendMessage, - composerFullScreen = composerFullScreen, - onComposerFullScreenChange = composerViewModel::onComposerFullScreenChange, - onComposerTextChange = composerViewModel::updateText, - composerMode = composerMode, - highlightedEventId = highlightedEventId, - onCloseSpecialMode = viewModel::setNormalMode, - composerCanSendMessage = composerCanSendMessage, - composerText = composerText, - onClick = { - Timber.v("onClick on timeline item: ${it.id}") - }, - onLongClick = { - focusManager.clearFocus(force = true) - viewModel.computeActionsSheetState(it) - coroutineScope.launch { - itemActionsBottomSheetState.show() - } - }, - snackbarHostState = snackbarHostState, - ) - TimelineItemActionsScreen( - viewModel = viewModel, - composerViewModel = composerViewModel, - modalBottomSheetState = itemActionsBottomSheetState, - ) - snackBarContent?.let { - coroutineScope.launch { - snackbarHostState.showSnackbar(it) - } - viewModel.onSnackbarShown() - } -} - -@Composable -fun MessagesScreenContent( - roomTitle: String?, - roomAvatar: AvatarData?, - timelineItems: ImmutableList, - hasMoreToLoad: Boolean, - onReachedLoadMore: () -> Unit, - onBackPressed: () -> Unit, - onSendMessage: (String) -> Unit, - onClick: (MessagesTimelineItemState.MessageEvent) -> Unit, - onLongClick: (MessagesTimelineItemState.MessageEvent) -> Unit, - composerFullScreen: Boolean, - onComposerFullScreenChange: () -> Unit, - onComposerTextChange: (CharSequence) -> Unit, - composerMode: MessageComposerMode, - highlightedEventId: String?, - onCloseSpecialMode: () -> Unit, - composerCanSendMessage: Boolean, - composerText: StableCharSequence?, - snackbarHostState: SnackbarHostState, - modifier: Modifier = Modifier, -) { - LogCompositions(tag = "MessagesScreen", msg = "Content") - Scaffold( - modifier = modifier, - contentWindowInsets = WindowInsets.statusBars, - topBar = { - MessagesTopAppBar( - roomTitle = roomTitle, - roomAvatar = roomAvatar, - onBackPressed = onBackPressed - ) - }, - content = { padding -> - MessagesContent( - modifier = Modifier.padding(padding), - timelineItems = timelineItems, - hasMoreToLoad = hasMoreToLoad, - onReachedLoadMore = onReachedLoadMore, - onSendMessage = onSendMessage, - onClick = onClick, - onLongClick = onLongClick, - highlightedEventId = highlightedEventId, - composerMode = composerMode, - onCloseSpecialMode = onCloseSpecialMode, - composerFullScreen = composerFullScreen, - onComposerFullScreenChange = onComposerFullScreenChange, - onComposerTextChange = onComposerTextChange, - composerCanSendMessage = composerCanSendMessage, - composerText = composerText - ) - }, - snackbarHost = { - SnackbarHost( - snackbarHostState, - modifier = Modifier.navigationBarsPadding() - ) - }, - ) -} - -@Composable -fun MessagesContent( - timelineItems: ImmutableList, - hasMoreToLoad: Boolean, - onReachedLoadMore: () -> Unit, - onSendMessage: (String) -> Unit, - onClick: (MessagesTimelineItemState.MessageEvent) -> Unit, - onLongClick: (MessagesTimelineItemState.MessageEvent) -> Unit, - composerMode: MessageComposerMode, - highlightedEventId: String?, - onCloseSpecialMode: () -> Unit, - composerFullScreen: Boolean, - onComposerFullScreenChange: () -> Unit, - onComposerTextChange: (CharSequence) -> Unit, - composerCanSendMessage: Boolean, - composerText: StableCharSequence?, - modifier: Modifier = Modifier -) { - val lazyListState = rememberLazyListState() - Column( - modifier = modifier - .fillMaxSize() - .navigationBarsPadding() - .imePadding() - ) { - if (!composerFullScreen) { - TimelineItems( - lazyListState = lazyListState, - timelineItems = timelineItems, - highlightedEventId = highlightedEventId, - hasMoreToLoad = hasMoreToLoad, - onReachedLoadMore = onReachedLoadMore, - modifier = Modifier.weight(1f), - onClick = onClick, - onLongClick = onLongClick - ) - } - TextComposer( - onSendMessage = onSendMessage, - fullscreen = composerFullScreen, - onFullscreenToggle = onComposerFullScreenChange, - composerMode = composerMode, - onCloseSpecialMode = onCloseSpecialMode, - onComposerTextChange = onComposerTextChange, - composerCanSendMessage = composerCanSendMessage, - composerText = composerText?.charSequence?.toString(), - modifier = Modifier - .fillMaxWidth() - .let { - if (composerFullScreen) { - it.weight(1f, fill = false) - } else { - it.wrapContentHeight(Alignment.Bottom) - } - }, - ) - } -} - -@Composable -fun MessagesTopAppBar( - roomTitle: String?, - roomAvatar: AvatarData?, - modifier: Modifier = Modifier, - onBackPressed: () -> Unit = {}, -) { - TopAppBar( - modifier = modifier, - navigationIcon = { - IconButton(onClick = onBackPressed) { - Icon( - imageVector = Icons.Filled.ArrowBack, - contentDescription = "Back" - ) - } - }, - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - if (roomAvatar != null) { - Avatar(roomAvatar) - Spacer(modifier = Modifier.width(8.dp)) - } - Text( - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - text = roomTitle ?: "Unknown room", - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - - ) -} - -@Composable -fun TimelineItems( - lazyListState: LazyListState, - timelineItems: ImmutableList, - highlightedEventId: String?, - modifier: Modifier = Modifier, - hasMoreToLoad: Boolean = false, - onClick: (MessagesTimelineItemState.MessageEvent) -> Unit = {}, - onLongClick: ((MessagesTimelineItemState.MessageEvent)) -> Unit = {}, - onReachedLoadMore: () -> Unit = {}, -) { - Box(modifier = modifier.fillMaxWidth()) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = lazyListState, - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Bottom, - reverseLayout = true - ) { - items( - items = timelineItems, - contentType = { timelineItem -> timelineItem.contentType() }, - key = { timelineItem -> timelineItem.key() }, - ) { timelineItem -> - TimelineItemRow( - timelineItem = timelineItem, - isHighlighted = timelineItem.key() == highlightedEventId, - onClick = onClick, - onLongClick = onLongClick - ) - } - if (hasMoreToLoad) { - item { - MessagesLoadingMoreIndicator() - } - } - } - MessagesScrollHelper( - lazyListState = lazyListState, - timelineItems = timelineItems, - onLoadMore = onReachedLoadMore - ) - } -} - -private fun MessagesTimelineItemState.key(): String { - return when (this) { - is MessagesTimelineItemState.MessageEvent -> id - is MessagesTimelineItemState.Virtual -> id - } -} - -private fun MessagesTimelineItemState.contentType(): Int { - return when (this) { - is MessagesTimelineItemState.MessageEvent -> 0 - is MessagesTimelineItemState.Virtual -> 1 - } -} - -@Composable -fun TimelineItemRow( - timelineItem: MessagesTimelineItemState, - isHighlighted: Boolean, - onClick: (MessagesTimelineItemState.MessageEvent) -> Unit, - onLongClick: (MessagesTimelineItemState.MessageEvent) -> Unit, -) { - when (timelineItem) { - is MessagesTimelineItemState.Virtual -> return - is MessagesTimelineItemState.MessageEvent -> MessageEventRow( - messageEvent = timelineItem, - isHighlighted = isHighlighted, - onClick = { onClick(timelineItem) }, - onLongClick = { onLongClick(timelineItem) } - ) - } -} - -@Composable -fun MessageEventRow( - messageEvent: MessagesTimelineItemState.MessageEvent, - isHighlighted: Boolean, - onClick: () -> Unit, - onLongClick: () -> Unit, - modifier: Modifier = Modifier -) { - val interactionSource = remember { MutableInteractionSource() } - val (parentAlignment, contentAlignment) = if (messageEvent.isMine) { - Pair(Alignment.CenterEnd, End) - } else { - Pair(Alignment.CenterStart, Start) - } - Box( - modifier = modifier - .fillMaxWidth() - .wrapContentHeight(), - contentAlignment = parentAlignment - ) { - Row { - if (!messageEvent.isMine) { - Spacer(modifier = Modifier.width(16.dp)) - } - Column(horizontalAlignment = contentAlignment) { - if (messageEvent.showSenderInformation) { - MessageSenderInformation( - messageEvent.safeSenderName, - messageEvent.senderAvatar, - Modifier.zIndex(1f) - ) - } - MessageEventBubble( - groupPosition = messageEvent.groupPosition, - isMine = messageEvent.isMine, - interactionSource = interactionSource, - isHighlighted = isHighlighted, - onClick = onClick, - onLongClick = onLongClick, - modifier = Modifier - .zIndex(-1f) - .widthIn(max = 320.dp) - ) { - val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) - when (messageEvent.content) { - is MessagesTimelineItemEncryptedContent -> MessagesTimelineItemEncryptedView( - content = messageEvent.content, - modifier = contentModifier - ) - is MessagesTimelineItemRedactedContent -> MessagesTimelineItemRedactedView( - content = messageEvent.content, - modifier = contentModifier - ) - is MessagesTimelineItemTextBasedContent -> MessagesTimelineItemTextView( - content = messageEvent.content, - interactionSource = interactionSource, - modifier = contentModifier, - onTextClicked = onClick, - onTextLongClicked = onLongClick - ) - is MessagesTimelineItemUnknownContent -> MessagesTimelineItemUnknownView( - content = messageEvent.content, - modifier = contentModifier - ) - is MessagesTimelineItemImageContent -> MessagesTimelineItemImageView( - content = messageEvent.content, - modifier = contentModifier - ) - } - } - MessagesReactionsView( - reactionsState = messageEvent.reactionsState, - modifier = Modifier - .zIndex(1f) - .offset(x = if (messageEvent.isMine) 0.dp else 20.dp, y = -(16.dp)) - ) - } - if (messageEvent.isMine) { - Spacer(modifier = Modifier.width(16.dp)) - } - } - } - if (messageEvent.groupPosition.isNew()) { - Spacer(modifier = modifier.height(8.dp)) - } else { - Spacer(modifier = modifier.height(2.dp)) - } -} - -@Composable -private fun MessageSenderInformation( - sender: String, - senderAvatar: AvatarData?, - modifier: Modifier = Modifier -) { - Row(modifier = modifier) { - if (senderAvatar != null) { - Avatar(senderAvatar) - Spacer(modifier = Modifier.width(4.dp)) - } - Text( - text = sender, - style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .alignBy(LastBaseline) - ) - } -} - -@Composable -internal fun BoxScope.MessagesScrollHelper( - lazyListState: LazyListState, - timelineItems: ImmutableList, - onLoadMore: () -> Unit = {}, -) { - val coroutineScope = rememberCoroutineScope() - val firstVisibleItemIndex by remember { derivedStateOf { lazyListState.firstVisibleItemIndex } } - - // Auto-scroll when new timeline items appear - LaunchedEffect(timelineItems, firstVisibleItemIndex) { - if (!lazyListState.isScrollInProgress && - firstVisibleItemIndex < 2 - ) coroutineScope.launch { - lazyListState.animateScrollToItem(0) - } - } - - // Handle load more preloading - val loadMore by remember { - derivedStateOf { - val layoutInfo = lazyListState.layoutInfo - val totalItemsNumber = layoutInfo.totalItemsCount - val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 - lastVisibleItemIndex > (totalItemsNumber - 30) - } - } - - LaunchedEffect(loadMore) { - snapshotFlow { loadMore } - .distinctUntilChanged() - .collect { - onLoadMore() - } - } - - // Jump to bottom button - if (firstVisibleItemIndex > 2) { - FloatingActionButton( - onClick = { - coroutineScope.launch { - if (firstVisibleItemIndex > 10) { - lazyListState.scrollToItem(0) - } else { - lazyListState.animateScrollToItem(0) - } - } - }, - shape = CircleShape, - modifier = Modifier - .align(Alignment.BottomCenter) - .size(40.dp), - containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant - ) { - Icon(Icons.Default.ArrowDownward, "") - } - } -} - -@Composable -internal fun MessagesLoadingMoreIndicator() { - Box( - Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(8.dp), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator( - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.primary - ) - } -} - -class MessagesItemGroupPositionToMessagesTimelineItemContentProvider : - PairCombinedPreviewParameter( - MessagesItemGroupPositionProvider() to MessagesTimelineItemContentProvider() - ) - -@Suppress("PreviewPublic") -@Preview(showBackground = true) -@Composable -fun TimelineItemsPreview( - @PreviewParameter(MessagesTimelineItemContentProvider::class) - content: MessagesTimelineItemContent -) { - TimelineItems( - lazyListState = LazyListState(), - timelineItems = persistentListOf( - // 3 items (First Middle Last) with isMine = false - createMessageEvent( - isMine = false, - content = content, - groupPosition = MessagesItemGroupPosition.First - ), - createMessageEvent( - isMine = false, - content = content, - groupPosition = MessagesItemGroupPosition.Middle - ), - createMessageEvent( - isMine = false, - content = content, - groupPosition = MessagesItemGroupPosition.Last - ), - // 3 items (First Middle Last) with isMine = true - createMessageEvent( - isMine = true, - content = content, - groupPosition = MessagesItemGroupPosition.First - ), - createMessageEvent( - isMine = true, - content = content, - groupPosition = MessagesItemGroupPosition.Middle - ), - createMessageEvent( - isMine = true, - content = content, - groupPosition = MessagesItemGroupPosition.Last - ), - ), - highlightedEventId = null, - hasMoreToLoad = true, - ) -} - -private fun createMessageEvent( - isMine: Boolean, - content: MessagesTimelineItemContent, - groupPosition: MessagesItemGroupPosition -): MessagesTimelineItemState { - return MessagesTimelineItemState.MessageEvent( - id = random().toString(), - senderId = "senderId", - senderAvatar = AvatarData("sender"), - content = content, - reactionsState = MessagesItemReactionState( - listOf( - AggregatedReaction("👍", "1") - ) - ), - isMine = isMine, - senderDisplayName = "sender", - groupPosition = groupPosition, - ) -} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesState.kt new file mode 100644 index 0000000000..a7fcefb88f --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesState.kt @@ -0,0 +1,19 @@ +package io.element.android.x.features.messages + +import androidx.compose.runtime.Immutable +import io.element.android.x.designsystem.components.avatar.AvatarData +import io.element.android.x.features.messages.actionlist.ActionListState +import io.element.android.x.features.messages.textcomposer.MessageComposerState +import io.element.android.x.features.messages.timeline.TimelineState +import io.element.android.x.matrix.core.RoomId + +@Immutable +data class MessagesState( + val roomId: RoomId, + val roomName: String? = null, + val roomAvatar: AvatarData? = null, + val composerState: MessageComposerState, + val timelineState: TimelineState, + val actionListState: ActionListState, + val eventSink: (MessagesEvents) -> Unit = {} +) diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesView.kt new file mode 100644 index 0000000000..6e8a761b28 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesView.kt @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn( + ExperimentalMaterial3Api::class, + ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class, +) + +package io.element.android.x.features.messages + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.x.core.compose.LogCompositions +import io.element.android.x.designsystem.components.avatar.Avatar +import io.element.android.x.designsystem.components.avatar.AvatarData +import io.element.android.x.features.messages.actionlist.TimelineItemAction +import io.element.android.x.features.messages.actionlist.ActionListView +import io.element.android.x.features.messages.model.MessagesTimelineItemState +import io.element.android.x.features.messages.textcomposer.MessageComposerView +import io.element.android.x.features.messages.timeline.TimelineView +import timber.log.Timber + +@Composable +fun MessagesView( + state: MessagesState, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit, +) { + + LogCompositions(tag = "MessagesScreen", msg = "Root") + val itemActionsBottomSheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + ) + val snackbarHostState = remember { SnackbarHostState() } + + LogCompositions(tag = "MessagesScreen", msg = "Content") + Scaffold( + modifier = modifier, + contentWindowInsets = WindowInsets.statusBars, + topBar = { + MessagesViewTopBar( + roomTitle = state.roomName, + roomAvatar = state.roomAvatar, + onBackPressed = onBackPressed + ) + }, + content = { padding -> + MessagesViewContent( + state = state, + modifier = Modifier.padding(padding), + ) + }, + snackbarHost = { + SnackbarHost( + snackbarHostState, + modifier = Modifier.navigationBarsPadding() + ) + }, + ) + + fun onActionSelected(action: TimelineItemAction, messageEvent: MessagesTimelineItemState.MessageEvent) { + state.eventSink(MessagesEvents.HandleAction(action, messageEvent)) + } + + ActionListView( + state = state.actionListState, + modalBottomSheetState = itemActionsBottomSheetState, + onActionSelected = ::onActionSelected + ) +} + +@Composable +fun MessagesViewContent( + state: MessagesState, + modifier: Modifier = Modifier +) { + + fun onMessageClicked(messageEvent: MessagesTimelineItemState.MessageEvent) { + Timber.v("OnMessageClicked= $messageEvent") + } + + fun onMessageLongClicked(messageEvent: MessagesTimelineItemState.MessageEvent) { + Timber.v("OnMessageLongClicked= $messageEvent") + } + + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding() + .imePadding() + ) { + if (!state.composerState.isFullScreen) { + TimelineView( + state = state.timelineState, + modifier = Modifier.fillMaxWidth(), + onMessageClicked = ::onMessageClicked, + onMessageLongClicked = ::onMessageLongClicked + ) + } + MessageComposerView( + state = state.composerState, + modifier = Modifier + .fillMaxWidth() + .let { + if (state.composerState.isFullScreen) { + it.weight(1f, fill = false) + } else { + it.wrapContentHeight(Alignment.Bottom) + } + }, + ) + } +} + +@Composable +fun MessagesViewTopBar( + roomTitle: String?, + roomAvatar: AvatarData?, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, +) { + TopAppBar( + modifier = modifier, + navigationIcon = { + IconButton(onClick = onBackPressed) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + if (roomAvatar != null) { + Avatar(roomAvatar) + Spacer(modifier = Modifier.width(8.dp)) + } + Text( + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + text = roomTitle ?: "Unknown room", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + ) +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt deleted file mode 100644 index c7171b7754..0000000000 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.features.messages - -import com.airbnb.mvrx.MavericksViewModel -import com.airbnb.mvrx.MavericksViewModelFactory -import com.airbnb.mvrx.Uninitialized -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory -import io.element.android.x.designsystem.components.avatar.AvatarSize -import io.element.android.x.di.SessionScope -import io.element.android.x.features.messages.model.MessagesItemAction -import io.element.android.x.features.messages.model.MessagesItemActionsSheetState -import io.element.android.x.features.messages.model.MessagesTimelineItemState -import io.element.android.x.features.messages.model.MessagesViewState -import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent -import io.element.android.x.matrix.MatrixClient -import io.element.android.x.matrix.timeline.MatrixTimeline -import io.element.android.x.matrix.timeline.MatrixTimelineItem -import io.element.android.x.matrix.ui.MatrixItemHelper -import io.element.android.x.textcomposer.MessageComposerMode -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch - -private const val PAGINATION_COUNT = 50 - -@ContributesViewModel(SessionScope::class) -class MessagesViewModel @AssistedInject constructor( - private val client: MatrixClient, - @Assisted private val initialState: MessagesViewState -) : - MavericksViewModel(initialState) { - - companion object : MavericksViewModelFactory by daggerMavericksViewModelFactory() - - private val matrixItemHelper = MatrixItemHelper(client) - private val room = client.getRoom(initialState.roomId)!! - private val messageTimelineItemStateFactory = - MessageTimelineItemStateFactory(matrixItemHelper, room, Dispatchers.Default) - private val timeline = room.timeline() - - private val timelineCallback = object : MatrixTimeline.Callback { - override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) { - viewModelScope.launch { - messageTimelineItemStateFactory.pushItem(timelineItem) - } - } - } - - init { - handleInit() - } - - fun loadMore() { - viewModelScope.launch { - timeline.paginateBackwards(PAGINATION_COUNT) - setState { copy(hasMoreToLoad = timeline.hasMoreToLoad) } - } - } - - fun sendMessage(text: String) { - viewModelScope.launch { - val state = awaitState() - // Reset composer right away - setNormalMode() - when (state.composerMode) { - is MessageComposerMode.Normal -> timeline.sendMessage(text) - is MessageComposerMode.Edit -> timeline.editMessage( - state.composerMode.eventId, - text - ) - is MessageComposerMode.Quote -> TODO() - is MessageComposerMode.Reply -> timeline.replyMessage( - state.composerMode.eventId, - text - ) - } - } - } - - suspend fun getTargetEvent(): MessagesTimelineItemState.MessageEvent? { - val currentState = awaitState() - return currentState.itemActionsSheetState.invoke()?.targetItem - } - - fun handleItemAction( - action: MessagesItemAction, - targetEvent: MessagesTimelineItemState.MessageEvent - ) { - viewModelScope.launch(Dispatchers.Default) { - when (action) { - MessagesItemAction.Copy -> notImplementedYet() - MessagesItemAction.Forward -> notImplementedYet() - MessagesItemAction.Redact -> handleActionRedact(targetEvent) - MessagesItemAction.Edit -> handleActionEdit(targetEvent) - MessagesItemAction.Reply -> handleActionReply(targetEvent) - } - } - } - - fun setNormalMode() { - setComposerMode(MessageComposerMode.Normal("")) - } - - fun onSnackbarShown() { - setSnackbarContent(null) - } - - fun computeActionsSheetState(messagesTimelineItemState: MessagesTimelineItemState.MessageEvent?) { - if (messagesTimelineItemState == null) { - setState { copy(itemActionsSheetState = Uninitialized) } - return - } - suspend { - val actions = - if (messagesTimelineItemState.content is MessagesTimelineItemRedactedContent) { - emptyList() - } else { - mutableListOf( - MessagesItemAction.Reply, - MessagesItemAction.Forward, - MessagesItemAction.Copy, - ).also { - if (messagesTimelineItemState.isMine) { - it.add(MessagesItemAction.Edit) - it.add(MessagesItemAction.Redact) - } - } - } - MessagesItemActionsSheetState( - targetItem = messagesTimelineItemState, - actions = actions - ) - }.execute(Dispatchers.Default) { - copy(itemActionsSheetState = it) - } - } - - private fun handleInit() { - timeline.initialize() - timeline.callback = timelineCallback - room.syncUpdateFlow() - .onEach { - val avatarData = - matrixItemHelper.loadAvatarData( - room = room, - size = AvatarSize.SMALL - ) - setState { - copy( - roomName = room.name, roomAvatar = avatarData, - ) - } - }.launchIn(viewModelScope) - - timeline - .timelineItems() - .onEach(messageTimelineItemStateFactory::replaceWith) - .launchIn(viewModelScope) - - messageTimelineItemStateFactory - .flow() - .execute { - copy(timelineItems = it) - } - } - - private fun setSnackbarContent(message: String?) { - setState { copy(snackbarContent = message) } - } - - private fun handleActionRedact(event: MessagesTimelineItemState.MessageEvent) { - viewModelScope.launch { - room.redactEvent(event.id) - } - } - - private fun handleActionEdit(targetEvent: MessagesTimelineItemState.MessageEvent) { - setComposerMode( - MessageComposerMode.Edit( - targetEvent.id, - (targetEvent.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty() - ) - ) - } - - private fun handleActionReply(targetEvent: MessagesTimelineItemState.MessageEvent) { - setComposerMode(MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, "")) - } - - private fun setComposerMode(mode: MessageComposerMode) { - setState { - copy( - composerMode = mode, - highlightedEventId = mode.relatedEventId - ) - } - } - - private fun notImplementedYet() { - setSnackbarContent("Not implemented yet!") - } - - override fun onCleared() { - super.onCleared() - timeline.callback = null - timeline.dispose() - } -} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListEvents.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListEvents.kt new file mode 100644 index 0000000000..07554c88b0 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListEvents.kt @@ -0,0 +1,8 @@ +package io.element.android.x.features.messages.actionlist + +import io.element.android.x.features.messages.model.MessagesTimelineItemState + +sealed interface ActionListEvents { + object Clear : ActionListEvents + data class ComputeForMessage(val messageEvent: MessagesTimelineItemState.MessageEvent) : ActionListEvents +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt new file mode 100644 index 0000000000..f8fac36a89 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt @@ -0,0 +1,59 @@ +package io.element.android.x.features.messages.actionlist + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.x.architecture.Presenter +import io.element.android.x.features.messages.model.MessagesTimelineItemState +import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ActionListPresenter @Inject constructor() : Presenter { + + @Composable + override fun present(): ActionListState { + + val localCoroutineScope = rememberCoroutineScope() + + val target: MutableState = remember { + mutableStateOf(ActionListState.Target.None) + } + + fun handleEvents(event: ActionListEvents) { + when (event) { + ActionListEvents.Clear -> target.value = ActionListState.Target.None + is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(event.messageEvent, target) + } + } + + return ActionListState( + target = target.value, + eventSink = ::handleEvents + ) + } + + fun CoroutineScope.computeForMessage(messagesTimelineItemState: MessagesTimelineItemState.MessageEvent, target: MutableState) = launch { + target.value = ActionListState.Target.Loading(messagesTimelineItemState) + val actions = + if (messagesTimelineItemState.content is MessagesTimelineItemRedactedContent) { + emptyList() + } else { + mutableListOf( + TimelineItemAction.Reply, + TimelineItemAction.Forward, + TimelineItemAction.Copy, + ).also { + if (messagesTimelineItemState.isMine) { + it.add(TimelineItemAction.Edit) + it.add(TimelineItemAction.Redact) + } + } + } + target.value = ActionListState.Target.Success(messagesTimelineItemState, actions.toImmutableList()) + } +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListState.kt new file mode 100644 index 0000000000..d81a794fa0 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListState.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.features.messages.actionlist + +import androidx.compose.runtime.Immutable +import io.element.android.x.features.messages.model.MessagesTimelineItemState +import kotlinx.collections.immutable.ImmutableList + +@Immutable +data class ActionListState( + val target: Target = Target.None, + val eventSink: (ActionListEvents) -> Unit = {}, +) { + + sealed interface Target { + object None : Target + data class Loading(val messageEvent: MessagesTimelineItemState.MessageEvent) : Target + data class Success( + val messageEvent: MessagesTimelineItemState.MessageEvent, + val actions: ImmutableList, + ) : Target + } +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt new file mode 100644 index 0000000000..06a3824686 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt @@ -0,0 +1,116 @@ +@file:OptIn(ExperimentalMaterialApi::class) + +package io.element.android.x.features.messages.actionlist + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ListItem +import androidx.compose.material.LocalContentColor +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import io.element.android.x.designsystem.components.VectorIcon +import io.element.android.x.features.messages.model.MessagesTimelineItemState +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch + +@Composable +fun ActionListView( + state: ActionListState, + modalBottomSheetState: ModalBottomSheetState, + onActionSelected: (action: TimelineItemAction, MessagesTimelineItemState.MessageEvent) -> Unit, + modifier: Modifier = Modifier +) { + val coroutineScope = rememberCoroutineScope() + LaunchedEffect(modalBottomSheetState) { + snapshotFlow { modalBottomSheetState.currentValue } + .filter { it == ModalBottomSheetValue.Hidden } + .collect { + state.eventSink(ActionListEvents.Clear) + } + } + + fun onItemActionClicked( + itemAction: TimelineItemAction, + targetItem: MessagesTimelineItemState.MessageEvent + ) { + onActionSelected(itemAction, targetItem) + coroutineScope.launch { + modalBottomSheetState.hide() + } + } + + ModalBottomSheetLayout( + modifier = modifier, + sheetState = modalBottomSheetState, + sheetContent = { + SheetContent( + state = state, + onActionClicked = ::onItemActionClicked, + modifier = Modifier + .navigationBarsPadding() + .imePadding() + ) + } + ) {} +} + +@Composable +private fun SheetContent( + state: ActionListState, + modifier: Modifier = Modifier, + onActionClicked: (TimelineItemAction, MessagesTimelineItemState.MessageEvent) -> Unit = { _, _ -> }, +) { + when (val target = state.target) { + is ActionListState.Target.Loading, + ActionListState.Target.None -> { + // Crashes if sheetContent size is zero + Box(modifier = modifier.size(1.dp)) + } + is ActionListState.Target.Success -> { + val actions = target.actions + LazyColumn( + modifier = modifier.fillMaxWidth() + ) { + items( + items = actions, + ) { action -> + ListItem( + modifier = Modifier.clickable { + onActionClicked(action, target.messageEvent) + }, + text = { + Text( + text = action.title, + color = if (action.destructive) MaterialTheme.colors.error else Color.Unspecified, + ) + }, + icon = { + VectorIcon( + resourceId = action.icon, + tint = if (action.destructive) MaterialTheme.colors.error else LocalContentColor.current, + ) + } + ) + } + } + } + } +} + diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemAction.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/TimelineItemAction.kt similarity index 64% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemAction.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/actionlist/TimelineItemAction.kt index 151f6be67a..36ce779ab3 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemAction.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/TimelineItemAction.kt @@ -14,21 +14,21 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model +package io.element.android.x.features.messages.actionlist import androidx.annotation.DrawableRes -import androidx.compose.runtime.Stable +import androidx.compose.runtime.Immutable import io.element.android.x.designsystem.VectorIcons -@Stable -sealed class MessagesItemAction( +@Immutable +sealed class TimelineItemAction( val title: String, @DrawableRes val icon: Int, val destructive: Boolean = false ) { - object Forward : MessagesItemAction("Forward", VectorIcons.ArrowForward) - object Copy : MessagesItemAction("Copy", VectorIcons.Copy) - object Redact : MessagesItemAction("Redact", VectorIcons.Delete, destructive = true) - object Reply : MessagesItemAction("Reply", VectorIcons.Reply) - object Edit : MessagesItemAction("Edit", VectorIcons.Edit) + object Forward : TimelineItemAction("Forward", VectorIcons.ArrowForward) + object Copy : TimelineItemAction("Copy", VectorIcons.Copy) + object Redact : TimelineItemAction("Redact", VectorIcons.Delete, destructive = true) + object Reply : TimelineItemAction("Reply", VectorIcons.Reply) + object Edit : TimelineItemAction("Edit", VectorIcons.Edit) } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemActionsSheet.kt b/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemActionsSheet.kt deleted file mode 100644 index f5bdb70471..0000000000 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemActionsSheet.kt +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn(ExperimentalMaterialApi::class) - -package io.element.android.x.features.messages.components - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ListItem -import androidx.compose.material.LocalContentColor -import androidx.compose.material.MaterialTheme -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.airbnb.mvrx.compose.collectAsState -import io.element.android.x.designsystem.components.VectorIcon -import io.element.android.x.features.messages.MessagesViewModel -import io.element.android.x.features.messages.model.MessagesItemAction -import io.element.android.x.features.messages.model.MessagesItemActionsSheetState -import io.element.android.x.features.messages.model.MessagesTimelineItemState -import io.element.android.x.features.messages.model.MessagesViewState -import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent -import io.element.android.x.features.messages.textcomposer.MessageComposerViewModel -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.launch - -@Composable -fun TimelineItemActionsScreen( - viewModel: MessagesViewModel, - composerViewModel: MessageComposerViewModel, - modalBottomSheetState: ModalBottomSheetState, - modifier: Modifier = Modifier -) { - val coroutineScope = rememberCoroutineScope() - LaunchedEffect(modalBottomSheetState) { - snapshotFlow { modalBottomSheetState.currentValue } - .filter { it == ModalBottomSheetValue.Hidden } - .collect { - viewModel.computeActionsSheetState(null) - } - } - - val itemActionsSheetState by viewModel.collectAsState(MessagesViewState::itemActionsSheetState) - - fun onItemActionClicked( - itemAction: MessagesItemAction, - targetItem: MessagesTimelineItemState.MessageEvent - ) { - viewModel.handleItemAction(itemAction, targetItem) - coroutineScope.launch { - val targetEvent = viewModel.getTargetEvent() - when (itemAction) { - is MessagesItemAction.Edit -> { - // Entering Edit mode, update the text in the composer. - val newComposerText = - (targetEvent?.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty() - composerViewModel.updateText(newComposerText) - } - else -> Unit - } - modalBottomSheetState.hide() - } - } - - ModalBottomSheetLayout( - modifier = modifier, - sheetState = modalBottomSheetState, - sheetContent = { - SheetContent( - actionsSheetState = itemActionsSheetState(), - onActionClicked = ::onItemActionClicked, - modifier = Modifier - .navigationBarsPadding() - .imePadding() - ) - } - ) {} -} - -@Composable -private fun SheetContent( - actionsSheetState: MessagesItemActionsSheetState?, - modifier: Modifier = Modifier, - onActionClicked: (MessagesItemAction, MessagesTimelineItemState.MessageEvent) -> Unit = { _, _ -> }, -) { - if (actionsSheetState == null || actionsSheetState.actions.isEmpty()) { - // Crashes if sheetContent size is zero - Box(modifier = modifier.size(1.dp)) - } else { - LazyColumn( - modifier = modifier - .fillMaxWidth() - ) { - items(actionsSheetState.actions) { - ListItem( - modifier = Modifier.clickable { - onActionClicked(it, actionsSheetState.targetItem) - }, - text = { - Text( - text = it.title, - color = if (it.destructive) MaterialTheme.colors.error else Color.Unspecified, - ) - }, - icon = { - VectorIcon( - resourceId = it.icon, - tint = if (it.destructive) MaterialTheme.colors.error else LocalContentColor.current, - ) - } - ) - } - } - } -} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt deleted file mode 100644 index 4441180ce0..0000000000 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.features.messages.model - -import androidx.compose.runtime.Stable -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MavericksState -import com.airbnb.mvrx.Uninitialized -import io.element.android.x.designsystem.components.avatar.AvatarData -import io.element.android.x.textcomposer.MessageComposerMode - -@Stable -data class MessagesViewState( - val roomId: String, - val roomName: String? = null, - val roomAvatar: AvatarData? = null, - val timelineItems: Async> = Uninitialized, - val hasMoreToLoad: Boolean = true, - val itemActionsSheetState: Async = Uninitialized, - val snackbarContent: String? = null, - val highlightedEventId: String? = null, - val composerMode: MessageComposerMode = MessageComposerMode.Normal(""), -) : MavericksState { - - @Suppress("unused") - constructor(roomId: String) : this( - roomId = roomId, - roomName = null, - roomAvatar = null - ) -} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerEvents.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerEvents.kt new file mode 100644 index 0000000000..a558b88e81 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerEvents.kt @@ -0,0 +1,11 @@ +package io.element.android.x.features.messages.textcomposer + +import io.element.android.x.textcomposer.MessageComposerMode + +sealed interface MessageComposerEvents { + object ToggleFullScreenState : MessageComposerEvents + data class SendMessage(val message: String) : MessageComposerEvents + object CloseSpecialMode : MessageComposerEvents + data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents + data class UpdateText(val text: CharSequence) : MessageComposerEvents +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt new file mode 100644 index 0000000000..2c51a0199b --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt @@ -0,0 +1,73 @@ +package io.element.android.x.features.messages.textcomposer + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.x.architecture.Presenter +import io.element.android.x.core.data.toStableCharSequence +import io.element.android.x.matrix.MatrixClient +import io.element.android.x.matrix.room.MatrixRoom +import io.element.android.x.textcomposer.MessageComposerMode +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class MessageComposerPresenter @Inject constructor( + private val appCoroutineScope: CoroutineScope, + private val client: MatrixClient, + private val room: MatrixRoom +) : Presenter { + + @Composable + override fun present(): MessageComposerState { + val isFullScreen = rememberSaveable { + mutableStateOf(false) + } + val text: MutableState = rememberSaveable { + mutableStateOf("") + } + val composerMode: MutableState = rememberSaveable { + mutableStateOf(MessageComposerMode.Normal("")) + } + + fun handleEvents(event: MessageComposerEvents) { + when (event) { + MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value + is MessageComposerEvents.UpdateText -> text.value = event.text + MessageComposerEvents.CloseSpecialMode -> composerMode.setToNormal() + is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode) + is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode + } + } + + return MessageComposerState( + text = text.value.toStableCharSequence(), + isFullScreen = isFullScreen.value, + mode = composerMode.value, + eventSink = ::handleEvents + ) + } + + private fun MutableState.setToNormal() { + value = MessageComposerMode.Normal("") + } + + private fun CoroutineScope.sendMessage(text: String, composerMode: MutableState) = launch { + val capturedMode = composerMode.value + // Reset composer right away + composerMode.setToNormal() + when (capturedMode) { + is MessageComposerMode.Normal -> room.sendMessage(text) + is MessageComposerMode.Edit -> room.editMessage( + capturedMode.eventId, + text + ) + is MessageComposerMode.Quote -> TODO() + is MessageComposerMode.Reply -> room.replyMessage( + capturedMode.eventId, + text + ) + } + } +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerViewState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerState.kt similarity index 82% rename from features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerViewState.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerState.kt index b85ce0da0c..dbd92f6139 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerViewState.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerState.kt @@ -16,12 +16,12 @@ package io.element.android.x.features.messages.textcomposer -import androidx.compose.runtime.Stable -import com.airbnb.mvrx.MavericksState +import androidx.compose.runtime.Immutable import io.element.android.x.core.data.StableCharSequence +import io.element.android.x.textcomposer.MessageComposerMode -@Stable -data class MessageComposerViewState( +@Immutable +data class MessageComposerState( // val roomId: String, // val canSendMessage: CanSendStatus = CanSendStatus.Allowed, val isSendButtonVisible: Boolean = false, @@ -32,4 +32,6 @@ data class MessageComposerViewState( // val voiceBroadcastState: VoiceBroadcastState? = null, val text: StableCharSequence? = null, val isFullScreen: Boolean = false, -) : MavericksState + val mode: MessageComposerMode = MessageComposerMode.Normal(""), + val eventSink: (MessageComposerEvents) -> Unit = {} +) diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt new file mode 100644 index 0000000000..74402a9e6d --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt @@ -0,0 +1,40 @@ +package io.element.android.x.features.messages.textcomposer + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.x.textcomposer.TextComposer + +@Composable +fun MessageComposerView( + state: MessageComposerState, + modifier: Modifier +) { + + fun onFullscreenToggle() { + state.eventSink(MessageComposerEvents.ToggleFullScreenState) + } + + fun sendMessage(message: String) { + state.eventSink(MessageComposerEvents.SendMessage(message)) + } + + fun onCloseSpecialMode() { + state.eventSink(MessageComposerEvents.CloseSpecialMode) + } + + fun onComposerTextChange(text: CharSequence) { + state.eventSink(MessageComposerEvents.UpdateText(text)) + } + + TextComposer( + onSendMessage = ::sendMessage, + fullscreen = state.isFullScreen, + onFullscreenToggle = ::onFullscreenToggle, + composerMode = state.mode, + onCloseSpecialMode = ::onCloseSpecialMode, + onComposerTextChange = ::onComposerTextChange, + composerCanSendMessage = state.isSendButtonVisible, + composerText = state.text?.charSequence?.toString(), + modifier = modifier + ) +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerViewModel.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerViewModel.kt deleted file mode 100644 index f4c83843dc..0000000000 --- a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerViewModel.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.features.messages.textcomposer - -import com.airbnb.mvrx.MavericksViewModel -import com.airbnb.mvrx.MavericksViewModelFactory -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory -import io.element.android.x.core.data.StableCharSequence -import io.element.android.x.di.SessionScope -import io.element.android.x.matrix.MatrixClient - -@ContributesViewModel(SessionScope::class) -class MessageComposerViewModel @AssistedInject constructor( - private val client: MatrixClient, - @Assisted private val initialState: MessageComposerViewState -) : MavericksViewModel(initialState) { - - companion object : - MavericksViewModelFactory by daggerMavericksViewModelFactory() - - fun onComposerFullScreenChange() { - setState { - copy( - isFullScreen = !isFullScreen - ) - } - } - - fun updateText(newText: CharSequence) { - setState { - copy( - text = StableCharSequence(newText), - isSendButtonVisible = newText.isNotEmpty(), - ) - } - } -} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineEvents.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineEvents.kt new file mode 100644 index 0000000000..5e978a38db --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineEvents.kt @@ -0,0 +1,8 @@ +package io.element.android.x.features.messages.timeline + +import io.element.android.x.matrix.core.EventId + +sealed interface TimelineEvents { + object LoadMore : TimelineEvents + data class SetHighlightedEvent(val eventId: EventId?): TimelineEvents +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt new file mode 100644 index 0000000000..35167d82d8 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt @@ -0,0 +1,97 @@ +package io.element.android.x.features.messages.timeline + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.x.architecture.Async +import io.element.android.x.architecture.Presenter +import io.element.android.x.features.messages.MessageTimelineItemStateFactory +import io.element.android.x.matrix.MatrixClient +import io.element.android.x.matrix.core.EventId +import io.element.android.x.matrix.room.MatrixRoom +import io.element.android.x.matrix.timeline.MatrixTimeline +import io.element.android.x.matrix.timeline.MatrixTimelineItem +import io.element.android.x.matrix.ui.MatrixItemHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject + +private const val PAGINATION_COUNT = 50 + +class TimelinePresenter @Inject constructor( + private val appCoroutineScope: CoroutineScope, + private val client: MatrixClient, + private val room: MatrixRoom +) : Presenter { + + private val timeline = room.timeline() + private val matrixItemHelper = MatrixItemHelper(client) + private val messageTimelineItemStateFactory = + MessageTimelineItemStateFactory(matrixItemHelper, room, Dispatchers.Default) + + private class TimelineCallback(private val coroutineScope: CoroutineScope, private val messageTimelineItemStateFactory: MessageTimelineItemStateFactory) : MatrixTimeline.Callback { + override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) { + coroutineScope.launch { + messageTimelineItemStateFactory.pushItem(timelineItem) + } + } + } + + @Composable + override fun present(): TimelineState { + + val localCoroutineScope = rememberCoroutineScope() + val hasMoreToLoad = rememberSaveable { + mutableStateOf(timeline.hasMoreToLoad) + } + val highlightedEventId: MutableState = rememberSaveable { + mutableStateOf(null) + } + val timelineItems = messageTimelineItemStateFactory + .flow() + .collectAsState(emptyList()) + + fun handleEvents(event: TimelineEvents) { + when (event) { + TimelineEvents.LoadMore -> localCoroutineScope.loadMore(hasMoreToLoad) + is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId + } + } + + LaunchedEffect(Unit) { + timeline + .timelineItems() + .onEach(messageTimelineItemStateFactory::replaceWith) + .launchIn(this) + } + + DisposableEffect(Unit) { + timeline.callback = TimelineCallback(localCoroutineScope, messageTimelineItemStateFactory) + timeline.initialize() + onDispose { + timeline.callback = null + timeline.dispose() + } + } + + return TimelineState( + highlightedEventId = highlightedEventId.value, + timelineItems = Async.Success(timelineItems.value), + hasMoreToLoad = hasMoreToLoad.value, + eventSink = ::handleEvents + ) + } + + fun CoroutineScope.loadMore(hasMoreToLoad: MutableState) = launch { + timeline.paginateBackwards(PAGINATION_COUNT) + hasMoreToLoad.value = timeline.hasMoreToLoad + } +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineState.kt new file mode 100644 index 0000000000..605bd43fb8 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineState.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.features.messages.timeline + +import androidx.compose.runtime.Immutable +import io.element.android.x.architecture.Async +import io.element.android.x.features.messages.model.MessagesTimelineItemState +import io.element.android.x.matrix.core.EventId + +@Immutable +data class TimelineState( + val timelineItems: Async> = Async.Uninitialized, + val hasMoreToLoad: Boolean = true, + val highlightedEventId: EventId? = null, + val eventSink: (TimelineEvents) -> Unit = {} +) diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineView.kt new file mode 100644 index 0000000000..b04cc2a1a3 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineView.kt @@ -0,0 +1,412 @@ +package io.element.android.x.features.messages.timeline + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDownward +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.LastBaseline +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import io.element.android.x.architecture.Async +import io.element.android.x.core.compose.PairCombinedPreviewParameter +import io.element.android.x.designsystem.components.avatar.Avatar +import io.element.android.x.designsystem.components.avatar.AvatarData +import io.element.android.x.features.messages.model.AggregatedReaction +import io.element.android.x.features.messages.model.MessagesItemGroupPosition +import io.element.android.x.features.messages.model.MessagesItemGroupPositionProvider +import io.element.android.x.features.messages.model.MessagesItemReactionState +import io.element.android.x.features.messages.model.MessagesTimelineItemState +import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent +import io.element.android.x.features.messages.model.content.MessagesTimelineItemContentProvider +import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent +import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent +import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent +import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent +import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent +import io.element.android.x.features.messages.timeline.components.MessageEventBubble +import io.element.android.x.features.messages.timeline.components.MessagesReactionsView +import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemEncryptedView +import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemImageView +import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemRedactedView +import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemTextView +import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemUnknownView +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +@Composable +fun TimelineView( + state: TimelineState, + modifier: Modifier = Modifier, + onMessageClicked: (MessagesTimelineItemState.MessageEvent) -> Unit = {}, + onMessageLongClicked: (MessagesTimelineItemState.MessageEvent) -> Unit = {}, +) { + val lazyListState = rememberLazyListState() + val timelineItems = state.timelineItems.dataOrNull().orEmpty().toImmutableList() + + Box(modifier = modifier.fillMaxWidth()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = lazyListState, + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Bottom, + reverseLayout = true + ) { + items( + items = timelineItems, + contentType = { timelineItem -> timelineItem.contentType() }, + key = { timelineItem -> timelineItem.key() }, + ) { timelineItem -> + TimelineItemRow( + timelineItem = timelineItem, + isHighlighted = timelineItem.key() == state.highlightedEventId?.value, + onClick = onMessageClicked, + onLongClick = onMessageLongClicked + ) + } + if (state.hasMoreToLoad) { + item { + TimelineLoadingMoreIndicator() + } + } + } + + fun onReachedLoadMore() { + state.eventSink(TimelineEvents.LoadMore) + } + + TimelineScrollHelper( + lazyListState = lazyListState, + timelineItems = timelineItems, + onLoadMore = ::onReachedLoadMore + ) + } +} + +private fun MessagesTimelineItemState.key(): String { + return when (this) { + is MessagesTimelineItemState.MessageEvent -> id + is MessagesTimelineItemState.Virtual -> id + } +} + +private fun MessagesTimelineItemState.contentType(): Int { + return when (this) { + is MessagesTimelineItemState.MessageEvent -> 0 + is MessagesTimelineItemState.Virtual -> 1 + } +} + +@Composable +fun TimelineItemRow( + timelineItem: MessagesTimelineItemState, + isHighlighted: Boolean, + onClick: (MessagesTimelineItemState.MessageEvent) -> Unit, + onLongClick: (MessagesTimelineItemState.MessageEvent) -> Unit, +) { + when (timelineItem) { + is MessagesTimelineItemState.Virtual -> return + is MessagesTimelineItemState.MessageEvent -> MessageEventRow( + messageEvent = timelineItem, + isHighlighted = isHighlighted, + onClick = { onClick(timelineItem) }, + onLongClick = { onLongClick(timelineItem) } + ) + } +} + +@Composable +fun MessageEventRow( + messageEvent: MessagesTimelineItemState.MessageEvent, + isHighlighted: Boolean, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier +) { + val interactionSource = remember { MutableInteractionSource() } + val (parentAlignment, contentAlignment) = if (messageEvent.isMine) { + Pair(Alignment.CenterEnd, Alignment.End) + } else { + Pair(Alignment.CenterStart, Alignment.Start) + } + Box( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight(), + contentAlignment = parentAlignment + ) { + Row { + if (!messageEvent.isMine) { + Spacer(modifier = Modifier.width(16.dp)) + } + Column(horizontalAlignment = contentAlignment) { + if (messageEvent.showSenderInformation) { + MessageSenderInformation( + messageEvent.safeSenderName, + messageEvent.senderAvatar, + Modifier.zIndex(1f) + ) + } + MessageEventBubble( + groupPosition = messageEvent.groupPosition, + isMine = messageEvent.isMine, + interactionSource = interactionSource, + isHighlighted = isHighlighted, + onClick = onClick, + onLongClick = onLongClick, + modifier = Modifier + .zIndex(-1f) + .widthIn(max = 320.dp) + ) { + val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) + when (messageEvent.content) { + is MessagesTimelineItemEncryptedContent -> MessagesTimelineItemEncryptedView( + content = messageEvent.content, + modifier = contentModifier + ) + is MessagesTimelineItemRedactedContent -> MessagesTimelineItemRedactedView( + content = messageEvent.content, + modifier = contentModifier + ) + is MessagesTimelineItemTextBasedContent -> MessagesTimelineItemTextView( + content = messageEvent.content, + interactionSource = interactionSource, + modifier = contentModifier, + onTextClicked = onClick, + onTextLongClicked = onLongClick + ) + is MessagesTimelineItemUnknownContent -> MessagesTimelineItemUnknownView( + content = messageEvent.content, + modifier = contentModifier + ) + is MessagesTimelineItemImageContent -> MessagesTimelineItemImageView( + content = messageEvent.content, + modifier = contentModifier + ) + } + } + MessagesReactionsView( + reactionsState = messageEvent.reactionsState, + modifier = Modifier + .zIndex(1f) + .offset(x = if (messageEvent.isMine) 0.dp else 20.dp, y = -(16.dp)) + ) + } + if (messageEvent.isMine) { + Spacer(modifier = Modifier.width(16.dp)) + } + } + } + if (messageEvent.groupPosition.isNew()) { + Spacer(modifier = modifier.height(8.dp)) + } else { + Spacer(modifier = modifier.height(2.dp)) + } +} + +@Composable +private fun MessageSenderInformation( + sender: String, + senderAvatar: AvatarData?, + modifier: Modifier = Modifier +) { + Row(modifier = modifier) { + if (senderAvatar != null) { + Avatar(senderAvatar) + Spacer(modifier = Modifier.width(4.dp)) + } + Text( + text = sender, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .alignBy(LastBaseline) + ) + } +} + +@Composable +internal fun BoxScope.TimelineScrollHelper( + lazyListState: LazyListState, + timelineItems: ImmutableList, + onLoadMore: () -> Unit = {}, +) { + val coroutineScope = rememberCoroutineScope() + val firstVisibleItemIndex by remember { derivedStateOf { lazyListState.firstVisibleItemIndex } } + + // Auto-scroll when new timeline items appear + LaunchedEffect(timelineItems, firstVisibleItemIndex) { + if (!lazyListState.isScrollInProgress && + firstVisibleItemIndex < 2 + ) coroutineScope.launch { + lazyListState.animateScrollToItem(0) + } + } + + // Handle load more preloading + val loadMore by remember { + derivedStateOf { + val layoutInfo = lazyListState.layoutInfo + val totalItemsNumber = layoutInfo.totalItemsCount + val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 + lastVisibleItemIndex > (totalItemsNumber - 30) + } + } + + LaunchedEffect(loadMore) { + snapshotFlow { loadMore } + .distinctUntilChanged() + .collect { + onLoadMore() + } + } + + // Jump to bottom button + if (firstVisibleItemIndex > 2) { + FloatingActionButton( + onClick = { + coroutineScope.launch { + if (firstVisibleItemIndex > 10) { + lazyListState.scrollToItem(0) + } else { + lazyListState.animateScrollToItem(0) + } + } + }, + shape = CircleShape, + modifier = Modifier + .align(Alignment.BottomCenter) + .size(40.dp), + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) { + Icon(Icons.Default.ArrowDownward, "") + } + } +} + +@Composable +internal fun TimelineLoadingMoreIndicator() { + Box( + Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(8.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.primary + ) + } +} + +class MessagesItemGroupPositionToMessagesTimelineItemContentProvider : + PairCombinedPreviewParameter( + MessagesItemGroupPositionProvider() to MessagesTimelineItemContentProvider() + ) + +@Suppress("PreviewPublic") +@Preview(showBackground = true) +@Composable +fun TimelineItemsPreview( + @PreviewParameter(MessagesTimelineItemContentProvider::class) + content: MessagesTimelineItemContent +) { + val timelineItems = persistentListOf( + // 3 items (First Middle Last) with isMine = false + createMessageEvent( + isMine = false, + content = content, + groupPosition = MessagesItemGroupPosition.First + ), + createMessageEvent( + isMine = false, + content = content, + groupPosition = MessagesItemGroupPosition.Middle + ), + createMessageEvent( + isMine = false, + content = content, + groupPosition = MessagesItemGroupPosition.Last + ), + // 3 items (First Middle Last) with isMine = true + createMessageEvent( + isMine = true, + content = content, + groupPosition = MessagesItemGroupPosition.First + ), + createMessageEvent( + isMine = true, + content = content, + groupPosition = MessagesItemGroupPosition.Middle + ), + createMessageEvent( + isMine = true, + content = content, + groupPosition = MessagesItemGroupPosition.Last + ), + ) + TimelineView( + state = TimelineState( + timelineItems = Async.Success(timelineItems) + ) + ) +} + +private fun createMessageEvent( + isMine: Boolean, + content: MessagesTimelineItemContent, + groupPosition: MessagesItemGroupPosition +): MessagesTimelineItemState { + return MessagesTimelineItemState.MessageEvent( + id = Math.random().toString(), + senderId = "senderId", + senderAvatar = AvatarData("sender"), + content = content, + reactionsState = MessagesItemReactionState( + listOf( + AggregatedReaction("👍", "1") + ) + ), + isMine = isMine, + senderDisplayName = "sender", + groupPosition = groupPosition, + ) +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessageEventBubble.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessageEventBubble.kt similarity index 98% rename from features/messages/src/main/java/io/element/android/x/features/messages/components/MessageEventBubble.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessageEventBubble.kt index 60f0c147a7..447e1bdd60 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessageEventBubble.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessageEventBubble.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.features.messages.components +package io.element.android.x.features.messages.timeline.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesReactionsView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesReactionsView.kt similarity index 97% rename from features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesReactionsView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesReactionsView.kt index 34d80f0a6e..14b5cf99e2 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesReactionsView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesReactionsView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.features.messages.components +package io.element.android.x.features.messages.timeline.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Row diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemEncryptedView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemEncryptedView.kt similarity index 94% rename from features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemEncryptedView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemEncryptedView.kt index b5a2a7753b..cfccdfae6b 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemEncryptedView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemEncryptedView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.features.messages.components +package io.element.android.x.features.messages.timeline.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemImageView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemImageView.kt similarity index 97% rename from features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemImageView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemImageView.kt index 9b0a270637..53647ff652 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemImageView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemImageView.kt @@ -16,7 +16,7 @@ @file:OptIn(ExperimentalFoundationApi::class) -package io.element.android.x.features.messages.components +package io.element.android.x.features.messages.timeline.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemInformativeView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemInformativeView.kt similarity index 96% rename from features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemInformativeView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemInformativeView.kt index e2c775198f..200b17374a 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemInformativeView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemInformativeView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.features.messages.components +package io.element.android.x.features.messages.timeline.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemRedactedView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemRedactedView.kt similarity index 87% rename from features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemRedactedView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemRedactedView.kt index 6ad7bb0772..e3ef130d50 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemRedactedView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemRedactedView.kt @@ -14,13 +14,14 @@ * limitations under the License. */ -package io.element.android.x.features.messages.components +package io.element.android.x.features.messages.timeline.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent +import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemInformativeView @Composable fun MessagesTimelineItemRedactedView( diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemTextView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemTextView.kt similarity index 95% rename from features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemTextView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemTextView.kt index 94f450e5c5..9bc80e0de5 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemTextView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemTextView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.features.messages.components +package io.element.android.x.features.messages.timeline.components import android.text.SpannableString import android.text.style.URLSpan @@ -30,7 +30,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.core.text.util.LinkifyCompat import io.element.android.x.designsystem.LinkColor import io.element.android.x.designsystem.components.ClickableLinkText -import io.element.android.x.features.messages.components.html.HtmlDocument +import io.element.android.x.features.messages.timeline.components.html.HtmlDocument import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent @Composable diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemUnknownView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemUnknownView.kt similarity index 94% rename from features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemUnknownView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemUnknownView.kt index 51021ab678..2b7ac2f85e 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemUnknownView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemUnknownView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.features.messages.components +package io.element.android.x.features.messages.timeline.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/html/HtmlDocument.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/html/HtmlDocument.kt similarity index 99% rename from features/messages/src/main/java/io/element/android/x/features/messages/components/html/HtmlDocument.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/html/HtmlDocument.kt index 56a7684a47..e5dcfc1112 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/html/HtmlDocument.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/html/HtmlDocument.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.features.messages.components.html +package io.element.android.x.features.messages.timeline.components.html import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemActionsSheetState.kt b/libraries/di/src/main/java/io/element/android/x/di/RoomScope.kt similarity index 70% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemActionsSheetState.kt rename to libraries/di/src/main/java/io/element/android/x/di/RoomScope.kt index 264fa85085..bcdb76c31d 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemActionsSheetState.kt +++ b/libraries/di/src/main/java/io/element/android/x/di/RoomScope.kt @@ -14,12 +14,6 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model +package io.element.android.x.di -import androidx.compose.runtime.Stable - -@Stable -data class MessagesItemActionsSheetState( - val targetItem: MessagesTimelineItemState.MessageEvent, - val actions: List -) +abstract class RoomScope private constructor() diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt index 6bd1031ba7..a302574b8e 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt @@ -18,7 +18,6 @@ package io.element.android.x.matrix.timeline import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.matrix.room.MatrixRoom -import java.util.Collections import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow @@ -33,6 +32,7 @@ import org.matrix.rustcomponents.sdk.TimelineChange import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineListener import timber.log.Timber +import java.util.Collections class MatrixTimeline( private val matrixRoom: MatrixRoom, diff --git a/libraries/textcomposer/build.gradle.kts b/libraries/textcomposer/build.gradle.kts index 7ef94fcf8e..e5d8942511 100644 --- a/libraries/textcomposer/build.gradle.kts +++ b/libraries/textcomposer/build.gradle.kts @@ -19,6 +19,7 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.ksp) + id("kotlin-parcelize") } android { diff --git a/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerMode.kt b/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerMode.kt index bf5b556525..224409829c 100644 --- a/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerMode.kt +++ b/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerMode.kt @@ -16,18 +16,25 @@ package io.element.android.x.textcomposer -sealed interface MessageComposerMode { +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +sealed interface MessageComposerMode : Parcelable { + @Parcelize data class Normal(val content: CharSequence?) : MessageComposerMode sealed class Special(open val eventId: String, open val defaultContent: CharSequence) : MessageComposerMode + @Parcelize data class Edit(override val eventId: String, override val defaultContent: CharSequence) : Special(eventId, defaultContent) + @Parcelize class Quote(override val eventId: String, override val defaultContent: CharSequence) : Special(eventId, defaultContent) + @Parcelize class Reply( val senderName: String, override val eventId: String, From c3ec363e777eced3dca67b3f7db60f45fd3c2022 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 12 Jan 2023 21:14:48 +0100 Subject: [PATCH 21/30] Refactor where Dagger Components belongs (in node) --- .../element/android/x/ElementXApplication.kt | 9 +-- .../java/io/element/android/x/MainActivity.kt | 2 - .../io/element/android/x/di/AppBindings.kt | 3 - .../element/android/x/di/SessionComponent.kt | 2 +- .../android/x/di/SessionComponentsOwner.kt | 61 ------------------- .../android/x/initializer/CoilInitializer.kt | 53 ---------------- .../android/x/node/LoggedInFlowNode.kt | 29 ++++++++- .../io/element/android/x/node/RoomFlowNode.kt | 2 + .../io/element/android/x/node/RootFlowNode.kt | 41 +++++-------- .../io/element/android/x/matrix/Matrix.kt | 4 ++ .../element/android/x/matrix/ui/MatrixUi.kt | 34 ----------- .../x/matrix/ui/di/MatrixUIBindings.kt | 12 ++++ .../x/matrix/ui/media/ImageLoaderFactories.kt | 34 +++++++++++ .../android/x/matrix/ui/media/MediaFetcher.kt | 5 +- 14 files changed, 98 insertions(+), 193 deletions(-) delete mode 100644 app/src/main/java/io/element/android/x/di/SessionComponentsOwner.kt delete mode 100644 app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt delete mode 100644 libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixUi.kt create mode 100644 libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/di/MatrixUIBindings.kt create mode 100644 libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/ImageLoaderFactories.kt diff --git a/app/src/main/java/io/element/android/x/ElementXApplication.kt b/app/src/main/java/io/element/android/x/ElementXApplication.kt index f13ba5d070..923a1ddc15 100644 --- a/app/src/main/java/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/java/io/element/android/x/ElementXApplication.kt @@ -18,13 +18,9 @@ package io.element.android.x import android.app.Application import androidx.startup.AppInitializer -import io.element.android.x.architecture.bindings import io.element.android.x.core.di.DaggerComponentOwner -import io.element.android.x.di.AppBindings import io.element.android.x.di.AppComponent import io.element.android.x.di.DaggerAppComponent -import io.element.android.x.di.SessionComponentsOwner -import io.element.android.x.initializer.CoilInitializer import io.element.android.x.initializer.CrashInitializer import io.element.android.x.initializer.MatrixInitializer import io.element.android.x.initializer.MavericksInitializer @@ -33,20 +29,17 @@ import io.element.android.x.initializer.TimberInitializer class ElementXApplication : Application(), DaggerComponentOwner { private lateinit var appComponent: AppComponent - private var sessionComponentsOwner: SessionComponentsOwner? = null override val daggerComponent: Any - get() = listOfNotNull(sessionComponentsOwner?.activeSessionComponent, appComponent) + get() = appComponent override fun onCreate() { super.onCreate() appComponent = DaggerAppComponent.factory().create(applicationContext) - sessionComponentsOwner = bindings().sessionComponentsOwner() AppInitializer.getInstance(this).apply { initializeComponent(CrashInitializer::class.java) initializeComponent(TimberInitializer::class.java) initializeComponent(MatrixInitializer::class.java) - initializeComponent(CoilInitializer::class.java) initializeComponent(MavericksInitializer::class.java) } } 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 1fd827be85..3f9ff827cc 100644 --- a/app/src/main/java/io/element/android/x/MainActivity.kt +++ b/app/src/main/java/io/element/android/x/MainActivity.kt @@ -39,7 +39,6 @@ class MainActivity : NodeComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { ElementXTheme { - // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background @@ -49,7 +48,6 @@ class MainActivity : NodeComponentActivity() { buildContext = it, appComponentOwner = applicationContext as DaggerComponentOwner, matrix = appBindings.matrix(), - sessionComponentsOwner = appBindings.sessionComponentsOwner(), rootPresenter = appBindings.rootPresenter() ) } 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 28efc08ba8..8cebc3e838 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 @@ -18,7 +18,6 @@ package io.element.android.x.di import com.squareup.anvil.annotations.ContributesTo import io.element.android.x.matrix.Matrix -import io.element.android.x.matrix.ui.MatrixUi import io.element.android.x.root.RootPresenter import kotlinx.coroutines.CoroutineScope @@ -27,6 +26,4 @@ interface AppBindings { fun coroutineScope(): CoroutineScope fun rootPresenter(): RootPresenter fun matrix(): Matrix - fun matrixUi(): MatrixUi - fun sessionComponentsOwner(): SessionComponentsOwner } diff --git a/app/src/main/java/io/element/android/x/di/SessionComponent.kt b/app/src/main/java/io/element/android/x/di/SessionComponent.kt index 15fd19a9d8..93e19d082b 100644 --- a/app/src/main/java/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/java/io/element/android/x/di/SessionComponent.kt @@ -26,7 +26,7 @@ import io.element.android.x.matrix.MatrixClient @SingleIn(SessionScope::class) @MergeSubcomponent(SessionScope::class) -interface SessionComponent: DaggerMavericksBindings, NodeFactoriesBindings { +interface SessionComponent: NodeFactoriesBindings { fun matrixClient(): MatrixClient 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 deleted file mode 100644 index 13b886e0a4..0000000000 --- a/app/src/main/java/io/element/android/x/di/SessionComponentsOwner.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.di - -import android.content.Context -import io.element.android.x.architecture.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() - var activeSessionComponent: SessionComponent? = null - private set - - fun setActive(sessionId: SessionId) { - val sessionComponent = sessionComponents[sessionId] - if (activeSessionComponent != sessionComponent) { - activeSessionComponent = sessionComponent - } - } - - fun create(matrixClient: MatrixClient) { - val sessionId = matrixClient.sessionId - val sessionComponent = - context.bindings().sessionComponentBuilder() - .client(matrixClient).build() - sessionComponents[sessionId] = sessionComponent - setActive(sessionId) - } - - fun releaseActiveSession() { - activeSessionComponent?.also { - release(it.matrixClient().sessionId) - } - } - - 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/CoilInitializer.kt b/app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt deleted file mode 100644 index 43d44ba867..0000000000 --- a/app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.initializer - -import android.content.Context -import androidx.startup.Initializer -import coil.Coil -import coil.ImageLoader -import coil.ImageLoaderFactory -import io.element.android.x.architecture.bindings -import io.element.android.x.di.AppBindings - -class CoilInitializer : Initializer { - - override fun create(context: Context) { - Coil.setImageLoader(ElementImageLoaderFactory(context)) - } - - override fun dependencies(): List>> = emptyList() -} - -private class ElementImageLoaderFactory( - private val context: Context -) : ImageLoaderFactory { - override fun newImageLoader(): ImageLoader { - return ImageLoader - .Builder(context) - .components { - val appBindings = context.bindings() - val matrixUi = appBindings.matrixUi() - val matrixClientProvider = { - appBindings - .sessionComponentsOwner().activeSessionComponent?.matrixClient() - } - matrixUi.registerCoilComponents(this, matrixClientProvider) - } - .build() - } -} 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 index ad8ad5743a..36e8416c16 100644 --- a/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt @@ -3,22 +3,30 @@ package io.element.android.x.node import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import coil.Coil 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.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push +import io.element.android.x.architecture.bindings import io.element.android.x.architecture.createNode +import io.element.android.x.core.di.DaggerComponentOwner +import io.element.android.x.di.SessionComponent import io.element.android.x.features.preferences.PreferencesFlowNode import io.element.android.x.features.roomlist.RoomListNode +import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.core.SessionId +import io.element.android.x.matrix.ui.di.MatrixUIBindings import kotlinx.parcelize.Parcelize class LoggedInFlowNode( buildContext: BuildContext, val sessionId: SessionId, + private val matrixClient: MatrixClient, private val onOpenBugReport: () -> Unit, private val backstack: BackStack = BackStack( initialElement = NavTarget.RoomList, @@ -27,7 +35,26 @@ class LoggedInFlowNode( ) : ParentNode( navModel = backstack, buildContext = buildContext -) { +), DaggerComponentOwner { + + override val daggerComponent: Any by lazy { + parent!!.bindings().sessionComponentBuilder().client(matrixClient).build() + } + + override fun onBuilt() { + super.onBuilt() + lifecycle.subscribe( + onCreate = { + val imageLoaderFactory = bindings().loggedInImageLoaderFactory() + Coil.setImageLoader(imageLoaderFactory) + matrixClient.startSync() + }, + onDestroy = { + val imageLoaderFactory = bindings().notLoggedInImageLoaderFactory() + Coil.setImageLoader(imageLoaderFactory) + } + ) + } private val roomListCallback = object : RoomListNode.Callback { override fun onRoomClicked(roomId: RoomId) { diff --git a/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt b/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt index c7b5f21585..b3c4eebb62 100644 --- a/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt @@ -27,6 +27,8 @@ class RoomFlowNode( buildContext = buildContext ) { + + init { lifecycle.subscribe( onCreate = { Timber.v("OnCreate") }, 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 index 115d6fb4de..2ef4870b74 100644 --- a/app/src/main/java/io/element/android/x/node/RootFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/RootFlowNode.kt @@ -9,10 +9,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -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 @@ -25,13 +22,11 @@ import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import io.element.android.x.architecture.createNode import io.element.android.x.architecture.presenterConnector -import io.element.android.x.core.compose.OnLifecycleEvent import io.element.android.x.core.di.DaggerComponentOwner -import io.element.android.x.di.SessionComponentsOwner import io.element.android.x.features.rageshake.bugreport.BugReportNode import io.element.android.x.matrix.Matrix +import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.core.SessionId -import io.element.android.x.root.RootEvents import io.element.android.x.root.RootPresenter import io.element.android.x.root.RootView import kotlinx.coroutines.flow.distinctUntilChanged @@ -39,22 +34,7 @@ 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) - } - ) - } - }) - } -} +import java.util.concurrent.ConcurrentHashMap class RootFlowNode( buildContext: BuildContext, @@ -64,20 +44,25 @@ class RootFlowNode( ), private val appComponentOwner: DaggerComponentOwner, private val matrix: Matrix, - private val sessionComponentsOwner: SessionComponentsOwner, rootPresenter: RootPresenter ) : ParentNode( navModel = backstack, buildContext = buildContext, - plugins = listOf(SessionComponentsOwnerInteractor(sessionComponentsOwner)), ), DaggerComponentOwner by appComponentOwner { + private val matrixClientsHolder = ConcurrentHashMap() private val presenterConnector = presenterConnector(rootPresenter) - init { + override fun onBuilt() { + super.onBuilt() + whenChildAttached(LoggedInFlowNode::class) { _, child -> + child.lifecycle.subscribe( + onDestroy = { matrixClientsHolder.remove(child.sessionId) } + ) + } matrix.isLoggedIn() .distinctUntilChanged() .onEach { isLoggedIn -> @@ -87,8 +72,7 @@ class RootFlowNode( if (matrixClient == null) { backstack.newRoot(NavTarget.NotLoggedInFlow) } else { - matrixClient.startSync() - sessionComponentsOwner.create(matrixClient) + matrixClientsHolder[matrixClient.sessionId] = matrixClient backstack.newRoot(NavTarget.LoggedInFlow(matrixClient.sessionId)) } } else { @@ -136,9 +120,12 @@ class RootFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.LoggedInFlow -> { + val matrixClient = + matrixClientsHolder[navTarget.sessionId] ?: throw IllegalStateException("Makes sure to give a matrixClient with the given sessionId") LoggedInFlowNode( buildContext = buildContext, sessionId = navTarget.sessionId, + matrixClient = matrixClient, onOpenBugReport = this::onOpenBugReport ) } 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 6d6f49eb40..cd14368cb6 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 @@ -57,6 +57,10 @@ class Matrix @Inject constructor( return sessionStore.isLoggedIn() } + suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io){ + sessionStore.getLatestSession()?.sessionId() + } + suspend fun restoreSession() = withContext(coroutineDispatchers.io) { sessionStore.getLatestSession() ?.let { session -> diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixUi.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixUi.kt deleted file mode 100644 index db4de72d7d..0000000000 --- a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixUi.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.matrix.ui - -import coil.ComponentRegistry -import io.element.android.x.matrix.MatrixClient -import io.element.android.x.matrix.ui.media.MediaFetcher -import io.element.android.x.matrix.ui.media.MediaKeyer -import javax.inject.Inject - -class MatrixUi @Inject constructor() { - - fun registerCoilComponents( - builder: ComponentRegistry.Builder, - activeClientProvider: () -> MatrixClient? - ) { - builder.add(MediaKeyer()) - builder.add(MediaFetcher.Factory(activeClientProvider)) - } -} diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/di/MatrixUIBindings.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/di/MatrixUIBindings.kt new file mode 100644 index 0000000000..4fd7e1aa35 --- /dev/null +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/di/MatrixUIBindings.kt @@ -0,0 +1,12 @@ +package io.element.android.x.matrix.ui.di + +import com.squareup.anvil.annotations.ContributesTo +import io.element.android.x.di.SessionScope +import io.element.android.x.matrix.ui.media.LoggedInImageLoaderFactory +import io.element.android.x.matrix.ui.media.NotLoggedInImageLoaderFactory + +@ContributesTo(SessionScope::class) +interface MatrixUIBindings { + fun loggedInImageLoaderFactory(): LoggedInImageLoaderFactory + fun notLoggedInImageLoaderFactory(): NotLoggedInImageLoaderFactory +} diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/ImageLoaderFactories.kt new file mode 100644 index 0000000000..b014d4cc02 --- /dev/null +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/ImageLoaderFactories.kt @@ -0,0 +1,34 @@ +package io.element.android.x.matrix.ui.media + +import android.content.Context +import coil.ImageLoader +import coil.ImageLoaderFactory +import io.element.android.x.di.ApplicationContext +import io.element.android.x.matrix.MatrixClient +import javax.inject.Inject + +class LoggedInImageLoaderFactory @Inject constructor( + @ApplicationContext private val context: Context, + private val matrixClient: MatrixClient, +) : ImageLoaderFactory { + override fun newImageLoader(): ImageLoader { + return ImageLoader + .Builder(context) + .components { + add(MediaKeyer()) + add(MediaFetcher.Factory(matrixClient)) + } + .build() + } +} + +class NotLoggedInImageLoaderFactory @Inject constructor( + @ApplicationContext private val context: Context, +) : ImageLoaderFactory { + override fun newImageLoader(): ImageLoader { + return ImageLoader + .Builder(context) + .build() + } +} + diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaFetcher.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaFetcher.kt index d345f26984..a443f7486d 100644 --- a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaFetcher.kt +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaFetcher.kt @@ -37,16 +37,15 @@ internal class MediaFetcher( return imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch() } - class Factory(private val activeClientProvider: () -> MatrixClient?) : + class Factory(private val client: MatrixClient) : Fetcher.Factory { override fun create( data: MediaResolver.Meta, options: Options, imageLoader: ImageLoader ): Fetcher { - val activeClient = activeClientProvider() return MediaFetcher( - mediaResolver = activeClient?.mediaResolver(), + mediaResolver = client.mediaResolver(), meta = data, options = options, imageLoader = imageLoader From 2869f492d9abb98c4d01292875ba351f3a8b8e67 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 13 Jan 2023 12:29:36 +0100 Subject: [PATCH 22/30] Create RoomComponent and manage in RoomFlowNode --- .../io/element/android/x/di/RoomComponent.kt | 43 +++++++++++++++++++ .../element/android/x/di/SessionComponent.kt | 2 +- .../android/x/node/LoggedInFlowNode.kt | 17 +++++++- .../io/element/android/x/node/RoomFlowNode.kt | 13 ++++-- .../x/features/messages/MessagesNode.kt | 23 ++++------ .../x/features/messages/MessagesPresenter.kt | 35 +++++++++++++-- .../element/android/x/matrix/MatrixClient.kt | 5 ++- 7 files changed, 111 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/io/element/android/x/di/RoomComponent.kt diff --git a/app/src/main/java/io/element/android/x/di/RoomComponent.kt b/app/src/main/java/io/element/android/x/di/RoomComponent.kt new file mode 100644 index 0000000000..d5964325ae --- /dev/null +++ b/app/src/main/java/io/element/android/x/di/RoomComponent.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.di + +import com.squareup.anvil.annotations.ContributesTo +import com.squareup.anvil.annotations.MergeSubcomponent +import dagger.BindsInstance +import dagger.Subcomponent +import io.element.android.x.architecture.NodeFactoriesBindings +import io.element.android.x.matrix.room.MatrixRoom + +@SingleIn(RoomScope::class) +@MergeSubcomponent(RoomScope::class) +interface RoomComponent : NodeFactoriesBindings { + + fun matrixRoom(): MatrixRoom + + @Subcomponent.Builder + interface Builder { + @BindsInstance + fun room(room: MatrixRoom): Builder + fun build(): RoomComponent + } + + @ContributesTo(SessionScope::class) + interface ParentBindings { + fun roomComponentBuilder(): Builder + } +} diff --git a/app/src/main/java/io/element/android/x/di/SessionComponent.kt b/app/src/main/java/io/element/android/x/di/SessionComponent.kt index 93e19d082b..0ba9a12d58 100644 --- a/app/src/main/java/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/java/io/element/android/x/di/SessionComponent.kt @@ -26,7 +26,7 @@ import io.element.android.x.matrix.MatrixClient @SingleIn(SessionScope::class) @MergeSubcomponent(SessionScope::class) -interface SessionComponent: NodeFactoriesBindings { +interface SessionComponent: NodeFactoriesBindings, RoomComponent.ParentBindings { fun matrixClient(): MatrixClient 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 index 36e8416c16..281d9f5aef 100644 --- a/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt @@ -1,7 +1,11 @@ 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.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import coil.Coil import com.bumble.appyx.core.composable.Children @@ -9,6 +13,7 @@ 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.push import io.element.android.x.architecture.bindings @@ -83,7 +88,17 @@ class LoggedInFlowNode( createNode(buildContext, plugins = listOf(roomListCallback)) } is NavTarget.Room -> { - RoomFlowNode(buildContext, navTarget.roomId) + val room = matrixClient.getRoom(roomId = navTarget.roomId) + if (room == null) { + // TODO CREATE UNKNOWN ROOM NODE + node(buildContext) { + Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(text = "Unknown room with id = ${navTarget.roomId}") + } + } + } else { + RoomFlowNode(buildContext, room) + } } NavTarget.Settings -> { PreferencesFlowNode(buildContext, onOpenBugReport) diff --git a/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt b/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt index b3c4eebb62..4c9fd64e68 100644 --- a/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt @@ -9,15 +9,18 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.navmodel.backstack.BackStack +import io.element.android.x.architecture.bindings import io.element.android.x.architecture.createNode +import io.element.android.x.core.di.DaggerComponentOwner +import io.element.android.x.di.RoomComponent import io.element.android.x.features.messages.MessagesNode -import io.element.android.x.matrix.core.RoomId +import io.element.android.x.matrix.room.MatrixRoom import kotlinx.parcelize.Parcelize import timber.log.Timber class RoomFlowNode( buildContext: BuildContext, - private val roomId: RoomId, + private val room: MatrixRoom, private val backstack: BackStack = BackStack( initialElement = NavTarget.Messages, savedStateMap = buildContext.savedStateMap, @@ -25,9 +28,11 @@ class RoomFlowNode( ) : ParentNode( navModel = backstack, buildContext = buildContext -) { - +), DaggerComponentOwner { + override val daggerComponent: Any by lazy { + parent!!.bindings().roomComponentBuilder().room(room).build() + } init { lifecycle.subscribe( diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt index 25bd74ec97..d556eecf04 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt @@ -1,10 +1,8 @@ package io.element.android.x.features.messages -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -12,29 +10,24 @@ import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.x.anvilannotations.ContributesNode -import io.element.android.x.di.SessionScope +import io.element.android.x.architecture.presenterConnector +import io.element.android.x.di.RoomScope -@ContributesNode(SessionScope::class) +@ContributesNode(RoomScope::class) class MessagesNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - //presenter: MessagesPresenter, + presenter: MessagesPresenter, ) : Node(buildContext, plugins = plugins) { + private val connector = presenterConnector(presenter) + @Composable override fun View(modifier: Modifier) { - /* val state by connector.stateFlow.collectAsState() MessagesView( state = state, onBackPressed = this::navigateUp, ) - */ - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(text = "MESSAGES NODE") - } } } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt index 253bc3c1c1..d78123ef3d 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt @@ -1,8 +1,16 @@ package io.element.android.x.features.messages import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.x.architecture.Presenter +import io.element.android.x.designsystem.components.avatar.AvatarData +import io.element.android.x.designsystem.components.avatar.AvatarSize import io.element.android.x.features.messages.actionlist.ActionListPresenter import io.element.android.x.features.messages.actionlist.TimelineItemAction import io.element.android.x.features.messages.model.MessagesTimelineItemState @@ -12,8 +20,8 @@ import io.element.android.x.features.messages.textcomposer.MessageComposerPresen import io.element.android.x.features.messages.textcomposer.MessageComposerState import io.element.android.x.features.messages.timeline.TimelinePresenter import io.element.android.x.matrix.MatrixClient -import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.room.MatrixRoom +import io.element.android.x.matrix.ui.MatrixItemHelper import io.element.android.x.textcomposer.MessageComposerMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -21,14 +29,15 @@ import timber.log.Timber import javax.inject.Inject class MessagesPresenter @Inject constructor( - private val client: MatrixClient, - private val roomId: RoomId, + private val matrixClient: MatrixClient, private val room: MatrixRoom, private val composerPresenter: MessageComposerPresenter, private val timelinePresenter: TimelinePresenter, private val actionListPresenter: ActionListPresenter, ) : Presenter { + private val matrixItemHelper = MatrixItemHelper(matrixClient) + @Composable override fun present(): MessagesState { val localCoroutineScope = rememberCoroutineScope() @@ -36,13 +45,31 @@ class MessagesPresenter @Inject constructor( val timelineState = timelinePresenter.present() val actionListState = actionListPresenter.present() + val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L) + val roomName: MutableState = rememberSaveable { + mutableStateOf(null) + } + val roomAvatar: MutableState = remember { + mutableStateOf(null) + } + LaunchedEffect(syncUpdateFlow) { + roomAvatar.value = + matrixItemHelper.loadAvatarData( + room = room, + size = AvatarSize.SMALL + ) + roomName.value = room.name + } + fun handleEvents(event: MessagesEvents) { when (event) { is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.messageEvent, composerState) } } return MessagesState( - roomId = roomId, + roomId = room.roomId, + roomName = roomName.value, + roomAvatar = roomAvatar.value, composerState = composerState, timelineState = timelineState, actionListState = actionListState, 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 efca0e4de4..48bb8816d0 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 @@ -17,6 +17,7 @@ package io.element.android.x.matrix import io.element.android.x.core.coroutine.CoroutineDispatchers +import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.core.SessionId import io.element.android.x.matrix.core.UserId import io.element.android.x.matrix.media.MediaResolver @@ -108,8 +109,8 @@ class MatrixClient internal constructor( slidingSyncObserverToken = slidingSync.sync() } - fun getRoom(roomId: String): MatrixRoom? { - val slidingSyncRoom = slidingSync.getRoom(roomId) ?: return null + fun getRoom(roomId: RoomId): MatrixRoom? { + val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null val room = slidingSyncRoom.fullRoom() ?: return null return MatrixRoom( slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow, From 7b197e6e8bb35ec0df3451dd79d1b47806adb2f4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 13 Jan 2023 18:05:14 +0100 Subject: [PATCH 23/30] Finish migration of Messages screen --- .../java/io/element/android/x/di/AppModule.kt | 15 +++ .../x/features/messages/MessagesEvents.kt | 6 +- .../x/features/messages/MessagesNode.kt | 1 + .../x/features/messages/MessagesPresenter.kt | 21 ++-- .../x/features/messages/MessagesState.kt | 6 +- .../x/features/messages/MessagesView.kt | 63 ++++++---- .../messages/actionlist/ActionListEvents.kt | 4 +- .../actionlist/ActionListPresenter.kt | 15 +-- .../messages/actionlist/ActionListState.kt | 11 +- .../messages/actionlist/ActionListView.kt | 9 +- .../{ => model}/TimelineItemAction.kt | 4 +- .../textcomposer/MessageComposerPresenter.kt | 24 ++-- .../textcomposer/MessageComposerState.kt | 20 ++-- .../textcomposer/MessageComposerView.kt | 2 +- .../TimelineItemsFactory.kt} | 101 ++++++++-------- .../messages/timeline/TimelinePresenter.kt | 21 ++-- .../messages/timeline/TimelineState.kt | 12 +- .../messages/timeline/TimelineView.kt | 112 +++++++++--------- .../timeline/components/MessageEventBubble.kt | 2 +- ...edView.kt => TimelineItemEncryptedView.kt} | 8 +- ...mImageView.kt => TimelineItemImageView.kt} | 6 +- ...View.kt => TimelineItemInformativeView.kt} | 2 +- ...nsView.kt => TimelineItemReactionsView.kt} | 8 +- ...tedView.kt => TimelineItemRedactedView.kt} | 9 +- ...temTextView.kt => TimelineItemTextView.kt} | 6 +- ...nownView.kt => TimelineItemUnknownView.kt} | 8 +- .../{ => timeline}/diff/CacheInvalidator.kt | 10 +- .../diff/MatrixTimelineItemsDiffCallback.kt | 2 +- .../model/TimelineItem.kt} | 21 ++-- .../model/TimelineItemGroupPosition.kt} | 8 +- .../model/TimelineItemReactions.kt} | 12 +- .../model/content/TimelineItemContent.kt} | 18 +-- .../content/TimelineItemEmoteContent.kt} | 6 +- .../content/TimelineItemEncryptedContent.kt} | 6 +- .../content/TimelineItemImageContent.kt} | 6 +- .../content/TimelineItemNoticeContent.kt} | 6 +- .../content/TimelineItemRedactedContent.kt} | 4 +- .../content/TimelineItemTextBasedContent.kt} | 4 +- .../model/content/TimelineItemTextContent.kt} | 6 +- .../content/TimelineItemUnknownContent.kt} | 4 +- .../{ => timeline}/util/MutableListExt.kt | 2 +- .../io/element/android/x/matrix/Matrix.kt | 17 +-- .../android/x/matrix/room/MatrixRoom.kt | 21 ++-- .../x/matrix/timeline/MatrixTimeline.kt | 5 +- .../android/x/matrix/ui/MatrixItemHelper.kt | 3 +- libraries/textcomposer/build.gradle.kts | 1 + .../x/textcomposer/MessageComposerMode.kt | 11 +- 47 files changed, 354 insertions(+), 315 deletions(-) rename features/messages/src/main/java/io/element/android/x/features/messages/actionlist/{ => model}/TimelineItemAction.kt (92%) rename features/messages/src/main/java/io/element/android/x/features/messages/{MessageTimelineItemStateFactory.kt => timeline/TimelineItemsFactory.kt} (70%) rename features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/{MessagesTimelineItemEncryptedView.kt => TimelineItemEncryptedView.kt} (82%) rename features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/{MessagesTimelineItemImageView.kt => TimelineItemImageView.kt} (92%) rename features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/{MessagesTimelineItemInformativeView.kt => TimelineItemInformativeView.kt} (97%) rename features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/{MessagesReactionsView.kt => TimelineItemReactionsView.kt} (91%) rename features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/{MessagesTimelineItemRedactedView.kt => TimelineItemRedactedView.kt} (76%) rename features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/{MessagesTimelineItemTextView.kt => TimelineItemTextView.kt} (94%) rename features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/{MessagesTimelineItemUnknownView.kt => TimelineItemUnknownView.kt} (82%) rename features/messages/src/main/java/io/element/android/x/features/messages/{ => timeline}/diff/CacheInvalidator.kt (86%) rename features/messages/src/main/java/io/element/android/x/features/messages/{ => timeline}/diff/MatrixTimelineItemsDiffCallback.kt (96%) rename features/messages/src/main/java/io/element/android/x/features/messages/{model/MessagesTimelineItemState.kt => timeline/model/TimelineItem.kt} (70%) rename features/messages/src/main/java/io/element/android/x/features/messages/{model/MessagesItemGroupPosition.kt => timeline/model/TimelineItemGroupPosition.kt} (84%) rename features/messages/src/main/java/io/element/android/x/features/messages/{model/MessagesItemReactionState.kt => timeline/model/TimelineItemReactions.kt} (74%) rename features/messages/src/main/java/io/element/android/x/features/messages/{model/content/MessagesTimelineItemContent.kt => timeline/model/content/TimelineItemContent.kt} (74%) rename features/messages/src/main/java/io/element/android/x/features/messages/{model/content/MessagesTimelineItemTextContent.kt => timeline/model/content/TimelineItemEmoteContent.kt} (82%) rename features/messages/src/main/java/io/element/android/x/features/messages/{model/content/MessagesTimelineItemEncryptedContent.kt => timeline/model/content/TimelineItemEncryptedContent.kt} (83%) rename features/messages/src/main/java/io/element/android/x/features/messages/{model/content/MessagesTimelineItemImageContent.kt => timeline/model/content/TimelineItemImageContent.kt} (84%) rename features/messages/src/main/java/io/element/android/x/features/messages/{model/content/MessagesTimelineItemEmoteContent.kt => timeline/model/content/TimelineItemNoticeContent.kt} (82%) rename features/messages/src/main/java/io/element/android/x/features/messages/{model/content/MessagesTimelineItemUnknownContent.kt => timeline/model/content/TimelineItemRedactedContent.kt} (81%) rename features/messages/src/main/java/io/element/android/x/features/messages/{model/content/MessagesTimelineItemTextBasedContent.kt => timeline/model/content/TimelineItemTextBasedContent.kt} (82%) rename features/messages/src/main/java/io/element/android/x/features/messages/{model/content/MessagesTimelineItemNoticeContent.kt => timeline/model/content/TimelineItemTextContent.kt} (82%) rename features/messages/src/main/java/io/element/android/x/features/messages/{model/content/MessagesTimelineItemRedactedContent.kt => timeline/model/content/TimelineItemUnknownContent.kt} (81%) rename features/messages/src/main/java/io/element/android/x/features/messages/{ => timeline}/util/MutableListExt.kt (92%) diff --git a/app/src/main/java/io/element/android/x/di/AppModule.kt b/app/src/main/java/io/element/android/x/di/AppModule.kt index b8e703c775..55c331680c 100644 --- a/app/src/main/java/io/element/android/x/di/AppModule.kt +++ b/app/src/main/java/io/element/android/x/di/AppModule.kt @@ -19,10 +19,14 @@ package io.element.android.x.di import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides +import io.element.android.x.core.coroutine.CoroutineDispatchers import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope +import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.plus +import java.util.concurrent.Executors @Module @ContributesTo(AppScope::class) @@ -33,4 +37,15 @@ object AppModule { fun providesAppCoroutineScope(): CoroutineScope { return MainScope() + CoroutineName("ElementX Scope") } + + @Provides + @SingleIn(AppScope::class) + fun providesCoroutineDispatchers(): CoroutineDispatchers { + return CoroutineDispatchers( + io = Dispatchers.IO, + computation = Dispatchers.Default, + main = Dispatchers.Main, + diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + ) + } } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesEvents.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesEvents.kt index b70c896e9f..45d4049713 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesEvents.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesEvents.kt @@ -1,8 +1,8 @@ package io.element.android.x.features.messages -import io.element.android.x.features.messages.actionlist.TimelineItemAction -import io.element.android.x.features.messages.model.MessagesTimelineItemState +import io.element.android.x.features.messages.actionlist.model.TimelineItemAction +import io.element.android.x.features.messages.timeline.model.TimelineItem sealed interface MessagesEvents { - data class HandleAction(val action: TimelineItemAction, val messageEvent: MessagesTimelineItemState.MessageEvent) : MessagesEvents + data class HandleAction(val action: TimelineItemAction, val messageEvent: TimelineItem.MessageEvent) : MessagesEvents } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt index d556eecf04..d02973f0dd 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt @@ -28,6 +28,7 @@ class MessagesNode @AssistedInject constructor( MessagesView( state = state, onBackPressed = this::navigateUp, + modifier = modifier ) } } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt index d78123ef3d..679109a6e0 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt @@ -12,13 +12,14 @@ import io.element.android.x.architecture.Presenter import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarSize import io.element.android.x.features.messages.actionlist.ActionListPresenter -import io.element.android.x.features.messages.actionlist.TimelineItemAction -import io.element.android.x.features.messages.model.MessagesTimelineItemState -import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent +import io.element.android.x.features.messages.actionlist.model.TimelineItemAction import io.element.android.x.features.messages.textcomposer.MessageComposerEvents import io.element.android.x.features.messages.textcomposer.MessageComposerPresenter import io.element.android.x.features.messages.textcomposer.MessageComposerState +import io.element.android.x.features.messages.timeline.TimelineEvents import io.element.android.x.features.messages.timeline.TimelinePresenter +import io.element.android.x.features.messages.timeline.model.TimelineItem +import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.ui.MatrixItemHelper @@ -60,7 +61,9 @@ class MessagesPresenter @Inject constructor( ) roomName.value = room.name } - + LaunchedEffect(composerState.mode.relatedEventId) { + timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId)) + } fun handleEvents(event: MessagesEvents) { when (event) { is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.messageEvent, composerState) @@ -79,7 +82,7 @@ class MessagesPresenter @Inject constructor( fun CoroutineScope.handleTimelineAction( action: TimelineItemAction, - targetEvent: MessagesTimelineItemState.MessageEvent, + targetEvent: TimelineItem.MessageEvent, composerState: MessageComposerState, ) = launch { when (action) { @@ -95,21 +98,21 @@ class MessagesPresenter @Inject constructor( Timber.v("NotImplementedYet") } - private suspend fun handleActionRedact(event: MessagesTimelineItemState.MessageEvent) { + private suspend fun handleActionRedact(event: TimelineItem.MessageEvent) { room.redactEvent(event.id) } - private fun handleActionEdit(targetEvent: MessagesTimelineItemState.MessageEvent, composerState: MessageComposerState) { + private fun handleActionEdit(targetEvent: TimelineItem.MessageEvent, composerState: MessageComposerState) { val composerMode = MessageComposerMode.Edit( targetEvent.id, - (targetEvent.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty() + (targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty() ) composerState.eventSink( MessageComposerEvents.SetMode(composerMode) ) } - private fun handleActionReply(targetEvent: MessagesTimelineItemState.MessageEvent, composerState: MessageComposerState) { + private fun handleActionReply(targetEvent: TimelineItem.MessageEvent, composerState: MessageComposerState) { val composerMode = MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, "") composerState.eventSink( MessageComposerEvents.SetMode(composerMode) diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesState.kt index a7fcefb88f..f9cdcb2b90 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesState.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesState.kt @@ -10,10 +10,10 @@ import io.element.android.x.matrix.core.RoomId @Immutable data class MessagesState( val roomId: RoomId, - val roomName: String? = null, - val roomAvatar: AvatarData? = null, + val roomName: String?, + val roomAvatar: AvatarData?, val composerState: MessageComposerState, val timelineState: TimelineState, val actionListState: ActionListState, - val eventSink: (MessagesEvents) -> Unit = {} + val eventSink: (MessagesEvents) -> Unit ) diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesView.kt index 6e8a761b28..36ebf90e35 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesView.kt @@ -48,8 +48,10 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -57,11 +59,13 @@ import androidx.compose.ui.unit.sp import io.element.android.x.core.compose.LogCompositions import io.element.android.x.designsystem.components.avatar.Avatar import io.element.android.x.designsystem.components.avatar.AvatarData -import io.element.android.x.features.messages.actionlist.TimelineItemAction +import io.element.android.x.features.messages.actionlist.ActionListEvents import io.element.android.x.features.messages.actionlist.ActionListView -import io.element.android.x.features.messages.model.MessagesTimelineItemState +import io.element.android.x.features.messages.actionlist.model.TimelineItemAction +import io.element.android.x.features.messages.timeline.model.TimelineItem import io.element.android.x.features.messages.textcomposer.MessageComposerView import io.element.android.x.features.messages.timeline.TimelineView +import kotlinx.coroutines.launch import timber.log.Timber @Composable @@ -76,8 +80,28 @@ fun MessagesView( initialValue = ModalBottomSheetValue.Hidden, ) val snackbarHostState = remember { SnackbarHostState() } + val focusManager = LocalFocusManager.current + val coroutineScope = rememberCoroutineScope() LogCompositions(tag = "MessagesScreen", msg = "Content") + + fun onMessageClicked(messageEvent: TimelineItem.MessageEvent) { + Timber.v("OnMessageClicked= ${messageEvent.id}") + } + + fun onMessageLongClicked(messageEvent: TimelineItem.MessageEvent) { + Timber.v("OnMessageLongClicked= ${messageEvent.id}") + focusManager.clearFocus(force = true) + state.actionListState.eventSink(ActionListEvents.ComputeForMessage(messageEvent)) + coroutineScope.launch { + itemActionsBottomSheetState.show() + } + } + + fun onActionSelected(action: TimelineItemAction, messageEvent: TimelineItem.MessageEvent) { + state.eventSink(MessagesEvents.HandleAction(action, messageEvent)) + } + Scaffold( modifier = modifier, contentWindowInsets = WindowInsets.statusBars, @@ -92,6 +116,8 @@ fun MessagesView( MessagesViewContent( state = state, modifier = Modifier.padding(padding), + onMessageClicked = ::onMessageClicked, + onMessageLongClicked = ::onMessageLongClicked ) }, snackbarHost = { @@ -102,10 +128,6 @@ fun MessagesView( }, ) - fun onActionSelected(action: TimelineItemAction, messageEvent: MessagesTimelineItemState.MessageEvent) { - state.eventSink(MessagesEvents.HandleAction(action, messageEvent)) - } - ActionListView( state = state.actionListState, modalBottomSheetState = itemActionsBottomSheetState, @@ -116,43 +138,32 @@ fun MessagesView( @Composable fun MessagesViewContent( state: MessagesState, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onMessageClicked: (TimelineItem.MessageEvent) -> Unit = {}, + onMessageLongClicked: (TimelineItem.MessageEvent) -> Unit = {}, ) { - - fun onMessageClicked(messageEvent: MessagesTimelineItemState.MessageEvent) { - Timber.v("OnMessageClicked= $messageEvent") - } - - fun onMessageLongClicked(messageEvent: MessagesTimelineItemState.MessageEvent) { - Timber.v("OnMessageLongClicked= $messageEvent") - } - Column( modifier = modifier .fillMaxSize() .navigationBarsPadding() .imePadding() ) { + // Hide timeline if composer is full screen if (!state.composerState.isFullScreen) { TimelineView( state = state.timelineState, - modifier = Modifier.fillMaxWidth(), - onMessageClicked = ::onMessageClicked, - onMessageLongClicked = ::onMessageLongClicked + modifier = Modifier.weight(1f), + onMessageClicked = onMessageClicked, + onMessageLongClicked = onMessageLongClicked ) } MessageComposerView( state = state.composerState, modifier = Modifier .fillMaxWidth() - .let { - if (state.composerState.isFullScreen) { - it.weight(1f, fill = false) - } else { - it.wrapContentHeight(Alignment.Bottom) - } - }, + .wrapContentHeight(Alignment.Bottom) ) + } } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListEvents.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListEvents.kt index 07554c88b0..3cddfb7f90 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListEvents.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListEvents.kt @@ -1,8 +1,8 @@ package io.element.android.x.features.messages.actionlist -import io.element.android.x.features.messages.model.MessagesTimelineItemState +import io.element.android.x.features.messages.timeline.model.TimelineItem sealed interface ActionListEvents { object Clear : ActionListEvents - data class ComputeForMessage(val messageEvent: MessagesTimelineItemState.MessageEvent) : ActionListEvents + data class ComputeForMessage(val messageEvent: TimelineItem.MessageEvent) : ActionListEvents } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt index f8fac36a89..e7e3020c4a 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt @@ -6,8 +6,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import io.element.android.x.architecture.Presenter -import io.element.android.x.features.messages.model.MessagesTimelineItemState -import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent +import io.element.android.x.features.messages.actionlist.model.TimelineItemAction +import io.element.android.x.features.messages.timeline.model.TimelineItem +import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -37,10 +38,10 @@ class ActionListPresenter @Inject constructor() : Presenter { ) } - fun CoroutineScope.computeForMessage(messagesTimelineItemState: MessagesTimelineItemState.MessageEvent, target: MutableState) = launch { - target.value = ActionListState.Target.Loading(messagesTimelineItemState) + fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.MessageEvent, target: MutableState) = launch { + target.value = ActionListState.Target.Loading(timelineItem) val actions = - if (messagesTimelineItemState.content is MessagesTimelineItemRedactedContent) { + if (timelineItem.content is TimelineItemRedactedContent) { emptyList() } else { mutableListOf( @@ -48,12 +49,12 @@ class ActionListPresenter @Inject constructor() : Presenter { TimelineItemAction.Forward, TimelineItemAction.Copy, ).also { - if (messagesTimelineItemState.isMine) { + if (timelineItem.isMine) { it.add(TimelineItemAction.Edit) it.add(TimelineItemAction.Redact) } } } - target.value = ActionListState.Target.Success(messagesTimelineItemState, actions.toImmutableList()) + target.value = ActionListState.Target.Success(timelineItem, actions.toImmutableList()) } } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListState.kt index d81a794fa0..00c3ec59ab 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListState.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListState.kt @@ -17,20 +17,21 @@ package io.element.android.x.features.messages.actionlist import androidx.compose.runtime.Immutable -import io.element.android.x.features.messages.model.MessagesTimelineItemState +import io.element.android.x.features.messages.actionlist.model.TimelineItemAction +import io.element.android.x.features.messages.timeline.model.TimelineItem import kotlinx.collections.immutable.ImmutableList @Immutable data class ActionListState( - val target: Target = Target.None, - val eventSink: (ActionListEvents) -> Unit = {}, + val target: Target, + val eventSink: (ActionListEvents) -> Unit, ) { sealed interface Target { object None : Target - data class Loading(val messageEvent: MessagesTimelineItemState.MessageEvent) : Target + data class Loading(val messageEvent: TimelineItem.MessageEvent) : Target data class Success( - val messageEvent: MessagesTimelineItemState.MessageEvent, + val messageEvent: TimelineItem.MessageEvent, val actions: ImmutableList, ) : Target } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt index 06a3824686..6f19db9562 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt @@ -26,7 +26,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import io.element.android.x.designsystem.components.VectorIcon -import io.element.android.x.features.messages.model.MessagesTimelineItemState +import io.element.android.x.features.messages.actionlist.model.TimelineItemAction +import io.element.android.x.features.messages.timeline.model.TimelineItem import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -34,7 +35,7 @@ import kotlinx.coroutines.launch fun ActionListView( state: ActionListState, modalBottomSheetState: ModalBottomSheetState, - onActionSelected: (action: TimelineItemAction, MessagesTimelineItemState.MessageEvent) -> Unit, + onActionSelected: (action: TimelineItemAction, TimelineItem.MessageEvent) -> Unit, modifier: Modifier = Modifier ) { val coroutineScope = rememberCoroutineScope() @@ -48,7 +49,7 @@ fun ActionListView( fun onItemActionClicked( itemAction: TimelineItemAction, - targetItem: MessagesTimelineItemState.MessageEvent + targetItem: TimelineItem.MessageEvent ) { onActionSelected(itemAction, targetItem) coroutineScope.launch { @@ -75,7 +76,7 @@ fun ActionListView( private fun SheetContent( state: ActionListState, modifier: Modifier = Modifier, - onActionClicked: (TimelineItemAction, MessagesTimelineItemState.MessageEvent) -> Unit = { _, _ -> }, + onActionClicked: (TimelineItemAction, TimelineItem.MessageEvent) -> Unit = { _, _ -> }, ) { when (val target = state.target) { is ActionListState.Target.Loading, diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/TimelineItemAction.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/model/TimelineItemAction.kt similarity index 92% rename from features/messages/src/main/java/io/element/android/x/features/messages/actionlist/TimelineItemAction.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/actionlist/model/TimelineItemAction.kt index 36ce779ab3..4e23eb34f0 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/TimelineItemAction.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/model/TimelineItemAction.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.features.messages.actionlist +package io.element.android.x.features.messages.actionlist.model import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt index 2c51a0199b..65652da85e 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt @@ -1,12 +1,13 @@ package io.element.android.x.features.messages.textcomposer import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.x.architecture.Presenter +import io.element.android.x.core.data.StableCharSequence import io.element.android.x.core.data.toStableCharSequence -import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.textcomposer.MessageComposerMode import kotlinx.coroutines.CoroutineScope @@ -15,7 +16,6 @@ import javax.inject.Inject class MessageComposerPresenter @Inject constructor( private val appCoroutineScope: CoroutineScope, - private val client: MatrixClient, private val room: MatrixRoom ) : Presenter { @@ -24,25 +24,32 @@ class MessageComposerPresenter @Inject constructor( val isFullScreen = rememberSaveable { mutableStateOf(false) } - val text: MutableState = rememberSaveable { - mutableStateOf("") + val text: MutableState = rememberSaveable { + mutableStateOf(StableCharSequence("")) } val composerMode: MutableState = rememberSaveable { mutableStateOf(MessageComposerMode.Normal("")) } + LaunchedEffect(composerMode.value) { + when (val modeValue = composerMode.value) { + is MessageComposerMode.Edit -> text.value = modeValue.defaultContent.toStableCharSequence() + else -> Unit + } + } + fun handleEvents(event: MessageComposerEvents) { when (event) { MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value - is MessageComposerEvents.UpdateText -> text.value = event.text + is MessageComposerEvents.UpdateText -> text.value = event.text.toStableCharSequence() MessageComposerEvents.CloseSpecialMode -> composerMode.setToNormal() - is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode) + is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text) is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode } } return MessageComposerState( - text = text.value.toStableCharSequence(), + text = text.value, isFullScreen = isFullScreen.value, mode = composerMode.value, eventSink = ::handleEvents @@ -53,9 +60,10 @@ class MessageComposerPresenter @Inject constructor( value = MessageComposerMode.Normal("") } - private fun CoroutineScope.sendMessage(text: String, composerMode: MutableState) = launch { + private fun CoroutineScope.sendMessage(text: String, composerMode: MutableState, textState: MutableState) = launch { val capturedMode = composerMode.value // Reset composer right away + textState.value = "".toStableCharSequence() composerMode.setToNormal() when (capturedMode) { is MessageComposerMode.Normal -> room.sendMessage(text) diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerState.kt index dbd92f6139..44c8e73d7e 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerState.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerState.kt @@ -22,16 +22,10 @@ import io.element.android.x.textcomposer.MessageComposerMode @Immutable data class MessageComposerState( - // val roomId: String, - // val canSendMessage: CanSendStatus = CanSendStatus.Allowed, - val isSendButtonVisible: Boolean = false, - val rootThreadEventId: String? = null, - val startsThread: Boolean = false, - // val sendMode: SendMode = SendMode.Regular("", false), - // val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle, - // val voiceBroadcastState: VoiceBroadcastState? = null, - val text: StableCharSequence? = null, - val isFullScreen: Boolean = false, - val mode: MessageComposerMode = MessageComposerMode.Normal(""), - val eventSink: (MessageComposerEvents) -> Unit = {} -) + val text: StableCharSequence?, + val isFullScreen: Boolean, + val mode: MessageComposerMode, + val eventSink: (MessageComposerEvents) -> Unit +) { + val isSendButtonVisible: Boolean = text?.charSequence.isNullOrEmpty().not() +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt index 74402a9e6d..cf417d5598 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt @@ -7,7 +7,7 @@ import io.element.android.x.textcomposer.TextComposer @Composable fun MessageComposerView( state: MessageComposerState, - modifier: Modifier + modifier: Modifier = Modifier, ) { fun onFullscreenToggle() { diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineItemsFactory.kt similarity index 70% rename from features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineItemsFactory.kt index 68bf073de7..ed9594aa8c 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineItemsFactory.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,29 +14,31 @@ * limitations under the License. */ -package io.element.android.x.features.messages +package io.element.android.x.features.messages.timeline import androidx.recyclerview.widget.DiffUtil import io.element.android.x.designsystem.components.avatar.AvatarSize -import io.element.android.x.features.messages.diff.CacheInvalidator -import io.element.android.x.features.messages.diff.MatrixTimelineItemsDiffCallback -import io.element.android.x.features.messages.model.AggregatedReaction -import io.element.android.x.features.messages.model.MessagesItemGroupPosition -import io.element.android.x.features.messages.model.MessagesItemReactionState -import io.element.android.x.features.messages.model.MessagesTimelineItemState -import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemEmoteContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemNoticeContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent -import io.element.android.x.features.messages.util.invalidateLast +import io.element.android.x.features.messages.timeline.diff.CacheInvalidator +import io.element.android.x.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback +import io.element.android.x.features.messages.timeline.model.AggregatedReaction +import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition +import io.element.android.x.features.messages.timeline.model.TimelineItemReactions +import io.element.android.x.features.messages.timeline.model.TimelineItem +import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemEmoteContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemNoticeContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent +import io.element.android.x.features.messages.timeline.util.invalidateLast +import io.element.android.x.matrix.core.EventId import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.timeline.MatrixTimelineItem import io.element.android.x.matrix.ui.MatrixItemHelper +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -50,24 +52,25 @@ import org.matrix.rustcomponents.sdk.FormattedBody import org.matrix.rustcomponents.sdk.MessageFormat import org.matrix.rustcomponents.sdk.MessageType import timber.log.Timber +import javax.inject.Inject import kotlin.system.measureTimeMillis -class MessageTimelineItemStateFactory( +class TimelineItemsFactory @Inject constructor( private val matrixItemHelper: MatrixItemHelper, private val room: MatrixRoom, private val dispatcher: CoroutineDispatcher, ) { - private val timelineItemStates = MutableStateFlow>(emptyList()) - private val timelineItemStatesCache = arrayListOf() + private val timelineItems = MutableStateFlow>(emptyList()) + private val timelineItemsCache = arrayListOf() // Items from rust sdk, used for diffing - private var timelineItems: List = emptyList() + private var matrixTimelineItems: List = emptyList() private val lock = Mutex() - private val cacheInvalidator = CacheInvalidator(timelineItemStatesCache) + private val cacheInvalidator = CacheInvalidator(timelineItemsCache) - fun flow(): StateFlow> = timelineItemStates.asStateFlow() + fun flow(): StateFlow> = timelineItems.asStateFlow() suspend fun replaceWith( timelineItems: List, @@ -83,17 +86,17 @@ class MessageTimelineItemStateFactory( ) = withContext(dispatcher) { lock.withLock { // Makes sure to invalidate last as we need to recompute some data (like groupPosition) - timelineItemStatesCache.invalidateLast() - timelineItemStatesCache.add(null) - timelineItems = timelineItems + timelineItem - buildAndEmitTimelineItemStates(timelineItems) + timelineItemsCache.invalidateLast() + timelineItemsCache.add(null) + matrixTimelineItems = matrixTimelineItems + timelineItem + buildAndEmitTimelineItemStates(matrixTimelineItems) } } private suspend fun buildAndEmitTimelineItemStates(timelineItems: List) { - val newTimelineItemStates = ArrayList() - for (index in timelineItemStatesCache.indices.reversed()) { - val cacheItem = timelineItemStatesCache[index] + val newTimelineItemStates = ArrayList() + for (index in timelineItemsCache.indices.reversed()) { + val cacheItem = timelineItemsCache[index] if (cacheItem == null) { buildAndCacheItem(timelineItems, index)?.also { timelineItemState -> newTimelineItemStates.add(timelineItemState) @@ -102,18 +105,18 @@ class MessageTimelineItemStateFactory( newTimelineItemStates.add(cacheItem) } } - timelineItemStates.emit(newTimelineItemStates) + this.timelineItems.emit(newTimelineItemStates) } private fun calculateAndApplyDiff(newTimelineItems: List) { val timeToDiff = measureTimeMillis { val diffCallback = MatrixTimelineItemsDiffCallback( - oldList = timelineItems, + oldList = matrixTimelineItems, newList = newTimelineItems ) val diffResult = DiffUtil.calculateDiff(diffCallback, false) - timelineItems = newTimelineItems + matrixTimelineItems = newTimelineItems diffResult.dispatchUpdatesTo(cacheInvalidator) } Timber.v("Time to apply diff on new list of ${newTimelineItems.size} items: $timeToDiff ms") @@ -122,7 +125,7 @@ class MessageTimelineItemStateFactory( private suspend fun buildAndCacheItem( timelineItems: List, index: Int - ): MessagesTimelineItemState? { + ): TimelineItem? { val timelineItemState = when (val currentTimelineItem = timelineItems[index]) { is MatrixTimelineItem.Event -> { @@ -132,12 +135,12 @@ class MessageTimelineItemStateFactory( timelineItems, ) } - is MatrixTimelineItem.Virtual -> MessagesTimelineItemState.Virtual( + is MatrixTimelineItem.Virtual -> TimelineItem.Virtual( "virtual_item_$index" ) MatrixTimelineItem.Other -> null } - timelineItemStatesCache[index] = timelineItemState + timelineItemsCache[index] = timelineItemState return timelineItemState } @@ -145,7 +148,7 @@ class MessageTimelineItemStateFactory( currentTimelineItem: MatrixTimelineItem.Event, index: Int, timelineItems: List, - ): MessagesTimelineItemState.MessageEvent { + ): TimelineItem.MessageEvent { val currentSender = currentTimelineItem.event.sender() val groupPosition = computeGroupPosition(currentTimelineItem, timelineItems, index) @@ -157,8 +160,8 @@ class MessageTimelineItemStateFactory( url = senderAvatarUrl, size = AvatarSize.SMALL ) - return MessagesTimelineItemState.MessageEvent( - id = currentTimelineItem.uniqueId, + return TimelineItem.MessageEvent( + id = EventId(currentTimelineItem.uniqueId), senderId = currentSender, senderDisplayName = senderDisplayName, senderAvatar = senderAvatarData, @@ -169,24 +172,24 @@ class MessageTimelineItemStateFactory( ) } - private fun MatrixTimelineItem.Event.computeReactionsState(): MessagesItemReactionState { + private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions { val aggregatedReactions = event.reactions().map { AggregatedReaction(key = it.key, count = it.count.toString(), isHighlighted = false) } - return MessagesItemReactionState(aggregatedReactions) + return TimelineItemReactions(aggregatedReactions.toImmutableList()) } - private fun MatrixTimelineItem.Event.computeContent(): MessagesTimelineItemContent { + private fun MatrixTimelineItem.Event.computeContent(): TimelineItemContent { val content = event.content() content.asUnableToDecrypt()?.let { encryptedMessage -> - return MessagesTimelineItemEncryptedContent(encryptedMessage) + return TimelineItemEncryptedContent(encryptedMessage) } if (content.isRedactedMessage()) { - return MessagesTimelineItemRedactedContent + return TimelineItemRedactedContent } val contentAsMessage = content.asMessage() return when (val messageType = contentAsMessage?.msgtype()) { - is MessageType.Emote -> MessagesTimelineItemEmoteContent( + is MessageType.Emote -> TimelineItemEmoteContent( body = messageType.content.body, htmlDocument = messageType.content.formatted?.toHtmlDocument() ) @@ -198,7 +201,7 @@ class MessageTimelineItemStateFactory( } else { 0.7f } - MessagesTimelineItemImageContent( + TimelineItemImageContent( body = messageType.content.body, imageMeta = MediaResolver.Meta( source = messageType.content.source, @@ -208,15 +211,15 @@ class MessageTimelineItemStateFactory( aspectRatio = aspectRatio ) } - is MessageType.Notice -> MessagesTimelineItemNoticeContent( + is MessageType.Notice -> TimelineItemNoticeContent( body = messageType.content.body, htmlDocument = messageType.content.formatted?.toHtmlDocument() ) - is MessageType.Text -> MessagesTimelineItemTextContent( + is MessageType.Text -> TimelineItemTextContent( body = messageType.content.body, htmlDocument = messageType.content.formatted?.toHtmlDocument() ) - else -> MessagesTimelineItemUnknownContent + else -> TimelineItemUnknownContent } } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt index 35167d82d8..6235f6784a 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt @@ -8,15 +8,14 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable -import io.element.android.x.architecture.Async import io.element.android.x.architecture.Presenter -import io.element.android.x.features.messages.MessageTimelineItemStateFactory import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.core.EventId import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.timeline.MatrixTimeline import io.element.android.x.matrix.timeline.MatrixTimelineItem import io.element.android.x.matrix.ui.MatrixItemHelper +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn @@ -34,13 +33,13 @@ class TimelinePresenter @Inject constructor( private val timeline = room.timeline() private val matrixItemHelper = MatrixItemHelper(client) - private val messageTimelineItemStateFactory = - MessageTimelineItemStateFactory(matrixItemHelper, room, Dispatchers.Default) + private val timelineItemsFactory = + TimelineItemsFactory(matrixItemHelper, room, Dispatchers.Default) - private class TimelineCallback(private val coroutineScope: CoroutineScope, private val messageTimelineItemStateFactory: MessageTimelineItemStateFactory) : MatrixTimeline.Callback { + private class TimelineCallback(private val coroutineScope: CoroutineScope, private val timelineItemsFactory: TimelineItemsFactory) : MatrixTimeline.Callback { override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) { coroutineScope.launch { - messageTimelineItemStateFactory.pushItem(timelineItem) + timelineItemsFactory.pushItem(timelineItem) } } } @@ -55,7 +54,7 @@ class TimelinePresenter @Inject constructor( val highlightedEventId: MutableState = rememberSaveable { mutableStateOf(null) } - val timelineItems = messageTimelineItemStateFactory + val timelineItems = timelineItemsFactory .flow() .collectAsState(emptyList()) @@ -69,12 +68,12 @@ class TimelinePresenter @Inject constructor( LaunchedEffect(Unit) { timeline .timelineItems() - .onEach(messageTimelineItemStateFactory::replaceWith) + .onEach(timelineItemsFactory::replaceWith) .launchIn(this) } DisposableEffect(Unit) { - timeline.callback = TimelineCallback(localCoroutineScope, messageTimelineItemStateFactory) + timeline.callback = TimelineCallback(localCoroutineScope, timelineItemsFactory) timeline.initialize() onDispose { timeline.callback = null @@ -84,13 +83,13 @@ class TimelinePresenter @Inject constructor( return TimelineState( highlightedEventId = highlightedEventId.value, - timelineItems = Async.Success(timelineItems.value), + timelineItems = timelineItems.value.toImmutableList(), hasMoreToLoad = hasMoreToLoad.value, eventSink = ::handleEvents ) } - fun CoroutineScope.loadMore(hasMoreToLoad: MutableState) = launch { + private fun CoroutineScope.loadMore(hasMoreToLoad: MutableState) = launch { timeline.paginateBackwards(PAGINATION_COUNT) hasMoreToLoad.value = timeline.hasMoreToLoad } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineState.kt index 605bd43fb8..c4ea612334 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineState.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineState.kt @@ -17,14 +17,14 @@ package io.element.android.x.features.messages.timeline import androidx.compose.runtime.Immutable -import io.element.android.x.architecture.Async -import io.element.android.x.features.messages.model.MessagesTimelineItemState +import io.element.android.x.features.messages.timeline.model.TimelineItem import io.element.android.x.matrix.core.EventId +import kotlinx.collections.immutable.ImmutableList @Immutable data class TimelineState( - val timelineItems: Async> = Async.Uninitialized, - val hasMoreToLoad: Boolean = true, - val highlightedEventId: EventId? = null, - val eventSink: (TimelineEvents) -> Unit = {} + val timelineItems: ImmutableList, + val hasMoreToLoad: Boolean, + val highlightedEventId: EventId?, + val eventSink: (TimelineEvents) -> Unit ) diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineView.kt index b04cc2a1a3..fa938f58dc 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineView.kt @@ -42,32 +42,31 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex -import io.element.android.x.architecture.Async import io.element.android.x.core.compose.PairCombinedPreviewParameter import io.element.android.x.designsystem.components.avatar.Avatar import io.element.android.x.designsystem.components.avatar.AvatarData -import io.element.android.x.features.messages.model.AggregatedReaction -import io.element.android.x.features.messages.model.MessagesItemGroupPosition -import io.element.android.x.features.messages.model.MessagesItemGroupPositionProvider -import io.element.android.x.features.messages.model.MessagesItemReactionState -import io.element.android.x.features.messages.model.MessagesTimelineItemState -import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemContentProvider -import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent +import io.element.android.x.features.messages.timeline.model.AggregatedReaction +import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition +import io.element.android.x.features.messages.timeline.model.TimelineItemGroupPositionProvider +import io.element.android.x.features.messages.timeline.model.TimelineItemReactions +import io.element.android.x.features.messages.timeline.model.TimelineItem +import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent +import io.element.android.x.features.messages.timeline.model.content.MessagesTimelineItemContentProvider +import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent import io.element.android.x.features.messages.timeline.components.MessageEventBubble -import io.element.android.x.features.messages.timeline.components.MessagesReactionsView -import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemEncryptedView -import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemImageView -import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemRedactedView -import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemTextView -import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemUnknownView +import io.element.android.x.features.messages.timeline.components.TimelineItemReactionsView +import io.element.android.x.features.messages.timeline.components.TimelineItemEncryptedView +import io.element.android.x.features.messages.timeline.components.TimelineItemImageView +import io.element.android.x.features.messages.timeline.components.TimelineItemRedactedView +import io.element.android.x.features.messages.timeline.components.TimelineItemTextView +import io.element.android.x.features.messages.timeline.components.TimelineItemUnknownView +import io.element.android.x.matrix.core.EventId import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -75,13 +74,11 @@ import kotlinx.coroutines.launch fun TimelineView( state: TimelineState, modifier: Modifier = Modifier, - onMessageClicked: (MessagesTimelineItemState.MessageEvent) -> Unit = {}, - onMessageLongClicked: (MessagesTimelineItemState.MessageEvent) -> Unit = {}, + onMessageClicked: (TimelineItem.MessageEvent) -> Unit = {}, + onMessageLongClicked: (TimelineItem.MessageEvent) -> Unit = {}, ) { val lazyListState = rememberLazyListState() - val timelineItems = state.timelineItems.dataOrNull().orEmpty().toImmutableList() - - Box(modifier = modifier.fillMaxWidth()) { + Box(modifier = modifier) { LazyColumn( modifier = Modifier.fillMaxSize(), state = lazyListState, @@ -90,7 +87,7 @@ fun TimelineView( reverseLayout = true ) { items( - items = timelineItems, + items = state.timelineItems, contentType = { timelineItem -> timelineItem.contentType() }, key = { timelineItem -> timelineItem.key() }, ) { timelineItem -> @@ -114,36 +111,36 @@ fun TimelineView( TimelineScrollHelper( lazyListState = lazyListState, - timelineItems = timelineItems, + timelineItems = state.timelineItems, onLoadMore = ::onReachedLoadMore ) } } -private fun MessagesTimelineItemState.key(): String { +private fun TimelineItem.key(): String { return when (this) { - is MessagesTimelineItemState.MessageEvent -> id - is MessagesTimelineItemState.Virtual -> id + is TimelineItem.MessageEvent -> id.value + is TimelineItem.Virtual -> id } } -private fun MessagesTimelineItemState.contentType(): Int { +private fun TimelineItem.contentType(): Int { return when (this) { - is MessagesTimelineItemState.MessageEvent -> 0 - is MessagesTimelineItemState.Virtual -> 1 + is TimelineItem.MessageEvent -> 0 + is TimelineItem.Virtual -> 1 } } @Composable fun TimelineItemRow( - timelineItem: MessagesTimelineItemState, + timelineItem: TimelineItem, isHighlighted: Boolean, - onClick: (MessagesTimelineItemState.MessageEvent) -> Unit, - onLongClick: (MessagesTimelineItemState.MessageEvent) -> Unit, + onClick: (TimelineItem.MessageEvent) -> Unit, + onLongClick: (TimelineItem.MessageEvent) -> Unit, ) { when (timelineItem) { - is MessagesTimelineItemState.Virtual -> return - is MessagesTimelineItemState.MessageEvent -> MessageEventRow( + is TimelineItem.Virtual -> return + is TimelineItem.MessageEvent -> MessageEventRow( messageEvent = timelineItem, isHighlighted = isHighlighted, onClick = { onClick(timelineItem) }, @@ -154,7 +151,7 @@ fun TimelineItemRow( @Composable fun MessageEventRow( - messageEvent: MessagesTimelineItemState.MessageEvent, + messageEvent: TimelineItem.MessageEvent, isHighlighted: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, @@ -197,32 +194,32 @@ fun MessageEventRow( ) { val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) when (messageEvent.content) { - is MessagesTimelineItemEncryptedContent -> MessagesTimelineItemEncryptedView( + is TimelineItemEncryptedContent -> TimelineItemEncryptedView( content = messageEvent.content, modifier = contentModifier ) - is MessagesTimelineItemRedactedContent -> MessagesTimelineItemRedactedView( + is TimelineItemRedactedContent -> TimelineItemRedactedView( content = messageEvent.content, modifier = contentModifier ) - is MessagesTimelineItemTextBasedContent -> MessagesTimelineItemTextView( + is TimelineItemTextBasedContent -> TimelineItemTextView( content = messageEvent.content, interactionSource = interactionSource, modifier = contentModifier, onTextClicked = onClick, onTextLongClicked = onLongClick ) - is MessagesTimelineItemUnknownContent -> MessagesTimelineItemUnknownView( + is TimelineItemUnknownContent -> TimelineItemUnknownView( content = messageEvent.content, modifier = contentModifier ) - is MessagesTimelineItemImageContent -> MessagesTimelineItemImageView( + is TimelineItemImageContent -> TimelineItemImageView( content = messageEvent.content, modifier = contentModifier ) } } - MessagesReactionsView( + TimelineItemReactionsView( reactionsState = messageEvent.reactionsState, modifier = Modifier .zIndex(1f) @@ -264,7 +261,7 @@ private fun MessageSenderInformation( @Composable internal fun BoxScope.TimelineScrollHelper( lazyListState: LazyListState, - timelineItems: ImmutableList, + timelineItems: ImmutableList, onLoadMore: () -> Unit = {}, ) { val coroutineScope = rememberCoroutineScope() @@ -338,8 +335,8 @@ internal fun TimelineLoadingMoreIndicator() { } class MessagesItemGroupPositionToMessagesTimelineItemContentProvider : - PairCombinedPreviewParameter( - MessagesItemGroupPositionProvider() to MessagesTimelineItemContentProvider() + PairCombinedPreviewParameter( + TimelineItemGroupPositionProvider() to MessagesTimelineItemContentProvider() ) @Suppress("PreviewPublic") @@ -347,7 +344,7 @@ class MessagesItemGroupPositionToMessagesTimelineItemContentProvider : @Composable fun TimelineItemsPreview( @PreviewParameter(MessagesTimelineItemContentProvider::class) - content: MessagesTimelineItemContent + content: TimelineItemContent ) { val timelineItems = persistentListOf( // 3 items (First Middle Last) with isMine = false @@ -385,23 +382,26 @@ fun TimelineItemsPreview( ) TimelineView( state = TimelineState( - timelineItems = Async.Success(timelineItems) + timelineItems = timelineItems, + hasMoreToLoad = true, + highlightedEventId = null, + eventSink = {} ) ) } private fun createMessageEvent( isMine: Boolean, - content: MessagesTimelineItemContent, + content: TimelineItemContent, groupPosition: MessagesItemGroupPosition -): MessagesTimelineItemState { - return MessagesTimelineItemState.MessageEvent( - id = Math.random().toString(), +): TimelineItem { + return TimelineItem.MessageEvent( + id = EventId(Math.random().toString()), senderId = "senderId", senderAvatar = AvatarData("sender"), content = content, - reactionsState = MessagesItemReactionState( - listOf( + reactionsState = TimelineItemReactions( + persistentListOf( AggregatedReaction("👍", "1") ) ), diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessageEventBubble.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessageEventBubble.kt index 447e1bdd60..7466d38e1a 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessageEventBubble.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessageEventBubble.kt @@ -36,7 +36,7 @@ import io.element.android.x.designsystem.SystemGrey5Dark import io.element.android.x.designsystem.SystemGrey5Light import io.element.android.x.designsystem.SystemGrey6Dark import io.element.android.x.designsystem.SystemGrey6Light -import io.element.android.x.features.messages.model.MessagesItemGroupPosition +import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition private val BUBBLE_RADIUS = 16.dp diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemEncryptedView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemEncryptedView.kt similarity index 82% rename from features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemEncryptedView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemEncryptedView.kt index cfccdfae6b..0b037c6f1f 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemEncryptedView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemEncryptedView.kt @@ -20,14 +20,14 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent @Composable -fun MessagesTimelineItemEncryptedView( - content: MessagesTimelineItemEncryptedContent, +fun TimelineItemEncryptedView( + content: TimelineItemEncryptedContent, modifier: Modifier = Modifier ) { - MessagesTimelineItemInformativeView( + TimelineItemInformativeView( text = "Decryption error", iconDescription = "Warning", icon = Icons.Default.Warning, diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemImageView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemImageView.kt similarity index 92% rename from features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemImageView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemImageView.kt index 53647ff652..a630959bcb 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemImageView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemImageView.kt @@ -33,11 +33,11 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import coil.compose.AsyncImage import coil.request.ImageRequest -import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent @Composable -fun MessagesTimelineItemImageView( - content: MessagesTimelineItemImageContent, +fun TimelineItemImageView( + content: TimelineItemImageContent, modifier: Modifier = Modifier ) { val widthPercent = if (content.aspectRatio > 1f) { diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemInformativeView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemInformativeView.kt similarity index 97% rename from features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemInformativeView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemInformativeView.kt index 200b17374a..6c876f9103 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemInformativeView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemInformativeView.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable -fun MessagesTimelineItemInformativeView( +fun TimelineItemInformativeView( text: String, iconDescription: String, icon: ImageVector, diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesReactionsView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemReactionsView.kt similarity index 91% rename from features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesReactionsView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemReactionsView.kt index 14b5cf99e2..8cc75befea 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesReactionsView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemReactionsView.kt @@ -32,12 +32,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.google.accompanist.flowlayout.FlowRow -import io.element.android.x.features.messages.model.AggregatedReaction -import io.element.android.x.features.messages.model.MessagesItemReactionState +import io.element.android.x.features.messages.timeline.model.AggregatedReaction +import io.element.android.x.features.messages.timeline.model.TimelineItemReactions @Composable -fun MessagesReactionsView( - reactionsState: MessagesItemReactionState, +fun TimelineItemReactionsView( + reactionsState: TimelineItemReactions, modifier: Modifier = Modifier, ) { if (reactionsState.reactions.isEmpty()) return diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemRedactedView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemRedactedView.kt similarity index 76% rename from features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemRedactedView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemRedactedView.kt index e3ef130d50..183b44de74 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemRedactedView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemRedactedView.kt @@ -20,15 +20,14 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent -import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemInformativeView +import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent @Composable -fun MessagesTimelineItemRedactedView( - content: MessagesTimelineItemRedactedContent, +fun TimelineItemRedactedView( + content: TimelineItemRedactedContent, modifier: Modifier = Modifier ) { - MessagesTimelineItemInformativeView( + TimelineItemInformativeView( text = "This message has been deleted", iconDescription = "Delete", icon = Icons.Default.Delete, diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemTextView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemTextView.kt similarity index 94% rename from features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemTextView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemTextView.kt index 9bc80e0de5..4751f2ae0d 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemTextView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemTextView.kt @@ -31,11 +31,11 @@ import androidx.core.text.util.LinkifyCompat import io.element.android.x.designsystem.LinkColor import io.element.android.x.designsystem.components.ClickableLinkText import io.element.android.x.features.messages.timeline.components.html.HtmlDocument -import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent @Composable -fun MessagesTimelineItemTextView( - content: MessagesTimelineItemTextBasedContent, +fun TimelineItemTextView( + content: TimelineItemTextBasedContent, interactionSource: MutableInteractionSource, modifier: Modifier = Modifier, onTextClicked: () -> Unit = {}, diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemUnknownView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemUnknownView.kt similarity index 82% rename from features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemUnknownView.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemUnknownView.kt index 2b7ac2f85e..88355abd90 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/MessagesTimelineItemUnknownView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/components/TimelineItemUnknownView.kt @@ -20,14 +20,14 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent @Composable -fun MessagesTimelineItemUnknownView( - content: MessagesTimelineItemUnknownContent, +fun TimelineItemUnknownView( + content: TimelineItemUnknownContent, modifier: Modifier = Modifier ) { - MessagesTimelineItemInformativeView( + TimelineItemInformativeView( text = "Event not handled by EAX", iconDescription = "Info", icon = Icons.Default.Info, diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/diff/CacheInvalidator.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/diff/CacheInvalidator.kt similarity index 86% rename from features/messages/src/main/java/io/element/android/x/features/messages/diff/CacheInvalidator.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/diff/CacheInvalidator.kt index a904951239..031d871033 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/diff/CacheInvalidator.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/diff/CacheInvalidator.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,14 @@ * limitations under the License. */ -package io.element.android.x.features.messages.diff +package io.element.android.x.features.messages.timeline.diff import androidx.recyclerview.widget.ListUpdateCallback -import io.element.android.x.features.messages.model.MessagesTimelineItemState -import io.element.android.x.features.messages.util.invalidateLast +import io.element.android.x.features.messages.timeline.model.TimelineItem +import io.element.android.x.features.messages.timeline.util.invalidateLast import timber.log.Timber -internal class CacheInvalidator(private val itemStatesCache: MutableList) : +internal class CacheInvalidator(private val itemStatesCache: MutableList) : ListUpdateCallback { override fun onChanged(position: Int, count: Int, payload: Any?) { diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/diff/MatrixTimelineItemsDiffCallback.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/diff/MatrixTimelineItemsDiffCallback.kt similarity index 96% rename from features/messages/src/main/java/io/element/android/x/features/messages/diff/MatrixTimelineItemsDiffCallback.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/diff/MatrixTimelineItemsDiffCallback.kt index e0198d626a..45b754043a 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/diff/MatrixTimelineItemsDiffCallback.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/diff/MatrixTimelineItemsDiffCallback.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.features.messages.diff +package io.element.android.x.features.messages.timeline.diff import androidx.recyclerview.widget.DiffUtil import io.element.android.x.matrix.timeline.MatrixTimelineItem diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesTimelineItemState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/TimelineItem.kt similarity index 70% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesTimelineItemState.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/TimelineItem.kt index 6dab9e37ae..ef93d38aa2 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesTimelineItemState.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/TimelineItem.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,27 +14,30 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model +package io.element.android.x.features.messages.timeline.model +import androidx.compose.runtime.Immutable import io.element.android.x.designsystem.components.avatar.AvatarData -import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent +import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent +import io.element.android.x.matrix.core.EventId -sealed interface MessagesTimelineItemState { +@Immutable +sealed interface TimelineItem { data class Virtual( val id: String - ) : MessagesTimelineItemState + ) : TimelineItem data class MessageEvent( - val id: String, + val id: EventId, val senderId: String, val senderDisplayName: String?, val senderAvatar: AvatarData, - val content: MessagesTimelineItemContent, + val content: TimelineItemContent, val sentTime: String = "", val isMine: Boolean = false, val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None, - val reactionsState: MessagesItemReactionState - ) : MessagesTimelineItemState { + val reactionsState: TimelineItemReactions + ) : TimelineItem { val showSenderInformation = groupPosition.isNew() && !isMine diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemGroupPosition.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/TimelineItemGroupPosition.kt similarity index 84% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemGroupPosition.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/TimelineItemGroupPosition.kt index b5385538a4..977c498bfc 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemGroupPosition.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/TimelineItemGroupPosition.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,12 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model +package io.element.android.x.features.messages.timeline.model +import androidx.compose.runtime.Immutable import androidx.compose.ui.tooling.preview.PreviewParameterProvider +@Immutable sealed interface MessagesItemGroupPosition { object First : MessagesItemGroupPosition object Middle : MessagesItemGroupPosition @@ -30,7 +32,7 @@ sealed interface MessagesItemGroupPosition { } } -internal class MessagesItemGroupPositionProvider : PreviewParameterProvider { +internal class TimelineItemGroupPositionProvider : PreviewParameterProvider { override val values = sequenceOf( MessagesItemGroupPosition.First, MessagesItemGroupPosition.Middle, diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemReactionState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/TimelineItemReactions.kt similarity index 74% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemReactionState.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/TimelineItemReactions.kt index b7cf57cf0a..ff0fd549bc 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemReactionState.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/TimelineItemReactions.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,14 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model +package io.element.android.x.features.messages.timeline.model -import androidx.compose.runtime.Stable +import kotlinx.collections.immutable.ImmutableList -@Stable -data class MessagesItemReactionState( - val reactions: List +data class TimelineItemReactions( + val reactions: ImmutableList ) -@Stable data class AggregatedReaction( val key: String, val count: String, diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemContent.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemContent.kt similarity index 74% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemContent.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemContent.kt index fdfb2448d4..9d7b8c3868 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemContent.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemContent.kt @@ -14,32 +14,32 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model.content +package io.element.android.x.features.messages.timeline.model.content import androidx.compose.ui.tooling.preview.PreviewParameterProvider import org.matrix.rustcomponents.sdk.EncryptedMessage -sealed interface MessagesTimelineItemContent +sealed interface TimelineItemContent -class MessagesTimelineItemContentProvider : PreviewParameterProvider { +class MessagesTimelineItemContentProvider : PreviewParameterProvider { override val values = sequenceOf( - MessagesTimelineItemEmoteContent( + TimelineItemEmoteContent( body = "Emote", htmlDocument = null ), - MessagesTimelineItemEncryptedContent( + TimelineItemEncryptedContent( encryptedMessage = EncryptedMessage.Unknown ), // TODO MessagesTimelineItemImageContent(), - MessagesTimelineItemNoticeContent( + TimelineItemNoticeContent( body = "Notice", htmlDocument = null ), - MessagesTimelineItemRedactedContent, - MessagesTimelineItemTextContent( + TimelineItemRedactedContent, + TimelineItemTextContent( body = "Text", htmlDocument = null ), - MessagesTimelineItemUnknownContent, + TimelineItemUnknownContent, ) } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemTextContent.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemEmoteContent.kt similarity index 82% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemTextContent.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemEmoteContent.kt index 6c4820dc51..c5c0a8a330 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemTextContent.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemEmoteContent.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model.content +package io.element.android.x.features.messages.timeline.model.content import org.jsoup.nodes.Document -data class MessagesTimelineItemTextContent( +data class TimelineItemEmoteContent( override val body: String, override val htmlDocument: Document? -) : MessagesTimelineItemTextBasedContent +) : TimelineItemTextBasedContent diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemEncryptedContent.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemEncryptedContent.kt similarity index 83% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemEncryptedContent.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemEncryptedContent.kt index 0e23c0ab87..62fd231191 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemEncryptedContent.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemEncryptedContent.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model.content +package io.element.android.x.features.messages.timeline.model.content import org.matrix.rustcomponents.sdk.EncryptedMessage -data class MessagesTimelineItemEncryptedContent( +data class TimelineItemEncryptedContent( val encryptedMessage: EncryptedMessage -) : MessagesTimelineItemContent +) : TimelineItemContent diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemImageContent.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemImageContent.kt similarity index 84% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemImageContent.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemImageContent.kt index 682c9838d2..25e983d6a4 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemImageContent.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemImageContent.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model.content +package io.element.android.x.features.messages.timeline.model.content import io.element.android.x.matrix.media.MediaResolver -data class MessagesTimelineItemImageContent( +data class TimelineItemImageContent( val body: String, val imageMeta: MediaResolver.Meta, val blurhash: String?, val aspectRatio: Float -) : MessagesTimelineItemContent +) : TimelineItemContent diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemEmoteContent.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemNoticeContent.kt similarity index 82% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemEmoteContent.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemNoticeContent.kt index 2d13b93471..1bb8df5673 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemEmoteContent.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemNoticeContent.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model.content +package io.element.android.x.features.messages.timeline.model.content import org.jsoup.nodes.Document -data class MessagesTimelineItemEmoteContent( +data class TimelineItemNoticeContent( override val body: String, override val htmlDocument: Document? -) : MessagesTimelineItemTextBasedContent +) : TimelineItemTextBasedContent diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemUnknownContent.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemRedactedContent.kt similarity index 81% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemUnknownContent.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemRedactedContent.kt index e46710c9fd..8de2088052 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemUnknownContent.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemRedactedContent.kt @@ -14,6 +14,6 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model.content +package io.element.android.x.features.messages.timeline.model.content -object MessagesTimelineItemUnknownContent : MessagesTimelineItemContent +object TimelineItemRedactedContent : TimelineItemContent diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemTextBasedContent.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemTextBasedContent.kt similarity index 82% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemTextBasedContent.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemTextBasedContent.kt index 14395a91fa..15f757bf6e 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemTextBasedContent.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemTextBasedContent.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model.content +package io.element.android.x.features.messages.timeline.model.content import org.jsoup.nodes.Document -sealed interface MessagesTimelineItemTextBasedContent : MessagesTimelineItemContent { +sealed interface TimelineItemTextBasedContent : TimelineItemContent { val body: String val htmlDocument: Document? } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemNoticeContent.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemTextContent.kt similarity index 82% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemNoticeContent.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemTextContent.kt index 8d85ba0c70..a3a887df66 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemNoticeContent.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemTextContent.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model.content +package io.element.android.x.features.messages.timeline.model.content import org.jsoup.nodes.Document -data class MessagesTimelineItemNoticeContent( +data class TimelineItemTextContent( override val body: String, override val htmlDocument: Document? -) : MessagesTimelineItemTextBasedContent +) : TimelineItemTextBasedContent diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemRedactedContent.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemUnknownContent.kt similarity index 81% rename from features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemRedactedContent.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemUnknownContent.kt index 36f7a29d55..44f6cb9af4 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemRedactedContent.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/model/content/TimelineItemUnknownContent.kt @@ -14,6 +14,6 @@ * limitations under the License. */ -package io.element.android.x.features.messages.model.content +package io.element.android.x.features.messages.timeline.model.content -object MessagesTimelineItemRedactedContent : MessagesTimelineItemContent +object TimelineItemUnknownContent : TimelineItemContent diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/util/MutableListExt.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/util/MutableListExt.kt similarity index 92% rename from features/messages/src/main/java/io/element/android/x/features/messages/util/MutableListExt.kt rename to features/messages/src/main/java/io/element/android/x/features/messages/timeline/util/MutableListExt.kt index 383cb04b95..d487af0cdc 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/util/MutableListExt.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/util/MutableListExt.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.features.messages.util +package io.element.android.x.features.messages.timeline.util internal inline fun MutableList.invalidateLast() { val indexOfLast = size 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 cd14368cb6..3c20fd677f 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 @@ -25,30 +25,23 @@ import io.element.android.x.matrix.core.SessionId 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 java.io.File -import java.util.concurrent.Executors -import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.AuthenticationService import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientBuilder import timber.log.Timber +import java.io.File +import javax.inject.Inject @SingleIn(AppScope::class) class Matrix @Inject constructor( private val coroutineScope: CoroutineScope, + private val coroutineDispatchers: CoroutineDispatchers, @ApplicationContext context: Context, ) { - private val coroutineDispatchers = CoroutineDispatchers( - io = Dispatchers.IO, - computation = Dispatchers.Default, - main = Dispatchers.Main, - diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - ) + private val baseDirectory = File(context.filesDir, "sessions") private val sessionStore = SessionStore(context) private val authService = AuthenticationService(baseDirectory.absolutePath) @@ -57,7 +50,7 @@ class Matrix @Inject constructor( return sessionStore.isLoggedIn() } - suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io){ + suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io) { sessionStore.getLatestSession()?.sessionId() } diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt index df86749a15..681b8839ee 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt @@ -17,6 +17,7 @@ package io.element.android.x.matrix.room import io.element.android.x.core.coroutine.CoroutineDispatchers +import io.element.android.x.matrix.core.EventId import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.timeline.MatrixTimeline import kotlinx.coroutines.CoroutineScope @@ -39,13 +40,15 @@ class MatrixRoom( private val coroutineDispatchers: CoroutineDispatchers, ) { - fun syncUpdateFlow(): Flow { + fun syncUpdateFlow(): Flow { return slidingSyncUpdateFlow .filter { it.rooms.contains(room.id()) } - .map { } - .onStart { emit(Unit) } + .map { + System.currentTimeMillis() + } + .onStart { emit(System.currentTimeMillis()) } } fun timeline(): MatrixTimeline { @@ -107,26 +110,26 @@ class MatrixRoom( } } - suspend fun editMessage(originalEventId: String, message: String): Result = withContext(coroutineDispatchers.io) { + suspend fun editMessage(originalEventId: EventId, message: String): Result = withContext(coroutineDispatchers.io) { val transactionId = genTransactionId() // val content = messageEventContentFromMarkdown(message) runCatching { - room.edit(/* TODO use content */ message, originalEventId, transactionId) + room.edit(/* TODO use content */ message, originalEventId.value, transactionId) } } - suspend fun replyMessage(eventId: String, message: String): Result = withContext(coroutineDispatchers.io) { + suspend fun replyMessage(eventId: EventId, message: String): Result = withContext(coroutineDispatchers.io) { val transactionId = genTransactionId() // val content = messageEventContentFromMarkdown(message) runCatching { - room.sendReply(/* TODO use content */ message, eventId, transactionId) + room.sendReply(/* TODO use content */ message, eventId.value, transactionId) } } - suspend fun redactEvent(eventId: String, reason: String? = null) = withContext(coroutineDispatchers.io) { + suspend fun redactEvent(eventId: EventId, reason: String? = null) = withContext(coroutineDispatchers.io) { val transactionId = genTransactionId() runCatching { - room.redact(eventId, reason, transactionId) + room.redact(eventId.value, reason, transactionId) } } } diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt index a302574b8e..bf613ca761 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt @@ -17,6 +17,7 @@ package io.element.android.x.matrix.timeline import io.element.android.x.core.coroutine.CoroutineDispatchers +import io.element.android.x.matrix.core.EventId import io.element.android.x.matrix.room.MatrixRoom import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview @@ -146,11 +147,11 @@ class MatrixTimeline( return matrixRoom.sendMessage(message) } - suspend fun editMessage(originalEventId: String, message: String): Result { + suspend fun editMessage(originalEventId: EventId, message: String): Result { return matrixRoom.editMessage(originalEventId, message = message) } - suspend fun replyMessage(inReplyToEventId: String, message: String): Result { + suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result { return matrixRoom.replyMessage(inReplyToEventId, message) } diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixItemHelper.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixItemHelper.kt index a040e18585..49a97b3a5f 100644 --- a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixItemHelper.kt +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixItemHelper.kt @@ -26,8 +26,9 @@ import io.element.android.x.matrix.ui.model.MatrixUser import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow +import javax.inject.Inject -class MatrixItemHelper( +class MatrixItemHelper @Inject constructor( private val client: MatrixClient ) { /** diff --git a/libraries/textcomposer/build.gradle.kts b/libraries/textcomposer/build.gradle.kts index e5d8942511..cda96fb319 100644 --- a/libraries/textcomposer/build.gradle.kts +++ b/libraries/textcomposer/build.gradle.kts @@ -32,6 +32,7 @@ android { dependencies { implementation(project(":libraries:elementresources")) implementation(project(":libraries:core")) + implementation(project(":libraries:matrix")) implementation(libs.wysiwyg) implementation(libs.androidx.constraintlayout) implementation("com.google.android.material:material:1.7.0") diff --git a/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerMode.kt b/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerMode.kt index 224409829c..715b9c6cd6 100644 --- a/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerMode.kt +++ b/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerMode.kt @@ -17,31 +17,32 @@ package io.element.android.x.textcomposer import android.os.Parcelable +import io.element.android.x.matrix.core.EventId import kotlinx.parcelize.Parcelize sealed interface MessageComposerMode : Parcelable { @Parcelize data class Normal(val content: CharSequence?) : MessageComposerMode - sealed class Special(open val eventId: String, open val defaultContent: CharSequence) : + sealed class Special(open val eventId: EventId, open val defaultContent: CharSequence) : MessageComposerMode @Parcelize - data class Edit(override val eventId: String, override val defaultContent: CharSequence) : + data class Edit(override val eventId: EventId, override val defaultContent: CharSequence) : Special(eventId, defaultContent) @Parcelize - class Quote(override val eventId: String, override val defaultContent: CharSequence) : + class Quote(override val eventId: EventId, override val defaultContent: CharSequence) : Special(eventId, defaultContent) @Parcelize class Reply( val senderName: String, - override val eventId: String, + override val eventId: EventId, override val defaultContent: CharSequence ) : Special(eventId, defaultContent) - val relatedEventId: String? + val relatedEventId: EventId? get() = when (this) { is Normal -> null is Edit -> eventId From 3abba8e88f080ec14118d8c5f8091f1aead792d3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 13 Jan 2023 18:20:10 +0100 Subject: [PATCH 24/30] Remove viewmodel/fragment references --- .../anvilannotations/ContributesViewModel.kt | 33 ---- .../ContributesViewModelCodeGenerator.kt | 143 ------------------ app/build.gradle.kts | 2 - .../element/android/x/ElementXApplication.kt | 2 - .../io/element/android/x/di/AppComponent.kt | 1 - .../element/android/x/di/SessionComponent.kt | 1 - .../x/initializer/MavericksInitializer.kt | 30 ---- features/login/build.gradle.kts | 1 - features/messages/build.gradle.kts | 1 - features/onboarding/build.gradle.kts | 1 - features/preferences/build.gradle.kts | 1 - features/rageshake/build.gradle.kts | 1 - gradle/libs.versions.toml | 5 - libraries/architecture/build.gradle.kts | 1 - .../viewmodel/AssistedViewModelFactory.kt | 24 --- .../DaggerMavericksViewModelFactory.kt | 74 --------- .../x/architecture/viewmodel/ViewModelKey.kt | 26 ---- .../viewmodel/ViewModelSupport.kt | 121 --------------- .../ui/viewmodels/user/UserViewModel.kt | 49 ------ .../ui/viewmodels/user/UserViewState.kt | 26 ---- .../java/extension/DependencyHandleScope.kt | 1 - 21 files changed, 544 deletions(-) delete mode 100644 anvilannotations/src/main/java/io/element/android/x/anvilannotations/ContributesViewModel.kt delete mode 100644 anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesViewModelCodeGenerator.kt delete mode 100644 app/src/main/java/io/element/android/x/initializer/MavericksInitializer.kt delete mode 100644 libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/AssistedViewModelFactory.kt delete mode 100644 libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/DaggerMavericksViewModelFactory.kt delete mode 100644 libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelKey.kt delete mode 100644 libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelSupport.kt delete mode 100644 libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewModel.kt delete mode 100644 libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewState.kt diff --git a/anvilannotations/src/main/java/io/element/android/x/anvilannotations/ContributesViewModel.kt b/anvilannotations/src/main/java/io/element/android/x/anvilannotations/ContributesViewModel.kt deleted file mode 100644 index 0695e93263..0000000000 --- a/anvilannotations/src/main/java/io/element/android/x/anvilannotations/ContributesViewModel.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.anvilannotations - -import kotlin.reflect.KClass - -/** - * Adds view model to the specified component graph. - * Equivalent to the following declaration in a dagger module: - * - * @Binds - * @IntoMap - * @ViewModelKey(YourViewModel::class) - * public abstract fun bindYourViewModelFactory(factory: YourViewModel.Factory): AssistedViewModelFactory<*, *> - */ -@Target(AnnotationTarget.CLASS) -annotation class ContributesViewModel( - val scope: KClass<*>, -) diff --git a/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesViewModelCodeGenerator.kt b/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesViewModelCodeGenerator.kt deleted file mode 100644 index e84d6cdd5f..0000000000 --- a/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesViewModelCodeGenerator.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn(ExperimentalAnvilApi::class) - -package io.element.android.x.anvilcodegen - -import com.google.auto.service.AutoService -import com.squareup.anvil.annotations.ContributesTo -import com.squareup.anvil.annotations.ExperimentalAnvilApi -import com.squareup.anvil.compiler.api.AnvilCompilationException -import com.squareup.anvil.compiler.api.AnvilContext -import com.squareup.anvil.compiler.api.CodeGenerator -import com.squareup.anvil.compiler.api.GeneratedFile -import com.squareup.anvil.compiler.api.createGeneratedFile -import com.squareup.anvil.compiler.internal.asClassName -import com.squareup.anvil.compiler.internal.buildFile -import com.squareup.anvil.compiler.internal.fqName -import com.squareup.anvil.compiler.internal.reference.ClassReference -import com.squareup.anvil.compiler.internal.reference.asClassName -import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences -import com.squareup.kotlinpoet.AnnotationSpec -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.FunSpec -import com.squareup.kotlinpoet.KModifier -import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy -import com.squareup.kotlinpoet.STAR -import com.squareup.kotlinpoet.TypeSpec -import dagger.Binds -import dagger.Module -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.multibindings.IntoMap -import io.element.android.x.anvilannotations.ContributesViewModel -import java.io.File -import org.jetbrains.kotlin.descriptors.ModuleDescriptor -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.psi.KtFile - -/** - * This is an anvil plugin that allows ViewModels to use [ContributesViewModel] alone and let this plugin automatically - * handle the rest of the Dagger wiring required for constructor injection. - */ -@AutoService(CodeGenerator::class) -class ContributesViewModelCodeGenerator : CodeGenerator { - - override fun isApplicable(context: AnvilContext): Boolean = true - - override fun generateCode(codeGenDir: File, module: ModuleDescriptor, projectFiles: Collection): Collection { - return projectFiles.classAndInnerClassReferences(module) - .filter { it.isAnnotatedWith(ContributesViewModel::class.fqName) } - .flatMap { listOf(generateModule(it, codeGenDir, module), generateAssistedFactory(it, codeGenDir, module)) } - .toList() - } - - private fun generateModule(vmClass: ClassReference.Psi, codeGenDir: File, module: ModuleDescriptor): GeneratedFile { - val generatedPackage = vmClass.packageFqName.toString() - val moduleClassName = "${vmClass.shortName}_Module" - val scope = vmClass.annotations.single { it.fqName == ContributesViewModel::class.fqName }.scope() - val content = FileSpec.buildFile(generatedPackage, moduleClassName) { - addType( - TypeSpec.classBuilder(moduleClassName) - .addModifiers(KModifier.ABSTRACT) - .addAnnotation(Module::class) - .addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.asClassName()).build()) - .addFunction( - FunSpec.builder("bind${vmClass.shortName}Factory") - .addModifiers(KModifier.ABSTRACT) - .addParameter("factory", ClassName(generatedPackage, "${vmClass.shortName}_AssistedFactory")) - .returns(assistedViewModelFactoryFqName.asClassName(module).parameterizedBy(STAR, STAR)) - .addAnnotation(Binds::class) - .addAnnotation(IntoMap::class) - .addAnnotation( - AnnotationSpec.Companion - .builder(viewModelKeyFqName.asClassName(module)) - .addMember("%T::class", vmClass.asClassName()) - .build() - ) - .build(), - ) - .build(), - ) - } - return createGeneratedFile(codeGenDir, generatedPackage, moduleClassName, content) - } - - private fun generateAssistedFactory(vmClass: ClassReference.Psi, codeGenDir: File, module: ModuleDescriptor): GeneratedFile { - val generatedPackage = vmClass.packageFqName.toString() - val assistedFactoryClassName = "${vmClass.shortName}_AssistedFactory" - val constructor = vmClass.constructors.singleOrNull { it.isAnnotatedWith(AssistedInject::class.fqName) } - val assistedParameter = constructor?.parameters?.singleOrNull { it.isAnnotatedWith(Assisted::class.fqName) } - if (constructor == null || assistedParameter == null) { - throw AnvilCompilationException( - "${vmClass.fqName} must have an @AssistedInject constructor with @Assisted initialState: S parameter", - element = vmClass.clazz, - ) - } - if (assistedParameter.name != "initialState") { - throw AnvilCompilationException( - "${vmClass.fqName} @Assisted parameter must be named initialState", - element = assistedParameter.parameter, - ) - } - val vmClassName = vmClass.asClassName() - val stateClassName = assistedParameter.type().asTypeName() - val content = FileSpec.buildFile(generatedPackage, assistedFactoryClassName) { - addType( - TypeSpec.interfaceBuilder(assistedFactoryClassName) - .addSuperinterface(assistedViewModelFactoryFqName.asClassName(module).parameterizedBy(vmClassName, stateClassName)) - .addAnnotation(AssistedFactory::class) - .addFunction( - FunSpec.builder("create") - .addModifiers(KModifier.OVERRIDE, KModifier.ABSTRACT) - .addParameter("initialState", stateClassName) - .returns(vmClassName) - .build(), - ) - .build(), - ) - } - return createGeneratedFile(codeGenDir, generatedPackage, assistedFactoryClassName, content) - } - - companion object { - private val assistedViewModelFactoryFqName = FqName("io.element.android.x.architecture.viewmodel.AssistedViewModelFactory") - private val viewModelKeyFqName = FqName("io.element.android.x.architecture.viewmodel.ViewModelKey") - } -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d3ce5a7a78..6c89c54683 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -175,11 +175,9 @@ dependencies { implementation(libs.androidx.corektx) implementation(libs.androidx.lifecycle.runtime) - implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.activity.compose) implementation(libs.androidx.startup) implementation(libs.coil) - implementation(libs.mavericks.compose) implementation(libs.dagger) kapt(libs.dagger.compiler) diff --git a/app/src/main/java/io/element/android/x/ElementXApplication.kt b/app/src/main/java/io/element/android/x/ElementXApplication.kt index 923a1ddc15..285c56d5d8 100644 --- a/app/src/main/java/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/java/io/element/android/x/ElementXApplication.kt @@ -23,7 +23,6 @@ import io.element.android.x.di.AppComponent import io.element.android.x.di.DaggerAppComponent import io.element.android.x.initializer.CrashInitializer import io.element.android.x.initializer.MatrixInitializer -import io.element.android.x.initializer.MavericksInitializer import io.element.android.x.initializer.TimberInitializer class ElementXApplication : Application(), DaggerComponentOwner { @@ -40,7 +39,6 @@ class ElementXApplication : Application(), DaggerComponentOwner { initializeComponent(CrashInitializer::class.java) initializeComponent(TimberInitializer::class.java) initializeComponent(MatrixInitializer::class.java) - initializeComponent(MavericksInitializer::class.java) } } } diff --git a/app/src/main/java/io/element/android/x/di/AppComponent.kt b/app/src/main/java/io/element/android/x/di/AppComponent.kt index 30f801e31d..342c18e3a4 100644 --- a/app/src/main/java/io/element/android/x/di/AppComponent.kt +++ b/app/src/main/java/io/element/android/x/di/AppComponent.kt @@ -21,7 +21,6 @@ import com.squareup.anvil.annotations.MergeComponent import dagger.BindsInstance import dagger.Component import io.element.android.x.architecture.NodeFactoriesBindings -import io.element.android.x.architecture.viewmodel.DaggerMavericksBindings @SingleIn(AppScope::class) @MergeComponent(AppScope::class) diff --git a/app/src/main/java/io/element/android/x/di/SessionComponent.kt b/app/src/main/java/io/element/android/x/di/SessionComponent.kt index 0ba9a12d58..298c97e66c 100644 --- a/app/src/main/java/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/java/io/element/android/x/di/SessionComponent.kt @@ -20,7 +20,6 @@ import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.annotations.MergeSubcomponent import dagger.BindsInstance import dagger.Subcomponent -import io.element.android.x.architecture.viewmodel.DaggerMavericksBindings import io.element.android.x.architecture.NodeFactoriesBindings import io.element.android.x.matrix.MatrixClient diff --git a/app/src/main/java/io/element/android/x/initializer/MavericksInitializer.kt b/app/src/main/java/io/element/android/x/initializer/MavericksInitializer.kt deleted file mode 100644 index f7d48b66c1..0000000000 --- a/app/src/main/java/io/element/android/x/initializer/MavericksInitializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.initializer - -import android.content.Context -import androidx.startup.Initializer -import com.airbnb.mvrx.Mavericks - -class MavericksInitializer : Initializer { - - override fun create(context: Context) { - Mavericks.initialize(context) - } - - override fun dependencies(): List>> = listOf() -} diff --git a/features/login/build.gradle.kts b/features/login/build.gradle.kts index de9192e437..29987189e9 100644 --- a/features/login/build.gradle.kts +++ b/features/login/build.gradle.kts @@ -41,7 +41,6 @@ dependencies { implementation(project(":libraries:designsystem")) implementation(project(":libraries:elementresources")) implementation(libs.appyx.core) - implementation(libs.mavericks.compose) ksp(libs.showkase.processor) testImplementation(libs.test.junit) androidTestImplementation(libs.test.junitext) diff --git a/features/messages/build.gradle.kts b/features/messages/build.gradle.kts index 9fc69683f1..861cb183c4 100644 --- a/features/messages/build.gradle.kts +++ b/features/messages/build.gradle.kts @@ -41,7 +41,6 @@ dependencies { implementation(project(":libraries:designsystem")) implementation(project(":libraries:textcomposer")) implementation(libs.appyx.core) - implementation(libs.mavericks.compose) implementation(libs.coil.compose) implementation(libs.datetime) implementation(libs.accompanist.flowlayout) diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index 791b3c47bc..4c25a25ea3 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -30,7 +30,6 @@ dependencies { implementation(project(":libraries:elementresources")) implementation(project(":libraries:designsystem")) implementation(project(":libraries:architecture")) - implementation(libs.mavericks.compose) implementation(libs.accompanist.pager) implementation(libs.accompanist.pagerindicator) implementation(libs.appyx.core) diff --git a/features/preferences/build.gradle.kts b/features/preferences/build.gradle.kts index e8c4b9abb3..32ad183d13 100644 --- a/features/preferences/build.gradle.kts +++ b/features/preferences/build.gradle.kts @@ -42,7 +42,6 @@ dependencies { implementation(project(":features:logout")) implementation(project(":libraries:designsystem")) implementation(project(":libraries:elementresources")) - implementation(libs.mavericks.compose) implementation(libs.datetime) implementation(libs.accompanist.placeholder) testImplementation(libs.test.junit) diff --git a/features/rageshake/build.gradle.kts b/features/rageshake/build.gradle.kts index 2cd99b6dea..92f9b75aaa 100644 --- a/features/rageshake/build.gradle.kts +++ b/features/rageshake/build.gradle.kts @@ -39,7 +39,6 @@ dependencies { implementation(project(":anvilannotations")) implementation(project(":libraries:designsystem")) implementation(project(":libraries:elementresources")) - implementation(libs.mavericks.compose) implementation(libs.squareup.seismic) implementation(libs.androidx.datastore.preferences) implementation(libs.coil) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9010629926..ea10bd7cfd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,6 @@ constraintlayout = "2.1.4" recyclerview = "1.2.1" lifecycle = "2.5.1" activity_compose = "1.6.1" -fragment = "1.5.5" startup = "1.1.1" # Compose @@ -42,7 +41,6 @@ test_hamcrest = "2.2" test_orchestrator = "1.4.1" #other -mavericks = "3.0.1" coil = "2.2.1" datetime = "0.4.0" wysiwyg = "0.7.0.1" @@ -79,9 +77,7 @@ androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-kt androidx_lifecycle_compose = { module = "androidx.lifecycle:compose", version.ref = "lifecycle" } androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } -androidx_lifecycle_viewmodel_compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity_compose" } -androidx_fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment" } androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" } androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" } @@ -117,7 +113,6 @@ test_hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "test_hamcrest test_orchestrator = { module = "androidx.test:orchestrator", version.ref = "test_orchestrator" } # Others -mavericks_compose = { module = "com.airbnb.android:mavericks-compose", version.ref = "mavericks" } coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } diff --git a/libraries/architecture/build.gradle.kts b/libraries/architecture/build.gradle.kts index 5efea85ebf..37b3fc1af7 100644 --- a/libraries/architecture/build.gradle.kts +++ b/libraries/architecture/build.gradle.kts @@ -14,5 +14,4 @@ dependencies { api(libs.dagger) api(libs.appyx.core) api(libs.androidx.lifecycle.runtime) - api(libs.mavericks.compose) } diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/AssistedViewModelFactory.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/AssistedViewModelFactory.kt deleted file mode 100644 index 9adc3558aa..0000000000 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/AssistedViewModelFactory.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.architecture.viewmodel - -import com.airbnb.mvrx.MavericksState -import com.airbnb.mvrx.MavericksViewModel - -interface AssistedViewModelFactory, S : MavericksState> { - fun create(initialState: S): VM -} diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/DaggerMavericksViewModelFactory.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/DaggerMavericksViewModelFactory.kt deleted file mode 100644 index 5c31a2b22d..0000000000 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/DaggerMavericksViewModelFactory.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.element.android.x.architecture.viewmodel - -import com.airbnb.mvrx.MavericksState -import com.airbnb.mvrx.MavericksViewModel -import com.airbnb.mvrx.MavericksViewModelFactory -import com.airbnb.mvrx.ViewModelContext -import io.element.android.x.architecture.bindings - -/** - * To connect Mavericks ViewModel creation with Anvil's dependency injection, add the following to your MavericksViewModel. - * - * Example: - * - * @ContributesViewModel(YourScope::class) - * class MyViewModel @AssistedInject constructor( - * @Assisted initialState: MyState, - * …, - * ): MavericksViewModel(...) { - * … - * - * companion object : MavericksViewModelFactory by daggerMavericksViewModelFactory() - * } - */ - -inline fun , S : MavericksState> daggerMavericksViewModelFactory() = DaggerMavericksViewModelFactory(VM::class.java) - -/** - * A [MavericksViewModelFactory] makes it easy to create instances of a ViewModel - * using its AssistedInject Factory. This class should be implemented by the companion object - * of every ViewModel which uses AssistedInject via [daggerMavericksViewModelFactory]. - * - * @param VM The ViewModel type - * @param S The ViewState type - * @param viewModelClass The [Class] of the ViewModel being requested for creation - * - * This class accesses the map of ViewModel class to [AssistedViewModelFactory]s from the nearest [DaggerComponentOwner] and - * uses it to retrieve the requested ViewModel's factory class. It then creates an instance of this ViewModel - * using the retrieved factory and returns it. - * @see daggerMavericksViewModelFactory - */ -class DaggerMavericksViewModelFactory, S : MavericksState>( - private val viewModelClass: Class -) : MavericksViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: S): VM { - val bindings: DaggerMavericksBindings = viewModelContext.activity.bindings() - val viewModelFactoryMap = bindings.viewModelFactories() - val viewModelFactory = viewModelFactoryMap[viewModelClass] ?: error("Cannot find ViewModelFactory for ${viewModelClass.name}.") - - @Suppress("UNCHECKED_CAST") - val castedViewModelFactory = viewModelFactory as? AssistedViewModelFactory - val viewModel = castedViewModelFactory?.create(state) - return viewModel as VM - } -} - -interface DaggerMavericksBindings { - fun viewModelFactories(): Map>, AssistedViewModelFactory<*, *>> -} diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelKey.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelKey.kt deleted file mode 100644 index 35b9b487be..0000000000 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelKey.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.architecture.viewmodel - -import com.airbnb.mvrx.MavericksViewModel -import dagger.MapKey -import kotlin.reflect.KClass - -@Retention(AnnotationRetention.RUNTIME) -@Target(AnnotationTarget.FUNCTION) -@MapKey -annotation class ViewModelKey(val value: KClass>) diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelSupport.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelSupport.kt deleted file mode 100644 index 7a7fc576c2..0000000000 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/viewmodel/ViewModelSupport.kt +++ /dev/null @@ -1,121 +0,0 @@ -package io.element.android.x.architecture.viewmodel - -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/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewModel.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewModel.kt deleted file mode 100644 index 6e621ec08c..0000000000 --- a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewModel.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.matrix.ui.viewmodels.user - -import com.airbnb.mvrx.MavericksViewModel -import com.airbnb.mvrx.MavericksViewModelFactory -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory -import io.element.android.x.designsystem.components.avatar.AvatarSize -import io.element.android.x.di.SessionScope -import io.element.android.x.matrix.MatrixClient -import io.element.android.x.matrix.ui.MatrixItemHelper - -@ContributesViewModel(SessionScope::class) -class UserViewModel @AssistedInject constructor( - client: MatrixClient, - @Assisted initialState: UserViewState -) : MavericksViewModel(initialState) { - - companion object : MavericksViewModelFactory by daggerMavericksViewModelFactory() - - private val matrixUserHelper = MatrixItemHelper(client) - - init { - handleInit() - } - - private fun handleInit() { - matrixUserHelper.getCurrentUserData(avatarSize = AvatarSize.SMALL).execute { - copy(user = it) - } - } -} diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewState.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewState.kt deleted file mode 100644 index 60d7c5bd48..0000000000 --- a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewState.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.matrix.ui.viewmodels.user - -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MavericksState -import com.airbnb.mvrx.Uninitialized -import io.element.android.x.matrix.ui.model.MatrixUser - -data class UserViewState( - val user: Async = Uninitialized, -) : MavericksState diff --git a/plugins/src/main/java/extension/DependencyHandleScope.kt b/plugins/src/main/java/extension/DependencyHandleScope.kt index 71d94f6ad0..bdfa1460bf 100644 --- a/plugins/src/main/java/extension/DependencyHandleScope.kt +++ b/plugins/src/main/java/extension/DependencyHandleScope.kt @@ -43,7 +43,6 @@ fun DependencyHandlerScope.composeDependencies() { implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1") implementation("androidx.activity:activity-compose:1.6.1") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1") - implementation("com.airbnb.android:mavericks-compose:3.0.1") debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") implementation("com.airbnb.android:showkase:1.0.0-beta14") From b286b8bf9d7c521d01dac0e30da8b20ace6356b6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 13 Jan 2023 18:36:16 +0100 Subject: [PATCH 25/30] Add copyright on new files --- .../x/anvilannotations/ContributesNode.kt | 16 ++++++++++++++++ .../anvilcodegen/ContributesNodeCodeGenerator.kt | 16 ++++++++++++++++ .../android/x/component/ShowkaseButton.kt | 16 ++++++++++++++++ .../element/android/x/node/LoggedInFlowNode.kt | 16 ++++++++++++++++ .../android/x/node/NotLoggedInFlowNode.kt | 16 ++++++++++++++++ .../io/element/android/x/node/RoomFlowNode.kt | 16 ++++++++++++++++ .../io/element/android/x/node/RootFlowNode.kt | 16 ++++++++++++++++ .../java/io/element/android/x/root/RootEvents.kt | 16 ++++++++++++++++ .../io/element/android/x/root/RootPresenter.kt | 16 ++++++++++++++++ .../java/io/element/android/x/root/RootState.kt | 16 ++++++++++++++++ .../java/io/element/android/x/root/RootView.kt | 16 ++++++++++++++++ .../android/x/features/login/LoginFlowNode.kt | 16 ++++++++++++++++ .../login/changeserver/ChangeServerEvents.kt | 16 ++++++++++++++++ .../login/changeserver/ChangeServerNode.kt | 16 ++++++++++++++++ .../login/changeserver/ChangeServerPresenter.kt | 16 ++++++++++++++++ .../login/changeserver/ChangeServerState.kt | 16 ++++++++++++++++ .../x/features/login/root/LoginRootEvents.kt | 16 ++++++++++++++++ .../x/features/login/root/LoginRootNode.kt | 16 ++++++++++++++++ .../x/features/login/root/LoginRootPresenter.kt | 16 ++++++++++++++++ .../x/features/login/root/LoginRootState.kt | 16 ++++++++++++++++ .../x/features/logout/LogoutPreferenceEvents.kt | 16 ++++++++++++++++ .../features/logout/LogoutPreferencePresenter.kt | 16 ++++++++++++++++ .../x/features/messages/MessagesEvents.kt | 16 ++++++++++++++++ .../android/x/features/messages/MessagesNode.kt | 16 ++++++++++++++++ .../x/features/messages/MessagesPresenter.kt | 16 ++++++++++++++++ .../android/x/features/messages/MessagesState.kt | 16 ++++++++++++++++ .../messages/actionlist/ActionListEvents.kt | 16 ++++++++++++++++ .../messages/actionlist/ActionListPresenter.kt | 16 ++++++++++++++++ .../messages/actionlist/ActionListView.kt | 16 ++++++++++++++++ .../textcomposer/MessageComposerEvents.kt | 16 ++++++++++++++++ .../textcomposer/MessageComposerPresenter.kt | 16 ++++++++++++++++ .../messages/textcomposer/MessageComposerView.kt | 16 ++++++++++++++++ .../features/messages/timeline/TimelineEvents.kt | 16 ++++++++++++++++ .../messages/timeline/TimelinePresenter.kt | 16 ++++++++++++++++ .../x/features/messages/timeline/TimelineView.kt | 16 ++++++++++++++++ .../features/preferences/PreferencesFlowNode.kt | 16 ++++++++++++++++ .../preferences/root/PreferencesRootNode.kt | 16 ++++++++++++++++ .../preferences/root/PreferencesRootPresenter.kt | 16 ++++++++++++++++ .../preferences/root/PreferencesRootState.kt | 16 ++++++++++++++++ .../preferences/root/PreferencesRootView.kt | 16 ++++++++++++++++ .../rageshake/bugreport/BugReportEvents.kt | 16 ++++++++++++++++ .../rageshake/bugreport/BugReportNode.kt | 16 ++++++++++++++++ .../rageshake/bugreport/BugReportPresenter.kt | 16 ++++++++++++++++ .../rageshake/crash/ui/CrashDetectionEvents.kt | 16 ++++++++++++++++ .../crash/ui/CrashDetectionPresenter.kt | 16 ++++++++++++++++ .../detection/RageshakeDetectionEvents.kt | 16 ++++++++++++++++ .../detection/RageshakeDetectionPresenter.kt | 16 ++++++++++++++++ .../preferences/RageshakePreferencesEvents.kt | 16 ++++++++++++++++ .../preferences/RageshakePreferencesPresenter.kt | 16 ++++++++++++++++ .../preferences/RageshakePreferencesState.kt | 16 ++++++++++++++++ .../android/x/features/roomlist/RoomListNode.kt | 16 ++++++++++++++++ .../x/features/roomlist/RoomListPresenter.kt | 16 ++++++++++++++++ .../x/features/roomlist/model/RoomListEvents.kt | 16 ++++++++++++++++ .../x/features/roomlist/model/RoomListState.kt | 16 ++++++++++++++++ libraries/architecture/build.gradle.kts | 16 ++++++++++++++++ .../x/architecture/AssistedNodeFactory.kt | 16 ++++++++++++++++ .../io/element/android/x/architecture/Async.kt | 16 ++++++++++++++++ .../element/android/x/architecture/Bindings.kt | 16 ++++++++++++++++ .../android/x/architecture/NodeFactories.kt | 16 ++++++++++++++++ .../element/android/x/architecture/Presenter.kt | 16 ++++++++++++++++ .../android/x/architecture/PresenterConnector.kt | 16 ++++++++++++++++ .../element/android/x/matrix/core/SessionId.kt | 16 ++++++++++++++++ .../element/android/x/matrix/session/Session.kt | 16 ++++++++++++++++ .../android/x/matrix/ui/di/MatrixUIBindings.kt | 16 ++++++++++++++++ .../x/matrix/ui/media/ImageLoaderFactories.kt | 16 ++++++++++++++++ 65 files changed, 1040 insertions(+) diff --git a/anvilannotations/src/main/java/io/element/android/x/anvilannotations/ContributesNode.kt b/anvilannotations/src/main/java/io/element/android/x/anvilannotations/ContributesNode.kt index 9a858e51a8..1367dc0dc7 100644 --- a/anvilannotations/src/main/java/io/element/android/x/anvilannotations/ContributesNode.kt +++ b/anvilannotations/src/main/java/io/element/android/x/anvilannotations/ContributesNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.anvilannotations import kotlin.reflect.KClass diff --git a/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesNodeCodeGenerator.kt b/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesNodeCodeGenerator.kt index 6a2d0df036..0ca9da53af 100644 --- a/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesNodeCodeGenerator.kt +++ b/anvilcodegen/src/main/java/io/element/android/x/anvilcodegen/ContributesNodeCodeGenerator.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + @file:OptIn(ExperimentalAnvilApi::class) package io.element.android.x.anvilcodegen 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 index a6656d6084..aab733bd84 100644 --- a/app/src/main/java/io/element/android/x/component/ShowkaseButton.kt +++ b/app/src/main/java/io/element/android/x/component/ShowkaseButton.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.component import androidx.compose.foundation.layout.padding 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 index 281d9f5aef..f3b407953a 100644 --- a/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.node import android.os.Parcelable 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 index 4f346bdbdd..3c619c5e38 100644 --- a/app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/NotLoggedInFlowNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.node import android.os.Parcelable diff --git a/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt b/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt index 4c9fd64e68..21ef4ad9cc 100644 --- a/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/RoomFlowNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.node import android.os.Parcelable 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 index 2ef4870b74..40569bd845 100644 --- a/app/src/main/java/io/element/android/x/node/RootFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/RootFlowNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.node import android.os.Parcelable diff --git a/app/src/main/java/io/element/android/x/root/RootEvents.kt b/app/src/main/java/io/element/android/x/root/RootEvents.kt index 174852ad2f..a37e9c3c5b 100644 --- a/app/src/main/java/io/element/android/x/root/RootEvents.kt +++ b/app/src/main/java/io/element/android/x/root/RootEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.root sealed interface RootEvents { diff --git a/app/src/main/java/io/element/android/x/root/RootPresenter.kt b/app/src/main/java/io/element/android/x/root/RootPresenter.kt index 3563fafb4f..2bcf49505a 100644 --- a/app/src/main/java/io/element/android/x/root/RootPresenter.kt +++ b/app/src/main/java/io/element/android/x/root/RootPresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.root import androidx.compose.runtime.Composable diff --git a/app/src/main/java/io/element/android/x/root/RootState.kt b/app/src/main/java/io/element/android/x/root/RootState.kt index cfcbb29499..6a062d2925 100644 --- a/app/src/main/java/io/element/android/x/root/RootState.kt +++ b/app/src/main/java/io/element/android/x/root/RootState.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.root import androidx.compose.runtime.Stable diff --git a/app/src/main/java/io/element/android/x/root/RootView.kt b/app/src/main/java/io/element/android/x/root/RootView.kt index fb0ac5c296..7aa3046719 100644 --- a/app/src/main/java/io/element/android/x/root/RootView.kt +++ b/app/src/main/java/io/element/android/x/root/RootView.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.root import androidx.compose.foundation.layout.Box diff --git a/features/login/src/main/java/io/element/android/x/features/login/LoginFlowNode.kt b/features/login/src/main/java/io/element/android/x/features/login/LoginFlowNode.kt index 5f1fcbfea9..12aa0887f2 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/LoginFlowNode.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/LoginFlowNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.login import android.os.Parcelable diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt index 893459b7cc..f1c3fa78bd 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.login.changeserver sealed interface ChangeServerEvents { diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt index a6dc59a703..2e20380182 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.login.changeserver import androidx.compose.runtime.Composable diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt index 84bb601259..ec6f60f2df 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerPresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.login.changeserver import androidx.compose.runtime.Composable diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt index 1cf98d788f..90dcb8ce64 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerState.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.login.changeserver import io.element.android.x.architecture.Async diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootEvents.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootEvents.kt index 9c7f401060..de72602775 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootEvents.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.login.root sealed interface LoginRootEvents { diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt index a0d809b9ad..aa7698089f 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.login.root import androidx.compose.runtime.Composable diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt index 79ac8a3d95..3c14632de6 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.login.root import androidx.compose.runtime.Composable diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt index 53fc6262f1..4481b3afff 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootState.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.login.root import android.os.Parcelable diff --git a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt index acf280422e..587fc2a9d8 100644 --- a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt +++ b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.logout sealed interface LogoutPreferenceEvents { diff --git a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt index 82a56c6bf8..578de59787 100644 --- a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt +++ b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.logout import androidx.compose.runtime.Composable diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesEvents.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesEvents.kt index 45d4049713..88e8c52221 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesEvents.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.messages import io.element.android.x.features.messages.actionlist.model.TimelineItemAction diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt index d02973f0dd..dd7b435ef0 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.messages import androidx.compose.runtime.Composable diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt index 679109a6e0..8324dc806b 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesPresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.messages import androidx.compose.runtime.Composable diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesState.kt index f9cdcb2b90..af3b0793b5 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesState.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesState.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.messages import androidx.compose.runtime.Immutable diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListEvents.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListEvents.kt index 3cddfb7f90..65b26cf84a 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListEvents.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.messages.actionlist import io.element.android.x.features.messages.timeline.model.TimelineItem diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt index e7e3020c4a..c2c201dc88 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.messages.actionlist import androidx.compose.runtime.Composable diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt index 6f19db9562..de6a40e3f9 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + @file:OptIn(ExperimentalMaterialApi::class) package io.element.android.x.features.messages.actionlist diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerEvents.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerEvents.kt index a558b88e81..8e4e3ffddd 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerEvents.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.messages.textcomposer import io.element.android.x.textcomposer.MessageComposerMode diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt index 65652da85e..e118034113 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.messages.textcomposer import androidx.compose.runtime.Composable diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt index cf417d5598..061518d3c7 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.messages.textcomposer import androidx.compose.runtime.Composable diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineEvents.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineEvents.kt index 5e978a38db..e2354891ea 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineEvents.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.messages.timeline import io.element.android.x.matrix.core.EventId diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt index 6235f6784a..e4204b5c14 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.messages.timeline import androidx.compose.runtime.Composable diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineView.kt index fa938f58dc..677487f5a4 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineView.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.messages.timeline import androidx.compose.foundation.interaction.MutableInteractionSource diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt index 9a69294f3b..6a5397074f 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.preferences import android.os.Parcelable diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt index f8afbd9461..429609e207 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.preferences.root import androidx.compose.runtime.Composable diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt index fd0fe26297..dbc233b805 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.preferences.root import androidx.compose.runtime.Composable diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootState.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootState.kt index e832582b45..9339f00464 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootState.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootState.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.preferences.root import io.element.android.x.architecture.Async diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt index fc0cbd3331..185287b67b 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.preferences.root import androidx.compose.runtime.Composable diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportEvents.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportEvents.kt index 05d150d12c..65183ad09c 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportEvents.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.rageshake.bugreport sealed interface BugReportEvents { diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt index c7bf7114c3..9a944e262e 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.rageshake.bugreport import androidx.compose.runtime.Composable diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt index 16a95ad6f0..c6f2f473e3 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportPresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.rageshake.bugreport import androidx.compose.runtime.Composable diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionEvents.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionEvents.kt index 0017421c7f..93330b4513 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionEvents.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.rageshake.crash.ui sealed interface CrashDetectionEvents { diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt index 0153227c5e..3549bf3cb7 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionPresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.rageshake.crash.ui import androidx.compose.runtime.Composable diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt index e47c8040fa..7bbb9088b8 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.rageshake.detection import io.element.android.x.core.screenshot.ImageResult diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt index 49d6097ed8..62e974703d 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.rageshake.detection import androidx.compose.runtime.Composable diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesEvents.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesEvents.kt index f3152086d1..ae178e9f2f 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesEvents.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.rageshake.preferences sealed interface RageshakePreferencesEvents { diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt index 6a0ed8416a..e435a1a756 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.rageshake.preferences import androidx.compose.runtime.Composable diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt index 216a386d5f..5a71b2a42f 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.rageshake.preferences data class RageshakePreferencesState( diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt index a35acb7345..9e6f11b2b9 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.roomlist import androidx.compose.runtime.Composable diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt index 7727adf348..107543877c 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.roomlist import androidx.compose.runtime.Composable diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt index c89a87280d..8bcb3f3362 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.roomlist.model sealed interface RoomListEvents { diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt index e6480a1677..063f05bc5e 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListState.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.features.roomlist.model import androidx.compose.runtime.Immutable diff --git a/libraries/architecture/build.gradle.kts b/libraries/architecture/build.gradle.kts index 37b3fc1af7..1bae19e00b 100644 --- a/libraries/architecture/build.gradle.kts +++ b/libraries/architecture/build.gradle.kts @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + // TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed @Suppress("DSL_SCOPE_VIOLATION") plugins { diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/AssistedNodeFactory.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/AssistedNodeFactory.kt index 06b8e108c5..eb1d416fa7 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/AssistedNodeFactory.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/AssistedNodeFactory.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.architecture import com.bumble.appyx.core.modality.BuildContext diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt index 1e63d127fb..9428eda509 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.architecture import androidx.compose.runtime.MutableState diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/Bindings.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/Bindings.kt index df6231485a..65badae56f 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/Bindings.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/Bindings.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.architecture import android.content.Context diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/NodeFactories.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/NodeFactories.kt index efccaafb25..c7a82bd481 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/NodeFactories.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/NodeFactories.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.architecture import com.bumble.appyx.core.modality.BuildContext diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt index 6926a5f475..0ffae8ef2c 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.architecture import androidx.compose.runtime.Composable diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt index b183c324e8..61659c7f80 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.architecture import androidx.lifecycle.LifecycleOwner 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 index 355999105e..d75f4c15a7 100644 --- 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 @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.matrix.core import java.io.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 index 6b50a09660..53b69ce975 100644 --- 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 @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.matrix.session import io.element.android.x.matrix.core.SessionId diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/di/MatrixUIBindings.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/di/MatrixUIBindings.kt index 4fd7e1aa35..557d896cb8 100644 --- a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/di/MatrixUIBindings.kt +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/di/MatrixUIBindings.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.matrix.ui.di import com.squareup.anvil.annotations.ContributesTo diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/ImageLoaderFactories.kt index b014d4cc02..584ed28ef3 100644 --- a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/ImageLoaderFactories.kt +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/ImageLoaderFactories.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.element.android.x.matrix.ui.media import android.content.Context From 1919058aac05d988ad2872937c52d0f0733f7663 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 13 Jan 2023 18:37:28 +0100 Subject: [PATCH 26/30] Apply ktlint format --- .../java/io/element/android/x/di/SessionComponent.kt | 2 +- .../element/android/x/initializer/MatrixInitializer.kt | 1 - build.gradle.kts | 1 - .../features/login/changeserver/ChangeServerEvents.kt | 2 +- .../x/features/login/changeserver/ChangeServerView.kt | 1 - .../android/x/features/login/root/LoginRootNode.kt | 1 - .../x/features/login/root/LoginRootPresenter.kt | 4 +--- .../x/features/logout/LogoutPreferenceEvents.kt | 2 +- .../android/x/features/messages/MessagesView.kt | 2 -- .../messages/actionlist/ActionListPresenter.kt | 1 - .../x/features/messages/actionlist/ActionListView.kt | 1 - .../messages/textcomposer/MessageComposerView.kt | 1 - .../x/features/messages/timeline/TimelineEvents.kt | 2 +- .../x/features/messages/timeline/TimelinePresenter.kt | 1 - .../x/features/preferences/root/PreferencesRootView.kt | 1 - .../x/features/rageshake/bugreport/BugReportEvents.kt | 10 +++++----- .../x/features/rageshake/bugreport/BugReportNode.kt | 1 - .../x/features/rageshake/bugreport/BugReportView.kt | 1 - .../rageshake/crash/ui/CrashDetectionScreen.kt | 2 +- .../rageshake/detection/RageshakeDetectionEvents.kt | 2 +- .../rageshake/preferences/RageshakePreferencesView.kt | 5 ++--- .../android/x/features/roomlist/RoomListNode.kt | 1 - .../android/x/features/roomlist/RoomListPresenter.kt | 1 - .../android/x/features/roomlist/RoomListView.kt | 5 ++--- .../x/features/roomlist/model/RoomListEvents.kt | 2 +- .../io/element/android/x/architecture/Presenter.kt | 1 - .../android/x/matrix/ui/media/ImageLoaderFactories.kt | 1 - .../io/element/android/x/matrix/ui/model/MatrixUser.kt | 1 - 28 files changed, 17 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/io/element/android/x/di/SessionComponent.kt b/app/src/main/java/io/element/android/x/di/SessionComponent.kt index 298c97e66c..a9716e57c1 100644 --- a/app/src/main/java/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/java/io/element/android/x/di/SessionComponent.kt @@ -25,7 +25,7 @@ import io.element.android.x.matrix.MatrixClient @SingleIn(SessionScope::class) @MergeSubcomponent(SessionScope::class) -interface SessionComponent: NodeFactoriesBindings, RoomComponent.ParentBindings { +interface SessionComponent : NodeFactoriesBindings, RoomComponent.ParentBindings { fun matrixClient(): MatrixClient 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 f071893d19..bfcdaea612 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 @@ -33,5 +33,4 @@ class MatrixInitializer : Initializer { } override fun dependencies(): List>> = listOf(TimberInitializer::class.java) - } diff --git a/build.gradle.kts b/build.gradle.kts index 5199a030ea..230878d587 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,7 +31,6 @@ plugins { alias(libs.plugins.dependencygraph) } - tasks.register("clean").configure { delete(rootProject.buildDir) } diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt index f1c3fa78bd..78e4e64ffd 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerEvents.kt @@ -18,5 +18,5 @@ package io.element.android.x.features.login.changeserver sealed interface ChangeServerEvents { data class SetServer(val server: String) : ChangeServerEvents - object Submit: ChangeServerEvents + object Submit : ChangeServerEvents } diff --git a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt index e08fb73e52..80dc4f2e0b 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/changeserver/ChangeServerView.kt @@ -59,7 +59,6 @@ import io.element.android.x.designsystem.components.VectorIcon import io.element.android.x.features.login.R import io.element.android.x.features.login.error.changeServerError - @Composable fun ChangeServerView( state: ChangeServerState, diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt index aa7698089f..eb1f1d304b 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootNode.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.Lifecycle -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.plugin.Plugin diff --git a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt index 3c14632de6..ef6b2eab7b 100644 --- a/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt +++ b/features/login/src/main/java/io/element/android/x/features/login/root/LoginRootPresenter.kt @@ -17,7 +17,6 @@ package io.element.android.x.features.login.root import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -26,7 +25,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.x.architecture.Presenter import io.element.android.x.matrix.Matrix import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import javax.inject.Inject @@ -45,7 +43,7 @@ class LoginRootPresenter @Inject constructor(private val matrix: Matrix) : Prese mutableStateOf(LoginFormState.Default) } - fun handleEvents(event: LoginRootEvents){ + fun handleEvents(event: LoginRootEvents) { when (event) { LoginRootEvents.RefreshHomeServer -> refreshHomeServer(homeserver) is LoginRootEvents.SetLogin -> updateFormState(formState) { diff --git a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt index 587fc2a9d8..83aa2fe14f 100644 --- a/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt +++ b/features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt @@ -17,5 +17,5 @@ package io.element.android.x.features.logout sealed interface LogoutPreferenceEvents { - object Logout: LogoutPreferenceEvents + object Logout : LogoutPreferenceEvents } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesView.kt index 36ebf90e35..7711233699 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesView.kt @@ -74,7 +74,6 @@ fun MessagesView( modifier: Modifier = Modifier, onBackPressed: () -> Unit, ) { - LogCompositions(tag = "MessagesScreen", msg = "Root") val itemActionsBottomSheetState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden, @@ -163,7 +162,6 @@ fun MessagesViewContent( .fillMaxWidth() .wrapContentHeight(Alignment.Bottom) ) - } } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt index c2c201dc88..cc894931c6 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListPresenter.kt @@ -34,7 +34,6 @@ class ActionListPresenter @Inject constructor() : Presenter { @Composable override fun present(): ActionListState { - val localCoroutineScope = rememberCoroutineScope() val target: MutableState = remember { diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt index de6a40e3f9..7984577de4 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/actionlist/ActionListView.kt @@ -130,4 +130,3 @@ private fun SheetContent( } } } - diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt index 061518d3c7..b5592ecceb 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerView.kt @@ -25,7 +25,6 @@ fun MessageComposerView( state: MessageComposerState, modifier: Modifier = Modifier, ) { - fun onFullscreenToggle() { state.eventSink(MessageComposerEvents.ToggleFullScreenState) } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineEvents.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineEvents.kt index e2354891ea..733a082d73 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineEvents.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineEvents.kt @@ -20,5 +20,5 @@ import io.element.android.x.matrix.core.EventId sealed interface TimelineEvents { object LoadMore : TimelineEvents - data class SetHighlightedEvent(val eventId: EventId?): TimelineEvents + data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt index e4204b5c14..271c830c28 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt @@ -62,7 +62,6 @@ class TimelinePresenter @Inject constructor( @Composable override fun present(): TimelineState { - val localCoroutineScope = rememberCoroutineScope() val hasMoreToLoad = rememberSaveable { mutableStateOf(timeline.hasMoreToLoad) diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt index 185287b67b..95049803a3 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt @@ -26,7 +26,6 @@ import io.element.android.x.element.resources.R import io.element.android.x.features.logout.LogoutPreferenceState import io.element.android.x.features.logout.LogoutPreferenceView import io.element.android.x.features.preferences.user.UserPreferences -import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState import io.element.android.x.features.rageshake.preferences.RageshakePreferencesView diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportEvents.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportEvents.kt index 65183ad09c..c8bbfe2741 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportEvents.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportEvents.kt @@ -18,10 +18,10 @@ package io.element.android.x.features.rageshake.bugreport sealed interface BugReportEvents { object SendBugReport : BugReportEvents - object ResetAll: BugReportEvents - data class SetDescription(val description: String): BugReportEvents - data class SetSendLog(val sendLog: Boolean): BugReportEvents - data class SetSendCrashLog(val sendCrashlog: Boolean): BugReportEvents - data class SetCanContact(val canContact: Boolean): BugReportEvents + object ResetAll : BugReportEvents + data class SetDescription(val description: String) : BugReportEvents + data class SetSendLog(val sendLog: Boolean) : BugReportEvents + data class SetSendCrashLog(val sendCrashlog: Boolean) : BugReportEvents + data class SetCanContact(val canContact: Boolean) : BugReportEvents data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvents } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt index 9a944e262e..c01f153ddd 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportNode.kt @@ -57,4 +57,3 @@ class BugReportNode @AssistedInject constructor( plugins().forEach { it.onBugReportSent() } } } - diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportView.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportView.kt index 06fa9dd539..394cf69a5b 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportView.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/bugreport/BugReportView.kt @@ -14,7 +14,6 @@ * limitations under the License. */ - package io.element.android.x.features.rageshake.bugreport import androidx.compose.foundation.layout.Box diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionScreen.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionScreen.kt index a0d764a286..7c1e855455 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionScreen.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/crash/ui/CrashDetectionScreen.kt @@ -31,7 +31,7 @@ fun CrashDetectionView( ) { LogCompositions(tag = "Crash", msg = "CrashDetectionScreen") - fun onPopupDismissed(){ + fun onPopupDismissed() { state.eventSink(CrashDetectionEvents.ResetAllCrashData) } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt index 7bbb9088b8..0ba2a26bae 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt @@ -19,7 +19,7 @@ package io.element.android.x.features.rageshake.detection import io.element.android.x.core.screenshot.ImageResult sealed interface RageshakeDetectionEvents { - object Dismiss: RageshakeDetectionEvents + object Dismiss : RageshakeDetectionEvents object Disable : RageshakeDetectionEvents object StartDetection : RageshakeDetectionEvents object StopDetection : RageshakeDetectionEvents diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt index ac7b2478fc..e9142a2c2e 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt @@ -35,12 +35,11 @@ fun RageshakePreferencesView( modifier: Modifier = Modifier, onOpenRageshake: () -> Unit = {}, ) { - - fun onSensitivityChanged(sensitivity: Float){ + fun onSensitivityChanged(sensitivity: Float) { state.eventSink(RageshakePreferencesEvents.SetSensitivity(sensitivity = sensitivity)) } - fun onEnabledChanged(isEnabled: Boolean){ + fun onEnabledChanged(isEnabled: Boolean) { state.eventSink(RageshakePreferencesEvents.SetIsEnabled(isEnabled = isEnabled)) } diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt index 9e6f11b2b9..246545588f 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -29,7 +29,6 @@ import dagger.assisted.AssistedInject import io.element.android.x.anvilannotations.ContributesNode import io.element.android.x.architecture.presenterConnector import io.element.android.x.di.SessionScope -import io.element.android.x.features.roomlist.model.RoomListEvents import io.element.android.x.matrix.core.RoomId @ContributesNode(SessionScope::class) diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt index 107543877c..991239ba59 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt @@ -23,7 +23,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.x.architecture.Presenter diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt index 4eb09e52b2..97d849415c 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListView.kt @@ -57,12 +57,11 @@ fun RoomListView( onRoomClicked: (RoomId) -> Unit = {}, onOpenSettings: () -> Unit = {}, ) { - - fun onFilterChanged(filter: String){ + fun onFilterChanged(filter: String) { state.eventSink(RoomListEvents.UpdateFilter(filter)) } - fun onVisibleRangedChanged(range: IntRange){ + fun onVisibleRangedChanged(range: IntRange) { state.eventSink(RoomListEvents.UpdateVisibleRange(range)) } diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt index 8bcb3f3362..e4cbf4430b 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListEvents.kt @@ -18,5 +18,5 @@ package io.element.android.x.features.roomlist.model sealed interface RoomListEvents { data class UpdateFilter(val newFilter: String) : RoomListEvents - data class UpdateVisibleRange(val range: IntRange): RoomListEvents + data class UpdateVisibleRange(val range: IntRange) : RoomListEvents } diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt index 0ffae8ef2c..6d5e7de444 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/Presenter.kt @@ -17,7 +17,6 @@ package io.element.android.x.architecture import androidx.compose.runtime.Composable -import kotlinx.coroutines.flow.Flow interface Presenter { @Composable diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/ImageLoaderFactories.kt index 584ed28ef3..eecb25f9f2 100644 --- a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/ImageLoaderFactories.kt +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/ImageLoaderFactories.kt @@ -47,4 +47,3 @@ class NotLoggedInImageLoaderFactory @Inject constructor( .build() } } - diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt index 3dfebaf16b..835ea35a7a 100644 --- a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt @@ -17,7 +17,6 @@ package io.element.android.x.matrix.ui.model import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.matrix.core.UserId From 12e9402474747c41b6013d32ce4127257f6b6adb Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 16 Jan 2023 17:15:16 +0100 Subject: [PATCH 27/30] Fix CI --- .../java/io/element/android/x/component/ShowkaseButton.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index aab733bd84..bd8906c2a7 100644 --- a/app/src/main/java/io/element/android/x/component/ShowkaseButton.kt +++ b/app/src/main/java/io/element/android/x/component/ShowkaseButton.kt @@ -30,14 +30,14 @@ import androidx.compose.ui.unit.dp @Composable internal fun ShowkaseButton( - modifier: Modifier = Modifier, isVisible: Boolean, onClick: () -> Unit, - onCloseClicked: () -> Unit + onCloseClicked: () -> Unit, + modifier: Modifier = Modifier, ) { if (isVisible) { Button( - modifier = Modifier + modifier = modifier .padding(top = 32.dp, start = 16.dp), onClick = onClick ) { From b622ad60007bcb3a7ec03f2f89195b7ec1e3211f Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 16 Jan 2023 18:11:54 +0100 Subject: [PATCH 28/30] Add some interfaces for matrix module --- .../io/element/android/x/di/RoomComponent.kt | 2 - .../element/android/x/di/SessionComponent.kt | 2 - .../messages/timeline/TimelineItemsFactory.kt | 2 +- .../messages/timeline/TimelinePresenter.kt | 10 +- .../io/element/android/x/matrix/Matrix.kt | 2 +- .../element/android/x/matrix/MatrixClient.kt | 185 ++-------------- .../android/x/matrix/RustMatrixClient.kt | 202 ++++++++++++++++++ .../android/x/matrix/room/MatrixRoom.kt | 115 ++-------- .../android/x/matrix/room/RustMatrixRoom.kt | 136 ++++++++++++ .../x/matrix/timeline/MatrixTimeline.kt | 143 ++----------- .../x/matrix/timeline/RustMatrixTimeline.kt | 160 ++++++++++++++ 11 files changed, 545 insertions(+), 414 deletions(-) create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/RustMatrixClient.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/room/RustMatrixRoom.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/RustMatrixTimeline.kt diff --git a/app/src/main/java/io/element/android/x/di/RoomComponent.kt b/app/src/main/java/io/element/android/x/di/RoomComponent.kt index d5964325ae..cfd7eee471 100644 --- a/app/src/main/java/io/element/android/x/di/RoomComponent.kt +++ b/app/src/main/java/io/element/android/x/di/RoomComponent.kt @@ -27,8 +27,6 @@ import io.element.android.x.matrix.room.MatrixRoom @MergeSubcomponent(RoomScope::class) interface RoomComponent : NodeFactoriesBindings { - fun matrixRoom(): MatrixRoom - @Subcomponent.Builder interface Builder { @BindsInstance diff --git a/app/src/main/java/io/element/android/x/di/SessionComponent.kt b/app/src/main/java/io/element/android/x/di/SessionComponent.kt index a9716e57c1..8da31df8eb 100644 --- a/app/src/main/java/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/java/io/element/android/x/di/SessionComponent.kt @@ -27,8 +27,6 @@ import io.element.android.x.matrix.MatrixClient @MergeSubcomponent(SessionScope::class) interface SessionComponent : NodeFactoriesBindings, RoomComponent.ParentBindings { - fun matrixClient(): MatrixClient - @Subcomponent.Builder interface Builder { @BindsInstance diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineItemsFactory.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineItemsFactory.kt index ed9594aa8c..0b8a3bfc48 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineItemsFactory.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineItemsFactory.kt @@ -22,8 +22,8 @@ import io.element.android.x.features.messages.timeline.diff.CacheInvalidator import io.element.android.x.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback import io.element.android.x.features.messages.timeline.model.AggregatedReaction import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition -import io.element.android.x.features.messages.timeline.model.TimelineItemReactions import io.element.android.x.features.messages.timeline.model.TimelineItem +import io.element.android.x.features.messages.timeline.model.TimelineItemReactions import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemEmoteContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt index 271c830c28..28db417053 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.x.architecture.Presenter +import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.core.EventId import io.element.android.x.matrix.room.MatrixRoom @@ -33,7 +34,6 @@ import io.element.android.x.matrix.timeline.MatrixTimelineItem import io.element.android.x.matrix.ui.MatrixItemHelper import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -42,15 +42,15 @@ import javax.inject.Inject private const val PAGINATION_COUNT = 50 class TimelinePresenter @Inject constructor( - private val appCoroutineScope: CoroutineScope, - private val client: MatrixClient, - private val room: MatrixRoom + coroutineDispatchers: CoroutineDispatchers, + client: MatrixClient, + room: MatrixRoom, ) : Presenter { private val timeline = room.timeline() private val matrixItemHelper = MatrixItemHelper(client) private val timelineItemsFactory = - TimelineItemsFactory(matrixItemHelper, room, Dispatchers.Default) + TimelineItemsFactory(matrixItemHelper, room, coroutineDispatchers.computation) private class TimelineCallback(private val coroutineScope: CoroutineScope, private val timelineItemsFactory: TimelineItemsFactory) : MatrixTimeline.Callback { override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) { 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 3c20fd677f..71c649e13e 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 @@ -97,7 +97,7 @@ class Matrix @Inject constructor( } private fun createMatrixClient(client: Client): MatrixClient { - return MatrixClient( + return RustMatrixClient( client = client, sessionStore = sessionStore, coroutineScope = coroutineScope, 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 48bb8816d0..d0d446909b 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,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,187 +16,30 @@ package io.element.android.x.matrix -import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.matrix.core.RoomId 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 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 kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.Client -import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.MediaSource -import org.matrix.rustcomponents.sdk.RequiredState -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, - private val sessionStore: SessionStore, - private val coroutineScope: CoroutineScope, - private val dispatchers: CoroutineDispatchers, - private val baseDirectory: File, -) : Closeable { - - val sessionId: SessionId = client.session().sessionId() - - private val clientDelegate = object : ClientDelegate { - override fun didReceiveAuthError(isSoftLogout: Boolean) { - Timber.v("didReceiveAuthError()") - } - - override fun didReceiveSyncUpdate() { - Timber.v("didReceiveSyncUpdate()") - } - - override fun didUpdateRestoreToken() { - Timber.v("didUpdateRestoreToken()") - } - } - - private val slidingSyncView = SlidingSyncViewBuilder() - .timelineLimit(limit = 10u) - .requiredState( - requiredState = listOf( - RequiredState(key = "m.room.avatar", value = ""), - RequiredState(key = "m.room.encryption", value = ""), - ) - ) - .name(name = "HomeScreenView") - .syncMode(mode = SlidingSyncMode.SELECTIVE) - .addRange(0u, 30u) - .build() - - private val slidingSync = client - .slidingSync() - .homeserver("https://slidingsync.lab.element.dev") - .withCommonExtensions() - // .coldCache("ElementX") - .addView(slidingSyncView) - .build() - - private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope, dispatchers) - private val roomSummaryDataSource: RustRoomSummaryDataSource = - RustRoomSummaryDataSource( - slidingSyncObserverProxy.updateSummaryFlow, - slidingSync, - slidingSyncView, - dispatchers, - ::onRestartSync - ) - private var slidingSyncObserverToken: StoppableSpawn? = null - - private val mediaResolver = RustMediaResolver(this) - private val isSyncing = AtomicBoolean(false) - - init { - client.setDelegate(clientDelegate) - } - - private fun onRestartSync() { - slidingSyncObserverToken = slidingSync.sync() - } - - fun getRoom(roomId: RoomId): MatrixRoom? { - val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null - val room = slidingSyncRoom.fullRoom() ?: return null - return MatrixRoom( - slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow, - slidingSyncRoom = slidingSyncRoom, - room = room, - coroutineScope = coroutineScope, - coroutineDispatchers = dispatchers - ) - } - - fun startSync() { - if (isSyncing.compareAndSet(false, true)) { - roomSummaryDataSource.startSync() - slidingSync.setObserver(slidingSyncObserverProxy) - slidingSyncObserverToken = slidingSync.sync() - } - } - - fun stopSync() { - if (isSyncing.compareAndSet(true, false)) { - roomSummaryDataSource.stopSync() - slidingSync.setObserver(null) - slidingSyncObserverToken?.cancel() - } - } - - fun roomSummaryDataSource(): RoomSummaryDataSource = roomSummaryDataSource - - fun mediaResolver(): MediaResolver = mediaResolver - - override fun close() { - stopSync() - roomSummaryDataSource.close() - client.setDelegate(null) - } - - suspend fun logout() = withContext(dispatchers.io) { - close() - try { - client.logout() - } catch (failure: Throwable) { - Timber.e(failure, "Fail to call logout on HS. Still delete local files.") - } - baseDirectory.deleteSessionDirectory(userID = client.userId()) - sessionStore.reset() - } - - fun userId(): UserId = UserId(client.userId()) - - suspend fun loadUserDisplayName(): Result = withContext(dispatchers.io) { - runCatching { - client.displayName() - } - } - - suspend fun loadUserAvatarURLString(): Result = withContext(dispatchers.io) { - runCatching { - client.avatarUrl() - } - } - - @OptIn(ExperimentalUnsignedTypes::class) - suspend fun loadMediaContentForSource(source: MediaSource): Result = - withContext(dispatchers.io) { - runCatching { - client.getMediaContent(source).toUByteArray().toByteArray() - } - } - - @OptIn(ExperimentalUnsignedTypes::class) +interface MatrixClient : Closeable { + val sessionId: SessionId + fun getRoom(roomId: RoomId): MatrixRoom? + fun startSync() + fun stopSync() + fun roomSummaryDataSource(): RoomSummaryDataSource + fun mediaResolver(): MediaResolver + suspend fun logout() + fun userId(): UserId + suspend fun loadUserDisplayName(): Result + suspend fun loadUserAvatarURLString(): Result + suspend fun loadMediaContentForSource(source: MediaSource): Result suspend fun loadMediaThumbnailForSource( source: MediaSource, width: Long, height: Long - ): Result = - withContext(dispatchers.io) { - runCatching { - client.getMediaThumbnail(source, width.toULong(), height.toULong()).toUByteArray() - .toByteArray() - } - } - - private fun File.deleteSessionDirectory(userID: String): Boolean { - // Rust sanitises the user ID replacing invalid characters with an _ - val sanitisedUserID = userID.replace(":", "_") - val sessionDirectory = File(this, sanitisedUserID) - return sessionDirectory.deleteRecursively() - } + ): Result } diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/RustMatrixClient.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/RustMatrixClient.kt new file mode 100644 index 0000000000..433b23df04 --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/RustMatrixClient.kt @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.matrix + +import io.element.android.x.core.coroutine.CoroutineDispatchers +import io.element.android.x.matrix.core.RoomId +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 +import io.element.android.x.matrix.room.MatrixRoom +import io.element.android.x.matrix.room.RoomSummaryDataSource +import io.element.android.x.matrix.room.RustMatrixRoom +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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.ClientDelegate +import org.matrix.rustcomponents.sdk.MediaSource +import org.matrix.rustcomponents.sdk.RequiredState +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.File +import java.util.concurrent.atomic.AtomicBoolean + +internal class RustMatrixClient internal constructor( + private val client: Client, + private val sessionStore: SessionStore, + private val coroutineScope: CoroutineScope, + private val dispatchers: CoroutineDispatchers, + private val baseDirectory: File, +) : MatrixClient { + + override val sessionId: SessionId = client.session().sessionId() + + private val clientDelegate = object : ClientDelegate { + override fun didReceiveAuthError(isSoftLogout: Boolean) { + Timber.v("didReceiveAuthError()") + } + + override fun didReceiveSyncUpdate() { + Timber.v("didReceiveSyncUpdate()") + } + + override fun didUpdateRestoreToken() { + Timber.v("didUpdateRestoreToken()") + } + } + + private val slidingSyncView = SlidingSyncViewBuilder() + .timelineLimit(limit = 10u) + .requiredState( + requiredState = listOf( + RequiredState(key = "m.room.avatar", value = ""), + RequiredState(key = "m.room.encryption", value = ""), + ) + ) + .name(name = "HomeScreenView") + .syncMode(mode = SlidingSyncMode.SELECTIVE) + .addRange(0u, 30u) + .build() + + private val slidingSync = client + .slidingSync() + .homeserver("https://slidingsync.lab.element.dev") + .withCommonExtensions() + // .coldCache("ElementX") + .addView(slidingSyncView) + .build() + + private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope, dispatchers) + private val roomSummaryDataSource: RustRoomSummaryDataSource = + RustRoomSummaryDataSource( + slidingSyncObserverProxy.updateSummaryFlow, + slidingSync, + slidingSyncView, + dispatchers, + ::onRestartSync + ) + private var slidingSyncObserverToken: StoppableSpawn? = null + + private val mediaResolver = RustMediaResolver(this) + private val isSyncing = AtomicBoolean(false) + + init { + client.setDelegate(clientDelegate) + } + + private fun onRestartSync() { + slidingSyncObserverToken = slidingSync.sync() + } + + override fun getRoom(roomId: RoomId): MatrixRoom? { + val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null + val room = slidingSyncRoom.fullRoom() ?: return null + return RustMatrixRoom( + slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow, + slidingSyncRoom = slidingSyncRoom, + room = room, + coroutineScope = coroutineScope, + coroutineDispatchers = dispatchers + ) + } + + override fun startSync() { + if (isSyncing.compareAndSet(false, true)) { + roomSummaryDataSource.startSync() + slidingSync.setObserver(slidingSyncObserverProxy) + slidingSyncObserverToken = slidingSync.sync() + } + } + + override fun stopSync() { + if (isSyncing.compareAndSet(true, false)) { + roomSummaryDataSource.stopSync() + slidingSync.setObserver(null) + slidingSyncObserverToken?.cancel() + } + } + + override fun roomSummaryDataSource(): RoomSummaryDataSource = roomSummaryDataSource + + override fun mediaResolver(): MediaResolver = mediaResolver + + override fun close() { + stopSync() + roomSummaryDataSource.close() + client.setDelegate(null) + } + + override suspend fun logout() = withContext(dispatchers.io) { + close() + try { + client.logout() + } catch (failure: Throwable) { + Timber.e(failure, "Fail to call logout on HS. Still delete local files.") + } + baseDirectory.deleteSessionDirectory(userID = client.userId()) + sessionStore.reset() + } + + override fun userId(): UserId = UserId(client.userId()) + + override suspend fun loadUserDisplayName(): Result = withContext(dispatchers.io) { + runCatching { + client.displayName() + } + } + + override suspend fun loadUserAvatarURLString(): Result = withContext(dispatchers.io) { + runCatching { + client.avatarUrl() + } + } + + @OptIn(ExperimentalUnsignedTypes::class) + override suspend fun loadMediaContentForSource(source: MediaSource): Result = + withContext(dispatchers.io) { + runCatching { + client.getMediaContent(source).toUByteArray().toByteArray() + } + } + + @OptIn(ExperimentalUnsignedTypes::class) + override suspend fun loadMediaThumbnailForSource( + source: MediaSource, + width: Long, + height: Long + ): Result = + withContext(dispatchers.io) { + runCatching { + client.getMediaThumbnail(source, width.toULong(), height.toULong()).toUByteArray() + .toByteArray() + } + } + + private fun File.deleteSessionDirectory(userID: String): Boolean { + // Rust sanitises the user ID replacing invalid characters with an _ + val sanitisedUserID = userID.replace(":", "_") + val sessionDirectory = File(this, sanitisedUserID) + return sessionDirectory.deleteRecursively() + } +} diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt index 681b8839ee..83238edc77 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,120 +16,33 @@ package io.element.android.x.matrix.room -import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.matrix.core.EventId import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.timeline.MatrixTimeline -import kotlinx.coroutines.CoroutineScope +import io.element.android.x.matrix.timeline.RustMatrixTimeline import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.Room -import org.matrix.rustcomponents.sdk.SlidingSyncRoom -import org.matrix.rustcomponents.sdk.UpdateSummary -import org.matrix.rustcomponents.sdk.genTransactionId -import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown - -class MatrixRoom( - private val slidingSyncUpdateFlow: Flow, - private val slidingSyncRoom: SlidingSyncRoom, - private val room: Room, - private val coroutineScope: CoroutineScope, - private val coroutineDispatchers: CoroutineDispatchers, -) { - - fun syncUpdateFlow(): Flow { - return slidingSyncUpdateFlow - .filter { - it.rooms.contains(room.id()) - } - .map { - System.currentTimeMillis() - } - .onStart { emit(System.currentTimeMillis()) } - } - - fun timeline(): MatrixTimeline { - return MatrixTimeline( - matrixRoom = this, - room = room, - slidingSyncRoom = slidingSyncRoom, - coroutineScope = coroutineScope, - coroutineDispatchers = coroutineDispatchers - ) - } - - val roomId = RoomId(room.id()) +interface MatrixRoom { + val roomId: RoomId val name: String? - get() { - return slidingSyncRoom.name() - } - val bestName: String - get() { - return name?.takeIf { it.isNotEmpty() } ?: room.id() - } - val displayName: String - get() { - return room.displayName() - } - val topic: String? - get() { - return room.topic() - } - val avatarUrl: String? - get() { - return room.avatarUrl() - } - suspend fun userDisplayName(userId: String): Result = - withContext(coroutineDispatchers.io) { - runCatching { - room.memberDisplayName(userId) - } - } + fun syncUpdateFlow(): Flow - suspend fun userAvatarUrl(userId: String): Result = - withContext(coroutineDispatchers.io) { - runCatching { - room.memberAvatarUrl(userId) - } - } + fun timeline(): MatrixTimeline - suspend fun sendMessage(message: String): Result = withContext(coroutineDispatchers.io) { - val transactionId = genTransactionId() - val content = messageEventContentFromMarkdown(message) - runCatching { - room.send(content, transactionId) - } - } + suspend fun userDisplayName(userId: String): Result - suspend fun editMessage(originalEventId: EventId, message: String): Result = withContext(coroutineDispatchers.io) { - val transactionId = genTransactionId() - // val content = messageEventContentFromMarkdown(message) - runCatching { - room.edit(/* TODO use content */ message, originalEventId.value, transactionId) - } - } + suspend fun userAvatarUrl(userId: String): Result - suspend fun replyMessage(eventId: EventId, message: String): Result = withContext(coroutineDispatchers.io) { - val transactionId = genTransactionId() - // val content = messageEventContentFromMarkdown(message) - runCatching { - room.sendReply(/* TODO use content */ message, eventId.value, transactionId) - } - } + suspend fun sendMessage(message: String): Result - suspend fun redactEvent(eventId: EventId, reason: String? = null) = withContext(coroutineDispatchers.io) { - val transactionId = genTransactionId() - runCatching { - room.redact(eventId.value, reason, transactionId) - } - } + suspend fun editMessage(originalEventId: EventId, message: String): Result + + suspend fun replyMessage(eventId: EventId, message: String): Result + + suspend fun redactEvent(eventId: EventId, reason: String? = null): Result } diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RustMatrixRoom.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RustMatrixRoom.kt new file mode 100644 index 0000000000..7d7c2414fb --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RustMatrixRoom.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.matrix.room + +import io.element.android.x.core.coroutine.CoroutineDispatchers +import io.element.android.x.matrix.core.EventId +import io.element.android.x.matrix.core.RoomId +import io.element.android.x.matrix.timeline.MatrixTimeline +import io.element.android.x.matrix.timeline.RustMatrixTimeline +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.SlidingSyncRoom +import org.matrix.rustcomponents.sdk.UpdateSummary +import org.matrix.rustcomponents.sdk.genTransactionId +import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown + +class RustMatrixRoom( + private val slidingSyncUpdateFlow: Flow, + private val slidingSyncRoom: SlidingSyncRoom, + private val room: Room, + private val coroutineScope: CoroutineScope, + private val coroutineDispatchers: CoroutineDispatchers, +) : MatrixRoom { + + override fun syncUpdateFlow(): Flow { + return slidingSyncUpdateFlow + .filter { + it.rooms.contains(room.id()) + } + .map { + System.currentTimeMillis() + } + .onStart { emit(System.currentTimeMillis()) } + } + + override fun timeline(): MatrixTimeline { + return RustMatrixTimeline( + matrixRoom = this, + room = room, + slidingSyncRoom = slidingSyncRoom, + coroutineScope = coroutineScope, + coroutineDispatchers = coroutineDispatchers + ) + } + + override val roomId = RoomId(room.id()) + + override val name: String? + get() { + return slidingSyncRoom.name() + } + + override val bestName: String + get() { + return name?.takeIf { it.isNotEmpty() } ?: room.id() + } + + override val displayName: String + get() { + return room.displayName() + } + + override val topic: String? + get() { + return room.topic() + } + + override val avatarUrl: String? + get() { + return room.avatarUrl() + } + + override suspend fun userDisplayName(userId: String): Result = + withContext(coroutineDispatchers.io) { + runCatching { + room.memberDisplayName(userId) + } + } + + override suspend fun userAvatarUrl(userId: String): Result = + withContext(coroutineDispatchers.io) { + runCatching { + room.memberAvatarUrl(userId) + } + } + + override suspend fun sendMessage(message: String): Result = withContext(coroutineDispatchers.io) { + val transactionId = genTransactionId() + val content = messageEventContentFromMarkdown(message) + runCatching { + room.send(content, transactionId) + } + } + + override suspend fun editMessage(originalEventId: EventId, message: String): Result = withContext(coroutineDispatchers.io) { + val transactionId = genTransactionId() + // val content = messageEventContentFromMarkdown(message) + runCatching { + room.edit(/* TODO use content */ message, originalEventId.value, transactionId) + } + } + + override suspend fun replyMessage(eventId: EventId, message: String): Result = withContext(coroutineDispatchers.io) { + val transactionId = genTransactionId() + // val content = messageEventContentFromMarkdown(message) + runCatching { + room.sendReply(/* TODO use content */ message, eventId.value, transactionId) + } + } + + override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(coroutineDispatchers.io) { + val transactionId = genTransactionId() + runCatching { + room.redact(eventId.value, reason, transactionId) + } + } +} diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt index bf613ca761..2f383cebf2 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,150 +16,31 @@ package io.element.android.x.matrix.timeline -import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.matrix.core.EventId -import io.element.android.x.matrix.room.MatrixRoom -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.sample -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.PaginationOutcome -import org.matrix.rustcomponents.sdk.Room -import org.matrix.rustcomponents.sdk.SlidingSyncRoom -import org.matrix.rustcomponents.sdk.TimelineChange -import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineListener -import timber.log.Timber -import java.util.Collections -class MatrixTimeline( - private val matrixRoom: MatrixRoom, - private val room: Room, - private val slidingSyncRoom: SlidingSyncRoom, - private val coroutineScope: CoroutineScope, - private val coroutineDispatchers: CoroutineDispatchers, -) : TimelineListener { +interface MatrixTimeline { + var callback: Callback? + val hasMoreToLoad: Boolean interface Callback { fun onUpdatedTimelineItem(timelineItem: MatrixTimelineItem) = Unit fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) = Unit } - var callback: Callback? = null - - private val paginationOutcome = MutableStateFlow(PaginationOutcome(true)) - private val timelineItems: MutableStateFlow> = - MutableStateFlow(emptyList()) - - @OptIn(FlowPreview::class) - fun timelineItems(): Flow> { - return timelineItems.sample(50) - } - - val hasMoreToLoad: Boolean - get() { - return paginationOutcome.value.moreMessages - } - - private fun MutableList.applyDiff(diff: TimelineDiff) { - when (diff.change()) { - TimelineChange.PUSH -> { - Timber.v("Apply push on list with size: $size") - val item = diff.push()?.asMatrixTimelineItem() ?: return - callback?.onPushedTimelineItem(item) - add(item) - } - TimelineChange.UPDATE_AT -> { - val updateAtData = diff.updateAt() ?: return - Timber.v("Apply $updateAtData on list with size: $size") - val item = updateAtData.item.asMatrixTimelineItem() - callback?.onUpdatedTimelineItem(item) - set(updateAtData.index.toInt(), item) - } - TimelineChange.INSERT_AT -> { - val insertAtData = diff.insertAt() ?: return - Timber.v("Apply $insertAtData on list with size: $size") - val item = insertAtData.item.asMatrixTimelineItem() - add(insertAtData.index.toInt(), item) - } - TimelineChange.MOVE -> { - val moveData = diff.move() ?: return - Timber.v("Apply $moveData on list with size: $size") - Collections.swap(this, moveData.oldIndex.toInt(), moveData.newIndex.toInt()) - } - TimelineChange.REMOVE_AT -> { - val removeAtData = diff.removeAt() ?: return - Timber.v("Apply $removeAtData on list with size: $size") - removeAt(removeAtData.toInt()) - } - TimelineChange.REPLACE -> { - Timber.v("Apply REPLACE on list with size: $size") - clear() - val items = diff.replace()?.map { it.asMatrixTimelineItem() } ?: return - addAll(items) - } - TimelineChange.POP -> { - Timber.v("Apply POP on list with size: $size") - removeLast() - } - TimelineChange.CLEAR -> { - Timber.v("Apply CLEAR on list with size: $size") - clear() - } - } - } - - suspend fun paginateBackwards(count: Int): Result = withContext(coroutineDispatchers.io) { - if (!paginationOutcome.value.moreMessages) { - return@withContext Result.failure(IllegalStateException("no more message")) - } - runCatching { - paginationOutcome.value = room.paginateBackwards(count.toUShort()) - } - } - - private suspend fun updateTimelineItems(block: MutableList.() -> Unit) = - withContext(coroutineDispatchers.diffUpdateDispatcher) { - val mutableTimelineItems = timelineItems.value.toMutableList() - block(mutableTimelineItems) - timelineItems.value = mutableTimelineItems - } - - fun addListener(timelineListener: TimelineListener) { - slidingSyncRoom.addTimelineListener(timelineListener) - } - - fun initialize() { - addListener(this) - } - - fun dispose() { - slidingSyncRoom.removeTimeline() - } + fun timelineItems(): Flow> + suspend fun paginateBackwards(count: Int): Result + fun addListener(timelineListener: TimelineListener) + fun initialize() + fun dispose() /** * @param message markdown message */ - suspend fun sendMessage(message: String): Result { - return matrixRoom.sendMessage(message) - } + suspend fun sendMessage(message: String): Result - suspend fun editMessage(originalEventId: EventId, message: String): Result { - return matrixRoom.editMessage(originalEventId, message = message) - } + suspend fun editMessage(originalEventId: EventId, message: String): Result - suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result { - return matrixRoom.replyMessage(inReplyToEventId, message) - } - - override fun onUpdate(update: TimelineDiff) { - coroutineScope.launch { - updateTimelineItems { - applyDiff(update) - } - } - } + suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result } diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/RustMatrixTimeline.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/RustMatrixTimeline.kt new file mode 100644 index 0000000000..ecfce4af7a --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/RustMatrixTimeline.kt @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.matrix.timeline + +import io.element.android.x.core.coroutine.CoroutineDispatchers +import io.element.android.x.matrix.core.EventId +import io.element.android.x.matrix.room.RustMatrixRoom +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.PaginationOutcome +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.SlidingSyncRoom +import org.matrix.rustcomponents.sdk.TimelineChange +import org.matrix.rustcomponents.sdk.TimelineDiff +import org.matrix.rustcomponents.sdk.TimelineListener +import timber.log.Timber +import java.util.Collections + +class RustMatrixTimeline( + private val matrixRoom: RustMatrixRoom, + private val room: Room, + private val slidingSyncRoom: SlidingSyncRoom, + private val coroutineScope: CoroutineScope, + private val coroutineDispatchers: CoroutineDispatchers, +) : TimelineListener, MatrixTimeline { + + override var callback: MatrixTimeline.Callback? = null + + private val paginationOutcome = MutableStateFlow(PaginationOutcome(true)) + private val timelineItems: MutableStateFlow> = + MutableStateFlow(emptyList()) + + @OptIn(FlowPreview::class) + override fun timelineItems(): Flow> { + return timelineItems.sample(50) + } + + override val hasMoreToLoad: Boolean + get() { + return paginationOutcome.value.moreMessages + } + + private fun MutableList.applyDiff(diff: TimelineDiff) { + when (diff.change()) { + TimelineChange.PUSH -> { + Timber.v("Apply push on list with size: $size") + val item = diff.push()?.asMatrixTimelineItem() ?: return + callback?.onPushedTimelineItem(item) + add(item) + } + TimelineChange.UPDATE_AT -> { + val updateAtData = diff.updateAt() ?: return + Timber.v("Apply $updateAtData on list with size: $size") + val item = updateAtData.item.asMatrixTimelineItem() + callback?.onUpdatedTimelineItem(item) + set(updateAtData.index.toInt(), item) + } + TimelineChange.INSERT_AT -> { + val insertAtData = diff.insertAt() ?: return + Timber.v("Apply $insertAtData on list with size: $size") + val item = insertAtData.item.asMatrixTimelineItem() + add(insertAtData.index.toInt(), item) + } + TimelineChange.MOVE -> { + val moveData = diff.move() ?: return + Timber.v("Apply $moveData on list with size: $size") + Collections.swap(this, moveData.oldIndex.toInt(), moveData.newIndex.toInt()) + } + TimelineChange.REMOVE_AT -> { + val removeAtData = diff.removeAt() ?: return + Timber.v("Apply $removeAtData on list with size: $size") + removeAt(removeAtData.toInt()) + } + TimelineChange.REPLACE -> { + Timber.v("Apply REPLACE on list with size: $size") + clear() + val items = diff.replace()?.map { it.asMatrixTimelineItem() } ?: return + addAll(items) + } + TimelineChange.POP -> { + Timber.v("Apply POP on list with size: $size") + removeLast() + } + TimelineChange.CLEAR -> { + Timber.v("Apply CLEAR on list with size: $size") + clear() + } + } + } + + override suspend fun paginateBackwards(count: Int): Result = withContext(coroutineDispatchers.io) { + if (!paginationOutcome.value.moreMessages) { + return@withContext Result.failure(IllegalStateException("no more message")) + } + runCatching { + paginationOutcome.value = room.paginateBackwards(count.toUShort()) + } + } + + private suspend fun updateTimelineItems(block: MutableList.() -> Unit) = + withContext(coroutineDispatchers.diffUpdateDispatcher) { + val mutableTimelineItems = timelineItems.value.toMutableList() + block(mutableTimelineItems) + timelineItems.value = mutableTimelineItems + } + + override fun addListener(timelineListener: TimelineListener) { + slidingSyncRoom.addTimelineListener(timelineListener) + } + + override fun initialize() { + addListener(this) + } + + override fun dispose() { + slidingSyncRoom.removeTimeline() + } + + /** + * @param message markdown message + */ + override suspend fun sendMessage(message: String): Result { + return matrixRoom.sendMessage(message) + } + + override suspend fun editMessage(originalEventId: EventId, message: String): Result { + return matrixRoom.editMessage(originalEventId, message = message) + } + + override suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result { + return matrixRoom.replyMessage(inReplyToEventId, message) + } + + override fun onUpdate(update: TimelineDiff) { + coroutineScope.launch { + updateTimelineItems { + applyDiff(update) + } + } + } +} From 0fc8ed9b330ae1c061183610a544fbf11a236fa5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 16 Jan 2023 18:17:56 +0100 Subject: [PATCH 29/30] Fix detekt --- .../textcomposer/MessageComposerPresenter.kt | 35 ++++++++++--------- .../messages/timeline/TimelinePresenter.kt | 5 ++- .../android/x/architecture/Bindings.kt | 2 -- .../element/android/x/architecture/NodeKey.kt | 4 +-- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt index e118034113..aba47ec90b 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/textcomposer/MessageComposerPresenter.kt @@ -76,22 +76,23 @@ class MessageComposerPresenter @Inject constructor( value = MessageComposerMode.Normal("") } - private fun CoroutineScope.sendMessage(text: String, composerMode: MutableState, textState: MutableState) = launch { - val capturedMode = composerMode.value - // Reset composer right away - textState.value = "".toStableCharSequence() - composerMode.setToNormal() - when (capturedMode) { - is MessageComposerMode.Normal -> room.sendMessage(text) - is MessageComposerMode.Edit -> room.editMessage( - capturedMode.eventId, - text - ) - is MessageComposerMode.Quote -> TODO() - is MessageComposerMode.Reply -> room.replyMessage( - capturedMode.eventId, - text - ) + private fun CoroutineScope.sendMessage(text: String, composerMode: MutableState, textState: MutableState) = + launch { + val capturedMode = composerMode.value + // Reset composer right away + textState.value = "".toStableCharSequence() + composerMode.setToNormal() + when (capturedMode) { + is MessageComposerMode.Normal -> room.sendMessage(text) + is MessageComposerMode.Edit -> room.editMessage( + capturedMode.eventId, + text + ) + is MessageComposerMode.Quote -> TODO() + is MessageComposerMode.Reply -> room.replyMessage( + capturedMode.eventId, + text + ) + } } - } } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt index 28db417053..9cbd2a4acf 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt @@ -52,7 +52,10 @@ class TimelinePresenter @Inject constructor( private val timelineItemsFactory = TimelineItemsFactory(matrixItemHelper, room, coroutineDispatchers.computation) - private class TimelineCallback(private val coroutineScope: CoroutineScope, private val timelineItemsFactory: TimelineItemsFactory) : MatrixTimeline.Callback { + private class TimelineCallback( + private val coroutineScope: CoroutineScope, + private val timelineItemsFactory: TimelineItemsFactory, + ) : MatrixTimeline.Callback { override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) { coroutineScope.launch { timelineItemsFactory.pushItem(timelineItem) diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/Bindings.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/Bindings.kt index 65badae56f..9844a99896 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/Bindings.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/Bindings.kt @@ -24,7 +24,6 @@ import io.element.android.x.core.di.DaggerComponentOwner inline fun Node.bindings() = bindings(T::class.java) inline fun Context.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 return generateSequence(this) { (it as? ContextWrapper)?.baseContext } @@ -37,7 +36,6 @@ fun Context.bindings(klass: Class): T { ?: error("Unable to find bindings for ${klass.name}") } -/** 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) diff --git a/libraries/architecture/src/main/java/io/element/android/x/architecture/NodeKey.kt b/libraries/architecture/src/main/java/io/element/android/x/architecture/NodeKey.kt index 63e178552e..59150edf5f 100644 --- a/libraries/architecture/src/main/java/io/element/android/x/architecture/NodeKey.kt +++ b/libraries/architecture/src/main/java/io/element/android/x/architecture/NodeKey.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, From b3aa9f7ba209b38a608a8687651043ef31654d0a Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 16 Jan 2023 19:38:04 +0100 Subject: [PATCH 30/30] Fix CI again --- app/build.gradle.kts | 5 ++++- .../main/java/io/element/android/x/matrix/room/MatrixRoom.kt | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2742f55f06..220835ffa4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ + + /* * Copyright (c) 2022 New Vector Ltd * @@ -169,7 +171,8 @@ dependencies { implementation(project(":anvilannotations")) anvil(project(":anvilcodegen")) - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.0") + // https://developer.android.com/studio/write/java8-support#library-desugaring-versions + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2") implementation(libs.compose.destinations) ksp(libs.compose.destinations.processor) implementation(libs.appyx.core) diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt index 83238edc77..f2ecdb2b70 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt @@ -19,7 +19,6 @@ package io.element.android.x.matrix.room import io.element.android.x.matrix.core.EventId import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.timeline.MatrixTimeline -import io.element.android.x.matrix.timeline.RustMatrixTimeline import kotlinx.coroutines.flow.Flow interface MatrixRoom {