fix: content filter triggers immediate refetch on change
- Fix race condition between content filter effect and general filter effect - Map content filters to proper content_type values for backend - Links → link, Image → image/picture/photo, Text → post/system/news_enriched - Live → live/camera/stream, Video → video - Ensure forceFetch fires moveend to trigger refetch Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3a469dfa8d
commit
c50c61ff36
|
|
@ -64,6 +64,11 @@ function invalidateCacheMatching(pattern) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate all smart-feed cache entries (called when filter changes)
|
||||||
|
export function invalidateSmartFeedCache() {
|
||||||
|
invalidateCacheMatching("smart-feed");
|
||||||
|
}
|
||||||
|
|
||||||
function cloneJson(value) {
|
function cloneJson(value) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(JSON.stringify(value));
|
return JSON.parse(JSON.stringify(value));
|
||||||
|
|
@ -513,10 +518,12 @@ export async function fetchSmartFeed(params = {}, { signal } = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${apiBase()}/smart-feed?${qs.toString()}`;
|
const url = `${apiBase()}/smart-feed?${qs.toString()}`;
|
||||||
|
console.log('[fetchSmartFeed] URL:', url);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchJsonCached(url, { ttlMs: 10000, signal });
|
const data = await fetchJsonCached(url, { ttlMs: 10000, signal });
|
||||||
const items = Array.isArray(data) ? data : data.results || data.items || [];
|
const items = Array.isArray(data) ? data : data.results || data.items || [];
|
||||||
|
console.log('[fetchSmartFeed] Got', items.length, 'posts, content_types:', [...new Set(items.map(p => p.content_type))]);
|
||||||
return await resolveAssetRefsInPosts(items);
|
return await resolveAssetRefsInPosts(items);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.name === "AbortError") return []; // silently ignore aborted requests
|
if (err.name === "AbortError") return []; // silently ignore aborted requests
|
||||||
|
|
|
||||||
|
|
@ -43,30 +43,29 @@ function isUnknownType(ct) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export filter matching function for reuse
|
// Export filter matching function for reuse
|
||||||
|
// Filters by content_type field
|
||||||
export function matchesContentFilter(post, filter) {
|
export function matchesContentFilter(post, filter) {
|
||||||
if (!filter || filter === "all") return true;
|
if (!filter || filter === "all") return true;
|
||||||
const ct = (post.content_type || post.contentType || "").toLowerCase();
|
const ct = (post.content_type || post.contentType || "").toLowerCase();
|
||||||
const cat = (post.category || "").toLowerCase();
|
|
||||||
const hasVideo = !!(post.video_url || post.videoUrl || post.embed_url || post.embedUrl);
|
|
||||||
const hasImage = !!(post.image_small || post.image_medium || post.image_large || (post.media_urls && post.media_urls.length > 0));
|
|
||||||
|
|
||||||
switch (filter) {
|
switch (filter) {
|
||||||
case "links":
|
case "links":
|
||||||
// News links + unknown types with news category (e.g., systems, news-scrapper)
|
// Link posts (content_type = "link")
|
||||||
return cat === "news" || ct === "news" || (isUnknownType(ct) && cat === "news");
|
return ct === "link";
|
||||||
case "video":
|
case "video":
|
||||||
// Recorded videos only
|
// Video posts (content_type = "video")
|
||||||
return ct === "video";
|
return ct === "video";
|
||||||
case "image":
|
case "image":
|
||||||
return hasImage && !hasVideo && ct !== "camera" && ct !== "video";
|
// Image posts (content_type = "image", "picture", "photo")
|
||||||
|
return ct === "image" || ct === "picture" || ct === "photo";
|
||||||
case "text":
|
case "text":
|
||||||
// Text posts + news + unknown types default to text (systems, news-scrapper, etc.)
|
// Text posts: post, system, news_enriched, text, blog, or empty/null
|
||||||
return ct === "post" || ct === "text" || ct === "blog" || ct === "news" || cat === "friends" || cat === "news" || isUnknownType(ct);
|
return ct === "post" || ct === "system" || ct === "news_enriched" || ct === "text" || ct === "blog" || ct === "";
|
||||||
case "live":
|
case "live":
|
||||||
// Only live content: cameras (quebec511), live streams - NOT recorded videos
|
// Live content: cameras, live streams (content_type = "camera", "live", "stream")
|
||||||
return ct === "live" || ct === "stream" || ct === "camera";
|
return ct === "live" || ct === "stream" || ct === "camera";
|
||||||
case "cluster":
|
case "cluster":
|
||||||
// Clusters filter shows all posts - the cluster zones overlay them
|
// Clusters filter shows all posts
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ export function clusterPosts(posts, clusterRadius = 0.001) {
|
||||||
export function prioritizePosts(posts, viewCenter, options = {}) {
|
export function prioritizePosts(posts, viewCenter, options = {}) {
|
||||||
const {
|
const {
|
||||||
maxFullCards = 15, // Max de cartes complètes à afficher (fallback)
|
maxFullCards = 15, // Max de cartes complètes à afficher (fallback)
|
||||||
|
maxSimpleMarkers = 50, // Max de marqueurs simples (pins)
|
||||||
clusterRadius = 0.001, // Rayon de clustering (~100m)
|
clusterRadius = 0.001, // Rayon de clustering (~100m)
|
||||||
minScoreForCard = 40, // Score minimum pour carte complète (fallback)
|
minScoreForCard = 40, // Score minimum pour carte complète (fallback)
|
||||||
} = options;
|
} = options;
|
||||||
|
|
@ -158,9 +159,12 @@ export function prioritizePosts(posts, viewCenter, options = {}) {
|
||||||
|
|
||||||
// If backend provided tiers, use them directly
|
// If backend provided tiers, use them directly
|
||||||
if (hasTiers && fullCardPosts.length > 0) {
|
if (hasTiers && fullCardPosts.length > 0) {
|
||||||
|
// Limit premium cards to maxFullCards and simple markers to maxSimpleMarkers
|
||||||
|
const limitedFull = fullCardPosts.slice(0, maxFullCards);
|
||||||
|
const limitedSimple = simpleMarkerPosts.slice(0, maxSimpleMarkers);
|
||||||
return {
|
return {
|
||||||
fullCardPosts,
|
fullCardPosts: limitedFull,
|
||||||
simpleMarkerPosts,
|
simpleMarkerPosts: limitedSimple,
|
||||||
clusters: new Map(),
|
clusters: new Map(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -232,9 +236,13 @@ export function prioritizePosts(posts, viewCenter, options = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Limit simple markers to maxSimpleMarkers (default 50)
|
||||||
|
// Sort by priority to keep best ones
|
||||||
|
const limitedSimple = finalSimple.slice(0, maxSimpleMarkers);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fullCardPosts: finalFull,
|
fullCardPosts: finalFull,
|
||||||
simpleMarkerPosts: finalSimple,
|
simpleMarkerPosts: limitedSimple,
|
||||||
clusters,
|
clusters,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { fetchPosts, fetchSmartFeed, fetchUnifiedSearch, resolveAssetRef } from "../../api/client";
|
import { fetchPosts, fetchSmartFeed, fetchUnifiedSearch, resolveAssetRef, invalidateSmartFeedCache } from "../../api/client";
|
||||||
import { categoryCode, matchesSubFilter, matchesTimeFilter, effectivePostTime } from "./mapFilter";
|
import { categoryCode, matchesSubFilter, matchesTimeFilter, effectivePostTime } from "./mapFilter";
|
||||||
import { getCoords, getViewFromMap, haversineKm } from "./mapGeo";
|
import { getCoords, getViewFromMap, haversineKm } from "./mapGeo";
|
||||||
import { createMarkerForPost, createSimpleMarkerForPost } from "./markerManager";
|
import { createMarkerForPost, createSimpleMarkerForPost } from "./markerManager";
|
||||||
|
|
@ -409,6 +409,7 @@ export function usePostsEngine({
|
||||||
const replacePoolRef = useRef(false);
|
const replacePoolRef = useRef(false);
|
||||||
const retryRef = useRef({ key: "", count: 0, ts: 0 });
|
const retryRef = useRef({ key: "", count: 0, ts: 0 });
|
||||||
const abortControllerRef = useRef(null);
|
const abortControllerRef = useRef(null);
|
||||||
|
const prevContentFilterRef = useRef(contentFilter);
|
||||||
const [mapReady, setMapReady] = useState(false);
|
const [mapReady, setMapReady] = useState(false);
|
||||||
const clusterModeRef = useRef(false);
|
const clusterModeRef = useRef(false);
|
||||||
|
|
||||||
|
|
@ -452,8 +453,38 @@ export function usePostsEngine({
|
||||||
}, 120);
|
}, 120);
|
||||||
}, [mapReady, mapRef]);
|
}, [mapReady, mapRef]);
|
||||||
|
|
||||||
|
// CRITICAL: Dedicated effect for contentFilter changes (media filter buttons)
|
||||||
|
// This triggers a forceFetch via moveend when user clicks live/video/image/text/links
|
||||||
|
useEffect(() => {
|
||||||
|
const prev = prevContentFilterRef.current;
|
||||||
|
if (prev === contentFilter) return; // No change
|
||||||
|
|
||||||
|
console.log('[usePostsEngine] CONTENT FILTER CHANGED:', prev, '->', contentFilter);
|
||||||
|
prevContentFilterRef.current = contentFilter;
|
||||||
|
|
||||||
|
// Clear cache and force refetch
|
||||||
|
invalidateSmartFeedCache();
|
||||||
|
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
// Clear all posts and trigger refetch
|
||||||
|
allPostsRef.current = [];
|
||||||
|
replacePoolRef.current = true;
|
||||||
|
|
||||||
|
// Set forceFetch flag and trigger moveend to refetch with new content filter
|
||||||
|
map.__swForceFetchAt = Date.now();
|
||||||
|
console.log('[usePostsEngine] CONTENT FILTER: triggering forceFetch via moveend');
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
map.fire("moveend");
|
||||||
|
} catch {}
|
||||||
|
}, 50);
|
||||||
|
}, [contentFilter, mapRef]);
|
||||||
|
|
||||||
const priorityOpts = {
|
const priorityOpts = {
|
||||||
maxFullCards: 15,
|
maxFullCards: 15,
|
||||||
|
maxSimpleMarkers: 50, // Limit simple pins to 50
|
||||||
clusterRadius: 0.002,
|
clusterRadius: 0.002,
|
||||||
minScoreForCard: 50,
|
minScoreForCard: 50,
|
||||||
};
|
};
|
||||||
|
|
@ -754,9 +785,10 @@ export function usePostsEngine({
|
||||||
let fullCardPosts = [];
|
let fullCardPosts = [];
|
||||||
let simpleMarkerPosts = [];
|
let simpleMarkerPosts = [];
|
||||||
const prioritized = prioritizePosts(visible, viewCenter, {
|
const prioritized = prioritizePosts(visible, viewCenter, {
|
||||||
maxFullCards: 15, // 10-15 cartes complètes selon le zoom
|
maxFullCards: 15, // Exactly 15 premium cards
|
||||||
clusterRadius: 0.002, // ~200m clustering radius (augmenté pour plus de groupement)
|
maxSimpleMarkers: 50, // Exactly 50 simple pins
|
||||||
minScoreForCard: 50, // Score minimum pour carte complète (augmenté de 35 à 50)
|
clusterRadius: 0.002, // ~200m clustering radius
|
||||||
|
minScoreForCard: 50,
|
||||||
});
|
});
|
||||||
fullCardPosts = prioritized.fullCardPosts;
|
fullCardPosts = prioritized.fullCardPosts;
|
||||||
simpleMarkerPosts = prioritized.simpleMarkerPosts;
|
simpleMarkerPosts = prioritized.simpleMarkerPosts;
|
||||||
|
|
@ -845,10 +877,14 @@ export function usePostsEngine({
|
||||||
[mapRef, markersRef, expandedElRef, theme, searchQuery, forceFullPostId, weatherEnabled]
|
[mapRef, markersRef, expandedElRef, theme, searchQuery, forceFullPostId, weatherEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle mainFilter/subFilter/timeHours changes (NOT contentFilter - that has its own dedicated effect above)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('[usePostsEngine] CATEGORY/TIME FILTER CHANGED: mainFilter=', mainFilter, 'subFilter=', subFilter, 'timeHours=', timeHours);
|
||||||
|
// Clear frontend cache to ensure fresh data for new filter
|
||||||
|
invalidateSmartFeedCache();
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
replacePoolRef.current = true;
|
replacePoolRef.current = true;
|
||||||
pendingFilterRef.current = `${mainFilter}|${subFilter}|${contentFilter}|${timeHours}`;
|
pendingFilterRef.current = `${mainFilter}|${subFilter}|${timeHours}`;
|
||||||
allPostsRef.current = [];
|
allPostsRef.current = [];
|
||||||
autoWidenRef.current = true; // Prevent auto-widen when user manually changes filter (keep their time choice)
|
autoWidenRef.current = true; // Prevent auto-widen when user manually changes filter (keep their time choice)
|
||||||
if (map && clustersEnabled && heatmapEnabled) {
|
if (map && clustersEnabled && heatmapEnabled) {
|
||||||
|
|
@ -856,13 +892,16 @@ export function usePostsEngine({
|
||||||
}
|
}
|
||||||
if (map) {
|
if (map) {
|
||||||
map.__swForceFetchAt = Date.now();
|
map.__swForceFetchAt = Date.now();
|
||||||
|
console.log('[usePostsEngine] CATEGORY/TIME FILTER: triggering forceFetch');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
map.fire("moveend");
|
map.fire("moveend");
|
||||||
} catch {}
|
} catch {}
|
||||||
}, 60);
|
}, 60);
|
||||||
}
|
}
|
||||||
}, [mainFilter, subFilter, contentFilter, timeHours, mapRef, clustersEnabled, heatmapEnabled, updateClusterAreas]);
|
// NOTE: contentFilter is NOT in deps - its changes are handled by the dedicated effect at line ~456
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [mainFilter, subFilter, timeHours, mapRef, clustersEnabled, heatmapEnabled, updateClusterAreas]);
|
||||||
|
|
||||||
const computeVisible = useCallback(
|
const computeVisible = useCallback(
|
||||||
(tf) => {
|
(tf) => {
|
||||||
|
|
@ -1015,14 +1054,37 @@ export function usePostsEngine({
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
console.log('[usePostsEngine] load() called, contentFilter=', contentFilter);
|
||||||
const mapObj = mapRef.current;
|
const mapObj = mapRef.current;
|
||||||
const resolvedView =
|
|
||||||
viewParams && viewParams.center ? viewParams : getViewFromMap(mapObj);
|
|
||||||
if (!resolvedView?.center) return;
|
|
||||||
if ((searchQuery || "").trim()) return;
|
|
||||||
|
|
||||||
// If expanded overlay is open: delay fetch (do NOT disturb UI)
|
// Check if this is a forced fetch (filter changed)
|
||||||
if (expandedElRef?.current || mapObj?.__swCenteredOverlay) {
|
const forceFetchAt = mapObj?.__swForceFetchAt || 0;
|
||||||
|
const isFilterChange = forceFetchAt && Date.now() - forceFetchAt < 10000;
|
||||||
|
|
||||||
|
let resolvedView =
|
||||||
|
viewParams && viewParams.center ? viewParams : getViewFromMap(mapObj);
|
||||||
|
if (!resolvedView?.center) {
|
||||||
|
console.log('[usePostsEngine] SKIP: no view center, isFilterChange=', isFilterChange);
|
||||||
|
// If filter changed but no view, try to get view from map directly
|
||||||
|
if (isFilterChange && mapObj) {
|
||||||
|
try {
|
||||||
|
const center = mapObj.getCenter();
|
||||||
|
const zoom = mapObj.getZoom();
|
||||||
|
if (center && zoom) {
|
||||||
|
resolvedView = { center: [center.lng, center.lat], radiusKm: 100 };
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
if (!resolvedView?.center) return;
|
||||||
|
}
|
||||||
|
if ((searchQuery || "").trim()) {
|
||||||
|
console.log('[usePostsEngine] SKIP: searchQuery active:', searchQuery);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If expanded overlay is open: delay fetch (do NOT disturb UI) - but NOT if filter changed
|
||||||
|
if (!isFilterChange && (expandedElRef?.current || mapObj?.__swCenteredOverlay)) {
|
||||||
|
console.log('[usePostsEngine] DELAY: overlay open');
|
||||||
if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
|
if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
|
||||||
delayedFetchRef.current = setTimeout(load, 1200);
|
delayedFetchRef.current = setTimeout(load, 1200);
|
||||||
return;
|
return;
|
||||||
|
|
@ -1063,7 +1125,7 @@ export function usePostsEngine({
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const last = lastFetchRef.current;
|
const last = lastFetchRef.current;
|
||||||
const forceFetchAt = mapObj?.__swForceFetchAt || 0;
|
// forceFetchAt already declared at top of load() for isFilterChange check
|
||||||
const forceFetch = forceFetchAt && Date.now() - forceFetchAt < 15000;
|
const forceFetch = forceFetchAt && Date.now() - forceFetchAt < 15000;
|
||||||
if (forceFetch) {
|
if (forceFetch) {
|
||||||
mapObj.__swForceFetchAt = 0;
|
mapObj.__swForceFetchAt = 0;
|
||||||
|
|
@ -1092,7 +1154,10 @@ export function usePostsEngine({
|
||||||
// Cap minMoveKm at 10km so even at large zoom levels we refetch reasonably
|
// Cap minMoveKm at 10km so even at large zoom levels we refetch reasonably
|
||||||
const minMoveKm = Math.min(10, Math.max(2, radiusKm * 0.03));
|
const minMoveKm = Math.min(10, Math.max(2, radiusKm * 0.03));
|
||||||
|
|
||||||
if (!radiusChanged && !boundsChanged && distKm < minMoveKm) return;
|
if (!radiusChanged && !boundsChanged && distKm < minMoveKm) {
|
||||||
|
console.log('[usePostsEngine] SKIP: no significant movement', { radiusChanged, boundsChanged, distKm, minMoveKm });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Clear pool if moved significantly (>50% of radius or >100km)
|
// Clear pool if moved significantly (>50% of radius or >100km)
|
||||||
if (distKm > Math.max(50, radiusKm * 0.5)) {
|
if (distKm > Math.max(50, radiusKm * 0.5)) {
|
||||||
|
|
@ -1112,27 +1177,29 @@ export function usePostsEngine({
|
||||||
subFilter && subFilter.toUpperCase() !== "ALL" && subFilter !== "All" ? subFilter : "";
|
subFilter && subFilter.toUpperCase() !== "ALL" && subFilter !== "All" ? subFilter : "";
|
||||||
|
|
||||||
// Use smart feed for personalized, trending posts
|
// Use smart feed for personalized, trending posts
|
||||||
|
// Always fetch at least 80 posts to fill 15 premium + 50 pins = 65 minimum
|
||||||
const zoom = typeof mapObj?.getZoom === "function" ? mapObj.getZoom() : undefined;
|
const zoom = typeof mapObj?.getZoom === "function" ? mapObj.getZoom() : undefined;
|
||||||
let limit = 50;
|
let limit = 80; // Minimum 80 to ensure enough posts for 15 premium + 50 pins
|
||||||
if (typeof zoom === "number") {
|
if (typeof zoom === "number") {
|
||||||
if (zoom <= 6) limit = 90;
|
if (zoom <= 6) limit = 100;
|
||||||
else if (zoom <= 8) limit = 70;
|
else if (zoom <= 8) limit = 90;
|
||||||
else if (zoom <= 10) limit = 60;
|
else if (zoom <= 10) limit = 80;
|
||||||
}
|
}
|
||||||
if (radiusKm > 800) {
|
if (radiusKm > 800) {
|
||||||
limit = Math.max(limit, 80);
|
limit = Math.max(limit, 100);
|
||||||
}
|
}
|
||||||
if (limit > 120) limit = 120;
|
if (limit > 120) limit = 120;
|
||||||
|
|
||||||
// Map contentFilter to content_type for API
|
// Map contentFilter to content_type for API - ALL filters go to backend
|
||||||
let contentTypeParam = undefined;
|
let contentTypeParam = undefined;
|
||||||
if (contentFilter && contentFilter !== "all" && contentFilter !== "cluster") {
|
if (contentFilter && contentFilter !== "all" && contentFilter !== "cluster") {
|
||||||
if (contentFilter === "live") contentTypeParam = "live,camera,stream";
|
if (contentFilter === "live") contentTypeParam = "live,camera,stream";
|
||||||
else if (contentFilter === "video") contentTypeParam = "video";
|
else if (contentFilter === "video") contentTypeParam = "video";
|
||||||
else if (contentFilter === "image") contentTypeParam = "image";
|
else if (contentFilter === "text") contentTypeParam = "post,system,news_enriched,text,blog";
|
||||||
else if (contentFilter === "text") contentTypeParam = "post";
|
else if (contentFilter === "image") contentTypeParam = "image,picture,photo";
|
||||||
else if (contentFilter === "links") contentTypeParam = "news";
|
else if (contentFilter === "links") contentTypeParam = "link";
|
||||||
}
|
}
|
||||||
|
console.log('[usePostsEngine] FETCH: contentFilter=', contentFilter, 'contentTypeParam=', contentTypeParam);
|
||||||
// For "cluster" filter, fetch all posts and filter client-side by clusterPostIds
|
// For "cluster" filter, fetch all posts and filter client-side by clusterPostIds
|
||||||
|
|
||||||
// Abort any previous in-flight request
|
// Abort any previous in-flight request
|
||||||
|
|
@ -1246,9 +1313,14 @@ export function usePostsEngine({
|
||||||
clearTimeout(moveDebounceRef.current);
|
clearTimeout(moveDebounceRef.current);
|
||||||
moveDebounceRef.current = null;
|
moveDebounceRef.current = null;
|
||||||
}
|
}
|
||||||
if (initialLoadRef.current) {
|
// Check if filter changed (forceFetch flag set)
|
||||||
|
const mapObj = mapRef.current;
|
||||||
|
const forceFetchNow = mapObj?.__swForceFetchAt && Date.now() - mapObj.__swForceFetchAt < 5000;
|
||||||
|
|
||||||
|
if (initialLoadRef.current || forceFetchNow) {
|
||||||
initialLoadRef.current = false;
|
initialLoadRef.current = false;
|
||||||
load();
|
// Note: flag is cleared inside load() at line ~1095, not here
|
||||||
|
load(); // Immediate fetch for initial load or filter change
|
||||||
} else {
|
} else {
|
||||||
moveDebounceRef.current = setTimeout(load, 300);
|
moveDebounceRef.current = setTimeout(load, 300);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -705,9 +705,13 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
|
||||||
|
|
||||||
const handleTruthVote = useCallback(async (vote) => {
|
const handleTruthVote = useCallback(async (vote) => {
|
||||||
if (!postId) return;
|
if (!postId) return;
|
||||||
|
if (!token) {
|
||||||
|
window.dispatchEvent(new CustomEvent("sociowire:auth-required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const res = await votePostTruth(postId, vote);
|
const res = await votePostTruth(postId, vote);
|
||||||
if (res) setTruth({ score: res.truth_score ?? res.user_score ?? truth.score, count: res.vote_count ?? truth.count });
|
if (res) setTruth({ score: res.truth_score ?? res.user_score ?? truth.score, count: res.vote_count ?? truth.count });
|
||||||
}, [postId, truth]);
|
}, [postId, truth, token]);
|
||||||
|
|
||||||
const handleComment = useCallback(async () => {
|
const handleComment = useCallback(async () => {
|
||||||
const body = commentInput.trim();
|
const body = commentInput.trim();
|
||||||
|
|
|
||||||
|
|
@ -1251,25 +1251,25 @@ body[data-theme="light"] .sw-modal-x{
|
||||||
|
|
||||||
.post-pin.sw-appear{
|
.post-pin.sw-appear{
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 640ms ease;
|
transition: opacity 800ms ease-out;
|
||||||
}
|
}
|
||||||
.post-pin.sw-appear.sw-appear-in{
|
.post-pin.sw-appear.sw-appear-in{
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.post-pin.sw-disappear{
|
.post-pin.sw-disappear{
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 640ms ease;
|
transition: opacity 700ms ease-out;
|
||||||
}
|
}
|
||||||
.post-pin--simple.sw-appear{
|
.post-pin--simple.sw-appear{
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 520ms ease;
|
transition: opacity 650ms ease-out;
|
||||||
}
|
}
|
||||||
.post-pin--simple.sw-appear.sw-appear-in{
|
.post-pin--simple.sw-appear.sw-appear-in{
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.post-pin--simple.sw-disappear{
|
.post-pin--simple.sw-disappear{
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 520ms ease;
|
transition: opacity 600ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cluster main news layout */
|
/* Cluster main news layout */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue