"use client"; import { useState, useEffect, useRef } from 'react'; import { socialCache } from '../../utils/social-embed-cache'; import { MediaCarousel } from '../media-carousel'; import { RedditContainer } from './embed-container'; import { formatLargeNumber } from '../../utils/format'; import { useRichMarkdownRuntime } from './rich-markdown-runtime'; import type { MediaItem as CarouselMediaItem } from '../../utils/media-carousel-utils-stub'; // Using inline SVG icons to avoid dependency issues const MessageCircleIcon = () => ( ); const ExternalLinkIcon = () => ( ); const ArrowUpIcon = () => ( ); const ClockIcon = () => ( ); const UserIcon = () => ( ); const RedditIcon = () => ( ); // Simplified Reddit profile picture component const RedditProfilePic = ({ username }: { username: string }) => { return (
u/
); }; interface RedditPost { title: string; selftext: string; author: string; subreddit: string; created_utc: number; ups: number; num_comments: number; url: string; permalink: string; preview?: { images: Array<{ source: { url: string; width: number; height: number; }; resolutions: Array<{ url: string; width: number; height: number; }>; }>; }; media?: { reddit_video?: { fallback_url: string; height: number; width: number; is_gif: boolean; }; }; secure_media?: { reddit_video?: { fallback_url: string; height: number; width: number; is_gif: boolean; }; }; post_hint?: string; is_video?: boolean; domain?: string; gallery_data?: { items: Array<{ media_id: string; }>; }; media_metadata?: Record; } // Internal media-item shape; we cast to the carousel's expected MediaItem at // the render boundary so we don't have to fabricate `id`s/`alt` for every push. interface MediaItem { type: 'image' | 'video'; src: string; width: number; height: number; alt?: string; isGif?: boolean; poster?: string; } interface RedditEmbedProps { url: string; maxWidth?: number; } export function RedditEmbedClient({ url, maxWidth = 700 }: RedditEmbedProps) { const { redditProxyUrl } = useRichMarkdownRuntime(); const [redditData, setRedditData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const initializationDone = useRef(false); useEffect(() => { // Only run once if (initializationDone.current) return; initializationDone.current = true; // Normalize the Reddit URL to JSON format const jsonUrl = url.endsWith('.json') ? url : `${url.replace(/\/$/, '')}.json`; // Reddit-specific data validator const validateRedditData = (data: any): boolean => { return data && Array.isArray(data) && data[0] && data[0].data && data[0].data.children && data[0].data.children[0]; }; // Use centralized cache hierarchy socialCache.fetchWithHierarchy({ platform: 'reddit', url: jsonUrl, apiEndpoint: redditProxyUrl, dataValidator: validateRedditData, onDataUpdate: (data) => { if (data[0]?.data?.children?.[0]?.data) { setRedditData(data[0].data.children[0].data); } }, onError: (errorMsg) => setError(errorMsg), onLoading: (loading) => setLoading(loading) }); }, []); // Empty dependency array - only run once if (loading) { return (
); } if (error || !redditData) { return (
Reddit post unavailable

This Reddit post could not be loaded. It may have been deleted, made private, or the subreddit may be restricted.

View on Reddit
); } // Enhanced media extraction from Reddit post data const getMediaContent = (): MediaItem[] => { // FIRST: Check if the post has been removed or deleted - if so, don't extract media // Reddit API exact-match indicators only. Previously this also did // `title.toLowerCase().includes('removed' | 'deleted')` which suppressed // legitimate posts whose titles mention those words (e.g. "Comment was // removed by mods", "Deleted scenes from my favorite movie"). const isRemovedOrDeleted = redditData.selftext === '[removed]' || redditData.selftext === '[deleted]' || redditData.author === '[deleted]' || (redditData.title && redditData.title.includes('[removed]')); if (isRemovedOrDeleted) { console.log('🚫 Post content removed - skipping all media extraction for:', redditData.title); return []; // Return empty media array for removed posts } const media: MediaItem[] = []; console.log('🔍 Reddit media extraction for:', redditData.title); console.log('📊 Full Reddit data structure:', { url: redditData.url, domain: redditData.domain, post_hint: redditData.post_hint, is_video: redditData.is_video, media: redditData.media, secure_media: redditData.secure_media, preview: redditData.preview, gallery_data: redditData.gallery_data, media_metadata: redditData.media_metadata }); // 1. Check for Reddit hosted video (v.redd.it) - PRIORITY const video = redditData.media?.reddit_video || redditData.secure_media?.reddit_video; if (video && video.fallback_url) { console.log('📹 Found Reddit video:', video); // Generate poster URL from video URL and preview data let posterUrl = ''; // Try to get poster from preview images first if (redditData.preview?.images?.[0]?.source?.url) { posterUrl = redditData.preview.images[0].source.url.replace(/&/g, '&'); console.log('✅ Using preview image as video poster:', posterUrl); } else { // Fallback: try to generate from video URL try { const baseUrl = video.fallback_url.replace(/DASH_\d+\.mp4.*$/, ''); posterUrl = `${baseUrl}DASH_720.jpg`; console.log('🎯 Generated poster URL:', posterUrl); } catch (e) { console.log('Could not generate poster URL'); } } // Try to get a better video URL by replacing DASH format let videoUrl = video.fallback_url; // If it's a DASH URL, try to get a direct MP4 format if (videoUrl.includes('DASH_')) { // Try different quality levels for Reddit videos const baseUrl = videoUrl.replace(/DASH_\d+\.mp4.*$/, ''); const qualities = ['480', '360', '720', '240']; // Start with 480p for better compatibility // Use 480p as default for better compatibility videoUrl = `${baseUrl}DASH_480.mp4`; console.log('🎯 Optimized Reddit video URL for compatibility:', videoUrl); } media.push({ type: 'video', src: videoUrl, width: video.width || 640, height: video.height || 480, isGif: video.is_gif || false, poster: posterUrl }); // Return early for videos to avoid showing preview images as well console.log('📋 Final Reddit media (video):', media); return media; } // 2. Check for Reddit gallery (multiple images) if (redditData.media_metadata && redditData.gallery_data) { console.log('🖼️ Found Reddit gallery'); const galleryItems = redditData.gallery_data.items || []; for (const item of galleryItems) { const mediaId = item.media_id; const mediaInfo = redditData.media_metadata[mediaId]; if (mediaInfo && mediaInfo.s && mediaInfo.s.u) { // Reddit encodes URLs, need to decode const imageUrl = mediaInfo.s.u.replace(/&/g, '&'); console.log('✅ Adding gallery image:', imageUrl); media.push({ type: 'image', src: imageUrl, width: mediaInfo.s.x || 0, height: mediaInfo.s.y || 0, alt: redditData.title }); } } if (media.length > 0) { console.log('📋 Final Reddit media (gallery):', media); return media; } } // 3. Check for single image preview (but not if it's actually a video) if (redditData.preview?.images?.[0] && !redditData.is_video) { const imageData = redditData.preview.images[0]; console.log('🖼️ Found preview image data:', imageData); // Use best resolution that fits our constraints let source = imageData.source; if (imageData.resolutions && imageData.resolutions.length > 0) { // Find best resolution under 1200px width, or use source const bestResolution = imageData.resolutions .filter(r => r.width <= 1200) .sort((a, b) => b.width - a.width)[0]; source = bestResolution || imageData.source; } if (source && source.url) { const cleanUrl = source.url.replace(/&/g, '&'); console.log('✅ Adding preview image:', cleanUrl); media.push({ type: 'image', src: cleanUrl, width: source.width, height: source.height, alt: redditData.title }); } } // 4. Check for direct media URLs (imgur, i.redd.it, etc.) - only if no other media found if (media.length === 0 && redditData.url) { const directUrl = redditData.url.toLowerCase(); console.log('🔗 Checking direct URL:', directUrl); // Image formats if (directUrl.match(/\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i)) { console.log('📸 Found direct image URL'); media.push({ type: 'image', src: redditData.url, width: 0, height: 0, alt: redditData.title }); } // Video formats else if (directUrl.match(/\.(mp4|webm|mov|avi)(\?.*)?$/i)) { console.log('🎬 Found direct video URL'); // Try to generate poster from preview if available let posterUrl = ''; if (redditData.preview?.images?.[0]?.source?.url) { posterUrl = redditData.preview.images[0].source.url.replace(/&/g, '&'); } media.push({ type: 'video', src: redditData.url, width: 0, height: 0, isGif: false, poster: posterUrl }); } // Special handling for imgur else if (directUrl.includes('imgur.com') && !directUrl.includes('.gifv')) { console.log('🌐 Found Imgur link'); // Convert imgur links to direct image links const imgurId = directUrl.match(/imgur\.com\/([a-zA-Z0-9]+)/)?.[1]; if (imgurId && !directUrl.includes('/a/') && !directUrl.includes('/gallery/')) { // Try both jpg and png media.push({ type: 'image', src: `https://i.imgur.com/${imgurId}.jpg`, width: 0, height: 0, alt: redditData.title }); } } // i.redd.it images else if (directUrl.includes('i.redd.it')) { console.log('🖼️ Found i.redd.it image'); media.push({ type: 'image', src: redditData.url, width: 0, height: 0, alt: redditData.title }); } } console.log('📋 Final Reddit media array:', media); return media; }; const mediaContent = getMediaContent(); // Lib's `MediaCarousel` expects items shaped like the carousel-utils-stub // `MediaItem` (which has a required `id`). Reddit constructs items without an // `id`, so we synthesize one at the boundary. Keep the runtime cast — lib // carousel keys by index and only reads `.type`/`.src`/`.poster`/`.alt`. const carouselItems: CarouselMediaItem[] = mediaContent.map((m, i) => ({ id: `reddit-${i}`, type: m.type, src: m.src, poster: m.poster, alt: m.alt, width: m.width, height: m.height, })); // Format time const formatTimeAgo = (timestamp: number) => { const now = Math.floor(Date.now() / 1000); const diffSeconds = now - timestamp; if (diffSeconds < 60) return 'just now'; if (diffSeconds < 3600) return `${Math.floor(diffSeconds / 60)}m ago`; if (diffSeconds < 86400) return `${Math.floor(diffSeconds / 3600)}h ago`; return `${Math.floor(diffSeconds / 86400)}d ago`; }; // Format numbers using utility function const formatNumber = formatLargeNumber; const truncateText = (text: string, maxLength: number = 600) => { if (text.length <= maxLength) return text; return text.slice(0, maxLength) + '...'; }; return (
{/* Header with Profile Picture */}
{/* Lazy-loaded User Profile Picture */}

r/{redditData.subreddit}

u/{redditData.author} {formatTimeAgo(redditData.created_utc)}
{/* Content - Matching Twitter Style */}

{redditData.title}

{redditData.selftext && (

{truncateText(redditData.selftext)}

)} {/* Enhanced Media Section with Carousel */} {carouselItems.length > 0 && ( )} {/* Stats - Matching Twitter Style */}
{formatNumber(redditData.ups)} upvotes
{formatNumber(redditData.num_comments)} comments
{/* Footer - Matching Twitter Style */}
); }