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
|
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
|
// Newsletter options
|
||||||
if (state.newsletterEnabled) {
|
if (state.newsletterEnabled) {
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
|
@ -769,6 +783,18 @@ fun ComposerScreen(
|
||||||
onDismiss = viewModel::cancelNewsletterConfirmation
|
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.
|
* Displays a 2-column grid of image thumbnails with remove buttons.
|
||||||
* Includes an "Add more" button at the end.
|
* Includes an "Add more" button at the end.
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,62 @@ class ComposerViewModel(application: Application) : AndroidViewModel(application
|
||||||
_uiState.update { it.copy(showNewsletterConfirmation = false) }
|
_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) {
|
fun updateTagInput(input: String) {
|
||||||
val suggestions = if (input.isBlank()) {
|
val suggestions = if (input.isBlank()) {
|
||||||
emptyList()
|
emptyList()
|
||||||
|
|
@ -536,7 +592,9 @@ data class ComposerUiState(
|
||||||
val sendAsNewsletter: Boolean = false,
|
val sendAsNewsletter: Boolean = false,
|
||||||
val emailSegment: String = "all",
|
val emailSegment: String = "all",
|
||||||
val showNewsletterConfirmation: Boolean = false,
|
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.
|
* Backwards compatibility: returns the first image URI or null.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue