From 5c931b138c808723ebe26adb05476122405fd1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Orzech?= Date: Fri, 20 Mar 2026 00:58:50 +0100 Subject: [PATCH] feat: handle email-only posts in PostUploadWorker, Feed, and Detail screens Phase 4b.3: Sent status in Feed + PostUploadWorker handling. - PostUploadWorker: handle QUEUED_EMAIL_ONLY with email_only=true on GhostPost, pass newsletter slug to repository.createPost() - FeedViewModel: map GhostPost email_only/sent status to FeedPost.emailOnly - FeedScreen FilterChipsBar: add "Sent" chip (magenta, only when newsletter enabled) - FeedScreen PostCardContent: show envelope icon + "Sent" in magenta for sent posts, replace "Share" with "Copy content" for email-only posts - FeedScreen StatusBadge: handle sent/emailOnly status - DetailScreen: show email-only info card with errorContainer color when post is sent via email only, noting it's not visible on the blog --- .../microblog/ui/detail/DetailScreen.kt | 38 +++++++++ .../swoosh/microblog/ui/feed/FeedScreen.kt | 85 ++++++++++++++++--- .../swoosh/microblog/ui/feed/FeedViewModel.kt | 7 +- .../microblog/worker/PostUploadWorker.kt | 13 ++- 4 files changed, 124 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt index dcfa1f1..67fa9e7 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.material.icons.automirrored.filled.Article import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Email import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Image @@ -375,6 +376,43 @@ fun DetailScreen( PostStatsSection(post) } } + + // Email-only info card + if (post.status == "sent" || post.emailOnly) { + Spacer(modifier = Modifier.height(16.dp)) + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Email, + contentDescription = null, + tint = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column { + Text( + text = "\u2709 SENT VIA EMAIL ONLY", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onErrorContainer + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "This post is not visible on your blog.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f) + ) + } + } + } + } } } diff --git a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt index 6de4430..8c94b7c 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt @@ -767,6 +767,12 @@ fun FilterChipsBar( activeFilter: PostFilter, onFilterSelected: (PostFilter) -> Unit ) { + val context = LocalContext.current + val newsletterEnabled = remember { + com.swoosh.microblog.data.NewsletterPreferences(context).isNewsletterEnabled() + } + val sentColor = Color(0xFF6A1B9A) // magenta + Row( modifier = Modifier .fillMaxWidth() @@ -775,19 +781,29 @@ fun FilterChipsBar( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { PostFilter.values().forEach { filter -> + // Only show SENT chip when newsletter is enabled + if (filter == PostFilter.SENT && !newsletterEnabled) return@forEach + val selected = filter == activeFilter val containerColor by animateColorAsState( - targetValue = if (selected) - MaterialTheme.colorScheme.primaryContainer - else - MaterialTheme.colorScheme.surface, + targetValue = when { + selected && filter == PostFilter.SENT -> sentColor.copy(alpha = 0.2f) + selected -> MaterialTheme.colorScheme.primaryContainer + else -> MaterialTheme.colorScheme.surface + }, animationSpec = SwooshMotion.quick(), label = "chipColor" ) FilterChip( selected = selected, onClick = { onFilterSelected(filter) }, - label = { Text(filter.displayName) }, + label = { + Text( + filter.displayName, + color = if (selected && filter == PostFilter.SENT) sentColor + else Color.Unspecified + ) + }, colors = FilterChipDefaults.filterChipColors( selectedContainerColor = containerColor ) @@ -1647,12 +1663,15 @@ fun PostCardContent( val stats = remember(post.textContent, post.imageUrl, post.linkUrl) { PostStats.fromFeedPost(post) } + val isSent = post.status == "sent" || post.emailOnly val statusLabel = when { post.queueStatus != QueueStatus.NONE -> "Pending" + isSent -> "Sent" else -> post.status.replaceFirstChar { it.uppercase() } } val statusColor = when { post.queueStatus != QueueStatus.NONE -> Color(0xFFE65100) + isSent -> Color(0xFF6A1B9A) post.status == "published" -> Color(0xFF2E7D32) post.status == "scheduled" -> Color(0xFF1565C0) else -> Color(0xFF7B1FA2) @@ -1661,19 +1680,28 @@ fun PostCardContent( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - Box( - modifier = Modifier - .size(8.dp) - .clip(CircleShape) - .background(statusColor) - ) + if (isSent) { + Icon( + Icons.Default.Email, + contentDescription = "Sent", + modifier = Modifier.size(12.dp), + tint = statusColor + ) + } else { + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background(statusColor) + ) + } Text( text = statusLabel, style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold), color = statusColor ) Text( - text = "ยท", + text = "\u00B7", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -1714,8 +1742,35 @@ fun PostCardContent( ) } - // Share action (copies link to clipboard) - if (isPublished && hasShareableUrl) { + // Share / Copy content action + if (isSent) { + // For sent (email-only) posts, show "Copy content" instead of "Share" + val copyContext = LocalContext.current + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.clickable { + val clipboard = copyContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText("Post content", post.textContent)) + snackbarHostState?.let { host -> + coroutineScope.launch { + host.showSnackbar("Content copied to clipboard") + } + } + } + ) { + Icon( + Icons.Default.ContentCopy, + contentDescription = "Copy content", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "Copy", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else if (isPublished && hasShareableUrl) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.clickable { @@ -2168,8 +2223,10 @@ fun buildHighlightedString( @Composable fun StatusBadge(post: FeedPost) { + val isSent = post.status == "sent" || post.emailOnly val (label, containerColor, labelColor) = when { post.queueStatus != QueueStatus.NONE -> Triple("Pending", Color(0xFFFFF3E0), Color(0xFFE65100)) + isSent -> Triple("Sent", Color(0xFFF3E5F5), Color(0xFF6A1B9A)) post.status == "published" -> Triple("Published", Color(0xFFE8F5E9), Color(0xFF2E7D32)) post.status == "scheduled" -> Triple("Scheduled", Color(0xFFE3F2FD), Color(0xFF1565C0)) else -> Triple("Draft", Color(0xFFF3E5F5), Color(0xFF7B1FA2)) diff --git a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedViewModel.kt b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedViewModel.kt index 0173054..f726103 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedViewModel.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedViewModel.kt @@ -536,6 +536,7 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) { } } val fileData = extractFileCardFromMobiledoc(mobiledoc) + val isEmailOnly = status == "sent" || email_only == true return FeedPost( ghostId = id, slug = slug, @@ -558,7 +559,8 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) { updatedAt = updated_at, isLocal = false, fileUrl = fileData?.first, - fileName = fileData?.second + fileName = fileData?.second, + emailOnly = isEmailOnly ) } @@ -645,7 +647,8 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) { isLocal = true, queueStatus = queueStatus, fileUrl = uploadedFileUrl ?: fileUri, - fileName = fileName + fileName = fileName, + emailOnly = emailOnly ) } diff --git a/app/src/main/java/com/swoosh/microblog/worker/PostUploadWorker.kt b/app/src/main/java/com/swoosh/microblog/worker/PostUploadWorker.kt index 94c9c59..ca8ae99 100644 --- a/app/src/main/java/com/swoosh/microblog/worker/PostUploadWorker.kt +++ b/app/src/main/java/com/swoosh/microblog/worker/PostUploadWorker.kt @@ -94,12 +94,15 @@ class PostUploadWorker( } val ghostTags = tagNames.map { GhostTag(name = it) } + val isEmailOnly = post.queueStatus == QueueStatus.QUEUED_EMAIL_ONLY + val ghostPost = GhostPost( title = post.title, mobiledoc = mobiledoc, status = when (post.queueStatus) { QueueStatus.QUEUED_PUBLISH -> "published" QueueStatus.QUEUED_SCHEDULED -> "scheduled" + QueueStatus.QUEUED_EMAIL_ONLY -> "published" else -> "draft" }, featured = post.featured, @@ -107,13 +110,17 @@ class PostUploadWorker( feature_image_alt = post.imageAlt, published_at = post.scheduledAt, visibility = "public", - tags = ghostTags.ifEmpty { null } + tags = ghostTags.ifEmpty { null }, + email_only = if (isEmailOnly) true else null ) + // Determine newsletter slug for email-only or newsletter posts + val newsletterSlug = post.newsletterSlug + val result = if (post.ghostId != null) { - repository.updatePost(post.ghostId, ghostPost) + repository.updatePost(post.ghostId, ghostPost, newsletter = newsletterSlug) } else { - repository.createPost(ghostPost) + repository.createPost(ghostPost, newsletter = newsletterSlug) } result.fold(