Migrate BugReport and CrashDetection to new architecture
This commit is contained in:
@@ -8,7 +8,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.navmodel.backstack.BackStack
|
||||
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.component.ShowkaseButton
|
||||
import io.element.android.x.core.di.DaggerComponentOwner
|
||||
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.matrix.Matrix
|
||||
import io.element.android.x.matrix.core.SessionId
|
||||
|
||||
@@ -20,6 +20,7 @@ plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.anvil)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -16,30 +16,37 @@
|
||||
|
||||
package io.element.android.x.features.rageshake.bugreport
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import android.os.Parcelable
|
||||
import io.element.android.x.architecture.Async
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
data class BugReportViewState(
|
||||
data class BugReportState(
|
||||
val formState: BugReportFormState = BugReportFormState.Default,
|
||||
val sendLogs: Boolean = true,
|
||||
val hasCrashLogs: Boolean = false,
|
||||
val sendCrashLogs: Boolean = true,
|
||||
val canContact: Boolean = false,
|
||||
val sendScreenshot: Boolean = false,
|
||||
val screenshotUri: String? = null,
|
||||
val sendingProgress: Float = 0F,
|
||||
val sending: Async<Unit> = Uninitialized,
|
||||
) : MavericksState {
|
||||
val sending: Async<Unit> = Async.Uninitialized,
|
||||
) {
|
||||
val submitEnabled =
|
||||
formState.description.length > 10 && sending !is Loading
|
||||
formState.description.length > 10 && sending !is Async.Loading
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class BugReportFormState(
|
||||
val description: String,
|
||||
) {
|
||||
val sendLogs: Boolean,
|
||||
val sendCrashLogs: Boolean,
|
||||
val canContact: Boolean,
|
||||
val sendScreenshot: Boolean
|
||||
|
||||
): Parcelable {
|
||||
companion object {
|
||||
val Default = BugReportFormState("")
|
||||
val Default = BugReportFormState(
|
||||
description = "",
|
||||
sendLogs = true,
|
||||
sendCrashLogs = true,
|
||||
canContact = false,
|
||||
sendScreenshot = false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -50,48 +51,17 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.airbnb.mvrx.Fail
|
||||
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.architecture.Async
|
||||
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.components.LabelledCheckbox
|
||||
import io.element.android.x.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.x.element.resources.R as ElementR
|
||||
|
||||
@Composable
|
||||
fun BugReportScreen(
|
||||
viewModel: BugReportViewModel = mavericksViewModel(),
|
||||
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,
|
||||
fun BugReportView(
|
||||
state: BugReportState,
|
||||
modifier: Modifier = Modifier,
|
||||
onDescriptionChanged: (String) -> Unit = {},
|
||||
onSetSendLog: (Boolean) -> Unit = {},
|
||||
@@ -102,6 +72,10 @@ fun BugReportContent(
|
||||
onFailureDialogClosed: () -> Unit = { },
|
||||
onDone: () -> Unit = { },
|
||||
) {
|
||||
LogCompositions(tag = "Rageshake", msg = "Root")
|
||||
if (state.sending is Async.Success) {
|
||||
onDone()
|
||||
}
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
@@ -120,8 +94,8 @@ fun BugReportContent(
|
||||
)
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
val isError = state.sending is Fail
|
||||
val isFormEnabled = state.sending !is Loading
|
||||
val isError = state.sending is Async.Failure
|
||||
val isFormEnabled = state.sending !is Async.Loading
|
||||
// Title
|
||||
Text(
|
||||
text = stringResource(id = ElementR.string.send_bug_report),
|
||||
@@ -140,11 +114,12 @@ fun BugReportContent(
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
var descriptionFieldState by textFieldState(stateValue = state.formState.description)
|
||||
Column(
|
||||
// modifier = Modifier.weight(1f),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = formState.description,
|
||||
value = descriptionFieldState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
@@ -155,7 +130,10 @@ fun BugReportContent(
|
||||
supportingText = {
|
||||
Text(text = stringResource(id = ElementR.string.send_bug_report_description_in_english))
|
||||
},
|
||||
onValueChange = onDescriptionChanged,
|
||||
onValueChange = {
|
||||
descriptionFieldState = it
|
||||
onDescriptionChanged(it)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Next
|
||||
@@ -164,33 +142,33 @@ fun BugReportContent(
|
||||
)
|
||||
}
|
||||
LabelledCheckbox(
|
||||
checked = state.sendLogs,
|
||||
checked = state.formState.sendLogs,
|
||||
onCheckedChange = onSetSendLog,
|
||||
enabled = isFormEnabled,
|
||||
text = stringResource(id = ElementR.string.send_bug_report_include_logs)
|
||||
)
|
||||
if (state.hasCrashLogs) {
|
||||
LabelledCheckbox(
|
||||
checked = state.sendCrashLogs,
|
||||
checked = state.formState.sendCrashLogs,
|
||||
onCheckedChange = onSetSendCrashLog,
|
||||
enabled = isFormEnabled,
|
||||
text = stringResource(id = ElementR.string.send_bug_report_include_crash_logs)
|
||||
)
|
||||
}
|
||||
LabelledCheckbox(
|
||||
checked = state.canContact,
|
||||
checked = state.formState.canContact,
|
||||
onCheckedChange = onSetCanContact,
|
||||
enabled = isFormEnabled,
|
||||
text = stringResource(id = ElementR.string.you_may_contact_me)
|
||||
)
|
||||
if (state.screenshotUri != null) {
|
||||
LabelledCheckbox(
|
||||
checked = state.sendScreenshot,
|
||||
checked = state.formState.sendScreenshot,
|
||||
onCheckedChange = onSetSendScreenshot,
|
||||
enabled = isFormEnabled,
|
||||
text = stringResource(id = ElementR.string.send_bug_report_include_screenshot)
|
||||
)
|
||||
if (state.sendScreenshot) {
|
||||
if (state.formState.sendScreenshot) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -219,18 +197,18 @@ fun BugReportContent(
|
||||
}
|
||||
}
|
||||
when (state.sending) {
|
||||
Uninitialized -> Unit
|
||||
is Loading -> {
|
||||
Async.Uninitialized -> Unit
|
||||
is Async.Loading -> {
|
||||
CircularProgressIndicator(
|
||||
progress = state.sendingProgress,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
is Fail -> ErrorDialog(
|
||||
is Async.Failure -> ErrorDialog(
|
||||
content = state.sending.error.toString(),
|
||||
onDismiss = onFailureDialogClosed,
|
||||
)
|
||||
is Success -> onDone()
|
||||
is Async.Success -> onDone()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,9 +218,8 @@ fun BugReportContent(
|
||||
@Preview
|
||||
fun BugReportContentPreview() {
|
||||
ElementXTheme(darkTheme = false) {
|
||||
BugReportContent(
|
||||
state = BugReportViewState(),
|
||||
formState = BugReportFormState.Default
|
||||
BugReportView(
|
||||
state = BugReportState(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.element.android.x.features.rageshake.crash.ui
|
||||
|
||||
sealed interface CrashDetectionEvents {
|
||||
object ResetAll : CrashDetectionEvents
|
||||
object ResetAppHasCrashed : CrashDetectionEvents
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -17,40 +17,33 @@
|
||||
package io.element.android.x.features.rageshake.crash.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.designsystem.ElementXTheme
|
||||
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.x.element.resources.R as ElementR
|
||||
|
||||
@Composable
|
||||
fun CrashDetectionScreen(
|
||||
viewModel: CrashDetectionViewModel = mavericksViewModel(),
|
||||
fun CrashDetectionView(
|
||||
state: CrashDetectionState,
|
||||
onOpenBugReport: () -> Unit = { },
|
||||
onPopupDismissed: () -> Unit = {}
|
||||
) {
|
||||
val state: CrashDetectionViewState by viewModel.collectAsState()
|
||||
LogCompositions(tag = "Crash", msg = "CrashDetectionScreen")
|
||||
|
||||
if (state.crashDetected) {
|
||||
CrashDetectionContent(
|
||||
state,
|
||||
onYesClicked = {
|
||||
viewModel.onYes()
|
||||
onOpenBugReport()
|
||||
},
|
||||
onNoClicked = viewModel::onPopupDismissed,
|
||||
onDismiss = viewModel::onPopupDismissed,
|
||||
onYesClicked = onOpenBugReport,
|
||||
onNoClicked = onPopupDismissed,
|
||||
onDismiss = onPopupDismissed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CrashDetectionContent(
|
||||
state: CrashDetectionViewState,
|
||||
state: CrashDetectionState,
|
||||
onNoClicked: () -> Unit = { },
|
||||
onYesClicked: () -> Unit = { },
|
||||
onDismiss: () -> Unit = { },
|
||||
@@ -71,7 +64,7 @@ fun CrashDetectionContent(
|
||||
fun CrashDetectionContentPreview() {
|
||||
ElementXTheme {
|
||||
CrashDetectionContent(
|
||||
state = CrashDetectionViewState()
|
||||
state = CrashDetectionState()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package io.element.android.x.features.rageshake.crash.ui
|
||||
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
|
||||
data class CrashDetectionViewState(
|
||||
data class CrashDetectionState(
|
||||
val crashDetected: Boolean = false,
|
||||
) : MavericksState
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user