"use client"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Agent, AppBskyEmbedImages, AppBskyFeedPost, AtUri, moderatePost, ModerationPrefs, } from "@atproto/api"; import { LoaderCircle } from "lucide-react"; import { motion } from "motion/react"; import Image from "next/image"; import Link from "next/link"; import Masonry from "react-masonry-css"; import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; import { SaveButton } from "./SaveButton"; import { UnsaveButton } from "./UnsaveButton"; import { LikeButton } from "./LikeButton"; import { useState, useEffect } from "react"; import { DEFAULT_LOGGED_OUT_LABEL_PREFERENCES, useModerationOpts, } from "@/lib/hooks/useModerationOpts"; import { useAuth } from "@/lib/hooks/useAuth"; import { ContentWarning } from "./ContentWarning"; import clsx from "clsx"; export type FeedItem = { id: string; imageUrl: string; alt?: string; author?: { avatar?: string; displayName?: string; handle: string; did?: string; }; text?: string; uri: string; aspectRatio?: { width: number; height: number }; blurDataURL?: string; }; // Props for the Feed component interface FeedProps { /** * Map of the index of the embedded media and post view */ feed?: [number, PostView][]; isLoading?: boolean; showUnsaveButton?: boolean; } function getText(post: PostView) { if (!AppBskyFeedPost.isRecord(post.record)) return; return (post.record as AppBskyFeedPost.Record).text; } function getImageFromItem(it: PostView, index: number) { if ( AppBskyEmbedImages.isMain(it.embed) || AppBskyEmbedImages.isView(it.embed) ) { return it.embed.images[index]; } else return null; } // Add this function to prefetch and cache images function prefetchAndCacheImages(feed: [number, PostView][] | undefined) { if (!feed || typeof window === "undefined") return; feed.forEach(([index, item]) => { const image = getImageFromItem(item, index); if (image && image.fullsize) { const img = new window.Image(); img.src = image.fullsize; // If service worker is active, explicitly add to cache if ("serviceWorker" in navigator && navigator.serviceWorker.controller) { fetch(image.fullsize, { mode: "no-cors" }).catch((err) => console.warn("Error prefetching image:", err) ); } } }); } function ImageCard({ item, showUnsaveButton, index, }: { item: PostView; showUnsaveButton?: boolean; index: number; }) { const image = getImageFromItem(item, index); const [isDropdownOpen, setDropdownOpen] = useState(false); const modOpts = useModerationOpts(); const { session, agent } = useAuth(); if (!image) return; const ActionButton = showUnsaveButton ? UnsaveButton : SaveButton; const txt = getText(item); const opts: ModerationPrefs = modOpts.moderationPrefs ?? { adultContentEnabled: false, labelers: agent.appLabelers.map((did) => ({ did, labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES, })), hiddenPosts: [], mutedWords: [], labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES, }; const mod = moderatePost(item, { prefs: opts, labelDefs: modOpts.labelDefs, userDid: session?.did, }); // Debug code removed for production return (
{/* Save/Unsave button – top-left */}
{ActionButton && ( )}
{/* Like button – top-right */}
{/* Link wraps image only */} {/* Blurred background */} {/* Foreground image */}
{image.alt
{/* Author info */} {item.author && (
{item.author.displayName || item.author.handle}
{item.author.displayName || item.author.handle} @{item.author.handle}
{txt && (
{txt.length > 100 ? txt.slice(0, 100) + "…" : txt}
)}
)}
); } export function feedAsMap(feed: PostView[]) { const map: [number, PostView][] = []; for (const it of feed) { if ( AppBskyEmbedImages.isMain(it.embed) || AppBskyEmbedImages.isView(it.embed) ) { it.embed.images.forEach((image, index) => map.push([index, it])); } } return map; } export const breakpointColumnsObj = { default: 5, 1536: 4, 1280: 3, 1024: 2, 768: 1, }; export function Feed({ feed, isLoading = false, showUnsaveButton = false, }: FeedProps) { // Use effect to prefetch and cache images when feed changes useEffect(() => { prefetchAndCacheImages(feed); }, [feed]); return ( <> {feed?.map(([index, item]) => ( ))} {isLoading && (
)} ); }