ganfra
2 years ago
26 changed files with 399 additions and 174 deletions
@ -0,0 +1,5 @@ |
|||||||
|
package io.element.android.x.features.logout |
||||||
|
|
||||||
|
sealed interface LogoutPreferenceEvents { |
||||||
|
object Logout: LogoutPreferenceEvents |
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
} |
@ -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) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -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) |
||||||
|
} |
||||||
|
} |
@ -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() |
|
||||||
} |
|
@ -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 |
||||||
|
} |
@ -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 |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -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 |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -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>, |
||||||
|
) |
@ -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) |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
} |
@ -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, |
||||||
|
) |
@ -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) |
||||||
|
} |
Loading…
Reference in new issue