From f9d060ed7db7a9db4f64df1bc0507ab1d80de66c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Orzech?= Date: Fri, 20 Mar 2026 00:54:34 +0100 Subject: [PATCH] feat: add SENT status, QUEUED_EMAIL_ONLY queue status, and email_only field for email-only posts Phase 4b.1: Add data model support for email-only posts. - PostStatus: add SENT enum value - QueueStatus: add QUEUED_EMAIL_ONLY enum value - PostFilter: add SENT filter with "status:sent" ghost filter - GhostPost: add email_only Boolean field - FeedPost: add emailOnly Boolean field - LocalPostDao: include QUEUED_EMAIL_ONLY in queued posts query - OverallStats: handle SENT status in stats calculation - FeedScreen: show "Pending email send" for QUEUED_EMAIL_ONLY queue status - Update tests for new enum values and fields --- .../swoosh/microblog/data/db/LocalPostDao.kt | 4 +- .../microblog/data/model/GhostModels.kt | 15 ++- .../microblog/data/model/OverallStats.kt | 2 + .../swoosh/microblog/ui/feed/FeedScreen.kt | 3 +- .../microblog/data/model/GhostModelsTest.kt | 101 +++++++++++++++++- .../microblog/data/model/PostFilterTest.kt | 27 ++++- 6 files changed, 139 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/swoosh/microblog/data/db/LocalPostDao.kt b/app/src/main/java/com/swoosh/microblog/data/db/LocalPostDao.kt index 572bcee..5238897 100644 --- a/app/src/main/java/com/swoosh/microblog/data/db/LocalPostDao.kt +++ b/app/src/main/java/com/swoosh/microblog/data/db/LocalPostDao.kt @@ -26,13 +26,13 @@ interface LocalPostDao { @Query("SELECT * FROM local_posts WHERE queueStatus IN (:statuses) ORDER BY createdAt ASC") suspend fun getQueuedPosts( - statuses: List = listOf(QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED) + statuses: List = listOf(QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED, QueueStatus.QUEUED_EMAIL_ONLY) ): List @Query("SELECT * FROM local_posts WHERE accountId = :accountId AND queueStatus IN (:statuses) ORDER BY createdAt ASC") suspend fun getQueuedPostsByAccount( accountId: String, - statuses: List = listOf(QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED) + statuses: List = listOf(QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED, QueueStatus.QUEUED_EMAIL_ONLY) ): List @Query("SELECT * FROM local_posts WHERE localId = :localId") diff --git a/app/src/main/java/com/swoosh/microblog/data/model/GhostModels.kt b/app/src/main/java/com/swoosh/microblog/data/model/GhostModels.kt index 45dca66..b923166 100644 --- a/app/src/main/java/com/swoosh/microblog/data/model/GhostModels.kt +++ b/app/src/main/java/com/swoosh/microblog/data/model/GhostModels.kt @@ -48,7 +48,8 @@ data class GhostPost( val visibility: String? = "public", val authors: List? = null, val reading_time: Int? = null, - val tags: List? = null + val tags: List? = null, + val email_only: Boolean? = null ) data class GhostTag( @@ -107,13 +108,15 @@ data class LocalPost( enum class PostStatus { DRAFT, PUBLISHED, - SCHEDULED + SCHEDULED, + SENT } enum class QueueStatus { NONE, QUEUED_PUBLISH, QUEUED_SCHEDULED, + QUEUED_EMAIL_ONLY, UPLOADING, FAILED } @@ -145,7 +148,8 @@ data class FeedPost( val isLocal: Boolean = false, val queueStatus: QueueStatus = QueueStatus.NONE, val fileUrl: String? = null, - val fileName: String? = null + val fileName: String? = null, + val emailOnly: Boolean = false ) @Stable @@ -162,7 +166,8 @@ enum class PostFilter(val displayName: String, val ghostFilter: String?) { ALL("All", null), PUBLISHED("Published", "status:published"), DRAFT("Drafts", "status:draft"), - SCHEDULED("Scheduled", "status:scheduled"); + SCHEDULED("Scheduled", "status:scheduled"), + SENT("Sent", "status:sent"); /** Returns the matching [PostStatus] for local filtering, or null for ALL. */ fun toPostStatus(): PostStatus? = when (this) { @@ -170,6 +175,7 @@ enum class PostFilter(val displayName: String, val ghostFilter: String?) { PUBLISHED -> PostStatus.PUBLISHED DRAFT -> PostStatus.DRAFT SCHEDULED -> PostStatus.SCHEDULED + SENT -> PostStatus.SENT } /** Empty-state message shown when filter yields no results. */ @@ -178,6 +184,7 @@ enum class PostFilter(val displayName: String, val ghostFilter: String?) { PUBLISHED -> "No published posts yet" DRAFT -> "No drafts yet" SCHEDULED -> "No scheduled posts yet" + SENT -> "No sent newsletters yet" } } diff --git a/app/src/main/java/com/swoosh/microblog/data/model/OverallStats.kt b/app/src/main/java/com/swoosh/microblog/data/model/OverallStats.kt index f5c11da..13b0399 100644 --- a/app/src/main/java/com/swoosh/microblog/data/model/OverallStats.kt +++ b/app/src/main/java/com/swoosh/microblog/data/model/OverallStats.kt @@ -38,6 +38,7 @@ data class OverallStats( PostStatus.PUBLISHED -> publishedCount++ PostStatus.DRAFT -> draftCount++ PostStatus.SCHEDULED -> scheduledCount++ + PostStatus.SENT -> publishedCount++ // sent counts as published } } @@ -47,6 +48,7 @@ data class OverallStats( "published" -> publishedCount++ "draft" -> draftCount++ "scheduled" -> scheduledCount++ + "sent" -> publishedCount++ } } 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 8395ce4..6de4430 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 @@ -1621,6 +1621,7 @@ fun PostCardContent( Spacer(modifier = Modifier.height(8.dp)) val queueLabel = when (post.queueStatus) { QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED -> "Pending upload" + QueueStatus.QUEUED_EMAIL_ONLY -> "Pending email send" QueueStatus.UPLOADING -> "Uploading..." QueueStatus.FAILED -> "Upload failed" else -> "" @@ -1631,7 +1632,7 @@ fun PostCardContent( label = queueLabel, isUploading = isUploading ) - if (post.queueStatus == QueueStatus.QUEUED_PUBLISH || post.queueStatus == QueueStatus.QUEUED_SCHEDULED) { + if (post.queueStatus == QueueStatus.QUEUED_PUBLISH || post.queueStatus == QueueStatus.QUEUED_SCHEDULED || post.queueStatus == QueueStatus.QUEUED_EMAIL_ONLY) { Spacer(modifier = Modifier.width(8.dp)) TextButton(onClick = onCancelQueue) { Text("Cancel", style = MaterialTheme.typography.labelSmall) diff --git a/app/src/test/java/com/swoosh/microblog/data/model/GhostModelsTest.kt b/app/src/test/java/com/swoosh/microblog/data/model/GhostModelsTest.kt index 1915767..27119f5 100644 --- a/app/src/test/java/com/swoosh/microblog/data/model/GhostModelsTest.kt +++ b/app/src/test/java/com/swoosh/microblog/data/model/GhostModelsTest.kt @@ -265,13 +265,13 @@ class GhostModelsTest { // --- Enum values --- @Test - fun `PostStatus has exactly 3 values`() { - assertEquals(3, PostStatus.values().size) + fun `PostStatus has exactly 4 values`() { + assertEquals(4, PostStatus.values().size) } @Test - fun `QueueStatus has exactly 5 values`() { - assertEquals(5, QueueStatus.values().size) + fun `QueueStatus has exactly 6 values`() { + assertEquals(6, QueueStatus.values().size) } @Test @@ -279,6 +279,7 @@ class GhostModelsTest { assertEquals(PostStatus.DRAFT, PostStatus.valueOf("DRAFT")) assertEquals(PostStatus.PUBLISHED, PostStatus.valueOf("PUBLISHED")) assertEquals(PostStatus.SCHEDULED, PostStatus.valueOf("SCHEDULED")) + assertEquals(PostStatus.SENT, PostStatus.valueOf("SENT")) } @Test @@ -286,6 +287,7 @@ class GhostModelsTest { assertEquals(QueueStatus.NONE, QueueStatus.valueOf("NONE")) assertEquals(QueueStatus.QUEUED_PUBLISH, QueueStatus.valueOf("QUEUED_PUBLISH")) assertEquals(QueueStatus.QUEUED_SCHEDULED, QueueStatus.valueOf("QUEUED_SCHEDULED")) + assertEquals(QueueStatus.QUEUED_EMAIL_ONLY, QueueStatus.valueOf("QUEUED_EMAIL_ONLY")) assertEquals(QueueStatus.UPLOADING, QueueStatus.valueOf("UPLOADING")) assertEquals(QueueStatus.FAILED, QueueStatus.valueOf("FAILED")) } @@ -422,4 +424,95 @@ class GhostModelsTest { val json = gson.toJson(wrapper) assertTrue(json.contains("\"feature_image_alt\":\"Photo description\"")) } + + // --- email_only field --- + + @Test + fun `GhostPost default email_only is null`() { + val post = GhostPost() + assertNull(post.email_only) + } + + @Test + fun `GhostPost stores email_only true`() { + val post = GhostPost(email_only = true) + assertEquals(true, post.email_only) + } + + @Test + fun `GhostPost serializes email_only to JSON`() { + val post = GhostPost(title = "Email post", email_only = true) + val json = gson.toJson(post) + assertTrue(json.contains("\"email_only\":true")) + } + + @Test + fun `GhostPost deserializes email_only from JSON`() { + val json = """{"id":"1","email_only":true}""" + val post = gson.fromJson(json, GhostPost::class.java) + assertEquals(true, post.email_only) + } + + @Test + fun `GhostPost deserializes with missing email_only`() { + val json = """{"id":"1","title":"Test"}""" + val post = gson.fromJson(json, GhostPost::class.java) + assertNull(post.email_only) + } + + // --- FeedPost emailOnly --- + + @Test + fun `FeedPost default emailOnly is false`() { + val post = FeedPost( + title = "Test", + textContent = "Content", + htmlContent = null, + imageUrl = null, + linkUrl = null, + linkTitle = null, + linkDescription = null, + linkImageUrl = null, + status = "published", + publishedAt = null, + createdAt = null, + updatedAt = null + ) + assertFalse(post.emailOnly) + } + + @Test + fun `FeedPost stores emailOnly true`() { + val post = FeedPost( + title = "Test", + textContent = "Content", + htmlContent = null, + imageUrl = null, + linkUrl = null, + linkTitle = null, + linkDescription = null, + linkImageUrl = null, + status = "sent", + publishedAt = null, + createdAt = null, + updatedAt = null, + emailOnly = true + ) + assertTrue(post.emailOnly) + } + + // --- LocalPost emailOnly --- + + @Test + fun `LocalPost default emailOnly is false`() { + val post = LocalPost() + assertFalse(post.emailOnly) + } + + @Test + fun `LocalPost stores emailOnly true`() { + val post = LocalPost(emailOnly = true, newsletterSlug = "default-newsletter") + assertTrue(post.emailOnly) + assertEquals("default-newsletter", post.newsletterSlug) + } } diff --git a/app/src/test/java/com/swoosh/microblog/data/model/PostFilterTest.kt b/app/src/test/java/com/swoosh/microblog/data/model/PostFilterTest.kt index 1cf1887..50a4d89 100644 --- a/app/src/test/java/com/swoosh/microblog/data/model/PostFilterTest.kt +++ b/app/src/test/java/com/swoosh/microblog/data/model/PostFilterTest.kt @@ -8,8 +8,8 @@ class PostFilterTest { // --- Enum values --- @Test - fun `PostFilter has exactly 4 values`() { - assertEquals(4, PostFilter.values().size) + fun `PostFilter has exactly 5 values`() { + assertEquals(5, PostFilter.values().size) } @Test @@ -18,6 +18,7 @@ class PostFilterTest { assertEquals(PostFilter.PUBLISHED, PostFilter.valueOf("PUBLISHED")) assertEquals(PostFilter.DRAFT, PostFilter.valueOf("DRAFT")) assertEquals(PostFilter.SCHEDULED, PostFilter.valueOf("SCHEDULED")) + assertEquals(PostFilter.SENT, PostFilter.valueOf("SENT")) } // --- Display names --- @@ -107,4 +108,26 @@ class PostFilterTest { fun `SCHEDULED emptyMessage returns No scheduled posts yet`() { assertEquals("No scheduled posts yet", PostFilter.SCHEDULED.emptyMessage()) } + + // --- SENT filter --- + + @Test + fun `SENT displayName is Sent`() { + assertEquals("Sent", PostFilter.SENT.displayName) + } + + @Test + fun `SENT ghostFilter is status_sent`() { + assertEquals("status:sent", PostFilter.SENT.ghostFilter) + } + + @Test + fun `SENT toPostStatus returns PostStatus SENT`() { + assertEquals(PostStatus.SENT, PostFilter.SENT.toPostStatus()) + } + + @Test + fun `SENT emptyMessage returns No sent newsletters yet`() { + assertEquals("No sent newsletters yet", PostFilter.SENT.emptyMessage()) + } }