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,
},
});