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
import io.element.android.x.architecture.createNode import io.element.android.x.architecture.createNode
import io.element.android.x.architecture.viewmodel.viewModelSupportNode import io.element.android.x.architecture.viewmodel.viewModelSupportNode
import io.element.android.x.features.messages.MessagesScreen 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.features.roomlist.RoomListNode
import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.core.RoomId
import io.element.android.x.matrix.core.SessionId import io.element.android.x.matrix.core.SessionId
@ -34,6 +35,10 @@ class LoggedInFlowNode(
override fun onRoomClicked(roomId: RoomId) { override fun onRoomClicked(roomId: RoomId) {
backstack.push(NavTarget.Messages(roomId)) backstack.push(NavTarget.Messages(roomId))
} }
override fun onSettingsClicked() {
backstack.push(NavTarget.Settings)
}
} }
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@ -42,6 +47,9 @@ class LoggedInFlowNode(
@Parcelize @Parcelize
data class Messages(val roomId: RoomId) : NavTarget data class Messages(val roomId: RoomId) : NavTarget
@Parcelize
object Settings : NavTarget
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -55,6 +63,9 @@ class LoggedInFlowNode(
onBackPressed = { backstack.pop() } onBackPressed = { backstack.pop() }
) )
} }
NavTarget.Settings -> {
PreferencesFlowNode(buildContext)
}
} }
} }

1
features/logout/build.gradle.kts

@ -39,7 +39,6 @@ dependencies {
implementation(project(":libraries:matrix")) implementation(project(":libraries:matrix"))
implementation(project(":libraries:designsystem")) implementation(project(":libraries:designsystem"))
implementation(project(":libraries:elementresources")) implementation(project(":libraries:elementresources"))
implementation(libs.mavericks.compose)
ksp(libs.showkase.processor) ksp(libs.showkase.processor)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
androidTestImplementation(libs.test.junitext) androidTestImplementation(libs.test.junitext)

5
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
}

