Load translation notes by visible chapter

This commit is contained in:
2026-07-12 11:47:31 -05:00
parent b91912649c
commit 41ca2b850c
7 changed files with 140 additions and 16 deletions
+53
View File
@@ -4,6 +4,7 @@ import {
getCommentaryForVerse,
getDictionaryEntriesForTerms,
getLibraryState,
getTranslationNotesForVerses,
getTranslations,
searchVerses,
} from './studyClient'
@@ -56,6 +57,16 @@ function reference(verse: Verse) {
return `${verse.book} ${verse.chapter}:${verse.verse}`
}
function replaceChangedVerses(verses: Verse[], hydrate: (verse: Verse) => Verse) {
let changed = false
const next = verses.map((verse) => {
const hydrated = hydrate(verse)
if (hydrated !== verse) changed = true
return hydrated
})
return changed ? next : verses
}
function createSession(title = 'Study Session', baseTranslation = defaultTranslationId): StudySession {
const now = new Date().toISOString()
const id =
@@ -268,6 +279,7 @@ function App() {
const pageMotionTimeout = useRef<number | null>(null)
const notesDragOpenTimeout = useRef<number | null>(null)
const readerRef = useRef<HTMLElement | null>(null)
const hydratedNoteChapters = useRef(new Set<string>())
async function loadLibrary(translationId = activeSession?.baseTranslation ?? defaultTranslationId) {
setError(null)
@@ -643,6 +655,47 @@ function App() {
.slice(start, start + pageSize)
.filter((verse) => verse.book === book)
}, [libraryVerses, pageStartId, selectedVerse, verses])
useEffect(() => {
if (!activeSession?.baseTranslation || pageVerses.length === 0) return
const chapterKeys = [
...new Set(
pageVerses.map(
(verse) => `${activeSession.baseTranslation}:${verse.book}:${verse.chapter}`,
),
),
]
const unloadedChapterKeys = chapterKeys.filter(
(chapterKey) => !hydratedNoteChapters.current.has(chapterKey),
)
if (unloadedChapterKeys.length === 0) return
for (const chapterKey of unloadedChapterKeys) hydratedNoteChapters.current.add(chapterKey)
let cancelled = false
getTranslationNotesForVerses(activeSession.baseTranslation, pageVerses)
.then((notesByVerse) => {
if (cancelled || notesByVerse.size === 0) return
const hydrate = (verse: Verse) => {
const notes = notesByVerse.get(String(verse.id))
if (!notes) return verse
if (
(verse.footnotes?.length ?? 0) === notes.footnotes.length &&
(verse.studyNotes?.length ?? 0) === notes.studyNotes.length
) {
return verse
}
return { ...verse, footnotes: notes.footnotes, studyNotes: notes.studyNotes }
}
setLibraryVerses((current) => replaceChangedVerses(current, hydrate))
setVerses((current) => replaceChangedVerses(current, hydrate))
})
.catch((reason) => setError(String(reason)))
return () => {
cancelled = true
}
}, [activeSession?.baseTranslation, pageVerses])
const pageRange = pageVerses.length
? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}`
: 'No page loaded'
+81 -10
View File
@@ -118,6 +118,8 @@ const browserLibraryCache = new Map<string, Promise<Verse[]>>()
const browserStrongDataCache = new Map<string, Promise<BrowserStrongData>>()
const browserFootnotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
const browserStudyNotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
const browserChapterFootnotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
const browserChapterStudyNotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
let browserCommentaryCache: Promise<Map<string, CommentaryEntry[]>> | null = null
let browserDictionaryCache: Promise<Map<string, DictionaryEntry>> | null = null
const browserVerseSourceIds = new Map<number, string>()
@@ -273,20 +275,12 @@ async function loadBrowserVersesForTranslation(translationId = defaultTranslatio
console.warn('Unable to load browser Strong data.', error)
return null
})
const footnotes = await loadBrowserFootnotes(translationId).catch((error) => {
console.warn('Unable to load browser footnotes.', error)
return new Map<string, Footnote[]>()
})
const studyNotes = await loadBrowserStudyNotes(translationId).catch((error) => {
console.warn('Unable to load browser study notes.', error)
return new Map<string, Footnote[]>()
})
const tagsByVerse = readBrowserTags()
return verses.map((verse) => ({
...verse,
strongs: strongData?.linksByVerse.get(browserVerseKey(verse)) ?? verse.strongs,
footnotes: footnotes.get(browserVerseKey(verse)) ?? verse.footnotes ?? [],
studyNotes: studyNotes.get(browserVerseKey(verse)) ?? verse.studyNotes ?? [],
footnotes: verse.footnotes ?? [],
studyNotes: verse.studyNotes ?? [],
tags: tagsByVerse[verse.id] ?? [],
}))
}
@@ -329,6 +323,37 @@ export async function getLibraryState(translationId = defaultTranslationId): Pro
}
}
export async function getTranslationNotesForVerses(
translationId: string,
verses: Verse[],
): Promise<Map<string, { footnotes: Footnote[]; studyNotes: Footnote[] }>> {
if (inTauri() || verses.length === 0) return new Map()
const chapterKeys = [
...new Set(
verses
.map((verse) => `${bookIdFromVerse(verse)}.${verse.chapter}`)
.filter((key) => !key.startsWith('unknown.')),
),
]
const [footnoteMaps, studyNoteMaps] = await Promise.all([
Promise.all(chapterKeys.map((key) => loadBrowserChapterNotes(translationId, key, 'footnotes'))),
Promise.all(chapterKeys.map((key) => loadBrowserChapterNotes(translationId, key, 'study-notes'))),
])
const notesByVerse = new Map<string, { footnotes: Footnote[]; studyNotes: Footnote[] }>()
for (const verse of verses) {
const verseKey = browserVerseKey(verse)
notesByVerse.set(String(verse.id), {
footnotes: footnoteMaps.flatMap((map) => map.get(verseKey) ?? []),
studyNotes: studyNoteMaps.flatMap((map) => map.get(verseKey) ?? []),
})
}
return notesByVerse
}
export async function searchVerses(query: string, translationId = defaultTranslationId): Promise<Verse[]> {
if (inTauri()) {
return invoke<Verse[]>('search_verses', { query })
@@ -539,6 +564,47 @@ async function loadLibreBibleDevStudyNotes(translationId = defaultTranslationId)
return mapImportedNotes(importedFootnotes)
}
async function loadBrowserChapterNotes(
translationId: string,
chapterKey: string,
kind: 'footnotes' | 'study-notes',
): Promise<Map<string, Footnote[]>> {
const cacheKey = `${translationId}:${chapterKey}:${kind}`
const cache =
kind === 'footnotes' ? browserChapterFootnotesCache : browserChapterStudyNotesCache
if (!cache.has(cacheKey)) {
cache.set(
cacheKey,
loadLibreBibleDevChapterNotes(translationId, chapterKey, kind).catch(async () => {
const fullNotes =
kind === 'footnotes'
? await loadBrowserFootnotes(translationId)
: await loadBrowserStudyNotes(translationId)
const filtered = new Map<string, Footnote[]>()
for (const [verseId, notes] of fullNotes) {
if (verseId.startsWith(`${chapterKey}.`)) filtered.set(verseId, notes)
}
return filtered
}),
)
}
return cache.get(cacheKey)!
}
async function loadLibreBibleDevChapterNotes(
translationId: string,
chapterKey: string,
kind: 'footnotes' | 'study-notes',
): Promise<Map<string, Footnote[]>> {
const importedNotes = await fetchJsonl<ImportedFootnote>(
`${libreBibleDevRoot}/${translationId}/notes-by-chapter/${chapterKey}.${kind}.jsonl`,
)
return mapImportedNotes(importedNotes)
}
function mapImportedNotes(importedFootnotes: ImportedFootnote[]): Map<string, Footnote[]> {
const footnotes = new Map<string, Footnote[]>()
@@ -619,6 +685,11 @@ function strongLanguage(strong: string): 'Hebrew' | 'Greek' {
return strong.startsWith('H') ? 'Hebrew' : 'Greek'
}
function bookIdFromVerse(verse: Verse): string {
const sourceId = browserVerseSourceIds.get(verse.id)
return sourceId?.split('.')[0] ?? 'unknown'
}
function browserVerseKey(verse: Verse) {
return browserVerseSourceIds.get(verse.id) ?? ''
}