mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 11:55:47 +00:00
feat: add newsletter model, API endpoint, and per-account preferences
- Create NewsletterModels.kt with GhostNewsletter and NewslettersResponse - Add getNewsletters() endpoint to GhostApiService - Add optional newsletter/emailSegment query params to createPost/updatePost - Create NewsletterPreferences for per-account newsletter toggle - Add fetchNewsletters() and fetchSubscriberCount() to PostRepository - Pass newsletter params through PostRepository to API service - Add Robolectric tests for NewsletterPreferences
This commit is contained in:
parent
807c6d559e
commit
ed11577be1
5 changed files with 217 additions and 6 deletions
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.swoosh.microblog.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
class NewsletterPreferences private constructor(
|
||||||
|
private val prefs: SharedPreferences,
|
||||||
|
private val accountIdProvider: () -> String
|
||||||
|
) {
|
||||||
|
|
||||||
|
constructor(context: Context) : this(
|
||||||
|
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE),
|
||||||
|
accountIdProvider = { AccountManager(context).getActiveAccount()?.id ?: "" }
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Constructor for testing with plain SharedPreferences and a fixed account ID. */
|
||||||
|
constructor(prefs: SharedPreferences, accountId: String) : this(
|
||||||
|
prefs = prefs,
|
||||||
|
accountIdProvider = { accountId }
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun activeAccountId(): String = accountIdProvider()
|
||||||
|
|
||||||
|
fun isNewsletterEnabled(): Boolean =
|
||||||
|
prefs.getBoolean("newsletter_enabled_${activeAccountId()}", false)
|
||||||
|
|
||||||
|
fun setNewsletterEnabled(enabled: Boolean) =
|
||||||
|
prefs.edit().putBoolean("newsletter_enabled_${activeAccountId()}", enabled).apply()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PREFS_NAME = "newsletter_prefs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.swoosh.microblog.data.api
|
||||||
|
|
||||||
import com.swoosh.microblog.data.model.GhostSite
|
import com.swoosh.microblog.data.model.GhostSite
|
||||||
import com.swoosh.microblog.data.model.MembersResponse
|
import com.swoosh.microblog.data.model.MembersResponse
|
||||||
|
import com.swoosh.microblog.data.model.NewslettersResponse
|
||||||
import com.swoosh.microblog.data.model.PageWrapper
|
import com.swoosh.microblog.data.model.PageWrapper
|
||||||
import com.swoosh.microblog.data.model.PagesResponse
|
import com.swoosh.microblog.data.model.PagesResponse
|
||||||
import com.swoosh.microblog.data.model.PostWrapper
|
import com.swoosh.microblog.data.model.PostWrapper
|
||||||
|
|
@ -28,14 +29,18 @@ interface GhostApiService {
|
||||||
@POST("ghost/api/admin/posts/")
|
@POST("ghost/api/admin/posts/")
|
||||||
@Headers("Content-Type: application/json")
|
@Headers("Content-Type: application/json")
|
||||||
suspend fun createPost(
|
suspend fun createPost(
|
||||||
@Body body: PostWrapper
|
@Body body: PostWrapper,
|
||||||
|
@Query("newsletter") newsletter: String? = null,
|
||||||
|
@Query("email_segment") emailSegment: String? = null
|
||||||
): Response<PostsResponse>
|
): Response<PostsResponse>
|
||||||
|
|
||||||
@PUT("ghost/api/admin/posts/{id}/")
|
@PUT("ghost/api/admin/posts/{id}/")
|
||||||
@Headers("Content-Type: application/json")
|
@Headers("Content-Type: application/json")
|
||||||
suspend fun updatePost(
|
suspend fun updatePost(
|
||||||
@Path("id") id: String,
|
@Path("id") id: String,
|
||||||
@Body body: PostWrapper
|
@Body body: PostWrapper,
|
||||||
|
@Query("newsletter") newsletter: String? = null,
|
||||||
|
@Query("email_segment") emailSegment: String? = null
|
||||||
): Response<PostsResponse>
|
): Response<PostsResponse>
|
||||||
|
|
||||||
@DELETE("ghost/api/admin/posts/{id}/")
|
@DELETE("ghost/api/admin/posts/{id}/")
|
||||||
|
|
@ -61,6 +66,12 @@ interface GhostApiService {
|
||||||
@Query("include") include: String = "newsletters,labels"
|
@Query("include") include: String = "newsletters,labels"
|
||||||
): Response<MembersResponse>
|
): Response<MembersResponse>
|
||||||
|
|
||||||
|
@GET("ghost/api/admin/newsletters/")
|
||||||
|
suspend fun getNewsletters(
|
||||||
|
@Query("filter") filter: String = "status:active",
|
||||||
|
@Query("limit") limit: String = "all"
|
||||||
|
): Response<NewslettersResponse>
|
||||||
|
|
||||||
@GET("ghost/api/admin/users/me/")
|
@GET("ghost/api/admin/users/me/")
|
||||||
suspend fun getCurrentUser(): Response<UsersResponse>
|
suspend fun getCurrentUser(): Response<UsersResponse>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.swoosh.microblog.data.model
|
||||||
|
|
||||||
|
data class NewslettersResponse(
|
||||||
|
val newsletters: List<GhostNewsletter>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GhostNewsletter(
|
||||||
|
val id: String,
|
||||||
|
val uuid: String?,
|
||||||
|
val name: String,
|
||||||
|
val slug: String,
|
||||||
|
val description: String?,
|
||||||
|
val status: String?,
|
||||||
|
val visibility: String?,
|
||||||
|
val subscribe_on_signup: Boolean?,
|
||||||
|
val sort_order: Int?,
|
||||||
|
val sender_name: String?,
|
||||||
|
val sender_email: String?,
|
||||||
|
val created_at: String?,
|
||||||
|
val updated_at: String?
|
||||||
|
)
|
||||||
|
|
@ -8,6 +8,7 @@ import com.swoosh.microblog.data.api.GhostApiService
|
||||||
import com.swoosh.microblog.data.db.AppDatabase
|
import com.swoosh.microblog.data.db.AppDatabase
|
||||||
import com.swoosh.microblog.data.db.LocalPostDao
|
import com.swoosh.microblog.data.db.LocalPostDao
|
||||||
import com.swoosh.microblog.data.model.*
|
import com.swoosh.microblog.data.model.*
|
||||||
|
import com.swoosh.microblog.data.model.GhostNewsletter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -68,10 +69,18 @@ class PostRepository(private val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createPost(post: GhostPost): Result<GhostPost> =
|
suspend fun createPost(
|
||||||
|
post: GhostPost,
|
||||||
|
newsletter: String? = null,
|
||||||
|
emailSegment: String? = null
|
||||||
|
): Result<GhostPost> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val response = getApi().createPost(PostWrapper(listOf(post)))
|
val response = getApi().createPost(
|
||||||
|
PostWrapper(listOf(post)),
|
||||||
|
newsletter = newsletter,
|
||||||
|
emailSegment = emailSegment
|
||||||
|
)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
Result.success(response.body()!!.posts.first())
|
Result.success(response.body()!!.posts.first())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -82,10 +91,20 @@ class PostRepository(private val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updatePost(id: String, post: GhostPost): Result<GhostPost> =
|
suspend fun updatePost(
|
||||||
|
id: String,
|
||||||
|
post: GhostPost,
|
||||||
|
newsletter: String? = null,
|
||||||
|
emailSegment: String? = null
|
||||||
|
): Result<GhostPost> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val response = getApi().updatePost(id, PostWrapper(listOf(post)))
|
val response = getApi().updatePost(
|
||||||
|
id,
|
||||||
|
PostWrapper(listOf(post)),
|
||||||
|
newsletter = newsletter,
|
||||||
|
emailSegment = emailSegment
|
||||||
|
)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
Result.success(response.body()!!.posts.first())
|
Result.success(response.body()!!.posts.first())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -96,6 +115,35 @@ class PostRepository(private val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun fetchNewsletters(): Result<List<GhostNewsletter>> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val response = getApi().getNewsletters()
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
Result.success(response.body()!!.newsletters)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("Newsletters fetch failed ${response.code()}: ${response.errorBody()?.string()}"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchSubscriberCount(): Result<Int> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val response = getApi().getMembers(limit = 1)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val total = response.body()!!.meta?.pagination?.total ?: 0
|
||||||
|
Result.success(total)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("Member count fetch failed ${response.code()}"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun deletePost(id: String): Result<Unit> =
|
suspend fun deletePost(id: String): Result<Unit> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
package com.swoosh.microblog.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.RuntimeEnvironment
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [28], application = android.app.Application::class)
|
||||||
|
class NewsletterPreferencesTest {
|
||||||
|
|
||||||
|
private lateinit var prefs: SharedPreferences
|
||||||
|
private lateinit var newsletterPreferences: NewsletterPreferences
|
||||||
|
|
||||||
|
private val testAccountId = "test-account-123"
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
val context = RuntimeEnvironment.getApplication()
|
||||||
|
prefs = context.getSharedPreferences(NewsletterPreferences.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
prefs.edit().clear().commit()
|
||||||
|
newsletterPreferences = NewsletterPreferences(prefs, testAccountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Default values ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `default newsletter enabled is false`() {
|
||||||
|
assertFalse(newsletterPreferences.isNewsletterEnabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Setting and getting ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `setting newsletter enabled to true persists`() {
|
||||||
|
newsletterPreferences.setNewsletterEnabled(true)
|
||||||
|
assertTrue(newsletterPreferences.isNewsletterEnabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `setting newsletter enabled to false persists`() {
|
||||||
|
newsletterPreferences.setNewsletterEnabled(true)
|
||||||
|
newsletterPreferences.setNewsletterEnabled(false)
|
||||||
|
assertFalse(newsletterPreferences.isNewsletterEnabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `newsletter enabled persists across instances`() {
|
||||||
|
newsletterPreferences.setNewsletterEnabled(true)
|
||||||
|
val newInstance = NewsletterPreferences(prefs, testAccountId)
|
||||||
|
assertTrue(newInstance.isNewsletterEnabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toggling on then off round-trips correctly`() {
|
||||||
|
newsletterPreferences.setNewsletterEnabled(true)
|
||||||
|
assertTrue(newsletterPreferences.isNewsletterEnabled())
|
||||||
|
newsletterPreferences.setNewsletterEnabled(false)
|
||||||
|
assertFalse(newsletterPreferences.isNewsletterEnabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Per-account isolation ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `different accounts have independent newsletter settings`() {
|
||||||
|
val prefs1 = NewsletterPreferences(prefs, "account-1")
|
||||||
|
val prefs2 = NewsletterPreferences(prefs, "account-2")
|
||||||
|
|
||||||
|
prefs1.setNewsletterEnabled(true)
|
||||||
|
prefs2.setNewsletterEnabled(false)
|
||||||
|
|
||||||
|
assertTrue(prefs1.isNewsletterEnabled())
|
||||||
|
assertFalse(prefs2.isNewsletterEnabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `enabling for one account does not affect another`() {
|
||||||
|
val prefs1 = NewsletterPreferences(prefs, "account-a")
|
||||||
|
val prefs2 = NewsletterPreferences(prefs, "account-b")
|
||||||
|
|
||||||
|
prefs1.setNewsletterEnabled(true)
|
||||||
|
|
||||||
|
assertTrue(prefs1.isNewsletterEnabled())
|
||||||
|
assertFalse(prefs2.isNewsletterEnabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty account id still works`() {
|
||||||
|
val emptyPrefs = NewsletterPreferences(prefs, "")
|
||||||
|
emptyPrefs.setNewsletterEnabled(true)
|
||||||
|
assertTrue(emptyPrefs.isNewsletterEnabled())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue