import React, { useCallback, useState, useRef, useMemo } from 'react'; import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, FlatList, Dimensions, Alert, ScrollView, Animated, Linking, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import TraktIcon from '../../../assets/rating-icons/trakt.svg'; import { useTheme } from '../../contexts/ThemeContext'; import { TraktContentComment } from '../../services/traktService'; import { logger } from '../../utils/logger'; import { useTraktComments } from '../../hooks/useTraktComments'; import { useSettings } from '../../hooks/useSettings'; import BottomSheet, { BottomSheetView, BottomSheetScrollView } from '@gorhom/bottom-sheet'; // Enhanced responsive breakpoints for Comments Section const BREAKPOINTS = { phone: 0, tablet: 768, largeTablet: 1024, tv: 1440, }; interface CommentsSectionProps { imdbId: string; type: 'movie' | 'show'; season?: number; episode?: number; onCommentPress?: (comment: TraktContentComment) => void; } interface CommentItemProps { comment: TraktContentComment; theme: any; } // Minimal markdown renderer with inline spoiler handling const MarkdownText: React.FC<{ text: string; theme: any; numberOfLines?: number; revealedInlineSpoilers: boolean; onSpoilerPress?: () => void; textStyle?: any; }> = ({ text, theme, numberOfLines, revealedInlineSpoilers, onSpoilerPress, textStyle }) => { // Regexes for simple markdown const linkRegex = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g; // [text](url) const boldRegex = /\*\*([^*]+)\*\*/g; // **bold** const italicRegex = /\*([^*]+)\*/g; // *italic* const codeRegex = /`([^`]+)`/g; // `code` const spoilerRegex = /\[spoiler\]([\s\S]*?)\[\/spoiler\]/gi; // Tokenize spoilers first to keep nesting simple const spoilerTokens: Array<{ type: 'spoiler' | 'text'; content: string }> = []; let lastIndex = 0; let match: RegExpExecArray | null; while ((match = spoilerRegex.exec(text)) !== null) { if (match.index > lastIndex) { spoilerTokens.push({ type: 'text', content: text.slice(lastIndex, match.index) }); } spoilerTokens.push({ type: 'spoiler', content: match[1] }); lastIndex = spoilerRegex.lastIndex; } if (lastIndex < text.length) { spoilerTokens.push({ type: 'text', content: text.slice(lastIndex) }); } const renderInline = (segment: string, keyPrefix: string) => { // Process code const codeSplit = segment.split(codeRegex); const codeNodes: React.ReactNode[] = []; for (let i = 0; i < codeSplit.length; i++) { if (i % 2 === 1) { codeNodes.push( {codeSplit[i]} ); } else { // process bold and italic and links inside normal text let chunk = codeSplit[i] ?? ''; const parts: React.ReactNode[] = []; // Links let cursor = 0; let linkMatch: RegExpExecArray | null; while ((linkMatch = linkRegex.exec(chunk)) !== null) { const before = chunk.slice(cursor, linkMatch.index); if (before) parts.push({before}); const label = linkMatch[1]; const url = linkMatch[2]; parts.push( Linking.openURL(url)} suppressHighlighting > {label} ); cursor = linkMatch.index + linkMatch[0].length; } if (cursor < chunk.length) { parts.push({chunk.slice(cursor)}); } // Wrap bold & italic via nested Text by replacing markers const applyFormat = (nodes: React.ReactNode[]): React.ReactNode[] => { return nodes.flatMap((node, idx) => { if (typeof node !== 'string' && !(node as any).props?.children) return node; const str = typeof node === 'string' ? node : (node as any).props.children as string; if (typeof str !== 'string') return node; // bold const boldSplit = str.split(boldRegex); const boldNodes: React.ReactNode[] = []; for (let b = 0; b < boldSplit.length; b++) { if (b % 2 === 1) { boldNodes.push({boldSplit[b]}); } else { // italic inside non-bold chunk const italSplit = boldSplit[b].split(italicRegex); for (let it = 0; it < italSplit.length; it++) { if (it % 2 === 1) { boldNodes.push({italSplit[it]}); } else { if (italSplit[it]) boldNodes.push({italSplit[it]}); } } } } return boldNodes; }); }; codeNodes.push( {applyFormat(parts)} ); } } return codeNodes; }; return ( {spoilerTokens.map((tok, idx) => { if (tok.type === 'text') { return {renderInline(tok.content, `seg-${idx}`)}; } if (revealedInlineSpoilers) { return {renderInline(tok.content, `spl-${idx}`)}; } return ( [spoiler] ); })} ); }; // Compact comment card for horizontal scrolling const CompactCommentCard: React.FC<{ comment: TraktContentComment; theme: any; onPress: () => void; isSpoilerRevealed: boolean; onSpoilerPress: () => void; }> = ({ comment, theme, onPress, isSpoilerRevealed, onSpoilerPress }) => { const [isPressed, setIsPressed] = useState(false); const fadeInOpacity = useRef(new Animated.Value(0)).current; React.useEffect(() => { Animated.timing(fadeInOpacity, { toValue: 1, duration: 220, useNativeDriver: true, }).start(); }, [fadeInOpacity]); // Enhanced responsive sizing for tablets and TV screens const deviceWidth = Dimensions.get('window').width; const deviceHeight = Dimensions.get('window').height; // Determine device type based on width const getDeviceType = useCallback(() => { if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; return 'phone'; }, [deviceWidth]); const deviceType = getDeviceType(); const isTablet = deviceType === 'tablet'; const isLargeTablet = deviceType === 'largeTablet'; const isTV = deviceType === 'tv'; const isLargeScreen = isTablet || isLargeTablet || isTV; // Enhanced comment card sizing const commentCardWidth = useMemo(() => { switch (deviceType) { case 'tv': return 360; case 'largeTablet': return 320; case 'tablet': return 300; default: return 280; // phone } }, [deviceType]); const commentCardHeight = useMemo(() => { switch (deviceType) { case 'tv': return 200; case 'largeTablet': return 185; case 'tablet': return 175; default: return 170; // phone } }, [deviceType]); const commentCardSpacing = useMemo(() => { switch (deviceType) { case 'tv': return 16; case 'largeTablet': return 14; case 'tablet': return 12; default: return 12; // phone } }, [deviceType]); // Safety check - ensure comment data exists if (!comment || !comment.comment) { return null; } // Handle missing user data gracefully const user = comment.user || {}; const username = user.name || user.username || 'Anonymous'; // Handle spoiler content const hasSpoiler = comment.spoiler; const shouldBlurContent = hasSpoiler && !isSpoilerRevealed; // We render markdown with inline spoilers; limit lines to keep card compact // Format relative time const formatRelativeTime = (dateString: string) => { try { const now = new Date(); const commentDate = new Date(dateString); const diffMs = now.getTime() - commentDate.getTime(); const diffMins = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffMins < 1) return 'now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; // For older dates, show month/day return commentDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } catch { return ''; } }; // Render stars for rating (convert 1-10 rating to 1-5 stars) const renderCompactStars = (rating: number) => { const stars = []; const fullStars = Math.floor(rating / 2); // Convert 10-point scale to 5 stars const hasHalfStar = rating % 2 >= 1; // Add full stars for (let i = 0; i < fullStars; i++) { stars.push( ); } // Add half star if needed if (hasHalfStar) { stars.push( ); } // Add empty stars to make 5 total const filledStars = fullStars + (hasHalfStar ? 1 : 0); const emptyStars = 5 - filledStars; for (let i = 0; i < emptyStars; i++) { stars.push( ); } return stars; }; return ( setIsPressed(true)} onPressOut={() => setIsPressed(false)} onPress={() => { console.log('CompactCommentCard: TouchableOpacity pressed for comment:', comment.id); onPress(); }} activeOpacity={1} > {/* Trakt Icon - Top Right Corner */} {/* Header Section - Fixed at top */} {username} {user.vip && ( VIP )} {/* Rating - Show stars */} {comment.user_stats?.rating && ( {renderCompactStars(comment.user_stats.rating)} {comment.user_stats.rating}/10 )} {/* Comment Preview - Flexible area that fills space */} {shouldBlurContent ? ( ⚠️ This comment contains spoilers. Tap to reveal. ) : ( )} {/* Meta Info - Fixed at bottom */} {comment.spoiler && ( Spoiler )} {formatRelativeTime(comment.created_at)} {comment.likes > 0 && ( 👍 {comment.likes} )} {comment.replies > 0 && ( 💬 {comment.replies} )} ); }; // Expanded comment bottom sheet const ExpandedCommentBottomSheet: React.FC<{ comment: TraktContentComment | null; visible: boolean; onClose: () => void; theme: any; isSpoilerRevealed: boolean; onSpoilerPress: () => void; }> = ({ comment, visible, onClose, theme, isSpoilerRevealed, onSpoilerPress }) => { const bottomSheetRef = useRef(null); // Handle visibility changes - always call this hook React.useEffect(() => { if (visible && comment) { bottomSheetRef.current?.expand(); } else { bottomSheetRef.current?.close(); } }, [visible, comment]); if (!comment) return null; const user = comment.user || {}; const username = user.name || user.username || 'Anonymous User'; const hasSpoiler = comment.spoiler; const shouldBlurModalContent = hasSpoiler && !isSpoilerRevealed; const formatDateParts = (dateString: string) => { try { const date = new Date(dateString); const datePart = date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); const timePart = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', }); return { datePart, timePart }; } catch { return { datePart: 'Unknown date', timePart: '' }; } }; const renderStars = (rating: number | null) => { if (rating === null) return null; const stars = []; const fullStars = Math.floor(rating / 2); const hasHalfStar = rating % 2 >= 1; for (let i = 0; i < fullStars; i++) { stars.push( ); } if (hasHalfStar) { stars.push( ); } const emptyStars = 5 - Math.ceil(rating / 2); for (let i = 0; i < emptyStars; i++) { stars.push( ); } return stars; }; return ( { if (index === -1) { onClose(); } }} index={-1} snapPoints={[200, '50%', '70%']} enableDynamicSizing={false} keyboardBehavior="interactive" android_keyboardInputMode="adjustResize" enablePanDownToClose={true} animateOnMount={true} backgroundStyle={{ backgroundColor: theme.colors.darkGray || '#0A0C0C', borderTopLeftRadius: 16, borderTopRightRadius: 16, }} handleIndicatorStyle={{ backgroundColor: theme.colors.mediumEmphasis || '#CCCCCC', }} > {/* Close Button */} {/* User Info */} {username} {user.vip && ( VIP )} {(() => { const { datePart, timePart } = formatDateParts(comment.created_at); return ( {datePart} {!!timePart && ( {timePart} )} ); })()} {/* Rating */} {comment.user_stats?.rating && ( {renderStars(comment.user_stats.rating)} {comment.user_stats.rating}/10 )} {/* Full Comment (Markdown with inline spoilers) */} {shouldBlurModalContent ? ( Contains spoilers Reveal ) : ( )} {/* Comment Meta */} {comment.spoiler && ( Spoiler )} {comment.likes > 0 && ( {comment.likes} )} {comment.replies > 0 && ( {comment.replies} )} ); }; export const CommentsSection: React.FC = ({ imdbId, type, season, episode, onCommentPress, }) => { const { currentTheme } = useTheme(); const { settings } = useSettings(); const [hasLoadedOnce, setHasLoadedOnce] = React.useState(false); // Enhanced responsive sizing for tablets and TV screens const deviceWidth = Dimensions.get('window').width; const deviceHeight = Dimensions.get('window').height; // Determine device type based on width const getDeviceType = useCallback(() => { if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; return 'phone'; }, [deviceWidth]); const deviceType = getDeviceType(); const isTablet = deviceType === 'tablet'; const isLargeTablet = deviceType === 'largeTablet'; const isTV = deviceType === 'tv'; const isLargeScreen = isTablet || isLargeTablet || isTV; // Enhanced spacing and padding const horizontalPadding = useMemo(() => { switch (deviceType) { case 'tv': return 32; case 'largeTablet': return 28; case 'tablet': return 24; default: return 16; // phone } }, [deviceType]); const { comments, loading, error, hasMore, isAuthenticated, loadMore, refresh, } = useTraktComments({ imdbId, type: type === 'show' ? (season !== undefined && episode !== undefined ? 'episode' : season !== undefined ? 'season' : 'show') : 'movie', season, episode, enabled: true, }); // Track when first load completes to avoid premature empty state React.useEffect(() => { if (!loading) { setHasLoadedOnce(true); } }, [loading]); // Debug logging console.log('CommentsSection: Comments data:', comments); console.log('CommentsSection: Comments length:', comments?.length); console.log('CommentsSection: Loading:', loading); console.log('CommentsSection: Error:', error); const renderComment = useCallback(({ item }: { item: TraktContentComment }) => { // Safety check for null/undefined items if (!item || !item.id) { console.log('CommentsSection: Invalid comment item:', item); return null; } console.log('CommentsSection: Rendering comment:', item.id); return ( { console.log('CommentsSection: Comment pressed:', item.id); onCommentPress?.(item); }} isSpoilerRevealed={true} onSpoilerPress={() => { // Do nothing for now - spoilers are handled by parent }} /> ); }, [currentTheme, onCommentPress]); const renderEmpty = useCallback(() => { if (loading) return null; return ( {error ? 'Comments unavailable' : 'No comments on Trakt yet'} {error ? 'This content may not be in Trakt\'s database yet' : 'Be the first to comment on Trakt.tv' } ); }, [loading, error, currentTheme]); const renderSkeletons = useCallback(() => { const placeholders = [0, 1, 2]; // Responsive skeleton sizes to match CompactCommentCard const skWidth = isTV ? 360 : isLargeTablet ? 320 : isTablet ? 300 : 280; const skHeight = isTV ? 200 : isLargeTablet ? 185 : isTablet ? 175 : 170; const skPad = isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12; const gap = isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12; const headLineWidth = isTV ? 160 : isLargeTablet ? 140 : isTablet ? 130 : 120; const ratingWidth = isTV ? 100 : isLargeTablet ? 90 : isTablet ? 85 : 80; const statWidth = isTV ? 44 : isLargeTablet ? 40 : isTablet ? 38 : 36; const badgeW = isTV ? 60 : isLargeTablet ? 56 : isTablet ? 52 : 50; const badgeH = isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12; return ( {placeholders.map((i) => ( ))} ); }, [currentTheme, isTV, isLargeTablet, isTablet]); // Don't show section if not authenticated, if comments are disabled in settings, or if still checking authentication // Only show when authentication is definitively true and settings allow it if (isAuthenticated !== true || !settings.showTraktComments) { // Show loading state only if we're checking authentication but settings allow comments if (isAuthenticated === null && settings.showTraktComments) { return ( ); } return null; } return ( Trakt Comments {error && ( {error} Retry )} {loading && Array.isArray(comments) && comments.length === 0 && renderSkeletons()} {(!loading && Array.isArray(comments) && comments.length === 0 && hasLoadedOnce && !error) && ( renderEmpty() )} {Array.isArray(comments) && comments.length > 0 && ( item?.id?.toString() || `comment-${index}`} renderItem={renderComment} contentContainerStyle={styles.horizontalList} removeClippedSubviews={false} getItemLayout={(data, index) => { const itemWidth = isTV ? 376 : isLargeTablet ? 334 : isTablet ? 312 : 292; // width + marginRight return { length: itemWidth, offset: itemWidth * index, index, }; }} onEndReached={() => { if (hasMore && !loading) { loadMore(); } }} onEndReachedThreshold={0.5} ListFooterComponent={ hasMore ? ( {loading ? ( ) : ( <> Load More )} ) : null } extraData={loading} style={{ opacity: 1 }} /> )} ); }; // BottomSheet component that should be rendered at a higher level export const CommentBottomSheet: React.FC<{ comment: TraktContentComment | null; visible: boolean; onClose: () => void; theme: any; isSpoilerRevealed: boolean; onSpoilerPress: () => void; }> = ({ comment, visible, onClose, theme, isSpoilerRevealed, onSpoilerPress }) => { const bottomSheetRef = useRef(null); console.log('CommentBottomSheet: Rendered with visible:', visible, 'comment:', comment?.id); // Calculate the index based on visibility - start at medium height (50%) const sheetIndex = visible && comment ? 1 : -1; console.log('CommentBottomSheet: Calculated sheetIndex:', sheetIndex); if (!comment) return null; const user = comment.user || {}; const username = user.name || user.username || 'Anonymous User'; const hasSpoiler = comment.spoiler; const shouldBlurModalContent = hasSpoiler && !isSpoilerRevealed; const formatDateParts = (dateString: string) => { try { const date = new Date(dateString); const datePart = date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); const timePart = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', }); return { datePart, timePart }; } catch { return { datePart: 'Unknown date', timePart: '' }; } }; const renderStars = (rating: number | null) => { if (rating === null) return null; const stars = []; const fullStars = Math.floor(rating / 2); const hasHalfStar = rating % 2 >= 1; for (let i = 0; i < fullStars; i++) { stars.push( ); } if (hasHalfStar) { stars.push( ); } const emptyStars = 5 - Math.ceil(rating / 2); for (let i = 0; i < emptyStars; i++) { stars.push( ); } return stars; }; return ( { console.log('CommentBottomSheet: onChange called with index:', index); if (index === -1) { onClose(); } }} index={sheetIndex} snapPoints={[200, '50%', '70%']} enableDynamicSizing={false} keyboardBehavior="interactive" android_keyboardInputMode="adjustResize" enablePanDownToClose={true} animateOnMount={true} backgroundStyle={{ backgroundColor: theme.colors.darkGray || '#0A0C0C', borderTopLeftRadius: 16, borderTopRightRadius: 16, }} handleIndicatorStyle={{ backgroundColor: theme.colors.mediumEmphasis || '#CCCCCC', }} > {/* User Info */} {username} {user.vip && ( VIP )} {(() => { const { datePart, timePart } = formatDateParts(comment.created_at); return ( {datePart} {!!timePart && ( {timePart} )} ); })()} {/* Rating */} {comment.user_stats?.rating && ( {renderStars(comment.user_stats.rating)} {comment.user_stats.rating}/10 )} {/* Full Comment (Markdown with inline spoilers) */} {shouldBlurModalContent ? ( Contains spoilers Reveal ) : ( )} {/* Comment Meta */} {comment.spoiler && ( Spoiler )} {comment.likes > 0 && ( {comment.likes} )} {comment.replies > 0 && ( {comment.replies} )} ); }; const styles = StyleSheet.create({ container: { marginBottom: 24, }, header: { flexDirection: 'row', alignItems: 'center', marginBottom: 16, }, title: { fontSize: 20, fontWeight: '600', flex: 1, }, horizontalList: { paddingRight: 16, }, compactCard: { paddingBottom: 16, borderRadius: 12, borderWidth: 1, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, flexDirection: 'column', }, compactHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 8, }, usernameContainer: { flexDirection: 'row', alignItems: 'center', flex: 1, }, compactUsername: { fontSize: 16, fontWeight: '600', marginRight: 8, }, miniVipBadge: { backgroundColor: '#FFD700', paddingHorizontal: 4, paddingVertical: 1, borderRadius: 6, marginLeft: 6, }, traktIconContainer: { position: 'absolute', top: 0, right: 0, zIndex: 1, }, skeletonTraktContainer: { position: 'absolute', top: 8, right: 8, zIndex: 1, }, miniVipText: { fontSize: 9, fontWeight: '700', color: '#000', }, compactRating: { flexDirection: 'row', alignItems: 'center', marginBottom: 8, gap: 2, }, compactRatingText: { fontSize: 14, fontWeight: '600', marginLeft: 4, }, commentContainer: { flex: 1, justifyContent: 'flex-start', marginBottom: 8, }, compactComment: { fontSize: 14, lineHeight: 18, }, blurredContent: { opacity: 0.3, backgroundColor: 'rgba(0, 0, 0, 0.1)', padding: 8, borderRadius: 4, }, compactMeta: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 'auto', paddingTop: 6, }, compactBadges: { flexDirection: 'row', gap: 4, }, spoilerMiniText: { fontSize: 11, fontWeight: '700', }, compactStats: { flexDirection: 'row', gap: 8, }, compactStat: { fontSize: 12, }, compactTime: { fontSize: 11, fontWeight: '500', }, loadMoreContainer: { justifyContent: 'center', alignItems: 'center', paddingHorizontal: 16, }, commentItem: { padding: 16, marginBottom: 12, borderRadius: 12, borderWidth: 1, }, commentHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8, }, userInfo: { flexDirection: 'row', alignItems: 'center', flex: 1, }, username: { fontSize: 16, fontWeight: '600', flex: 1, }, vipBadge: { backgroundColor: '#FFD700', paddingHorizontal: 6, paddingVertical: 2, borderRadius: 10, marginLeft: 8, }, vipText: { fontSize: 10, fontWeight: '700', color: '#000', }, date: { fontSize: 12, }, contentTitle: { fontSize: 14, fontWeight: '500', marginBottom: 4, }, ratingContainer: { flexDirection: 'row', alignItems: 'center', marginBottom: 8, }, ratingText: { fontSize: 12, marginLeft: 4, }, commentText: { fontSize: 15, lineHeight: 22, marginBottom: 12, }, commentMeta: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, spoilerText: { fontSize: 12, fontWeight: '600', marginRight: 8, }, metaRight: { flexDirection: 'row', alignItems: 'center', }, likesContainer: { flexDirection: 'row', alignItems: 'center', marginRight: 12, }, likesText: { fontSize: 12, marginLeft: 4, }, repliesContainer: { flexDirection: 'row', alignItems: 'center', }, repliesText: { fontSize: 12, marginLeft: 4, }, loadMoreButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 8, marginTop: 8, marginBottom: 16, }, loadMoreText: { fontSize: 14, fontWeight: '600', marginRight: 8, }, emptyContainer: { alignItems: 'center', justifyContent: 'center', paddingVertical: 40, }, emptyText: { fontSize: 16, fontWeight: '500', marginTop: 12, }, emptySubtext: { fontSize: 14, marginTop: 4, textAlign: 'center', }, emptyList: { flexGrow: 1, justifyContent: 'center', }, loadingContainer: { alignItems: 'center', justifyContent: 'center', paddingVertical: 20, }, loadingText: { fontSize: 14, marginTop: 12, }, skeletonLine: { height: 12, borderRadius: 6, backgroundColor: 'rgba(255,255,255,0.06)', }, skeletonBadge: { backgroundColor: 'rgba(255,255,255,0.08)', }, skeletonDot: { width: 16, height: 16, borderRadius: 8, backgroundColor: 'rgba(255,255,255,0.08)' }, errorContainer: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 8, marginBottom: 16, }, errorText: { fontSize: 14, marginLeft: 8, flex: 1, }, retryButton: { borderWidth: 1, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6, marginLeft: 12, }, retryButtonText: { fontSize: 12, fontWeight: '600', }, bottomSheetContent: { flex: 1, padding: 20, }, closeButton: { position: 'absolute', top: 12, right: 12, padding: 8, borderRadius: 20, backgroundColor: 'rgba(0, 0, 0, 0.1)', }, modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 16, paddingTop: 20, }, modalUsername: { fontSize: 18, fontWeight: '600', flexShrink: 1, marginRight: 8, }, modalDate: { fontSize: 12, marginTop: 4, }, modalTime: { fontSize: 12, marginTop: 2, }, dateTimeContainer: { alignItems: 'flex-end', }, modalRating: { flexDirection: 'row', alignItems: 'center', marginBottom: 16, }, modalRatingText: { fontSize: 14, fontWeight: '600', marginLeft: 8, }, modalComment: { fontSize: 16, lineHeight: 24, marginBottom: 16, }, modalCommentScroll: { // Let the scroll view expand to use available space inside the sheet flex: 1, flexGrow: 1, flexShrink: 1, minHeight: 0, marginBottom: 16, }, modalCommentContent: { paddingBottom: 16, }, spoilerContainer: { alignItems: 'center', paddingVertical: 16, }, spoilerIcon: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', marginBottom: 10, }, spoilerTitle: { fontSize: 14, fontWeight: '600', textAlign: 'center', marginBottom: 12, }, revealButton: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 14, paddingVertical: 8, borderRadius: 999, borderWidth: 1, }, revealButtonText: { fontSize: 14, fontWeight: '600', }, modalMeta: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, modalStats: { flexDirection: 'row', gap: 12, }, });