What It Does
Two main content feeds power the app: the Following feed (Home tab, authenticated only) and the Discover feed (all content, visible to everyone). Both use masonry grid layouts with infinite scroll.
All main tabs share a consistent header:
- Center: “RECIPE ROOM” in Courier font, uppercase, bold, letter-spacing 2
- Left:
Plus icon → navigates to /create (authenticated only)
- Right:
NotificationBell component (authenticated only)
Home Tab — Following Feed
app/(tabs)/index.tsx — shows posts + recipes from users you follow, interleaved by created_at. Only visible when authenticated; unauthenticated users are redirected to Discover.
Feed Card Layout
The Following feed uses FeedPostCard — an Instagram-style vertical layout:
- Full-width image with dynamic height (60%–125% of screen width, based on aspect ratio)
- Author overlay (avatar + username + content type icon) on top-left with gradient
- Multi-image posts show
ImageCarousel with pagination dots; single-image posts use BaseCard
- Double-tap on image triggers like with animated heart overlay (80px white heart, scale + fade)
- Below image: action row (Heart, MessageCircle, SaveButton), linked recipe tag (if post links to a recipe), caption with username prefix, “View all N comments” link, relative timestamp
- Loading state:
FeedSkeleton (two FeedPostCardSkeleton stacked)
Double-Tap Home Tab Reset
The tabs layout implements double-tap detection on the Home tab:
- Tracks
lastHomeTabTapRef with DOUBLE_TAP_DELAY = 300ms
- On double-tap: calls
(global as any).homeTabDoubleTap?.()
- The Home screen registers a handler on
global.homeTabDoubleTap to scroll the feed to top
This pattern is unconventional (using global as an event bus) but works because React Native runs in a single JS context.
Saved Item Pre-loading
The Home feed pre-loads all saved item IDs at the feed level via useSavedItemIds so every SaveButton renders with the correct bookmark state from the first frame, avoiding a flash from unfilled → filled.
Discover Tab
app/(tabs)/discover.tsx — all content, visible to everyone.
Discover Sub-Tabs
Two sub-tabs with icon + label:
| Tab | Icon | Sort Parameter | Algorithm |
|---|
| Discover | Compass | trending | Favorite count in last 24 hours |
| Popular | Heart | popular | Total favorite count (all time) |
Active tab has a 2px bottom border in foreground color. Both tabs call get_discover_content_list RPC with different p_sort values.
Discover Card Layout
Cards use ContentCard with variant="imageOnly" — cover image only, no text overlays. Recipe items show a small Film icon badge in the top-right corner (showRecipeIcon={item.content_type === 'recipe'}). Cards are displayed in a VirtualizedMasonryGrid (two-column masonry layout with fixed 300px card height).
Loading state: MasonryGridSkeleton (two-column grid of grey placeholder blocks).
Screens & Components
| Screen/Component | Purpose |
|---|
app/(tabs)/index.tsx | Home/Following feed |
app/(tabs)/discover.tsx | Discover feed (all content) |
ContentMasonryGrid | Masonry layout for content cards |
VirtualizedMasonryGrid | Virtualized masonry for performance |
MasonryGrid | Base masonry grid |
MasonryContentItem | Individual masonry item |
ContentCard | Recipe/post card |
FeedPostCard | Post-specific card in feeds |
ContentTabs | Tab switcher (Recipes/Posts/All) |
MetadataPills | Film sim, camera model pills |
TypeBadge | Recipe/Post type indicator |
ImageWithPlaceholder | Image with loading placeholder |
CreatorCard | Featured creator card |
Hooks
| Hook | Purpose |
|---|
useDiscoverContent | Discover feed fetching (mixed recipes + posts). useRecipes is a deprecated alias. |
usePosts | Following feed fetching (posts + recipes from followed users) |
useInfiniteScroll | Pagination/infinite scroll logic |
useScrollPosition | Scroll tracking for tab bar hide/show |
useTabBarScroll | Context for animated tab bar visibility |
useReducedMotion | Respects accessibility motion preferences |
Tab Bar Behavior
AnimatedTabBar (src/components/navigation/AnimatedTabBar.tsx) — custom animated tab bar that hides on scroll down, shows on scroll up (via tabBarTranslateY shared value from TabBarScrollContext). Uses React Native Reanimated for smooth 60fps animations. Positioned absolutely at the bottom of the screen (height: 80px with 20px bottom padding for safe area).
The tab bar hides the Home and Saved tabs for unauthenticated users via the hiddenRoutes prop. Tab icons are configured in app/(tabs)/_layout.tsx — see Common Components for the full icon mapping.
Data Access Pattern
The mobile app calls Supabase RPC functions directly — supabase.rpc('get_discover_content_list'), supabase.rpc('get_follow_content_list'), etc. It does NOT route through the Next.js API routes in apps/web.
RPC Functions
| Function | Parameters | Returns | Notes |
|---|
get_follow_content_list | p_limit (20), p_offset (0), p_current_user_id | JSON | Posts + recipes from followed users, interleaved by created_at |
get_discover_content_list | p_limit (20), p_offset (0), p_sort, p_current_user_id | JSON | All content. Trending = favorite count in last 24h. Popular = total favorite count. Recent = created_at. |
get_posts_list | p_limit (20), p_offset (0), p_sort, p_current_user_id | JSON | Posts-only listing |
There is no get_creators_list or get_recipes_list RPC function in the database. Recipe listing is handled through the discover content list or via direct Supabase queries. Creator/user listing is handled through profile queries.
React Query Configuration
staleTime: 30000 (30 seconds) on both useDiscoverContent and usePosts hooks
gcTime: 300000 (5 minutes) — keeps data in cache after becoming stale
- Page size: 12 items per page (both feeds)
- Cache cleared on logout (
queryClient.clear())
freezeOnBlur: false for Home tab specifically (prevents skeleton getting stuck when query resolves while tab is blurred)