From 6db159e9447af6bc9942fc1dd4250542b727a525 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 5 Oct 2025 19:44:16 +0530 Subject: [PATCH] update trakt comment --- .gitignore | 1 + package-lock.json | 44 +- package.json | 1 + src/components/metadata/CommentsSection.tsx | 480 ++++++++++++++------ src/screens/MetadataScreen.tsx | 66 ++- 5 files changed, 441 insertions(+), 151 deletions(-) diff --git a/.gitignore b/.gitignore index 28ff76f7..70fdd630 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,4 @@ src/screens/xavio.md toast.md ffmpegreadme.md sliderreadme.md +bottomsheet.md diff --git a/package-lock.json b/package-lock.json index 3b98bc07..72a9b015 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@expo/env": "^2.0.7", "@expo/metro-runtime": "~4.0.1", "@expo/vector-icons": "~14.0.4", + "@gorhom/bottom-sheet": "^5.2.6", "@lottiefiles/dotlottie-react": "^0.6.5", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/blur": "^4.4.1", @@ -3098,6 +3099,45 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/@gorhom/bottom-sheet": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-5.2.6.tgz", + "integrity": "sha512-vmruJxdiUGDg+ZYcDmS30XDhq/h/+QkINOI5LY/uGjx8cPGwgJW0H6AB902gNTKtccbiKe/rr94EwdmIEz+LAQ==", + "license": "MIT", + "dependencies": { + "@gorhom/portal": "1.0.14", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-native": "*", + "react": "*", + "react-native": "*", + "react-native-gesture-handler": ">=2.16.1", + "react-native-reanimated": ">=3.16.0 || >=4.0.0-" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-native": { + "optional": true + } + } + }, + "node_modules/@gorhom/portal": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@gorhom/portal/-/portal-1.0.14.tgz", + "integrity": "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@ide/backoff": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz", @@ -4066,7 +4106,7 @@ "version": "0.72.8", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz", "integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "invariant": "^2.2.4", @@ -5185,7 +5225,7 @@ "version": "0.72.8", "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.72.8.tgz", "integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@react-native/virtualized-lists": "^0.72.4", diff --git a/package.json b/package.json index ab730331..47e1c4f1 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@expo/env": "^2.0.7", "@expo/metro-runtime": "~4.0.1", "@expo/vector-icons": "~14.0.4", + "@gorhom/bottom-sheet": "^5.2.6", "@lottiefiles/dotlottie-react": "^0.6.5", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/blur": "^4.4.1", diff --git a/src/components/metadata/CommentsSection.tsx b/src/components/metadata/CommentsSection.tsx index d94b8566..6664c88a 100644 --- a/src/components/metadata/CommentsSection.tsx +++ b/src/components/metadata/CommentsSection.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useState, useRef } from 'react'; import { View, Text, @@ -7,14 +7,15 @@ import { TouchableOpacity, FlatList, Dimensions, - Modal, Alert, + ScrollView, } 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'; +import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet'; const { width } = Dimensions.get('window'); @@ -23,6 +24,7 @@ interface CommentsSectionProps { type: 'movie' | 'show'; season?: number; episode?: number; + onCommentPress?: (comment: TraktContentComment) => void; } interface CommentItemProps { @@ -38,6 +40,8 @@ const CompactCommentCard: React.FC<{ isSpoilerRevealed: boolean; onSpoilerPress: () => void; }> = ({ comment, theme, onPress, isSpoilerRevealed, onSpoilerPress }) => { + const [isPressed, setIsPressed] = useState(false); + // Safety check - ensure comment data exists if (!comment || !comment.comment) { return null; @@ -112,18 +116,22 @@ const CompactCommentCard: React.FC<{ return stars; }; - const handlePress = () => { - if (hasSpoiler && !isSpoilerRevealed) { - onSpoilerPress(); - } else { - onPress(); - } - }; - return ( setIsPressed(true)} + onPressOut={() => setIsPressed(false)} + onPress={() => { + console.log('CompactCommentCard: TouchableOpacity pressed for comment:', comment.id); + onPress(); + }} activeOpacity={0.7} > {/* Header Section - Fixed at top */} @@ -162,15 +170,8 @@ const CompactCommentCard: React.FC<{ {/* Meta Info - Fixed at bottom */} - {comment.review && ( - - Review - - )} {comment.spoiler && ( - - Spoiler - + Spoiler )} @@ -193,8 +194,8 @@ const CompactCommentCard: React.FC<{ ); }; -// Expanded comment modal -const ExpandedCommentModal: React.FC<{ +// Expanded comment bottom sheet +const ExpandedCommentBottomSheet: React.FC<{ comment: TraktContentComment | null; visible: boolean; onClose: () => void; @@ -202,6 +203,17 @@ const ExpandedCommentModal: React.FC<{ 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 || {}; @@ -209,18 +221,21 @@ const ExpandedCommentModal: React.FC<{ const hasSpoiler = comment.spoiler; const shouldBlurModalContent = hasSpoiler && !isSpoilerRevealed; - const formatDate = (dateString: string) => { + const formatDateParts = (dateString: string) => { try { const date = new Date(dateString); - return date.toLocaleDateString('en-US', { + 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 'Unknown date'; + return { datePart: 'Unknown date', timePart: '' }; } }; @@ -254,26 +269,29 @@ const ExpandedCommentModal: React.FC<{ }; return ( - { + if (index === -1) { + onClose(); + } + }} + index={-1} + snapPoints={[200, '50%', '90%']} + enablePanDownToClose={true} + animateOnMount={true} + backgroundStyle={{ + backgroundColor: theme.colors.darkGray || '#0A0C0C', + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + }} + handleIndicatorStyle={{ + backgroundColor: theme.colors.mediumEmphasis || '#CCCCCC', + }} > - - {}} // Prevent closing when clicking on modal content - > + {/* Close Button */} @@ -291,9 +309,21 @@ const ExpandedCommentModal: React.FC<{ )} - - {formatDate(comment.created_at)} - + {(() => { + const { datePart, timePart } = formatDateParts(comment.created_at); + return ( + + + {datePart} + + {!!timePart && ( + + {timePart} + + )} + + ); + })()} {/* Rating */} @@ -322,18 +352,21 @@ const ExpandedCommentModal: React.FC<{ ) : ( - - {comment.comment} - + + + {comment.comment} + + )} {/* Comment Meta */} - {comment.review && ( - Review - )} {comment.spoiler && ( - Spoiler + Spoiler )} {comment.likes > 0 && ( @@ -354,9 +387,8 @@ const ExpandedCommentModal: React.FC<{ )} - - - + + ); }; @@ -365,11 +397,9 @@ export const CommentsSection: React.FC = ({ type, season, episode, + onCommentPress, }) => { const { currentTheme } = useTheme(); - const [selectedComment, setSelectedComment] = useState(null); - const [modalVisible, setModalVisible] = useState(false); - const [revealedSpoilers, setRevealedSpoilers] = useState>(new Set()); const { comments, @@ -388,45 +418,36 @@ export const CommentsSection: React.FC = ({ enabled: true, }); - const handleCommentPress = useCallback((comment: TraktContentComment) => { - setSelectedComment(comment); - setModalVisible(true); - }, []); + // 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 handleModalClose = useCallback(() => { - setModalVisible(false); - setSelectedComment(null); - }, []); + 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; + } - 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()])); - }, - }, - ] + 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 + }} + /> ); - }, []); - - const renderComment = useCallback(({ item }: { item: TraktContentComment }) => ( - handleCommentPress(item)} - isSpoilerRevealed={revealedSpoilers.has(item.id.toString())} - onSpoilerPress={() => handleSpoilerPress(item)} - /> - ), [currentTheme, handleCommentPress, revealedSpoilers, handleSpoilerPress]); + }, [currentTheme, onCommentPress]); const renderEmpty = useCallback(() => { if (loading) return null; @@ -496,6 +517,12 @@ export const CommentsSection: React.FC = ({ keyExtractor={(item) => item?.id?.toString() || Math.random().toString()} renderItem={renderComment} contentContainerStyle={styles.horizontalList} + removeClippedSubviews={false} + getItemLayout={(data, index) => ({ + length: 292, // width + marginRight + offset: 292 * index, + index, + })} onEndReached={() => { if (hasMore && !loading) { loadMore(); @@ -527,19 +554,202 @@ export const CommentsSection: React.FC = ({ /> )} - {/* Expanded Comment Modal */} - selectedComment && handleSpoilerPress(selectedComment)} - /> ); }; +// 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%']} + 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 */} + {shouldBlurModalContent ? ( + + + ⚠️ This comment contains spoilers + + + + Reveal Spoilers + + + + ) : ( + + + {comment.comment} + + + )} + + {/* Comment Meta */} + + {comment.spoiler && ( + Spoiler + )} + + {comment.likes > 0 && ( + + + + {comment.likes} + + + )} + {comment.replies > 0 && ( + + + + {comment.replies} + + + )} + + + + + ); +}; + const styles = StyleSheet.create({ container: { padding: 16, @@ -629,20 +839,9 @@ const styles = StyleSheet.create({ flexDirection: 'row', gap: 4, }, - miniReviewBadgeContainer: { - paddingHorizontal: 6, - paddingVertical: 2, - borderRadius: 8, - }, - miniSpoilerBadgeContainer: { - paddingHorizontal: 6, - paddingVertical: 2, - borderRadius: 8, - }, - miniBadgeText: { - fontSize: 9, + spoilerMiniText: { + fontSize: 11, fontWeight: '700', - color: '#FFFFFF', }, compactStats: { flexDirection: 'row', @@ -721,22 +920,10 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'space-between', }, - reviewBadge: { + spoilerText: { 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, + marginRight: 8, }, metaRight: { flexDirection: 'row', @@ -825,24 +1012,10 @@ const styles = StyleSheet.create({ fontSize: 12, fontWeight: '600', }, - modalOverlay: { + bottomSheetContent: { 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, @@ -867,6 +1040,13 @@ const styles = StyleSheet.create({ fontSize: 12, marginTop: 4, }, + modalTime: { + fontSize: 12, + marginTop: 2, + }, + dateTimeContainer: { + alignItems: 'flex-end', + }, modalRating: { flexDirection: 'row', alignItems: 'center', @@ -882,6 +1062,12 @@ const styles = StyleSheet.create({ lineHeight: 24, marginBottom: 16, }, + modalCommentScroll: { + // Constrain height so only text area scrolls, not the entire modal + maxHeight: 400, + marginBottom: 16, + flexShrink: 1, + }, spoilerContainer: { alignItems: 'center', paddingVertical: 20, diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 65df88b2..4a71492c 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -10,6 +10,7 @@ import { InteractionManager, BackHandler, Platform, + Alert, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useRoute, useNavigation, useFocusEffect } from '@react-navigation/native'; @@ -24,7 +25,7 @@ import { SeriesContent } from '../components/metadata/SeriesContent'; import { MovieContent } from '../components/metadata/MovieContent'; import { MoreLikeThisSection } from '../components/metadata/MoreLikeThisSection'; import { RatingsSection } from '../components/metadata/RatingsSection'; -import { CommentsSection } from '../components/metadata/CommentsSection'; +import { CommentsSection, CommentBottomSheet } from '../components/metadata/CommentsSection'; import { RouteParams, Episode } from '../types/metadata'; import Animated, { useAnimatedStyle, @@ -88,6 +89,20 @@ const MetadataScreen: React.FC = () => { const transitionOpacity = useSharedValue(1); const interactionComplete = useRef(false); + // Comment bottom sheet state + const [commentBottomSheetVisible, setCommentBottomSheetVisible] = useState(false); + const [selectedComment, setSelectedComment] = useState(null); + const [revealedSpoilers, setRevealedSpoilers] = useState>(new Set()); + + // Debug state changes + React.useEffect(() => { + console.log('MetadataScreen: commentBottomSheetVisible changed to:', commentBottomSheetVisible); + }, [commentBottomSheetVisible]); + + React.useEffect(() => { + console.log('MetadataScreen: selectedComment changed to:', selectedComment?.id); + }, [selectedComment]); + const { metadata, loading, @@ -527,6 +542,43 @@ const MetadataScreen: React.FC = () => { setShowCastModal(true); }, [isScreenFocused]); + const handleCommentPress = useCallback((comment: any) => { + console.log('MetadataScreen: handleCommentPress called with comment:', comment?.id); + if (!isScreenFocused) { + console.log('MetadataScreen: Screen not focused, ignoring'); + return; + } + console.log('MetadataScreen: Setting selected comment and opening bottomsheet'); + setSelectedComment(comment); + setCommentBottomSheetVisible(true); + console.log('MetadataScreen: State should be updated now'); + }, [isScreenFocused]); + + const handleCommentBottomSheetClose = useCallback(() => { + setCommentBottomSheetVisible(false); + setSelectedComment(null); + }, []); + + const handleSpoilerPress = useCallback((comment: any) => { + 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()])); + }, + }, + ] + ); + }, []); + // Source switching removed // Ultra-optimized animated styles - minimal calculations with conditional updates @@ -678,8 +730,8 @@ const MetadataScreen: React.FC = () => { {shouldLoadSecondaryData && imdbId && ( 0 ? 'show' : 'movie'} + onCommentPress={handleCommentPress} /> )} @@ -718,6 +770,16 @@ const MetadataScreen: React.FC = () => { castMember={selectedCastMember} /> )} + + {/* Comment Bottom Sheet - Memoized */} + selectedComment && handleSpoilerPress(selectedComment)} + /> );