41
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<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
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Logout import androidx.compose.material.icons.filled.Logout
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.airbnb.mvrx.Loading import io.element.android.x.architecture.Async
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.ProgressDialog import io.element.android.x.designsystem.components.ProgressDialog
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog 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 import io.element.android.x.element.resources.R as ElementR
@Composable @Composable
fun LogoutPreference( fun LogoutPreferenceView(
viewModel: LogoutViewModel = mavericksViewModel(), state: LogoutPreferenceState,
onSuccessLogout: () -> Unit = { }, onLogoutClicked: () -> Unit = {},
onSuccessLogout: () -> Unit = {},
) { ) {
val state: LogoutViewState by viewModel.collectAsState() if (state.logoutAction is Async.Success) {
if (state.logoutAction is Success) {
onSuccessLogout() onSuccessLogout()
return return
} }
@ -65,7 +61,7 @@ fun LogoutPreference(
}, },
onSubmitClicked = { onSubmitClicked = {
openDialog.value = false openDialog.value = false
viewModel.logout() onLogoutClicked()
}, },
onDismiss = { onDismiss = {
openDialog.value = false openDialog.value = false
@ -73,7 +69,7 @@ fun LogoutPreference(
) )
} }
if (state.logoutAction is Loading) { if (state.logoutAction is Async.Loading) {
ProgressDialog(text = "Login out...") ProgressDialog(text = "Login out...")
} }
} }
@ -95,6 +91,6 @@ fun LogoutPreferenceContent(
@Preview @Preview
fun LogoutContentPreview() { fun LogoutContentPreview() {
ElementXTheme(darkTheme = false) { 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 @@
package io.element.android.x.features.logout package io.element.android.x.features.logout
import com.airbnb.mvrx.Async import io.element.android.x.architecture.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
data class LogoutViewState( data class LogoutPreferenceState(
val logoutAction: Async<Unit> = Uninitialized, val logoutAction: Async<Unit> = Async.Uninitialized,
) : MavericksState )

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

@ -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 {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.anvil) alias(libs.plugins.anvil)
id("kotlin-parcelize")
} }
android { android {

41
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<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 @@
/*
* 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 @@
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 @@
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 @@
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 @@
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 @@
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
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.airbnb.mvrx.compose.collectAsState import io.element.android.x.architecture.Async
import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.matrix.ui.components.MatrixUserHeader 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.model.MatrixUser
import io.element.android.x.matrix.ui.viewmodels.user.UserViewState
@Composable @Composable
fun UserPreferences( fun UserPreferences(
user: Async<MatrixUser>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: UserViewModel = mavericksViewModel(),
) { ) {
val user by viewModel.collectAsState(UserViewState::user) when (val userData = user.dataOrNull()) {
when (user()) {
null -> Spacer(modifier = modifier.height(1.dp)) null -> Spacer(modifier = modifier.height(1.dp))
else -> MatrixUserHeader( else -> MatrixUserHeader(
modifier = modifier, 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 @@
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 @@
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 @@
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
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.BugReport
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview 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.PreferenceCategory
import io.element.android.x.designsystem.components.preferences.PreferenceSlide 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.PreferenceSwitch
import io.element.android.x.designsystem.components.preferences.PreferenceText import io.element.android.x.designsystem.components.preferences.PreferenceText
import io.element.android.x.element.resources.R as ElementR 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 @Composable
fun RageshakePreferences( fun RageshakePreferencesView(
onOpenRageShake: () -> Unit = {}, state: RageshakePreferencesState,
) {
RageshakePreferencesContent(
onOpenRageShake = onOpenRageShake,
)
}
@Composable
fun RageshakePreferencesContent(
modifier: Modifier = Modifier, 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) { Column(modifier = modifier) {
PreferenceCategory(title = stringResource(id = ElementR.string.send_bug_report)) { PreferenceCategory(title = stringResource(id = ElementR.string.send_bug_report)) {
PreferenceText( PreferenceText(
title = stringResource(id = ElementR.string.send_bug_report), title = stringResource(id = ElementR.string.send_bug_report),
icon = Icons.Default.BugReport, icon = Icons.Default.BugReport,
onClick = onOpenRageShake onClick = onOpenRageshake
) )
} }
PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) { PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) {
@ -63,7 +50,7 @@ fun RageshakePreferencesContent(
PreferenceSwitch( PreferenceSwitch(
title = stringResource(id = ElementR.string.send_bug_report_rage_shake), title = stringResource(id = ElementR.string.send_bug_report_rage_shake),
isChecked = state.isEnabled, isChecked = state.isEnabled,
onCheckedChange = viewModel::onEnableClicked onCheckedChange = onIsEnabledChanged
) )
PreferenceSlide( PreferenceSlide(
title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold), title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold),
@ -71,7 +58,7 @@ fun RageshakePreferencesContent(
value = state.sensitivity, value = state.sensitivity,
enabled = state.isEnabled, enabled = state.isEnabled,
steps = 3 /* 5 possible values - steps are in ]0, 1[ */, steps = 3 /* 5 possible values - steps are in ]0, 1[ */,
onValueChange = viewModel::onSensitivityChange onValueChange = onSensitivityChanged
) )
} else { } else {
PreferenceText(title = "Rageshaking is not supported by your device") PreferenceText(title = "Rageshaking is not supported by your device")
@ -82,6 +69,6 @@ fun RageshakePreferencesContent(
@Composable @Composable
@Preview @Preview
fun RageshakePreferencePreview() { fun RageshakePreferencesPreview() {
RageshakePreferences() 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(
interface Callback : Plugin { interface Callback : Plugin {
fun onRoomClicked(roomId: RoomId) fun onRoomClicked(roomId: RoomId)
fun onSettingsClicked()
} }
private val connector = presenterConnector(presenter) private val connector = presenterConnector(presenter)
@ -45,6 +46,10 @@ class RoomListNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onRoomClicked(roomId) } plugins<Callback>().forEach { it.onRoomClicked(roomId) }
} }
private fun onOpenSettings() {
plugins<Callback>().forEach { it.onSettingsClicked() }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by connector.stateFlow.collectAsState() val state by connector.stateFlow.collectAsState()
@ -53,7 +58,7 @@ class RoomListNode @AssistedInject constructor(
onRoomClicked = this::onRoomClicked, onRoomClicked = this::onRoomClicked,
onFilterChanged = this::updateFilter, onFilterChanged = this::updateFilter,
onScrollOver = this::updateVisibleRange, onScrollOver = this::updateVisibleRange,
onOpenSettings = this::logout onOpenSettings = this::onOpenSettings
) )
} }
} }

2
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 { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.molecule) 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
sealed interface Async<out T> { sealed interface Async<out T> {
object Uninitialized : Async<Nothing> object Uninitialized : Async<Nothing>
data class Loading<out T>(val prevState: T? = null) : Async<T> 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> 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>>) { 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
import app.cash.molecule.RecompositionClock import app.cash.molecule.RecompositionClock
import app.cash.molecule.launchMolecule import app.cash.molecule.launchMolecule
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow 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> = inline fun <reified State, reified Event> LifecycleOwner.presenterConnector(presenter: Presenter<State, Event>): LifecyclePresenterConnector<State, Event> =
LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter) LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter)
class LifecyclePresenterConnector<State, Event>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State, Event>) { class LifecyclePresenterConnector<State, Event>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State, Event>) {
private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main) 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) { val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.Immediate) {
presenter.present(events = mutableEventFlow) presenter.present(events = eventFlow.asSharedFlow())
} }
fun emitEvent(event: Event) { 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 @@
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
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun PreferenceScreen( fun PreferenceView(
title: String, title: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {}, onBackPressed: () -> Unit = {},
@ -113,7 +113,7 @@ fun PreferenceTopAppBar(
@Composable @Composable
@Preview(showBackground = false) @Preview(showBackground = false)
fun PreferenceScreenPreview() { fun PreferenceScreenPreview() {
PreferenceScreen( PreferenceView(
title = "Preference screen" title = "Preference screen"
) { ) {
PreferenceCategoryPreview() PreferenceCategoryPreview()

Loading…
Cancel
Save