/** * MessageContent - Renders message content (text, images, videos) * Handles media reveal logic, expired media, and loading states. */ import React, { useState, useEffect } from 'react'; import { View, TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { Ionicons } from '@expo/vector-icons'; import { colors, gradients, spacing, radius } from '../../../theme'; import { MessageMeta } from './MessageMeta'; import { ChatVideoPlayer } from './ChatVideoPlayer'; import { supabase } from '../../../lib/supabase'; import type { Message } from '../types'; const ACCENT = colors.premium.gold; export interface MessageContentProps { item: Message; isMe: boolean; currentUserId: string; revealMessage: (id: string) => void; onImagePress: (uri: string) => void; onVideoFullScreen: (uri: string) => void; } const MessageContentComponent: React.FC = ({ item, isMe, currentUserId, revealMessage, onImagePress, onVideoFullScreen, }) => { // Check if message was deleted for everyone if (item.deleted_at) { return ( {isMe ? 'You deleted this message' : 'This message was deleted'} ); } const isVideo = item.media_type === 'video'; const isViewed = !!item.media_viewed_at; const isRecipientHidden = !isMe && !isViewed; // Get signed URL for media const [mediaUri, setMediaUri] = useState(null); const [mediaLoading, setMediaLoading] = useState(false); const [mediaError, setMediaError] = useState(false); useEffect(() => { if (!item.media_path || isRecipientHidden) { setMediaUri(null); return; } const fetchMediaUrl = async () => { setMediaLoading(true); setMediaError(false); try { const { data, error } = await supabase.storage .from('chat-media') .createSignedUrl(item.media_path!, 3600); // 1 hour expiry if (error) throw error; setMediaUri(data.signedUrl); } catch (err) { console.error('Failed to get media URL:', err); setMediaError(true); } finally { setMediaLoading(false); } }; fetchMediaUrl(); }, [item.media_path, isRecipientHidden]); // Handle expired videos const isExpired = !!item.media_expired; if (isExpired && isVideo) { return ( Video expired Videos are deleted 30 days after viewing ); } // Handle media (images and videos) if (item.media_path || isExpired) { const canOpenFullScreen = (isMe || isViewed) && !isVideo; // Only images can go fullscreen const isLoading = mediaLoading || (!mediaUri && !mediaError && !isRecipientHidden); // Video content if (isVideo) { return ( {isRecipientHidden ? ( // Clean reveal UI for unrevealed videos revealMessage(item.id)} > Video hidden Tap to reveal ) : mediaUri || mediaError ? ( ) : ( )} {/* Viewed indicator for sender */} {isMe && isViewed && ( Viewed )} ); } // Image content return ( { if (canOpenFullScreen && mediaUri) { onImagePress(mediaUri); } }} disabled={!(canOpenFullScreen && !!mediaUri)} > {isRecipientHidden ? ( revealMessage(item.id)} > Photo hidden Tap to reveal ) : mediaUri ? ( ) : mediaError ? ( Failed to load image ) : ( )} {/* Viewed indicator for sender */} {isMe && isViewed && ( Viewed )} ); } // Text message - display content directly return ( <> {item.content ?? 'Message unavailable'} ); }; // Wrap with React.memo for performance export const MessageContent = React.memo(MessageContentComponent); const styles = StyleSheet.create({ deletedContainer: { flexDirection: 'row', alignItems: 'center', gap: spacing.xs, }, deletedText: { color: colors.textTertiary, fontSize: 14, fontStyle: 'italic', }, messageText: { color: colors.text, fontSize: 16, lineHeight: 22, }, messageImage: { width: 200, height: 200, borderRadius: radius.md, backgroundColor: 'rgba(255, 255, 255, 0.05)', }, messageImageLoading: { justifyContent: 'center', alignItems: 'center', }, messageImageError: { justifyContent: 'center', alignItems: 'center', gap: spacing.sm, }, messageImageErrorText: { color: colors.textSecondary, fontSize: 12, }, unrevealedPlaceholder: { justifyContent: 'center', alignItems: 'center', gap: spacing.sm, overflow: 'hidden', }, unrevealedText: { color: colors.text, fontSize: 13, fontWeight: '600', }, revealHint: { color: gradients.premiumGold[0], fontSize: 12, fontWeight: '500', }, messageVideo: { width: 200, height: 200, borderRadius: radius.md, backgroundColor: 'rgba(255, 255, 255, 0.05)', overflow: 'hidden', }, revealIconContainer: { width: 48, height: 48, borderRadius: 24, justifyContent: 'center', alignItems: 'center', }, viewedBadge: { flexDirection: 'row', alignItems: 'center', gap: 4, marginTop: spacing.xs, }, viewedText: { color: colors.success, fontSize: 11, fontWeight: '500', }, expiredMedia: { width: 200, height: 150, borderRadius: radius.md, overflow: 'hidden', justifyContent: 'center', alignItems: 'center', gap: spacing.sm, }, expiredIconContainer: { width: 48, height: 48, borderRadius: 24, backgroundColor: 'rgba(255, 255, 255, 0.08)', justifyContent: 'center', alignItems: 'center', }, expiredText: { color: colors.textSecondary, fontSize: 14, fontWeight: '600', }, expiredSubtext: { color: colors.textTertiary, fontSize: 11, textAlign: 'center', paddingHorizontal: spacing.md, }, });