Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useContext, useCallback } from 'react';

import { useRouter } from 'next/router';
import { VirtuosoHandle } from 'react-virtuoso';
Expand All @@ -8,6 +8,7 @@ import { MushafLines, QuranFont, QuranReaderDataType } from '@/types/QuranReader
import { getMushafId } from '@/utils/api';
import { makeVersesFilterUrl } from '@/utils/apiPaths';
import { getVerseNumberFromKey } from '@/utils/verse';
import { AudioPlayerMachineContext } from '@/xstate/AudioPlayerMachineContext';
import { fetcher } from 'src/api';
import { VersesResponse } from 'types/ApiResponses';
import LookupRecord from 'types/LookupRecord';
Expand Down Expand Up @@ -82,6 +83,9 @@ const useScrollToVirtualizedReadingView = (
const { startingVerse, chapterId } = router.query;
const shouldScroll = useRef(true);

// Ref to hold latest AbortController so we can cancel stale requests
const abortControllerRef = useRef<AbortController | null>(null);

/**
* We need to scroll again when we have just changed the font since the same
* verse might lie on another page/position. Same for when we change the
Expand All @@ -91,82 +95,126 @@ const useScrollToVirtualizedReadingView = (
shouldScroll.current = true;
}, [quranFont, mushafLines, startingVerse]);

useEffect(
/**
* Helper: scroll to a verse number. This consolidates the logic used in
* both the initial startingVerse effect and the audio player subscription.
*
* @param {number} verseNumber
* @param {boolean} useShouldScroll - when true, guard scrolling with shouldScroll ref
*/
const scrollToVerse = useCallback(
// eslint-disable-next-line react-func/max-lines-per-function
() => {
// if we have the data of the page lookup
if (!isPagesLookupLoading && virtuosoRef.current && Object.keys(pagesVersesRange).length) {
// if startingVerse is present in the url
if (quranReaderDataType === QuranReaderDataType.Chapter && startingVerse) {
const startingVerseNumber = Number(startingVerse);
// if the startingVerse is a valid integer and is above 1
if (Number.isInteger(startingVerseNumber) && startingVerseNumber > 0) {
const firstPageOfCurrentChapter = isUsingDefaultFont
? initialData.verses[0].pageNumber
: Number(Object.keys(pagesVersesRange)[0]);
// search for the verse number in the already fetched verses first
const startFromVerseData = verses.find(
(verse) => verse.verseNumber === startingVerseNumber,
);
if (
startFromVerseData &&
shouldScroll.current === true &&
pagesVersesRange[startFromVerseData.pageNumber]
) {
const scrollToPageIndex = startFromVerseData.pageNumber - firstPageOfCurrentChapter;
(verseNumber: number, useShouldScroll = false) => {
if (useShouldScroll && shouldScroll.current === false) return;
if (!virtuosoRef.current || !Object.keys(pagesVersesRange).length) return;

const firstPageOfCurrentChapter = isUsingDefaultFont
? initialData.verses[0].pageNumber
: Number(Object.keys(pagesVersesRange)[0]);

const startFromVerseData = verses.find((verse) => verse.verseNumber === verseNumber);

if (startFromVerseData && pagesVersesRange[startFromVerseData.pageNumber]) {
const scrollToPageIndex = startFromVerseData.pageNumber - firstPageOfCurrentChapter;
virtuosoRef.current.scrollToIndex({
index: scrollToPageIndex,
align: getVersePositionWithinAMushafPage(
`${chapterId}:${verseNumber}`,
startFromVerseData.pageNumber,
pagesVersesRange,
),
});
if (useShouldScroll) shouldScroll.current = false;
return;
}

// Abort previous request (if any) to prevent race conditions
if (abortControllerRef.current) abortControllerRef.current.abort();

const controller = new AbortController();
abortControllerRef.current = controller;

// fallback: fetch page number for the verse and scroll if possible
fetcher(
makeVersesFilterUrl({
filters: `${chapterId}:${verseNumber}`,
fields: `page_number`,
...getMushafId(quranReaderStyles.quranFont, quranReaderStyles.mushafLines),
}),
{ signal: controller.signal },
)
.then((response: VersesResponse) => {
if (response.verses.length && (useShouldScroll ? shouldScroll.current === true : true)) {
const page = response.verses[0].pageNumber;
const scrollToPageIndex = page - firstPageOfCurrentChapter;
if (pagesVersesRange[page]) {
virtuosoRef.current.scrollToIndex({
index: scrollToPageIndex,
align: getVersePositionWithinAMushafPage(
`${chapterId}:${startingVerseNumber}`,
startFromVerseData.pageNumber,
`${chapterId}:${verseNumber}`,
page,
pagesVersesRange,
),
});
shouldScroll.current = false;
} else {
// get the page number by the verse key and the mushafId (because the page will be different for Indopak Mushafs)
fetcher(
makeVersesFilterUrl({
filters: `${chapterId}:${startingVerseNumber}`,
fields: `page_number`,
...getMushafId(quranReaderStyles.quranFont, quranReaderStyles.mushafLines),
}),
).then((response: VersesResponse) => {
if (response.verses.length && shouldScroll.current === true) {
const scrollToPageIndex =
response.verses[0].pageNumber - firstPageOfCurrentChapter;
if (pagesVersesRange[response.verses[0].pageNumber]) {
virtuosoRef.current.scrollToIndex({
index: scrollToPageIndex,
align: getVersePositionWithinAMushafPage(
`${chapterId}:${startingVerseNumber}`,
response.verses[0].pageNumber,
pagesVersesRange,
),
});
shouldScroll.current = false;
}
}
});

if (useShouldScroll) shouldScroll.current = false;
}
}
}
}
})
.catch(() => {
// Ignore errors
});
},
[
chapterId,
initialData.verses,
isPagesLookupLoading,
isUsingDefaultFont,
virtuosoRef,
pagesVersesRange,
quranReaderDataType,
quranReaderStyles.mushafLines,
quranReaderStyles.quranFont,
startingVerse,
isUsingDefaultFont,
initialData,
verses,
virtuosoRef,
chapterId,
quranReaderStyles,
],
);

useEffect(() => {
// if we have the data of the page lookup
if (!isPagesLookupLoading && virtuosoRef.current && Object.keys(pagesVersesRange).length) {
// if startingVerse is present in the url
if (quranReaderDataType === QuranReaderDataType.Chapter && startingVerse) {
const startingVerseNumber = Number(startingVerse);
// if the startingVerse is a valid integer and is above 1
if (Number.isInteger(startingVerseNumber) && startingVerseNumber > 0) {
scrollToVerse(startingVerseNumber, true);
}
}
}
}, [
isPagesLookupLoading,
quranReaderDataType,
startingVerse,
virtuosoRef,
scrollToVerse,
pagesVersesRange,
]);

const audioService = useContext(AudioPlayerMachineContext);

// Subscribe to NEXT_AYAH and PREV_AYAH events to scroll when user clicks buttons in audio player
useEffect(() => {
if (!audioService || quranReaderDataType !== QuranReaderDataType.Chapter) return undefined;

const subscription = audioService.subscribe((state) => {
if (state.event.type === 'NEXT_AYAH' || state.event.type === 'PREV_AYAH') {
const { ayahNumber } = state.context;
if (!Number.isInteger(ayahNumber) || ayahNumber <= 0) return;
scrollToVerse(ayahNumber, false);
}
});

return () => {
subscription.unsubscribe();
};
}, [audioService, scrollToVerse, quranReaderDataType]);
};

export default useScrollToVirtualizedReadingView;
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Verse from '@/types/Verse';
import { getPageNumberFromIndexAndPerPage } from '@/utils/number';
import { isValidVerseId } from '@/utils/validator';
import { makeVerseKey } from '@/utils/verse';
import { AudioPlayerMachineContext } from '@/xstate/AudioPlayerMachineContext';
import ScrollAlign from 'types/ScrollAlign';

/**
Expand Down Expand Up @@ -38,6 +39,8 @@ const useScrollToVirtualizedTranslationView = (
const timeoutId = useRef<ReturnType<typeof setTimeout>>(null);
const { verseKeysQueue, shouldTrackObservedVerses } = useVerseTrackerContext();

const audioService = useContext(AudioPlayerMachineContext);

const { startingVerse } = router.query;
const startingVerseNumber = Number(startingVerse);
const isValidStartingVerse =
Expand Down Expand Up @@ -125,6 +128,21 @@ const useScrollToVirtualizedTranslationView = (
chapterId,
]);

// Subscribe to NEXT_AYAH and PREV_AYAH events to scroll when user clicks buttons
useEffect(() => {
if (!audioService || quranReaderDataType !== QuranReaderDataType.Chapter) return undefined;

const subscription = audioService.subscribe((state) => {
if (state.event.type === 'NEXT_AYAH' || state.event.type === 'PREV_AYAH') {
scrollToBeginningOfVerseCell(state.context.ayahNumber);
}
});

return () => {
subscription.unsubscribe();
};
}, [audioService, scrollToBeginningOfVerseCell, quranReaderDataType]);

// this effect clears the timeout when the component unmounts
useEffect(() => {
return () => {
Expand Down