mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 10:51:19 +00:00
feat(social): implement link and image rendering in social posts
- Create contentParser utility to detect URLs and images in text - Enhance EnhancedSocialPost to render links as clickable and images as embedded - Archive unused SocialPost component following project conventions - Update CHANGELOG.md to document the enhancement This fixes the issue where links and images weren't properly rendered in the social feeds, improving the user experience by making content more interactive and visual.
This commit is contained in:
parent
5ff311bc4a
commit
6c7c54a54e
@ -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
|
||||
|
@ -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 = () => (
|
||||
<Text className="mb-2">{post.content}</Text>
|
||||
<View className="mb-2">
|
||||
{contentSegments.map((segment, index) => {
|
||||
switch (segment.type) {
|
||||
case 'text':
|
||||
return <Text key={index}>{segment.content}</Text>;
|
||||
|
||||
case 'image':
|
||||
return (
|
||||
<View key={index} className="my-2">
|
||||
<Image
|
||||
source={{ uri: segment.content }}
|
||||
className="w-full h-48 rounded-md"
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
case 'url':
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
onPress={() => Linking.openURL(segment.content)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text className="text-primary underline">{segment.content}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
|
||||
// Render quoted content if available
|
||||
|
@ -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) {
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
90
utils/contentParser.ts
Normal file
90
utils/contentParser.ts
Normal file
@ -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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user