Fix issues with creating notes and labels on Android

This commit is contained in:
Jackson Harper
2023-11-09 11:31:57 +08:00
parent b71a89e370
commit bfc90b607f
7 changed files with 205 additions and 70 deletions

View File

@ -1,5 +1,6 @@
package app.omnivore.omnivore.dataService package app.omnivore.omnivore.dataService
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
import app.omnivore.omnivore.graphql.generated.type.HighlightType import app.omnivore.omnivore.graphql.generated.type.HighlightType
import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.models.ServerSyncStatus
import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.networking.*
@ -50,7 +51,7 @@ suspend fun DataService.createWebHighlight(jsonString: String, colorName: String
} }
suspend fun DataService.createNoteHighlight(savedItemId: String, note: String): String { suspend fun DataService.createNoteHighlight(savedItemId: String, note: String): String {
val shortId = UUID.randomUUID().toString() val shortId = NanoId.generate(size=14)
val createHighlightId = UUID.randomUUID().toString() val createHighlightId = UUID.randomUUID().toString()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View File

@ -0,0 +1,121 @@
package app.omnivore.omnivore.dataService
import org.jetbrains.annotations.NotNull
import java.security.SecureRandom
import java.util.*
import kotlin.math.abs
import kotlin.math.ceil
/**
* NanoId is a utility object providing functions for generating secure, URL-friendly, unique identifiers.
*
* The object offers methods for generating random strings with adjustable parameters like size, alphabet,
* overhead factor, and a custom random number generator.
*
* Example usage:
* ```
* val id = NanoId.generate()
* ```
*/
object NanoId {
/**
* Generates a random string based on specified or default parameters.
*
* @param size The desired length of the generated string. Default is 21.
* @param alphabet The set of characters to choose from for generating the string. Default includes alphanumeric characters along with "_" and "-".
* @param additionalBytesFactor The additional bytes factor used for calculating the step size. Default is 1.6.
* @param random The random number generator to use. Default is `SecureRandom`.
* @return The generated random string.
* @throws IllegalArgumentException if the alphabet is empty or larger than 255 characters, or if the size is not greater than zero.
*/
@JvmOverloads
fun generate(
@NotNull
size: Int = 21,
@NotNull
alphabet: String = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
@NotNull
additionalBytesFactor: Double = 1.6,
@NotNull
random: Random = SecureRandom()
): String {
require(!(alphabet.isEmpty() || alphabet.length >= 256)) { "alphabet must contain between 1 and 255 symbols." }
require(size > 0) { "size must be greater than zero." }
require(additionalBytesFactor >= 1) { "additionalBytesFactor must be greater or equal 1." }
val mask = calculateMask(alphabet)
val step = calculateStep(size, alphabet, additionalBytesFactor)
return generateOptimized(size, alphabet, mask, step, random)
}
/**
* Generates an optimized random string of a specified size using the given alphabet, mask, and step.
* Optionally, you can specify a custom random number generator. This optimized version is designed for
* higher performance and lower memory overhead.
*
* @param size The desired length of the generated string.
* @param alphabet The set of characters to choose from for generating the string.
* @param mask The mask used for mapping random bytes to alphabet indices. Should be `(2^n) - 1` where `n` is a power of 2 less than or equal to the alphabet size.
* @param step The number of random bytes to generate in each iteration. A larger value may speed up the function but increase memory usage.
* @param random The random number generator. Default is `SecureRandom`.
* @return The generated optimized string.
*/
@JvmOverloads
fun generateOptimized(@NotNull size: Int, @NotNull alphabet: String, @NotNull mask: Int, @NotNull step: Int, @NotNull random: Random = SecureRandom()): String {
val idBuilder = StringBuilder(size)
val bytes = ByteArray(step)
while (true) {
random.nextBytes(bytes)
for (i in 0 until step) {
val alphabetIndex = bytes[i].toInt() and mask
if (alphabetIndex < alphabet.length) {
idBuilder.append(alphabet[alphabetIndex])
if (idBuilder.length == size) {
return idBuilder.toString()
}
}
}
}
}
/**
* Calculates the optimal additional bytes factor needed for the generation of the step size, which is used to generate random bytes in each iteration.
*
* @param alphabet The set of characters to use for generating the string.
* @return The additional bytes factor, rounded to two decimal places.
*/
fun calculateAdditionalBytesFactor(@NotNull alphabet: String): Double {
val mask = calculateMask(alphabet)
return (1 + abs((mask - alphabet.length.toDouble()) / alphabet.length)).round(2)
}
/**
* Calculates the mask used to map random bytes to indices in the alphabet.
*
* @param alphabet The set of characters to use for generating the string.
* @return The calculated mask value.
*/
fun calculateMask(@NotNull alphabet: String) = (2 shl (Integer.SIZE - 1 - Integer.numberOfLeadingZeros(alphabet.length - 1))) - 1
/**
* Calculates the number of random bytes to generate in each iteration for a given size and alphabet.
*
* @param size The length of the generated string.
* @param alphabet The set of characters to use for generating the string.
* @param additionalBytesFactor The additional bytes factor. Default value is calculated using `calculateAdditionalBytesFactor()`.
* @return The number of random bytes to generate in each iteration.
*/
@JvmOverloads
fun calculateStep(@NotNull size: Int, @NotNull alphabet: String, @NotNull additionalBytesFactor: Double = calculateAdditionalBytesFactor(alphabet)) =
ceil(additionalBytesFactor * calculateMask(alphabet) * size / alphabet.length).toInt()
@JvmSynthetic
internal fun Double.round(decimals: Int): Double {
var multiplier = 1.0
repeat(decimals) { multiplier *= 10 }
return kotlin.math.round(this * multiplier) / multiplier
}
}

View File

@ -139,6 +139,8 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? {
try { try {
val result = authenticatedApolloClient().mutation(CreateHighlightMutation(input)).execute() val result = authenticatedApolloClient().mutation(CreateHighlightMutation(input)).execute()
Log.d("Loggo", "result: ${result.data}")
val createdHighlight = result.data?.createHighlight?.onCreateHighlightSuccess?.highlight val createdHighlight = result.data?.createHighlight?.onCreateHighlightSuccess?.highlight

View File

@ -0,0 +1,59 @@
package app.omnivore.omnivore.ui
import app.omnivore.omnivore.dataService.DataService
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput
import app.omnivore.omnivore.networking.Networker
import app.omnivore.omnivore.networking.updateLabelsForSavedItem
import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
import com.apollographql.apollo3.api.Optional
suspend fun setSavedItemLabels(
networker: Networker,
dataService: DataService,
savedItemID: String,
labels: List<SavedItemLabel>
): Boolean {
val input = SetLabelsInput(
pageId = savedItemID,
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 = dataService.db.savedItemLabelDao()
.namedLabels(updatedLabels.map { it.labelFields.name })
val existingNames = existingNamedLabels.map { it.name }
val newNamedLabels = updatedLabels.filter { !existingNames.contains(it.labelFields.name) }
dataService.db.savedItemLabelDao().insertAll(newNamedLabels.map {
SavedItemLabel(
savedItemLabelId = it.labelFields.id,
name = it.labelFields.name,
color = it.labelFields.color,
createdAt = null,
labelDescription = null
)
})
val allNamedLabels = dataService.db.savedItemLabelDao()
.namedLabels(updatedLabels.map { it.labelFields.name })
val crossRefs = allNamedLabels.map {
SavedItemAndSavedItemLabelCrossRef(
savedItemLabelId = it.savedItemLabelId,
savedItemId = savedItemID
)
}
dataService.db.savedItemAndSavedItemLabelCrossRefDao().deleteRefsBySavedItemId(savedItemID)
dataService.db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs)
return true
} ?: run {
return false
}
}

View File

@ -20,6 +20,8 @@ import app.omnivore.omnivore.models.ServerSyncStatus
import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.networking.*
import app.omnivore.omnivore.persistence.entities.* import app.omnivore.omnivore.persistence.entities.*
import app.omnivore.omnivore.ui.ResourceProvider import app.omnivore.omnivore.ui.ResourceProvider
import app.omnivore.omnivore.ui.setSavedItemLabels
import coil.util.CoilUtils.result
import com.apollographql.apollo3.api.Optional import com.apollographql.apollo3.api.Optional
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -305,43 +307,16 @@ class LibraryViewModel @Inject constructor(
fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) { fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val input = SetLabelsInput( val result = setSavedItemLabels(
pageId = savedItemID, networker = networker,
labels = Optional.presentIfNotNull(labels.map { CreateLabelInput(color = Optional.presentIfNotNull(it.color), name = it.name) }), dataService = dataService,
savedItemID = savedItemID,
labels = labels
) )
val updatedLabels = networker.updateLabelsForSavedItem(input) if (result) {
// Figure out which of the labels are new
updatedLabels?.let { updatedLabels ->
val existingNamedLabels = dataService.db.savedItemLabelDao()
.namedLabels(updatedLabels.map { it.labelFields.name })
val existingNames = existingNamedLabels.map { it.name }
val newNamedLabels = updatedLabels.filter { !existingNames.contains(it.labelFields.name) }
dataService.db.savedItemLabelDao().insertAll(newNamedLabels.map {
SavedItemLabel(
savedItemLabelId = it.labelFields.id,
name = it.labelFields.name,
color = it.labelFields.color,
createdAt = null,
labelDescription = null
)
})
val allNamedLabels = dataService.db.savedItemLabelDao()
.namedLabels(updatedLabels.map { it.labelFields.name })
val crossRefs = allNamedLabels.map {
SavedItemAndSavedItemLabelCrossRef(
savedItemLabelId = it.savedItemLabelId,
savedItemId = savedItemID
)
}
dataService.db.savedItemAndSavedItemLabelCrossRefDao().deleteRefsBySavedItemId(savedItemID)
dataService.db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs)
snackbarMessage = resourceProvider.getString(R.string.library_view_model_snackbar_success) snackbarMessage = resourceProvider.getString(R.string.library_view_model_snackbar_success)
} ?: run { } else {
snackbarMessage = resourceProvider.getString(R.string.library_view_model_snackbar_error) snackbarMessage = resourceProvider.getString(R.string.library_view_model_snackbar_error)
} }

View File

@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.omnivore.omnivore.DatastoreRepository import app.omnivore.omnivore.DatastoreRepository
import app.omnivore.omnivore.dataService.DataService import app.omnivore.omnivore.dataService.DataService
import app.omnivore.omnivore.dataService.NanoId
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
@ -146,11 +147,11 @@ class PDFReaderViewModel @Inject constructor(
fun syncHighlightUpdates(newAnnotation: Annotation, quote: String, overlapIds: List<String>, note: String? = null) { fun syncHighlightUpdates(newAnnotation: Annotation, quote: String, overlapIds: List<String>, note: String? = null) {
val itemID = pdfReaderParamsLiveData.value?.item?.savedItemId ?: return val itemID = pdfReaderParamsLiveData.value?.item?.savedItemId ?: return
val highlightID = UUID.randomUUID().toString() val highlightID = UUID.randomUUID().toString()
val shortID = UUID.randomUUID().toString().replace("-","").substring(0,8) val shortId = NanoId.generate(size=14)
val jsonValues = JSONObject() val jsonValues = JSONObject()
.put("id", highlightID) .put("id", highlightID)
.put("shortId", shortID) .put("shortId", shortId)
.put("quote", quote) .put("quote", quote)
.put("articleId", itemID) .put("articleId", itemID)
@ -164,7 +165,7 @@ class PDFReaderViewModel @Inject constructor(
overlapHighlightIdList = overlapIds, overlapHighlightIdList = overlapIds,
patch = newAnnotation.toInstantJson(), patch = newAnnotation.toInstantJson(),
quote = quote, quote = quote,
shortId = shortID shortId = shortId
) )
viewModelScope.launch { viewModelScope.launch {
@ -177,7 +178,7 @@ class PDFReaderViewModel @Inject constructor(
id = highlightID, id = highlightID,
patch = Optional.presentIfNotNull(newAnnotation.toInstantJson()), patch = Optional.presentIfNotNull(newAnnotation.toInstantJson()),
quote = Optional.presentIfNotNull(quote), quote = Optional.presentIfNotNull(quote),
shortId = shortID, shortId = shortId,
) )
viewModelScope.launch { viewModelScope.launch {

View File

@ -23,6 +23,7 @@ import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCros
import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.persistence.entities.SavedItemLabel
import app.omnivore.omnivore.ui.components.HighlightColor import app.omnivore.omnivore.ui.components.HighlightColor
import app.omnivore.omnivore.ui.library.SavedItemAction import app.omnivore.omnivore.ui.library.SavedItemAction
import app.omnivore.omnivore.ui.setSavedItemLabels
import com.apollographql.apollo3.api.Optional import com.apollographql.apollo3.api.Optional
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
import com.google.gson.Gson import com.google.gson.Gson
@ -500,38 +501,13 @@ class WebReaderViewModel @Inject constructor(
fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) { fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val namedLabels = dataService.db.savedItemLabelDao().namedLabels(labels.map { it.name })
namedLabels.filter { it.serverSyncStatus != ServerSyncStatus.IS_SYNCED.rawValue }.mapNotNull { setSavedItemLabels(
val result = networker.createNewLabel(CreateLabelInput(color = presentIfNotNull(it.color), name = it.name)) networker = networker,
result?.let { it1 -> dataService = dataService,
SavedItemLabel( savedItemID = savedItemID,
savedItemLabelId = it1.id, labels = labels
name = result.name,
color = result.color,
createdAt = result.createdAt.toString(),
labelDescription = result.description,
serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
) )
}
}
val input = SetLabelsInput(labelIds = Optional.presentIfNotNull(namedLabels.map { it.savedItemLabelId }), pageId = savedItemID)
val networkResult = networker.updateLabelsForSavedItem(input)
// TODO: assign a server sync status to these
val crossRefs = namedLabels.map {
SavedItemAndSavedItemLabelCrossRef(
savedItemLabelId = it.savedItemLabelId,
savedItemId = savedItemID
)
}
// Remove all labels first
dataService.db.savedItemAndSavedItemLabelCrossRefDao().deleteRefsBySavedItemId(savedItemID)
// Add back the current labels
dataService.db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs)
slug?.let { slug?.let {
loadItemFromDB(it) loadItemFromDB(it)