mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-03 16:07:32 +00:00
176 lines
6.6 KiB
TypeScript
176 lines
6.6 KiB
TypeScript
import React from 'react';
|
|
import { View, StyleSheet, ActivityIndicator } from 'react-native';
|
|
import type { Email, EmailAddress, Identity } from '../../api/types';
|
|
import { spacing, radius, type ThemePalette } from '../../theme/tokens';
|
|
import { useColors } from '../../theme/colors';
|
|
import { useSettingsStore } from '../../stores/settings-store';
|
|
import EmailBodyView from '../EmailBodyView';
|
|
import { CalendarInvitationBanner } from './CalendarInvitationBanner';
|
|
import { StructuredMailBanner } from './structured-mail/StructuredMailBanner';
|
|
import { MessageHeader } from './MessageHeader';
|
|
import { AttachmentChips } from './AttachmentChips';
|
|
import { UnsubscribeBanner } from './UnsubscribeBanner';
|
|
import { ReadReceiptBanner } from './ReadReceiptBanner';
|
|
import { useBodyOverride } from './use-body-override';
|
|
import { deriveHeaderInfo } from '../../lib/email-headers';
|
|
import { calendarBannerShownFor } from '../../lib/attachment-display';
|
|
|
|
export interface MessageContentProps {
|
|
email: Email;
|
|
jmapAccountId?: string;
|
|
identities: Identity[];
|
|
currentMailboxRole?: string | null;
|
|
/** On screen rather than pre-rendered by the pager (gates auto-sent read receipts). */
|
|
active: boolean;
|
|
themeOverride?: 'light' | 'dark' | null;
|
|
/** Per-message "Show as plain text"; null follows the "Plain Text Only" setting. */
|
|
plainView?: boolean | null;
|
|
/** Turn the per-message plain-text view off ("Show formatted"). */
|
|
onShowFormatted?: (email: Email) => void;
|
|
onSwipe?: (direction: 'prev' | 'next') => void;
|
|
onZoomChange?: (zoom: { pinching: boolean; zoomed: boolean }) => void;
|
|
onToggleStar?: (email: Email) => void;
|
|
onAddressPress: (address: EmailAddress) => void;
|
|
/** Reflect a local keyword change (e.g. `$mdnsent`) in the caller's cache. */
|
|
onEmailPatched: (email: Email) => void;
|
|
/** Compact header (thread cards). */
|
|
compact?: boolean;
|
|
/**
|
|
* Show a placeholder where the body goes: `email` is the list row, painted
|
|
* while the message itself loads, or the page is off screen and its
|
|
* WebView waits for the one on screen.
|
|
*/
|
|
deferBody?: boolean;
|
|
/** The body loaded and reported its height. */
|
|
onBodySettled?: () => void;
|
|
/**
|
|
* Stretch to the space the parent leaves (a page showing one message), so
|
|
* the body starts at the full page height instead of growing into it.
|
|
*/
|
|
fill?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Everything below the subject for one message: header block with details,
|
|
* receipt / unsubscribe banners, attachment chips, calendar invitation and
|
|
* the body. Used both by the single-message pane and by expanded thread
|
|
* cards.
|
|
*/
|
|
export function MessageContent({
|
|
email, jmapAccountId, identities, currentMailboxRole, active, themeOverride, plainView, onShowFormatted,
|
|
onSwipe, onZoomChange, onToggleStar, onAddressPress, onEmailPatched, compact, deferBody, onBodySettled, fill,
|
|
}: MessageContentProps) {
|
|
const c = useColors();
|
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
|
const attachmentPosition = useSettingsStore((s) => s.attachmentPosition);
|
|
const calendarParsing = useSettingsStore((s) => s.calendarInvitationParsingEnabled);
|
|
const headerInfo = React.useMemo(() => deriveHeaderInfo(email), [email]);
|
|
const unwrap = useBodyOverride(email, jmapAccountId);
|
|
const calendarBannerShown = calendarBannerShownFor(email, calendarParsing);
|
|
const from = email.from?.[0];
|
|
|
|
const chips = (
|
|
<AttachmentChips
|
|
email={email}
|
|
jmapAccountId={jmapAccountId}
|
|
calendarBannerShown={calendarBannerShown}
|
|
tnefUnpacked={unwrap.tnefUnpacked}
|
|
extracted={unwrap.extracted}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<View style={fill ? styles.fill : undefined}>
|
|
<View style={styles.headerBlock}>
|
|
<MessageHeader
|
|
email={email}
|
|
identities={identities}
|
|
headerInfo={headerInfo}
|
|
onToggleStar={onToggleStar}
|
|
onAddressPress={onAddressPress}
|
|
compact={compact}
|
|
jmapAccountId={jmapAccountId}
|
|
/>
|
|
{attachmentPosition === 'beside-sender' && chips}
|
|
</View>
|
|
{attachmentPosition === 'below-header' && <View style={styles.chipsBlock}>{chips}</View>}
|
|
|
|
{headerInfo.readReceiptRequestedBy && (
|
|
<ReadReceiptBanner
|
|
email={email}
|
|
requestedBy={headerInfo.readReceiptRequestedBy}
|
|
jmapAccountId={jmapAccountId}
|
|
currentMailboxRole={currentMailboxRole}
|
|
active={active}
|
|
onHandled={onEmailPatched}
|
|
/>
|
|
)}
|
|
{headerInfo.list.listUnsubscribe?.preferred && (
|
|
<UnsubscribeBanner
|
|
email={email}
|
|
list={headerInfo.list}
|
|
messageKey={headerInfo.messageId ?? email.id}
|
|
jmapAccountId={jmapAccountId}
|
|
/>
|
|
)}
|
|
|
|
<CalendarInvitationBanner email={email} jmapAccountId={jmapAccountId} />
|
|
<StructuredMailBanner email={email} jmapAccountId={jmapAccountId} />
|
|
|
|
<View style={[styles.body, fill && styles.fill]}>
|
|
{deferBody ? (
|
|
<BodyPlaceholder styles={styles} fill={fill} />
|
|
) : unwrap.loading ? (
|
|
<View style={styles.loading}><ActivityIndicator color={c.primary} /></View>
|
|
) : (
|
|
<EmailBodyView
|
|
email={email}
|
|
senderEmail={from?.email}
|
|
jmapAccountId={jmapAccountId}
|
|
onSwipe={onSwipe}
|
|
onZoomChange={onZoomChange}
|
|
themeOverride={themeOverride}
|
|
plainView={plainView}
|
|
onShowFormatted={onShowFormatted ? () => onShowFormatted(email) : undefined}
|
|
bodyOverride={unwrap.override}
|
|
onSettled={onBodySettled}
|
|
fill={fill}
|
|
/>
|
|
)}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const PLACEHOLDER_LINE_WIDTHS = ['92%', '100%', '85%', '96%', '60%', '88%', '74%', '40%'] as const;
|
|
|
|
// Static bones where the body will be: cheap enough for off-screen pages.
|
|
function BodyPlaceholder({ styles, fill }: { styles: ReturnType<typeof makeStyles>; fill?: boolean }) {
|
|
return (
|
|
<View style={[styles.placeholder, fill && styles.fill]}>
|
|
{PLACEHOLDER_LINE_WIDTHS.map((w, i) => (
|
|
<View key={i} style={[styles.placeholderLine, { width: w }]} />
|
|
))}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function makeStyles(c: ThemePalette) {
|
|
return StyleSheet.create({
|
|
headerBlock: {
|
|
backgroundColor: c.background,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: c.border,
|
|
},
|
|
chipsBlock: {
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: c.border,
|
|
},
|
|
body: { backgroundColor: c.background },
|
|
fill: { flexGrow: 1 },
|
|
loading: { padding: spacing.xl, alignItems: 'center' },
|
|
placeholder: { paddingHorizontal: spacing.lg, paddingVertical: spacing.lg, gap: spacing.sm },
|
|
placeholderLine: { height: 12, borderRadius: radius.xs, backgroundColor: c.surfaceHover },
|
|
});
|
|
}
|