diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt index 5e1656982..d85be98cf 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt @@ -1,12 +1,14 @@ package app.omnivore.omnivore import android.content.Context +import android.util.Log import androidx.room.Room import app.omnivore.omnivore.networking.Networker import app.omnivore.omnivore.networking.savedItem import app.omnivore.omnivore.networking.savedItemUpdates import app.omnivore.omnivore.persistence.AppDatabase import app.omnivore.omnivore.persistence.entities.SavedItem +import app.omnivore.omnivore.persistence.entities.SavedItemAndHighlightCrossRef import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef import app.omnivore.omnivore.persistence.entities.SavedItemLabel import javax.inject.Inject @@ -89,7 +91,28 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): S suspend fun DataService.syncSavedItemContent(slug: String) { val syncResult = networker.savedItem(slug) -// syncResult.item + val savedItem = syncResult.item ?: return + db.savedItemDao().insert(savedItem) + + // Persist Labels + db.savedItemLabelDao().insertAll(syncResult.labels) + + val labelCrossRefs = syncResult.labels.map { + SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = savedItem.savedItemId) + } + + db.savedItemAndSavedItemLabelCrossRefDao().insertAll(labelCrossRefs) + + // Persist Highlights + db.highlightDao().insertAll(syncResult.highlights) + + val highlightCrossRefs = syncResult.highlights.map { + SavedItemAndHighlightCrossRef(highlightId = it.highlightId, savedItemId = savedItem.savedItemId) + } + + db.savedItemAndHighlightCrossRefDao().insertAll(highlightCrossRefs) + + Log.d("sync", "saved content for item with id: ${savedItem.savedItemId}") } suspend fun DataService.syncOfflineItemsWithServerIfNeeded() { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt index 2130a2f9a..56faf0998 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt @@ -9,13 +9,17 @@ import app.omnivore.omnivore.persistence.entities.* Viewer::class, SavedItem::class, SavedItemLabel::class, - SavedItemAndSavedItemLabelCrossRef::class + Highlight::class, + SavedItemAndSavedItemLabelCrossRef::class, + SavedItemAndHighlightCrossRef::class ], version = 2 ) abstract class AppDatabase : RoomDatabase() { abstract fun viewerDao(): ViewerDao abstract fun savedItemDao(): SavedItemDao + abstract fun highlightDao(): HighlightDao abstract fun savedItemLabelDao(): SavedItemLabelDao abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao + abstract fun savedItemAndHighlightCrossRefDao(): SavedItemAndHighlightCrossRefDao } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt index ebe3eed84..fcfb76ba6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt @@ -1,7 +1,6 @@ package app.omnivore.omnivore.persistence.entities -import androidx.room.Entity -import androidx.room.PrimaryKey +import androidx.room.* import java.time.LocalDate import java.util.Date @@ -9,7 +8,7 @@ import java.util.Date data class Highlight( @PrimaryKey val highlightId: String, val annotation: String?, - val createdAt: Date?, + val createdAt: String?, val createdByMe: Boolean, val markedForDeletion: Boolean, // default false val patch: String, @@ -18,9 +17,45 @@ data class Highlight( val serverSyncStatus: Int, // default 0 val shortId: String, val suffix: String?, - val updatedAt: LocalDate? + val updatedAt: String? // has many SavedItemLabels (inverse: labels have many highlights) // has one savedItem (inverse: savedItem has many highlights // has a UserProfile (no inverse) ) + +@Entity(primaryKeys = ["highlightId", "savedItemId"]) +data class SavedItemAndHighlightCrossRef( + val highlightId: String, + val savedItemId: String +) + +@Dao +interface SavedItemAndHighlightCrossRefDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAll(items: List) +} + +data class SavedItemWithLabelsAndHighlights( + @Embedded val savedItem: SavedItem, + + @Relation( + parentColumn = "savedItemId", + entityColumn = "savedItemLabelId", + associateBy = Junction(SavedItemAndSavedItemLabelCrossRef::class) + ) + val labels: List, + + @Relation( + parentColumn = "savedItemId", + entityColumn = "highlightId", + associateBy = Junction(SavedItemAndHighlightCrossRef::class) + ) + val highlights: List +) + +@Dao +interface HighlightDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAll(items: List) +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index 33c37d2c1..440ed2461 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -92,9 +92,15 @@ interface SavedItemDao { @Query("SELECT * FROM savedItem") fun getAll(): List + @Query("SELECT * FROM savedItem WHERE slug = :slug") + fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights? + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(items: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(item: SavedItem) + @Query("DELETE FROM savedItem WHERE savedItemId = :itemID") fun deleteById(itemID: String) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt index 26ac468a5..512c1c564 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt @@ -1,6 +1,5 @@ package app.omnivore.omnivore.persistence.entities -import androidx.lifecycle.LiveData import androidx.room.* @Entity diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt index 3dc93ad44..a67afa781 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt @@ -83,6 +83,12 @@ class LibraryViewModel @Inject constructor( private suspend fun performItemSync(cursor: String?, since: String, count: Int, startTime: String) { dataService.syncOfflineItemsWithServerIfNeeded() val result = dataService.sync(since = since, cursor = cursor) + + // TODO: Defer this until later? + for (slug in result.savedItemSlugs) { + dataService.syncSavedItemContent(slug) + } + val totalCount = count + result.count Log.d("sync", "fetched ${result.count} items") diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt index fe6c1b103..21835f088 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.ScrollState import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.omnivore.omnivore.DataService import app.omnivore.omnivore.DatastoreKeys import app.omnivore.omnivore.DatastoreRepository import app.omnivore.omnivore.persistence.entities.SavedItem @@ -12,10 +13,7 @@ import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.ui.library.SavedItemAction import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import java.util.* import javax.inject.Inject @@ -31,6 +29,7 @@ data class AnnotationWebViewMessage( @HiltViewModel class WebReaderViewModel @Inject constructor( private val datastoreRepo: DatastoreRepository, + private val dataService: DataService, private val networker: Networker ): ViewModel() { var lastJavascriptActionLoopUUID: UUID = UUID.randomUUID() @@ -47,23 +46,47 @@ class WebReaderViewModel @Inject constructor( fun loadItem(slug: String) { viewModelScope.launch { - val articleQueryResult = networker.savedItem(slug) + // Attempt to load from db first + withContext(Dispatchers.IO) { + val persistedItem = dataService.db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug) - val article = articleQueryResult.item ?: return@launch + if (persistedItem?.savedItem != null) { + val articleContent = ArticleContent( + title = persistedItem.savedItem.title, + htmlContent = persistedItem.savedItem.content ?: "", + highlights = persistedItem.highlights, + contentStatus = "SUCCEEDED", + objectID = "", + labelsJSONString = Gson().toJson(persistedItem.labels) + ) - val articleContent = ArticleContent( - title = article.title, - htmlContent = article.content ?: "", - highlights = articleQueryResult.highlights, - contentStatus = "SUCCEEDED", - objectID = "", - labelsJSONString = Gson().toJson(articleQueryResult.labels) - ) - - webReaderParamsLiveData.value = WebReaderParams(article, articleContent) + Log.d("sync", "data loaded from db") + webReaderParamsLiveData.postValue(WebReaderParams(persistedItem.savedItem, articleContent)) + } else { + loadItemFromServer(slug) + } + } } } + private suspend fun loadItemFromServer(slug: String) { + val articleQueryResult = networker.savedItem(slug) + + val article = articleQueryResult.item ?: return + + val articleContent = ArticleContent( + title = article.title, + htmlContent = article.content ?: "", + highlights = articleQueryResult.highlights, + contentStatus = "SUCCEEDED", + objectID = "", + labelsJSONString = Gson().toJson(articleQueryResult.labels) + ) + + Log.d("sync", "data loaded from server") + webReaderParamsLiveData.postValue(WebReaderParams(article, articleContent)) + } + fun handleSavedItemAction(itemID: String, action: SavedItemAction) { when (action) { SavedItemAction.Delete -> {