import React, { useCallback, useState } from 'react';
import {
View,
Text,
StyleSheet,
ActivityIndicator,
TouchableOpacity,
FlatList,
Dimensions,
Modal,
Alert,
} from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../../contexts/ThemeContext';
import { TraktContentComment } from '../../services/traktService';
import { logger } from '../../utils/logger';
import { useTraktComments } from '../../hooks/useTraktComments';
const { width } = Dimensions.get('window');
interface CommentsSectionProps {
imdbId: string;
type: 'movie' | 'show';
season?: number;
episode?: number;
}
interface CommentItemProps {
comment: TraktContentComment;
theme: any;
}
// 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 }) => {
// 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;
const truncatedComment = comment.comment.length > 100
? comment.comment.substring(0, 100) + '...'
: comment.comment;
// 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;
};
const handlePress = () => {
if (hasSpoiler && !isSpoilerRevealed) {
onSpoilerPress();
} else {
onPress();
}
};
return (
{/* 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.' : truncatedComment}
{/* Meta Info - Fixed at bottom */}
{comment.review && (
Review
)}
{comment.spoiler && (
Spoiler
)}
{formatRelativeTime(comment.created_at)}
{comment.likes > 0 && (
👍 {comment.likes}
)}
{comment.replies > 0 && (
💬 {comment.replies}
)}
);
};
// Expanded comment modal
const ExpandedCommentModal: React.FC<{
comment: TraktContentComment | null;
visible: boolean;
onClose: () => void;
theme: any;
isSpoilerRevealed: boolean;
onSpoilerPress: () => void;
}> = ({ comment, visible, onClose, theme, isSpoilerRevealed, onSpoilerPress }) => {
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 formatDate = (dateString: string) => {
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return 'Unknown date';
}
};
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 (
{}} // Prevent closing when clicking on modal content
>
{/* Close Button */}
{/* User Info */}
{username}
{user.vip && (
VIP
)}
{formatDate(comment.created_at)}
{/* Rating */}
{comment.user_stats?.rating && (
{renderStars(comment.user_stats.rating)}
{comment.user_stats.rating}/10
)}
{/* Full Comment */}
{shouldBlurModalContent ? (
⚠️ This comment contains spoilers
Reveal Spoilers
) : (
{comment.comment}
)}
{/* Comment Meta */}
{comment.review && (
Review
)}
{comment.spoiler && (
Spoiler
)}
{comment.likes > 0 && (
{comment.likes}
)}
{comment.replies > 0 && (
{comment.replies}
)}
);
};
export const CommentsSection: React.FC = ({
imdbId,
type,
season,
episode,
}) => {
const { currentTheme } = useTheme();
const [selectedComment, setSelectedComment] = useState(null);
const [modalVisible, setModalVisible] = useState(false);
const [revealedSpoilers, setRevealedSpoilers] = useState>(new Set());
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,
});
const handleCommentPress = useCallback((comment: TraktContentComment) => {
setSelectedComment(comment);
setModalVisible(true);
}, []);
const handleModalClose = useCallback(() => {
setModalVisible(false);
setSelectedComment(null);
}, []);
const handleSpoilerPress = useCallback((comment: TraktContentComment) => {
Alert.alert(
'Spoiler Warning',
'This comment contains spoilers. Are you sure you want to reveal it?',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Reveal Spoilers',
style: 'destructive',
onPress: () => {
setRevealedSpoilers(prev => new Set([...prev, comment.id.toString()]));
},
},
]
);
}, []);
const renderComment = useCallback(({ item }: { item: TraktContentComment }) => (
handleCommentPress(item)}
isSpoilerRevealed={revealedSpoilers.has(item.id.toString())}
onSpoilerPress={() => handleSpoilerPress(item)}
/>
), [currentTheme, handleCommentPress, revealedSpoilers, handleSpoilerPress]);
const renderEmpty = useCallback(() => {
if (loading) return null;
return (
{error ? 'Comments unavailable' : 'No comments yet'}
{error
? 'This content may not be in Trakt\'s database yet'
: 'Be the first to comment on Trakt.tv'
}
);
}, [loading, error, currentTheme]);
// Don't show section if not authenticated
if (!isAuthenticated) {
return null;
}
return (
Trakt Comments
{error && (
{error}
Retry
)}
{loading && comments.length === 0 && (
Loading comments...
)}
{comments.length === 0 ? (
renderEmpty()
) : (
item?.id?.toString() || Math.random().toString()}
renderItem={renderComment}
contentContainerStyle={styles.horizontalList}
onEndReached={() => {
if (hasMore && !loading) {
loadMore();
}
}}
onEndReachedThreshold={0.5}
ListFooterComponent={
hasMore ? (
{loading ? (
) : (
<>
Load More
>
)}
) : null
}
/>
)}
{/* Expanded Comment Modal */}
selectedComment && handleSpoilerPress(selectedComment)}
/>
);
};
const styles = StyleSheet.create({
container: {
padding: 16,
marginBottom: 24,
},
header: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
title: {
fontSize: 20,
fontWeight: '600',
flex: 1,
},
horizontalList: {
paddingRight: 16,
},
compactCard: {
width: 280,
height: 160,
padding: 12,
marginRight: 12,
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,
},
compactUsername: {
fontSize: 16,
fontWeight: '600',
flex: 1,
},
miniVipBadge: {
backgroundColor: '#FFD700',
paddingHorizontal: 4,
paddingVertical: 1,
borderRadius: 6,
marginLeft: 6,
},
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: 20,
},
blurredContent: {
opacity: 0.3,
backgroundColor: 'rgba(0, 0, 0, 0.1)',
padding: 8,
borderRadius: 4,
},
compactMeta: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
compactBadges: {
flexDirection: 'row',
gap: 4,
},
miniReviewBadgeContainer: {
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 8,
},
miniSpoilerBadgeContainer: {
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 8,
},
miniBadgeText: {
fontSize: 9,
fontWeight: '700',
color: '#FFFFFF',
},
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',
},
reviewBadge: {
fontSize: 12,
fontWeight: '600',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 12,
backgroundColor: 'rgba(33, 150, 243, 0.1)',
},
spoilerBadge: {
fontSize: 12,
fontWeight: '600',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 12,
backgroundColor: 'rgba(244, 67, 54, 0.1)',
marginLeft: 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: 40,
},
loadingText: {
fontSize: 14,
marginTop: 12,
},
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',
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
modalContent: {
width: '90%',
maxWidth: 400,
maxHeight: '80%',
borderRadius: 16,
padding: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.3,
shadowRadius: 10,
},
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',
flex: 1,
},
modalDate: {
fontSize: 12,
marginTop: 4,
},
modalRating: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
modalRatingText: {
fontSize: 14,
fontWeight: '600',
marginLeft: 8,
},
modalComment: {
fontSize: 16,
lineHeight: 24,
marginBottom: 16,
},
spoilerContainer: {
alignItems: 'center',
paddingVertical: 20,
},
spoilerWarning: {
fontSize: 16,
fontWeight: '600',
textAlign: 'center',
marginBottom: 16,
},
revealButton: {
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 8,
},
revealButtonText: {
fontSize: 16,
fontWeight: '600',
},
modalMeta: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
modalStats: {
flexDirection: 'row',
gap: 12,
},
});