Fix image gallery deduplication for absolute vs relative URLs

- Extract filename only (not full path) for deduplication
- Same image as https://domain/path/file.jpg and /path/file.jpg
  were not being deduplicated because full paths differ
- Updated both PostDetailModal.jsx and markerManager.js
- Added fetchPostById to get full post data with image_urls

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-01-21 23:02:54 +00:00
parent 3a29812319
commit 91e024e156
2 changed files with 25 additions and 7 deletions

View File

@ -789,10 +789,13 @@ function collectGalleryImages(post) {
const seen = new Set(); const seen = new Set();
const seenBase = new Set(); const seenBase = new Set();
// Get base filename without size suffix to detect same image in different sizes // Get base filename without size suffix to detect same image in different sizes
// Extract just the filename (not full path) to handle absolute vs relative URL dedup
// Handles both long (_large, _medium, _small) and short (_l, _m, _s) variants // Handles both long (_large, _medium, _small) and short (_l, _m, _s) variants
const getBase = (url) => { const getBase = (url) => {
if (!url) return ''; if (!url) return '';
return url.replace(/[-_](large|medium|small|thumb|thumbnail|l|m|s)(\.[^.]+)?$/i, '$2') // Extract just the filename, remove path and domain
const filename = url.split('/').pop() || url;
return filename.replace(/[-_](large|medium|small|thumb|thumbnail|l|m|s)(\.[^.]+)?$/i, '$2')
.replace(/\?.*$/, ''); // Remove query params .replace(/\?.*$/, ''); // Remove query params
}; };
const add = (value) => { const add = (value) => {

View File

@ -17,6 +17,7 @@ import {
deletePost, deletePost,
updatePostVisibility, updatePostVisibility,
fetchProfile, fetchProfile,
fetchPostById,
} from "../../api/client"; } from "../../api/client";
// ============================================================================ // ============================================================================
@ -567,6 +568,7 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
const [showComments, setShowComments] = useState(true); const [showComments, setShowComments] = useState(true);
const [showEdit, setShowEdit] = useState(false); const [showEdit, setShowEdit] = useState(false);
const [showDelete, setShowDelete] = useState(false); const [showDelete, setShowDelete] = useState(false);
const [fullPost, setFullPost] = useState(null); // Full post data fetched from API
const [editTitle, setEditTitle] = useState(post?.title || ""); const [editTitle, setEditTitle] = useState(post?.title || "");
const [editSnippet, setEditSnippet] = useState(post?.snippet || ""); const [editSnippet, setEditSnippet] = useState(post?.snippet || "");
const [editVisibility, setEditVisibility] = useState(post?.visibility || "public"); const [editVisibility, setEditVisibility] = useState(post?.visibility || "public");
@ -585,6 +587,9 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
const config = getCategoryConfig(post?.category); const config = getCategoryConfig(post?.category);
const author = post?.author || post?.username || post?.source_name || t("common.anon"); const author = post?.author || post?.username || post?.source_name || t("common.anon");
// Merged post data: prefer fullPost (fetched from API) over initial post prop
const postData = useMemo(() => fullPost ?? post, [fullPost, post]);
// Media gallery - dedupe by base filename (ignore size variants like _large, _medium, _small, _l, _m, _s) // Media gallery - dedupe by base filename (ignore size variants like _large, _medium, _small, _l, _m, _s)
// Note: media_urls is for videos only, image_urls is for images // Note: media_urls is for videos only, image_urls is for images
const gallery = useMemo(() => { const gallery = useMemo(() => {
@ -592,9 +597,11 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
const seenBase = new Set(); const seenBase = new Set();
const images = []; const images = [];
const getBase = (url) => { const getBase = (url) => {
// Extract just the filename, remove path and domain
const filename = url.split('/').pop() || url;
// Remove size suffixes to detect same image in different sizes // Remove size suffixes to detect same image in different sizes
// Handles both long (_large, _medium, _small) and short (_l, _m, _s) variants // Handles both long (_large, _medium, _small) and short (_l, _m, _s) variants
return url.replace(/[-_](large|medium|small|thumb|thumbnail|l|m|s)(\.[^.]+)?$/i, '$2') return filename.replace(/[-_](large|medium|small|thumb|thumbnail|l|m|s)(\.[^.]+)?$/i, '$2')
.replace(/\?.*$/, ''); // Remove query params .replace(/\?.*$/, ''); // Remove query params
}; };
const add = (url) => { const add = (url) => {
@ -608,11 +615,11 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
images.push(n); images.push(n);
}; };
// Add from image_urls array first (user-uploaded galleries) - NOT media_urls (that's for videos) // Add from image_urls array first (user-uploaded galleries) - NOT media_urls (that's for videos)
post?.image_urls?.forEach(add); postData?.image_urls?.forEach(add);
// Then single images (prefer large > medium > small) // Then single images (prefer large > medium > small)
[post?.image_large, post?.image_medium, post?.image_small, post?.image_url, post?.image].forEach(add); [postData?.image_large, postData?.image_medium, postData?.image_small, postData?.image_url, postData?.image].forEach(add);
return images; return images;
}, [post]); }, [postData]);
// Initial data fetch // Initial data fetch
useEffect(() => { useEffect(() => {
@ -624,6 +631,14 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
fetchPostTruth(postId).then(res => { if (res) setTruth({ score: res.truth_score ?? res.user_score ?? 50, count: res.vote_count ?? 0 }); }); fetchPostTruth(postId).then(res => { if (res) setTruth({ score: res.truth_score ?? res.user_score ?? 50, count: res.vote_count ?? 0 }); });
}, [postId]); }, [postId]);
// Fetch full post data from API to get image_urls (map markers don't have it)
useEffect(() => {
if (!postId) return;
fetchPostById(postId).then(fresh => {
if (fresh) setFullPost(fresh);
});
}, [postId]);
// Fetch author avatar // Fetch author avatar
useEffect(() => { useEffect(() => {
const authorName = post?.author || post?.username; const authorName = post?.author || post?.username;