mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
merge: integrate Phase 4b (Email-only Posts) - all 8 features complete
This commit is contained in:
commit
e71d15805c
11 changed files with 461 additions and 33 deletions
|
|
@ -26,13 +26,13 @@ interface LocalPostDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM local_posts WHERE queueStatus IN (:statuses) ORDER BY createdAt ASC")
|
@Query("SELECT * FROM local_posts WHERE queueStatus IN (:statuses) ORDER BY createdAt ASC")
|
||||||
suspend fun getQueuedPosts(
|
suspend fun getQueuedPosts(
|
||||||
statuses: List<QueueStatus> = listOf(QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED)
|
statuses: List<QueueStatus> = listOf(QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED, QueueStatus.QUEUED_EMAIL_ONLY)
|
||||||
): List<LocalPost>
|
): List<LocalPost>
|
||||||
|
|
||||||
@Query("SELECT * FROM local_posts WHERE accountId = :accountId AND queueStatus IN (:statuses) ORDER BY createdAt ASC")
|
@Query("SELECT * FROM local_posts WHERE accountId = :accountId AND queueStatus IN (:statuses) ORDER BY createdAt ASC")
|
||||||
suspend fun getQueuedPostsByAccount(
|
suspend fun getQueuedPostsByAccount(
|
||||||
accountId: String,
|
accountId: String,
|
||||||
statuses: List<QueueStatus> = listOf(QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED)
|
statuses: List<QueueStatus> = listOf(QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED, QueueStatus.QUEUED_EMAIL_ONLY)
|
||||||
): List<LocalPost>
|
): List<LocalPost>
|
||||||
|
|
||||||
@Query("SELECT * FROM local_posts WHERE localId = :localId")
|
@Query("SELECT * FROM local_posts WHERE localId = :localId")
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ data class GhostPost(
|
||||||
val visibility: String? = "public",
|
val visibility: String? = "public",
|
||||||
val authors: List<Author>? = null,
|
val authors: List<Author>? = null,
|
||||||
val reading_time: Int? = null,
|
val reading_time: Int? = null,
|
||||||
val tags: List<GhostTag>? = null
|
val tags: List<GhostTag>? = null,
|
||||||
|
val email_only: Boolean? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GhostTag(
|
data class GhostTag(
|
||||||
|
|
@ -107,13 +108,15 @@ data class LocalPost(
|
||||||
enum class PostStatus {
|
enum class PostStatus {
|
||||||
DRAFT,
|
DRAFT,
|
||||||
PUBLISHED,
|
PUBLISHED,
|
||||||
SCHEDULED
|
SCHEDULED,
|
||||||
|
SENT
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class QueueStatus {
|
enum class QueueStatus {
|
||||||
NONE,
|
NONE,
|
||||||
QUEUED_PUBLISH,
|
QUEUED_PUBLISH,
|
||||||
QUEUED_SCHEDULED,
|
QUEUED_SCHEDULED,
|
||||||
|
QUEUED_EMAIL_ONLY,
|
||||||
UPLOADING,
|
UPLOADING,
|
||||||
FAILED
|
FAILED
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +150,8 @@ data class FeedPost(
|
||||||
val isLocal: Boolean = false,
|
val isLocal: Boolean = false,
|
||||||
val queueStatus: QueueStatus = QueueStatus.NONE,
|
val queueStatus: QueueStatus = QueueStatus.NONE,
|
||||||
val fileUrl: String? = null,
|
val fileUrl: String? = null,
|
||||||
val fileName: String? = null
|
val fileName: String? = null,
|
||||||
|
val emailOnly: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
|
|
@ -164,7 +168,8 @@ enum class PostFilter(val displayName: String, val ghostFilter: String?) {
|
||||||
ALL("All", null),
|
ALL("All", null),
|
||||||
PUBLISHED("Published", "status:published"),
|
PUBLISHED("Published", "status:published"),
|
||||||
DRAFT("Drafts", "status:draft"),
|
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. */
|
/** Returns the matching [PostStatus] for local filtering, or null for ALL. */
|
||||||
fun toPostStatus(): PostStatus? = when (this) {
|
fun toPostStatus(): PostStatus? = when (this) {
|
||||||
|
|
@ -172,6 +177,7 @@ enum class PostFilter(val displayName: String, val ghostFilter: String?) {
|
||||||
PUBLISHED -> PostStatus.PUBLISHED
|
PUBLISHED -> PostStatus.PUBLISHED
|
||||||
DRAFT -> PostStatus.DRAFT
|
DRAFT -> PostStatus.DRAFT
|
||||||
SCHEDULED -> PostStatus.SCHEDULED
|
SCHEDULED -> PostStatus.SCHEDULED
|
||||||
|
SENT -> PostStatus.SENT
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Empty-state message shown when filter yields no results. */
|
/** Empty-state message shown when filter yields no results. */
|
||||||
|
|
@ -180,6 +186,7 @@ enum class PostFilter(val displayName: String, val ghostFilter: String?) {
|
||||||
PUBLISHED -> "No published posts yet"
|
PUBLISHED -> "No published posts yet"
|
||||||
DRAFT -> "No drafts yet"
|
DRAFT -> "No drafts yet"
|
||||||
SCHEDULED -> "No scheduled posts yet"
|
SCHEDULED -> "No scheduled posts yet"
|
||||||
|
SENT -> "No sent newsletters yet"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ data class OverallStats(
|
||||||
PostStatus.PUBLISHED -> publishedCount++
|
PostStatus.PUBLISHED -> publishedCount++
|
||||||
PostStatus.DRAFT -> draftCount++
|
PostStatus.DRAFT -> draftCount++
|
||||||
PostStatus.SCHEDULED -> scheduledCount++
|
PostStatus.SCHEDULED -> scheduledCount++
|
||||||
|
PostStatus.SENT -> publishedCount++ // sent counts as published
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,6 +48,7 @@ data class OverallStats(
|
||||||
"published" -> publishedCount++
|
"published" -> publishedCount++
|
||||||
"draft" -> draftCount++
|
"draft" -> draftCount++
|
||||||
"scheduled" -> scheduledCount++
|
"scheduled" -> scheduledCount++
|
||||||
|
"sent" -> publishedCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,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))
|
||||||
|
|
@ -823,6 +837,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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1065,6 +1091,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()
|
||||||
|
|
@ -601,7 +657,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.
|
||||||
|
|
|
||||||
|
|
@ -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.ContentCopy
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
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.ExpandLess
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material.icons.filled.Image
|
import androidx.compose.material.icons.filled.Image
|
||||||
|
|
@ -399,6 +400,43 @@ fun DetailScreen(
|
||||||
PostStatsSection(post)
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -769,6 +769,12 @@ fun FilterChipsBar(
|
||||||
activeFilter: PostFilter,
|
activeFilter: PostFilter,
|
||||||
onFilterSelected: (PostFilter) -> Unit
|
onFilterSelected: (PostFilter) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val newsletterEnabled = remember {
|
||||||
|
com.swoosh.microblog.data.NewsletterPreferences(context).isNewsletterEnabled()
|
||||||
|
}
|
||||||
|
val sentColor = Color(0xFF6A1B9A) // magenta
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -777,19 +783,29 @@ fun FilterChipsBar(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
PostFilter.values().forEach { filter ->
|
PostFilter.values().forEach { filter ->
|
||||||
|
// Only show SENT chip when newsletter is enabled
|
||||||
|
if (filter == PostFilter.SENT && !newsletterEnabled) return@forEach
|
||||||
|
|
||||||
val selected = filter == activeFilter
|
val selected = filter == activeFilter
|
||||||
val containerColor by animateColorAsState(
|
val containerColor by animateColorAsState(
|
||||||
targetValue = if (selected)
|
targetValue = when {
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
selected && filter == PostFilter.SENT -> sentColor.copy(alpha = 0.2f)
|
||||||
else
|
selected -> MaterialTheme.colorScheme.primaryContainer
|
||||||
MaterialTheme.colorScheme.surface,
|
else -> MaterialTheme.colorScheme.surface
|
||||||
|
},
|
||||||
animationSpec = SwooshMotion.quick(),
|
animationSpec = SwooshMotion.quick(),
|
||||||
label = "chipColor"
|
label = "chipColor"
|
||||||
)
|
)
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onClick = { onFilterSelected(filter) },
|
onClick = { onFilterSelected(filter) },
|
||||||
label = { Text(filter.displayName) },
|
label = {
|
||||||
|
Text(
|
||||||
|
filter.displayName,
|
||||||
|
color = if (selected && filter == PostFilter.SENT) sentColor
|
||||||
|
else Color.Unspecified
|
||||||
|
)
|
||||||
|
},
|
||||||
colors = FilterChipDefaults.filterChipColors(
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
selectedContainerColor = containerColor
|
selectedContainerColor = containerColor
|
||||||
)
|
)
|
||||||
|
|
@ -1635,6 +1651,7 @@ fun PostCardContent(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
val queueLabel = when (post.queueStatus) {
|
val queueLabel = when (post.queueStatus) {
|
||||||
QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED -> "Pending upload"
|
QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED -> "Pending upload"
|
||||||
|
QueueStatus.QUEUED_EMAIL_ONLY -> "Pending email send"
|
||||||
QueueStatus.UPLOADING -> "Uploading..."
|
QueueStatus.UPLOADING -> "Uploading..."
|
||||||
QueueStatus.FAILED -> "Upload failed"
|
QueueStatus.FAILED -> "Upload failed"
|
||||||
else -> ""
|
else -> ""
|
||||||
|
|
@ -1645,7 +1662,7 @@ fun PostCardContent(
|
||||||
label = queueLabel,
|
label = queueLabel,
|
||||||
isUploading = isUploading
|
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))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
TextButton(onClick = onCancelQueue) {
|
TextButton(onClick = onCancelQueue) {
|
||||||
Text("Cancel", style = MaterialTheme.typography.labelSmall)
|
Text("Cancel", style = MaterialTheme.typography.labelSmall)
|
||||||
|
|
@ -1660,12 +1677,15 @@ fun PostCardContent(
|
||||||
val stats = remember(post.textContent, post.imageUrl, post.linkUrl) {
|
val stats = remember(post.textContent, post.imageUrl, post.linkUrl) {
|
||||||
PostStats.fromFeedPost(post)
|
PostStats.fromFeedPost(post)
|
||||||
}
|
}
|
||||||
|
val isSent = post.status == "sent" || post.emailOnly
|
||||||
val statusLabel = when {
|
val statusLabel = when {
|
||||||
post.queueStatus != QueueStatus.NONE -> "Pending"
|
post.queueStatus != QueueStatus.NONE -> "Pending"
|
||||||
|
isSent -> "Sent"
|
||||||
else -> post.status.replaceFirstChar { it.uppercase() }
|
else -> post.status.replaceFirstChar { it.uppercase() }
|
||||||
}
|
}
|
||||||
val statusColor = when {
|
val statusColor = when {
|
||||||
post.queueStatus != QueueStatus.NONE -> Color(0xFFE65100)
|
post.queueStatus != QueueStatus.NONE -> Color(0xFFE65100)
|
||||||
|
isSent -> Color(0xFF6A1B9A)
|
||||||
post.status == "published" -> Color(0xFF2E7D32)
|
post.status == "published" -> Color(0xFF2E7D32)
|
||||||
post.status == "scheduled" -> Color(0xFF1565C0)
|
post.status == "scheduled" -> Color(0xFF1565C0)
|
||||||
else -> Color(0xFF7B1FA2)
|
else -> Color(0xFF7B1FA2)
|
||||||
|
|
@ -1674,19 +1694,28 @@ fun PostCardContent(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
Box(
|
if (isSent) {
|
||||||
modifier = Modifier
|
Icon(
|
||||||
.size(8.dp)
|
Icons.Default.Email,
|
||||||
.clip(CircleShape)
|
contentDescription = "Sent",
|
||||||
.background(statusColor)
|
modifier = Modifier.size(12.dp),
|
||||||
)
|
tint = statusColor
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(8.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(statusColor)
|
||||||
|
)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = statusLabel,
|
text = statusLabel,
|
||||||
style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold),
|
style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold),
|
||||||
color = statusColor
|
color = statusColor
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "·",
|
text = "\u00B7",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
@ -1727,8 +1756,35 @@ fun PostCardContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Share action (copies link to clipboard)
|
// Share / Copy content action
|
||||||
if (isPublished && hasShareableUrl) {
|
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(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
|
|
@ -2181,8 +2237,10 @@ fun buildHighlightedString(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StatusBadge(post: FeedPost) {
|
fun StatusBadge(post: FeedPost) {
|
||||||
|
val isSent = post.status == "sent" || post.emailOnly
|
||||||
val (label, containerColor, labelColor) = when {
|
val (label, containerColor, labelColor) = when {
|
||||||
post.queueStatus != QueueStatus.NONE -> Triple("Pending", Color(0xFFFFF3E0), Color(0xFFE65100))
|
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 == "published" -> Triple("Published", Color(0xFFE8F5E9), Color(0xFF2E7D32))
|
||||||
post.status == "scheduled" -> Triple("Scheduled", Color(0xFFE3F2FD), Color(0xFF1565C0))
|
post.status == "scheduled" -> Triple("Scheduled", Color(0xFFE3F2FD), Color(0xFF1565C0))
|
||||||
else -> Triple("Draft", Color(0xFFF3E5F5), Color(0xFF7B1FA2))
|
else -> Triple("Draft", Color(0xFFF3E5F5), Color(0xFF7B1FA2))
|
||||||
|
|
|
||||||
|
|
@ -537,6 +537,7 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
}
|
}
|
||||||
val fileData = extractFileCardFromMobiledoc(mobiledoc)
|
val fileData = extractFileCardFromMobiledoc(mobiledoc)
|
||||||
val (videoUrl, audioUrl) = extractMediaUrlsFromMobiledoc(mobiledoc)
|
val (videoUrl, audioUrl) = extractMediaUrlsFromMobiledoc(mobiledoc)
|
||||||
|
val isEmailOnly = status == "sent" || email_only == true
|
||||||
return FeedPost(
|
return FeedPost(
|
||||||
ghostId = id,
|
ghostId = id,
|
||||||
slug = slug,
|
slug = slug,
|
||||||
|
|
@ -561,7 +562,8 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
updatedAt = updated_at,
|
updatedAt = updated_at,
|
||||||
isLocal = false,
|
isLocal = false,
|
||||||
fileUrl = fileData?.first,
|
fileUrl = fileData?.first,
|
||||||
fileName = fileData?.second
|
fileName = fileData?.second,
|
||||||
|
emailOnly = isEmailOnly
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -686,7 +688,8 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
isLocal = true,
|
isLocal = true,
|
||||||
queueStatus = queueStatus,
|
queueStatus = queueStatus,
|
||||||
fileUrl = uploadedFileUrl ?: fileUri,
|
fileUrl = uploadedFileUrl ?: fileUri,
|
||||||
fileName = fileName
|
fileName = fileName,
|
||||||
|
emailOnly = emailOnly
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,12 +124,15 @@ class PostUploadWorker(
|
||||||
}
|
}
|
||||||
val ghostTags = tagNames.map { GhostTag(name = it) }
|
val ghostTags = tagNames.map { GhostTag(name = it) }
|
||||||
|
|
||||||
|
val isEmailOnly = post.queueStatus == QueueStatus.QUEUED_EMAIL_ONLY
|
||||||
|
|
||||||
val ghostPost = GhostPost(
|
val ghostPost = GhostPost(
|
||||||
title = post.title,
|
title = post.title,
|
||||||
mobiledoc = mobiledoc,
|
mobiledoc = mobiledoc,
|
||||||
status = when (post.queueStatus) {
|
status = when (post.queueStatus) {
|
||||||
QueueStatus.QUEUED_PUBLISH -> "published"
|
QueueStatus.QUEUED_PUBLISH -> "published"
|
||||||
QueueStatus.QUEUED_SCHEDULED -> "scheduled"
|
QueueStatus.QUEUED_SCHEDULED -> "scheduled"
|
||||||
|
QueueStatus.QUEUED_EMAIL_ONLY -> "published"
|
||||||
else -> "draft"
|
else -> "draft"
|
||||||
},
|
},
|
||||||
featured = post.featured,
|
featured = post.featured,
|
||||||
|
|
@ -137,13 +140,17 @@ class PostUploadWorker(
|
||||||
feature_image_alt = post.imageAlt,
|
feature_image_alt = post.imageAlt,
|
||||||
published_at = post.scheduledAt,
|
published_at = post.scheduledAt,
|
||||||
visibility = "public",
|
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) {
|
val result = if (post.ghostId != null) {
|
||||||
repository.updatePost(post.ghostId, ghostPost)
|
repository.updatePost(post.ghostId, ghostPost, newsletter = newsletterSlug)
|
||||||
} else {
|
} else {
|
||||||
repository.createPost(ghostPost)
|
repository.createPost(ghostPost, newsletter = newsletterSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
result.fold(
|
result.fold(
|
||||||
|
|
|
||||||
|
|
@ -265,13 +265,13 @@ class GhostModelsTest {
|
||||||
// --- Enum values ---
|
// --- Enum values ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `PostStatus has exactly 3 values`() {
|
fun `PostStatus has exactly 4 values`() {
|
||||||
assertEquals(3, PostStatus.values().size)
|
assertEquals(4, PostStatus.values().size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `QueueStatus has exactly 5 values`() {
|
fun `QueueStatus has exactly 6 values`() {
|
||||||
assertEquals(5, QueueStatus.values().size)
|
assertEquals(6, QueueStatus.values().size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -279,6 +279,7 @@ class GhostModelsTest {
|
||||||
assertEquals(PostStatus.DRAFT, PostStatus.valueOf("DRAFT"))
|
assertEquals(PostStatus.DRAFT, PostStatus.valueOf("DRAFT"))
|
||||||
assertEquals(PostStatus.PUBLISHED, PostStatus.valueOf("PUBLISHED"))
|
assertEquals(PostStatus.PUBLISHED, PostStatus.valueOf("PUBLISHED"))
|
||||||
assertEquals(PostStatus.SCHEDULED, PostStatus.valueOf("SCHEDULED"))
|
assertEquals(PostStatus.SCHEDULED, PostStatus.valueOf("SCHEDULED"))
|
||||||
|
assertEquals(PostStatus.SENT, PostStatus.valueOf("SENT"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -286,6 +287,7 @@ class GhostModelsTest {
|
||||||
assertEquals(QueueStatus.NONE, QueueStatus.valueOf("NONE"))
|
assertEquals(QueueStatus.NONE, QueueStatus.valueOf("NONE"))
|
||||||
assertEquals(QueueStatus.QUEUED_PUBLISH, QueueStatus.valueOf("QUEUED_PUBLISH"))
|
assertEquals(QueueStatus.QUEUED_PUBLISH, QueueStatus.valueOf("QUEUED_PUBLISH"))
|
||||||
assertEquals(QueueStatus.QUEUED_SCHEDULED, QueueStatus.valueOf("QUEUED_SCHEDULED"))
|
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.UPLOADING, QueueStatus.valueOf("UPLOADING"))
|
||||||
assertEquals(QueueStatus.FAILED, QueueStatus.valueOf("FAILED"))
|
assertEquals(QueueStatus.FAILED, QueueStatus.valueOf("FAILED"))
|
||||||
}
|
}
|
||||||
|
|
@ -422,4 +424,95 @@ class GhostModelsTest {
|
||||||
val json = gson.toJson(wrapper)
|
val json = gson.toJson(wrapper)
|
||||||
assertTrue(json.contains("\"feature_image_alt\":\"Photo description\""))
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ class PostFilterTest {
|
||||||
// --- Enum values ---
|
// --- Enum values ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `PostFilter has exactly 4 values`() {
|
fun `PostFilter has exactly 5 values`() {
|
||||||
assertEquals(4, PostFilter.values().size)
|
assertEquals(5, PostFilter.values().size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -18,6 +18,7 @@ class PostFilterTest {
|
||||||
assertEquals(PostFilter.PUBLISHED, PostFilter.valueOf("PUBLISHED"))
|
assertEquals(PostFilter.PUBLISHED, PostFilter.valueOf("PUBLISHED"))
|
||||||
assertEquals(PostFilter.DRAFT, PostFilter.valueOf("DRAFT"))
|
assertEquals(PostFilter.DRAFT, PostFilter.valueOf("DRAFT"))
|
||||||
assertEquals(PostFilter.SCHEDULED, PostFilter.valueOf("SCHEDULED"))
|
assertEquals(PostFilter.SCHEDULED, PostFilter.valueOf("SCHEDULED"))
|
||||||
|
assertEquals(PostFilter.SENT, PostFilter.valueOf("SENT"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Display names ---
|
// --- Display names ---
|
||||||
|
|
@ -107,4 +108,26 @@ class PostFilterTest {
|
||||||
fun `SCHEDULED emptyMessage returns No scheduled posts yet`() {
|
fun `SCHEDULED emptyMessage returns No scheduled posts yet`() {
|
||||||
assertEquals("No scheduled posts yet", PostFilter.SCHEDULED.emptyMessage())
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue