Continue migrating BugReport/Rageshake/Crash screens

This commit is contained in:
ganfra
2023-01-10 21:18:16 +01:00
parent 6d2e5edc82
commit 56e54bb172
21 changed files with 366 additions and 80 deletions

View File

@@ -49,7 +49,8 @@ class MainActivity : NodeComponentActivity() {
buildContext = it,
appComponentOwner = applicationContext as DaggerComponentOwner,
matrix = appBindings.matrix(),
sessionComponentsOwner = appBindings.sessionComponentsOwner()
sessionComponentsOwner = appBindings.sessionComponentsOwner(),
rootPresenter = appBindings.rootPresenter()
)
}
}

View File

@@ -19,11 +19,13 @@ package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo
import io.element.android.x.matrix.Matrix
import io.element.android.x.matrix.ui.MatrixUi
import io.element.android.x.root.RootPresenter
import kotlinx.coroutines.CoroutineScope
@ContributesTo(AppScope::class)
interface AppBindings {
fun coroutineScope(): CoroutineScope
fun rootPresenter(): RootPresenter
fun matrix(): Matrix
fun matrixUi(): MatrixUi
fun sessionComponentsOwner(): SessionComponentsOwner

View File

@@ -22,6 +22,7 @@ import kotlinx.parcelize.Parcelize
class LoggedInFlowNode(
buildContext: BuildContext,
val sessionId: SessionId,
private val onOpenBugReport: () -> Unit,
private val backstack: BackStack<NavTarget> = BackStack(
initialElement = NavTarget.RoomList,
savedStateMap = buildContext.savedStateMap,
@@ -64,7 +65,7 @@ class LoggedInFlowNode(
)
}
NavTarget.Settings -> {
PreferencesFlowNode(buildContext)
PreferencesFlowNode(buildContext, onOpenBugReport)
}
}
}

View File

