diff --git a/CHANGELOG.md b/CHANGELOG.md index 4af150e..88bc505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Preserved underlying functionality for future re-implementation ### Fixed +- Link and image rendering in social feeds + - Added URL detection and rendering in social post content + - Implemented image URL detection and automatic embedding in posts + - Created contentParser utility for parsing text content with links + - Enhanced EnhancedSocialPost component to properly render links and images + - Archived unused SocialPost component following the project's archiving pattern + - UI issues in workout completion flow - Fixed weird line across input box in the 1301 event content box by adding subtle border styling - Removed template options section that was causing bugs with workout templates diff --git a/components/social/EnhancedSocialPost.tsx b/components/social/EnhancedSocialPost.tsx index 91654c6..0559fe6 100644 --- a/components/social/EnhancedSocialPost.tsx +++ b/components/social/EnhancedSocialPost.tsx @@ -1,6 +1,6 @@ // components/social/EnhancedSocialPost.tsx import React, { useEffect, useState, useMemo } from 'react'; -import { View, TouchableOpacity, Image, ScrollView } from 'react-native'; +import { View, TouchableOpacity, Image, ScrollView, Linking } from 'react-native'; import { Text } from '@/components/ui/text'; import { Badge } from '@/components/ui/badge'; import { Heart, MessageCircle, Repeat, Share, Clock, Dumbbell, CheckCircle, FileText, User } from 'lucide-react-native'; @@ -16,6 +16,7 @@ import { ParsedWorkoutTemplate, ParsedLongformContent } from '@/types/nostr-workout'; +import { parseContent } from '@/utils/contentParser'; import { formatDistance } from 'date-fns'; import Markdown from 'react-native-markdown-display'; import { useExerciseNames, useTemplateExerciseNames } from '@/lib/hooks/useExerciseNames'; @@ -511,9 +512,46 @@ function TemplateContent({ template }: { template: ParsedWorkoutTemplate }) { // Component for social posts function SocialContent({ post }: { post: ParsedSocialPost }) { - // Render the social post content + // Parse content to identify URLs and images + const contentSegments = useMemo(() => { + return parseContent(post.content); + }, [post.content]); + + // Render the social post content with links and images const renderMainContent = () => ( - {post.content} + + {contentSegments.map((segment, index) => { + switch (segment.type) { + case 'text': + return {segment.content}; + + case 'image': + return ( + + + + ); + + case 'url': + return ( + Linking.openURL(segment.content)} + activeOpacity={0.7} + > + {segment.content} + + ); + + default: + return null; + } + })} + ); // Render quoted content if available diff --git a/components/social/SocialPost.tsx b/components/social/archive/SocialPost.tsx similarity index 96% rename from components/social/SocialPost.tsx rename to components/social/archive/SocialPost.tsx index 7f3117f..9e830c6 100644 --- a/components/social/SocialPost.tsx +++ b/components/social/archive/SocialPost.tsx @@ -1,4 +1,9 @@ -// components/social/SocialPost.tsx +// components/social/archive/SocialPost.tsx +// ARCHIVED: April 9, 2025 +// This component was archived because it is no longer used in the application. +// The app exclusively uses EnhancedSocialPost for rendering social feed content. +// This file is kept for historical reference only. + import React from 'react'; import { View, TouchableOpacity } from 'react-native'; import { Text } from '@/components/ui/text'; @@ -234,4 +239,4 @@ export default function SocialPost({ post }: SocialPostProps) { ); -} \ No newline at end of file +} diff --git a/utils/contentParser.ts b/utils/contentParser.ts new file mode 100644 index 0000000..21aade8 --- /dev/null +++ b/utils/contentParser.ts @@ -0,0 +1,90 @@ +// utils/contentParser.ts +/** + * Utility functions for parsing content in social posts, including URL detection, + * image URL detection, and content segmentation for rendering. + */ + +/** + * Regular expression for detecting URLs + * Matches common URL patterns with or without protocol + */ +const URL_REGEX = /(https?:\/\/[^\s]+)|(www\.[^\s]+)/gi; + +/** + * Regular expression for detecting image URLs + * Matches URLs ending with common image extensions + */ +const IMAGE_URL_REGEX = /\.(gif|jpe?g|tiff?|png|webp|bmp)(\?.*)?$/i; + +/** + * Interface for content segments + */ +export interface ContentSegment { + type: 'text' | 'url' | 'image'; + content: string; +} + +/** + * Check if a URL is an image URL based on its extension + * + * @param url URL to check + * @returns Boolean indicating if URL is an image + */ +export function isImageUrl(url: string): boolean { + return IMAGE_URL_REGEX.test(url); +} + +/** + * Extract URLs from text + * + * @param text Text to extract URLs from + * @returns Array of URLs found in the text + */ +export function extractUrls(text: string): string[] { + return text.match(URL_REGEX) || []; +} + +/** + * Parse text content into segments for rendering + * Each segment is either plain text, a URL, or an image URL + * + * @param content Text content to parse + * @returns Array of content segments + */ +export function parseContent(content: string): ContentSegment[] { + if (!content) return []; + + const segments: ContentSegment[] = []; + const urls = extractUrls(content); + + // If no URLs, return whole content as text + if (urls.length === 0) { + return [{ type: 'text', content }]; + } + + // Split content by URLs and create segments for each part + let remainingContent = content; + + urls.forEach(url => { + const parts = remainingContent.split(url); + + // Add text before URL if exists + if (parts[0]) { + segments.push({ type: 'text', content: parts[0] }); + } + + // Add URL (as image or link) + const type = isImageUrl(url) ? 'image' : 'url'; + segments.push({ type, content: url }); + + // Update remaining content + remainingContent = parts.slice(1).join(url); + }); + + // Add any remaining text + if (remainingContent) { + segments.push({ type: 'text', content: remainingContent }); + } + + return segments; +}