Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ object HighlightsEndpoints : HighlightsApi {
color: String,
): Boolean =
httpClient
.put {
highlightsUrl()
.put(highlightsUrl()) {
contentType(ContentType.Application.Json)
buildJsonObject {
put("version_id", versionId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ package com.youversion.platform.core.highlights.domain

import com.youversion.platform.core.bibles.domain.BibleReference
import com.youversion.platform.core.highlights.models.BibleHighlight
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import java.util.Date
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes

Expand All @@ -24,25 +29,24 @@ object BibleHighlightCache {
)

// ----- Observable State
private val cachedHighlights = mutableListOf<CachedHighlight>()
val highlights: List<CachedHighlight>
get() = cachedHighlights.toList()
private val _highlights = MutableStateFlow<List<CachedHighlight>>(emptyList())
val highlights: StateFlow<List<CachedHighlight>> = _highlights.asStateFlow()

// ----- Throttling and Loading
private var recentChapterFetches = mutableMapOf<BibleReference, Date>()
private var currentlyLoadingChapters = mutableSetOf<BibleReference>()
private val recentChapterFetches = ConcurrentHashMap<BibleReference, Date>()
private val currentlyLoadingChapters = ConcurrentHashMap.newKeySet<BibleReference>()
private val throttlingInterval: Duration = 5.minutes

// ----- Public API - State Management
fun clear() {
cachedHighlights.clear()
_highlights.value = emptyList()
recentChapterFetches.clear()
currentlyLoadingChapters.clear()
}

// ----- Public API - Queries
fun highlights(overlapping: BibleReference): List<BibleHighlight> =
cachedHighlights
_highlights.value
.filter { it.highlight.bibleReference.overlaps(otherReference = overlapping) }
.map { it.highlight }

Expand All @@ -55,14 +59,8 @@ object BibleHighlightCache {
fun isChapterLoading(chapter: BibleReference): Boolean =
currentlyLoadingChapters.contains(normalizeToChapter(chapter))

fun markChapterAsLoading(chapter: BibleReference): Boolean {
val normalized = normalizeToChapter(chapter)
if (currentlyLoadingChapters.contains(normalized)) {
return false
}
currentlyLoadingChapters.add(normalized)
return true
}
fun markChapterAsLoading(chapter: BibleReference): Boolean =
currentlyLoadingChapters.add(normalizeToChapter(chapter))

fun unmarkChapterAsLoading(chapter: BibleReference) {
currentlyLoadingChapters.remove(normalizeToChapter(chapter))
Expand All @@ -77,65 +75,78 @@ object BibleHighlightCache {

// ----- Public API - Mutations (write APIs preserved)
fun addHighlights(highlights: List<BibleHighlight>) {
for (highlight in highlights) {
// Remove any existing for same exact reference; then append as pending create
cachedHighlights.removeAll { it.highlight.bibleReference == highlight.bibleReference }
cachedHighlights.add(
CachedHighlight(highlight = highlight, state = CachedHighlightState.LOCAL_PENDING_CREATE),
)
_highlights.update { current ->
current.toMutableList().apply {
for (highlight in highlights) {
// Remove any existing for same exact reference; then append as pending create
removeAll { it.highlight.bibleReference == highlight.bibleReference }
add(
CachedHighlight(highlight = highlight, state = CachedHighlightState.LOCAL_PENDING_CREATE),
)
}
}
}
}

fun removeHighlights(references: List<BibleReference>) {
for (reference in references) {
// If there is a pending create for this reference, just drop it; otherwise mark pending delete
val pendingCreateIdx =
cachedHighlights.indexOfFirst {
it.highlight.bibleReference == reference && it.state == CachedHighlightState.LOCAL_PENDING_CREATE
}
if (pendingCreateIdx != -1) {
cachedHighlights.removeAt(pendingCreateIdx)
} else {
val idx = cachedHighlights.indexOfFirst { it.highlight.bibleReference == reference }
if (idx != -1) {
cachedHighlights[idx] =
cachedHighlights[idx].copy(
state = CachedHighlightState.LOCAL_PENDING_DELETE,
lastModifiedAt = Date(),
)
_highlights.update { current ->
current.toMutableList().apply {
for (reference in references) {
// If there is a pending create for this reference, just drop it; otherwise mark pending delete
val pendingCreateIndex =
indexOfFirst {
it.highlight.bibleReference == reference &&
it.state == CachedHighlightState.LOCAL_PENDING_CREATE
}
if (pendingCreateIndex != -1) {
removeAt(pendingCreateIndex)
} else {
val index = indexOfFirst { it.highlight.bibleReference == reference }
if (index != -1) {
this[index] =
this[index].copy(
state = CachedHighlightState.LOCAL_PENDING_DELETE,
lastModifiedAt = Date(),
)
}
}
}
// Physically remove deletes so the visible list hides them
removeAll { it.state == CachedHighlightState.LOCAL_PENDING_DELETE }
}
}
// Optionally, physically remove localPendingDelete from visible list here if UI should hide deletes
cachedHighlights.removeAll { it.state == CachedHighlightState.LOCAL_PENDING_DELETE }
}

fun updateHighlightColors(
references: List<BibleReference>,
newColor: String,
) {
for (reference in references) {
val idx = cachedHighlights.indexOfFirst { it.highlight.bibleReference == reference }
if (idx != -1) {
cachedHighlights[idx] =
cachedHighlights[idx].copy(
highlight = BibleHighlight(bibleReference = reference, hexColor = newColor),
state =
if (cachedHighlights[idx].state != CachedHighlightState.LOCAL_PENDING_CREATE) {
CachedHighlightState.LOCAL_PENDING_UPDATE
} else {
cachedHighlights[idx].state
},
lastModifiedAt = Date(),
)
} else {
// Create if not exists, pending create
cachedHighlights.add(
CachedHighlight(
highlight = BibleHighlight(bibleReference = reference, hexColor = newColor),
state = CachedHighlightState.LOCAL_PENDING_CREATE,
),
)
_highlights.update { current ->
current.toMutableList().apply {
for (reference in references) {
val index = indexOfFirst { it.highlight.bibleReference == reference }
if (index != -1) {
this[index] =
this[index].copy(
highlight = BibleHighlight(bibleReference = reference, hexColor = newColor),
state =
if (this[index].state != CachedHighlightState.LOCAL_PENDING_CREATE) {
CachedHighlightState.LOCAL_PENDING_UPDATE
} else {
this[index].state
},
lastModifiedAt = Date(),
)
} else {
// Create if not exists, pending create
add(
CachedHighlight(
highlight = BibleHighlight(bibleReference = reference, hexColor = newColor),
state = CachedHighlightState.LOCAL_PENDING_CREATE,
),
)
}
}
}
}
}
Expand All @@ -147,22 +158,26 @@ object BibleHighlightCache {
) {
val chapterRef = normalizeToChapter(chapter)

// Remove existing remote-synced highlights for this chapter
cachedHighlights.removeAll { ch ->
ch.state == CachedHighlightState.REMOTE_SYNCED &&
ch.highlight.bibleReference.bookUSFM == chapterRef.bookUSFM &&
ch.highlight.bibleReference.chapter == chapterRef.chapter &&
ch.highlight.bibleReference.versionId == chapterRef.versionId
}
_highlights.update { current ->
current.toMutableList().apply {
// Remove existing remote-synced highlights for this chapter
removeAll { cached ->
cached.state == CachedHighlightState.REMOTE_SYNCED &&
cached.highlight.bibleReference.bookUSFM == chapterRef.bookUSFM &&
cached.highlight.bibleReference.chapter == chapterRef.chapter &&
cached.highlight.bibleReference.versionId == chapterRef.versionId
}

// Append server highlights as remoteSynced
for (h in highlights) {
cachedHighlights.add(
CachedHighlight(
highlight = h,
state = CachedHighlightState.REMOTE_SYNCED,
),
)
// Append server highlights as remoteSynced
for (highlight in highlights) {
add(
CachedHighlight(
highlight = highlight,
state = CachedHighlightState.REMOTE_SYNCED,
),
)
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
}
}
}

Expand Down
Loading
Loading