Use EventSink lambda in state instead of Flow in Presenter

This commit is contained in:
ganfra 2023-01-11 15:53:52 +01:00
parent 56e54bb172
commit 1a0c9df1da
43 changed files with 277 additions and 490 deletions

View file

@ -25,8 +25,8 @@ import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push 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.presenterConnector import io.element.android.x.architecture.presenterConnector
import io.element.android.x.core.compose.OnLifecycleEvent
import io.element.android.x.core.di.DaggerComponentOwner import io.element.android.x.core.di.DaggerComponentOwner
import io.element.android.x.core.screenshot.ImageResult
import io.element.android.x.di.SessionComponentsOwner import io.element.android.x.di.SessionComponentsOwner
import io.element.android.x.features.rageshake.bugreport.BugReportNode import io.element.android.x.features.rageshake.bugreport.BugReportNode
import io.element.android.x.matrix.Matrix import io.element.android.x.matrix.Matrix
@ -77,22 +77,6 @@ class RootFlowNode(
private val presenterConnector = presenterConnector(rootPresenter) private val presenterConnector = presenterConnector(rootPresenter)
init {
Timber.v("Init")
lifecycle.subscribe(
onCreate = { Timber.v("OnCreate") },
onResume = {
Timber.v("OnResume")
presenterConnector.emitEvent(RootEvents.StartRageshakeDetection)
},
onPause = {
Timber.v("OnPause")
presenterConnector.emitEvent(RootEvents.StopRageshakeDetection)
},
onDestroy = { Timber.v("OnDestroy") }
)
}
init { init {
matrix.isLoggedIn() matrix.isLoggedIn()
.distinctUntilChanged() .distinctUntilChanged()
@ -114,47 +98,21 @@ class RootFlowNode(
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
} }
private fun hideShowkaseButton() {
presenterConnector.emitEvent(RootEvents.HideShowkaseButton)
}
private fun onOpenBugReport() { private fun onOpenBugReport() {
presenterConnector.emitEvent(RootEvents.ResetAppHasCrashed)
backstack.push(NavTarget.BugReport) backstack.push(NavTarget.BugReport)
} }
private fun onCrashDetectedDismissed() {
presenterConnector.emitEvent(RootEvents.ResetAllCrashData)
}
private fun onDismissRageshake() {
presenterConnector.emitEvent(RootEvents.DismissRageshake)
}
private fun onDisableRageshake() {
presenterConnector.emitEvent(RootEvents.DisableRageshake)
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState() val state by presenterConnector.stateFlow.collectAsState()
RootView( RootView(
state = state, state = state,
onHideShowkaseClicked = this::hideShowkaseButton,
onOpenBugReport = this::onOpenBugReport, onOpenBugReport = this::onOpenBugReport,
onCrashDetectedDismissed = this::onCrashDetectedDismissed,
onDisableRageshake = this::onDisableRageshake,
onDismissRageshake = this::onDismissRageshake,
onScreenshotTaken = this::onScreenshotTaken
) { ) {
Children(navModel = backstack) Children(navModel = backstack)
} }
} }
private fun onScreenshotTaken(imageResult: ImageResult) {
presenterConnector.emitEvent(RootEvents.ProcessScreenshot(imageResult))
}
private val bugReportNodeCallback = object : BugReportNode.Callback { private val bugReportNodeCallback = object : BugReportNode.Callback {
override fun onBugReportSent() { override fun onBugReportSent() {
backstack.pop() backstack.pop()

View file

@ -1,14 +1,5 @@
package io.element.android.x.root package io.element.android.x.root
import io.element.android.x.core.screenshot.ImageResult
sealed interface RootEvents { sealed interface RootEvents {
data class ProcessScreenshot(val imageResult: ImageResult) : RootEvents object HideShowkaseButton : RootEvents
object HideShowkaseButton: RootEvents
object ResetAllCrashData : RootEvents
object ResetAppHasCrashed: RootEvents
object DisableRageshake: RootEvents
object DismissRageshake: RootEvents
object StartRageshakeDetection: RootEvents
object StopRageshakeDetection: RootEvents
} }

View file

@ -1,62 +1,45 @@
package io.element.android.x.root package io.element.android.x.root
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Presenter import io.element.android.x.architecture.Presenter
import io.element.android.x.architecture.SharedFlowHolder
import io.element.android.x.features.rageshake.bugreport.BugReportEvents
import io.element.android.x.features.rageshake.bugreport.BugReportPresenter import io.element.android.x.features.rageshake.bugreport.BugReportPresenter
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionEvents
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionPresenter import io.element.android.x.features.rageshake.crash.ui.CrashDetectionPresenter
import io.element.android.x.features.rageshake.detection.RageshakeDetectionEvents
import io.element.android.x.features.rageshake.detection.RageshakeDetectionPresenter import io.element.android.x.features.rageshake.detection.RageshakeDetectionPresenter
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject import javax.inject.Inject
class RootPresenter @Inject constructor( class RootPresenter @Inject constructor(
private val bugReportPresenter: BugReportPresenter, private val bugReportPresenter: BugReportPresenter,
private val crashDetectionPresenter: CrashDetectionPresenter, private val crashDetectionPresenter: CrashDetectionPresenter,
private val rageshakeDetectionPresenter: RageshakeDetectionPresenter, private val rageshakeDetectionPresenter: RageshakeDetectionPresenter,
) : Presenter<RootState, RootEvents> { ) : Presenter<RootState> {
private val rageshakeDetectionEventsFlow = SharedFlowHolder<RageshakeDetectionEvents>()
private val bugReporterEventsFlow = SharedFlowHolder<BugReportEvents>()
private val crashDetectionEventsFlow = SharedFlowHolder<CrashDetectionEvents>()
@Composable @Composable
override fun present(events: Flow<RootEvents>): RootState { override fun present(): RootState {
val isBugReportVisible = rememberSaveable { val isBugReportVisible = rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
val isShowkaseButtonVisible = rememberSaveable { val isShowkaseButtonVisible = rememberSaveable {
mutableStateOf(true) mutableStateOf(true)
} }
val rageshakeDetectionState = rageshakeDetectionPresenter.present(events = rageshakeDetectionEventsFlow.asSharedFlow()) val rageshakeDetectionState = rageshakeDetectionPresenter.present()
val crashDetectionState = crashDetectionPresenter.present(events = crashDetectionEventsFlow.asSharedFlow()) val crashDetectionState = crashDetectionPresenter.present()
val bugReportState = bugReportPresenter.present(events = bugReporterEventsFlow.asSharedFlow()) val bugReportState = bugReportPresenter.present()
LaunchedEffect(Unit) { fun handleEvent(event: RootEvents) {
events.collect { event -> when (event) {
when (event) { RootEvents.HideShowkaseButton -> isShowkaseButtonVisible.value = false
RootEvents.HideShowkaseButton -> isShowkaseButtonVisible.value = false
RootEvents.ResetAllCrashData -> crashDetectionEventsFlow.emit(CrashDetectionEvents.ResetAllCrashData)
RootEvents.ResetAppHasCrashed -> crashDetectionEventsFlow.emit(CrashDetectionEvents.ResetAppHasCrashed)
RootEvents.DisableRageshake -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.Disable)
RootEvents.DismissRageshake -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.Dismiss)
RootEvents.StartRageshakeDetection -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.StartDetection)
RootEvents.StopRageshakeDetection -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.StopDetection)
is RootEvents.ProcessScreenshot -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.ProcessScreenshot(event.imageResult))
}
} }
} }
return RootState( return RootState(
isBugReportVisible = isBugReportVisible.value, isBugReportVisible = isBugReportVisible.value,
isShowkaseButtonVisible = isShowkaseButtonVisible.value, isShowkaseButtonVisible = isShowkaseButtonVisible.value,
rageshakeDetectionState = rageshakeDetectionState, rageshakeDetectionState = rageshakeDetectionState,
crashDetectionState = crashDetectionState, crashDetectionState = crashDetectionState,
bugReportState = bugReportState bugReportState = bugReportState,
eventSink = ::handleEvent
) )
} }
} }

View file

@ -11,5 +11,6 @@ data class RootState(
val isShowkaseButtonVisible: Boolean, val isShowkaseButtonVisible: Boolean,
val rageshakeDetectionState: RageshakeDetectionState, val rageshakeDetectionState: RageshakeDetectionState,
val crashDetectionState: CrashDetectionState, val crashDetectionState: CrashDetectionState,
val bugReportState: BugReportState val bugReportState: BugReportState,
val eventSink: (RootEvents) -> Unit
) )

View file

@ -10,8 +10,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.airbnb.android.showkase.models.Showkase import com.airbnb.android.showkase.models.Showkase
import io.element.android.x.component.ShowkaseButton import io.element.android.x.component.ShowkaseButton
import io.element.android.x.core.screenshot.ImageResult import io.element.android.x.features.rageshake.crash.ui.CrashDetectionEvents
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionView import io.element.android.x.features.rageshake.crash.ui.CrashDetectionView
import io.element.android.x.features.rageshake.detection.RageshakeDetectionEvents
import io.element.android.x.features.rageshake.detection.RageshakeDetectionView import io.element.android.x.features.rageshake.detection.RageshakeDetectionView
import io.element.android.x.getBrowserIntent import io.element.android.x.getBrowserIntent
@ -19,12 +20,7 @@ import io.element.android.x.getBrowserIntent
fun RootView( fun RootView(
state: RootState, state: RootState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onHideShowkaseClicked: () -> Unit = { },
onOpenBugReport: () -> Unit = {}, onOpenBugReport: () -> Unit = {},
onCrashDetectedDismissed: () -> Unit = {},
onDisableRageshake: () -> Unit = {},
onDismissRageshake: () -> Unit = {},
onScreenshotTaken: (ImageResult) -> Unit = {},
children: @Composable BoxScope.() -> Unit, children: @Composable BoxScope.() -> Unit,
) { ) {
Box( Box(
@ -33,23 +29,27 @@ fun RootView(
contentAlignment = Alignment.TopCenter, contentAlignment = Alignment.TopCenter,
) { ) {
children() children()
val eventSink = state.eventSink
val context = LocalContext.current val context = LocalContext.current
fun onOpenBugReport() {
state.crashDetectionState.eventSink(CrashDetectionEvents.ResetAppHasCrashed)
state.rageshakeDetectionState.eventSink(RageshakeDetectionEvents.Dismiss)
onOpenBugReport.invoke()
}
ShowkaseButton( ShowkaseButton(
isVisible = state.isShowkaseButtonVisible, isVisible = state.isShowkaseButtonVisible,
onCloseClicked = onHideShowkaseClicked, onCloseClicked = { eventSink(RootEvents.HideShowkaseButton) },
onClick = { ContextCompat.startActivity(context, Showkase.getBrowserIntent(context), null) } onClick = { ContextCompat.startActivity(context, Showkase.getBrowserIntent(context), null) }
) )
RageshakeDetectionView( RageshakeDetectionView(
state = state.rageshakeDetectionState, state = state.rageshakeDetectionState,
onOpenBugReport = onOpenBugReport, onOpenBugReport = ::onOpenBugReport,
onDisableClicked = onDisableRageshake,
onNoClicked = onDismissRageshake,
onScreenshotTaken = onScreenshotTaken
) )
CrashDetectionView( CrashDetectionView(
state = state.crashDetectionState, state = state.crashDetectionState,
onOpenBugReport = onOpenBugReport, onOpenBugReport = ::onOpenBugReport,
onPopupDismissed = onCrashDetectedDismissed
) )
} }
} }

View file

@ -22,14 +22,6 @@ class ChangeServerNode @AssistedInject constructor(
private val presenterConnector = presenterConnector(presenter) private val presenterConnector = presenterConnector(presenter)
private fun onChangeServer(server: String) {
presenterConnector.emitEvent(ChangeServerEvents.SetServer(server))
}
private fun onSubmit() {
presenterConnector.emitEvent(ChangeServerEvents.Submit)
}
private fun onSuccess() { private fun onSuccess() {
navigateUp() navigateUp()
} }
@ -39,8 +31,6 @@ class ChangeServerNode @AssistedInject constructor(
val state by presenterConnector.stateFlow.collectAsState() val state by presenterConnector.stateFlow.collectAsState()
ChangeServerView( ChangeServerView(
state = state, state = state,
onChangeServer = this::onChangeServer,
onChangeServerSubmit = this::onSubmit,
onChangeServerSuccess = this::onSuccess, onChangeServerSuccess = this::onSuccess,
) )
} }

View file

@ -1,41 +1,42 @@
package io.element.android.x.features.login.changeserver package io.element.android.x.features.login.changeserver
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Async import io.element.android.x.architecture.Async
import io.element.android.x.architecture.Presenter import io.element.android.x.architecture.Presenter
import io.element.android.x.architecture.execute import io.element.android.x.architecture.execute
import io.element.android.x.matrix.Matrix import io.element.android.x.matrix.Matrix
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class ChangeServerPresenter @Inject constructor(private val matrix: Matrix) : Presenter<ChangeServerState, ChangeServerEvents> { class ChangeServerPresenter @Inject constructor(private val matrix: Matrix) : Presenter<ChangeServerState> {
@Composable @Composable
override fun present(events: Flow<ChangeServerEvents>): ChangeServerState { override fun present(): ChangeServerState {
val localCoroutineScope = rememberCoroutineScope()
val homeserver = rememberSaveable { val homeserver = rememberSaveable {
mutableStateOf(matrix.getHomeserverOrDefault()) mutableStateOf(matrix.getHomeserverOrDefault())
} }
val changeServerAction: MutableState<Async<Unit>> = remember { val changeServerAction: MutableState<Async<Unit>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(Async.Uninitialized)
} }
LaunchedEffect(Unit) {
events.collect { event -> fun handleEvents(event: ChangeServerEvents) {
when (event) { when (event) {
is ChangeServerEvents.SetServer -> homeserver.value = event.server is ChangeServerEvents.SetServer -> homeserver.value = event.server
ChangeServerEvents.Submit -> submit(homeserver.value, changeServerAction) ChangeServerEvents.Submit -> localCoroutineScope.submit(homeserver.value, changeServerAction)
}
} }
} }
return ChangeServerState( return ChangeServerState(
homeserver = homeserver.value, homeserver = homeserver.value,
changeServerAction = changeServerAction.value changeServerAction = changeServerAction.value,
eventSink = ::handleEvents
) )
} }

View file

@ -5,6 +5,7 @@ import io.element.android.x.architecture.Async
data class ChangeServerState( data class ChangeServerState(
val homeserver: String = "", val homeserver: String = "",
val changeServerAction: Async<Unit> = Async.Uninitialized, val changeServerAction: Async<Unit> = Async.Uninitialized,
val eventSink: (ChangeServerEvents) -> Unit = {},
) { ) {
val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Async.Loading val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Async.Loading
} }

View file

@ -64,14 +64,13 @@ import io.element.android.x.features.login.error.changeServerError
fun ChangeServerView( fun ChangeServerView(
state: ChangeServerState, state: ChangeServerState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onChangeServer: (String) -> Unit = {},
onChangeServerSubmit: () -> Unit = {},
onChangeServerSuccess: () -> Unit = {}, onChangeServerSuccess: () -> Unit = {},
) { ) {
Surface( Surface(
modifier = modifier, modifier = modifier,
color = MaterialTheme.colorScheme.background, color = MaterialTheme.colorScheme.background,
) { ) {
val eventSink = state.eventSink
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Box( Box(
modifier = Modifier modifier = Modifier
@ -135,7 +134,7 @@ fun ChangeServerView(
.padding(top = 200.dp), .padding(top = 200.dp),
onValueChange = { onValueChange = {
homeserverFieldState = it homeserverFieldState = it
onChangeServer(it) eventSink(ChangeServerEvents.SetServer(it))
}, },
label = { label = {
Text(text = "Server") Text(text = "Server")
@ -146,7 +145,7 @@ fun ChangeServerView(
imeAction = ImeAction.Done, imeAction = ImeAction.Done,
), ),
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onDone = { onChangeServerSubmit() } onDone = { eventSink(ChangeServerEvents.Submit) }
) )
) )
if (state.changeServerAction is Async.Failure) { if (state.changeServerAction is Async.Failure) {
@ -161,7 +160,7 @@ fun ChangeServerView(
) )
} }
Button( Button(
onClick = onChangeServerSubmit, onClick = { eventSink(ChangeServerEvents.Submit) },
enabled = state.submitEnabled, enabled = state.submitEnabled,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
@ -13,6 +14,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesNode import io.element.android.x.anvilannotations.ContributesNode
import io.element.android.x.architecture.presenterConnector import io.element.android.x.architecture.presenterConnector
import io.element.android.x.core.compose.OnLifecycleEvent
import io.element.android.x.di.AppScope import io.element.android.x.di.AppScope
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@ -24,12 +26,6 @@ class LoginRootNode @AssistedInject constructor(
private val presenterConnector = presenterConnector(presenter) private val presenterConnector = presenterConnector(presenter)
init {
lifecycle.subscribe(
onResume = { presenterConnector.emitEvent(LoginRootEvents.RefreshHomeServer) }
)
}
interface Callback : Plugin { interface Callback : Plugin {
fun onChangeHomeServer() fun onChangeHomeServer()
} }
@ -38,27 +34,18 @@ class LoginRootNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onChangeHomeServer() } plugins<Callback>().forEach { it.onChangeHomeServer() }
} }
private fun onLoginChanged(login: String) {
presenterConnector.emitEvent(LoginRootEvents.SetLogin(login))
}
private fun onPasswordChanged(password: String) {
presenterConnector.emitEvent(LoginRootEvents.SetPassword(password))
}
private fun onSubmit() {
presenterConnector.emitEvent(LoginRootEvents.Submit)
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState() val state by presenterConnector.stateFlow.collectAsState()
OnLifecycleEvent { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> state.eventSink(LoginRootEvents.RefreshHomeServer)
else -> Unit
}
}
LoginRootScreen( LoginRootScreen(
state = state, state = state,
onChangeServer = this::onChangeHomeServer, onChangeServer = this::onChangeHomeServer,
onLoginChanged = this::onLoginChanged,
onPasswordChanged = this::onPasswordChanged,
onSubmitClicked = this::onSubmit
) )
} }
} }

View file

@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Presenter import io.element.android.x.architecture.Presenter
import io.element.android.x.matrix.Matrix import io.element.android.x.matrix.Matrix
@ -13,10 +14,11 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class LoginRootPresenter @Inject constructor(private val matrix: Matrix) : Presenter<LoginRootState, LoginRootEvents> { class LoginRootPresenter @Inject constructor(private val matrix: Matrix) : Presenter<LoginRootState> {
@Composable @Composable
override fun present(events: Flow<LoginRootEvents>): LoginRootState { override fun present(): LoginRootState {
val localCoroutineScope = rememberCoroutineScope()
val homeserver = rememberSaveable { val homeserver = rememberSaveable {
mutableStateOf(matrix.getHomeserverOrDefault()) mutableStateOf(matrix.getHomeserverOrDefault())
} }
@ -27,24 +29,24 @@ class LoginRootPresenter @Inject constructor(private val matrix: Matrix) : Prese
mutableStateOf(LoginFormState.Default) mutableStateOf(LoginFormState.Default)
} }
LaunchedEffect(Unit) { fun handleEvents(event: LoginRootEvents){
events.collect { event -> when (event) {
when (event) { LoginRootEvents.RefreshHomeServer -> refreshHomeServer(homeserver)
LoginRootEvents.RefreshHomeServer -> refreshHomeServer(homeserver) is LoginRootEvents.SetLogin -> updateFormState(formState) {
is LoginRootEvents.SetLogin -> updateFormState(formState) { copy(login = event.login)
copy(login = event.login)
}
is LoginRootEvents.SetPassword -> updateFormState(formState) {
copy(password = event.password)
}
LoginRootEvents.Submit -> submit(homeserver.value, formState.value, loggedInState)
} }
is LoginRootEvents.SetPassword -> updateFormState(formState) {
copy(password = event.password)
}
LoginRootEvents.Submit -> localCoroutineScope.submit(homeserver.value, formState.value, loggedInState)
} }
} }
return LoginRootState( return LoginRootState(
homeserver = homeserver.value, homeserver = homeserver.value,
loggedInState = loggedInState.value, loggedInState = loggedInState.value,
formState = formState.value formState = formState.value,
eventSink = ::handleEvents
) )
} }

View file

@ -68,11 +68,9 @@ fun LoginRootScreen(
state: LoginRootState, state: LoginRootState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onChangeServer: () -> Unit = {}, onChangeServer: () -> Unit = {},
onLoginChanged: (String) -> Unit = {},
onPasswordChanged: (String) -> Unit = {},
onSubmitClicked: () -> Unit = {},
onLoginWithSuccess: (SessionId) -> Unit = {}, onLoginWithSuccess: (SessionId) -> Unit = {},
) { ) {
val eventSink = state.eventSink
Surface( Surface(
modifier = modifier, modifier = modifier,
color = MaterialTheme.colorScheme.background, color = MaterialTheme.colorScheme.background,
@ -144,7 +142,7 @@ fun LoginRootScreen(
}, },
onValueChange = { onValueChange = {
loginFieldState = it loginFieldState = it
onLoginChanged(it) eventSink(LoginRootEvents.SetLogin(it))
}, },
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email, keyboardType = KeyboardType.Email,
@ -163,7 +161,7 @@ fun LoginRootScreen(
.padding(top = 24.dp), .padding(top = 24.dp),
onValueChange = { onValueChange = {
passwordFieldState = it passwordFieldState = it
onPasswordChanged(it) eventSink(LoginRootEvents.SetPassword(it))
}, },
label = { label = {
Text(text = "Password") Text(text = "Password")
@ -185,7 +183,7 @@ fun LoginRootScreen(
imeAction = ImeAction.Done, imeAction = ImeAction.Done,
), ),
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onDone = { onSubmitClicked() } onDone = { eventSink(LoginRootEvents.Submit) }
), ),
) )
if (state.loggedInState is LoggedInState.ErrorLoggingIn) { if (state.loggedInState is LoggedInState.ErrorLoggingIn) {
@ -199,7 +197,7 @@ fun LoginRootScreen(
} }
// Submit // Submit
Button( Button(
onClick = onSubmitClicked, onClick = { eventSink(LoginRootEvents.Submit) },
enabled = state.submitEnabled, enabled = state.submitEnabled,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -8,6 +8,7 @@ data class LoginRootState(
val homeserver: String = "", val homeserver: String = "",
val loggedInState: LoggedInState = LoggedInState.NotLoggedIn, val loggedInState: LoggedInState = LoggedInState.NotLoggedIn,
val formState: LoginFormState = LoginFormState.Default, val formState: LoginFormState = LoginFormState.Default,
val eventSink: (LoginRootEvents) -> Unit = {}
) { ) {
val submitEnabled = val submitEnabled =
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState != LoggedInState.LoggingIn formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState != LoggedInState.LoggingIn

View file

@ -1,35 +1,36 @@
package io.element.android.x.features.logout package io.element.android.x.features.logout
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.x.architecture.Async import io.element.android.x.architecture.Async
import io.element.android.x.architecture.Presenter import io.element.android.x.architecture.Presenter
import io.element.android.x.architecture.execute import io.element.android.x.architecture.execute
import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.MatrixClient
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) : Presenter<LogoutPreferenceState, LogoutPreferenceEvents> { class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) : Presenter<LogoutPreferenceState> {
@Composable @Composable
override fun present(events: Flow<LogoutPreferenceEvents>): LogoutPreferenceState { override fun present(): LogoutPreferenceState {
val localCoroutineScope = rememberCoroutineScope()
val logoutAction: MutableState<Async<Unit>> = remember { val logoutAction: MutableState<Async<Unit>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(Async.Uninitialized)
} }
LaunchedEffect(Unit) {
events.collect { event -> fun handleEvents(event: LogoutPreferenceEvents) {
when (event) { when (event) {
LogoutPreferenceEvents.Logout -> logout(logoutAction) LogoutPreferenceEvents.Logout -> localCoroutineScope.logout(logoutAction)
}
} }
} }
return LogoutPreferenceState( return LogoutPreferenceState(
logoutAction = logoutAction.value logoutAction = logoutAction.value,
eventSink = ::handleEvents
) )
} }

View file

@ -19,6 +19,7 @@ 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.LaunchedEffect
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
@ -34,14 +35,15 @@ import io.element.android.x.element.resources.R as ElementR
@Composable @Composable
fun LogoutPreferenceView( fun LogoutPreferenceView(
state: LogoutPreferenceState, state: LogoutPreferenceState,
onLogoutClicked: () -> Unit = {}, onSuccessLogout: () -> Unit = {}
onSuccessLogout: () -> Unit = {},
) { ) {
val eventSink = state.eventSink
if (state.logoutAction is Async.Success) { if (state.logoutAction is Async.Success) {
onSuccessLogout() LaunchedEffect(state.logoutAction) {
onSuccessLogout()
}
return return
} }
val openDialog = remember { mutableStateOf(false) } val openDialog = remember { mutableStateOf(false) }
LogoutPreferenceContent( LogoutPreferenceContent(
@ -61,7 +63,7 @@ fun LogoutPreferenceView(
}, },
onSubmitClicked = { onSubmitClicked = {
openDialog.value = false openDialog.value = false
onLogoutClicked() eventSink(LogoutPreferenceEvents.Logout)
}, },
onDismiss = { onDismiss = {
openDialog.value = false openDialog.value = false

View file

@ -20,4 +20,5 @@ import io.element.android.x.architecture.Async
data class LogoutPreferenceState( data class LogoutPreferenceState(
val logoutAction: Async<Unit> = Async.Uninitialized, val logoutAction: Async<Unit> = Async.Uninitialized,
val eventSink: (LogoutPreferenceEvents) -> Unit = {},
) )

View file

@ -1,31 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.x.features.onboarding
import com.airbnb.mvrx.MavericksViewModel
class OnBoardingViewModel(initialState: OnBoardingViewState) :
MavericksViewModel<OnBoardingViewState>(initialState) {
fun onPageChanged(page: Int) {
setState {
copy(
currentPage = page,
)
}
}
}

View file

@ -1,23 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.x.features.onboarding
import com.airbnb.mvrx.MavericksState
data class OnBoardingViewState(
val currentPage: Int = 0,
) : MavericksState

View file

@ -1,7 +0,0 @@
package io.element.android.x.features.preferences.root
sealed interface PreferencesRootEvents {
object Logout : PreferencesRootEvents
data class SetRageshakeSensitivity(val sensitivity: Float) : PreferencesRootEvents
data class SetRageshakeEnabled(val enabled: Boolean) : PreferencesRootEvents
}

View file

@ -21,24 +21,12 @@ class PreferencesRootNode @AssistedInject constructor(
private val presenter: PreferencesRootPresenter, private val presenter: PreferencesRootPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
public interface Callback : Plugin { interface Callback : Plugin {
fun onOpenBugReport() fun onOpenBugReport()
} }
private val presenterConnector = presenterConnector(presenter) private val presenterConnector = presenterConnector(presenter)
private fun onLogoutClicked() {
presenterConnector.emitEvent(PreferencesRootEvents.Logout)
}
private fun onRageshakeEnabledChanged(isEnabled: Boolean) {
presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeEnabled(isEnabled))
}
private fun onRageshakeSensitivityChanged(sensitivity: Float) {
presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeSensitivity(sensitivity))
}
private fun onOpenBugReport() { private fun onOpenBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() } plugins<Callback>().forEach { it.onOpenBugReport() }
} }
@ -48,10 +36,7 @@ class PreferencesRootNode @AssistedInject constructor(
val state by presenterConnector.stateFlow.collectAsState() val state by presenterConnector.stateFlow.collectAsState()
PreferencesRootView( PreferencesRootView(
state = state, state = state,
onLogoutClicked = this::onLogoutClicked,
onBackPressed = this::navigateUp, onBackPressed = this::navigateUp,
onRageshakeEnabledChanged = this::onRageshakeEnabledChanged,
onRageshakeSensitivityChanged = this::onRageshakeSensitivityChanged,
onOpenRageShake = this::onOpenBugReport onOpenRageShake = this::onOpenBugReport
) )
} }

View file

@ -1,42 +1,26 @@
package io.element.android.x.features.preferences.root package io.element.android.x.features.preferences.root
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import io.element.android.x.architecture.Async import io.element.android.x.architecture.Async
import io.element.android.x.architecture.Presenter 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.logout.LogoutPreferencePresenter
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject import javax.inject.Inject
class PreferencesRootPresenter @Inject constructor( class PreferencesRootPresenter @Inject constructor(
private val logoutPresenter: LogoutPreferencePresenter, private val logoutPresenter: LogoutPreferencePresenter,
private val rageshakePresenter: RageshakePreferencesPresenter, private val rageshakePresenter: RageshakePreferencesPresenter,
) : Presenter<PreferencesRootState, PreferencesRootEvents> { ) : Presenter<PreferencesRootState> {
private val logoutEventsFlow = SharedFlowHolder<LogoutPreferenceEvents>()
private val rageshakeEventsFlow = SharedFlowHolder<RageshakePreferencesEvents>()
@Composable @Composable
override fun present(events: Flow<PreferencesRootEvents>): PreferencesRootState { override fun present(): PreferencesRootState {
val logoutState = logoutPresenter.present(events = logoutEventsFlow.asSharedFlow()) val logoutState = logoutPresenter.present()
val rageshakeState = rageshakePresenter.present(events = rageshakeEventsFlow.asSharedFlow()) val rageshakeState = rageshakePresenter.present()
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( return PreferencesRootState(
logoutState = logoutState, logoutState = logoutState,
rageshakeState = rageshakeState, rageshakeState = rageshakeState,
myUser = Async.Uninitialized myUser = Async.Uninitialized,
) )
} }
} }

View file

@ -10,6 +10,7 @@ import io.element.android.x.element.resources.R
import io.element.android.x.features.logout.LogoutPreferenceState import io.element.android.x.features.logout.LogoutPreferenceState
import io.element.android.x.features.logout.LogoutPreferenceView import io.element.android.x.features.logout.LogoutPreferenceView
import io.element.android.x.features.preferences.user.UserPreferences import io.element.android.x.features.preferences.user.UserPreferences
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesView import io.element.android.x.features.rageshake.preferences.RageshakePreferencesView
@ -18,10 +19,7 @@ fun PreferencesRootView(
state: PreferencesRootState, state: PreferencesRootState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {}, onBackPressed: () -> Unit = {},
onLogoutClicked: () -> Unit = {},
onOpenRageShake: () -> Unit = {}, onOpenRageShake: () -> Unit = {},
onRageshakeEnabledChanged: (Boolean) -> Unit = {},
onRageshakeSensitivityChanged: (Float) -> Unit = {},
) { ) {
// TODO Hierarchy! // TODO Hierarchy!
// Include pref from other modules // Include pref from other modules
@ -34,12 +32,9 @@ fun PreferencesRootView(
RageshakePreferencesView( RageshakePreferencesView(
state = state.rageshakeState, state = state.rageshakeState,
onOpenRageshake = onOpenRageShake, onOpenRageshake = onOpenRageShake,
onSensitivityChanged = onRageshakeSensitivityChanged,
onIsEnabledChanged = onRageshakeEnabledChanged,
) )
LogoutPreferenceView( LogoutPreferenceView(
state = state.logoutState, state = state.logoutState,
onLogoutClicked = onLogoutClicked,
) )
} }
} }

View file

@ -33,43 +33,12 @@ class BugReportNode @AssistedInject constructor(
BugReportView( BugReportView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onDescriptionChanged = this::onDescriptionChanged,
onSetSendLog = this::onSetSendLog,
onSetSendCrashLog = this::onSetSendCrashLog,
onSetCanContact = this::onSetCanContact,
onSetSendScreenshot = this::onSetSendScreenshot,
onSubmit = this::onSubmit,
onDone = this::onDone onDone = this::onDone
) )
} }
private fun onDone() { private fun onDone() {
presenterConnector.emitEvent(BugReportEvents.ResetAll)
plugins<Callback>().forEach { it.onBugReportSent() } plugins<Callback>().forEach { it.onBugReportSent() }
} }
private fun onSubmit() {
presenterConnector.emitEvent(BugReportEvents.SendBugReport)
}
private fun onSetSendLog(sendLog: Boolean) {
presenterConnector.emitEvent(BugReportEvents.SetSendLog(sendLog))
}
private fun onSetSendCrashLog(sendCrashLog: Boolean) {
presenterConnector.emitEvent(BugReportEvents.SetSendCrashLog(sendCrashLog))
}
private fun onSetSendScreenshot(sendScreenshot: Boolean) {
presenterConnector.emitEvent(BugReportEvents.SetSendScreenshot(sendScreenshot))
}
private fun onSetCanContact(canContact: Boolean) {
presenterConnector.emitEvent(BugReportEvents.SetCanContact(canContact))
}
private fun onDescriptionChanged(description: String) {
presenterConnector.emitEvent(BugReportEvents.SetDescription(description))
}
} }

View file

@ -1,10 +1,8 @@
package io.element.android.x.features.rageshake.bugreport package io.element.android.x.features.rageshake.bugreport
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue 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
@ -18,7 +16,6 @@ import io.element.android.x.features.rageshake.reporter.BugReporter
import io.element.android.x.features.rageshake.reporter.ReportType import io.element.android.x.features.rageshake.reporter.ReportType
import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -27,7 +24,7 @@ class BugReportPresenter @Inject constructor(
private val crashDataStore: CrashDataStore, private val crashDataStore: CrashDataStore,
private val screenshotHolder: ScreenshotHolder, private val screenshotHolder: ScreenshotHolder,
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
) : Presenter<BugReportState, BugReportEvents> { ) : Presenter<BugReportState> {
private class BugReporterUploadListener( private class BugReporterUploadListener(
private val sendingProgress: MutableState<Float>, private val sendingProgress: MutableState<Float>,
@ -56,7 +53,7 @@ class BugReportPresenter @Inject constructor(
} }
@Composable @Composable
override fun present(events: Flow<BugReportEvents>): BugReportState { override fun present(): BugReportState {
val screenshotUri = rememberSaveable { val screenshotUri = rememberSaveable {
mutableStateOf( mutableStateOf(
screenshotHolder.getFile()?.toUri()?.toString() screenshotHolder.getFile()?.toUri()?.toString()
@ -76,58 +73,54 @@ class BugReportPresenter @Inject constructor(
mutableStateOf(BugReportFormState.Default) mutableStateOf(BugReportFormState.Default)
} }
val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction) val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction)
val state by remember {
derivedStateOf { fun handleEvents(event: BugReportEvents) {
BugReportState( when (event) {
hasCrashLogs = crashInfo.isNotEmpty(), BugReportEvents.SendBugReport -> appCoroutineScope.sendBugReport(formState.value, crashInfo.isNotEmpty(), uploadListener)
sendingProgress = sendingProgress.value, BugReportEvents.ResetAll -> appCoroutineScope.resetAll()
sending = sendingAction.value, is BugReportEvents.SetDescription -> updateFormState(formState) {
formState = formState.value, copy(description = event.description)
screenshotUri = screenshotUri.value }
) is BugReportEvents.SetCanContact -> updateFormState(formState) {
} copy(canContact = event.canContact)
} }
LaunchedEffect(Unit) { is BugReportEvents.SetSendCrashLog -> updateFormState(formState) {
events.collect { event -> copy(sendCrashLogs = event.sendCrashlog)
when (event) { }
BugReportEvents.SendBugReport -> appCoroutineScope.sendBugReport(state, uploadListener) is BugReportEvents.SetSendLog -> updateFormState(formState) {
BugReportEvents.ResetAll -> appCoroutineScope.resetAll() copy(sendLogs = event.sendLog)
is BugReportEvents.SetDescription -> updateFormState(formState) { }
copy(description = event.description) is BugReportEvents.SetSendScreenshot -> updateFormState(formState) {
} copy(sendScreenshot = event.sendScreenshot)
is BugReportEvents.SetCanContact -> updateFormState(formState) {
copy(canContact = event.canContact)
}
is BugReportEvents.SetSendCrashLog -> updateFormState(formState) {
copy(sendCrashLogs = event.sendCrashlog)
}
is BugReportEvents.SetSendLog -> updateFormState(formState) {
copy(sendLogs = event.sendLog)
}
is BugReportEvents.SetSendScreenshot -> updateFormState(formState) {
copy(sendScreenshot = event.sendScreenshot)
}
} }
} }
} }
return state
return BugReportState(
hasCrashLogs = crashInfo.isNotEmpty(),
sendingProgress = sendingProgress.value,
sending = sendingAction.value,
formState = formState.value,
screenshotUri = screenshotUri.value,
eventSink = ::handleEvents
)
} }
private fun updateFormState(formState: MutableState<BugReportFormState>, operation: BugReportFormState.() -> BugReportFormState) { private fun updateFormState(formState: MutableState<BugReportFormState>, operation: BugReportFormState.() -> BugReportFormState) {
formState.value = operation(formState.value) formState.value = operation(formState.value)
} }
private fun CoroutineScope.sendBugReport(state: BugReportState, listener: BugReporter.IMXBugReportListener) = launch { private fun CoroutineScope.sendBugReport(formState: BugReportFormState, hasCrashLogs: Boolean, listener: BugReporter.IMXBugReportListener) = launch {
bugReporter.sendBugReport( bugReporter.sendBugReport(
coroutineScope = this, coroutineScope = this,
reportType = ReportType.BUG_REPORT, reportType = ReportType.BUG_REPORT,
withDevicesLogs = state.formState.sendLogs, withDevicesLogs = formState.sendLogs,
withCrashLogs = state.hasCrashLogs && state.formState.sendCrashLogs, withCrashLogs = hasCrashLogs && formState.sendCrashLogs,
withKeyRequestHistory = false, withKeyRequestHistory = false,
withScreenshot = state.formState.sendScreenshot, withScreenshot = formState.sendScreenshot,
theBugDescription = state.formState.description, theBugDescription = formState.description,
serverVersion = "", serverVersion = "",
canContact = state.formState.canContact, canContact = formState.canContact,
customFields = emptyMap(), customFields = emptyMap(),
listener = listener listener = listener
) )

View file

@ -26,6 +26,7 @@ data class BugReportState(
val screenshotUri: String? = null, val screenshotUri: String? = null,
val sendingProgress: Float = 0F, val sendingProgress: Float = 0F,
val sending: Async<Unit> = Async.Uninitialized, val sending: Async<Unit> = Async.Uninitialized,
val eventSink: (BugReportEvents) -> Unit = {}
) { ) {
val submitEnabled = val submitEnabled =
formState.description.length > 10 && sending !is Async.Loading formState.description.length > 10 && sending !is Async.Loading

View file

@ -14,7 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.x.features.rageshake.bugreport package io.element.android.x.features.rageshake.bugreport
@ -36,6 +35,7 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -59,22 +59,21 @@ import io.element.android.x.designsystem.components.LabelledCheckbox
import io.element.android.x.designsystem.components.dialogs.ErrorDialog import io.element.android.x.designsystem.components.dialogs.ErrorDialog
import io.element.android.x.element.resources.R as ElementR import io.element.android.x.element.resources.R as ElementR
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BugReportView( fun BugReportView(
state: BugReportState, state: BugReportState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onDescriptionChanged: (String) -> Unit = {},
onSetSendLog: (Boolean) -> Unit = {},
onSetSendCrashLog: (Boolean) -> Unit = {},
onSetCanContact: (Boolean) -> Unit = {},
onSetSendScreenshot: (Boolean) -> Unit = {},
onSubmit: () -> Unit = {},
onFailureDialogClosed: () -> Unit = { },
onDone: () -> Unit = { }, onDone: () -> Unit = { },
) { ) {
LogCompositions(tag = "Rageshake", msg = "Root") LogCompositions(tag = "Rageshake", msg = "Root")
val eventSink = state.eventSink
if (state.sending is Async.Success) { if (state.sending is Async.Success) {
onDone() LaunchedEffect(state.sending) {
eventSink(BugReportEvents.ResetAll)
onDone()
}
return
} }
Surface( Surface(
modifier = modifier, modifier = modifier,
@ -132,7 +131,7 @@ fun BugReportView(
}, },
onValueChange = { onValueChange = {
descriptionFieldState = it descriptionFieldState = it
onDescriptionChanged(it) eventSink(BugReportEvents.SetDescription(it))
}, },
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text, keyboardType = KeyboardType.Text,
@ -143,28 +142,28 @@ fun BugReportView(
} }
LabelledCheckbox( LabelledCheckbox(
checked = state.formState.sendLogs, checked = state.formState.sendLogs,
onCheckedChange = onSetSendLog, onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) },
enabled = isFormEnabled, enabled = isFormEnabled,
text = stringResource(id = ElementR.string.send_bug_report_include_logs) text = stringResource(id = ElementR.string.send_bug_report_include_logs)
) )
if (state.hasCrashLogs) { if (state.hasCrashLogs) {
LabelledCheckbox( LabelledCheckbox(
checked = state.formState.sendCrashLogs, checked = state.formState.sendCrashLogs,
onCheckedChange = onSetSendCrashLog, onCheckedChange = { eventSink(BugReportEvents.SetSendCrashLog(it)) },
enabled = isFormEnabled, enabled = isFormEnabled,
text = stringResource(id = ElementR.string.send_bug_report_include_crash_logs) text = stringResource(id = ElementR.string.send_bug_report_include_crash_logs)
) )
} }
LabelledCheckbox( LabelledCheckbox(
checked = state.formState.canContact, checked = state.formState.canContact,
onCheckedChange = onSetCanContact, onCheckedChange = { eventSink(BugReportEvents.SetCanContact(it)) },
enabled = isFormEnabled, enabled = isFormEnabled,
text = stringResource(id = ElementR.string.you_may_contact_me) text = stringResource(id = ElementR.string.you_may_contact_me)
) )
if (state.screenshotUri != null) { if (state.screenshotUri != null) {
LabelledCheckbox( LabelledCheckbox(
checked = state.formState.sendScreenshot, checked = state.formState.sendScreenshot,
onCheckedChange = onSetSendScreenshot, onCheckedChange = { eventSink(BugReportEvents.SetSendScreenshot(it)) },
enabled = isFormEnabled, enabled = isFormEnabled,
text = stringResource(id = ElementR.string.send_bug_report_include_screenshot) text = stringResource(id = ElementR.string.send_bug_report_include_screenshot)
) )
@ -187,7 +186,7 @@ fun BugReportView(
} }
// Submit // Submit
Button( Button(
onClick = onSubmit, onClick = { eventSink(BugReportEvents.SendBugReport) },
enabled = state.submitEnabled, enabled = state.submitEnabled,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -197,7 +196,6 @@ fun BugReportView(
} }
} }
when (state.sending) { when (state.sending) {
Async.Uninitialized -> Unit
is Async.Loading -> { is Async.Loading -> {
CircularProgressIndicator( CircularProgressIndicator(
progress = state.sendingProgress, progress = state.sendingProgress,
@ -206,9 +204,8 @@ fun BugReportView(
} }
is Async.Failure -> ErrorDialog( is Async.Failure -> ErrorDialog(
content = state.sending.error.toString(), content = state.sending.error.toString(),
onDismiss = onFailureDialogClosed,
) )
is Async.Success -> onDone() else -> Unit
} }
} }
} }

View file

@ -1,30 +1,31 @@
package io.element.android.x.features.rageshake.crash.ui package io.element.android.x.features.rageshake.crash.ui
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.x.architecture.Presenter import io.element.android.x.architecture.Presenter
import io.element.android.x.features.rageshake.crash.CrashDataStore import io.element.android.x.features.rageshake.crash.CrashDataStore
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class CrashDetectionPresenter @Inject constructor(private val crashDataStore: CrashDataStore) : Presenter<CrashDetectionState, CrashDetectionEvents> { class CrashDetectionPresenter @Inject constructor(private val crashDataStore: CrashDataStore) : Presenter<CrashDetectionState> {
@Composable @Composable
override fun present(events: Flow<CrashDetectionEvents>): CrashDetectionState { override fun present(): CrashDetectionState {
val localCoroutineScope = rememberCoroutineScope()
val crashDetected = crashDataStore.appHasCrashed().collectAsState(initial = false) val crashDetected = crashDataStore.appHasCrashed().collectAsState(initial = false)
LaunchedEffect(Unit) {
events.collect { event -> fun handleEvents(event: CrashDetectionEvents) {
when (event) { when (event) {
CrashDetectionEvents.ResetAllCrashData -> resetAll() CrashDetectionEvents.ResetAllCrashData -> localCoroutineScope.resetAll()
CrashDetectionEvents.ResetAppHasCrashed -> resetAppHasCrashed() CrashDetectionEvents.ResetAppHasCrashed -> localCoroutineScope.resetAppHasCrashed()
}
} }
} }
return CrashDetectionState( return CrashDetectionState(
crashDetected = crashDetected.value crashDetected = crashDetected.value,
eventSink = ::handleEvents
) )
} }

View file

@ -28,15 +28,19 @@ import io.element.android.x.element.resources.R as ElementR
fun CrashDetectionView( fun CrashDetectionView(
state: CrashDetectionState, state: CrashDetectionState,
onOpenBugReport: () -> Unit = { }, onOpenBugReport: () -> Unit = { },
onPopupDismissed: () -> Unit = {}
) { ) {
LogCompositions(tag = "Crash", msg = "CrashDetectionScreen") LogCompositions(tag = "Crash", msg = "CrashDetectionScreen")
fun onPopupDismissed(){
state.eventSink(CrashDetectionEvents.ResetAllCrashData)
}
if (state.crashDetected) { if (state.crashDetected) {
CrashDetectionContent( CrashDetectionContent(
state, state,
onYesClicked = onOpenBugReport, onYesClicked = onOpenBugReport,
onNoClicked = onPopupDismissed, onNoClicked = ::onPopupDismissed,
onDismiss = onPopupDismissed, onDismiss = ::onPopupDismissed,
) )
} }
} }

View file

@ -18,4 +18,5 @@ package io.element.android.x.features.rageshake.crash.ui
data class CrashDetectionState( data class CrashDetectionState(
val crashDetected: Boolean = false, val crashDetected: Boolean = false,
val eventSink: (CrashDetectionEvents) -> Unit = {}
) )

View file

@ -3,19 +3,17 @@ package io.element.android.x.features.rageshake.detection
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Presenter import io.element.android.x.architecture.Presenter
import io.element.android.x.architecture.SharedFlowHolder
import io.element.android.x.core.screenshot.ImageResult import io.element.android.x.core.screenshot.ImageResult
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter
import io.element.android.x.features.rageshake.rageshake.RageShake import io.element.android.x.features.rageshake.rageshake.RageShake
import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -24,13 +22,12 @@ class RageshakeDetectionPresenter @Inject constructor(
private val screenshotHolder: ScreenshotHolder, private val screenshotHolder: ScreenshotHolder,
private val rageShake: RageShake, private val rageShake: RageShake,
private val preferencesPresenter: RageshakePreferencesPresenter, private val preferencesPresenter: RageshakePreferencesPresenter,
) : Presenter<RageshakeDetectionState, RageshakeDetectionEvents> { ) : Presenter<RageshakeDetectionState> {
private val preferencesEventsFlow = SharedFlowHolder<RageshakePreferencesEvents>()
@Composable @Composable
override fun present(events: Flow<RageshakeDetectionEvents>): RageshakeDetectionState { override fun present(): RageshakeDetectionState {
val preferencesState = preferencesPresenter.present(events = preferencesEventsFlow.asSharedFlow()) val localCoroutineScope = rememberCoroutineScope()
val preferencesState = preferencesPresenter.present()
val isStarted = rememberSaveable { val isStarted = rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
@ -40,33 +37,38 @@ class RageshakeDetectionPresenter @Inject constructor(
val showDialog = rememberSaveable { val showDialog = rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
fun handleEvents(event: RageshakeDetectionEvents) {
when (event) {
RageshakeDetectionEvents.Disable -> {
preferencesState.eventSink(RageshakePreferencesEvents.SetIsEnabled(false))
showDialog.value = false
}
RageshakeDetectionEvents.StartDetection -> isStarted.value = true
RageshakeDetectionEvents.StopDetection -> isStarted.value = false
is RageshakeDetectionEvents.ProcessScreenshot -> localCoroutineScope.processScreenshot(takeScreenshot, showDialog, event.imageResult)
RageshakeDetectionEvents.Dismiss -> showDialog.value = false
}
}
val state = remember(preferencesState, isStarted.value, takeScreenshot.value, showDialog.value) { val state = remember(preferencesState, isStarted.value, takeScreenshot.value, showDialog.value) {
RageshakeDetectionState( RageshakeDetectionState(
isStarted = isStarted.value, isStarted = isStarted.value,
takeScreenshot = takeScreenshot.value, takeScreenshot = takeScreenshot.value,
showDialog = showDialog.value, showDialog = showDialog.value,
preferenceState = preferencesState preferenceState = preferencesState,
eventSink = ::handleEvents
) )
} }
LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
RageshakeDetectionEvents.Disable -> preferencesEventsFlow.emit(RageshakePreferencesEvents.SetIsEnabled(false))
RageshakeDetectionEvents.StartDetection -> isStarted.value = true
RageshakeDetectionEvents.StopDetection -> isStarted.value = false
is RageshakeDetectionEvents.ProcessScreenshot -> processScreenshot(takeScreenshot, showDialog, event.imageResult)
RageshakeDetectionEvents.Dismiss -> showDialog.value = false
}
}
}
LaunchedEffect(preferencesState.sensitivity) { LaunchedEffect(preferencesState.sensitivity) {
rageShake.setSensitivity(preferencesState.sensitivity) rageShake.setSensitivity(preferencesState.sensitivity)
} }
val shouldStart = preferencesState.isEnabled && val shouldStart = preferencesState.isEnabled &&
preferencesState.isSupported && preferencesState.isSupported &&
isStarted.value && isStarted.value &&
!takeScreenshot.value && !takeScreenshot.value &&
!showDialog.value !showDialog.value
LaunchedEffect(shouldStart) { LaunchedEffect(shouldStart) {
handleRageShake(shouldStart, state, takeScreenshot) handleRageShake(shouldStart, state, takeScreenshot)

View file

@ -24,5 +24,6 @@ data class RageshakeDetectionState(
val takeScreenshot: Boolean = false, val takeScreenshot: Boolean = false,
val showDialog: Boolean = false, val showDialog: Boolean = false,
val isStarted: Boolean = false, val isStarted: Boolean = false,
val preferenceState: RageshakePreferencesState = RageshakePreferencesState() val preferenceState: RageshakePreferencesState = RageshakePreferencesState(),
val eventSink: (RageshakeDetectionEvents) -> Unit = {}
) )

View file

@ -22,7 +22,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
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 androidx.lifecycle.Lifecycle
import io.element.android.x.core.compose.LogCompositions import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.core.compose.OnLifecycleEvent
import io.element.android.x.core.hardware.vibrate import io.element.android.x.core.hardware.vibrate
import io.element.android.x.core.screenshot.ImageResult import io.element.android.x.core.screenshot.ImageResult
import io.element.android.x.core.screenshot.screenshot import io.element.android.x.core.screenshot.screenshot
@ -34,24 +36,28 @@ import io.element.android.x.element.resources.R as ElementR
fun RageshakeDetectionView( fun RageshakeDetectionView(
state: RageshakeDetectionState, state: RageshakeDetectionState,
onOpenBugReport: () -> Unit = { }, onOpenBugReport: () -> Unit = { },
onScreenshotTaken: (ImageResult) -> Unit = {},
onDisableClicked: () -> Unit = {},
onNoClicked: () -> Unit = {}
) { ) {
LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen") LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen")
val eventSink = state.eventSink
val context = LocalContext.current val context = LocalContext.current
OnLifecycleEvent { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> eventSink(RageshakeDetectionEvents.StartDetection)
Lifecycle.Event.ON_PAUSE -> eventSink(RageshakeDetectionEvents.StopDetection)
else -> Unit
}
}
when { when {
state.takeScreenshot -> TakeScreenshot( state.takeScreenshot -> TakeScreenshot(
onScreenshotTaken = onScreenshotTaken onScreenshotTaken = { eventSink(RageshakeDetectionEvents.ProcessScreenshot(it)) }
) )
state.showDialog -> { state.showDialog -> {
LaunchedEffect(key1 = "RS_diag") { LaunchedEffect(Unit) {
context.vibrate() context.vibrate()
} }
RageshakeDialogContent( RageshakeDialogContent(
state, onNoClicked = { eventSink(RageshakeDetectionEvents.Dismiss) },
onNoClicked = onNoClicked, onDisableClicked = { eventSink(RageshakeDetectionEvents.Disable) },
onDisableClicked = onDisableClicked,
onYesClicked = onOpenBugReport onYesClicked = onOpenBugReport
) )
} }
@ -72,7 +78,6 @@ private fun TakeScreenshot(
@Composable @Composable
fun RageshakeDialogContent( fun RageshakeDialogContent(
state: RageshakeDetectionState,
onNoClicked: () -> Unit = { }, onNoClicked: () -> Unit = { },
onDisableClicked: () -> Unit = { }, onDisableClicked: () -> Unit = { },
onYesClicked: () -> Unit = { }, onYesClicked: () -> Unit = { },
@ -83,6 +88,7 @@ fun RageshakeDialogContent(
thirdButtonText = stringResource(id = ElementR.string.action_disable), thirdButtonText = stringResource(id = ElementR.string.action_disable),
submitText = stringResource(id = ElementR.string.yes), submitText = stringResource(id = ElementR.string.yes),
cancelText = stringResource(id = ElementR.string.no), cancelText = stringResource(id = ElementR.string.no),
onCancelClicked = onNoClicked,
onThirdButtonClicked = onDisableClicked, onThirdButtonClicked = onDisableClicked,
onSubmitClicked = onYesClicked, onSubmitClicked = onYesClicked,
onDismiss = onNoClicked, onDismiss = onNoClicked,
@ -93,8 +99,6 @@ fun RageshakeDialogContent(
@Composable @Composable
fun RageshakeDialogContentPreview() { fun RageshakeDialogContentPreview() {
ElementXTheme { ElementXTheme {
RageshakeDialogContent( RageshakeDialogContent()
state = RageshakeDetectionState()
)
} }
} }

View file

@ -1,26 +1,26 @@
package io.element.android.x.features.rageshake.preferences package io.element.android.x.features.rageshake.preferences
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Presenter import io.element.android.x.architecture.Presenter
import io.element.android.x.features.rageshake.rageshake.RageShake import io.element.android.x.features.rageshake.rageshake.RageShake
import io.element.android.x.features.rageshake.rageshake.RageshakeDataStore import io.element.android.x.features.rageshake.rageshake.RageshakeDataStore
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class RageshakePreferencesPresenter @Inject constructor( class RageshakePreferencesPresenter @Inject constructor(
private val rageshake: RageShake, private val rageshake: RageShake,
private val rageshakeDataStore: RageshakeDataStore, private val rageshakeDataStore: RageshakeDataStore,
) : Presenter<RageshakePreferencesState, RageshakePreferencesEvents> { ) : Presenter<RageshakePreferencesState> {
@Composable @Composable
override fun present(events: Flow<RageshakePreferencesEvents>): RageshakePreferencesState { override fun present(): RageshakePreferencesState {
val localCoroutineScope = rememberCoroutineScope()
val isSupported: MutableState<Boolean> = rememberSaveable { val isSupported: MutableState<Boolean> = rememberSaveable {
mutableStateOf(rageshake.isAvailable()) mutableStateOf(rageshake.isAvailable())
} }
@ -32,19 +32,18 @@ class RageshakePreferencesPresenter @Inject constructor(
.sensitivity() .sensitivity()
.collectAsState(initial = 0f) .collectAsState(initial = 0f)
LaunchedEffect(Unit) { fun handleEvents(event: RageshakePreferencesEvents) {
events.collect { event -> when (event) {
when (event) { is RageshakePreferencesEvents.SetIsEnabled -> localCoroutineScope.setIsEnabled(event.isEnabled)
is RageshakePreferencesEvents.SetIsEnabled -> setIsEnabled(event.isEnabled) is RageshakePreferencesEvents.SetSensitivity -> localCoroutineScope.setSensitivity(event.sensitivity)
is RageshakePreferencesEvents.SetSensitivity -> setSensitivity(event.sensitivity)
}
} }
} }
return RageshakePreferencesState( return RageshakePreferencesState(
isEnabled = isEnabled.value, isEnabled = isEnabled.value,
isSupported = isSupported.value, isSupported = isSupported.value,
sensitivity = sensitivity.value sensitivity = sensitivity.value,
eventSink = ::handleEvents
) )
} }

View file

@ -4,4 +4,5 @@ data class RageshakePreferencesState(
val isEnabled: Boolean = false, val isEnabled: Boolean = false,
val isSupported: Boolean = true, val isSupported: Boolean = true,
val sensitivity: Float = 0.3f, val sensitivity: Float = 0.3f,
val eventSink: (RageshakePreferencesEvents) -> Unit = {},
) )

View file

@ -34,9 +34,16 @@ fun RageshakePreferencesView(
state: RageshakePreferencesState, state: RageshakePreferencesState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onOpenRageshake: () -> Unit = {}, onOpenRageshake: () -> Unit = {},
onIsEnabledChanged: (Boolean) -> Unit = {},
onSensitivityChanged: (Float) -> Unit = {}
) { ) {
fun onSensitivityChanged(sensitivity: Float){
state.eventSink(RageshakePreferencesEvents.SetSensitivity(sensitivity = sensitivity))
}
fun onEnabledChanged(isEnabled: Boolean){
state.eventSink(RageshakePreferencesEvents.SetIsEnabled(isEnabled = isEnabled))
}
Column(modifier = modifier) { Column(modifier = modifier) {
PreferenceCategory(title = stringResource(id = ElementR.string.send_bug_report)) { PreferenceCategory(title = stringResource(id = ElementR.string.send_bug_report)) {
PreferenceText( PreferenceText(
@ -45,12 +52,13 @@ fun RageshakePreferencesView(
onClick = onOpenRageshake onClick = onOpenRageshake
) )
} }
PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) { PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) {
if (state.isSupported) { if (state.isSupported) {
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 = onIsEnabledChanged onCheckedChange = ::onEnabledChanged
) )
PreferenceSlide( PreferenceSlide(
title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold), title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold),
@ -58,7 +66,7 @@ fun RageshakePreferencesView(
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 = onSensitivityChanged onValueChange = ::onSensitivityChanged
) )
} else { } else {
PreferenceText(title = "Rageshaking is not supported by your device") PreferenceText(title = "Rageshaking is not supported by your device")

View file

@ -30,18 +30,6 @@ class RoomListNode @AssistedInject constructor(
private val connector = presenterConnector(presenter) private val connector = presenterConnector(presenter)
private fun updateFilter(filter: String) {
connector.emitEvent(RoomListEvents.UpdateFilter(filter))
}
private fun updateVisibleRange(range: IntRange) {
connector.emitEvent((RoomListEvents.UpdateVisibleRange(range)))
}
private fun logout() {
connector.emitEvent(RoomListEvents.Logout)
}
private fun onRoomClicked(roomId: RoomId) { private fun onRoomClicked(roomId: RoomId) {
plugins<Callback>().forEach { it.onRoomClicked(roomId) } plugins<Callback>().forEach { it.onRoomClicked(roomId) }
} }
@ -56,8 +44,6 @@ class RoomListNode @AssistedInject constructor(
RoomListView( RoomListView(
state = state, state = state,
onRoomClicked = this::onRoomClicked, onRoomClicked = this::onRoomClicked,
onFilterChanged = this::updateFilter,
onScrollOver = this::updateVisibleRange,
onOpenSettings = this::onOpenSettings onOpenSettings = this::onOpenSettings
) )
} }

View file

@ -7,8 +7,10 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.x.architecture.Presenter
import io.element.android.x.core.coroutine.parallelMap import io.element.android.x.core.coroutine.parallelMap
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize import io.element.android.x.designsystem.components.avatar.AvatarSize
@ -19,13 +21,12 @@ import io.element.android.x.features.roomlist.model.RoomListState
import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.RoomSummary import io.element.android.x.matrix.room.RoomSummary
import io.element.android.x.architecture.Presenter
import io.element.android.x.matrix.ui.model.MatrixUser import io.element.android.x.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
private const val extendedRangeSize = 40 private const val extendedRangeSize = 40
@ -33,10 +34,10 @@ private const val extendedRangeSize = 40
class RoomListPresenter @Inject constructor( class RoomListPresenter @Inject constructor(
private val client: MatrixClient, private val client: MatrixClient,
private val lastMessageFormatter: LastMessageFormatter, private val lastMessageFormatter: LastMessageFormatter,
) : Presenter<RoomListState, RoomListEvents> { ) : Presenter<RoomListState> {
@Composable @Composable
override fun present(events: Flow<RoomListEvents>): RoomListState { override fun present(): RoomListState {
val matrixUser: MutableState<MatrixUser?> = remember { val matrixUser: MutableState<MatrixUser?> = remember {
mutableStateOf(null) mutableStateOf(null)
} }
@ -52,14 +53,15 @@ class RoomListPresenter @Inject constructor(
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
initialLoad(matrixUser) initialLoad(matrixUser)
events.collect { event -> }
when (event) {
RoomListEvents.Logout -> logout(isLoginOut) fun handleEvents(event: RoomListEvents) {
is RoomListEvents.UpdateFilter -> filter = event.newFilter when (event) {
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range) is RoomListEvents.UpdateFilter -> filter = event.newFilter
} is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
} }
} }
LaunchedEffect(roomSummaries, filter) { LaunchedEffect(roomSummaries, filter) {
filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter) filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter)
} }
@ -67,7 +69,8 @@ class RoomListPresenter @Inject constructor(
matrixUser = matrixUser.value, matrixUser = matrixUser.value,
roomList = filteredRoomSummaries.value, roomList = filteredRoomSummaries.value,
filter = filter, filter = filter,
isLoginOut = isLoginOut.value isLoginOut = isLoginOut.value,
eventSink = ::handleEvents
) )
} }
@ -83,7 +86,7 @@ class RoomListPresenter @Inject constructor(
}.toImmutableList() }.toImmutableList()
} }
private suspend fun initialLoad(matrixUser: MutableState<MatrixUser?>) { private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
val userDisplayName = client.loadUserDisplayName().getOrNull() val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData = val avatarData =
@ -100,13 +103,6 @@ class RoomListPresenter @Inject constructor(
) )
} }
private suspend fun logout(isLoginOut: MutableState<Boolean>) {
isLoginOut.value = true
delay(2000)
client.logout()
isLoginOut.value = false
}
private fun updateVisibleRange(range: IntRange) { private fun updateVisibleRange(range: IntRange) {
if (range.isEmpty()) return if (range.isEmpty()) return
val midExtendedRangeSize = extendedRangeSize / 2 val midExtendedRangeSize = extendedRangeSize / 2

View file

@ -41,6 +41,7 @@ import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.roomlist.components.RoomListTopBar import io.element.android.x.features.roomlist.components.RoomListTopBar
import io.element.android.x.features.roomlist.components.RoomSummaryRow import io.element.android.x.features.roomlist.components.RoomSummaryRow
import io.element.android.x.features.roomlist.model.RoomListEvents
import io.element.android.x.features.roomlist.model.RoomListRoomSummary import io.element.android.x.features.roomlist.model.RoomListRoomSummary
import io.element.android.x.features.roomlist.model.RoomListState import io.element.android.x.features.roomlist.model.RoomListState
import io.element.android.x.features.roomlist.model.stubbedRoomSummaries import io.element.android.x.features.roomlist.model.stubbedRoomSummaries
@ -54,19 +55,26 @@ fun RoomListView(
state: RoomListState, state: RoomListState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onRoomClicked: (RoomId) -> Unit = {}, onRoomClicked: (RoomId) -> Unit = {},
onFilterChanged: (String) -> Unit = {},
onOpenSettings: () -> Unit = {}, onOpenSettings: () -> Unit = {},
onScrollOver: (IntRange) -> Unit = {},
) { ) {
fun onFilterChanged(filter: String){
state.eventSink(RoomListEvents.UpdateFilter(filter))
}
fun onVisibleRangedChanged(range: IntRange){
state.eventSink(RoomListEvents.UpdateVisibleRange(range))
}
RoomListView( RoomListView(
roomSummaries = state.roomList, roomSummaries = state.roomList,
matrixUser = state.matrixUser, matrixUser = state.matrixUser,
filter = state.filter, filter = state.filter,
modifier = modifier, modifier = modifier,
onRoomClicked = onRoomClicked, onRoomClicked = onRoomClicked,
onFilterChanged = onFilterChanged, onFilterChanged = ::onFilterChanged,
onOpenSettings = onOpenSettings, onOpenSettings = onOpenSettings,
onScrollOver = onScrollOver, onScrollOver = ::onVisibleRangedChanged,
) )
} }

View file

@ -1,7 +1,6 @@
package io.element.android.x.features.roomlist.model package io.element.android.x.features.roomlist.model
sealed interface RoomListEvents { sealed interface RoomListEvents {
object Logout : RoomListEvents
data class UpdateFilter(val newFilter: String) : RoomListEvents data class UpdateFilter(val newFilter: String) : RoomListEvents
data class UpdateVisibleRange(val range: IntRange): RoomListEvents data class UpdateVisibleRange(val range: IntRange): RoomListEvents
} }

View file

@ -10,4 +10,5 @@ data class RoomListState(
val roomList: ImmutableList<RoomListRoomSummary>, val roomList: ImmutableList<RoomListRoomSummary>,
val filter: String, val filter: String,
val isLoginOut: Boolean, val isLoginOut: Boolean,
val eventSink: (RoomListEvents) -> Unit = {}
) )

View file

@ -3,7 +3,7 @@ package io.element.android.x.architecture
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface Presenter<State, Event> { interface Presenter<State> {
@Composable @Composable
fun present(events: Flow<Event>): State fun present(): State
} }

View file

@ -8,19 +8,14 @@ import app.cash.molecule.launchMolecule
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
inline fun <reified State, reified Event> LifecycleOwner.presenterConnector(presenter: Presenter<State, Event>): LifecyclePresenterConnector<State, Event> = inline fun <reified State> LifecycleOwner.presenterConnector(presenter: Presenter<State>): LifecyclePresenterConnector<State> =
LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter) LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter)
class LifecyclePresenterConnector<State, Event>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State, Event>) { class LifecyclePresenterConnector<State>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State>) {
private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main) private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main)
private val eventFlow = SharedFlowHolder<Event>()
val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.Immediate) { val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.Immediate) {
presenter.present(events = eventFlow.asSharedFlow()) presenter.present()
}
fun emitEvent(event: Event) {
eventFlow.emit(event)
} }
} }

View file

@ -9,4 +9,6 @@ class SharedFlowHolder<Data>(capacity: Int = 64) {
fun asSharedFlow() = mutableFlow.asSharedFlow() fun asSharedFlow() = mutableFlow.asSharedFlow()
fun emit(data: Data) = mutableFlow.tryEmit(data) fun emit(data: Data) = mutableFlow.tryEmit(data)
suspend fun awaitEmit(data: Data) = mutableFlow.emit(data)
} }