Continue migrating BugReport/Rageshake/Crash screens
This commit is contained in:
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
app/src/main/java/io/element/android/x/root/RootEvents.kt
Normal file
14
app/src/main/java/io/element/android/x/root/RootEvents.kt
Normal 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
|
||||
}
|
||||
62
app/src/main/java/io/element/android/x/root/RootPresenter.kt
Normal file
62
app/src/main/java/io/element/android/x/root/RootPresenter.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
15
app/src/main/java/io/element/android/x/root/RootState.kt
Normal file
15
app/src/main/java/io/element/android/x/root/RootState.kt
Normal 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
|
||||
)
|
||||
55
app/src/main/java/io/element/android/x/root/RootView.kt
Normal file
55
app/src/main/java/io/element/android/x/root/RootView.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -38,8 +38,7 @@ data class BugReportFormState(
|
||||
val sendCrashLogs: Boolean,
|
||||
val canContact: Boolean,
|
||||
val sendScreenshot: Boolean
|
||||
|
||||
): Parcelable {
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
val Default = BugReportFormState(
|
||||
description = "",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user