feat(reporting): show a clear confirmation dialog after submitting a problem report
Replace the small inline "Report sent. Thank you!" text with an AlertDialog carrying the fuller thank-you/no-guarantee message, and gate the screen's auto-navigate-on-delete LaunchedEffect so it no longer fires while a submit is in flight or has just succeeded — the dialog's acknowledgement is what calls onDone() for that path instead. This closes the race where ReportUploadWorker deletes the report row (and thus flips state.exists to false) moments after SubmitUiState.SUCCEEDED, which could previously navigate the user away before the confirmation was ever visible. Discard and the other non-success paths (FAILED/UNAVAILABLE) are unaffected and still auto-navigate immediately. Closes #161 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -33,8 +34,10 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -57,6 +60,9 @@ fun ReportReviewScreen(onDone: () -> Unit, viewModel: ReportReviewViewModel = hi
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
// Latches once the post-submit confirmation dialog is acknowledged, so it can't reappear
|
||||
// during this screen's exit transition (state.submit stays SUCCEEDED after that point).
|
||||
var reportSubmittedAcknowledged by remember { mutableStateOf(false) }
|
||||
|
||||
val savedMessage = stringResource(R.string.report_saved)
|
||||
val copiedMessage = stringResource(R.string.report_copied)
|
||||
@@ -79,9 +85,16 @@ fun ReportReviewScreen(onDone: () -> Unit, viewModel: ReportReviewViewModel = hi
|
||||
}
|
||||
}
|
||||
|
||||
// Once the report has been submitted (deleted by the worker) or discarded, leave the screen.
|
||||
LaunchedEffect(state.loaded, state.exists) {
|
||||
if (state.loaded && !state.exists) onDone()
|
||||
// Leave once the report is gone — but a successful submit deletes the row from
|
||||
// ReportUploadWorker as soon as the upload finishes, which can race ahead of `state.submit`
|
||||
// itself reporting SUCCEEDED. So while a submit is in flight or has just succeeded, this
|
||||
// effect defers to ReportSubmittedDialog below: its acknowledgement calls onDone() instead,
|
||||
// guaranteeing the confirmation is seen. Plain discard (or a submit that never enqueued
|
||||
// anything, e.g. FAILED/UNAVAILABLE) is unaffected and still auto-navigates immediately.
|
||||
LaunchedEffect(state.loaded, state.exists, state.submit) {
|
||||
val awaitingSubmitOutcome =
|
||||
state.submit == SubmitUiState.SUBMITTING || state.submit == SubmitUiState.SUCCEEDED
|
||||
if (state.loaded && !state.exists && !awaitingSubmitOutcome) onDone()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -164,6 +177,17 @@ fun ReportReviewScreen(onDone: () -> Unit, viewModel: ReportReviewViewModel = hi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gates leaving the screen on the success path (see the LaunchedEffect above) so the message
|
||||
// is guaranteed to be seen, not just present for an instant before an auto-navigate.
|
||||
if (state.submit == SubmitUiState.SUCCEEDED && !reportSubmittedAcknowledged) {
|
||||
ReportSubmittedDialog(
|
||||
onAcknowledge = {
|
||||
reportSubmittedAcknowledged = true
|
||||
onDone()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -212,19 +236,38 @@ private fun PayloadBox(payload: String) {
|
||||
|
||||
@Composable
|
||||
private fun SubmitStatusText(state: SubmitUiState) {
|
||||
if (state == SubmitUiState.IDLE) return
|
||||
// SUCCEEDED is surfaced via ReportSubmittedDialog instead: a modal is what guarantees the
|
||||
// message survives the screen's auto-navigate-on-delete race (see ReportReviewScreen above).
|
||||
val text = when (state) {
|
||||
SubmitUiState.SUBMITTING -> stringResource(R.string.report_submitting)
|
||||
SubmitUiState.SUCCEEDED -> stringResource(R.string.report_submitted)
|
||||
SubmitUiState.FAILED -> stringResource(R.string.report_submit_failed)
|
||||
SubmitUiState.UNAVAILABLE -> stringResource(R.string.report_submit_unavailable)
|
||||
SubmitUiState.IDLE -> ""
|
||||
SubmitUiState.IDLE, SubmitUiState.SUCCEEDED -> return
|
||||
}
|
||||
val color = when (state) {
|
||||
SubmitUiState.SUCCEEDED -> MaterialTheme.colorScheme.primary
|
||||
SubmitUiState.FAILED, SubmitUiState.UNAVAILABLE -> MaterialTheme.colorScheme.error
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val color = if (state == SubmitUiState.FAILED || state == SubmitUiState.UNAVAILABLE) {
|
||||
MaterialTheme.colorScheme.error
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(text, color = color, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmation shown after a successful submission (#161). It — not the deleted-row auto-navigate
|
||||
* — is what leaves the screen for that path, so the fuller thank-you message is guaranteed to be
|
||||
* seen even though the report row (and therefore `state.exists`) can flip to gone moments after
|
||||
* `SubmitUiState.SUCCEEDED`, once `ReportUploadWorker` finishes.
|
||||
*/
|
||||
@Composable
|
||||
private fun ReportSubmittedDialog(onAcknowledge: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onAcknowledge,
|
||||
text = { Text(stringResource(R.string.report_submitted)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onAcknowledge) {
|
||||
Text(stringResource(R.string.report_submitted_dismiss))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -337,7 +337,10 @@
|
||||
<string name="report_copy">Copy</string>
|
||||
<string name="report_save">Save to file</string>
|
||||
<string name="report_submitting">Submitting…</string>
|
||||
<string name="report_submitted">Report sent. Thank you!</string>
|
||||
<!-- Shown in a confirmation dialog (not just inline) so it can't be missed if the report row
|
||||
is deleted by ReportUploadWorker moments after the submit succeeds (#161). -->
|
||||
<string name="report_submitted">Thank you for submitting a problem report! It\'s people like you that help make LibreMail better! Please note that submission of a problem does not guarantee a response or a fix, as this is a volunteer led project.</string>
|
||||
<string name="report_submitted_dismiss">OK</string>
|
||||
<string name="report_submit_failed">Couldn\'t send — it will be retried.</string>
|
||||
<string name="report_submit_unavailable">Online submission isn\'t available in this build. Use Copy or Save to share the report.</string>
|
||||
<string name="report_copied">Copied to clipboard</string>
|
||||
|
||||
Reference in New Issue
Block a user