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()