From f93a21e74386325bf7e86200da3b30f9b1549254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Orzech?= Date: Fri, 20 Mar 2026 00:56:04 +0100 Subject: [PATCH] feat: add email-only post option in Composer with confirmation dialog Phase 4b.2: Email-only option in Composer. - Add "Send via Email Only" dropdown menu item (visible when newsletter enabled) - Add showEmailOnlyConfirmation state to ComposerUiState - Add sendEmailOnly(), confirmEmailOnly(), cancelEmailOnly() to ViewModel - submitEmailOnlyPost() saves with emailOnly=true, QUEUED_EMAIL_ONLY status - Add EmailOnlyConfirmationDialog with warning icon, post preview, newsletter picker (if multiple), bold warning about irreversibility, and error-colored confirm button --- .../microblog/ui/composer/ComposerScreen.kt | 139 ++++++++++++++++++ .../ui/composer/ComposerViewModel.kt | 60 +++++++- 2 files changed, 198 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt index bdaeac9..c35a3df 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt @@ -236,6 +236,20 @@ fun ComposerScreen( enabled = canSubmit ) + // Email-only option + if (state.newsletterEnabled) { + HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) + DropdownMenuItem( + text = { Text("Send via Email Only") }, + onClick = { + showSendMenu = false + viewModel.sendEmailOnly() + }, + leadingIcon = { Icon(Icons.Default.Email, null) }, + enabled = canSubmit + ) + } + // Newsletter options if (state.newsletterEnabled) { HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) @@ -769,6 +783,18 @@ fun ComposerScreen( onDismiss = viewModel::cancelNewsletterConfirmation ) } + + // Email-only confirmation dialog + if (state.showEmailOnlyConfirmation) { + EmailOnlyConfirmationDialog( + postPreview = state.text.take(80), + availableNewsletters = state.availableNewsletters, + selectedNewsletter = state.selectedNewsletter, + onSelectNewsletter = viewModel::selectNewsletter, + onConfirm = viewModel::confirmEmailOnly, + onDismiss = viewModel::cancelEmailOnly + ) + } } /** @@ -1011,6 +1037,119 @@ fun NewsletterConfirmationDialog( ) } +/** + * Confirmation dialog for email-only sending. + * Shows a warning that the post will NOT appear on the blog. + */ +@Composable +fun EmailOnlyConfirmationDialog( + postPreview: String, + availableNewsletters: List, + selectedNewsletter: GhostNewsletter?, + onSelectNewsletter: (GhostNewsletter) -> Unit, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + Icons.Default.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(32.dp) + ) + }, + title = { + Text( + text = "Send via email only?", + style = MaterialTheme.typography.titleMedium + ) + }, + text = { + Column { + // Post content preview + if (postPreview.isNotBlank()) { + OutlinedCard(modifier = Modifier.fillMaxWidth()) { + Text( + text = postPreview + if (postPreview.length >= 80) "..." else "", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(12.dp), + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.height(12.dp)) + } + + // Newsletter picker (only if multiple) + if (availableNewsletters.size > 1) { + Text( + text = "Newsletter:", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Column(modifier = Modifier.selectableGroup()) { + availableNewsletters.forEach { newsletter -> + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = selectedNewsletter?.id == newsletter.id, + onClick = { onSelectNewsletter(newsletter) } + ) + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedNewsletter?.id == newsletter.id, + onClick = { onSelectNewsletter(newsletter) } + ) + Text( + text = newsletter.name, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 4.dp) + ) + } + } + } + Spacer(modifier = Modifier.height(12.dp)) + } else if (selectedNewsletter != null) { + Text( + text = "Newsletter: ${selectedNewsletter.name}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(12.dp)) + } + + // Bold warning + Text( + text = "This cannot be undone. Post will NOT appear on blog.", + style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.error + ) + } + }, + confirmButton = { + Button( + onClick = onConfirm, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError + ) + ) { + Text("\u2709 SEND EMAIL") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) +} + /** * Displays a 2-column grid of image thumbnails with remove buttons. * Includes an "Add more" button at the end. diff --git a/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerViewModel.kt b/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerViewModel.kt index 802b316..9e85adb 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerViewModel.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerViewModel.kt @@ -105,6 +105,62 @@ class ComposerViewModel(application: Application) : AndroidViewModel(application _uiState.update { it.copy(showNewsletterConfirmation = false) } } + fun sendEmailOnly() { + _uiState.update { it.copy(showEmailOnlyConfirmation = true) } + } + + fun confirmEmailOnly() { + _uiState.update { it.copy(showEmailOnlyConfirmation = false) } + submitEmailOnlyPost() + } + + fun cancelEmailOnly() { + _uiState.update { it.copy(showEmailOnlyConfirmation = false) } + } + + private fun submitEmailOnlyPost() { + val state = _uiState.value + if (state.text.isBlank() && state.imageUris.isEmpty() && state.fileUri == null) return + + viewModelScope.launch { + _uiState.update { it.copy(isSubmitting = true, error = null) } + + val title = state.text.take(60) + val hashtagTags = HashtagParser.parse(state.text) + val allTags = (state.extractedTags + hashtagTags).distinctBy { it.lowercase() } + val tagsJson = Gson().toJson(allTags) + val altText = state.imageAlt.ifBlank { null } + val newsletterSlug = state.selectedNewsletter?.slug + + // Save locally and queue for upload + val localPost = LocalPost( + localId = editingLocalId ?: 0, + ghostId = editingGhostId, + title = title, + content = state.text, + status = PostStatus.PUBLISHED, + featured = false, + imageUri = state.imageUris.firstOrNull()?.toString(), + imageUris = Converters.stringListToJson(state.imageUris.map { it.toString() }), + imageAlt = altText, + linkUrl = state.linkPreview?.url, + linkTitle = state.linkPreview?.title, + linkDescription = state.linkPreview?.description, + linkImageUrl = state.linkPreview?.imageUrl, + tags = tagsJson, + queueStatus = QueueStatus.QUEUED_EMAIL_ONLY, + emailOnly = true, + newsletterSlug = newsletterSlug, + fileUri = state.fileUri?.toString(), + fileName = state.fileName + ) + repository.saveLocalPost(localPost) + PostUploadWorker.enqueue(appContext) + + _uiState.update { it.copy(isSubmitting = false, isSuccess = true) } + } + } + fun updateTagInput(input: String) { val suggestions = if (input.isBlank()) { emptyList() @@ -536,7 +592,9 @@ data class ComposerUiState( val sendAsNewsletter: Boolean = false, val emailSegment: String = "all", val showNewsletterConfirmation: Boolean = false, - val subscriberCount: Int? = null + val subscriberCount: Int? = null, + // Email-only + val showEmailOnlyConfirmation: Boolean = false ) { /** * Backwards compatibility: returns the first image URI or null.