16 KiB
Post Rendering Flow Documentation
This document traces the complete data flow and rendering pipeline for opening and displaying a post in the pm-pics application, from the PhotoGrid component through to the CompactMediaViewer.
Overview
The post rendering system follows this component chain:
PhotoGrid → Post.tsx → CompactRenderer → CompactMediaViewer
Each layer transforms and enriches the data, handling navigation state, media types, and rendering options.
1. Entry Point: PhotoGrid Component
File: PhotoGrid.tsx
Data Source
PhotoGrid receives media items from two possible sources:
- Custom Pictures (props): Pre-loaded media items passed directly
- Feed Data (hook): Fetched via
useFeedDatahook with parameters:source: 'home' | 'collection' | 'tag' | 'user' | 'widget'sourceId: Identifier for the sourcesortBy: 'latest' | other sort optionscategorySlugs: Optional category filtering
Data Structure: MediaItemType
interface MediaItemType {
id: string;
picture_id?: string;
title: string;
description: string | null;
image_url: string;
thumbnail_url: string | null;
type: MediaType;
meta: any | null;
likes_count: number;
created_at: string;
user_id: string;
comments: { count: number }[];
// Enriched fields
author?: UserProfile;
job?: any;
responsive?: any;
versionCount?: number;
}
Navigation Data Setup
When a user clicks on a media item (line 258-274):
const handleMediaClick = (mediaId: string, type: MediaType, index: number) => {
// Special handling for internal pages
if (type === 'page-intern') {
navigate(`/user/${username}/pages/${slug}`);
return;
}
// Update navigation context with current index
setNavigationData({ ...navigationData, currentIndex: index });
// Navigate to post view
navigate(`/post/${mediaId}`);
}
Navigation Context includes:
posts: Array of post metadata (id, title, image_url, user_id, type)currentIndex: Position in the feedsource: Where the user came fromsourceId: Source identifier
2. Post Container: Post.tsx
File: Post.tsx
Data Fetching Strategy
The fetchMedia function (lines 477-674) implements a 3-tier fallback strategy:
Tier 1: Fetch as Post
const postData = await db.fetchPostById(id);
Returns a PostItem with:
- Post metadata (title, description, settings)
- Array of
pictures(sorted by position) - User information
Tier 2: Fetch as Picture
const pictureData = await db.fetchPictureById(id);
If found and has post_id, fetches the full post. Otherwise creates a pseudo-post with single picture.
Tier 3: Fetch as Page
const pageData = await db.fetchPageById(id);
Creates a pseudo-post for internal page content with type: 'page-intern'.
Version Resolution
After fetching, the system resolves image versions (lines 478-526):
const resolveVersions = async (items: any[]) => {
// 1. Extract root IDs (parent_id || id)
const rootIds = items.map(i => i.parent_id || i.id);
// 2. Fetch all related versions
const allVersions = await db.fetchRelatedVersions(rootIds);
// 3. Preserve original positions
// 4. Sort by position, then created_at
// 5. Deduplicate by ID
}
This ensures that:
- AI-generated variations are grouped with originals
- User-selected versions are displayed
- Position order is maintained
State Management
Key state variables:
const [post, setPost] = useState<PostItem | null>(null);
const [mediaItems, setMediaItems] = useState<MediaItem[]>([]);
const [mediaItem, setMediaItem] = useState<MediaItem | null>(null); // Currently displayed
const [viewMode, setViewMode] = useState<'compact' | 'article' | 'thumbs'>('compact');
View Mode Synchronization
View mode is synchronized with URL parameters (lines 82-124):
// Initialize from URL
const viewParam = searchParams.get('view');
if (viewParam === 'article' || viewParam === 'compact' || viewParam === 'thumbs') {
return viewParam;
}
// Sync state to URL
useEffect(() => {
const newParams = new URLSearchParams(searchParams);
newParams.set('view', viewMode);
setSearchParams(newParams, { replace: true });
}, [viewMode]);
Data Types
PostItem
interface PostItem {
id: string;
title: string;
description: string | null;
user_id: string;
created_at: string;
updated_at: string;
pictures?: PostMediaItem[];
settings?: Json;
meta?: Json;
isPseudo?: boolean; // For single pictures without posts
type?: string; // 'page-intern' for internal pages
}
PostMediaItem
interface PostMediaItem {
id: string;
title: string;
description: string | null;
image_url: string;
thumbnail_url: string | null;
user_id: string;
type: MediaType;
created_at: string;
position: number | null;
post_id: string | null;
parent_id: string | null; // For versions
meta: Json | null;
likes_count: number;
visible: boolean;
is_liked?: boolean; // Enriched by user context
renderKey: string; // Unique key for React rendering
}
3. Renderer Selection: CompactRenderer
File: CompactRenderer.tsx
Props Interface
interface PostRendererProps {
post: PostItem;
authorProfile: UserProfile;
mediaItems: PostMediaItem[];
mediaItem: PostMediaItem; // Currently selected
// State
isOwner: boolean;
isLiked: boolean;
likesCount: number;
currentImageIndex: number;
// Media-specific
videoPlaybackUrl?: string;
videoPosterUrl?: string;
versionImages: ImageFile[];
// Callbacks
onMediaSelect: (item: PostMediaItem) => void;
onExpand: (item: PostMediaItem) => void;
onLike: () => void;
onDeletePicture: (id: string) => void;
// ... many more action handlers
// Navigation
navigationData: any;
handleNavigate: (direction: 'next' | 'prev') => void;
// Edit mode
isEditMode?: boolean;
localPost?: { title: string; description: string };
localMediaItems?: any[];
// ... edit-related handlers
}
Layout Strategy
CompactRenderer implements a responsive 2-column layout:
Desktop (lg breakpoint):
- Left Column: Media viewer + filmstrip
- Right Column: Header + details (scrollable)
Mobile:
- Stacked Layout: Header → Media feed → Details
Component Breakdown
CompactRenderer
├── CompactPostHeader (mobile top, desktop right)
│ ├── User info, title, description
│ ├── View mode switcher
│ └── Action menu (edit, delete, export)
│
├── Left Column (Desktop)
│ ├── CompactMediaViewer (main media display)
│ └── CompactFilmStrip (thumbnail navigation)
│
├── MobileGroupedFeed (Mobile only)
│ └── Vertical scrolling media cards
│
└── Right Column (Desktop)
├── CompactPostHeader (repeated)
└── CompactMediaDetails
├── Like/comment stats
├── Description
├── Comments section
└── Action buttons
4. Media Display: CompactMediaViewer
File: CompactMediaViewer.tsx
Media Type Handling
The viewer handles 6 distinct media types:
1. Internal Videos (video, video-intern)
<MediaPlayer
src={videoPlaybackUrl}
poster={videoPosterUrl}
load="idle"
posterLoad="eager"
/>
2. TikTok Videos (tiktok)
<iframe
src={mediaItem.image_url} // Embed URL
className="h-full aspect-[9/16]"
allow="encrypted-media;"
/>
3. YouTube Videos (detected from page-external meta)
const ytId = getYouTubeVideoId(url);
<iframe
src={`https://www.youtube.com/embed/${ytId}`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media;"
allowFullScreen
/>
4. TikTok Links (detected from page-external meta)
// Shows thumbnail with play button overlay
// On click, loads iframe embed
const tikTokId = getTikTokVideoId(url);
<iframe src={`https://www.tiktok.com/embed/v2/${tikTokId}`} />
5. Images (default)
<ResponsiveImage
src={`${mediaItem.image_url}${cacheBustKeys[mediaItem.id] ? `?v=${cacheBustKeys[mediaItem.id]}` : ''}`}
onClick={() => onExpand(mediaItem)}
sizes="(max-width: 1024px) 100vw, 1200px"
/>
6. External Pages (page-external)
Falls through to image display with thumbnail.
Version Control
CompactMediaViewer implements version navigation (lines 59-84):
const getVersionsForCurrentItem = () => {
const rootId = mediaItem.parent_id || mediaItem.id;
// Filter versions in same family
const visibleVersions = mediaItems.filter(item => {
const isGroupMatch = (item.id === rootId) || (item.parent_id === rootId);
if (!isOwner && !item.visible) return false;
return isGroupMatch;
});
// Sort by creation time
return visibleVersions.sort((a, b) =>
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
);
};
Keyboard Navigation:
ArrowUp: Previous versionArrowDown: Next versionArrowLeft: Previous media item or postArrowRight: Next media item or post
Navigation Controls
Two layers of navigation:
1. Media Item Navigation (within post)
onClick={() => {
if (currentImageIndex < mediaItems.length - 1) {
onMediaSelect(mediaItems[currentImageIndex + 1]);
} else {
handleNavigate('next'); // Move to next post
}
}}
2. Post Navigation (between posts)
Uses navigationData context to move between posts in the feed.
Data Flow Summary
graph TD
A[PhotoGrid] -->|Click mediaItem| B[navigate to /post/:id]
B --> C[Post.tsx fetchMedia]
C -->|Try 1| D[fetchPostById]
C -->|Try 2| E[fetchPictureById]
C -->|Try 3| F[fetchPageById]
D --> G[resolveVersions]
E --> G
F --> H[Create pseudo-post]
G --> I[setMediaItems]
H --> I
I --> J{viewMode?}
J -->|compact| K[CompactRenderer]
J -->|article| L[ArticleRenderer]
J -->|thumbs| M[ThumbsRenderer]
K --> N[CompactMediaViewer]
N --> O{mediaType?}
O -->|video| P[MediaPlayer]
O -->|tiktok| Q[TikTok iframe]
O -->|youtube| R[YouTube iframe]
O -->|image| S[ResponsiveImage]
Refactoring Considerations
Current Issues
- Data Duplication:
MediaItemType(PhotoGrid) vsPostMediaItem(Post.tsx) have overlapping fields - Type Confusion:
typefield is overloaded (media type vs page type) - Meta Ambiguity:
metafield contains different structures for different types - Navigation Coupling: Navigation state tightly coupled to PhotoGrid
- Version Logic Scattered: Version resolution happens in multiple places
Proposed Improvements
1. Unified Media Type System
// Base media item
interface BaseMediaItem {
id: string;
title: string;
description: string | null;
user_id: string;
created_at: string;
likes_count: number;
visible: boolean;
}
// Discriminated union for media types
type MediaItem =
| ImageMediaItem
| VideoMediaItem
| YouTubeMediaItem
| TikTokMediaItem
| PageMediaItem;
interface ImageMediaItem extends BaseMediaItem {
mediaType: 'image';
image_url: string;
thumbnail_url: string | null;
parent_id?: string; // For versions
responsive?: ResponsiveImageData;
}
interface VideoMediaItem extends BaseMediaItem {
mediaType: 'video';
video_url: string;
thumbnail_url: string;
duration?: number;
resolution?: { width: number; height: number };
}
interface YouTubeMediaItem extends BaseMediaItem {
mediaType: 'youtube';
videoId: string;
thumbnail_url: string;
}
interface TikTokMediaItem extends BaseMediaItem {
mediaType: 'tiktok';
videoId: string;
thumbnail_url: string;
}
interface PageMediaItem extends BaseMediaItem {
mediaType: 'page';
pageType: 'internal' | 'external';
slug?: string; // For internal pages
url?: string; // For external pages
thumbnail_url?: string;
}
2. Centralized Version Management
class VersionManager {
// Group items by version family
groupByFamily(items: MediaItem[]): Map<string, MediaItem[]>
// Get selected version for each family
getSelectedVersions(items: MediaItem[], userId?: string): MediaItem[]
// Navigate versions within a family
getNextVersion(currentId: string, items: MediaItem[]): MediaItem | null
getPrevVersion(currentId: string, items: MediaItem[]): MediaItem | null
}
3. Separate Navigation State
interface NavigationContext {
// Feed-level navigation
feedState: {
posts: PostReference[];
currentIndex: number;
source: FeedSource;
sourceId?: string;
};
// Post-level navigation
postState: {
mediaItems: MediaItem[];
currentIndex: number;
versionFamilies: Map<string, MediaItem[]>;
};
}
4. Renderer Configuration
interface RenderOptions {
viewMode: 'compact' | 'article' | 'thumbs';
layout: {
showFilmstrip: boolean;
showComments: boolean;
showVersions: boolean;
autoplayVideos: boolean;
};
permissions: {
canEdit: boolean;
canDelete: boolean;
canComment: boolean;
};
}
Migration Strategy
- Phase 1: Create new type definitions alongside existing ones
- Phase 2: Add adapters to convert between old and new types
- Phase 3: Refactor CompactMediaViewer to use discriminated unions
- Phase 4: Extract VersionManager as standalone service
- Phase 5: Update PhotoGrid and Post.tsx to use new types
- Phase 6: Remove old type definitions and adapters
Key Files Reference
- PhotoGrid.tsx - Feed display and navigation setup
- Post.tsx - Post container and data fetching
- CompactRenderer.tsx - Layout orchestration
- CompactMediaViewer.tsx - Media type rendering
- types.ts - Database schema types
Database Schema Reference
Key Tables
posts
- id: string (PK)
- title: string
- description: string | null
- user_id: string
- settings: json | null
- meta: json | null
- created_at: timestamp
pictures
- id: string (PK)
- title: string
- description: string | null
- image_url: string
- thumbnail_url: string | null
- user_id: string
- post_id: string | null (FK)
- parent_id: string | null (FK) -- For versions
- position: integer | null
- type: string | null
- meta: json | null
- visible: boolean
- likes_count: integer
- created_at: timestamp
pages
- id: string (PK)
- title: string
- slug: string
- owner: string
- content: json | null
- meta: json | null
- type: string | null
- is_public: boolean
- visible: boolean
- created_at: timestamp
Last updated: 2026-02-06