mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
update trakt comment
This commit is contained in:
parent
35d3096ce4
commit
6db159e944
5 changed files with 441 additions and 151 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -63,3 +63,4 @@ src/screens/xavio.md
|
|||
toast.md
|
||||
ffmpegreadme.md
|
||||
sliderreadme.md
|
||||
bottomsheet.md
|
||||
|
|
|
|||
44
package-lock.json
generated
44
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<TouchableOpacity
|
||||
style={[styles.compactCard, { backgroundColor: theme.colors.card, borderColor: theme.colors.border }]}
|
||||
onPress={handlePress}
|
||||
style={[
|
||||
styles.compactCard,
|
||||
{
|
||||
backgroundColor: isPressed ? theme.colors.primary + '20' : theme.colors.card,
|
||||
borderColor: theme.colors.border,
|
||||
transform: isPressed ? [{ scale: 0.98 }] : [{ scale: 1 }]
|
||||
}
|
||||
]}
|
||||
onPressIn={() => 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 */}
|
||||
<View style={styles.compactMeta}>
|
||||
<View style={styles.compactBadges}>
|
||||
{comment.review && (
|
||||
<View style={[styles.miniReviewBadgeContainer, { backgroundColor: theme.colors.primary }]}>
|
||||
<Text style={styles.miniBadgeText}>Review</Text>
|
||||
</View>
|
||||
)}
|
||||
{comment.spoiler && (
|
||||
<View style={[styles.miniSpoilerBadgeContainer, { backgroundColor: theme.colors.error }]}>
|
||||
<Text style={styles.miniBadgeText}>Spoiler</Text>
|
||||
</View>
|
||||
<Text style={[styles.spoilerMiniText, { color: theme.colors.error }]}>Spoiler</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.compactStats}>
|
||||
|
|
@ -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<BottomSheet>(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 (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent={true}
|
||||
animationType="fade"
|
||||
onRequestClose={onClose}
|
||||
<BottomSheet
|
||||
ref={bottomSheetRef}
|
||||
onChange={(index) => {
|
||||
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',
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={onClose}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalContent, {
|
||||
backgroundColor: theme.colors.darkGray || '#0A0C0C',
|
||||
borderColor: theme.colors.border || '#CCCCCC',
|
||||
borderWidth: 1
|
||||
}]}
|
||||
activeOpacity={1}
|
||||
onPress={() => {}} // Prevent closing when clicking on modal content
|
||||
>
|
||||
<BottomSheetView style={[styles.bottomSheetContent, {
|
||||
backgroundColor: theme.colors.darkGray || '#0A0C0C',
|
||||
}]}>
|
||||
{/* Close Button */}
|
||||
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
||||
<MaterialIcons name="close" size={24} color={theme.colors.highEmphasis} />
|
||||
|
|
@ -291,9 +309,21 @@ const ExpandedCommentModal: React.FC<{
|
|||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.modalDate, { color: theme.colors.mediumEmphasis }]}>
|
||||
{formatDate(comment.created_at)}
|
||||
</Text>
|
||||
{(() => {
|
||||
const { datePart, timePart } = formatDateParts(comment.created_at);
|
||||
return (
|
||||
<View style={styles.dateTimeContainer}>
|
||||
<Text style={[styles.modalDate, { color: theme.colors.mediumEmphasis }]}>
|
||||
{datePart}
|
||||
</Text>
|
||||
{!!timePart && (
|
||||
<Text style={[styles.modalTime, { color: theme.colors.mediumEmphasis }]}>
|
||||
{timePart}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})()}
|
||||
</View>
|
||||
|
||||
{/* Rating */}
|
||||
|
|
@ -322,18 +352,21 @@ const ExpandedCommentModal: React.FC<{
|
|||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={[styles.modalComment, { color: theme.colors.highEmphasis }]}>
|
||||
{comment.comment}
|
||||
</Text>
|
||||
<ScrollView
|
||||
style={styles.modalCommentScroll}
|
||||
showsVerticalScrollIndicator={true}
|
||||
nestedScrollEnabled
|
||||
>
|
||||
<Text style={[styles.modalComment, { color: theme.colors.highEmphasis }]}>
|
||||
{comment.comment}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{/* Comment Meta */}
|
||||
<View style={styles.modalMeta}>
|
||||
{comment.review && (
|
||||
<Text style={[styles.reviewBadge, { color: theme.colors.primary }]}>Review</Text>
|
||||
)}
|
||||
{comment.spoiler && (
|
||||
<Text style={[styles.spoilerBadge, { color: theme.colors.error }]}>Spoiler</Text>
|
||||
<Text style={[styles.spoilerText, { color: theme.colors.error }]}>Spoiler</Text>
|
||||
)}
|
||||
<View style={styles.modalStats}>
|
||||
{comment.likes > 0 && (
|
||||
|
|
@ -354,9 +387,8 @@ const ExpandedCommentModal: React.FC<{
|
|||
)}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</BottomSheetView>
|
||||
</BottomSheet>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -365,11 +397,9 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
|
|||
type,
|
||||
season,
|
||||
episode,
|
||||
onCommentPress,
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const [selectedComment, setSelectedComment] = useState<TraktContentComment | null>(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [revealedSpoilers, setRevealedSpoilers] = useState<Set<string>>(new Set());
|
||||
|
||||
const {
|
||||
comments,
|
||||
|
|
@ -388,45 +418,36 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
|
|||
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 (
|
||||
<CompactCommentCard
|
||||
comment={item}
|
||||
theme={currentTheme}
|
||||
onPress={() => {
|
||||
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 }) => (
|
||||
<CompactCommentCard
|
||||
comment={item}
|
||||
theme={currentTheme}
|
||||
onPress={() => 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<CommentsSectionProps> = ({
|
|||
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<CommentsSectionProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Expanded Comment Modal */}
|
||||
<ExpandedCommentModal
|
||||
comment={selectedComment}
|
||||
visible={modalVisible}
|
||||
onClose={handleModalClose}
|
||||
theme={currentTheme}
|
||||
isSpoilerRevealed={selectedComment ? revealedSpoilers.has(selectedComment.id.toString()) : false}
|
||||
onSpoilerPress={() => selectedComment && handleSpoilerPress(selectedComment)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// 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<BottomSheet>(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(
|
||||
<MaterialIcons key={`full-${i}`} name="star" size={16} color="#FFD700" />
|
||||
);
|
||||
}
|
||||
|
||||
if (hasHalfStar) {
|
||||
stars.push(
|
||||
<MaterialIcons key="half" name="star-half" size={16} color="#FFD700" />
|
||||
);
|
||||
}
|
||||
|
||||
const emptyStars = 5 - Math.ceil(rating / 2);
|
||||
for (let i = 0; i < emptyStars; i++) {
|
||||
stars.push(
|
||||
<MaterialIcons key={`empty-${i}`} name="star-border" size={16} color="#FFD700" />
|
||||
);
|
||||
}
|
||||
|
||||
return stars;
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
ref={bottomSheetRef}
|
||||
onChange={(index) => {
|
||||
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',
|
||||
}}
|
||||
>
|
||||
<BottomSheetView style={[styles.bottomSheetContent, {
|
||||
backgroundColor: theme.colors.darkGray || '#0A0C0C',
|
||||
}]}>
|
||||
{/* User Info */}
|
||||
<View style={styles.modalHeader}>
|
||||
<View style={styles.userInfo}>
|
||||
<Text style={[styles.modalUsername, { color: theme.colors.highEmphasis }]}>
|
||||
{username}
|
||||
</Text>
|
||||
{user.vip && (
|
||||
<View style={styles.vipBadge}>
|
||||
<Text style={styles.vipText}>VIP</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{(() => {
|
||||
const { datePart, timePart } = formatDateParts(comment.created_at);
|
||||
return (
|
||||
<View style={styles.dateTimeContainer}>
|
||||
<Text style={[styles.modalDate, { color: theme.colors.mediumEmphasis }]}>
|
||||
{datePart}
|
||||
</Text>
|
||||
{!!timePart && (
|
||||
<Text style={[styles.modalTime, { color: theme.colors.mediumEmphasis }]}>
|
||||
{timePart}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})()}
|
||||
</View>
|
||||
|
||||
{/* Rating */}
|
||||
{comment.user_stats?.rating && (
|
||||
<View style={styles.modalRating}>
|
||||
{renderStars(comment.user_stats.rating)}
|
||||
<Text style={[styles.modalRatingText, { color: theme.colors.mediumEmphasis }]}>
|
||||
{comment.user_stats.rating}/10
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Full Comment */}
|
||||
{shouldBlurModalContent ? (
|
||||
<View style={styles.spoilerContainer}>
|
||||
<Text style={[styles.spoilerWarning, { color: theme.colors.error }]}>
|
||||
⚠️ This comment contains spoilers
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.revealButton, { backgroundColor: theme.colors.primary }]}
|
||||
onPress={onSpoilerPress}
|
||||
>
|
||||
<Text style={[styles.revealButtonText, { color: theme.colors.white }]}>
|
||||
Reveal Spoilers
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
style={styles.modalCommentScroll}
|
||||
showsVerticalScrollIndicator={true}
|
||||
nestedScrollEnabled
|
||||
>
|
||||
<Text style={[styles.modalComment, { color: theme.colors.highEmphasis }]}>
|
||||
{comment.comment}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{/* Comment Meta */}
|
||||
<View style={styles.modalMeta}>
|
||||
{comment.spoiler && (
|
||||
<Text style={[styles.spoilerText, { color: theme.colors.error }]}>Spoiler</Text>
|
||||
)}
|
||||
<View style={styles.modalStats}>
|
||||
{comment.likes > 0 && (
|
||||
<View style={styles.likesContainer}>
|
||||
<MaterialIcons name="thumb-up" size={16} color={theme.colors.mediumEmphasis} />
|
||||
<Text style={[styles.likesText, { color: theme.colors.mediumEmphasis }]}>
|
||||
{comment.likes}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{comment.replies > 0 && (
|
||||
<View style={styles.repliesContainer}>
|
||||
<MaterialIcons name="chat-bubble-outline" size={16} color={theme.colors.mediumEmphasis} />
|
||||
<Text style={[styles.repliesText, { color: theme.colors.mediumEmphasis }]}>
|
||||
{comment.replies}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</BottomSheetView>
|
||||
</BottomSheet>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<any>(null);
|
||||
const [revealedSpoilers, setRevealedSpoilers] = useState<Set<string>>(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 && (
|
||||
<MemoizedCommentsSection
|
||||
imdbId={imdbId}
|
||||
tmdbId={tmdbId || undefined}
|
||||
type={Object.keys(groupedEpisodes).length > 0 ? 'show' : 'movie'}
|
||||
onCommentPress={handleCommentPress}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -718,6 +770,16 @@ const MetadataScreen: React.FC = () => {
|
|||
castMember={selectedCastMember}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Comment Bottom Sheet - Memoized */}
|
||||
<CommentBottomSheet
|
||||
comment={selectedComment}
|
||||
visible={commentBottomSheetVisible}
|
||||
onClose={handleCommentBottomSheetClose}
|
||||
theme={currentTheme}
|
||||
isSpoilerRevealed={selectedComment ? revealedSpoilers.has(selectedComment.id.toString()) : false}
|
||||
onSpoilerPress={() => selectedComment && handleSpoilerPress(selectedComment)}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</Animated.View>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue