Files
EMI-ExpoAPP/Views/BibleChapterView.js
T

244 lines
11 KiB
JavaScript

import React from "react";
import { FlatList, Pressable, View } from "react-native";
import { ActivityIndicator, IconButton, Menu, Text, Button } from "react-native-paper";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native";
import { fetchBibleChapter, parseBibleReference, AVAILABLE_TRANSLATIONS, BIBLE_BOOKS, getBookChapterCount } from "../utils/bibleReferences.js";
import GlobalState from "../contexts/GlobalState.js";
import { useSnapshot } from "valtio";
import API from "../API.js";
import i18n from "../i18nMessages.js";
const BibleChapterView = ({ route }) => {
const navigation = useNavigation();
const gState = useSnapshot(GlobalState);
const reference = route?.params?.reference || "";
const selectable = route?.params?.selectable === true;
const { chapterReference, verse: selectedVerse, book, chapter } = parseBibleReference(reference);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState("");
const [chapterData, setChapterData] = React.useState(null);
const initialTranslation = gState.me?.data?.bibleTranslation || "";
const [selectedTranslation, setSelectedTranslation] = React.useState(initialTranslation);
const [translationMenuVisible, setTranslationMenuVisible] = React.useState(false);
const listRef = React.useRef(null);
const autoScrolledRef = React.useRef(false);
const bookIndex = BIBLE_BOOKS.indexOf(book);
const chapterCount = getBookChapterCount(book);
let prevReference = null;
if (chapter > 1) {
prevReference = `${book} ${chapter - 1}`;
} else if (bookIndex > 0) {
const prevBook = BIBLE_BOOKS[bookIndex - 1];
const prevBookChapters = getBookChapterCount(prevBook);
prevReference = `${prevBook} ${prevBookChapters}`;
}
let nextReference = null;
if (chapter < chapterCount) {
nextReference = `${book} ${chapter + 1}`;
} else if (bookIndex >= 0 && bookIndex < BIBLE_BOOKS.length - 1) {
const nextBook = BIBLE_BOOKS[bookIndex + 1];
nextReference = `${nextBook} 1`;
}
React.useEffect(() => {
let mounted = true;
const loadChapter = async () => {
setLoading(true);
setError("");
try {
const payload = await fetchBibleChapter(chapterReference, i18n.locale, selectedTranslation);
if (!mounted) return;
setChapterData(payload);
} catch (_error) {
if (!mounted) return;
setError(i18n.t("message.unableLoadChapter") || "Unable to load chapter");
setChapterData(null);
} finally {
if (mounted) setLoading(false);
}
};
loadChapter();
return () => {
mounted = false;
};
}, [chapterReference, selectedTranslation]);
const verses = Array.isArray(chapterData?.verses) ? chapterData.verses : [];
const selectedVerseNumber = Number(selectedVerse || 1);
const selectedIndex = verses.findIndex((item) => Number(item?.verse || 0) === selectedVerseNumber);
React.useEffect(() => {
autoScrolledRef.current = false;
}, [chapterReference, selectedTranslation, selectedVerseNumber]);
const scrollToSelectedVerse = React.useCallback((animated = false) => {
if (autoScrolledRef.current) return;
if (!listRef.current || selectedIndex < 0 || !verses.length) return;
try {
listRef.current.scrollToIndex({
index: selectedIndex,
animated,
viewPosition: 0.35,
});
autoScrolledRef.current = true;
} catch (_error) {
// FlatList can throw before enough measurements are available.
}
}, [selectedIndex, verses.length]);
if (loading) {
return (
<SafeAreaView style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<ActivityIndicator />
</SafeAreaView>
);
}
if (error) {
return (
<SafeAreaView style={{ flex: 1, justifyContent: "center", alignItems: "center", padding: 16 }}>
<Text style={{ color: "#b91c1c" }}>{error}</Text>
</SafeAreaView>
);
}
const handleVersePress = (verseNumber) => {
if (!selectable) return;
GlobalState.bibleChapterSelection = {
...parseBibleReference(`${chapterReference}:${verseNumber}`),
ts: Date.now(),
};
navigation.goBack();
};
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1, padding: 12 }}>
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start" }}>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 22, fontWeight: "700", marginBottom: 4 }}>
{chapterData?.reference || chapterReference}
</Text>
<Menu
visible={translationMenuVisible}
onDismiss={() => setTranslationMenuVisible(false)}
anchor={
<Pressable onPress={() => setTranslationMenuVisible(true)}>
<Text style={{ color: "#2563eb", marginBottom: 10, fontWeight: "600", textDecorationLine: "underline" }}>
{chapterData?.translation_name || chapterData?.translation_id || "KJV"}
</Text>
</Pressable>
}
>
{AVAILABLE_TRANSLATIONS.map((trans) => (
<Menu.Item
key={trans.id}
onPress={() => {
setSelectedTranslation(trans.id);
setTranslationMenuVisible(false);
if (GlobalState.me) {
if (!GlobalState.me.data) GlobalState.me.data = {};
GlobalState.me.data.bibleTranslation = trans.id;
}
API.setDataValue("bibleTranslation", trans.id);
}}
title={trans.name}
/>
))}
</Menu>
</View>
{!selectable ? (
<IconButton
icon="menu-book"
size={28}
onPress={() => navigation.navigate("Bible")}
style={{ margin: 0, marginTop: -4 }}
/>
) : null}
</View>
{selectable ? (
<Text style={{ color: "#6b7280", marginBottom: 8 }}>{i18n.t("message.tapVerseToSelect")}</Text>
) : null}
<FlatList
ref={listRef}
data={verses}
keyExtractor={(item, idx) => `${item?.verse || idx}`}
initialNumToRender={24}
onLayout={() => {
setTimeout(() => scrollToSelectedVerse(false), 40);
}}
onContentSizeChange={() => {
setTimeout(() => scrollToSelectedVerse(false), 40);
}}
onScrollToIndexFailed={({ index, averageItemLength }) => {
if (!listRef.current) return;
listRef.current.scrollToOffset({
offset: Math.max(0, (averageItemLength || 36) * index),
animated: false,
});
setTimeout(() => {
scrollToSelectedVerse(false);
}, 120);
}}
ListFooterComponent={() => (
<View style={{ flexDirection: "row", justifyContent: "space-between", marginTop: 16, marginBottom: 24, paddingHorizontal: 4 }}>
<Button
mode="outlined"
disabled={!prevReference}
onPress={() => {
if (prevReference) {
navigation.replace("BibleChapter", { reference: prevReference, selectable });
}
}}
>
{i18n.t("message.previous") || "Previous"}
</Button>
<Button
mode="outlined"
disabled={!nextReference}
onPress={() => {
if (nextReference) {
navigation.replace("BibleChapter", { reference: nextReference, selectable });
}
}}
>
{i18n.t("message.next") || "Next"}
</Button>
</View>
)}
renderItem={({ item }) => {
const verseNumber = Number(item?.verse || 0);
const isSelected = verseNumber === selectedVerseNumber;
return (
<Pressable
onPress={() => handleVersePress(verseNumber)}
style={{
paddingVertical: 4,
paddingHorizontal: 8,
borderRadius: 8,
marginBottom: 2,
backgroundColor: isSelected ? "#fff3cd" : "transparent",
borderWidth: isSelected ? 1 : 0,
borderColor: isSelected ? "#f59e0b" : "transparent",
flexDirection: "row",
}}
>
<Text style={{ fontWeight: "700", color: isSelected ? "#92400e" : "#374151", marginRight: 8, marginTop: 2, fontSize: 12 }}>
{verseNumber}
</Text>
<Text style={{ color: "#111827", lineHeight: 22, flex: 1, fontSize: 16 }}>{(item?.text || "").replace(/\n/g, " ").trim()}</Text>
</Pressable>
);
}}
/>
</View>
</SafeAreaView>
);
};
export default BibleChapterView;