import React from 'react'; import { View, Text, StyleSheet, Pressable } from 'react-native'; import { Paperclip, Reply, ReplyAll, Forward, Star } from '../icons'; import type { Email } from '../../api/types'; import { spacing, radius, typography, componentSizes, type ThemePalette } from '../../theme/tokens'; import { useColors } from '../../theme/colors'; import { useSettingsStore } from '../../stores/settings-store'; import { useLocaleStore } from '../../stores/locale-store'; import SenderAvatar from '../SenderAvatar'; import { MessageContent, type MessageContentProps } from './MessageContent'; import { emailDisplayDate, formatHeaderDate, formatHeaderTime } from '../../lib/email-date'; import { singleLine } from '../../lib/single-line'; interface Props extends MessageContentProps { expanded: boolean; onToggleExpanded: () => void; onReply: (mode: 'reply' | 'replyAll' | 'forward', email: Email) => void; /** * The message is not part of a conversation: just its content, without the * card's collapse handle, reply buttons and header star. Rendered through * the card so the content survives a conversation arriving around it. */ bare?: boolean; /** The full message is here (not only its header), so it can be replied to. */ replyable?: boolean; } /** * One message of a conversation: a collapsed summary row (sender, date, * preview) that expands into the full message with its own reply / forward * actions - the webmail's thread-conversation-view cards. */ export function ThreadMessageCard({ expanded, onToggleExpanded, onReply, bare, replyable = true, ...content }: Props) { const { email, onToggleStar } = content; const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); const t = useLocaleStore((s) => s.t); const locale = useLocaleStore((s) => s.locale); const timeFormat = useSettingsStore((s) => s.timeFormat); const from = email.from?.[0]; const unread = !email.keywords?.$seen; const starred = !!email.keywords?.$flagged; const date = emailDisplayDate(email); if (!expanded) { return ( [styles.collapsed, pressed && styles.pressed]}> {from?.name || from?.email || t('email_viewer.unknown_sender', 'Unknown')} {email.hasAttachment && } {formatHeaderDate(date, locale)} {formatHeaderTime(date, timeFormat, locale)} {singleLine(email.preview)} {starred && } ); } // The content keeps its place among the children whether bare or not, so // it (and its WebView) is not remounted when a conversation arrives. const reply = (mode: 'reply' | 'replyAll' | 'forward') => (replyable ? () => onReply(mode, email) : undefined); return ( {!bare && ( )} {!bare && ( {t('email_viewer.reply', 'Reply')} {t('email_viewer.reply_all', 'Reply All')} {t('email_viewer.forward', 'Forward')} )} ); } /** * Stands in for a collapsed card while a conversation's headers load, at the * same height, so the opened message does not move when they arrive. */ export function ThreadCardPlaceholder() { const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); return ( ); } function makeStyles(c: ThemePalette) { return StyleSheet.create({ collapsed: { flexDirection: 'row', alignItems: 'center', gap: spacing.md, paddingHorizontal: spacing.lg, paddingVertical: spacing.md, backgroundColor: c.surface, borderBottomWidth: 1, borderBottomColor: c.border, }, pressed: { backgroundColor: c.surfaceHover }, collapsedInfo: { flex: 1, minWidth: 0 }, collapsedTop: { flexDirection: 'row', alignItems: 'center', gap: spacing.xs }, collapsedName: { ...typography.body, color: c.text, flex: 1 }, unread: { fontWeight: '700' }, collapsedDate: { ...typography.small, color: c.textMuted }, collapsedPreview: { ...typography.caption, color: c.textMuted, marginTop: 2 }, expanded: { backgroundColor: c.background, borderBottomWidth: 1, borderBottomColor: c.border, }, bareFill: { flexGrow: 1 }, placeholderAvatar: { width: componentSizes.avatarSm, height: componentSizes.avatarSm, borderRadius: radius.full, backgroundColor: c.surfaceHover, }, actionsDisabled: { opacity: 0.4 }, collapseHandle: { height: 6, backgroundColor: c.surfaceHover, borderRadius: radius.xs, }, actions: { flexDirection: 'row', gap: spacing.sm, paddingHorizontal: spacing.lg, paddingVertical: spacing.sm, borderTopWidth: 1, borderTopColor: c.border, }, actionBtn: { flexDirection: 'row', alignItems: 'center', gap: spacing.xs, // Webmail thread card: outline buttons, h-9, rounded-md. paddingHorizontal: spacing.md, minHeight: 36, borderRadius: radius.sm, borderWidth: 1, borderColor: c.border, backgroundColor: 'transparent', }, actionLabel: { ...typography.bodyMedium, color: c.text }, }); }