feat(rsvp): split words option, faster countdown, and skip pages RSVP cant open (#3755)

* fix: use string-parsed year to avoid UTC/timezone boundary issues

* feat: Add option to split words in RSVP mode

* fix: change the checkbox to a toggle

* fix(rsvp): speed up countdown from 2.4s to 1.5s

* feat(rsvp): skip to next page when no readable text is found

* fix(rsvp): replace lookbehind regex with lookahead-only split in getHyphenParts
This commit is contained in:
Lex Moulton
2026-04-05 05:29:30 -07:00
committed by GitHub
parent d682fcbb44
commit d53f3b42e2
6 changed files with 141 additions and 33 deletions
@@ -56,7 +56,7 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
const _ = useTranslation();
const { themeCode, isDarkMode: _isDarkMode } = useThemeStore();
const [state, setState] = useState<RsvpState>(controller.currentState);
const currentWord = state.words[state.currentIndex] ?? null;
const currentWord = controller.currentDisplayWord;
const [countdown, setCountdown] = useState<number | null>(controller.currentCountdown);
const [showChapterDropdown, setShowChapterDropdown] = useState(false);
const chapterDropdownRef = useRef<HTMLDivElement>(null);
@@ -93,15 +93,10 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
}
return 0;
});
// Context window: only rebuild the rendered word list when the current word
// falls outside the window or nears its edge, keeping the DOM stable.
const [contextWindow, setContextWindow] = useState(() => {
if (!state || state.words.length === 0) return { start: 0, end: 0 };
return {
start: Math.max(0, state.currentIndex - 50),
end: Math.min(state.words.length, state.currentIndex + 151),
};
});
const [contextWindow, setContextWindow] = useState(() => ({
start: 0,
end: state.words.length,
}));
const contextWordRef = useRef<HTMLSpanElement>(null);
const contextPanelRef = useRef<HTMLDivElement>(null);
const touchStartX = useRef(0);
@@ -134,25 +129,11 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
const newState = (e as CustomEvent<RsvpState>).detail;
setState(newState);
// Update context window only when current word falls outside or nears edge
const idx = newState.currentIndex;
// Reset context window to show all words when the chapter changes
const total = newState.words.length;
setContextWindow((prev) => {
if (total === 0) return { start: 0, end: 0 };
// Outside window — reset
if (idx < prev.start || idx >= prev.end) {
return {
start: Math.max(0, idx - 50),
end: Math.min(total, idx + 151),
};
}
// Near end of window — extend forward
const windowSize = prev.end - prev.start;
if (idx - prev.start > windowSize * 0.8) {
return { start: prev.start, end: Math.min(total, prev.end + 100) };
}
// No change — return same reference to avoid re-render
return prev;
if (total === prev.end && prev.start === 0) return prev;
return { start: 0, end: total };
});
};
@@ -851,6 +832,17 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
</button>
</div>
{/* Split hyphenated words */}
<div className='config-item gap-2'>
<span className='opacity-50'>{_('Split Hyphens')}</span>
<input
type='checkbox'
className='toggle'
checked={state.splitHyphens}
onChange={(e) => controller.setSplitHyphens(e.target.checked)}
/>
</div>
{/* ORP color */}
<div className='flex items-center gap-1.5'>
<span className='mr-0.5 font-medium opacity-50'>{_('Focus')}</span>