I have yet to see any problem, however complicated, which, when you looked at it in the right way, did not become still more complicated, Poul Anderson.

We’re building the prayer page viewer – the central component that displays one page of a prayer at a time, with full HTML formatting, optional images, and page navigation.
The prayer page content is stored as HTML strings like:
<h2>Presenceh2>
<p>What I pay attention to has a hold on my becoming.p>
<p>I reflect on the presence of God in this moment…p>
React Native’s <Text> component only renders plain text — it ignores HTML tags. There are three ways to display HTML in React Native:
| Approach | Pros | Cons |
|---|---|---|
Strip tags with .replace(/<[^>]+>/g, "") |
No dependency | Loses all formatting |
WebView with html content |
Full browser rendering | Heavy, slow, overkill for text |
react-native-render-html |
Native rendering, fast, lightweight | Requires install |
react-native-render-html (RRH) converts HTML to native React Native components. <h2> becomes a styled <Text>, <p> becomes a <Text>, <strong> becomes bold <Text>. It is the standard solution.
react-native-render-htmlnpx expo install react-native-render-html
Using npx expo install ensures you get a version compatible with your current Expo SDK.
src/components/PrayerPageViewer.tsxWe extract the page viewer into its own component so prayers.tsx stays readable. This component receives everything it needs as props.
// src/components/PrayerPageViewer.tsx
//
// PRAYER PAGE VIEWER
// ───────────────────
// Renders one page of a prayer with:
// - HTML content via react-native-render-html
// - Optional page image (base64 URI)
// - Page indicator ("Page 2 of 5")
// - Prev / Next navigation buttons
//
// USED BY: app/(tabs)/prayers.tsx
//
import React from "react";
import {
View,
Text,
Image,
TouchableOpacity,
ScrollView,
StyleSheet,
useWindowDimensions,
} from "react-native";
import RenderHtml from "react-native-render-html";
import type { Page } from "../db/types";
// ── Colour constants ──────────────────────────────────────────────────────────
// These are copied directly from the app’s theme. \
// Repeating them here keeps the component independent for now.
const C = {
bg: "#1a1f2e",
panel: "#222840",
border: "#2d3550",
accent: "#5b8fd4",
text: "#dde4f0",
muted: "#9ca3af",
faint: "#6b7280",
gold: "#c9a84c",
dim: "#4b5563",
};
// ── Props ─────────────────────────────────────────────────────────────────────
interface PrayerPageViewerProps {
/** The current Page object from the database, containing content (HTML) and optionally image (base64).
* null when there are no pages at all. */
page: Page | null;
/** 0-based index of the current page */
currentPage: number;
/** Total pages in this prayer */
totalPages: number;
/** true if on the first page (disables Prev button) */
isFirstPage: boolean;
/** true if on the last page (disables Next button) */
isLastPage: boolean;
/** Called when the user taps ◀ Prev.
* The parent (the PrayersScreen) updates its state, and the component becomes a controlled component. */
onPrev: () => void;
/** Called when the user taps Next ▶ */
onNext: () => void;
/** Called when the user taps a page dot */
onGoToPage: (index: number) => void;
}
// ── Helpers ───────────────────────────────────────────────────────────────────
/**
* Convert a raw base64 string to a data URI suitable for <Image source>.
*
* The database stores images as plain base64 strings (no prefix).
* React Native's <Image source> requires a full data URI format:
* "data:image/jpeg;base64,<base64data>"
*
* We detect the prefix and add it if missing.
* We default to image/jpeg because that is the most common format.
*/
function toImageUri(base64: string): string {
if (base64.startsWith("data:")) {
return base64; // already a valid data URI
}
return `data:image/jpeg;base64,${base64}`;
}
// ── HTML render styles ────────────────────────────────────────────────────────
// react-native-render-html uses "tagsStyles" to style HTML elements.
// These replace what CSS would do in a browser and control the appearance of every HTML tag.
// They are passed to RenderHtml, e.g., h2 – gold, bold, larger font;
// p – regular light‑coloured body text with spacing; etc.
const HTML_STYLES = {
h2: {
color: C.gold,
fontSize: 20,
fontWeight: "700" as const,
marginBottom: 8,
marginTop: 4,
},
h3: {
color: C.accent,
fontSize: 17,
fontWeight: "600" as const,
marginBottom: 6,
},
p: {
color: C.text,
fontSize: 15,
lineHeight: 24,
marginBottom: 12,
},
strong: {
color: C.gold,
fontWeight: "700" as const,
},
em: {
color: C.muted,
fontStyle: "italic" as const,
},
blockquote: {
borderLeftWidth: 3,
borderLeftColor: C.gold,
paddingLeft: 14,
marginLeft: 0,
color: C.muted,
},
};
// ── Component ─────────────────────────────────────────────────────────────────
/* The PrayerPageViewer component is the core reader for a single prayer’s pages.
It replaces the placeholder from the previous article with a working paginated view,
using react-native-render-html to display the stored HTML content.
The component does not own page‑navigation logic; it simply displays what it’s given and invokes callbacks.
It handles: */
export function PrayerPageViewer({
page, // the current page object (or undefined if none)
currentPage, // zero‑based index of the active page
totalPages, // total number of pages for this prayer
isFirstPage, // true if on the very first page
isLastPage, // true if on the very last page
onPrev, // go to previous page
onNext, // go to next page
onGoToPage, // jump to a specific page by index
}: PrayerPageViewerProps) {
// useWindowDimensions is a React Native hook that returns the current screen width and height,
// and automatically updates on rotation or window resize.
// react-native-render-html REQUIRES the contentWidth prop to know
// how wide to render HTML content. Without it, text might render at an incorrect width and overflow.
const { width } = useWindowDimensions();
// The content area is the screen width minus the sidebar (200px) that exists in the Prayers tab layout
// minus padding on both sides (20px × 2).
// This gives the exact available width for the HTML content.
const contentWidth = width - 200 - 40;
// ── No pages case ──────────────────────────────────────────────────────────
// Empty State. Gracefully handles the case where a prayer has zero pages
// – e.g., right after creation or during development.
// Why !page || totalPages === 0? page could be undefined if the pages array is empty, or if there’s a timing issue.
// totalPages === 0 explicitly catches a prayer with zero pages.
// The empty state is rendered as a centred, low‑opacity group (opacity: 0.6),
// which doesn’t distract but clearly communicates the situation.
if (!page || totalPages === 0) {
return (
<View style={s.emptyPages}>
<Text style={s.emptyIcon}>📄</Text>
<Text style={s.emptyText}>This prayer has no pages yet.</Text>
<Text style={s.emptySubtext}>
Add pages using the database seed or the Add Prayer form.
</Text>
</View>
);
}
/* Main Layout Structure. container has flex: 1 to fill the remaining space in the right panel. */
return (
<View style={s.container}>
{/* ── Page indicator ──────────────────────────────────────────────────── */}
<View style={s.indicatorRow}>
<Text style={s.pageIndicator}>
Page {currentPage + 1} of {totalPages}
</Text>
{/* Page dots — tap any to jump directly to that page */}
/* Each dot is a View inside a TouchableOpacity */
<View style={s.dots}>
{Array.from({ length: totalPages }).map((_, i) => (
<TouchableOpacity
key={i}
onPress={() => onGoToPage(i)}
hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
// hitSlop enlarges the tap area without changing visual size —
// important for small dots that are hard to tap precisely
>
<View style={[s.dot, i === currentPage && s.dotActive]} />
</TouchableOpacity>
))}
</View>
</View>
{/* ── Scrollable Content Area ────────────────────────────────────────── */}
<ScrollView
style={s.scroll}
contentContainerStyle={s.scrollContent}
showsVerticalScrollIndicator={false}
// When the key prop changes (we use the page id as the key — when the key changes),
// React unmounts and remounts the ScrollView. This resets its internal scroll position to the very top.
key={page.id}
>
{/* HTML content from the database */}
{/*
RenderHtml props:
contentWidth — required: mandatory for the library to know the available width for text wrapping.
source — the HTML string wrapped in { html: "..." }
tagsStyles — a separate style object that maps HTML tags to React Native styles,
giving proper formatting to headings, paragraphs, lists, etc.
baseStyle — sets a default text colour (C.text) and font size (15) for all text.
WHY source={{ html: content }} and not source={{ uri: ... }}?
We have the HTML string already in memory (from SQLite).
The `uri` variant would fetch from a remote URL, which we don’t.
The component imports a constant HTML_STYLES, this object maps HTML tags to React Native styles.
*/}
<RenderHtml
contentWidth={contentWidth}
source={{ html: page.content || "<p>No content.</p>" }}
tagsStyles={HTML_STYLES}
baseStyle={{
color: C.text,
fontSize: 15,
}}
// Disable the default font scaling — we control font sizes explicitly
enableExperimentalMarginCollapsing={true}
/>
{/* Page image (optional). The image is shown below the HTML content.
The container has a border, rounded corners, and overflow: "hidden" to clip the image nicely. */}
{page.image ? (
<View style={s.imageContainer}>
<Image
/* toImageUri (imported from a utility) converts a file path to a URI that React Native’s
Image component can display (using file:// for local files). */
source={{ uri: toImageUri(page.image) }}
style={s.pageImage}
/* It ensures the whole image fits without cropping. */
resizeMode="contain"
// accessibilityLabel helps screen readers describe the image
accessibilityLabel="Prayer page image"
/>
</View>
) : null}
</ScrollView>
{/* ── Navigation buttons ───────────────────────────────────────────────── */}
<View style={s.navRow}>
{/* ◀ Prev button */}
<TouchableOpacity
/* Use the accent colour (C.accent) when active,
and a disabled style (backgroundColor: C.border) when at the boundary.*/
style={[s.navBtn, isFirstPage && s.navBtnDisabled]}
onPress={onPrev}
disabled={isFirstPage}
// accessibilityLabel: read aloud by screen readers
accessibilityLabel="Previous page"
// it tells the accessibility system that this is a button.
accessibilityRole="button"
// accessibilityState.disabled: tells screen readers the button is inactive
accessibilityState={{ disabled: isFirstPage }}
activeOpacity={0.75}
>
<Text style={[s.navBtnText, isFirstPage && s.navBtnTextDisabled]}>
◀ Prev
</Text>
</TouchableOpacity>
{/* Centre: a simple fraction showing the current page number and total pages.
Uses C.faint colour and a small font size to be unobtrusive. */}
<Text style={s.navFraction}>
{currentPage + 1} / {totalPages}
</Text>
{/* Next ▶ button */}
<TouchableOpacity
style={[s.navBtn, isLastPage && s.navBtnDisabled]}
onPress={onNext}
disabled={isLastPage}
accessibilityLabel="Next page"
accessibilityRole="button"
accessibilityState={{ disabled: isLastPage }}
activeOpacity={0.75}
>
<Text style={[s.navBtnText, isLastPage && s.navBtnTextDisabled]}>
Next ▶
</Text>
</TouchableOpacity>
</View>
</View>
);
}
// ── Styles ────────────────────────────────────────────────────────────────────
/* The styles object s is created with StyleSheet.create and references a shared colour constant C. */
const s = StyleSheet.create({
container: {
flex: 1,
// Fills the parent (flex: 1), no extra padding.
},
// ── Empty state ───────────────────────────────────────────────────────────────
// Centered, low‑opacity block for the “no pages” state.
emptyPages: {
flex: 1,
alignItems: "center",
justifyContent: "center",
gap: 10,
opacity: 0.6,
},
// Emoji + messages in the empty state.
emptyIcon: { fontSize: 36 },
emptyText: { fontSize: 14, color: C.muted, textAlign: "center" },
emptySubtext: { fontSize: 12, color: C.faint, textAlign: "center",
lineHeight: 18 },
// ── Page indicator ─────────────────────────────────────────────────────────────
// Horizontal row for page label and dots, with a bottom border.
indicatorRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 14,
paddingBottom: 12,
borderBottomWidth: 1,
borderBottomColor: C.border,
},
// Uppercase, blue, bold label (“PAGE 2 OF 5”).
pageIndicator: {
fontSize: 12,
color: C.accent,
fontWeight: "700",
textTransform: "uppercase",
letterSpacing: 0.8,
},
// ── Page dots ──────────────────────────────────────────────────────────────────
// Row of touchable dots, with gap.
dots: {
flexDirection: "row",
gap: 7,
alignItems: "center",
},
// Small circles; active one is larger and gold.
dot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: C.border,
},
dotActive: {
backgroundColor: C.gold,
width: 10,
height: 10,
borderRadius: 5,
},
// ── Scroll area ────────────────────────────────────────────────────────────────
// Flex‑1 container for the page content.
scroll: {
flex: 1,
},
// Bottom padding so the last line isn’t flush with the navigation bar.
scrollContent: {
paddingBottom: 16,
},
// ── Page image ─────────────────────────────────────────────────────────────────
// Card for the optional page image, with border and corner clipping.
imageContainer: {
marginTop: 16,
borderRadius: 10,
overflow: "hidden", // clips the image to the border radius
borderWidth: 1,
borderColor: C.border,
},
// Full‑width, fixed height (200), contain‑resized image.
pageImage: {
width: "100%",
height: 200,
},
// ── Navigation bar ─────────────────────────────────────────────────────────────
// Navigation footer with top border, space‑between layout.
navRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingTop: 14,
marginTop: 8,
borderTopWidth: 1,
borderTopColor: C.border,
gap: 8,
},
// Blue button; disabled version uses border colour.
navBtn: {
backgroundColor: C.accent,
borderRadius: 8,
paddingVertical: 10,
paddingHorizontal: 18,
minWidth: 90,
alignItems: "center",
},
navBtnDisabled: {
backgroundColor: C.border,
},
// White text; disabled version is faint.
navBtnText: {
color: "#ffffff",
fontSize: 14,
fontWeight: "600",
},
navBtnTextDisabled: {
color: C.faint,
},
// Centered fraction text, flexible width.
navFraction: {
flex: 1,
textAlign: "center",
color: C.faint,
fontSize: 13,
},
});
src/components/PrayerCoverImage.tsxPrayerCoverImage is a reusable presentational component that displays a prayer’s cover image stored as a Base64-encoded string in the SQLite database or nothing if absent. It is designed to be dropped into the prayer detail panel without needing the parent to worry about converting the data to a URI or handling missing images.
Why a separate component?
<Image> can display; that logic lives here.// src/components/PrayerCoverImage.tsx
//
// Renders a prayer's cover image from a base64 string.
// Shows nothing (returns null) if no image is stored.
import React from "react";
import { Image, View, StyleSheet } from "react-native";
interface PrayerCoverImageProps {
/** The raw Base64-encoded image data, exactly as stored in the database.
* If null or falsy, the component renders nothing.
* Why base64 can be null? Many prayers won’t have a cover image. */
base64: string | null;
/** The height of the image container in logical pixels. (default 140).
* The width is always "100%" of the parent. */
height?: number;
}
/**
* Convert raw base64 to a data URI for <Image>.
* Adds "data:image/jpeg;base64," prefix if missing.
*/
function toDataUri(base64: string): string {
/* If the string already starts with data:, it is already a valid data URI. We pass it through unchanged.
Otherwise, we prepend data:image/jpeg;base64, treating the content as JPEG. */
return base64.startsWith("data:")
? base64
: `data:image/jpeg;base64,${base64}`;
}
/* Component Logic */
export function PrayerCoverImage({ base64, height = 140 }: PrayerCoverImageProps) {
/* If base64 is null, undefined, or an empty string, the component renders nothing (null).
This is the common case, and it avoids wasting layout space. */
if (!base64) return null;
return (
/* The Container View is not just decorative: It has a fixed height (default 140,
overridable via the height prop). This height is defined on the container so that
the layout never collapses even if the image hasn’t loaded yet.
borderRadius: 10 and overflow: "hidden" ensure the inner <Image> is clipped to rounded corners.
borderWidth: 1 with borderColor: "#2d3550" (the app’s subtle border colour) gives the image a delicate frame,
marginBottom: 14 gives spacing between the image and whatever follows. */
<View style={[s.container, { height }]}>
<Image
/* It converts the raw string to a proper data URI and passes it to the native image loader. */
source={{ uri: toDataUri(base64) }}
/* style={s.image} – width: "100%" and height: "100%" make the image fill the container completely. */
style={s.image}
/* It scales the image to cover the entire container while maintaining aspect ratio. */
resizeMode="cover"
/* It provides a descriptive label for screen readers. */
accessibilityLabel="Prayer cover image"
/>
</View>
);
}
const s = StyleSheet.create({
container: {
width: "100%",
borderRadius: 10,
overflow: "hidden",
marginBottom: 14,
borderWidth: 1,
borderColor: "#2d3550",
},
image: {
width: "100%",
height: "100%",
},
});
app/(tabs)/prayers.tsxBy swapping the old placeholder-laden PrayerDetail with this new version, we integrate the real cover image renderer and the multi‑page viewer into the two‑column layout.
The component remains purely presentational — it receives everything from the prayers hook.
Replace the PrayerDetail function and add the import. Only two changes:
1. Add to the imports at the top of prayers.tsx:
// We bring in PrayerPageViewer and PrayerCoverImage, the two components we just built.
import { PrayerPageViewer } from "../../src/components/PrayerPageViewer";
import { PrayerCoverImage } from "../../src/components/PrayerCoverImage";
2. Replace the entire PrayerDetail function with this version.
function PrayerDetail({ prayers }: { prayers: ReturnType<typeof usePrayers> }) {
/* Unchanged: Empty State.
This part is identical to previous posts. When no prayer is loaded, the empty state is shown. */
if (!prayers.prayer) {
return (
<View style={s.main}>
<EmptyState
icon="✝"
title="No prayer selected"
subtitle={
prayers.prayerCount > 0
? "Tap a prayer in the list to open it."
: "Tap + to add your first prayer."
}
/>
</View>
);
}
const { prayer } = prayers;
return (
// The main panel uses flex: 1, column direction (default).
// The header has a fixed height, the page viewer takes the rest.
<View style={s.main}>
{/* ── Cover image ───────────────────────────────────────────────────────
No conditional rendering — PrayerCoverImage itself returns null if base64 is falsy.
The parent doesn’t need an {prayer.image ? ...} check.
height={120} matches the original placeholder height.
The image now actually displays the prayer’s cover, if present. */}
<PrayerCoverImage base64={prayer.image} height={120} />
{/* ── Prayer header ────────────────────────────────────────────────────── */}
<View style={s.detailHeader}>
{/* Title + date row.
detailTitleRow. Layout: flexDirection: "row", justifyContent: "space-between".
The title is on the left, the date + badge on the right. */}
<View style={s.detailTitleRow}>
<Text style={s.detailTitle} numberOfLines={2}>
{prayer.title}
</Text>
<View style={s.detailDateCol}>
<Text style={s.detailDate}>{prayer.date || "—"}</Text>
{isToday(prayer.date) && (
<View style={s.todayBadge}>
<Text style={s.todayBadgeText}>Today</Text>
</View>
)}
</View>
</View>
{/* Description. numberOfLines={3} clips long descriptions. */}
{prayer.description ? (
<Text style={s.detailDescription} numberOfLines={3}>
{prayer.description}
</Text>
) : null}
</View>
{/* ── Horizontal divider ──────────────────────────────────────────────────
A thin horizontal line that separates the header from the page content. */}
<View style={s.detailDivider} />
{/* ── Page viewer ──────────────────────────────────────────────────────── */}
{/*
flex: 1 makes PrayerPageViewer take ALL remaining space in the main panel.
This means the scroll area inside it fills the screen correctly.
All page‑navigation logic remains in the usePrayers hook. PrayerDetail merely pipes the data and callbacks through.
*/}
<View style={s.pageViewerContainer}>
<PrayerPageViewer
page={prayers.pageContent} // The current page object with id, content (HTML), image, etc.
currentPage={prayers.currentPage} // Zero‑based index of the active page.
totalPages={prayers.totalPages} // Total number of pages in this prayer.
isFirstPage={prayers.isFirstPage} // true if on page 0.
isLastPage={prayers.isLastPage} // true if on the last page.
onPrev={prayers.goPrev} // Callback to navigate to previous page.
onNext={prayers.goNext} // Callback to navigate to next page.
onGoToPage={prayers.goToPage} // Callback to jump to a specific page by index.
/>
</View>
</View>
);
}
The following style additions should be placed after the existing detailDescription style in the StyleSheet.create block
// Add to the styles object in prayers.tsx:
detailTitleRow: {
flexDirection: "row",
alignItems: "flex-start",
justifyContent: "space-between",
marginBottom: 6,
gap: 10,
},
detailTitle: {
flex: 1,
fontSize: 20,
fontWeight: "bold",
color: "#c9a84c",
lineHeight: 26,
},
detailDateCol: {
alignItems: "flex-end",
gap: 4,
flexShrink: 0,
},
detailDate: {
fontSize: 11,
color: "#6b7280",
fontFamily: "monospace",
},
todayBadge: {
backgroundColor: "#2a2010",
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
borderWidth: 1,
borderColor: "#c9a84c",
},
todayBadgeText: {
fontSize: 10,
color: "#c9a84c",
fontWeight: "600",
},
detailDescription: {
fontSize: 13,
color: "#9ca3af",
lineHeight: 20,
marginBottom: 12,
},
detailDivider: {
height: 1,
backgroundColor: "#2d3550",
marginBottom: 14,
},
pageViewerContainer: {
flex: 1, // takes all remaining vertical space — critical for scrolling
},
The root container is a row (flexDirection: “row”):
┌────────────────┬──────────────────────────────────────────────┐
│ │ │
│ SIDEBAR │ MAIN PANEL │
│ (200px) │ (flex: 1) │
│ │ │
│ ┌──────────┐ │ ┌──────────────────────────────────────────┐│
│ │ Prayer │ │ │ Cover Image (120px height, or hidden) ││
│ │ List │ │ ├──────────────────────────────────────────┤│
│ │ (FlatList│ │ │ Title + Date (auto height) ││
│ │ ) │ │ ├──────────────────────────────────────────┤│
│ │ │ │ │ Description (auto height) ││
│ │ │ │ ├──────────────────────────────────────────┤│
│ │ │ │ │────────── divider (1px) ─────────────────││
│ │ │ │ ├──────────────────────────────────────────┤│
│ │ │ │ │ ││
│ │ │ │ │ PAGE VIEWER ││
│ │ │ │ │ (flex: 1 – fills all remaining space) ││
│ │ │ │ │ ScrollView with HTML content, ││
│ │ │ │ │ page dots, nav buttons ││
│ │ │ │ │ ││
│ └──────────┘ │ └──────────────────────────────────────────┘│
└────────────────┴──────────────────────────────────────────────┘
The right column is a column (default flexDirection: “column”) with the following children, from top to bottom:
┌──────────────────────────────────────────┐
│ COVER IMAGE (height: 120 or absent) │ ← fixed height
├──────────────────────────────────────────┤
│ HEADER │
│ ┌────────────────────────┬────────────┐ │
│ │ Title (flex:1) │ Date + │ │ ← auto height
│ │ │ Today Badge│ │
│ └────────────────────────┴────────────┘ │
│ Description (optional, auto height) │
├──────────────────────────────────────────┤
│ DIVIDER (1px, margin bottom) │ ← fixed height
├──────────────────────────────────────────┤
│ │
│ PAGE VIEWER │
│ (flex: 1) │ ← takes ALL remaining
│ ┌────────────────────────────────────┐ │ vertical space
│ │ Page indicator + dots │ │
│ │ ────────────────────────────────── │ │
│ │ │ │
│ │ ScrollView with HTML content │ │
│ │ (scrolls independently) │ │
│ │ │ │
│ ├────────────────────────────────────┤ │
│ │ ◀ Prev 1 / 5 Next ▶ │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
The crucial detail is the pageViewerContainer with flex: 1. Because the main panel is a column with a finite height (determined by the root’s flex: 1 and the screen height), flex: 1 causes that container to expand to consume any space not used by the cover image, header, and divider.
Run npx expo start --clear, navigate to the ✝ Prayers tab, and check.
| Action | Expected |
|---|---|
| Open Prayers tab | Ignatian Morning Prayer, Page 1 of 5 |
| Page 1 content | “Presence” in gold heading, paragraph below |
| ◀ Prev button | Greyed out, disabled |
| Tap Next ▶ | Page 2 of 5, “Freedom” heading appears |
| Tap Next ▶ × 3 | Page 5 of 5, “Review” heading |
| Next ▶ | Greyed out, disabled |
| Tap dot 1 | Jumps to Page 1 |
| Tap “Evening Examen” | Switches prayer, resets to Page 1 of 4 |