diff --git a/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql b/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql new file mode 100644 index 000000000..e7426f016 --- /dev/null +++ b/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql @@ -0,0 +1,56 @@ +query UpdatesSince($after: String, $first: Int, $since: Date!) { + updatesSince(after: $after, first: $first, since: $since) { + ... on UpdatesSinceSuccess { + edges { + cursor + itemID + updateReason + node { + id + title + slug + url + pageType + contentReader + createdAt + isArchived + readingProgressPercent + readingProgressAnchorIndex + author + image + description + publishedAt + ownedByViewer + originalArticleUrl + uploadFileId + labels { + id + name + color + } + pageId + shortId + quote + annotation + state + siteName + subscription + readAt + savedAt + updatedAt + language + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + totalCount + } + } + ... on UpdatesSinceError { + errorCodes + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt index 075eb297b..a7a015770 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt @@ -12,6 +12,7 @@ object DatastoreKeys { const val omnivoreAuthToken = "omnivoreAuthToken" const val omnivoreAuthCookieString = "omnivoreAuthCookieString" const val omnivorePendingUserToken = "omnivorePendingUserToken" + const val libraryLastSyncTimestamp = "libraryLastSyncTimestamp" const val preferredWebFontSize = "preferredWebFontSize" const val preferredWebLineHeight = "preferredWebLineHeight" const val preferredWebMaxWidthPercentage = "preferredWebMaxWidthPercentage" 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 07f449474..460c5486c 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 @@ -15,3 +15,7 @@ class DataService @Inject constructor( AppDatabase::class.java, "omnivore-database" ).build() } + +//suspend fun DataService.sync(): Boolean { +// +//} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/ServerSyncStatus.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/ServerSyncStatus.kt new file mode 100644 index 000000000..e55ffe1ef --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/ServerSyncStatus.kt @@ -0,0 +1,11 @@ +package app.omnivore.omnivore.models + +public enum class ServerSyncStatus( + public val rawValue: Int, +) { + IS_SYNCED(0), + IS_SYNCING(1), + NEEDS_DELETION(2), + NEEDS_CREATION(3), + NEEDS_UPDATE(4) +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt index 866b694e3..bf9ea94c6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt @@ -8,6 +8,7 @@ import app.omnivore.omnivore.graphql.generated.UpdateHighlightMutation import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput +import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.persistence.entities.Highlight import com.apollographql.apollo3.api.Optional import com.google.gson.Gson @@ -150,8 +151,7 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? { createdAt = null, // TODO: update gql query to get this updatedAt = null, // TODO: fix updatedAtString?.let { LocalDate.parse(it) }, createdByMe = createdHighlight.highlightFields.createdByMe, - markedForDeletion = false, - serverSyncStatus = 1 // TODO: create enum for this + markedForDeletion = false ) } else { return null diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt index 107d17e12..882c3d304 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt @@ -53,7 +53,6 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse { updatedAt = null, //updatedAtString?.let { str -> LocalDate.parse(str) }, TODO: fix date parsing createdByMe = it.highlightFields.createdByMe, markedForDeletion = false, - serverSyncStatus = 1 // TODO: create enum for this ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt index 429030f6c..1c705280f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt @@ -2,6 +2,8 @@ package app.omnivore.omnivore.networking import app.omnivore.omnivore.graphql.generated.SearchQuery import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery +import app.omnivore.omnivore.graphql.generated.UpdatesSinceQuery +import app.omnivore.omnivore.graphql.generated.type.UpdateReason import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.persistence.entities.SavedItemCardData import com.apollographql.apollo3.api.Optional @@ -11,6 +13,14 @@ data class SearchQueryResponse( val cardsData: List ) +data class SavedItemUpdatesQueryResponse( + val cursor: String?, + val hasMoreItems: Boolean, + val totalCount: Int, + val deletedItemIDs: List, + val items: List +) + suspend fun Networker.typeaheadSearch( query: String ): SearchQueryResponse { @@ -55,7 +65,6 @@ suspend fun Networker.search( ) ).execute() - val newCursor = result.data?.search?.onSearchSuccess?.pageInfo?.endCursor val itemList = result.data?.search?.onSearchSuccess?.edges ?: listOf() @@ -78,3 +87,65 @@ suspend fun Networker.search( return SearchQueryResponse(null, listOf()) } } + +suspend fun Networker.savedItemUpdates( + cursor: String? = null, + limit: Int = 15, + since: String +): SavedItemUpdatesQueryResponse? { + try { + val result = authenticatedApolloClient().query( + UpdatesSinceQuery( + after = Optional.presentIfNotNull(cursor), + first = Optional.presentIfNotNull(limit), + since = since + ) + ).execute() + + val payload = result.data?.updatesSince?.onUpdatesSinceSuccess ?: return null + val itemNodes: MutableList = mutableListOf() + val deletedItemIDs: MutableList = mutableListOf() + + for (edge in payload.edges) { + if (edge.updateReason == UpdateReason.DELETED) { + deletedItemIDs.add(edge.itemID) + } else if (edge.node != null) { + itemNodes.add(edge.node) + } + } + + val savedItems = itemNodes.map { + SavedItem( + id = 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 + ) + } + + return SavedItemUpdatesQueryResponse( + cursor = payload.pageInfo.endCursor, + hasMoreItems = payload.pageInfo.hasNextPage, + totalCount = savedItems.size, + deletedItemIDs = deletedItemIDs, + items = savedItems + ) + } catch (e: java.lang.Exception) { + return null + } +} 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 7b2331330..327f039fc 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 @@ -2,6 +2,7 @@ package app.omnivore.omnivore.persistence.entities import androidx.room.Entity import androidx.room.PrimaryKey +import app.omnivore.omnivore.models.ServerSyncStatus import java.time.LocalDate import java.util.Date 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 33cc02ca0..1f0935697 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 @@ -2,6 +2,7 @@ package app.omnivore.omnivore.persistence.entities import androidx.core.net.toUri import androidx.room.* +import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.persistence.BaseDao @Entity @@ -35,7 +36,7 @@ data class SavedItem( val onDeviceImageURLString: String? = null, val originalHtml: String? = null, @ColumnInfo(typeAffinity = ColumnInfo.BLOB) val pdfData: ByteArray? = null, - val serverSyncStatus: Int = 0, // TODO: implement, + val serverSyncStatus: Int = 0, val tempPDFURL: String? = null // hasMany highlights 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 14a0bbdf0..f13983838 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 @@ -2,6 +2,7 @@ package app.omnivore.omnivore.persistence.entities import androidx.room.Entity import androidx.room.PrimaryKey +import app.omnivore.omnivore.models.ServerSyncStatus @Entity data class SavedItemLabel( 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 e6fecd759..e3b41b37a 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 @@ -8,19 +8,20 @@ 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.networking.* import app.omnivore.omnivore.persistence.entities.SavedItemCardData import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* +import java.time.LocalDateTime import javax.inject.Inject @HiltViewModel class LibraryViewModel @Inject constructor( private val networker: Networker, - private val dataService: DataService + private val dataService: DataService, + private val datastoreRepo: DatastoreRepository ): ViewModel() { private var cursor: String? = null private var items: List = listOf() @@ -51,6 +52,19 @@ class LibraryViewModel @Inject constructor( load(true) } + fun getLastSyncTime(): LocalDateTime? = runBlocking { + datastoreRepo.getString(DatastoreKeys.libraryLastSyncTimestamp)?.let { + LocalDateTime.parse(it) + } + } + + fun syncItems() { + val syncStart = LocalDateTime.now() + val lastSyncDate = getLastSyncTime() ?: LocalDateTime.MIN + +// try? await dataService.syncOfflineItemsWithServerIfNeeded() + } + fun load(clearPreviousSearch: Boolean = false) { if (clearPreviousSearch) { cursor = null diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 8c99130f9..c0f0b1520 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -107,6 +107,13 @@ import Views _ = try? await dataService.labels() } } + + // Sync Items + // 1 - Create start timestamp + // 2 - Retrieve last sync time from datastore + // 3 - Call syncOfflineItemsWithServerIfNeeded (DataService) + // 4 - Call dataService.syncLinkedItems + // func syncItems(dataService: DataService) async { let syncStart = Date.now