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, buildContext = it,
appComponentOwner = applicationContext as DaggerComponentOwner, appComponentOwner = applicationContext as DaggerComponentOwner,
matrix = appBindings.matrix(), 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 com.squareup.anvil.annotations.ContributesTo
import io.element.android.x.matrix.Matrix import io.element.android.x.matrix.Matrix
import io.element.android.x.matrix.ui.MatrixUi import io.element.android.x.matrix.ui.MatrixUi
import io.element.android.x.root.RootPresenter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
interface AppBindings { interface AppBindings {
fun coroutineScope(): CoroutineScope fun coroutineScope(): CoroutineScope
fun rootPresenter(): RootPresenter
fun matrix(): Matrix fun matrix(): Matrix
fun matrixUi(): MatrixUi fun matrixUi(): MatrixUi
fun sessionComponentsOwner(): SessionComponentsOwner fun sessionComponentsOwner(): SessionComponentsOwner

View file

@ -22,6 +22,7 @@ import kotlinx.parcelize.Parcelize
class LoggedInFlowNode( class LoggedInFlowNode(
buildContext: BuildContext, buildContext: BuildContext,
val sessionId: SessionId, val sessionId: SessionId,
private val onOpenBugReport: () -> Unit,
private val backstack: BackStack<NavTarget> = BackStack( private val backstack: BackStack<NavTarget> = BackStack(
initialElement = NavTarget.RoomList, initialElement = NavTarget.RoomList,
savedStateMap = buildContext.savedStateMap, savedStateMap = buildContext.savedStateMap,
@ -64,7 +65,7 @@ class LoggedInFlowNode(
) )
} }
NavTarget.Settings -> { 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.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.airbnb.android.showkase.models.Showkase
import com.bumble.appyx.core.children.whenChildAttached import com.bumble.appyx.core.children.whenChildAttached
import com.bumble.appyx.core.clienthelper.interactor.Interactor import com.bumble.appyx.core.clienthelper.interactor.Interactor
import com.bumble.appyx.core.composable.Children 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.core.node.node
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.newRoot
import io.element.android.x.BuildConfig import com.bumble.appyx.navmodel.backstack.operation.pop
import io.element.android.x.component.ShowkaseButton 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.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.getBrowserIntent import io.element.android.x.features.rageshake.bugreport.BugReportNode
import io.element.android.x.matrix.Matrix import io.element.android.x.matrix.Matrix
import io.element.android.x.matrix.core.SessionId 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.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -64,6 +65,7 @@ class RootFlowNode(
private val appComponentOwner: DaggerComponentOwner, private val appComponentOwner: DaggerComponentOwner,
private val matrix: Matrix, private val matrix: Matrix,
private val sessionComponentsOwner: SessionComponentsOwner, private val sessionComponentsOwner: SessionComponentsOwner,
rootPresenter: RootPresenter
) : ) :
ParentNode<RootFlowNode.NavTarget>( ParentNode<RootFlowNode.NavTarget>(
navModel = backstack, navModel = backstack,
@ -73,10 +75,20 @@ class RootFlowNode(
DaggerComponentOwner by appComponentOwner { DaggerComponentOwner by appComponentOwner {
private val presenterConnector = presenterConnector(rootPresenter)
init { init {
Timber.v("Init") Timber.v("Init")
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { Timber.v("OnCreate") }, onCreate = { Timber.v("OnCreate") },
onResume = {
Timber.v("OnResume")
presenterConnector.emitEvent(RootEvents.StartRageshakeDetection)
},
onPause = {
Timber.v("OnPause")
presenterConnector.emitEvent(RootEvents.StopRageshakeDetection)
},
onDestroy = { Timber.v("OnDestroy") } onDestroy = { Timber.v("OnDestroy") }
) )
} }
@ -85,7 +97,7 @@ class RootFlowNode(
matrix.isLoggedIn() matrix.isLoggedIn()
.distinctUntilChanged() .distinctUntilChanged()
.onEach { isLoggedIn -> .onEach { isLoggedIn ->
Timber.v("IsLoggedIn") Timber.v("isLoggedIn=$isLoggedIn")
if (isLoggedIn) { if (isLoggedIn) {
val matrixClient = matrix.restoreSession() val matrixClient = matrix.restoreSession()
if (matrixClient == null) { if (matrixClient == null) {
@ -102,43 +114,50 @@ class RootFlowNode(
.launchIn(lifecycleScope) .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 @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
var isShowkaseButtonVisible by remember { mutableStateOf(BuildConfig.DEBUG) } val state by presenterConnector.stateFlow.collectAsState()
Box( RootView(
modifier = modifier state = state,
.fillMaxSize(), onHideShowkaseClicked = this::hideShowkaseButton,
contentAlignment = Alignment.TopCenter, onOpenBugReport = this::onOpenBugReport,
onCrashDetectedDismissed = this::onCrashDetectedDismissed,
onDisableRageshake = this::onDisableRageshake,
onDismissRageshake = this::onDismissRageshake,
onScreenshotTaken = this::onScreenshotTaken
) { ) {
Children(navModel = backstack) Children(navModel = backstack)
val context = LocalContext.current }
ShowkaseButton( }
isVisible = isShowkaseButtonVisible,
onCloseClicked = { isShowkaseButtonVisible = false },
onClick = { startActivity(context, Showkase.getBrowserIntent(context), null) }
)
/* private fun onScreenshotTaken(imageResult: ImageResult) {
var isBugReportVisible by rememberSaveable { presenterConnector.emitEvent(RootEvents.ProcessScreenshot(imageResult))
mutableStateOf(false) }
}
RageshakeDetectionScreen( private val bugReportNodeCallback = object : BugReportNode.Callback {
onOpenBugReport = { override fun onBugReportSent() {
isBugReportVisible = true backstack.pop()
}
)
CrashDetectionScreen(
onOpenBugReport = {
isBugReportVisible = true
}
)
if (isBugReportVisible) {
// TODO Improve the navigation, when pressing back here, it closes the app.
BugReportScreen(
onDone = { isBugReportVisible = false }
)
}
*/
} }
} }
@ -151,12 +170,19 @@ class RootFlowNode(
@Parcelize @Parcelize
data class LoggedInFlow(val sessionId: SessionId) : NavTarget data class LoggedInFlow(val sessionId: SessionId) : NavTarget
@Parcelize
object BugReport : NavTarget
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
is NavTarget.LoggedInFlow -> { is NavTarget.LoggedInFlow -> {
LoggedInFlowNode(buildContext, navTarget.sessionId) LoggedInFlowNode(
buildContext = buildContext,
sessionId = navTarget.sessionId,
onOpenBugReport = this::onOpenBugReport
)
} }
NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext) NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext)
NavTarget.SplashScreen -> node(buildContext) { NavTarget.SplashScreen -> node(buildContext) {
@ -164,6 +190,7 @@ class RootFlowNode(
CircularProgressIndicator() 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( class PreferencesFlowNode(
buildContext: BuildContext, buildContext: BuildContext,
private val onOpenBugReport: () -> Unit,
private val backstack: BackStack<NavTarget> = BackStack( private val backstack: BackStack<NavTarget> = BackStack(
initialElement = NavTarget.Root, initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap, savedStateMap = buildContext.savedStateMap,
@ -23,6 +24,12 @@ class PreferencesFlowNode(
buildContext = buildContext buildContext = buildContext
) { ) {
private val preferencesRootNodeCallback = object : PreferencesRootNode.Callback {
override fun onOpenBugReport() {
onOpenBugReport.invoke()
}
}
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@Parcelize @Parcelize
object Root : NavTarget object Root : NavTarget
@ -30,7 +37,7 @@ class PreferencesFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { 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.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted 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
@ -20,6 +21,10 @@ class PreferencesRootNode @AssistedInject constructor(
private val presenter: PreferencesRootPresenter, private val presenter: PreferencesRootPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
public interface Callback : Plugin {
fun onOpenBugReport()
}
private val presenterConnector = presenterConnector(presenter) private val presenterConnector = presenterConnector(presenter)
private fun onLogoutClicked() { private fun onLogoutClicked() {
@ -34,6 +39,10 @@ class PreferencesRootNode @AssistedInject constructor(
presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeSensitivity(sensitivity)) presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeSensitivity(sensitivity))
} }
private fun onOpenBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by presenterConnector.stateFlow.collectAsState() val state by presenterConnector.stateFlow.collectAsState()
@ -42,7 +51,8 @@ class PreferencesRootNode @AssistedInject constructor(
onLogoutClicked = this::onLogoutClicked, onLogoutClicked = this::onLogoutClicked,
onBackPressed = this::navigateUp, onBackPressed = this::navigateUp,
onRageshakeEnabledChanged = this::onRageshakeEnabledChanged, 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.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
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.core.net.toUri
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.features.rageshake.crash.CrashDataStore import io.element.android.x.features.rageshake.crash.CrashDataStore
@ -31,6 +33,7 @@ class BugReportPresenter @Inject constructor(
private val sendingProgress: MutableState<Float>, private val sendingProgress: MutableState<Float>,
private val sendingAction: MutableState<Async<Unit>> private val sendingAction: MutableState<Async<Unit>>
) : BugReporter.IMXBugReportListener { ) : BugReporter.IMXBugReportListener {
override fun onUploadCancelled() { override fun onUploadCancelled() {
sendingProgress.value = 0f sendingProgress.value = 0f
sendingAction.value = Async.Uninitialized sendingAction.value = Async.Uninitialized
@ -54,6 +57,11 @@ class BugReportPresenter @Inject constructor(
@Composable @Composable
override fun present(events: Flow<BugReportEvents>): BugReportState { override fun present(events: Flow<BugReportEvents>): BugReportState {
val screenshotUri = rememberSaveable {
mutableStateOf(
screenshotHolder.getFile()?.toUri()?.toString()
)
}
val crashInfo: String by crashDataStore val crashInfo: String by crashDataStore
.crashInfo() .crashInfo()
.collectAsState(initial = "") .collectAsState(initial = "")
@ -64,15 +72,21 @@ class BugReportPresenter @Inject constructor(
val sendingAction: MutableState<Async<Unit>> = remember { val sendingAction: MutableState<Async<Unit>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(Async.Uninitialized)
} }
val formState: MutableState<BugReportFormState> = rememberSaveable { val formState: MutableState<BugReportFormState> = remember {
mutableStateOf(BugReportFormState.Default) mutableStateOf(BugReportFormState.Default)
} }
val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction) val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction)
val state = BugReportState( val state by remember {
hasCrashLogs = crashInfo.isNotEmpty(), derivedStateOf {
sendingProgress = sendingProgress.value, BugReportState(
sending = sendingAction.value hasCrashLogs = crashInfo.isNotEmpty(),
) sendingProgress = sendingProgress.value,
sending = sendingAction.value,
formState = formState.value,
screenshotUri = screenshotUri.value
)
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
events.collect { event -> events.collect { event ->
when (event) { when (event) {

View file

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

View file

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

View file

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

View file

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

View file

@ -34,9 +34,9 @@ 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, onScreenshotTaken: (ImageResult) -> Unit = {},
onDisableClicked: () -> Unit, onDisableClicked: () -> Unit = {},
onNoClicked: () -> Unit onNoClicked: () -> Unit = {}
) { ) {
LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen") LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen")
val context = LocalContext.current val context = LocalContext.current
@ -63,8 +63,10 @@ private fun TakeScreenshot(
onScreenshotTaken: (ImageResult) -> Unit = {} onScreenshotTaken: (ImageResult) -> Unit = {}
) { ) {
val view = LocalView.current val view = LocalView.current
view.screenshot { LaunchedEffect(Unit) {
onScreenshotTaken(it) view.screenshot {
onScreenshotTaken(it)
}
} }
} }

View file

@ -17,8 +17,7 @@ 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, RageshakePreferencesEvents> {
@Composable @Composable
override fun present(events: Flow<RageshakePreferencesEvents>): RageshakePreferencesState { 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.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import io.element.android.x.core.bitmap.writeBitmap 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.ApplicationContext
import io.element.android.x.di.SingleIn
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@SingleIn(AppScope::class)
class ScreenshotHolder @Inject constructor( class ScreenshotHolder @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
) { ) {

View file

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