diff --git a/app/src/main/java/com/swoosh/microblog/data/NewsletterPreferences.kt b/app/src/main/java/com/swoosh/microblog/data/NewsletterPreferences.kt new file mode 100644 index 0000000..1e58b4a --- /dev/null +++ b/app/src/main/java/com/swoosh/microblog/data/NewsletterPreferences.kt @@ -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" + } +} diff --git a/app/src/main/java/com/swoosh/microblog/data/api/GhostApiService.kt b/app/src/main/java/com/swoosh/microblog/data/api/GhostApiService.kt index 952ed4d..840019a 100644 --- a/app/src/main/java/com/swoosh/microblog/data/api/GhostApiService.kt +++ b/app/src/main/java/com/swoosh/microblog/data/api/GhostApiService.kt @@ -2,6 +2,7 @@ package com.swoosh.microblog.data.api import com.swoosh.microblog.data.model.GhostSite 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.PagesResponse import com.swoosh.microblog.data.model.PostWrapper @@ -28,14 +29,18 @@ interface GhostApiService { @POST("ghost/api/admin/posts/") @Headers("Content-Type: application/json") suspend fun createPost( - @Body body: PostWrapper + @Body body: PostWrapper, + @Query("newsletter") newsletter: String? = null, + @Query("email_segment") emailSegment: String? = null ): Response @PUT("ghost/api/admin/posts/{id}/") @Headers("Content-Type: application/json") suspend fun updatePost( @Path("id") id: String, - @Body body: PostWrapper + @Body body: PostWrapper, + @Query("newsletter") newsletter: String? = null, + @Query("email_segment") emailSegment: String? = null ): Response @DELETE("ghost/api/admin/posts/{id}/") @@ -61,6 +66,12 @@ interface GhostApiService { @Query("include") include: String = "newsletters,labels" ): Response + @GET("ghost/api/admin/newsletters/") + suspend fun getNewsletters( + @Query("filter") filter: String = "status:active", + @Query("limit") limit: String = "all" + ): Response + @GET("ghost/api/admin/users/me/") suspend fun getCurrentUser(): Response diff --git a/app/src/main/java/com/swoosh/microblog/data/model/NewsletterModels.kt b/app/src/main/java/com/swoosh/microblog/data/model/NewsletterModels.kt new file mode 100644 index 0000000..619b38a --- /dev/null +++ b/app/src/main/java/com/swoosh/microblog/data/model/NewsletterModels.kt @@ -0,0 +1,21 @@ +package com.swoosh.microblog.data.model + +data class NewslettersResponse( + val newsletters: List +) + +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? +) diff --git a/app/src/main/java/com/swoosh/microblog/data/repository/PostRepository.kt b/app/src/main/java/com/swoosh/microblog/data/repository/PostRepository.kt index ec0a46e..c41415e 100644 --- a/app/src/main/java/com/swoosh/microblog/data/repository/PostRepository.kt +++ b/app/src/main/java/com/swoosh/microblog/data/repository/PostRepository.kt @@ -8,6 +8,7 @@ import com.swoosh.microblog.data.api.GhostApiService import com.swoosh.microblog.data.db.AppDatabase import com.swoosh.microblog.data.db.LocalPostDao import com.swoosh.microblog.data.model.* +import com.swoosh.microblog.data.model.GhostNewsletter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext @@ -68,10 +69,18 @@ class PostRepository(private val context: Context) { } } - suspend fun createPost(post: GhostPost): Result = + suspend fun createPost( + post: GhostPost, + newsletter: String? = null, + emailSegment: String? = null + ): Result = withContext(Dispatchers.IO) { try { - val response = getApi().createPost(PostWrapper(listOf(post))) + val response = getApi().createPost( + PostWrapper(listOf(post)), + newsletter = newsletter, + emailSegment = emailSegment + ) if (response.isSuccessful) { Result.success(response.body()!!.posts.first()) } else { @@ -82,10 +91,20 @@ class PostRepository(private val context: Context) { } } - suspend fun updatePost(id: String, post: GhostPost): Result = + suspend fun updatePost( + id: String, + post: GhostPost, + newsletter: String? = null, + emailSegment: String? = null + ): Result = withContext(Dispatchers.IO) { try { - val response = getApi().updatePost(id, PostWrapper(listOf(post))) + val response = getApi().updatePost( + id, + PostWrapper(listOf(post)), + newsletter = newsletter, + emailSegment = emailSegment + ) if (response.isSuccessful) { Result.success(response.body()!!.posts.first()) } else { @@ -96,6 +115,35 @@ class PostRepository(private val context: Context) { } } + suspend fun fetchNewsletters(): Result> = + 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 = + 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 = withContext(Dispatchers.IO) { try { diff --git a/app/src/test/java/com/swoosh/microblog/data/NewsletterPreferencesTest.kt b/app/src/test/java/com/swoosh/microblog/data/NewsletterPreferencesTest.kt new file mode 100644 index 0000000..dac1073 --- /dev/null +++ b/app/src/test/java/com/swoosh/microblog/data/NewsletterPreferencesTest.kt @@ -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()) + } +}