@@ -5,17 +5,12 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.airbnb.android.showkase.models.Showkase
import com.bumble.appyx.core.children.whenChildAttached
import com.bumble.appyx.core.clienthelper.interactor.Interactor
import com.bumble.appyx.core.composable.Children
@@ -26,13 +21,19 @@ import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.node.node
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import io.element.android.x.BuildConfig
import io.element.android.x.component.ShowkaseButton
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.x.architecture.createNode
import io.element.android.x.architecture.presenterConnector
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.getBrowserIntent
import io.element.android.x.features.rageshake.bugreport.BugReportNode
import io.element.android.x.matrix.Matrix
import io.element.android.x.matrix.core.SessionId
import io.element.android.x.root.RootEvents
import io.element.android.x.root.RootPresenter
import io.element.android.x.root.RootView
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -64,6 +65,7 @@ class RootFlowNode(
private val appComponentOwner: DaggerComponentOwner,
private val matrix: Matrix,
private val sessionComponentsOwner: SessionComponentsOwner,
rootPresenter: RootPresenter
) :
ParentNode<RootFlowNode.NavTarget>(
navModel = backstack,
@@ -73,10 +75,20 @@ class RootFlowNode(
DaggerComponentOwner by appComponentOwner {
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") }
)
}
@@ -85,7 +97,7 @@ class RootFlowNode(
matrix.isLoggedIn()
.distinctUntilChanged()
.onEach { isLoggedIn ->
Timber.v("IsLoggedIn")
Timber.v("isLoggedIn=$isLoggedIn")
if (isLoggedIn) {
val matrixClient = matrix.restoreSession()
if (matrixClient == null) {
@@ -102,43 +114,50 @@ class RootFlowNode(
.launchIn(lifecycleScope)
}
private fun hideShowkaseButton() {
presenterConnector.emitEvent(RootEvents.HideShowkaseButton)
}
private fun onOpenBugReport() {
presenterConnector.emitEvent(RootEvents.ResetAppHasCrashed)
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
override fun View(modifier: Modifier) {
var isShowkaseButtonVisible by remember { mutableStateOf(BuildConfig.DEBUG) }
Box(
modifier = modifier
.fillMaxSize(),
contentAlignment = Alignment.TopCenter,
val state by presenterConnector.stateFlow.collectAsState()
RootView(
state = state,
onHideShowkaseClicked = this::hideShowkaseButton,
onOpenBugReport = this::onOpenBugReport,
onCrashDetectedDismissed = this::onCrashDetectedDismissed,
onDisableRageshake = this::onDisableRageshake,
onDismissRageshake = this::onDismissRageshake,
onScreenshotTaken = this::onScreenshotTaken
) {
Children(navModel = backstack)
val context = LocalContext.current
ShowkaseButton(
isVisible = isShowkaseButtonVisible,
onCloseClicked = { isShowkaseButtonVisible = false },
onClick = { startActivity(context, Showkase.getBrowserIntent(context), null) }
)
}
}
/*
var isBugReportVisible by rememberSaveable {
mutableStateOf(false)
}
RageshakeDetectionScreen(
onOpenBugReport = {
isBugReportVisible = true
}
)
CrashDetectionScreen(
onOpenBugReport = {
isBugReportVisible = true
}
)
if (isBugReportVisible) {
// TODO Improve the navigation, when pressing back here, it closes the app.
BugReportScreen(
onDone = { isBugReportVisible = false }
)
}
*/
private fun onScreenshotTaken(imageResult: ImageResult) {
presenterConnector.emitEvent(RootEvents.ProcessScreenshot(imageResult))
}
private val bugReportNodeCallback = object : BugReportNode.Callback {
override fun onBugReportSent() {
backstack.pop()
}
}
@@ -151,12 +170,19 @@ class RootFlowNode(
@Parcelize
data class LoggedInFlow(val sessionId: SessionId) : NavTarget
@Parcelize
object BugReport : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
is NavTarget.LoggedInFlow -> {
LoggedInFlowNode(buildContext, navTarget.sessionId)
LoggedInFlowNode(
buildContext = buildContext,
sessionId = navTarget.sessionId,
onOpenBugReport = this::onOpenBugReport
)
}
NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext)
NavTarget.SplashScreen -> node(buildContext) {
@@ -164,6 +190,7 @@ class RootFlowNode(
CircularProgressIndicator()
}
}
NavTarget.BugReport -> createNode<BugReportNode>(buildContext, plugins = listOf(bugReportNodeCallback))
}
}
}

View File

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

View File

@@ -0,0 +1,62 @@
package io.element.android.x.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
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.crash.ui.CrashDetectionEvents
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 kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class RootPresenter @Inject constructor(
private val bugReportPresenter: BugReportPresenter,
private val crashDetectionPresenter: CrashDetectionPresenter,
private val rageshakeDetectionPresenter: RageshakeDetectionPresenter,
) : Presenter<RootState, RootEvents> {
private val rageshakeDetectionEventsFlow = SharedFlowHolder<RageshakeDetectionEvents>()
private val bugReporterEventsFlow = SharedFlowHolder<BugReportEvents>()
private val crashDetectionEventsFlow = SharedFlowHolder<CrashDetectionEvents>()
@Composable
override fun present(events: Flow<RootEvents>): RootState {
val isBugReportVisible = rememberSaveable {
mutableStateOf(false)
}
val isShowkaseButtonVisible = rememberSaveable {
mutableStateOf(true)
}
val rageshakeDetectionState = rageshakeDetectionPresenter.present(events = rageshakeDetectionEventsFlow.asSharedFlow())
val crashDetectionState = crashDetectionPresenter.present(events = crashDetectionEventsFlow.asSharedFlow())
val bugReportState = bugReportPresenter.present(events = bugReporterEventsFlow.asSharedFlow())
LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
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(
isBugReportVisible = isBugReportVisible.value,
isShowkaseButtonVisible = isShowkaseButtonVisible.value,
rageshakeDetectionState = rageshakeDetectionState,
crashDetectionState = crashDetectionState,
bugReportState = bugReportState
)
}
}

View File

@@ -0,0 +1,15 @@
package io.element.android.x.root
import androidx.compose.runtime.Stable
import io.element.android.x.features.rageshake.bugreport.BugReportState
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionState
import io.element.android.x.features.rageshake.detection.RageshakeDetectionState
@Stable
data class RootState(
val isBugReportVisible: Boolean,
val isShowkaseButtonVisible: Boolean,
val rageshakeDetectionState: RageshakeDetectionState,
val crashDetectionState: CrashDetectionState,
val bugReportState: BugReportState
)

View File

@@ -0,0 +1,55 @@
package io.element.android.x.root
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import com.airbnb.android.showkase.models.Showkase
import io.element.android.x.component.ShowkaseButton
import io.element.android.x.core.screenshot.ImageResult
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionView
import io.element.android.x.features.rageshake.detection.RageshakeDetectionView
import io.element.android.x.getBrowserIntent
@Composable
fun RootView(
state: RootState,
modifier: Modifier = Modifier,
onHideShowkaseClicked: () -> Unit = { },
onOpenBugReport: () -> Unit = {},
onCrashDetectedDismissed: () -> Unit = {},
onDisableRageshake: () -> Unit = {},
onDismissRageshake: () -> Unit = {},
onScreenshotTaken: (ImageResult) -> Unit = {},
children: @Composable BoxScope.() -> Unit,
) {
Box(
modifier = modifier
.fillMaxSize(),
contentAlignment = Alignment.TopCenter,
) {
children()
val context = LocalContext.current
ShowkaseButton(
isVisible = state.isShowkaseButtonVisible,
onCloseClicked = onHideShowkaseClicked,
onClick = { ContextCompat.startActivity(context, Showkase.getBrowserIntent(context), null) }
)
RageshakeDetectionView(
state = state.rageshakeDetectionState,
onOpenBugReport = onOpenBugReport,
onDisableClicked = onDisableRageshake,
onNoClicked = onDismissRageshake,
onScreenshotTaken = onScreenshotTaken
)
CrashDetectionView(
state = state.crashDetectionState,
onOpenBugReport = onOpenBugReport,
onPopupDismissed = onCrashDetectedDismissed
)
}
}

View File

@@ -14,6 +14,7 @@ import kotlinx.parcelize.Parcelize
class PreferencesFlowNode(
buildContext: BuildContext,
private val onOpenBugReport: () -> Unit,
private val backstack: BackStack<NavTarget> = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
@@ -23,6 +24,12 @@ class PreferencesFlowNode(
buildContext = buildContext
) {
private val preferencesRootNodeCallback = object : PreferencesRootNode.Callback {
override fun onOpenBugReport() {
onOpenBugReport.invoke()
}
}
sealed interface NavTarget : Parcelable {
@Parcelize
object Root : NavTarget
@@ -30,7 +37,7 @@ class PreferencesFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Root -> createNode<PreferencesRootNode>(buildContext)
NavTarget.Root -> createNode<PreferencesRootNode>(buildContext, plugins = listOf(preferencesRootNodeCallback))
}
}

View File

@@ -7,6 +7,7 @@ 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 com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesNode
@@ -20,6 +21,10 @@ class PreferencesRootNode @AssistedInject constructor(
private val presenter: PreferencesRootPresenter,
) : Node(buildContext, plugins = plugins) {
public interface Callback : Plugin {
fun onOpenBugReport()
}
private val presenterConnector = presenterConnector(presenter)
private fun onLogoutClicked() {
@@ -34,6 +39,10 @@ class PreferencesRootNode @AssistedInject constructor(
presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeSensitivity(sensitivity))
}
private fun onOpenBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() }
}
@Composable
override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState()
@@ -42,7 +51,8 @@ class PreferencesRootNode @AssistedInject constructor(
onLogoutClicked = this::onLogoutClicked,
onBackPressed = this::navigateUp,
onRageshakeEnabledChanged = this::onRageshakeEnabledChanged,
onRageshakeSensitivityChanged = this::onRageshakeSensitivityChanged
onRageshakeSensitivityChanged = this::onRageshakeSensitivityChanged,
onOpenRageShake = this::onOpenBugReport
)
}
}

View File

@@ -0,0 +1,75 @@
package io.element.android.x.features.rageshake.bugreport
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 com.bumble.appyx.core.plugin.plugins
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.AppScope
@ContributesNode(AppScope::class)
class BugReportNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenter: BugReportPresenter,
) : Node(buildContext, plugins = plugins) {
private val presenterConnector = presenterConnector(presenter)
interface Callback : Plugin {
fun onBugReportSent()
}
@Composable
override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState()
BugReportView(
state = state,
modifier = modifier,
onDescriptionChanged = this::onDescriptionChanged,
onSetSendLog = this::onSetSendLog,
onSetSendCrashLog = this::onSetSendCrashLog,
onSetCanContact = this::onSetCanContact,
onSetSendScreenshot = this::onSetSendScreenshot,
onSubmit = this::onSubmit,
onDone = this::onDone
)
}
private fun onDone() {
presenterConnector.emitEvent(BugReportEvents.ResetAll)
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

@@ -4,10 +4,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.core.net.toUri
import io.element.android.x.architecture.Async
import io.element.android.x.architecture.Presenter
import io.element.android.x.features.rageshake.crash.CrashDataStore
@@ -31,6 +33,7 @@ class BugReportPresenter @Inject constructor(
private val sendingProgress: MutableState<Float>,
private val sendingAction: MutableState<Async<Unit>>
) : BugReporter.IMXBugReportListener {
override fun onUploadCancelled() {
sendingProgress.value = 0f
sendingAction.value = Async.Uninitialized
@@ -54,6 +57,11 @@ class BugReportPresenter @Inject constructor(
@Composable
override fun present(events: Flow<BugReportEvents>): BugReportState {
val screenshotUri = rememberSaveable {
mutableStateOf(
screenshotHolder.getFile()?.toUri()?.toString()
)
}
val crashInfo: String by crashDataStore
.crashInfo()
.collectAsState(initial = "")
@@ -64,15 +72,21 @@ class BugReportPresenter @Inject constructor(
val sendingAction: MutableState<Async<Unit>> = remember {
mutableStateOf(Async.Uninitialized)
}
val formState: MutableState<BugReportFormState> = rememberSaveable {
val formState: MutableState<BugReportFormState> = remember {
mutableStateOf(BugReportFormState.Default)
}
val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction)
val state = BugReportState(
hasCrashLogs = crashInfo.isNotEmpty(),
sendingProgress = sendingProgress.value,
sending = sendingAction.value
)
val state by remember {
derivedStateOf {
BugReportState(
hasCrashLogs = crashInfo.isNotEmpty(),
sendingProgress = sendingProgress.value,
sending = sendingAction.value,
formState = formState.value,
screenshotUri = screenshotUri.value
)
}
}
LaunchedEffect(Unit) {
events.collect { event ->
when (event) {

View File

@@ -38,8 +38,7 @@ data class BugReportFormState(
val sendCrashLogs: Boolean,
val canContact: Boolean,
val sendScreenshot: Boolean
): Parcelable {
) : Parcelable {
companion object {
val Default = BugReportFormState(
description = "",

View File

@@ -1,6 +1,6 @@
package io.element.android.x.features.rageshake.crash.ui
sealed interface CrashDetectionEvents {
object ResetAll : CrashDetectionEvents
object ResetAllCrashData : CrashDetectionEvents
object ResetAppHasCrashed : CrashDetectionEvents
}

View File

@@ -18,7 +18,7 @@ class CrashDetectionPresenter @Inject constructor(private val crashDataStore: Cr
LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
CrashDetectionEvents.ResetAll -> resetAll()
CrashDetectionEvents.ResetAllCrashData -> resetAll()
CrashDetectionEvents.ResetAppHasCrashed -> resetAppHasCrashed()
}
}
@@ -32,7 +32,7 @@ class CrashDetectionPresenter @Inject constructor(private val crashDataStore: Cr
crashDataStore.resetAppHasCrashed()
}
fun CoroutineScope.resetAll() = launch {
private fun CoroutineScope.resetAll() = launch {
crashDataStore.reset()
}
}

View File

@@ -13,7 +13,6 @@ 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.RageshakePreferencesPresenter
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.screenshot.ScreenshotHolder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -22,7 +21,6 @@ import timber.log.Timber
import javax.inject.Inject
class RageshakeDetectionPresenter @Inject constructor(
private val rageshakeDataStore: RageshakeDataStore,
private val screenshotHolder: ScreenshotHolder,
private val rageShake: RageShake,
private val preferencesPresenter: RageshakePreferencesPresenter,
@@ -42,12 +40,14 @@ class RageshakeDetectionPresenter @Inject constructor(
val showDialog = rememberSaveable {
mutableStateOf(false)
}
val state = RageshakeDetectionState(
isStarted = isStarted.value,
takeScreenshot = takeScreenshot.value,
showDialog = showDialog.value,
preferenceState = preferencesState
)
val state = remember(preferencesState, isStarted.value, takeScreenshot.value, showDialog.value) {
RageshakeDetectionState(
isStarted = isStarted.value,
takeScreenshot = takeScreenshot.value,
showDialog = showDialog.value,
preferenceState = preferencesState
)
}
LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
@@ -62,22 +62,18 @@ class RageshakeDetectionPresenter @Inject constructor(
LaunchedEffect(preferencesState.sensitivity) {
rageShake.setSensitivity(preferencesState.sensitivity)
}
val shouldStart = remember {
derivedStateOf {
preferencesState.isEnabled &&
val shouldStart = preferencesState.isEnabled &&
preferencesState.isSupported &&
isStarted.value &&
!takeScreenshot.value &&
!showDialog.value
}
}
LaunchedEffect(shouldStart) {
handleRageShake(shouldStart.value, state, takeScreenshot)
handleRageShake(shouldStart, state, takeScreenshot)
}
return state
}
private fun handleRageShake(start: Boolean, state: RageshakeDetectionState, takeScreenshot: MutableState<Boolean>) {
if (start) {
rageShake.start(state.preferenceState.sensitivity)

View File

@@ -16,8 +16,10 @@
package io.element.android.x.features.rageshake.detection
import androidx.compose.runtime.Stable
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState
@Stable
data class RageshakeDetectionState(
val takeScreenshot: Boolean = false,
val showDialog: Boolean = false,

View File

@@ -34,9 +34,9 @@ import io.element.android.x.element.resources.R as ElementR
fun RageshakeDetectionView(
state: RageshakeDetectionState,
onOpenBugReport: () -> Unit = { },
onScreenshotTaken: (ImageResult) -> Unit,
onDisableClicked: () -> Unit,
onNoClicked: () -> Unit
onScreenshotTaken: (ImageResult) -> Unit = {},
onDisableClicked: () -> Unit = {},
onNoClicked: () -> Unit = {}
) {
LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen")
val context = LocalContext.current
@@ -63,8 +63,10 @@ private fun TakeScreenshot(
onScreenshotTaken: (ImageResult) -> Unit = {}
) {
val view = LocalView.current
view.screenshot {
onScreenshotTaken(it)
LaunchedEffect(Unit) {
view.screenshot {
onScreenshotTaken(it)
}
}
}

View File

@@ -17,8 +17,7 @@ import javax.inject.Inject
class RageshakePreferencesPresenter @Inject constructor(
private val rageshake: RageShake,
private val rageshakeDataStore: RageshakeDataStore,
) : Presenter<RageshakePreferencesState, RageshakePreferencesEvents> {
) : Presenter<RageshakePreferencesState, RageshakePreferencesEvents> {
@Composable
override fun present(events: Flow<RageshakePreferencesEvents>): RageshakePreferencesState {

View File

@@ -19,10 +19,13 @@ package io.element.android.x.features.rageshake.screenshot
import android.content.Context
import android.graphics.Bitmap
import io.element.android.x.core.bitmap.writeBitmap
import io.element.android.x.di.AppScope
import io.element.android.x.di.ApplicationContext
import io.element.android.x.di.SingleIn
import java.io.File
import javax.inject.Inject
@SingleIn(AppScope::class)
class ScreenshotHolder @Inject constructor(
@ApplicationContext private val context: Context,
) {

View File

@@ -1,7 +1,9 @@
package io.element.android.x.architecture
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
@Stable
sealed interface Async<out T> {
object Uninitialized : Async<Nothing>
data class Loading<out T>(val prevState: T? = null) : Async<T>