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:
parent
3a29812319
commit
91e024e156
|
|
@ -789,11 +789,14 @@ 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
|
||||||
.replace(/\?.*$/, ''); // Remove query params
|
const filename = url.split('/').pop() || url;
|
||||||
|
return filename.replace(/[-_](large|medium|small|thumb|thumbnail|l|m|s)(\.[^.]+)?$/i, '$2')
|
||||||
|
.replace(/\?.*$/, ''); // Remove query params
|
||||||
};
|
};
|
||||||
const add = (value) => {
|
const add = (value) => {
|
||||||
const normalized = normalizeImageUrl(value);
|
const normalized = normalizeImageUrl(value);
|
||||||
|
|
|
||||||
|
|
@ -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,10 +597,12 @@ 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) => {
|
||||||
const n = normalizeMediaUrl(url);
|
const n = normalizeMediaUrl(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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue