diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/LibrarySync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/LibrarySync.kt index f3ffba8a6..fc4e469b3 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/LibrarySync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/LibrarySync.kt @@ -1,160 +1,151 @@ package app.omnivore.omnivore.core.data import android.util.Log +import app.omnivore.omnivore.core.data.model.ServerSyncStatus import app.omnivore.omnivore.core.database.entities.Highlight import app.omnivore.omnivore.core.database.entities.SavedItem import app.omnivore.omnivore.core.database.entities.SavedItemLabel import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights -import app.omnivore.omnivore.core.network.savedItem import app.omnivore.omnivore.core.network.savedItemUpdates import app.omnivore.omnivore.core.network.search -import app.omnivore.omnivore.core.data.model.ServerSyncStatus suspend fun DataService.librarySearch(cursor: String?, query: String): SearchResult { - val searchResult = networker.search(cursor = cursor, limit = 10, query = query) + val searchResult = networker.search(cursor = cursor, limit = 10, query = query) - val savedItems = searchResult.items.map { - SavedItemWithLabelsAndHighlights( - savedItem = it.item, - labels = it.labels, - highlights = it.highlights, + val savedItems = searchResult.items.map { + SavedItemWithLabelsAndHighlights( + savedItem = it.item, + labels = it.labels, + highlights = it.highlights, + ) + } + + db.savedItemWithLabelsAndHighlightsDao().insertAll(savedItems) + + Log.d( + "sync", + "found ${searchResult.items.size} items with search api. Query: $query cursor: $cursor" ) - } - db.savedItemWithLabelsAndHighlightsDao().insertAll(savedItems) - - Log.d("sync", "found ${searchResult.items.size} items with search api. Query: $query cursor: $cursor") - - return SearchResult( - hasError = false, - hasMoreItems = false, - cursor = searchResult.cursor, - count = searchResult.items.size, - savedItems = savedItems - ) + return SearchResult( + hasError = false, + hasMoreItems = false, + cursor = searchResult.cursor, + count = searchResult.items.size, + savedItems = savedItems + ) } suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): SavedItemSyncResult { - val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) - ?: return SavedItemSyncResult.errorResult + val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) + ?: return SavedItemSyncResult.errorResult - if (syncResult.deletedItemIDs.isNotEmpty()) { - db.savedItemDao().deleteByIds(syncResult.deletedItemIDs) - } + if (syncResult.deletedItemIDs.isNotEmpty()) { + db.savedItemDao().deleteByIds(syncResult.deletedItemIDs) + } - val savedItems = syncResult.items.map { - val savedItem = SavedItem( - savedItemId = it.id, - title = it.title, - createdAt = it.createdAt as String, - savedAt = it.savedAt as String, - readAt = it.readAt as String?, - updatedAt = it.updatedAt as String?, - readingProgress = it.readingProgressPercent, - readingProgressAnchor = it.readingProgressAnchorIndex, - imageURLString = it.image, - pageURLString = it.url, - descriptionText = it.description, - publisherURLString = it.originalArticleUrl, - siteName = it.siteName, - author = it.author, - publishDate = it.publishedAt as String?, - slug = it.slug, - isArchived = it.isArchived, - contentReader = it.contentReader.rawValue, - content = null, - wordsCount = it.wordsCount - ) - val labels = it.labels?.map { label -> - SavedItemLabel( - savedItemLabelId = label.labelFields.id, - name = label.labelFields.name, - color = label.labelFields.color, - createdAt = null, - labelDescription = null - ) - } ?: listOf() - val highlights = it.highlights?.map { highlight -> - Highlight( - type = highlight.highlightFields.type.toString(), - highlightId = highlight.highlightFields.id, - annotation = highlight.highlightFields.annotation, - createdByMe = highlight.highlightFields.createdByMe, - markedForDeletion = false, - patch = highlight.highlightFields.patch, - prefix = highlight.highlightFields.prefix, - quote = highlight.highlightFields.quote, - serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue, - shortId = highlight.highlightFields.shortId, - suffix = highlight.highlightFields.suffix, - createdAt = null, - updatedAt = highlight.highlightFields.updatedAt as String?, - color = highlight.highlightFields.color, - highlightPositionPercent = highlight.highlightFields.highlightPositionPercent, - highlightPositionAnchorIndex = highlight.highlightFields.highlightPositionAnchorIndex, - ) - } ?: listOf() - SavedItemWithLabelsAndHighlights( - savedItem = savedItem, - labels = labels, - highlights = highlights - ) - } + val savedItems = syncResult.items.map { + val savedItem = SavedItem( + savedItemId = it.id, + title = it.title, + createdAt = it.createdAt as String, + savedAt = it.savedAt as String, + readAt = it.readAt as String?, + updatedAt = it.updatedAt as String?, + readingProgress = it.readingProgressPercent, + readingProgressAnchor = it.readingProgressAnchorIndex, + imageURLString = it.image, + pageURLString = it.url, + descriptionText = it.description, + publisherURLString = it.originalArticleUrl, + siteName = it.siteName, + author = it.author, + publishDate = it.publishedAt as String?, + slug = it.slug, + isArchived = it.isArchived, + contentReader = it.contentReader.rawValue, + content = null, + wordsCount = it.wordsCount + ) + val labels = it.labels?.map { label -> + SavedItemLabel( + savedItemLabelId = label.labelFields.id, + name = label.labelFields.name, + color = label.labelFields.color, + createdAt = null, + labelDescription = null + ) + } ?: listOf() + val highlights = it.highlights?.map { highlight -> + Highlight( + type = highlight.highlightFields.type.toString(), + highlightId = highlight.highlightFields.id, + annotation = highlight.highlightFields.annotation, + createdByMe = highlight.highlightFields.createdByMe, + markedForDeletion = false, + patch = highlight.highlightFields.patch, + prefix = highlight.highlightFields.prefix, + quote = highlight.highlightFields.quote, + serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue, + shortId = highlight.highlightFields.shortId, + suffix = highlight.highlightFields.suffix, + createdAt = null, + updatedAt = highlight.highlightFields.updatedAt as String?, + color = highlight.highlightFields.color, + highlightPositionPercent = highlight.highlightFields.highlightPositionPercent, + highlightPositionAnchorIndex = highlight.highlightFields.highlightPositionAnchorIndex, + ) + } ?: listOf() + SavedItemWithLabelsAndHighlights( + savedItem = savedItem, labels = labels, highlights = highlights + ) + } - db.savedItemWithLabelsAndHighlightsDao().insertAll(savedItems) + db.savedItemWithLabelsAndHighlightsDao().insertAll(savedItems) - Log.d("sync", "found ${syncResult.items.size} items with sync api. Since: $since") + Log.d("sync", "found ${syncResult.items.size} items with sync api. Since: $since") - return SavedItemSyncResult( - hasError = false, - hasMoreItems = syncResult.hasMoreItems, - cursor = syncResult.cursor, - count = syncResult.items.size, - savedItemSlugs = syncResult.items.map { it.slug } - ) + return SavedItemSyncResult(hasError = false, + hasMoreItems = syncResult.hasMoreItems, + cursor = syncResult.cursor, + count = syncResult.items.size, + savedItemSlugs = syncResult.items.map { it.slug }) } -fun DataService.isSavedItemContentStoredInDB(slug: String): Boolean { - val existingItem = db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug) - val content = existingItem?.savedItem?.content ?: "" - return content.length > 10 +suspend fun DataService.isSavedItemContentStoredInDB(slug: String): Boolean { + val existingItem = db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug) + val content = existingItem?.savedItem?.content ?: "" + return content.length > 10 } -suspend fun DataService.fetchSavedItemContent(slug: String) { - val syncResult = networker.savedItem(slug) - - val savedItem = syncResult.item - savedItem?.let { - val item = SavedItemWithLabelsAndHighlights( - savedItem = savedItem, - labels = syncResult.labels, - highlights = syncResult.highlights - ) - db.savedItemWithLabelsAndHighlightsDao().insertAll(listOf(item)) - } -} - - data class SavedItemSyncResult( - val hasError: Boolean, - val hasMoreItems: Boolean, - val count: Int, - val savedItemSlugs: List, - val cursor: String? + val hasError: Boolean, + val hasMoreItems: Boolean, + val count: Int, + val savedItemSlugs: List, + val cursor: String? ) { - companion object { - val errorResult = SavedItemSyncResult(hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItemSlugs = listOf()) - } + companion object { + val errorResult = SavedItemSyncResult( + hasError = true, + hasMoreItems = true, + cursor = null, + count = 0, + savedItemSlugs = listOf() + ) + } } data class SearchResult( - val hasError: Boolean, - val hasMoreItems: Boolean, - val count: Int, - val savedItems: List, - val cursor: String? + val hasError: Boolean, + val hasMoreItems: Boolean, + val count: Int, + val savedItems: List, + val cursor: String? ) { - companion object { - val errorResult = SearchResult(hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItems = listOf()) - } + companion object { + val errorResult = SearchResult( + hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItems = listOf() + ) + } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SyncOfflineChanges.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SyncOfflineChanges.kt index 7247d8702..da66f66fc 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SyncOfflineChanges.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SyncOfflineChanges.kt @@ -21,198 +21,204 @@ import com.apollographql.apollo3.api.Optional import kotlinx.coroutines.delay suspend fun DataService.startSyncChannels() { - Log.d("sync", "Starting sync channels") - for (savedItem in savedItemSyncChannel) { - syncSavedItem(savedItem) - } + Log.d("sync", "Starting sync channels") + for (savedItem in savedItemSyncChannel) { + syncSavedItem(savedItem) + } } suspend fun DataService.performHighlightChange(highlightChange: HighlightChange) { - val highlight = highlightChangeToHighlight(highlightChange) - if (syncHighlightChange(highlightChange)) { - db.highlightChangesDao().deleteById(highlight.highlightId) - } + val highlight = highlightChangeToHighlight(highlightChange) + if (syncHighlightChange(highlightChange)) { + db.highlightChangesDao().deleteById(highlight.highlightId) + } } suspend fun DataService.syncOfflineItemsWithServerIfNeeded() { - val unSyncedSavedItems = db.savedItemDao().getUnSynced() - val unSyncedHighlights = db.highlightChangesDao().getUnSynced() + val unSyncedSavedItems = db.savedItemDao().getUnSynced() + val unSyncedHighlights = db.highlightChangesDao().getUnSynced() - for (savedItem in unSyncedSavedItems) { - delay(250) - savedItemSyncChannel.send(savedItem) - } + for (savedItem in unSyncedSavedItems) { + delay(250) + savedItemSyncChannel.send(savedItem) + } - for (change in unSyncedHighlights) { - performHighlightChange(change) - } + for (change in unSyncedHighlights) { + performHighlightChange(change) + } } private suspend fun DataService.syncSavedItem(item: SavedItem) { - suspend fun updateSyncStatus(status: ServerSyncStatus) { - item.serverSyncStatus = status.rawValue - db.savedItemDao().update(item) - } - - when (item.serverSyncStatus) { - ServerSyncStatus.NEEDS_DELETION.rawValue -> { - updateSyncStatus(ServerSyncStatus.IS_SYNCING) - - val isDeletedOnServer = networker.deleteSavedItem(item.savedItemId) - - if (isDeletedOnServer) { - db.savedItemDao().deleteById(item.savedItemId) - } else { - updateSyncStatus(ServerSyncStatus.NEEDS_DELETION) - } + suspend fun updateSyncStatus(status: ServerSyncStatus) { + item.serverSyncStatus = status.rawValue + db.savedItemDao().update(item) } - ServerSyncStatus.NEEDS_UPDATE.rawValue -> { - updateSyncStatus(ServerSyncStatus.IS_SYNCING) - val isArchiveServerSynced = networker.updateArchiveStatusSavedItem(itemID = item.savedItemId, setAsArchived = item.isArchived) + when (item.serverSyncStatus) { + ServerSyncStatus.NEEDS_DELETION.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) - val isReadingProgressSynced = networker.updateReadingProgress( - ReadingProgressParams( - id = item.savedItemId, - force = item.contentReader == "PDF", - readingProgressPercent = item.readingProgress, - readingProgressAnchorIndex = item.readingProgressAnchor - ) - ) + val isDeletedOnServer = networker.deleteSavedItem(item.savedItemId) - if (isArchiveServerSynced && isReadingProgressSynced) { - updateSyncStatus(ServerSyncStatus.IS_SYNCED) - } else { - updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE) - } + if (isDeletedOnServer) { + db.savedItemDao().deleteById(item.savedItemId) + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_DELETION) + } + } + + ServerSyncStatus.NEEDS_UPDATE.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) + + val isArchiveServerSynced = networker.updateArchiveStatusSavedItem( + itemID = item.savedItemId, setAsArchived = item.isArchived + ) + + val isReadingProgressSynced = networker.updateReadingProgress( + ReadingProgressParams( + id = item.savedItemId, + force = item.contentReader == "PDF", + readingProgressPercent = item.readingProgress, + readingProgressAnchorIndex = item.readingProgressAnchor + ) + ) + + if (isArchiveServerSynced && isReadingProgressSynced) { + updateSyncStatus(ServerSyncStatus.IS_SYNCED) + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE) + } + } + + ServerSyncStatus.NEEDS_CREATION.rawValue -> { + // TODO: implement when we are able to create content on device + // updateSyncStatus(ServerSyncStatus.IS_SYNCING) + // send update to server + // update db + } + + else -> return } - ServerSyncStatus.NEEDS_CREATION.rawValue -> { - // TODO: implement when we are able to create content on device - // updateSyncStatus(ServerSyncStatus.IS_SYNCING) - // send update to server - // update db - } - else -> return - } } private suspend fun DataService.syncHighlightChange(highlightChange: HighlightChange): Boolean { - val highlight = highlightChangeToHighlight(highlightChange) + val highlight = highlightChangeToHighlight(highlightChange) - fun updateSyncStatus(status: ServerSyncStatus) { - highlight.serverSyncStatus = status.rawValue - db.highlightDao().update(highlight) - } - - when (highlight.serverSyncStatus) { - ServerSyncStatus.NEEDS_DELETION.rawValue -> { - updateSyncStatus(ServerSyncStatus.IS_SYNCING) - val isDeletedOnServer = networker.deleteHighlights(listOf(highlight.highlightId)) - - if (isDeletedOnServer) { - db.highlightDao().deleteById(highlight.highlightId) - } else { - updateSyncStatus(ServerSyncStatus.NEEDS_DELETION) - } - return isDeletedOnServer != null + fun updateSyncStatus(status: ServerSyncStatus) { + highlight.serverSyncStatus = status.rawValue + db.highlightDao().update(highlight) } - ServerSyncStatus.NEEDS_UPDATE.rawValue -> { - updateSyncStatus(ServerSyncStatus.IS_SYNCING) + when (highlight.serverSyncStatus) { + ServerSyncStatus.NEEDS_DELETION.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) + val isDeletedOnServer = networker.deleteHighlights(listOf(highlight.highlightId)) - val isUpdatedOnServer = networker.updateHighlight( - UpdateHighlightInput( - annotation = Optional.presentIfNotNull(highlight.annotation), - highlightId = highlight.highlightId, - sharedAt = Optional.absent() - ) - ) + if (isDeletedOnServer) { + db.highlightDao().deleteById(highlight.highlightId) + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_DELETION) + } + return isDeletedOnServer != null + } - if (isUpdatedOnServer) { - updateSyncStatus(ServerSyncStatus.IS_SYNCED) - } else { - updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE) - } - return isUpdatedOnServer != null + ServerSyncStatus.NEEDS_UPDATE.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) + + val isUpdatedOnServer = networker.updateHighlight( + UpdateHighlightInput( + annotation = Optional.presentIfNotNull(highlight.annotation), + highlightId = highlight.highlightId, + sharedAt = Optional.absent() + ) + ) + + if (isUpdatedOnServer) { + updateSyncStatus(ServerSyncStatus.IS_SYNCED) + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE) + } + return isUpdatedOnServer != null + } + + ServerSyncStatus.NEEDS_CREATION.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) + + val input = CreateHighlightInput( + id = highlight.highlightId, + shortId = highlight.shortId, + articleId = highlightChange.savedItemId, + type = Optional.presentIfNotNull(HighlightType.safeValueOf(highlight.type)), + annotation = Optional.presentIfNotNull(highlight.annotation), + patch = Optional.presentIfNotNull(highlight.patch), + quote = Optional.presentIfNotNull(highlight.quote), + ) + Log.d("sync", "Creating highlight from input: ${input}") + val createResult = networker.createHighlight( + input + ) + return if (createResult.newHighlight != null || createResult.alreadyExists) { + updateSyncStatus(ServerSyncStatus.IS_SYNCED) + true + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE) + false + } + } + + ServerSyncStatus.NEEDS_MERGE.rawValue -> { + Log.d("sync", "NEEDS MERGE: ${highlightChange}") + + val mergeHighlightInput = MergeHighlightInput( + id = highlight.highlightId, + shortId = highlight.shortId, + articleId = highlightChange.savedItemId, + annotation = Optional.presentIfNotNull(highlight.annotation), + color = Optional.presentIfNotNull(highlight.color), + highlightPositionAnchorIndex = Optional.presentIfNotNull(highlight.highlightPositionAnchorIndex), + highlightPositionPercent = Optional.presentIfNotNull(highlight.highlightPositionPercent), + html = Optional.presentIfNotNull(highlightChange.html), + overlapHighlightIdList = highlightChange.overlappingIDs ?: emptyList(), + patch = highlight.patch ?: "", + prefix = Optional.presentIfNotNull(highlight.prefix), + quote = highlight.quote ?: "", + suffix = Optional.presentIfNotNull(highlight.suffix) + ) + + val isUpdatedOnServer = networker.mergeHighlights(mergeHighlightInput) + if (!isUpdatedOnServer) { + Log.d("sync", "FAILED TO MERGE HIGHLIGHT") + highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue + return false + } + + for (highlightID in mergeHighlightInput.overlapHighlightIdList) { + Log.d("sync", "DELETING MERGED HIGHLIGHT: ${highlightID}") + val deleteChange = HighlightChange( + highlightId = highlightID, + savedItemId = highlightChange.savedItemId, + type = "", + shortId = "", + annotation = null, + createdAt = null, + patch = null, + prefix = null, + quote = null, + serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue, + html = null, + suffix = null, + updatedAt = null, + color = null, + highlightPositionPercent = null, + highlightPositionAnchorIndex = null, + overlappingIDs = null + ) + performHighlightChange(deleteChange) + } + return true + } + + else -> return false } - - ServerSyncStatus.NEEDS_CREATION.rawValue -> { - updateSyncStatus(ServerSyncStatus.IS_SYNCING) - - val input = CreateHighlightInput( - id = highlight.highlightId, - shortId = highlight.shortId, - articleId = highlightChange.savedItemId, - type = Optional.presentIfNotNull(HighlightType.safeValueOf(highlight.type)), - annotation = Optional.presentIfNotNull(highlight.annotation), - patch = Optional.presentIfNotNull(highlight.patch), - quote = Optional.presentIfNotNull(highlight.quote), - ) - Log.d("sync", "Creating highlight from input: ${input}") - val createResult = networker.createHighlight( - input - ) - if (createResult.newHighlight != null || createResult.alreadyExists) { - updateSyncStatus(ServerSyncStatus.IS_SYNCED) - return true - } else { - updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE) - return false - } - } - - ServerSyncStatus.NEEDS_MERGE.rawValue -> { - Log.d("sync", "NEEDS MERGE: ${highlightChange}") - - val mergeHighlightInput = MergeHighlightInput( - id = highlight.highlightId, - shortId = highlight.shortId, - articleId = highlightChange.savedItemId, - annotation = Optional.presentIfNotNull(highlight.annotation), - color = Optional.presentIfNotNull(highlight.color), - highlightPositionAnchorIndex = Optional.presentIfNotNull(highlight.highlightPositionAnchorIndex), - highlightPositionPercent = Optional.presentIfNotNull(highlight.highlightPositionPercent), - html = Optional.presentIfNotNull(highlightChange.html), - overlapHighlightIdList = highlightChange.overlappingIDs ?: emptyList(), - patch = highlight.patch ?: "", - prefix = Optional.presentIfNotNull(highlight.prefix), - quote = highlight.quote ?: "", - suffix = Optional.presentIfNotNull(highlight.suffix) - ) - - val isUpdatedOnServer = networker.mergeHighlights(mergeHighlightInput) - if (!isUpdatedOnServer) { - Log.d("sync", "FAILED TO MERGE HIGHLIGHT") - highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue - return false - } - - for (highlightID in mergeHighlightInput.overlapHighlightIdList) { - Log.d("sync", "DELETING MERGED HIGHLIGHT: ${highlightID}") - val deleteChange = HighlightChange( - highlightId = highlightID, - savedItemId = highlightChange.savedItemId, - type = "", - shortId = "", - annotation = null, - createdAt = null, - patch = null, - prefix = null, - quote = null, - serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue, - html = null, - suffix = null, - updatedAt = null, - color = null, - highlightPositionPercent = null, - highlightPositionAnchorIndex = null, - overlappingIDs = null - ) - performHighlightChange(deleteChange) - } - return true - } - else -> return false - } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/LibraryRepository.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/LibraryRepository.kt index fe290b209..67091e73c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/LibraryRepository.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/LibraryRepository.kt @@ -1,6 +1,10 @@ package app.omnivore.omnivore.core.data.repository +import app.omnivore.omnivore.core.data.SavedItemSyncResult +import app.omnivore.omnivore.core.data.SearchResult import app.omnivore.omnivore.core.data.model.LibraryQuery +import app.omnivore.omnivore.core.database.entities.HighlightChange +import app.omnivore.omnivore.core.database.entities.SavedItemLabel import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights import kotlinx.coroutines.flow.Flow @@ -8,9 +12,37 @@ interface LibraryRepository { fun getSavedItems(query: LibraryQuery): Flow> + fun getSavedItemsLabels(): Flow> + + suspend fun getLabels(): List + + suspend fun fetchSavedItemContent(slug: String) + + suspend fun insertAllLabels(labels: List) + + suspend fun setSavedItemLabels(itemId: String, labels: List): Boolean + suspend fun updateReadingProgress( itemId: String, readingProgressPercentage: Double, readingProgressAnchorIndex: Int ) + + suspend fun createNewSavedItemLabel(labelName: String, hexColorValue: String) + + suspend fun librarySearch(cursor: String?, query: String): SearchResult + + suspend fun isSavedItemContentStoredInDB(slug: String): Boolean + + suspend fun deleteSavedItem(itemID: String) + + suspend fun archiveSavedItem(itemID: String) + + suspend fun unarchiveSavedItem(itemID: String) + + suspend fun syncOfflineItemsWithServerIfNeeded() + + suspend fun syncHighlightChange(highlightChange: HighlightChange): Boolean + + suspend fun sync(since: String, cursor: String?, limit: Int = 20): SavedItemSyncResult } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/impl/LibraryRepositoryImpl.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/impl/LibraryRepositoryImpl.kt index f7f748a19..d48f88c81 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/impl/LibraryRepositoryImpl.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/impl/LibraryRepositoryImpl.kt @@ -1,20 +1,62 @@ package app.omnivore.omnivore.core.data.repository.impl +import android.util.Log +import app.omnivore.omnivore.core.data.DataService +import app.omnivore.omnivore.core.data.SavedItemSyncResult +import app.omnivore.omnivore.core.data.SearchResult import app.omnivore.omnivore.core.data.model.LibraryQuery import app.omnivore.omnivore.core.data.model.ServerSyncStatus import app.omnivore.omnivore.core.data.repository.LibraryRepository +import app.omnivore.omnivore.core.database.dao.HighlightChangesDao +import app.omnivore.omnivore.core.database.dao.HighlightDao +import app.omnivore.omnivore.core.database.dao.SavedItemAndSavedItemLabelCrossRefDao import app.omnivore.omnivore.core.database.dao.SavedItemDao +import app.omnivore.omnivore.core.database.dao.SavedItemLabelDao +import app.omnivore.omnivore.core.database.dao.SavedItemWithLabelsAndHighlightsDao +import app.omnivore.omnivore.core.database.entities.Highlight +import app.omnivore.omnivore.core.database.entities.HighlightChange +import app.omnivore.omnivore.core.database.entities.SavedItem +import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRef +import app.omnivore.omnivore.core.database.entities.SavedItemLabel import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights +import app.omnivore.omnivore.core.database.entities.highlightChangeToHighlight import app.omnivore.omnivore.core.network.Networker import app.omnivore.omnivore.core.network.ReadingProgressParams +import app.omnivore.omnivore.core.network.archiveSavedItem +import app.omnivore.omnivore.core.network.createHighlight +import app.omnivore.omnivore.core.network.createNewLabel +import app.omnivore.omnivore.core.network.deleteHighlights +import app.omnivore.omnivore.core.network.deleteSavedItem +import app.omnivore.omnivore.core.network.mergeHighlights +import app.omnivore.omnivore.core.network.savedItem +import app.omnivore.omnivore.core.network.savedItemLabels +import app.omnivore.omnivore.core.network.savedItemUpdates +import app.omnivore.omnivore.core.network.search +import app.omnivore.omnivore.core.network.unarchiveSavedItem +import app.omnivore.omnivore.core.network.updateHighlight +import app.omnivore.omnivore.core.network.updateLabelsForSavedItem import app.omnivore.omnivore.core.network.updateReadingProgress +import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput +import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput +import app.omnivore.omnivore.graphql.generated.type.HighlightType +import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput +import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput +import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput +import com.apollographql.apollo3.api.Optional import com.google.gson.Gson +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import javax.inject.Inject class LibraryRepositoryImpl @Inject constructor( private val savedItemDao: SavedItemDao, - private val networker: Networker + private val savedItemLabelDao: SavedItemLabelDao, + private val savedItemWithLabelsAndHighlightsDao: SavedItemWithLabelsAndHighlightsDao, + private val savedItemAndSavedItemLabelCrossRefDao: SavedItemAndSavedItemLabelCrossRefDao, + private val highlightDao: HighlightDao, + private val highlightChangesDao: HighlightChangesDao, + private val networker: Networker, + private val dataService: DataService ): LibraryRepository { override fun getSavedItems(query: LibraryQuery): Flow> = @@ -28,6 +70,26 @@ class LibraryRepositoryImpl @Inject constructor( query.allowedContentReaders ) + override fun getSavedItemsLabels(): Flow> = savedItemLabelDao.getSavedItemLabels() + + override suspend fun getLabels(): List = networker.savedItemLabels() + + override suspend fun insertAllLabels(labels: List) { + savedItemLabelDao.insertAll(labels) + } + + override suspend fun fetchSavedItemContent(slug: String) { + val syncResult = networker.savedItem(slug) + + val savedItem = syncResult.item + savedItem?.let { + val item = SavedItemWithLabelsAndHighlights( + savedItem = savedItem, labels = syncResult.labels, highlights = syncResult.highlights + ) + savedItemWithLabelsAndHighlightsDao.insertAll(listOf(item)) + } + } + override suspend fun updateReadingProgress( itemId: String, readingProgressPercentage: Double, @@ -63,4 +125,360 @@ class LibraryRepositoryImpl @Inject constructor( updatedItem?.let { savedItemDao.update(updatedItem) } } } + + override suspend fun setSavedItemLabels( + itemId: String, + labels: List + ): Boolean { + val input = SetLabelsInput( + pageId = itemId, + labels = Optional.presentIfNotNull(labels.map { CreateLabelInput(color = Optional.presentIfNotNull(it.color), name = it.name) }), + ) + + val updatedLabels = networker.updateLabelsForSavedItem(input) + + // Figure out which of the labels are new + updatedLabels?.let { updatedLabels -> + val existingNamedLabels = savedItemLabelDao.namedLabels(updatedLabels.map { it.labelFields.name }) + val existingNames = existingNamedLabels.map { it.name } + val newNamedLabels = updatedLabels.filter { !existingNames.contains(it.labelFields.name) } + + savedItemLabelDao.insertAll(newNamedLabels.map { + SavedItemLabel( + savedItemLabelId = it.labelFields.id, + name = it.labelFields.name, + color = it.labelFields.color, + createdAt = null, + labelDescription = null + ) + }) + + val allNamedLabels = savedItemLabelDao.namedLabels(updatedLabels.map { it.labelFields.name }) + val crossRefs = allNamedLabels.map { + SavedItemAndSavedItemLabelCrossRef( + savedItemLabelId = it.savedItemLabelId, + savedItemId = itemId + ) + } + savedItemAndSavedItemLabelCrossRefDao.deleteRefsBySavedItemId(itemId) + savedItemAndSavedItemLabelCrossRefDao.insertAll(crossRefs) + + return true + } ?: run { + return false + } + } + + override suspend fun createNewSavedItemLabel(labelName: String, hexColorValue: String) { + val newLabel = networker.createNewLabel( + CreateLabelInput( + color = Optional.presentIfNotNull(hexColorValue), name = labelName + ) + ) + + newLabel?.let { + val savedItemLabel = SavedItemLabel( + savedItemLabelId = it.id, + name = it.name, + color = it.color, + createdAt = it.createdAt as String?, + labelDescription = it.description + ) + + savedItemLabelDao.insertAll(listOf(savedItemLabel)) + } + } + + override suspend fun librarySearch(cursor: String?, query: String): SearchResult { + val searchResult = networker.search(cursor = cursor, limit = 10, query = query) + + val savedItems = searchResult.items.map { + SavedItemWithLabelsAndHighlights( + savedItem = it.item, + labels = it.labels, + highlights = it.highlights, + ) + } + + savedItemWithLabelsAndHighlightsDao.insertAll(savedItems) + + Log.d( + "sync", + "found ${searchResult.items.size} items with search api. Query: $query cursor: $cursor" + ) + + return SearchResult( + hasError = false, + hasMoreItems = false, + cursor = searchResult.cursor, + count = searchResult.items.size, + savedItems = savedItems + ) + } + + override suspend fun isSavedItemContentStoredInDB(slug: String): Boolean { + val existingItem = savedItemDao.getSavedItemWithLabelsAndHighlights(slug) + val content = existingItem?.savedItem?.content ?: "" + return content.length > 10 + } + + override suspend fun deleteSavedItem(itemID: String) { + val savedItem = savedItemDao.findById(itemID = itemID) ?: return + savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue + savedItemDao.update(savedItem) + + val isUpdatedOnServer = networker.deleteSavedItem(itemID) + + if (isUpdatedOnServer) { + savedItemDao.deleteById(itemID) + } + } + + override suspend fun archiveSavedItem(itemID: String) { + val savedItem = savedItemDao.findById(itemID = itemID) ?: return + + savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue + savedItem.isArchived = true + savedItemDao.update(savedItem) + + val isUpdatedOnServer = networker.archiveSavedItem(itemID) + + if (isUpdatedOnServer) { + savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue + savedItemDao.update(savedItem) + } + } + + override suspend fun unarchiveSavedItem(itemID: String) { + val savedItem = savedItemDao.findById(itemID = itemID) ?: return + + savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue + savedItem.isArchived = false + savedItemDao.update(savedItem) + + val isUpdatedOnServer = networker.unarchiveSavedItem(itemID) + + if (isUpdatedOnServer) { + savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue + savedItemDao.update(savedItem) + } + } + + override suspend fun syncOfflineItemsWithServerIfNeeded() { + val unSyncedSavedItems = savedItemDao.getUnSynced() + val unSyncedHighlights = highlightChangesDao.getUnSynced() + + for (savedItem in unSyncedSavedItems) { + delay(250) + dataService.savedItemSyncChannel.send(savedItem) + } + + for (change in unSyncedHighlights) { + performHighlightChange(change) + } + } + + override suspend fun syncHighlightChange(highlightChange: HighlightChange): Boolean { + val highlight = highlightChangeToHighlight(highlightChange) + + fun updateSyncStatus(status: ServerSyncStatus) { + highlight.serverSyncStatus = status.rawValue + highlightDao.update(highlight) + } + + when (highlight.serverSyncStatus) { + ServerSyncStatus.NEEDS_DELETION.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) + val isDeletedOnServer = networker.deleteHighlights(listOf(highlight.highlightId)) + + if (isDeletedOnServer) { + highlightDao.deleteById(highlight.highlightId) + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_DELETION) + } + return isDeletedOnServer != null + } + + ServerSyncStatus.NEEDS_UPDATE.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) + + val isUpdatedOnServer = networker.updateHighlight( + UpdateHighlightInput( + annotation = Optional.presentIfNotNull(highlight.annotation), + highlightId = highlight.highlightId, + sharedAt = Optional.absent() + ) + ) + + if (isUpdatedOnServer) { + updateSyncStatus(ServerSyncStatus.IS_SYNCED) + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE) + } + return isUpdatedOnServer != null + } + + ServerSyncStatus.NEEDS_CREATION.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) + + val input = CreateHighlightInput( + id = highlight.highlightId, + shortId = highlight.shortId, + articleId = highlightChange.savedItemId, + type = Optional.presentIfNotNull(HighlightType.safeValueOf(highlight.type)), + annotation = Optional.presentIfNotNull(highlight.annotation), + patch = Optional.presentIfNotNull(highlight.patch), + quote = Optional.presentIfNotNull(highlight.quote), + ) + Log.d("sync", "Creating highlight from input: ${input}") + val createResult = networker.createHighlight( + input + ) + if (createResult.newHighlight != null || createResult.alreadyExists) { + updateSyncStatus(ServerSyncStatus.IS_SYNCED) + return true + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE) + return false + } + } + + ServerSyncStatus.NEEDS_MERGE.rawValue -> { + Log.d("sync", "NEEDS MERGE: ${highlightChange}") + + val mergeHighlightInput = MergeHighlightInput( + id = highlight.highlightId, + shortId = highlight.shortId, + articleId = highlightChange.savedItemId, + annotation = Optional.presentIfNotNull(highlight.annotation), + color = Optional.presentIfNotNull(highlight.color), + highlightPositionAnchorIndex = Optional.presentIfNotNull(highlight.highlightPositionAnchorIndex), + highlightPositionPercent = Optional.presentIfNotNull(highlight.highlightPositionPercent), + html = Optional.presentIfNotNull(highlightChange.html), + overlapHighlightIdList = highlightChange.overlappingIDs ?: emptyList(), + patch = highlight.patch ?: "", + prefix = Optional.presentIfNotNull(highlight.prefix), + quote = highlight.quote ?: "", + suffix = Optional.presentIfNotNull(highlight.suffix) + ) + + val isUpdatedOnServer = networker.mergeHighlights(mergeHighlightInput) + if (!isUpdatedOnServer) { + Log.d("sync", "FAILED TO MERGE HIGHLIGHT") + highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue + return false + } + + for (highlightID in mergeHighlightInput.overlapHighlightIdList) { + Log.d("sync", "DELETING MERGED HIGHLIGHT: ${highlightID}") + val deleteChange = HighlightChange( + highlightId = highlightID, + savedItemId = highlightChange.savedItemId, + type = "", + shortId = "", + annotation = null, + createdAt = null, + patch = null, + prefix = null, + quote = null, + serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue, + html = null, + suffix = null, + updatedAt = null, + color = null, + highlightPositionPercent = null, + highlightPositionAnchorIndex = null, + overlappingIDs = null + ) + performHighlightChange(deleteChange) + } + return true + } + + else -> return false + } + } + + private suspend fun performHighlightChange(highlightChange: HighlightChange) { + val highlight = highlightChangeToHighlight(highlightChange) + if (syncHighlightChange(highlightChange)) { + highlightChangesDao.deleteById(highlight.highlightId) + } + } + + override suspend fun sync(since: String, cursor: String?, limit: Int): SavedItemSyncResult { + val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) + ?: return SavedItemSyncResult.errorResult + + if (syncResult.deletedItemIDs.isNotEmpty()) { + savedItemDao.deleteByIds(syncResult.deletedItemIDs) + } + + val savedItems = syncResult.items.map { + val savedItem = SavedItem( + savedItemId = it.id, + title = it.title, + createdAt = it.createdAt as String, + savedAt = it.savedAt as String, + readAt = it.readAt as String?, + updatedAt = it.updatedAt as String?, + readingProgress = it.readingProgressPercent, + readingProgressAnchor = it.readingProgressAnchorIndex, + imageURLString = it.image, + pageURLString = it.url, + descriptionText = it.description, + publisherURLString = it.originalArticleUrl, + siteName = it.siteName, + author = it.author, + publishDate = it.publishedAt as String?, + slug = it.slug, + isArchived = it.isArchived, + contentReader = it.contentReader.rawValue, + content = null, + wordsCount = it.wordsCount + ) + val labels = it.labels?.map { label -> + SavedItemLabel( + savedItemLabelId = label.labelFields.id, + name = label.labelFields.name, + color = label.labelFields.color, + createdAt = null, + labelDescription = null + ) + } ?: listOf() + val highlights = it.highlights?.map { highlight -> + Highlight( + type = highlight.highlightFields.type.toString(), + highlightId = highlight.highlightFields.id, + annotation = highlight.highlightFields.annotation, + createdByMe = highlight.highlightFields.createdByMe, + markedForDeletion = false, + patch = highlight.highlightFields.patch, + prefix = highlight.highlightFields.prefix, + quote = highlight.highlightFields.quote, + serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue, + shortId = highlight.highlightFields.shortId, + suffix = highlight.highlightFields.suffix, + createdAt = null, + updatedAt = highlight.highlightFields.updatedAt as String?, + color = highlight.highlightFields.color, + highlightPositionPercent = highlight.highlightFields.highlightPositionPercent, + highlightPositionAnchorIndex = highlight.highlightFields.highlightPositionAnchorIndex, + ) + } ?: listOf() + SavedItemWithLabelsAndHighlights( + savedItem = savedItem, labels = labels, highlights = highlights + ) + } + + savedItemWithLabelsAndHighlightsDao.insertAll(savedItems) + + Log.d("sync", "found ${syncResult.items.size} items with sync api. Since: $since") + + return SavedItemSyncResult(hasError = false, + hasMoreItems = syncResult.hasMoreItems, + cursor = syncResult.cursor, + count = syncResult.items.size, + savedItemSlugs = syncResult.items.map { it.slug }) + } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/OmnivoreDatabase.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/OmnivoreDatabase.kt index 7739afffb..ed292707a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/OmnivoreDatabase.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/OmnivoreDatabase.kt @@ -2,19 +2,19 @@ package app.omnivore.omnivore.core.database import androidx.room.Database import androidx.room.RoomDatabase +import app.omnivore.omnivore.core.database.dao.HighlightChangesDao +import app.omnivore.omnivore.core.database.dao.HighlightDao +import app.omnivore.omnivore.core.database.dao.SavedItemAndSavedItemLabelCrossRefDao import app.omnivore.omnivore.core.database.dao.SavedItemDao +import app.omnivore.omnivore.core.database.dao.SavedItemLabelDao +import app.omnivore.omnivore.core.database.dao.SavedItemWithLabelsAndHighlightsDao import app.omnivore.omnivore.core.database.entities.Highlight import app.omnivore.omnivore.core.database.entities.HighlightChange -import app.omnivore.omnivore.core.database.entities.HighlightChangesDao -import app.omnivore.omnivore.core.database.entities.HighlightDao import app.omnivore.omnivore.core.database.entities.SavedItem import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRef import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRefDao import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRef -import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRefDao import app.omnivore.omnivore.core.database.entities.SavedItemLabel -import app.omnivore.omnivore.core.database.entities.SavedItemLabelDao -import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlightsDao import app.omnivore.omnivore.core.database.entities.Viewer import app.omnivore.omnivore.core.database.entities.ViewerDao diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/dao/SavedItemDao.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/dao/SavedItemDao.kt index 3e26960e0..c041cc721 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/dao/SavedItemDao.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/dao/SavedItemDao.kt @@ -23,7 +23,7 @@ interface SavedItemDao { fun getUnSynced(): List @Query("SELECT * FROM savedItem WHERE slug = :slug") - fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights? + suspend fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights? @Query("DELETE FROM savedItem WHERE savedItemId = :itemID") fun deleteById(itemID: String) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Highlight.kt index 50c078ab0..0e06cb9be 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Highlight.kt @@ -1,6 +1,15 @@ package app.omnivore.omnivore.core.database.entities -import androidx.room.* +import androidx.room.Dao +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Insert +import androidx.room.Junction +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Relation import app.omnivore.omnivore.core.data.model.ServerSyncStatus import com.google.gson.annotations.SerializedName @@ -87,25 +96,3 @@ data class SavedItemWithLabelsAndHighlights( return savedItem.savedItemId.hashCode() } } - -@Dao -interface HighlightDao { - @Query("SELECT * FROM highlight WHERE serverSyncStatus != 0") - fun getUnSynced(): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertAll(items: List) - - @Query("DELETE FROM highlight WHERE highlightId = :highlightId") - fun deleteById(highlightId: String) - - @Query("SELECT * FROM highlight WHERE highlightId = :highlightId") - fun findById(highlightId: String): Highlight? - - // Server sync status is passed in here to work around Room compile-time query rules, but should always be NEEDS_UPDATE - @Query("UPDATE highlight SET annotation = :note, serverSyncStatus = :serverSyncStatus WHERE highlightId = :highlightId") - fun updateNote(highlightId: String, note: String, serverSyncStatus: Int = ServerSyncStatus.NEEDS_UPDATE.rawValue) - - @Update - fun update(highlight: Highlight) -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/HighlightChange.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/HighlightChange.kt index 8a09cb0cb..2d14618be 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/HighlightChange.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/HighlightChange.kt @@ -1,15 +1,12 @@ package app.omnivore.omnivore.core.database.entities import android.util.Log -import androidx.room.Dao import androidx.room.Entity -import androidx.room.Insert -import androidx.room.OnConflictStrategy import androidx.room.PrimaryKey -import androidx.room.Query import androidx.room.TypeConverter import androidx.room.TypeConverters import app.omnivore.omnivore.core.data.model.ServerSyncStatus +import app.omnivore.omnivore.core.database.dao.HighlightChangesDao import com.google.gson.Gson import com.google.gson.reflect.TypeToken @@ -110,16 +107,3 @@ fun highlightChangeToHighlight(change: HighlightChange): Highlight { serverSyncStatus = change.serverSyncStatus ) } - -@Dao -interface HighlightChangesDao { - @Query("SELECT * FROM highlightChange WHERE serverSyncStatus != 0 ORDER BY updatedAt ASC") - fun getUnSynced(): List - - @Query("DELETE FROM highlightChange WHERE highlightId = :highlightId") - fun deleteById(highlightId: String) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertAll(items: List) -} - diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItem.kt index fe4abc810..92e72cabb 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItem.kt @@ -2,12 +2,8 @@ package app.omnivore.omnivore.core.database.entities import androidx.core.net.toUri import androidx.room.ColumnInfo -import androidx.room.Dao import androidx.room.Entity -import androidx.room.Insert -import androidx.room.OnConflictStrategy import androidx.room.PrimaryKey -import androidx.room.Transaction @Entity data class SavedItem( @@ -74,67 +70,6 @@ data class TypeaheadCardData( val isArchived: Boolean, ) -@Dao -abstract class SavedItemWithLabelsAndHighlightsDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertSavedItems(items: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertLabelCrossRefs(items: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertLabels(items: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertHighlights(items: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertHighlightCrossRefs(items: List) - - @Transaction - open fun insertAll(savedItems: List) { - insertSavedItems(savedItems.map { it.savedItem }) - - val labels: MutableList = mutableListOf() - val highlights: MutableList = mutableListOf() - - val labelCrossRefs: MutableList = mutableListOf() - val highlightCrossRefs: MutableList = mutableListOf() - - for (searchItem in savedItems) { - labels.addAll(searchItem.labels) - highlights.addAll(searchItem.highlights) - - val newLabelCrossRefs = searchItem.labels.map { - SavedItemAndSavedItemLabelCrossRef( - savedItemLabelId = it.savedItemLabelId, - savedItemId = searchItem.savedItem.savedItemId - ) - } - - val newHighlightCrossRefs = searchItem.highlights.map { - SavedItemAndHighlightCrossRef( - highlightId = it.highlightId, - savedItemId = searchItem.savedItem.savedItemId - ) - } - - labelCrossRefs.addAll(newLabelCrossRefs) - highlightCrossRefs.addAll(newHighlightCrossRefs) - } - - insertLabels(labels) - insertLabelCrossRefs(labelCrossRefs) - - insertHighlights(highlights) - insertHighlightCrossRefs(highlightCrossRefs) - } -} - - - - object SavedItemQueryConstants { const val columns = "savedItemId, slug, publisherURLString, title, author, descriptionText, imageURLString, isArchived, pageURLString, contentReader, savedAt, readingProgress, wordsCount" diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItemLabel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItemLabel.kt index a014cc3e2..854fe8414 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItemLabel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItemLabel.kt @@ -1,15 +1,8 @@ package app.omnivore.omnivore.core.database.entities -import androidx.lifecycle.LiveData -import androidx.room.Dao import androidx.room.Entity import androidx.room.ForeignKey -import androidx.room.Insert -import androidx.room.OnConflictStrategy import androidx.room.PrimaryKey -import androidx.room.Query -import androidx.room.Transaction -import app.omnivore.omnivore.core.data.model.ServerSyncStatus @Entity data class SavedItemLabel( @@ -21,26 +14,6 @@ data class SavedItemLabel( val serverSyncStatus: Int = 0 ) -@Dao -interface SavedItemLabelDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertAll(items: List) - - @Transaction - @Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC") - fun getSavedItemLabelsLiveData(): LiveData> - - @Transaction - @Query("UPDATE SavedItemLabel set savedItemLabelId = :permanentId, serverSyncStatus = :status WHERE savedItemLabelId = :tempId") - fun updateTempLabel( - tempId: String, permanentId: String, status: ServerSyncStatus = ServerSyncStatus.IS_SYNCED - ) - - @Transaction - @Query("SELECT * FROM SavedItemLabel WHERE name in (:names) ORDER BY name ASC") - fun namedLabels(names: List): List -} - @Entity( primaryKeys = ["savedItemLabelId", "savedItemId"], foreignKeys = [ForeignKey( entity = SavedItem::class, @@ -56,16 +29,3 @@ interface SavedItemLabelDao { data class SavedItemAndSavedItemLabelCrossRef( val savedItemLabelId: String, val savedItemId: String ) - - -@Dao -interface SavedItemAndSavedItemLabelCrossRefDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertAll(items: List) - - @Query("DELETE FROM savedItemAndSavedItemLabelCrossRef WHERE savedItemId = :savedItemId") - fun deleteRefsBySavedItemId(savedItemId: String) -} - -// has many highlights -// has many savedItems diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/ReadingProgressMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/ReadingProgressMutations.kt index 1e33d801b..86fa5ffe5 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/ReadingProgressMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/ReadingProgressMutations.kt @@ -1,49 +1,41 @@ package app.omnivore.omnivore.core.network -import app.omnivore.omnivore.graphql.generated.SaveArticleReadingProgressMutation -import app.omnivore.omnivore.graphql.generated.type.SaveArticleReadingProgressInput - import android.util.Log +import app.omnivore.omnivore.graphql.generated.SaveArticleReadingProgressMutation +import app.omnivore.omnivore.graphql.generated.type.SaveArticleReadingProgressInput import com.apollographql.apollo3.api.Optional -import com.google.gson.Gson data class ReadingProgressParams( - val id: String?, - val readingProgressPercent: Double?, - val readingProgressAnchorIndex: Int?, - val force: Boolean? + val id: String?, + val readingProgressPercent: Double?, + val readingProgressAnchorIndex: Int?, + val force: Boolean? ) { - fun asSaveReadingProgressInput() = SaveArticleReadingProgressInput( - id = id ?: "", - force = Optional.presentIfNotNull(force), - readingProgressPercent = readingProgressPercent ?: 0.0, - readingProgressAnchorIndex = Optional.presentIfNotNull(readingProgressAnchorIndex ?: 0) - ) -} - -suspend fun Networker.updateWebReadingProgress(jsonString: String): Boolean { - val params = Gson().fromJson(jsonString, ReadingProgressParams::class.java) - return updateReadingProgress(params) + fun asSaveReadingProgressInput() = SaveArticleReadingProgressInput( + id = id ?: "", + force = Optional.presentIfNotNull(force), + readingProgressPercent = readingProgressPercent ?: 0.0, + readingProgressAnchorIndex = Optional.presentIfNotNull(readingProgressAnchorIndex ?: 0) + ) } suspend fun Networker.updateReadingProgress(params: ReadingProgressParams): Boolean { - try { - val input = params.asSaveReadingProgressInput() + try { + val input = params.asSaveReadingProgressInput() - Log.d("Loggo", "created reading progress input: $input") + Log.d("Loggo", "created reading progress input: $input") - val result = authenticatedApolloClient() - .mutation(SaveArticleReadingProgressMutation(input)) - .execute() + val result = authenticatedApolloClient().mutation(SaveArticleReadingProgressMutation(input)) + .execute() - val articleID = - result.data?.saveArticleReadingProgress?.onSaveArticleReadingProgressSuccess?.updatedArticle?.id + val articleID = + result.data?.saveArticleReadingProgress?.onSaveArticleReadingProgressSuccess?.updatedArticle?.id - Log.d("Loggo", "updated article with id: $articleID") + Log.d("Loggo", "updated article with id: $articleID") - return articleID != null - } catch (e: java.lang.Exception) { - return false - } + return articleID != null + } catch (e: java.lang.Exception) { + return false + } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemLabelQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemLabelQuery.kt index 27c21a8aa..4b07f7f10 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemLabelQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SavedItemLabelQuery.kt @@ -1,24 +1,24 @@ package app.omnivore.omnivore.core.network -import app.omnivore.omnivore.graphql.generated.GetLabelsQuery import app.omnivore.omnivore.core.database.entities.SavedItemLabel +import app.omnivore.omnivore.graphql.generated.GetLabelsQuery suspend fun Networker.savedItemLabels(): List { - try { - val result = authenticatedApolloClient().query(GetLabelsQuery()).execute() - val labels = result.data?.labels?.onLabelsSuccess?.labels ?: listOf() + try { + val result = authenticatedApolloClient().query(GetLabelsQuery()).execute() + val labels = result.data?.labels?.onLabelsSuccess?.labels ?: listOf() - return labels.map { - SavedItemLabel( - savedItemLabelId = it.labelFields.id, - name = it.labelFields.name, - color = it.labelFields.color, - createdAt = it.labelFields.createdAt as String?, - labelDescription = it.labelFields.description - ) + return labels.map { + SavedItemLabel( + savedItemLabelId = it.labelFields.id, + name = it.labelFields.name, + color = it.labelFields.color, + createdAt = it.labelFields.createdAt as String?, + labelDescription = it.labelFields.description + ) + } + } catch (e: java.lang.Exception) { + return listOf() } - } catch (e: java.lang.Exception) { - return listOf() - } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryFilterBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryFilterBar.kt index c9b1fa236..99e49aa07 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryFilterBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryFilterBar.kt @@ -33,11 +33,13 @@ import app.omnivore.omnivore.feature.components.LabelChipColors @Composable fun LibraryFilterBar( + isFollowingScreen: Boolean, viewModel: LibraryViewModel = hiltViewModel() ) { + var isSavedItemFilterMenuExpanded by remember { mutableStateOf(false) } val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState( - SavedItemFilter.INBOX + if (isFollowingScreen) SavedItemFilter.FOLLOWING else SavedItemFilter.INBOX ) val activeLabels: List by viewModel.activeLabelsLiveData.observeAsState(listOf()) @@ -58,7 +60,9 @@ fun LibraryFilterBar( ) { item { AssistChip(onClick = { isSavedItemFilterMenuExpanded = true }, - label = { Text(activeSavedItemFilter.displayText) }, + label = { Text( + activeSavedItemFilter.displayText + ) }, trailingIcon = { Icon( Icons.Default.ArrowDropDown, @@ -113,9 +117,12 @@ fun LibraryFilterBar( } } - SavedItemFilterContextMenu(isExpanded = isSavedItemFilterMenuExpanded, + SavedItemFilterContextMenu( + isFollowingScreen = isFollowingScreen, + isExpanded = isSavedItemFilterMenuExpanded, onDismiss = { isSavedItemFilterMenuExpanded = false }, - actionHandler = { viewModel.updateSavedItemFilter(it) }) + actionHandler = { viewModel.updateSavedItemFilter(it) } + ) SavedItemSortFilterContextMenu(isExpanded = isSavedItemSortFilterMenuExpanded, onDismiss = { isSavedItemSortFilterMenuExpanded = false }, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt index 5f51a4c04..a5f440625 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt @@ -21,7 +21,6 @@ import androidx.compose.material.DismissState import androidx.compose.material.DismissValue import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.FractionalThreshold -import androidx.compose.material.Icon import androidx.compose.material.ScaffoldState import androidx.compose.material.SwipeToDismiss import androidx.compose.material.icons.Icons @@ -33,6 +32,7 @@ import androidx.compose.material.rememberScaffoldState import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold @@ -60,7 +60,6 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController -import app.omnivore.omnivore.core.database.entities.SavedItemLabel import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights import app.omnivore.omnivore.feature.components.AddLinkSheetContent import app.omnivore.omnivore.feature.components.LabelsSelectionSheetContent @@ -147,6 +146,7 @@ internal fun LibraryView( when (uiState) { is LibraryUiState.Success -> { LibraryViewContent( + isFollowingScreen = currentTopLevelDestination == TopLevelDestination.FOLLOWING, viewModel, paddingValues = paddingValues, uiState = uiState @@ -189,9 +189,9 @@ fun LabelBottomSheet( ) { val currentSavedItemData = libraryViewModel.currentSavedItemUnderEdit() - val labels: List by libraryViewModel.savedItemLabelsLiveData.observeAsState( - listOf() - ) + + val labels by libraryViewModel.labelsState.collectAsStateWithLifecycle() + if (currentSavedItemData != null) { LabelsSelectionSheetContent( labels = labels, @@ -302,6 +302,7 @@ fun EditBottomSheet( @OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable fun LibraryViewContent( + isFollowingScreen: Boolean, libraryViewModel: LibraryViewModel, paddingValues: PaddingValues, uiState: LibraryUiState @@ -328,7 +329,7 @@ fun LibraryViewContent( .nestedScroll(pullToRefreshState.nestedScrollConnection) ) { Column { - LibraryFilterBar() + LibraryFilterBar(isFollowingScreen) HorizontalDivider() LazyColumn( state = listState, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt index f231db99e..8a06a7cc4 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt @@ -1,5 +1,6 @@ package app.omnivore.omnivore.feature.library +import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -7,30 +8,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.omnivore.omnivore.R -import app.omnivore.omnivore.core.data.DataService -import app.omnivore.omnivore.core.data.archiveSavedItem -import app.omnivore.omnivore.core.data.deleteSavedItem -import app.omnivore.omnivore.core.data.fetchSavedItemContent -import app.omnivore.omnivore.core.data.isSavedItemContentStoredInDB -import app.omnivore.omnivore.core.data.librarySearch import app.omnivore.omnivore.core.data.model.LibraryQuery import app.omnivore.omnivore.core.data.repository.LibraryRepository -import app.omnivore.omnivore.core.data.sync -import app.omnivore.omnivore.core.data.syncLabels -import app.omnivore.omnivore.core.data.syncOfflineItemsWithServerIfNeeded -import app.omnivore.omnivore.core.data.unarchiveSavedItem import app.omnivore.omnivore.core.database.entities.SavedItemLabel import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights import app.omnivore.omnivore.core.datastore.DatastoreRepository -import app.omnivore.omnivore.core.network.Networker -import app.omnivore.omnivore.core.network.createNewLabel -import app.omnivore.omnivore.feature.ResourceProvider -import app.omnivore.omnivore.feature.setSavedItemLabels -import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput import app.omnivore.omnivore.utils.DatastoreKeys -import com.apollographql.apollo3.api.Optional import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel @@ -50,11 +35,9 @@ import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class LibraryViewModel @Inject constructor( - private val networker: Networker, - private val dataService: DataService, private val datastoreRepo: DatastoreRepository, - private val resourceProvider: ResourceProvider, private val libraryRepository: LibraryRepository, + @ApplicationContext private val applicationContext: Context ) : ViewModel(), SavedItemViewModel { private val contentRequestChannel = Channel(capacity = Channel.UNLIMITED) @@ -62,7 +45,7 @@ class LibraryViewModel @Inject constructor( var snackbarMessage by mutableStateOf(null) private set - + private val _libraryQuery = MutableStateFlow( LibraryQuery( allowedArchiveStates = listOf(0), @@ -75,48 +58,43 @@ class LibraryViewModel @Inject constructor( val uiState: StateFlow = _libraryQuery.flatMapLatest { query -> libraryRepository.getSavedItems(query) - } - .map(LibraryUiState::Success) - .stateIn( + }.map(LibraryUiState::Success).stateIn( scope = viewModelScope, started = SharingStarted.Lazily, initialValue = LibraryUiState.Loading ) - - val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX) + val appliedFilterLiveData = MutableLiveData() val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST) val bottomSheetState = MutableLiveData(LibraryBottomSheetState.HIDDEN) val currentItem = mutableStateOf(null) - val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() + + val labelsState = libraryRepository.getSavedItemsLabels().stateIn( + scope = viewModelScope, started = SharingStarted.Lazily, initialValue = listOf() + ) + val activeLabelsLiveData = MutableLiveData>(listOf()) override val actionsMenuItemLiveData = MutableLiveData(null) - var isRefreshing by mutableStateOf(false) - private var hasLoadedInitialFilters = false - private fun loadInitialFilterValues() { - - if (hasLoadedInitialFilters) { - return - } - hasLoadedInitialFilters = false - - viewModelScope.launch { - withContext(Dispatchers.IO) { - dataService.syncLabels() - } - } + syncLabels() viewModelScope.launch { handleFilterChanges() for (slug in contentRequestChannel) { - CoroutineScope(Dispatchers.IO).launch { - dataService.fetchSavedItemContent(slug) - } + libraryRepository.fetchSavedItemContent(slug) } } + + updateSavedItemFilter(appliedFilterLiveData.value ?: SavedItemFilter.INBOX) + } + + private fun syncLabels() { + viewModelScope.launch { + val labels = libraryRepository.getLabels() + libraryRepository.insertAllLabels(labels) + } } fun clearSnackbarMessage() { @@ -125,7 +103,6 @@ class LibraryViewModel @Inject constructor( fun refresh() { librarySearchCursor = null - isRefreshing = true load() } @@ -141,13 +118,8 @@ class LibraryViewModel @Inject constructor( fun initialLoad() { if (getLastSyncTime() == null) { - hasLoadedInitialFilters = false librarySearchCursor = null } - - if (hasLoadedInitialFilters) { - return - } load() } @@ -162,24 +134,18 @@ class LibraryViewModel @Inject constructor( fun loadUsingSearchAPI() { viewModelScope.launch { - withContext(Dispatchers.IO) { - val result = dataService.librarySearch( - cursor = librarySearchCursor, query = searchQueryString() - ) - result.cursor?.let { - librarySearchCursor = it - } - CoroutineScope(Dispatchers.Main).launch { - isRefreshing = false - } + val result = libraryRepository.librarySearch( + cursor = librarySearchCursor, query = searchQueryString() + ) + result.cursor?.let { + librarySearchCursor = it + } + result.savedItems.map { + val isSavedInDB = libraryRepository.isSavedItemContentStoredInDB(it.savedItem.slug) - result.savedItems.map { - val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.savedItem.slug) - - if (!isSavedInDB) { - delay(2000) - contentRequestChannel.send(it.savedItem.slug) - } + if (!isSavedInDB) { + delay(2000) + contentRequestChannel.send(it.savedItem.slug) } } } @@ -232,6 +198,7 @@ class LibraryViewModel @Inject constructor( } var requiredLabels = when (appliedFilterLiveData.value) { + SavedItemFilter.FOLLOWING -> listOf("Newsletter", "RSS") SavedItemFilter.NEWSLETTERS -> listOf("Newsletter") SavedItemFilter.FEEDS -> listOf("RSS") else -> (activeLabelsLiveData.value ?: listOf()).map { it.name } @@ -268,9 +235,6 @@ class LibraryViewModel @Inject constructor( count = 0, startTime = syncStart.toString() ) - CoroutineScope(Dispatchers.Main).launch { - isRefreshing = false - } } } @@ -281,8 +245,8 @@ class LibraryViewModel @Inject constructor( startTime: String, isInitialBatch: Boolean = true ) { - dataService.syncOfflineItemsWithServerIfNeeded() - val result = dataService.sync(since = since, cursor = cursor, limit = 20) + libraryRepository.syncOfflineItemsWithServerIfNeeded() + val result = libraryRepository.sync(since = since, cursor = cursor, limit = 20) // Fetch content for the initial batch only if (isInitialBatch) { @@ -348,66 +312,39 @@ class LibraryViewModel @Inject constructor( fun deleteSavedItem(itemID: String) { viewModelScope.launch { - dataService.deleteSavedItem(itemID) + libraryRepository.deleteSavedItem(itemID) } } fun archiveSavedItem(itemID: String) { viewModelScope.launch { - dataService.archiveSavedItem(itemID) + libraryRepository.archiveSavedItem(itemID) } } fun unarchiveSavedItem(itemID: String) { viewModelScope.launch { - dataService.unarchiveSavedItem(itemID) + libraryRepository.unarchiveSavedItem(itemID) } } fun updateSavedItemLabels(savedItemID: String, labels: List) { viewModelScope.launch { - withContext(Dispatchers.IO) { - val result = setSavedItemLabels( - networker = networker, - dataService = dataService, - savedItemID = savedItemID, - labels = labels - ) - - snackbarMessage = if (result) { - resourceProvider.getString(R.string.library_view_model_snackbar_success) - } else { - resourceProvider.getString(R.string.library_view_model_snackbar_error) - } - - CoroutineScope(Dispatchers.Main).launch { - handleFilterChanges() - } + val result = libraryRepository.setSavedItemLabels( + itemId = savedItemID, labels = labels + ) + snackbarMessage = if (result) { + applicationContext.getString(R.string.library_view_model_snackbar_success) + } else { + applicationContext.getString(R.string.library_view_model_snackbar_error) } + handleFilterChanges() } } fun createNewSavedItemLabel(labelName: String, hexColorValue: String) { viewModelScope.launch { - withContext(Dispatchers.IO) { - val newLabel = networker.createNewLabel( - CreateLabelInput( - color = Optional.presentIfNotNull(hexColorValue), name = labelName - ) - ) - - newLabel?.let { - val savedItemLabel = SavedItemLabel( - savedItemLabelId = it.id, - name = it.name, - color = it.color, - createdAt = it.createdAt as String?, - labelDescription = it.description - ) - - dataService.db.savedItemLabelDao().insertAll(listOf(savedItemLabel)) - } - } + libraryRepository.createNewSavedItemLabel(labelName, hexColorValue) } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemFilter.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemFilter.kt index 4949c3f2e..88a9dd8f4 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemFilter.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/SavedItemFilter.kt @@ -7,38 +7,45 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.text.font.FontWeight enum class SavedItemFilter(val displayText: String, val rawValue: String, val queryString: String) { - INBOX("Inbox", rawValue = "inbox", "in:inbox"), READ_LATER( - "Non-Feed Items", "nonFeed", "no:subscription" - ), - FEEDS("Feeds", "feeds", "in:inbox label:RSS"), NEWSLETTERS( - "Newsletters", "newsletters", "in:inbox label:Newsletter" - ), - - // RECOMMENDED("Recommended", "recommended", "recommendedBy:*"), - ALL("All", "all", "in:all"), ARCHIVED("Archived", "archived", "in:archive"), - + FOLLOWING("Following", "following", "in:following use:folders"), + INBOX("Inbox", rawValue = "inbox", "in:inbox"), + READ_LATER("Non-Feed Items", "nonFeed", "no:subscription"), + FEEDS("Feeds", "feeds", "in:inbox label:RSS"), + NEWSLETTERS("Newsletters", "newsletters", "in:inbox label:Newsletter"), + ALL("All", "all", "in:all"), + ARCHIVED("Archived", "archived", "in:archive"), // HAS_HIGHLIGHTS("Highlighted", "hasHighlights", "has:highlights"), FILES("Files", "files", "type:file"), } @Composable fun SavedItemFilterContextMenu( - isExpanded: Boolean, onDismiss: () -> Unit, actionHandler: (SavedItemFilter) -> Unit + isFollowingScreen: Boolean, + isExpanded: Boolean, onDismiss: () -> Unit, + actionHandler: (SavedItemFilter) -> Unit ) { + + val filters = if (isFollowingScreen) { + listOf( + SavedItemFilter.FOLLOWING, + SavedItemFilter.FEEDS, + SavedItemFilter.NEWSLETTERS + ) + } else { + listOf( + SavedItemFilter.INBOX, + SavedItemFilter.READ_LATER, + SavedItemFilter.ALL, + SavedItemFilter.ARCHIVED, + SavedItemFilter.FILES + ) + } DropdownMenu( expanded = isExpanded, onDismissRequest = onDismiss ) { // Displaying only a subset of filters until we figure out the Room DB queries (and labels) // SavedItemFilter.values().forEach { - listOf( - SavedItemFilter.INBOX, - SavedItemFilter.READ_LATER, - SavedItemFilter.NEWSLETTERS, - SavedItemFilter.FEEDS, - SavedItemFilter.ALL, - SavedItemFilter.ARCHIVED, - SavedItemFilter.FILES - ).forEach { + filters.forEach { DropdownMenuItem(text = { Text(text = it.displayText, fontWeight = FontWeight.Normal) }, onClick = { actionHandler(it) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/LabelUtils.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/LabelUtils.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/LabelUtils.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/LabelUtils.kt index 0826d78b2..fb861fda2 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/LabelUtils.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/LabelUtils.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.feature +package app.omnivore.omnivore.feature.reader import app.omnivore.omnivore.core.data.DataService import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderViewModel.kt index 925c03c7c..60ce7e117 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderViewModel.kt @@ -34,7 +34,6 @@ import app.omnivore.omnivore.core.network.saveUrl import app.omnivore.omnivore.core.network.savedItem import app.omnivore.omnivore.feature.components.HighlightColor import app.omnivore.omnivore.feature.library.SavedItemAction -import app.omnivore.omnivore.feature.setSavedItemLabels import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput import app.omnivore.omnivore.utils.DatastoreKeys import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull @@ -464,7 +463,7 @@ class WebReaderViewModel @Inject constructor( val storedThemePreference = datastoreRepo.getString(DatastoreKeys.preferredTheme) ?: "System" val storedWebFont = - WebFont.values().firstOrNull { it.rawValue == storedFontFamily } ?: WebFont.values() + WebFont.entries.firstOrNull { it.rawValue == storedFontFamily } ?: WebFont.entries .first() val prefersHighContrastFont =