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:
2026-07-02 17:03:35 -05:00
co-authored by Claude Opus 4.8
parent 31639fdacc
commit 134d13dea7
2 changed files with 57 additions and 11 deletions
@@ -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))
}
},
)
}
+4 -1
View File
@@ -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>