import React from 'react'; import { View, StyleSheet, Linking, Platform, Text, Pressable, useWindowDimensions } from 'react-native'; import { WebView, type WebViewMessageEvent } from 'react-native-webview'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { Image as ImageIcon, ShieldCheck } from 'lucide-react-native'; import type { Email } from '../api/types'; import { extractCidRefs } from '../lib/email-html'; import { hasTruncatedDisplayedBody, pickEmailBody, selectRenderableHtml } from '../lib/email-body'; import { buildQuoteCollapseScript } from '../lib/quote-collapse'; import { bodyDocument } from '../lib/email-body-document'; import { estimateBodyHeight, lastBodyHeight, rememberBodyHeight } from '../lib/body-heights'; import { parseMailtoUrl } from '../lib/unsubscribe'; import { fetchInlineImageDataUri } from '../lib/email-export'; import { isSenderContentTrusted, isTrustedSendersSyncOn } from '../lib/trusted-senders'; import { useHasContacts } from '../lib/capabilities'; import { useSettingsStore } from '../stores/settings-store'; import { useContactsStore } from '../stores/contacts-store'; import { useLocaleStore } from '../stores/locale-store'; import { spacing, typography, type ThemePalette } from '../theme/tokens'; import { useColors, useResolvedTheme } from '../theme/colors'; import type { RootStackParamList } from '../navigation/types'; type Nav = NativeStackNavigationProp; interface EmailBodyViewProps { email: Email; senderEmail?: string; /** Owning account when the message lives in a shared/group mailbox. */ jmapAccountId?: string; // The body is a native WebView, which on Android swallows horizontal touches // before the surrounding pager can see them. We detect a clear horizontal // swipe inside the page and report it here so the pager can change messages. onSwipe?: (direction: 'prev' | 'next') => void; // Pinch-zoom state from inside the page: `pinching` while two fingers are // down, `zoomed` while the content is scaled in. The hosts use it to freeze // the surrounding vertical scroll / horizontal pager so a zoom gesture can't // scroll the pane away or switch mails. onZoomChange?: (zoom: { pinching: boolean; zoomed: boolean }) => void; /** Per-message override of the light/dark rendering (More sheet toggle). */ themeOverride?: 'light' | 'dark' | null; /** * Replace the message's own body - used when the outer message is an empty * envelope around a message/rfc822 attachment or a TNEF winmail.dat and the * real content was extracted client-side. */ bodyOverride?: { html?: string | null; text?: string | null } | null; /** Called once the body has loaded and reported its height (it is on screen). */ onSettled?: () => void; /** * Until its height is known, take the space the parent leaves (the whole * page below the header) rather than a guessed height. */ fill?: boolean; } // Dark-mode re-inversion, mirroring the webmail's handleIframeLoad pass. The // body's `filter: invert(1)` flips colored emoji (yellow smiley -> blue, red // heart -> cyan) and stylesheet-defined background images, which the static CSS // attribute selectors can't reach. This walks the DOM once and wraps/re-inverts // those so they keep their original colors. Only injected when inversion is on. const DARK_REINVERT_SCRIPT = ` (function () { if (document.__rnDarkInvertDone) return; var doc = document; var win = window; if (!doc.body) return; document.__rnDarkInvertDone = true; // Re-invert elements with stylesheet-defined background images (CSS attribute // selectors only catch inline styles, not rules that survive in