mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-17 19:07:02 +05:30
Merge #130
130: Split API and database models r=msfjarvis a=msfjarvis - Introduces a new `SavedPost` table stripped down to only the necessary fields for persistence - Deletes the old `LobstersPost` table - Switches `LobstersItem` to take in a `SavedPost` for rendering - Replaces moshi-metadata-reflect with moshi-ksp Fixes #118 bors r+ Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
commit
f10e2b79ac
27 changed files with 195 additions and 200 deletions
1
.github/ci-gradle.properties
vendored
Normal file
1
.github/ci-gradle.properties
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
5
.github/workflows/pull_request.yml
vendored
5
.github/workflows/pull_request.yml
vendored
|
@ -10,12 +10,17 @@ jobs:
|
|||
test-pr:
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-java@d202f5dbf7256730fb690ec59f6381650114feb2
|
||||
with:
|
||||
java-version: '11'
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- uses: burrunan/gradle-cache-action@03c71a8ba93d670980695505f48f49daf43704a6
|
||||
name: Run unit tests
|
||||
with:
|
||||
|
|
6
.idea/kotlinScripting.xml
generated
Normal file
6
.idea/kotlinScripting.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinScriptingSettings">
|
||||
<option name="suppressDefinitionsCheck" value="true" />
|
||||
</component>
|
||||
</project>
|
|
@ -1,12 +1,12 @@
|
|||
plugins {
|
||||
kotlin("jvm")
|
||||
id("com.google.devtools.ksp") version "1.4.30-1.0.0-alpha04"
|
||||
`lobsters-plugin`
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(Dependencies.ThirdParty.Retrofit.lib)
|
||||
implementation(project(":database"))
|
||||
implementation(Dependencies.ThirdParty.Moshi.moshiMetadataReflect)
|
||||
ksp(Dependencies.ThirdParty.Moshi.ksp)
|
||||
implementation(Dependencies.ThirdParty.Retrofit.moshi)
|
||||
testImplementation(Dependencies.Kotlin.Coroutines.core)
|
||||
testImplementation(Dependencies.Testing.junit)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package dev.msfjarvis.lobsters.data.api
|
||||
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package dev.msfjarvis.lobsters.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class KeybaseSignature(
|
||||
@Json(name = "kb_username")
|
||||
val kbUsername: String,
|
||||
@Json(name = "sig_hash")
|
||||
val sigHash: String
|
||||
val sigHash: String,
|
||||
)
|
|
@ -0,0 +1,26 @@
|
|||
package dev.msfjarvis.lobsters.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class LobstersPost(
|
||||
@Json(name = "short_id")
|
||||
val shortId: String,
|
||||
@Json(name = "short_id_url")
|
||||
val shortIdUrl: String,
|
||||
@Json(name = "created_at")
|
||||
val createdAt: String,
|
||||
val title: String,
|
||||
val url: String,
|
||||
val score: Long,
|
||||
val flags: Long,
|
||||
@Json(name = "comment_count")
|
||||
val commentCount: Long,
|
||||
val description: String,
|
||||
@Json(name = "comments_url")
|
||||
val commentsUrl: String,
|
||||
@Json(name = "submitter_user")
|
||||
val submitterUser: Submitter,
|
||||
val tags: List<String>,
|
||||
)
|
|
@ -1,7 +1,9 @@
|
|||
package dev.msfjarvis.lobsters.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class Submitter(
|
||||
val username: String,
|
||||
@Json(name = "created_at")
|
||||
|
@ -21,5 +23,5 @@ class Submitter(
|
|||
@Json(name = "twitter_username")
|
||||
val twitterUsername: String? = null,
|
||||
@Json(name = "keybase_signatures")
|
||||
val keybaseSignatures: List<KeybaseSignature> = emptyList()
|
||||
val keybaseSignatures: List<KeybaseSignature> = emptyList(),
|
||||
)
|
|
@ -2,7 +2,6 @@ package dev.msfjarvis.lobsters.data.api
|
|||
|
||||
import com.squareup.moshi.Moshi
|
||||
import dev.msfjarvis.lobsters.util.TestUtils
|
||||
import dev.zacsweers.moshix.reflect.MetadataKotlinJsonAdapterFactory
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mockwebserver3.Dispatcher
|
||||
import mockwebserver3.MockResponse
|
||||
|
@ -24,7 +23,6 @@ class LobstersApiTest {
|
|||
private val webServer = MockWebServer()
|
||||
private val apiData = TestUtils.getJson("hottest.json")
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(MetadataKotlinJsonAdapterFactory())
|
||||
.build()
|
||||
private val okHttp = OkHttpClient.Builder()
|
||||
.build()
|
||||
|
@ -63,7 +61,7 @@ class LobstersApiTest {
|
|||
fun `no moderator posts in test data`() = runBlocking {
|
||||
val posts = apiClient.getHottestPosts(1)
|
||||
val moderatorPosts = posts.asSequence()
|
||||
.filter { it.submitter_user.isModerator }
|
||||
.filter { it.submitterUser.isModerator }
|
||||
.toSet()
|
||||
assertTrue(moderatorPosts.isEmpty())
|
||||
}
|
||||
|
|
|
@ -54,7 +54,6 @@ dependencies {
|
|||
implementation(Dependencies.Kotlin.Coroutines.android)
|
||||
implementation(Dependencies.ThirdParty.accompanist)
|
||||
implementation(Dependencies.ThirdParty.Moshi.lib)
|
||||
implementation(Dependencies.ThirdParty.Moshi.moshiMetadataReflect)
|
||||
implementation(Dependencies.ThirdParty.Retrofit.moshi)
|
||||
implementation(Dependencies.ThirdParty.SQLDelight.androidDriver)
|
||||
testImplementation(Dependencies.Testing.junit)
|
||||
|
|
|
@ -2,8 +2,8 @@ package dev.msfjarvis.lobsters.data.remote
|
|||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.repo.LobstersRepository
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import javax.inject.Inject
|
||||
|
||||
class LobstersPagingSource @Inject constructor(
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package dev.msfjarvis.lobsters.data.repo
|
||||
|
||||
import dev.msfjarvis.lobsters.data.api.LobstersApi
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.local.SavedPost
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import dev.msfjarvis.lobsters.database.LobstersDatabase
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -13,7 +14,7 @@ class LobstersRepository constructor(
|
|||
private val lobstersDatabase: LobstersDatabase,
|
||||
) {
|
||||
|
||||
private val savedPostsCache: MutableMap<String, LobstersPost> = mutableMapOf()
|
||||
private val savedPostsCache: MutableMap<String, SavedPost> = mutableMapOf()
|
||||
private val _isCacheReady = MutableStateFlow(false)
|
||||
val isCacheReady = _isCacheReady.asStateFlow()
|
||||
|
||||
|
@ -21,7 +22,7 @@ class LobstersRepository constructor(
|
|||
return savedPostsCache.containsKey(postId)
|
||||
}
|
||||
|
||||
fun getAllPostsFromCache(): List<LobstersPost> {
|
||||
fun getAllPostsFromCache(): List<SavedPost> {
|
||||
return savedPostsCache.values.toList()
|
||||
}
|
||||
|
||||
|
@ -31,29 +32,29 @@ class LobstersRepository constructor(
|
|||
|
||||
suspend fun updateCache() {
|
||||
if (_isCacheReady.value) return
|
||||
val posts = getAllPosts()
|
||||
val posts = getSavedPosts()
|
||||
|
||||
posts.forEach {
|
||||
savedPostsCache[it.short_id] = it
|
||||
savedPostsCache[it.shortId] = it
|
||||
}
|
||||
_isCacheReady.value = true
|
||||
}
|
||||
|
||||
private suspend fun getAllPosts(): List<LobstersPost> = withContext(Dispatchers.IO) {
|
||||
return@withContext lobstersDatabase.postQueries.selectAllPosts().executeAsList()
|
||||
private suspend fun getSavedPosts(): List<SavedPost> = withContext(Dispatchers.IO) {
|
||||
return@withContext lobstersDatabase.savedPostQueries.selectAllPosts().executeAsList()
|
||||
}
|
||||
|
||||
suspend fun addPost(post: LobstersPost) = withContext(Dispatchers.IO) {
|
||||
if (!savedPostsCache.containsKey(post.short_id)) {
|
||||
savedPostsCache.putIfAbsent(post.short_id, post)
|
||||
lobstersDatabase.postQueries.insertOrReplacePost(post)
|
||||
suspend fun addPost(post: SavedPost) = withContext(Dispatchers.IO) {
|
||||
if (!savedPostsCache.containsKey(post.shortId)) {
|
||||
savedPostsCache.putIfAbsent(post.shortId, post)
|
||||
lobstersDatabase.savedPostQueries.insertOrReplacePost(post)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removePost(post: LobstersPost) = withContext(Dispatchers.IO) {
|
||||
if (savedPostsCache.containsKey(post.short_id)) {
|
||||
savedPostsCache.remove(post.short_id)
|
||||
lobstersDatabase.postQueries.deletePost(post.short_id)
|
||||
suspend fun removePost(post: SavedPost) = withContext(Dispatchers.IO) {
|
||||
if (savedPostsCache.containsKey(post.shortId)) {
|
||||
savedPostsCache.remove(post.shortId)
|
||||
lobstersDatabase.savedPostQueries.deletePost(post.shortId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package dev.msfjarvis.lobsters.injection
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import dagger.Module
|
||||
|
@ -10,10 +9,8 @@ import dagger.Reusable
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.local.SavedPost
|
||||
import dev.msfjarvis.lobsters.database.LobstersDatabase
|
||||
import dev.msfjarvis.lobsters.model.Submitter
|
||||
import dev.msfjarvis.lobsters.model.SubmitterAdapter
|
||||
import dev.msfjarvis.lobsters.model.TagsAdapter
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -21,12 +18,6 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Reusable
|
||||
fun providesSubmitterAdapter(jsonAdapter: JsonAdapter<Submitter>): SubmitterAdapter {
|
||||
return SubmitterAdapter(jsonAdapter)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Reusable
|
||||
fun providesTagsAdapter(): TagsAdapter {
|
||||
|
@ -43,12 +34,11 @@ object DatabaseModule {
|
|||
@Singleton
|
||||
fun providesLobstersDatabase(
|
||||
sqlDriver: SqlDriver,
|
||||
submitterAdapter: SubmitterAdapter,
|
||||
tagsAdapter: TagsAdapter
|
||||
): LobstersDatabase {
|
||||
return LobstersDatabase(
|
||||
sqlDriver,
|
||||
LobstersPost.Adapter(submitterAdapter, tagsAdapter)
|
||||
SavedPost.Adapter(tagsAdapter),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
package dev.msfjarvis.lobsters.injection
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.msfjarvis.lobsters.model.Submitter
|
||||
import dev.zacsweers.moshix.reflect.MetadataKotlinJsonAdapterFactory
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
|
@ -18,14 +14,6 @@ object MoshiModule {
|
|||
@Reusable
|
||||
fun provideMoshi(): Moshi {
|
||||
return Moshi.Builder()
|
||||
.add(MetadataKotlinJsonAdapterFactory())
|
||||
.build()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Provides
|
||||
@Reusable
|
||||
fun provideSubmitterJsonAdapter(moshi: Moshi): JsonAdapter<Submitter> {
|
||||
return moshi.adapter()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,10 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.local.SavedPost
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher
|
||||
import dev.msfjarvis.lobsters.util.toDbModel
|
||||
|
||||
@Composable
|
||||
fun HottestPosts(
|
||||
|
@ -20,7 +22,7 @@ fun HottestPosts(
|
|||
listState: LazyListState,
|
||||
isPostSaved: (String) -> Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
saveAction: (LobstersPost) -> Unit,
|
||||
saveAction: (SavedPost) -> Unit,
|
||||
) {
|
||||
val urlLauncher = LocalUrlLauncher.current
|
||||
|
||||
|
@ -33,13 +35,15 @@ fun HottestPosts(
|
|||
) {
|
||||
items(posts) { item ->
|
||||
if (item != null) {
|
||||
var isSaved by remember(item.short_id) { mutableStateOf(isPostSaved(item.short_id)) }
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val item = item.toDbModel()
|
||||
var isSaved by remember(item.shortId) { mutableStateOf(isPostSaved(item.shortId)) }
|
||||
|
||||
LobstersItem(
|
||||
post = item,
|
||||
isSaved = isSaved,
|
||||
onClick = { urlLauncher.launch(item.url.ifEmpty { item.comments_url }) },
|
||||
onLongClick = { urlLauncher.launch(item.comments_url) },
|
||||
onClick = { urlLauncher.launch(item.url.ifEmpty { item.commentsUrl }) },
|
||||
onLongClick = { urlLauncher.launch(item.commentsUrl) },
|
||||
onSaveButtonClick = {
|
||||
isSaved = isSaved.not()
|
||||
saveAction.invoke(item)
|
||||
|
|
|
@ -29,43 +29,26 @@ import coil.transform.CircleCropTransformation
|
|||
import dev.chrisbanes.accompanist.coil.CoilImage
|
||||
import dev.msfjarvis.lobsters.R
|
||||
import dev.msfjarvis.lobsters.data.api.LobstersApi
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.model.Submitter
|
||||
import dev.msfjarvis.lobsters.data.local.SavedPost
|
||||
import dev.msfjarvis.lobsters.ui.theme.LobstersTheme
|
||||
import dev.msfjarvis.lobsters.ui.theme.titleColor
|
||||
import dev.msfjarvis.lobsters.util.IconResource
|
||||
|
||||
val TEST_POST = LobstersPost(
|
||||
"zqyydb",
|
||||
"https://lobste.rs/s/zqyydb",
|
||||
"2020-09-21T07:11:14.000-05:00",
|
||||
"k2k20 hackathon report: Bob Beck on LibreSSL progress",
|
||||
"https://undeadly.org/cgi?action=article;sid=20200921105847",
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
"https://lobste.rs/s/zqyydb/k2k20_hackathon_report_bob_beck_on",
|
||||
Submitter(
|
||||
"Vigdis",
|
||||
"2017-02-27T21:08:14.000-06:00",
|
||||
false,
|
||||
"Alleycat for the fun, sys/net admin for a living and OpenBSD contributions for the pleasure. (Not so) French dude in Montreal\r\n\r\nhttps://chown.me",
|
||||
false,
|
||||
76,
|
||||
"/avatars/Vigdis-100.png",
|
||||
"sevan",
|
||||
null,
|
||||
null,
|
||||
emptyList(),
|
||||
),
|
||||
listOf("openbsd", "linux", "containers", "hack the planet", "no thanks"),
|
||||
val TEST_POST = SavedPost(
|
||||
shortId = "zqyydb",
|
||||
title = "k2k20 hackathon report: Bob Beck on LibreSSL progress",
|
||||
url = "https://undeadly.org/cgi?action=article;sid=20200921105847",
|
||||
createdAt = "2020-09-21T07:11:14.000-05:00",
|
||||
commentsUrl = "https://lobste.rs/s/zqyydb/k2k20_hackathon_report_bob_beck_on",
|
||||
submitterName = "Vigdis",
|
||||
submitterAvatarUrl = "/avatars/Vigdis-100.png",
|
||||
tags = listOf("openbsd", "linux", "containers", "hack the planet", "no thanks"),
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LobstersItem(
|
||||
post: LobstersPost,
|
||||
post: SavedPost,
|
||||
isSaved: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
|
@ -99,10 +82,10 @@ fun LobstersItem(
|
|||
)
|
||||
Row {
|
||||
CoilImage(
|
||||
data = "${LobstersApi.BASE_URL}/${post.submitter_user.avatarUrl}",
|
||||
data = "${LobstersApi.BASE_URL}/${post.submitterAvatarUrl}",
|
||||
contentDescription = stringResource(
|
||||
R.string.avatar_content_description,
|
||||
post.submitter_user.username
|
||||
post.submitterName
|
||||
),
|
||||
fadeIn = true,
|
||||
requestBuilder = {
|
||||
|
@ -113,7 +96,7 @@ fun LobstersItem(
|
|||
.padding(4.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.submitted_by, post.submitter_user.username),
|
||||
text = stringResource(id = R.string.submitted_by, post.submitterName),
|
||||
modifier = Modifier
|
||||
.padding(4.dp),
|
||||
)
|
||||
|
|
|
@ -6,16 +6,16 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.local.SavedPost
|
||||
import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher
|
||||
import dev.msfjarvis.lobsters.util.asZonedDateTime
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SavedPosts(
|
||||
posts: List<LobstersPost>,
|
||||
posts: List<SavedPost>,
|
||||
modifier: Modifier = Modifier,
|
||||
saveAction: (LobstersPost) -> Unit,
|
||||
saveAction: (SavedPost) -> Unit,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val urlLauncher = LocalUrlLauncher.current
|
||||
|
@ -27,7 +27,7 @@ fun SavedPosts(
|
|||
state = listState,
|
||||
modifier = Modifier.then(modifier),
|
||||
) {
|
||||
val grouped = posts.groupBy { it.created_at.asZonedDateTime().month }
|
||||
val grouped = posts.groupBy { it.createdAt.asZonedDateTime().month }
|
||||
grouped.forEach { (month, posts) ->
|
||||
stickyHeader {
|
||||
MonthHeader(month = month)
|
||||
|
@ -36,8 +36,8 @@ fun SavedPosts(
|
|||
LobstersItem(
|
||||
post = item,
|
||||
isSaved = true,
|
||||
onClick = { urlLauncher.launch(item.url.ifEmpty { item.comments_url }) },
|
||||
onLongClick = { urlLauncher.launch(item.comments_url) },
|
||||
onClick = { urlLauncher.launch(item.url.ifEmpty { item.commentsUrl }) },
|
||||
onLongClick = { urlLauncher.launch(item.commentsUrl) },
|
||||
onSaveButtonClick = { saveAction.invoke(item) },
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import androidx.paging.Pager
|
|||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.local.SavedPost
|
||||
import dev.msfjarvis.lobsters.data.remote.LobstersPagingSource
|
||||
import dev.msfjarvis.lobsters.data.repo.LobstersRepository
|
||||
import javax.inject.Inject
|
||||
|
@ -21,7 +21,7 @@ class LobstersViewModel @Inject constructor(
|
|||
private val lobstersRepository: LobstersRepository,
|
||||
private val pagingSource: LobstersPagingSource,
|
||||
) : ViewModel() {
|
||||
private val _savedPosts = MutableStateFlow<List<LobstersPost>>(emptyList())
|
||||
private val _savedPosts = MutableStateFlow<List<SavedPost>>(emptyList())
|
||||
val savedPosts = _savedPosts.asStateFlow()
|
||||
val posts = Pager(PagingConfig(25)) {
|
||||
pagingSource
|
||||
|
@ -35,9 +35,9 @@ class LobstersViewModel @Inject constructor(
|
|||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun toggleSave(post: LobstersPost) {
|
||||
fun toggleSave(post: SavedPost) {
|
||||
viewModelScope.launch {
|
||||
val isSaved = lobstersRepository.isPostSaved(post.short_id)
|
||||
val isSaved = lobstersRepository.isPostSaved(post.shortId)
|
||||
if (isSaved) removeSavedPost(post) else savePost(post)
|
||||
}
|
||||
}
|
||||
|
@ -46,14 +46,14 @@ class LobstersViewModel @Inject constructor(
|
|||
return lobstersRepository.isPostSaved(postId)
|
||||
}
|
||||
|
||||
private fun savePost(post: LobstersPost) {
|
||||
private fun savePost(post: SavedPost) {
|
||||
viewModelScope.launch {
|
||||
lobstersRepository.addPost(post)
|
||||
_savedPosts.value = lobstersRepository.getAllPostsFromCache()
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeSavedPost(post: LobstersPost) {
|
||||
private fun removeSavedPost(post: SavedPost) {
|
||||
viewModelScope.launch {
|
||||
lobstersRepository.removePost(post)
|
||||
_savedPosts.value = lobstersRepository.getAllPostsFromCache()
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package dev.msfjarvis.lobsters.util
|
||||
|
||||
import dev.msfjarvis.lobsters.data.local.SavedPost
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
|
||||
/**
|
||||
* Convert a [LobstersPost] object returned by the API into a [SavedPost] for persistence.
|
||||
*/
|
||||
fun LobstersPost.toDbModel(): SavedPost {
|
||||
return SavedPost(
|
||||
shortId = shortId,
|
||||
title = title,
|
||||
url = url,
|
||||
createdAt = createdAt,
|
||||
commentsUrl = commentsUrl,
|
||||
submitterName = submitterUser.username,
|
||||
submitterAvatarUrl = submitterUser.avatarUrl,
|
||||
tags = tags,
|
||||
)
|
||||
}
|
|
@ -1,3 +1,12 @@
|
|||
plugins {
|
||||
`lobsters-plugin`
|
||||
}
|
||||
|
||||
subprojects {
|
||||
configurations.configureEach {
|
||||
resolutionStrategy {
|
||||
// Retrofit depends on a very old version of Moshi that causes moshi-ksp to fail
|
||||
force(Dependencies.ThirdParty.Moshi.lib)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ object Dependencies {
|
|||
|
||||
private const val version = "1.11.0"
|
||||
const val lib = "com.squareup.moshi:moshi:$version"
|
||||
const val moshiMetadataReflect = "dev.zacsweers.moshix:moshi-metadata-reflect:0.9.1"
|
||||
const val ksp = "dev.zacsweers.moshix:moshi-ksp:0.9.1"
|
||||
}
|
||||
|
||||
object Retrofit {
|
||||
|
|
|
@ -5,8 +5,6 @@ plugins {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(Dependencies.ThirdParty.Moshi.lib)
|
||||
implementation(Dependencies.ThirdParty.Moshi.moshiMetadataReflect)
|
||||
testImplementation(Dependencies.Kotlin.Coroutines.core)
|
||||
testImplementation(Dependencies.ThirdParty.SQLDelight.jvmDriver)
|
||||
testImplementation(Dependencies.Testing.junit)
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
package dev.msfjarvis.lobsters.model
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.sqldelight.ColumnAdapter
|
||||
|
||||
class SubmitterAdapter(private val submitterJsonAdapter: JsonAdapter<Submitter>) :
|
||||
ColumnAdapter<Submitter, String> {
|
||||
|
||||
override fun decode(databaseValue: String): Submitter {
|
||||
return submitterJsonAdapter.fromJson(databaseValue)!!
|
||||
}
|
||||
|
||||
override fun encode(value: Submitter): String {
|
||||
return submitterJsonAdapter.toJson(value)
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import dev.msfjarvis.lobsters.model.Submitter;
|
||||
import java.lang.Boolean;
|
||||
import kotlin.collections.List;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS LobstersPost(
|
||||
short_id TEXT NOT NULL PRIMARY KEY,
|
||||
short_id_url TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
score INTEGER NOT NULL,
|
||||
flags INTEGER NOT NULL,
|
||||
comment_count INTEGER NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
comments_url TEXT NOT NULL,
|
||||
submitter_user TEXT as Submitter NOT NULL,
|
||||
tags TEXT as List<String> NOT NULL
|
||||
);
|
||||
|
||||
selectPost:
|
||||
SELECT *
|
||||
FROM LobstersPost
|
||||
WHERE short_id = ?;
|
||||
|
||||
selectAllPosts:
|
||||
SELECT *
|
||||
FROM LobstersPost;
|
||||
|
||||
insertOrReplacePost:
|
||||
INSERT OR REPLACE
|
||||
INTO LobstersPost
|
||||
VALUES ?;
|
||||
|
||||
deletePost:
|
||||
DELETE
|
||||
FROM LobstersPost
|
||||
WHERE short_id = ?;
|
||||
|
||||
deleteAllPosts:
|
||||
DELETE
|
||||
FROM LobstersPost;
|
||||
|
||||
selectCount:
|
||||
SELECT COUNT(*)
|
||||
FROM LobstersPost;
|
|
@ -0,0 +1,39 @@
|
|||
import kotlin.collections.List;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS SavedPost(
|
||||
shortId TEXT NOT NULL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
createdAt TEXT NOT NULL,
|
||||
commentsUrl TEXT NOT NULL,
|
||||
submitterName TEXT NOT NULL,
|
||||
submitterAvatarUrl TEXT NOT NULL,
|
||||
tags TEXT as List<String> NOT NULL
|
||||
);
|
||||
|
||||
insertOrReplacePost:
|
||||
INSERT OR REPLACE
|
||||
INTO SavedPost
|
||||
VALUES ?;
|
||||
|
||||
selectAllPosts:
|
||||
SELECT *
|
||||
FROM SavedPost;
|
||||
|
||||
selectCount:
|
||||
SELECT COUNT(*)
|
||||
FROM SavedPost;
|
||||
|
||||
deleteAllPosts:
|
||||
DELETE
|
||||
FROM SavedPost;
|
||||
|
||||
deletePost:
|
||||
DELETE
|
||||
FROM SavedPost
|
||||
WHERE shortId = ?;
|
||||
|
||||
selectPost:
|
||||
SELECT *
|
||||
FROM SavedPost
|
||||
WHERE shortId = ?;
|
|
@ -1,13 +1,8 @@
|
|||
package dev.msfjarvis.lobsters.data.local
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
|
||||
import dev.msfjarvis.lobsters.database.LobstersDatabase
|
||||
import dev.msfjarvis.lobsters.model.Submitter
|
||||
import dev.msfjarvis.lobsters.model.SubmitterAdapter
|
||||
import dev.msfjarvis.lobsters.model.TagsAdapter
|
||||
import dev.zacsweers.moshix.reflect.MetadataKotlinJsonAdapterFactory
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
|
@ -16,19 +11,17 @@ import org.junit.Test
|
|||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class SqlDelightQueriesTest {
|
||||
|
||||
private lateinit var postQueries: PostQueries
|
||||
private lateinit var postQueries: SavedPostQueries
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val moshi = Moshi.Builder().add(MetadataKotlinJsonAdapterFactory()).build()
|
||||
val submitterJsonAdapter = moshi.adapter<Submitter>()
|
||||
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
|
||||
LobstersDatabase.Schema.create(driver)
|
||||
val database = LobstersDatabase(
|
||||
driver,
|
||||
LobstersPost.Adapter(SubmitterAdapter(submitterJsonAdapter), TagsAdapter())
|
||||
SavedPost.Adapter(TagsAdapter()),
|
||||
)
|
||||
postQueries = database.postQueries
|
||||
postQueries = database.savedPostQueries
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -63,7 +56,7 @@ class SqlDelightQueriesTest {
|
|||
postQueries.insertOrReplacePost(post)
|
||||
|
||||
// Create a new post and try replacing it
|
||||
val newPost = post.copy(comment_count = 100)
|
||||
val newPost = post.copy(submitterName = "Fake name")
|
||||
postQueries.insertOrReplacePost(newPost)
|
||||
|
||||
// Check post count
|
||||
|
@ -71,8 +64,8 @@ class SqlDelightQueriesTest {
|
|||
assertEquals(1, postsCount)
|
||||
|
||||
// Check if post is updated
|
||||
val postFromDb = postQueries.selectPost(post.short_id).executeAsOne()
|
||||
assertEquals(100, postFromDb.comment_count)
|
||||
val postFromDb = postQueries.selectPost(post.shortId).executeAsOne()
|
||||
assertEquals("Fake name", postFromDb.submitterName)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -84,7 +77,7 @@ class SqlDelightQueriesTest {
|
|||
postQueries.insertOrReplacePost(post)
|
||||
|
||||
val postFromDb = postQueries.selectAllPosts().executeAsOne()
|
||||
assertEquals("test_id_1", postFromDb.short_id)
|
||||
assertEquals("test_id_1", postFromDb.shortId)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -97,9 +90,9 @@ class SqlDelightQueriesTest {
|
|||
|
||||
val postsFromDb = postQueries.selectAllPosts().executeAsList()
|
||||
|
||||
// Check if all posts have correct short_id
|
||||
// Check if all posts have correct shortId
|
||||
for (i in 1..5) {
|
||||
assertEquals("test_id_$i", postsFromDb[i - 1].short_id)
|
||||
assertEquals("test_id_$i", postsFromDb[i - 1].shortId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,8 +109,8 @@ class SqlDelightQueriesTest {
|
|||
|
||||
// Check if size is 2, and only the correct post is deleted
|
||||
assertEquals(2, postsFromDB.size)
|
||||
assertEquals("test_id_1", postsFromDB[0].short_id)
|
||||
assertEquals("test_id_3", postsFromDB[1].short_id)
|
||||
assertEquals("test_id_1", postsFromDB[0].shortId)
|
||||
assertEquals("test_id_3", postsFromDB[1].shortId)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -136,32 +129,18 @@ class SqlDelightQueriesTest {
|
|||
}
|
||||
|
||||
|
||||
private fun createTestData(count: Int): ArrayList<LobstersPost> {
|
||||
val posts = arrayListOf<LobstersPost>()
|
||||
private fun createTestData(count: Int): ArrayList<SavedPost> {
|
||||
val posts = arrayListOf<SavedPost>()
|
||||
|
||||
for (i in 1..count) {
|
||||
val submitter = Submitter(
|
||||
username = "test_user_$i",
|
||||
val post = SavedPost(
|
||||
shortId = "test_id_$i",
|
||||
createdAt = "0",
|
||||
about = "test",
|
||||
avatarUrl = "test_avatar_url",
|
||||
invitedByUser = "test_user",
|
||||
isAdmin = false,
|
||||
isModerator = false
|
||||
)
|
||||
|
||||
val post = LobstersPost(
|
||||
short_id = "test_id_$i",
|
||||
short_id_url = "test_id_url",
|
||||
created_at = "0",
|
||||
title = "test",
|
||||
url = "test_url",
|
||||
score = 0,
|
||||
flags = 0,
|
||||
comment_count = 0,
|
||||
description = "test",
|
||||
comments_url = "test_comments_url",
|
||||
submitter_user = submitter,
|
||||
commentsUrl = "test_comments_url",
|
||||
submitterName = "test_user_$i",
|
||||
submitterAvatarUrl = "test_avatar_url",
|
||||
tags = listOf(),
|
||||
)
|
||||
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
rootProject.name = "Claw"
|
||||
include(":app", ":api", ":database")
|
||||
enableFeaturePreview("GRADLE_METADATA")
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue