Browse Source

Migrate Preferences to new architecture

feature/bma/flipper
ganfra 2 years ago
parent
commit
ae273bd4ea
  1. 11
      app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt
  2. 1
      features/logout/build.gradle.kts
  3. 5
      features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt
  4. 41
      features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt
  5. 22
      features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceScreen.kt
  6. 10
      features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceState.kt
  7. 46
      features/logout/src/main/java/io/element/android/x/features/logout/LogoutViewModel.kt
  8. 1
      features/preferences/build.gradle.kts
  9. 41
      features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt
  10. 66
      features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesScreen.kt
  11. 7
      features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootEvents.kt
  12. 48
      features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt
  13. 42
      features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt
  14. 12
      features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootState.kt
  15. 56
      features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt
  16. 14
      features/preferences/src/main/java/io/element/android/x/features/preferences/user/UserPreferences.kt
  17. 6
      features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesEvents.kt
  18. 59
      features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt
  19. 7
      features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt
  20. 33
      features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt
  21. 7
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt
  22. 2
      libraries/architecture/build.gradle.kts
  23. 11
      libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt
  24. 9
      libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt
  25. 12
      libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt
  26. 4
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceScreen.kt

11
app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt

@ -13,6 +13,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push @@ -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( @@ -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( @@ -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( @@ -55,6 +63,9 @@ class LoggedInFlowNode(
onBackPressed = { backstack.pop() }
)
}
NavTarget.Settings -> {
PreferencesFlowNode(buildContext)
}
}
}

1
features/logout/build.gradle.kts

@ -39,7 +39,6 @@ dependencies { @@ -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)

5
features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceEvents.kt

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
package io.element.android.x.features.logout
sealed interface LogoutPreferenceEvents {
object Logout: LogoutPreferenceEvents
}

41
features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferencePresenter.kt

@ -0,0 +1,41 @@ @@ -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<LogoutPreferenceState, LogoutPreferenceEvents> {
@Composable
override fun present(events: Flow<LogoutPreferenceEvents>): LogoutPreferenceState {
val logoutAction: MutableState<Async<Unit>> = 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<Async<Unit>>) = launch {
suspend {
matrixClient.logout()
}.execute(logoutAction)
}
}

22
features/logout/src/main/java/io/element/android/x/features/logout/LogoutScreen.kt → features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceScreen.kt

@ -19,15 +19,11 @@ package io.element.android.x.features.logout @@ -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 @@ -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( @@ -65,7 +61,7 @@ fun LogoutPreference(
},
onSubmitClicked = {
openDialog.value = false
viewModel.logout()
onLogoutClicked()
},
onDismiss = {
openDialog.value = false
@ -73,7 +69,7 @@ fun LogoutPreference( @@ -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( @@ -95,6 +91,6 @@ fun LogoutPreferenceContent(
@Preview
fun LogoutContentPreview() {
ElementXTheme(darkTheme = false) {
LogoutPreference()
LogoutPreferenceView(LogoutPreferenceState())
}
}

10
features/logout/src/main/java/io/element/android/x/features/logout/LogoutViewState.kt → features/logout/src/main/java/io/element/android/x/features/logout/LogoutPreferenceState.kt

@ -16,10 +16,8 @@ @@ -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<Unit> = Uninitialized,
) : MavericksState
data class LogoutPreferenceState(
val logoutAction: Async<Unit> = Async.Uninitialized,
)

46
features/logout/src/main/java/io/element/android/x/features/logout/LogoutViewModel.kt

@ -1,46 +0,0 @@ @@ -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<LogoutViewState>(initialState) {
companion object : MavericksViewModelFactory<LogoutViewModel, LogoutViewState> by daggerMavericksViewModelFactory()
fun logout() {
viewModelScope.launch {
suspend {
client.logout()
}.execute {
copy(logoutAction = it)
}
}
}
}

1
features/preferences/build.gradle.kts

@ -20,6 +20,7 @@ plugins { @@ -20,6 +20,7 @@ plugins {
id("io.element.android-compose-library")
alias(libs.plugins.ksp)
alias(libs.plugins.anvil)
id("kotlin-parcelize")
}
android {

41
features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesFlowNode.kt

@ -0,0 +1,41 @@ @@ -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<NavTarget> = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
),
) : ParentNode<PreferencesFlowNode.NavTarget>(
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<PreferencesRootNode>(buildContext)
}
}
@Composable
override fun View(modifier: Modifier) {
Children(navModel = backstack)
}
}

66
features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesScreen.kt

@ -1,66 +0,0 @@ @@ -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()
}

7
features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootEvents.kt

@ -0,0 +1,7 @@ @@ -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
}

48
features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootNode.kt

@ -0,0 +1,48 @@ @@ -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<Plugin>,
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
)
}
}

42
features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootPresenter.kt

@ -0,0 +1,42 @@ @@ -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<PreferencesRootState, PreferencesRootEvents> {
private val logoutEventsFlow = SharedFlowHolder<LogoutPreferenceEvents>()
private val rageshakeEventsFlow = SharedFlowHolder<RageshakePreferencesEvents>()
@Composable
override fun present(events: Flow<PreferencesRootEvents>): 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
)
}
}

12
features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootState.kt

@ -0,0 +1,12 @@ @@ -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<MatrixUser>,
)

56
features/preferences/src/main/java/io/element/android/x/features/preferences/root/PreferencesRootView.kt

@ -0,0 +1,56 @@ @@ -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)
}

14
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 @@ -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<MatrixUser>,
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
)
}
}

6
features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesEvents.kt

@ -0,0 +1,6 @@ @@ -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
}

59
features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesPresenter.kt

@ -0,0 +1,59 @@ @@ -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<RageshakePreferencesState, RageshakePreferencesEvents> {
@Composable
override fun present(events: Flow<RageshakePreferencesEvents>): RageshakePreferencesState {
val isSupported: MutableState<Boolean> = 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)
}
}

7
features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesState.kt

@ -0,0 +1,7 @@ @@ -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,
)

33
features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferenceCategory.kt → features/rageshake/src/main/java/io/element/android/x/features/rageshake/preferences/RageshakePreferencesView.kt

@ -20,42 +20,29 @@ import androidx.compose.foundation.layout.Column @@ -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( @@ -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( @@ -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( @@ -82,6 +69,6 @@ fun RageshakePreferencesContent(
@Composable
@Preview
fun RageshakePreferencePreview() {
RageshakePreferences()
fun RageshakePreferencesPreview() {
RageshakePreferencesView(RageshakePreferencesState(isEnabled = true, isSupported = true, sensitivity = 0.5f))
}

7
features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt

@ -25,6 +25,7 @@ class RoomListNode @AssistedInject constructor( @@ -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( @@ -45,6 +46,10 @@ class RoomListNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onRoomClicked(roomId) }
}
private fun onOpenSettings() {
plugins<Callback>().forEach { it.onSettingsClicked() }
}
@Composable
override fun View(modifier: Modifier) {
val state by connector.stateFlow.collectAsState()
@ -53,7 +58,7 @@ class RoomListNode @AssistedInject constructor( @@ -53,7 +58,7 @@ class RoomListNode @AssistedInject constructor(
onRoomClicked = this::onRoomClicked,
onFilterChanged = this::updateFilter,
onScrollOver = this::updateVisibleRange,
onOpenSettings = this::logout
onOpenSettings = this::onOpenSettings
)
}
}

2
libraries/architecture/build.gradle.kts

@ -1,3 +1,5 @@ @@ -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)

11
libraries/architecture/src/main/java/io/element/android/x/architecture/Async.kt

@ -5,8 +5,17 @@ import androidx.compose.runtime.MutableState @@ -5,8 +5,17 @@ import androidx.compose.runtime.MutableState
sealed interface Async<out T> {
object Uninitialized : Async<Nothing>
data class Loading<out T>(val prevState: T? = null) : Async<T>
data class Failure<out T>(val error: Throwable) : Async<T>
data class Failure<out T>(val error: Throwable, val prevState: T? = null) : Async<T>
data class Success<out T>(val state: T) : Async<T>
fun dataOrNull(): T? {
return when (this) {
is Failure -> prevState
is Loading -> prevState
is Success -> state
Uninitialized -> null
}
}
}
suspend fun <T> (suspend () -> T).execute(state: MutableState<Async<T>>) {

9
libraries/architecture/src/main/java/io/element/android/x/architecture/PresenterConnector.kt

@ -6,24 +6,21 @@ import app.cash.molecule.AndroidUiDispatcher @@ -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 <reified State, reified Event> LifecycleOwner.presenterConnector(presenter: Presenter<State, Event>): LifecyclePresenterConnector<State, Event> =
LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter)
class LifecyclePresenterConnector<State, Event>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State, Event>) {
private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main)
private val mutableEventFlow: MutableSharedFlow<Event> = MutableSharedFlow(extraBufferCapacity = 64)
private val eventFlow = SharedFlowHolder<Event>()
val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.Immediate) {
presenter.present(events = mutableEventFlow)
presenter.present(events = eventFlow.asSharedFlow())
}
fun emitEvent(event: Event) {
mutableEventFlow.tryEmit(event)
eventFlow.emit(event)
}
}

12
libraries/architecture/src/main/java/io/element/android/x/architecture/SharedFlowHolder.kt

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
package io.element.android.x.architecture
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
class SharedFlowHolder<Data>(capacity: Int = 64) {
private val mutableFlow: MutableSharedFlow<Data> = MutableSharedFlow(extraBufferCapacity = capacity)
fun asSharedFlow() = mutableFlow.asSharedFlow()
fun emit(data: Data) = mutableFlow.tryEmit(data)
}

4
libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceScreen.kt

@ -45,7 +45,7 @@ import androidx.compose.ui.unit.sp @@ -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( @@ -113,7 +113,7 @@ fun PreferenceTopAppBar(
@Composable
@Preview(showBackground = false)
fun PreferenceScreenPreview() {
PreferenceScreen(
PreferenceView(
title = "Preference screen"
) {
PreferenceCategoryPreview()

Loading…
Cancel
Save