Migrate BugReport and CrashDetection to new architecture

This commit is contained in:
ganfra 2023-01-09 20:39:58 +01:00
parent 7edfcac62b
commit acc091ef5f
12 changed files with 242 additions and 329 deletions

View file

@ -8,7 +8,6 @@ import androidx.compose.runtime.Composable
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.setValue 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
@ -27,14 +26,10 @@ 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 com.bumble.appyx.navmodel.backstack.operation.replace
import io.element.android.x.BuildConfig import io.element.android.x.BuildConfig
import io.element.android.x.component.ShowkaseButton import io.element.android.x.component.ShowkaseButton
import io.element.android.x.core.di.DaggerComponentOwner import io.element.android.x.core.di.DaggerComponentOwner
import io.element.android.x.di.SessionComponentsOwner import io.element.android.x.di.SessionComponentsOwner
import io.element.android.x.features.rageshake.bugreport.BugReportScreen
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionScreen
import io.element.android.x.features.rageshake.detection.RageshakeDetectionScreen
import io.element.android.x.getBrowserIntent import io.element.android.x.getBrowserIntent
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

View file

@ -20,6 +20,7 @@ plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.anvil) alias(libs.plugins.anvil)
id("kotlin-parcelize")
} }
android { android {

View file

@ -0,0 +1,11 @@
package io.element.android.x.features.rageshake.bugreport
sealed interface BugReportEvents {
object SendBugReport : BugReportEvents
object ResetAll: BugReportEvents
data class SetDescription(val description: String): BugReportEvents
data class SetSendLog(val sendLog: Boolean): BugReportEvents
data class SetSendCrashLog(val sendCrashlog: Boolean): BugReportEvents
data class SetCanContact(val canContact: Boolean): BugReportEvents
data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvents
}

View file

@ -0,0 +1,127 @@
package io.element.android.x.features.rageshake.bugreport
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Async
import io.element.android.x.architecture.Presenter
import io.element.android.x.features.rageshake.crash.CrashDataStore
import io.element.android.x.features.rageshake.logs.VectorFileLogger
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.screenshot.ScreenshotHolder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import javax.inject.Inject
class BugReportPresenter @Inject constructor(
private val bugReporter: BugReporter,
private val crashDataStore: CrashDataStore,
private val screenshotHolder: ScreenshotHolder,
private val appCoroutineScope: CoroutineScope,
) : Presenter<BugReportState, BugReportEvents> {
private class BugReporterUploadListener(
private val sendingProgress: MutableState<Float>,
private val sendingAction: MutableState<Async<Unit>>
) : BugReporter.IMXBugReportListener {
override fun onUploadCancelled() {
sendingProgress.value = 0f
sendingAction.value = Async.Uninitialized
}
override fun onUploadFailed(reason: String?) {
sendingProgress.value = 0f
sendingAction.value = Async.Failure(Exception(reason))
}
override fun onProgress(progress: Int) {
sendingProgress.value = progress.toFloat() / 100
sendingAction.value = Async.Loading()
}
override fun onUploadSucceed(reportUrl: String?) {
sendingProgress.value = 0f
sendingAction.value = Async.Success(Unit)
}
}
@Composable
override fun present(events: Flow<BugReportEvents>): BugReportState {
val crashInfo: String by crashDataStore
.crashInfo()
.collectAsState(initial = "")
val sendingProgress = remember {
mutableStateOf(0f)
}
val sendingAction: MutableState<Async<Unit>> = remember {
mutableStateOf(Async.Uninitialized)
}
val formState: MutableState<BugReportFormState> = rememberSaveable {
mutableStateOf(BugReportFormState.Default)
}
val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction)
val state = BugReportState(
hasCrashLogs = crashInfo.isNotEmpty(),
sendingProgress = sendingProgress.value,
sending = sendingAction.value
)
LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
BugReportEvents.SendBugReport -> appCoroutineScope.sendBugReport(state, uploadListener)
BugReportEvents.ResetAll -> appCoroutineScope.resetAll()
is BugReportEvents.SetDescription -> updateFormState(formState) {
copy(description = event.description)
}
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
}
private fun updateFormState(formState: MutableState<BugReportFormState>, operation: BugReportFormState.() -> BugReportFormState) {
formState.value = operation(formState.value)
}
private fun CoroutineScope.sendBugReport(state: BugReportState, listener: BugReporter.IMXBugReportListener) = launch {
bugReporter.sendBugReport(
coroutineScope = this,
reportType = ReportType.BUG_REPORT,
withDevicesLogs = state.formState.sendLogs,
withCrashLogs = state.hasCrashLogs && state.formState.sendCrashLogs,
withKeyRequestHistory = false,
withScreenshot = state.formState.sendScreenshot,
theBugDescription = state.formState.description,
serverVersion = "",
canContact = state.formState.canContact,
customFields = emptyMap(),
listener = listener
)
}
private fun CoroutineScope.resetAll() = launch {
screenshotHolder.reset()
crashDataStore.reset()
VectorFileLogger.getFromTimber().reset()
}
}

View file

@ -16,30 +16,37 @@
package io.element.android.x.features.rageshake.bugreport package io.element.android.x.features.rageshake.bugreport
import com.airbnb.mvrx.Async import android.os.Parcelable
import com.airbnb.mvrx.Loading import io.element.android.x.architecture.Async
import com.airbnb.mvrx.MavericksState import kotlinx.parcelize.Parcelize
import com.airbnb.mvrx.Uninitialized
data class BugReportViewState( data class BugReportState(
val formState: BugReportFormState = BugReportFormState.Default, val formState: BugReportFormState = BugReportFormState.Default,
val sendLogs: Boolean = true,
val hasCrashLogs: Boolean = false, val hasCrashLogs: Boolean = false,
val sendCrashLogs: Boolean = true,
val canContact: Boolean = false,
val sendScreenshot: Boolean = false,
val screenshotUri: String? = null, val screenshotUri: String? = null,
val sendingProgress: Float = 0F, val sendingProgress: Float = 0F,
val sending: Async<Unit> = Uninitialized, val sending: Async<Unit> = Async.Uninitialized,
) : MavericksState { ) {
val submitEnabled = val submitEnabled =
formState.description.length > 10 && sending !is Loading formState.description.length > 10 && sending !is Async.Loading
} }
@Parcelize
data class BugReportFormState( data class BugReportFormState(
val description: String, val description: String,
) { val sendLogs: Boolean,
val sendCrashLogs: Boolean,
val canContact: Boolean,
val sendScreenshot: Boolean
): Parcelable {
companion object { companion object {
val Default = BugReportFormState("") val Default = BugReportFormState(
description = "",
sendLogs = true,
sendCrashLogs = true,
canContact = false,
sendScreenshot = false
)
} }
} }

View file

@ -37,6 +37,7 @@ 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.getValue import androidx.compose.runtime.getValue
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.compose.ui.platform.LocalContext
@ -50,48 +51,17 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import com.airbnb.mvrx.Fail import io.element.android.x.architecture.Async
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.core.compose.LogCompositions import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.core.compose.textFieldState
import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.LabelledCheckbox 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
@Composable @Composable
fun BugReportScreen( fun BugReportView(
viewModel: BugReportViewModel = mavericksViewModel(), state: BugReportState,
onDone: () -> Unit = { },
) {
val state: BugReportViewState by viewModel.collectAsState()
val formState: BugReportFormState by viewModel.formState
LogCompositions(tag = "Rageshake", msg = "Root")
if (state.sending is Success) {
onDone()
}
BugReportContent(
state = state,
formState = formState,
onDescriptionChanged = viewModel::onSetDescription,
onSetSendLog = viewModel::onSetSendLog,
onSetSendCrashLog = viewModel::onSetSendCrashLog,
onSetCanContact = viewModel::onSetCanContact,
onSetSendScreenshot = viewModel::onSetSendScreenshot,
onSubmit = viewModel::onSubmit,
onFailureDialogClosed = viewModel::onFailureDialogClosed,
onDone = onDone,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BugReportContent(
state: BugReportViewState,
formState: BugReportFormState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onDescriptionChanged: (String) -> Unit = {}, onDescriptionChanged: (String) -> Unit = {},
onSetSendLog: (Boolean) -> Unit = {}, onSetSendLog: (Boolean) -> Unit = {},
@ -102,6 +72,10 @@ fun BugReportContent(
onFailureDialogClosed: () -> Unit = { }, onFailureDialogClosed: () -> Unit = { },
onDone: () -> Unit = { }, onDone: () -> Unit = { },
) { ) {
LogCompositions(tag = "Rageshake", msg = "Root")
if (state.sending is Async.Success) {
onDone()
}
Surface( Surface(
modifier = modifier, modifier = modifier,
color = MaterialTheme.colorScheme.background, color = MaterialTheme.colorScheme.background,
@ -120,8 +94,8 @@ fun BugReportContent(
) )
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
) { ) {
val isError = state.sending is Fail val isError = state.sending is Async.Failure
val isFormEnabled = state.sending !is Loading val isFormEnabled = state.sending !is Async.Loading
// Title // Title
Text( Text(
text = stringResource(id = ElementR.string.send_bug_report), text = stringResource(id = ElementR.string.send_bug_report),
@ -140,11 +114,12 @@ fun BugReportContent(
.padding(horizontal = 16.dp, vertical = 16.dp), .padding(horizontal = 16.dp, vertical = 16.dp),
fontSize = 16.sp, fontSize = 16.sp,
) )
var descriptionFieldState by textFieldState(stateValue = state.formState.description)
Column( Column(
// modifier = Modifier.weight(1f), // modifier = Modifier.weight(1f),
) { ) {
OutlinedTextField( OutlinedTextField(
value = formState.description, value = descriptionFieldState,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 16.dp), .padding(top = 16.dp),
@ -155,7 +130,10 @@ fun BugReportContent(
supportingText = { supportingText = {
Text(text = stringResource(id = ElementR.string.send_bug_report_description_in_english)) Text(text = stringResource(id = ElementR.string.send_bug_report_description_in_english))
}, },
onValueChange = onDescriptionChanged, onValueChange = {
descriptionFieldState = it
onDescriptionChanged(it)
},
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text, keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next imeAction = ImeAction.Next
@ -164,33 +142,33 @@ fun BugReportContent(
) )
} }
LabelledCheckbox( LabelledCheckbox(
checked = state.sendLogs, checked = state.formState.sendLogs,
onCheckedChange = onSetSendLog, onCheckedChange = onSetSendLog,
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.sendCrashLogs, checked = state.formState.sendCrashLogs,
onCheckedChange = onSetSendCrashLog, onCheckedChange = onSetSendCrashLog,
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.canContact, checked = state.formState.canContact,
onCheckedChange = onSetCanContact, onCheckedChange = onSetCanContact,
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.sendScreenshot, checked = state.formState.sendScreenshot,
onCheckedChange = onSetSendScreenshot, onCheckedChange = onSetSendScreenshot,
enabled = isFormEnabled, enabled = isFormEnabled,
text = stringResource(id = ElementR.string.send_bug_report_include_screenshot) text = stringResource(id = ElementR.string.send_bug_report_include_screenshot)
) )
if (state.sendScreenshot) { if (state.formState.sendScreenshot) {
Box( Box(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@ -219,18 +197,18 @@ fun BugReportContent(
} }
} }
when (state.sending) { when (state.sending) {
Uninitialized -> Unit Async.Uninitialized -> Unit
is Loading -> { is Async.Loading -> {
CircularProgressIndicator( CircularProgressIndicator(
progress = state.sendingProgress, progress = state.sendingProgress,
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
} }
is Fail -> ErrorDialog( is Async.Failure -> ErrorDialog(
content = state.sending.error.toString(), content = state.sending.error.toString(),
onDismiss = onFailureDialogClosed, onDismiss = onFailureDialogClosed,
) )
is Success -> onDone() is Async.Success -> onDone()
} }
} }
} }
@ -240,9 +218,8 @@ fun BugReportContent(
@Preview @Preview
fun BugReportContentPreview() { fun BugReportContentPreview() {
ElementXTheme(darkTheme = false) { ElementXTheme(darkTheme = false) {
BugReportContent( BugReportView(
state = BugReportViewState(), state = BugReportState(),
formState = BugReportFormState.Default
) )
} }
} }

View file

@ -1,175 +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.rageshake.bugreport
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import androidx.core.net.toUri
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory
import io.element.android.x.di.AppScope
import io.element.android.x.features.rageshake.crash.CrashDataStore
import io.element.android.x.features.rageshake.logs.VectorFileLogger
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.screenshot.ScreenshotHolder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ContributesViewModel(AppScope::class)
class BugReportViewModel @AssistedInject constructor(
@Assisted initialState: BugReportViewState,
private val bugReporter: BugReporter,
private val crashDataStore: CrashDataStore,
private val screenshotHolder: ScreenshotHolder,
private val appCoroutineScope: CoroutineScope
) :
MavericksViewModel<BugReportViewState>(initialState) {
companion object :
MavericksViewModelFactory<BugReportViewModel, BugReportViewState> by daggerMavericksViewModelFactory()
var formState = mutableStateOf(BugReportFormState.Default)
private set
init {
snapshotFlow { formState.value }
.onEach {
setState { copy(formState = it) }
}.launchIn(viewModelScope)
observerCrashDataStore()
setState {
copy(
screenshotUri = screenshotHolder.getFile()?.toUri()?.toString()
)
}
}
private fun observerCrashDataStore() {
viewModelScope.launch {
crashDataStore.crashInfo().collect {
setState {
copy(
hasCrashLogs = it.isNotEmpty()
)
}
}
}
}
private val listener: BugReporter.IMXBugReportListener = object : BugReporter.IMXBugReportListener {
override fun onUploadCancelled() {
setState {
copy(
sendingProgress = 0F,
sending = Uninitialized
)
}
}
override fun onUploadFailed(reason: String?) {
setState {
copy(
sendingProgress = 0F,
sending = Fail(Exception(reason))
)
}
}
override fun onProgress(progress: Int) {
setState {
copy(
sendingProgress = progress.toFloat() / 100,
sending = Loading()
)
}
}
override fun onUploadSucceed(reportUrl: String?) {
setState {
copy(
sendingProgress = 1F,
sending = Success(Unit)
)
}
}
}
override fun onCleared() {
// Use appCoroutineScope because we don't want this coroutine to be cancelled
appCoroutineScope.launch(Dispatchers.IO) {
screenshotHolder.reset()
crashDataStore.reset()
VectorFileLogger.getFromTimber().reset()
}
super.onCleared()
}
fun onSubmit() {
setState {
copy(
sendingProgress = 0F,
sending = Loading()
)
}
withState { state ->
bugReporter.sendBugReport(
coroutineScope = viewModelScope,
reportType = ReportType.BUG_REPORT,
withDevicesLogs = state.sendLogs,
withCrashLogs = state.hasCrashLogs && state.sendCrashLogs,
withKeyRequestHistory = false,
withScreenshot = state.sendScreenshot,
theBugDescription = state.formState.description,
serverVersion = "",
canContact = state.canContact,
customFields = emptyMap(),
listener = listener
)
}
}
fun onFailureDialogClosed() {
setState {
copy(
sendingProgress = 0F,
sending = Uninitialized
)
}
}
fun onSetDescription(str: String) {
formState.value = formState.value.copy(description = str)
setState { copy(sending = Uninitialized) }
}
fun onSetSendLog(value: Boolean) = setState { copy(sendLogs = value) }
fun onSetSendCrashLog(value: Boolean) = setState { copy(sendCrashLogs = value) }
fun onSetCanContact(value: Boolean) = setState { copy(canContact = value) }
fun onSetSendScreenshot(value: Boolean) = setState { copy(sendScreenshot = value) }
}

View file

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

View file

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

View file

@ -17,40 +17,33 @@
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.getValue
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 com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.core.compose.LogCompositions import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.x.element.resources.R as ElementR import io.element.android.x.element.resources.R as ElementR
@Composable @Composable
fun CrashDetectionScreen( fun CrashDetectionView(
viewModel: CrashDetectionViewModel = mavericksViewModel(), state: CrashDetectionState,
onOpenBugReport: () -> Unit = { }, onOpenBugReport: () -> Unit = { },
onPopupDismissed: () -> Unit = {}
) { ) {
val state: CrashDetectionViewState by viewModel.collectAsState()
LogCompositions(tag = "Crash", msg = "CrashDetectionScreen") LogCompositions(tag = "Crash", msg = "CrashDetectionScreen")
if (state.crashDetected) { if (state.crashDetected) {
CrashDetectionContent( CrashDetectionContent(
state, state,
onYesClicked = { onYesClicked = onOpenBugReport,
viewModel.onYes() onNoClicked = onPopupDismissed,
onOpenBugReport() onDismiss = onPopupDismissed,
},
onNoClicked = viewModel::onPopupDismissed,
onDismiss = viewModel::onPopupDismissed,
) )
} }
} }
@Composable @Composable
fun CrashDetectionContent( fun CrashDetectionContent(
state: CrashDetectionViewState, state: CrashDetectionState,
onNoClicked: () -> Unit = { }, onNoClicked: () -> Unit = { },
onYesClicked: () -> Unit = { }, onYesClicked: () -> Unit = { },
onDismiss: () -> Unit = { }, onDismiss: () -> Unit = { },
@ -71,7 +64,7 @@ fun CrashDetectionContent(
fun CrashDetectionContentPreview() { fun CrashDetectionContentPreview() {
ElementXTheme { ElementXTheme {
CrashDetectionContent( CrashDetectionContent(
state = CrashDetectionViewState() state = CrashDetectionState()
) )
} }
} }

View file

@ -16,8 +16,6 @@
package io.element.android.x.features.rageshake.crash.ui package io.element.android.x.features.rageshake.crash.ui
import com.airbnb.mvrx.MavericksState data class CrashDetectionState(
data class CrashDetectionViewState(
val crashDetected: Boolean = false, val crashDetected: Boolean = false,
) : MavericksState )

View file

@ -1,65 +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.rageshake.crash.ui
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory
import io.element.android.x.di.AppScope
import io.element.android.x.features.rageshake.crash.CrashDataStore
import kotlinx.coroutines.launch
@ContributesViewModel(AppScope::class)
class CrashDetectionViewModel @AssistedInject constructor(
@Assisted initialState: CrashDetectionViewState,
private val crashDataStore: CrashDataStore,
) : MavericksViewModel<CrashDetectionViewState>(initialState) {
companion object :
MavericksViewModelFactory<CrashDetectionViewModel, CrashDetectionViewState> by daggerMavericksViewModelFactory()
init {
observeDataStore()
}
private fun observeDataStore() {
viewModelScope.launch {
crashDataStore.appHasCrashed().collect { appHasCrashed ->
setState {
copy(
crashDetected = appHasCrashed
)
}
}
}
}
fun onYes() {
viewModelScope.launch {
crashDataStore.resetAppHasCrashed()
}
}
fun onPopupDismissed() {
viewModelScope.launch {
crashDataStore.reset()
}
}
}