mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
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
This commit is contained in:
parent
f9d060ed7d
commit
f93a21e743
2 changed files with 198 additions and 1 deletions
|
|
@ -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<GhostNewsletter>,
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue