mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-26 19:12:54 +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
|
toast.md
|
||||||
ffmpegreadme.md
|
ffmpegreadme.md
|
||||||
sliderreadme.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/env": "^2.0.7",
|
||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "~14.0.4",
|
"@expo/vector-icons": "~14.0.4",
|
||||||
|
"@gorhom/bottom-sheet": "^5.2.6",
|
||||||
"@lottiefiles/dotlottie-react": "^0.6.5",
|
"@lottiefiles/dotlottie-react": "^0.6.5",
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-community/blur": "^4.4.1",
|
"@react-native-community/blur": "^4.4.1",
|
||||||
|
|
@ -3098,6 +3099,45 @@
|
||||||
"@babel/highlight": "^7.10.4"
|
"@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": {
|
"node_modules/@ide/backoff": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
|
||||||
|
|
@ -4066,7 +4106,7 @@
|
||||||
"version": "0.72.8",
|
"version": "0.72.8",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz",
|
||||||
"integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==",
|
"integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"invariant": "^2.2.4",
|
"invariant": "^2.2.4",
|
||||||
|
|
@ -5185,7 +5225,7 @@
|
||||||
"version": "0.72.8",
|
"version": "0.72.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.72.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.72.8.tgz",
|
||||||
"integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==",
|
"integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-native/virtualized-lists": "^0.72.4",
|
"@react-native/virtualized-lists": "^0.72.4",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"@expo/env": "^2.0.7",
|
"@expo/env": "^2.0.7",
|
||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "~14.0.4",
|
"@expo/vector-icons": "~14.0.4",
|
||||||
|
"@gorhom/bottom-sheet": "^5.2.6",
|
||||||
"@lottiefiles/dotlottie-react": "^0.6.5",
|
"@lottiefiles/dotlottie-react": "^0.6.5",
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-community/blur": "^4.4.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 {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -7,14 +7,15 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
FlatList,
|
FlatList,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Modal,
|
|
||||||
Alert,
|
Alert,
|
||||||
|
ScrollView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { TraktContentComment } from '../../services/traktService';
|
import { TraktContentComment } from '../../services/traktService';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { useTraktComments } from '../../hooks/useTraktComments';
|
import { useTraktComments } from '../../hooks/useTraktComments';
|
||||||
|
import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -23,6 +24,7 @@ interface CommentsSectionProps {
|
||||||
type: 'movie' | 'show';
|
type: 'movie' | 'show';
|
||||||
season?: number;
|
season?: number;
|
||||||
episode?: number;
|
episode?: number;
|
||||||
|
onCommentPress?: (comment: TraktContentComment) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommentItemProps {
|
interface CommentItemProps {
|
||||||
|
|
@ -38,6 +40,8 @@ const CompactCommentCard: React.FC<{
|
||||||
isSpoilerRevealed: boolean;
|
isSpoilerRevealed: boolean;
|
||||||
onSpoilerPress: () => void;
|
onSpoilerPress: () => void;
|
||||||
}> = ({ comment, theme, onPress, isSpoilerRevealed, onSpoilerPress }) => {
|
}> = ({ comment, theme, onPress, isSpoilerRevealed, onSpoilerPress }) => {
|
||||||
|
const [isPressed, setIsPressed] = useState(false);
|
||||||
|
|
||||||
// Safety check - ensure comment data exists
|
// Safety check - ensure comment data exists
|
||||||
if (!comment || !comment.comment) {
|
if (!comment || !comment.comment) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -112,18 +116,22 @@ const CompactCommentCard: React.FC<{
|
||||||
return stars;
|
return stars;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePress = () => {
|
|
||||||
if (hasSpoiler && !isSpoilerRevealed) {
|
|
||||||
onSpoilerPress();
|
|
||||||
} else {
|
|
||||||
onPress();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.compactCard, { backgroundColor: theme.colors.card, borderColor: theme.colors.border }]}
|
style={[
|
||||||
onPress={handlePress}
|
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}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
{/* Header Section - Fixed at top */}
|
{/* Header Section - Fixed at top */}
|
||||||
|
|
@ -162,15 +170,8 @@ const CompactCommentCard: React.FC<{
|
||||||
{/* Meta Info - Fixed at bottom */}
|
{/* Meta Info - Fixed at bottom */}
|
||||||
<View style={styles.compactMeta}>
|
<View style={styles.compactMeta}>
|
||||||
<View style={styles.compactBadges}>
|
<View style={styles.compactBadges}>
|
||||||
{comment.review && (
|
|
||||||
<View style={[styles.miniReviewBadgeContainer, { backgroundColor: theme.colors.primary }]}>
|
|
||||||
<Text style={styles.miniBadgeText}>Review</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{comment.spoiler && (
|
{comment.spoiler && (
|
||||||
<View style={[styles.miniSpoilerBadgeContainer, { backgroundColor: theme.colors.error }]}>
|
<Text style={[styles.spoilerMiniText, { color: theme.colors.error }]}>Spoiler</Text>
|
||||||
<Text style={styles.miniBadgeText}>Spoiler</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.compactStats}>
|
<View style={styles.compactStats}>
|
||||||
|
|
@ -193,8 +194,8 @@ const CompactCommentCard: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expanded comment modal
|
// Expanded comment bottom sheet
|
||||||
const ExpandedCommentModal: React.FC<{
|
const ExpandedCommentBottomSheet: React.FC<{
|
||||||
comment: TraktContentComment | null;
|
comment: TraktContentComment | null;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -202,6 +203,17 @@ const ExpandedCommentModal: React.FC<{
|
||||||
isSpoilerRevealed: boolean;
|
isSpoilerRevealed: boolean;
|
||||||
onSpoilerPress: () => void;
|
onSpoilerPress: () => void;
|
||||||
}> = ({ comment, visible, onClose, theme, isSpoilerRevealed, onSpoilerPress }) => {
|
}> = ({ 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;
|
if (!comment) return null;
|
||||||
|
|
||||||
const user = comment.user || {};
|
const user = comment.user || {};
|
||||||
|
|
@ -209,18 +221,21 @@ const ExpandedCommentModal: React.FC<{
|
||||||
const hasSpoiler = comment.spoiler;
|
const hasSpoiler = comment.spoiler;
|
||||||
const shouldBlurModalContent = hasSpoiler && !isSpoilerRevealed;
|
const shouldBlurModalContent = hasSpoiler && !isSpoilerRevealed;
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDateParts = (dateString: string) => {
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleDateString('en-US', {
|
const datePart = date.toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
const timePart = date.toLocaleTimeString('en-US', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
});
|
});
|
||||||
|
return { datePart, timePart };
|
||||||
} catch {
|
} catch {
|
||||||
return 'Unknown date';
|
return { datePart: 'Unknown date', timePart: '' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -254,26 +269,29 @@ const ExpandedCommentModal: React.FC<{
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<BottomSheet
|
||||||
visible={visible}
|
ref={bottomSheetRef}
|
||||||
transparent={true}
|
onChange={(index) => {
|
||||||
animationType="fade"
|
if (index === -1) {
|
||||||
onRequestClose={onClose}
|
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
|
<BottomSheetView style={[styles.bottomSheetContent, {
|
||||||
style={styles.modalOverlay}
|
backgroundColor: theme.colors.darkGray || '#0A0C0C',
|
||||||
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
|
|
||||||
>
|
|
||||||
{/* Close Button */}
|
{/* Close Button */}
|
||||||
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
||||||
<MaterialIcons name="close" size={24} color={theme.colors.highEmphasis} />
|
<MaterialIcons name="close" size={24} color={theme.colors.highEmphasis} />
|
||||||
|
|
@ -291,9 +309,21 @@ const ExpandedCommentModal: React.FC<{
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.modalDate, { color: theme.colors.mediumEmphasis }]}>
|
{(() => {
|
||||||
{formatDate(comment.created_at)}
|
const { datePart, timePart } = formatDateParts(comment.created_at);
|
||||||
</Text>
|
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>
|
</View>
|
||||||
|
|
||||||
{/* Rating */}
|
{/* Rating */}
|
||||||
|
|
@ -322,18 +352,21 @@ const ExpandedCommentModal: React.FC<{
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Text style={[styles.modalComment, { color: theme.colors.highEmphasis }]}>
|
<ScrollView
|
||||||
{comment.comment}
|
style={styles.modalCommentScroll}
|
||||||
</Text>
|
showsVerticalScrollIndicator={true}
|
||||||
|
nestedScrollEnabled
|
||||||
|
>
|
||||||
|
<Text style={[styles.modalComment, { color: theme.colors.highEmphasis }]}>
|
||||||
|
{comment.comment}
|
||||||
|
</Text>
|
||||||
|
</ScrollView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Comment Meta */}
|
{/* Comment Meta */}
|
||||||
<View style={styles.modalMeta}>
|
<View style={styles.modalMeta}>
|
||||||
{comment.review && (
|
|
||||||
<Text style={[styles.reviewBadge, { color: theme.colors.primary }]}>Review</Text>
|
|
||||||
)}
|
|
||||||
{comment.spoiler && (
|
{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}>
|
<View style={styles.modalStats}>
|
||||||
{comment.likes > 0 && (
|
{comment.likes > 0 && (
|
||||||
|
|
@ -354,9 +387,8 @@ const ExpandedCommentModal: React.FC<{
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</BottomSheetView>
|
||||||
</TouchableOpacity>
|
</BottomSheet>
|
||||||
</Modal>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -365,11 +397,9 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
|
||||||
type,
|
type,
|
||||||
season,
|
season,
|
||||||
episode,
|
episode,
|
||||||
|
onCommentPress,
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const [selectedComment, setSelectedComment] = useState<TraktContentComment | null>(null);
|
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
|
||||||
const [revealedSpoilers, setRevealedSpoilers] = useState<Set<string>>(new Set());
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
comments,
|
comments,
|
||||||
|
|
@ -388,45 +418,36 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCommentPress = useCallback((comment: TraktContentComment) => {
|
// Debug logging
|
||||||
setSelectedComment(comment);
|
console.log('CommentsSection: Comments data:', comments);
|
||||||
setModalVisible(true);
|
console.log('CommentsSection: Comments length:', comments?.length);
|
||||||
}, []);
|
console.log('CommentsSection: Loading:', loading);
|
||||||
|
console.log('CommentsSection: Error:', error);
|
||||||
|
|
||||||
const handleModalClose = useCallback(() => {
|
const renderComment = useCallback(({ item }: { item: TraktContentComment }) => {
|
||||||
setModalVisible(false);
|
// Safety check for null/undefined items
|
||||||
setSelectedComment(null);
|
if (!item || !item.id) {
|
||||||
}, []);
|
console.log('CommentsSection: Invalid comment item:', item);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const handleSpoilerPress = useCallback((comment: TraktContentComment) => {
|
console.log('CommentsSection: Rendering comment:', item.id);
|
||||||
Alert.alert(
|
|
||||||
'Spoiler Warning',
|
return (
|
||||||
'This comment contains spoilers. Are you sure you want to reveal it?',
|
<CompactCommentCard
|
||||||
[
|
comment={item}
|
||||||
{
|
theme={currentTheme}
|
||||||
text: 'Cancel',
|
onPress={() => {
|
||||||
style: 'cancel',
|
console.log('CommentsSection: Comment pressed:', item.id);
|
||||||
},
|
onCommentPress?.(item);
|
||||||
{
|
}}
|
||||||
text: 'Reveal Spoilers',
|
isSpoilerRevealed={true}
|
||||||
style: 'destructive',
|
onSpoilerPress={() => {
|
||||||
onPress: () => {
|
// Do nothing for now - spoilers are handled by parent
|
||||||
setRevealedSpoilers(prev => new Set([...prev, comment.id.toString()]));
|
}}
|
||||||
},
|
/>
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}, []);
|
}, [currentTheme, onCommentPress]);
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
const renderEmpty = useCallback(() => {
|
const renderEmpty = useCallback(() => {
|
||||||
if (loading) return null;
|
if (loading) return null;
|
||||||
|
|
@ -496,6 +517,12 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
|
||||||
keyExtractor={(item) => item?.id?.toString() || Math.random().toString()}
|
keyExtractor={(item) => item?.id?.toString() || Math.random().toString()}
|
||||||
renderItem={renderComment}
|
renderItem={renderComment}
|
||||||
contentContainerStyle={styles.horizontalList}
|
contentContainerStyle={styles.horizontalList}
|
||||||
|
removeClippedSubviews={false}
|
||||||
|
getItemLayout={(data, index) => ({
|
||||||
|
length: 292, // width + marginRight
|
||||||
|
offset: 292 * index,
|
||||||
|
index,
|
||||||
|
})}
|
||||||
onEndReached={() => {
|
onEndReached={() => {
|
||||||
if (hasMore && !loading) {
|
if (hasMore && !loading) {
|
||||||
loadMore();
|
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>
|
</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({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
|
|
@ -629,20 +839,9 @@ const styles = StyleSheet.create({
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
},
|
},
|
||||||
miniReviewBadgeContainer: {
|
spoilerMiniText: {
|
||||||
paddingHorizontal: 6,
|
fontSize: 11,
|
||||||
paddingVertical: 2,
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
miniSpoilerBadgeContainer: {
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
paddingVertical: 2,
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
miniBadgeText: {
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: '#FFFFFF',
|
|
||||||
},
|
},
|
||||||
compactStats: {
|
compactStats: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -721,22 +920,10 @@ const styles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
},
|
},
|
||||||
reviewBadge: {
|
spoilerText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
paddingHorizontal: 8,
|
marginRight: 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: {
|
metaRight: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -825,24 +1012,10 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
modalOverlay: {
|
bottomSheetContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 20,
|
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: {
|
closeButton: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 12,
|
top: 12,
|
||||||
|
|
@ -867,6 +1040,13 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
|
modalTime: {
|
||||||
|
fontSize: 12,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
dateTimeContainer: {
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
},
|
||||||
modalRating: {
|
modalRating: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
@ -882,6 +1062,12 @@ const styles = StyleSheet.create({
|
||||||
lineHeight: 24,
|
lineHeight: 24,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
|
modalCommentScroll: {
|
||||||
|
// Constrain height so only text area scrolls, not the entire modal
|
||||||
|
maxHeight: 400,
|
||||||
|
marginBottom: 16,
|
||||||
|
flexShrink: 1,
|
||||||
|
},
|
||||||
spoilerContainer: {
|
spoilerContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 20,
|
paddingVertical: 20,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
InteractionManager,
|
InteractionManager,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
Platform,
|
Platform,
|
||||||
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useRoute, useNavigation, useFocusEffect } from '@react-navigation/native';
|
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 { MovieContent } from '../components/metadata/MovieContent';
|
||||||
import { MoreLikeThisSection } from '../components/metadata/MoreLikeThisSection';
|
import { MoreLikeThisSection } from '../components/metadata/MoreLikeThisSection';
|
||||||
import { RatingsSection } from '../components/metadata/RatingsSection';
|
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 { RouteParams, Episode } from '../types/metadata';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
|
|
@ -88,6 +89,20 @@ const MetadataScreen: React.FC = () => {
|
||||||
const transitionOpacity = useSharedValue(1);
|
const transitionOpacity = useSharedValue(1);
|
||||||
const interactionComplete = useRef(false);
|
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 {
|
const {
|
||||||
metadata,
|
metadata,
|
||||||
loading,
|
loading,
|
||||||
|
|
@ -527,6 +542,43 @@ const MetadataScreen: React.FC = () => {
|
||||||
setShowCastModal(true);
|
setShowCastModal(true);
|
||||||
}, [isScreenFocused]);
|
}, [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
|
// Source switching removed
|
||||||
|
|
||||||
// Ultra-optimized animated styles - minimal calculations with conditional updates
|
// Ultra-optimized animated styles - minimal calculations with conditional updates
|
||||||
|
|
@ -678,8 +730,8 @@ const MetadataScreen: React.FC = () => {
|
||||||
{shouldLoadSecondaryData && imdbId && (
|
{shouldLoadSecondaryData && imdbId && (
|
||||||
<MemoizedCommentsSection
|
<MemoizedCommentsSection
|
||||||
imdbId={imdbId}
|
imdbId={imdbId}
|
||||||
tmdbId={tmdbId || undefined}
|
|
||||||
type={Object.keys(groupedEpisodes).length > 0 ? 'show' : 'movie'}
|
type={Object.keys(groupedEpisodes).length > 0 ? 'show' : 'movie'}
|
||||||
|
onCommentPress={handleCommentPress}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -718,6 +770,16 @@ const MetadataScreen: React.FC = () => {
|
||||||
castMember={selectedCastMember}
|
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>
|
</SafeAreaView>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue