diff --git a/app/src/main/kotlin/org/libremail/ui/reporting/ReportReviewScreen.kt b/app/src/main/kotlin/org/libremail/ui/reporting/ReportReviewScreen.kt index 559bf64..d14c76d 100644 --- a/app/src/main/kotlin/org/libremail/ui/reporting/ReportReviewScreen.kt +++ b/app/src/main/kotlin/org/libremail/ui/reporting/ReportReviewScreen.kt @@ -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)) + } + }, + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9001283..976335b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -337,7 +337,10 @@ Copy Save to file Submitting… - Report sent. Thank you! + + 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. + OK Couldn\'t send — it will be retried. Online submission isn\'t available in this build. Use Copy or Save to share the report. Copied to clipboard