diff --git a/src/components/BackToTop.tsx b/src/components/BackToTop.tsx index 26e4475..b8b4656 100644 --- a/src/components/BackToTop.tsx +++ b/src/components/BackToTop.tsx @@ -31,7 +31,7 @@ export function BackToTop() { exit={{ opacity: 0, scale: 0.8, y: 8 }} transition={{ type: "spring", stiffness: 380, damping: 26 }} onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })} - className="fixed bottom-[94px] right-4 z-30 flex h-11 w-11 items-center justify-center rounded-full bg-ark-gold text-black shadow-lg shadow-black/40 outline-none transition hover:bg-ark-gold2 active:scale-95 focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg md:bottom-8 md:right-8" + className="fixed bottom-[calc(84px+max(env(safe-area-inset-bottom),0px))] right-4 z-30 flex h-11 w-11 items-center justify-center rounded-full bg-ark-gold text-black shadow-lg shadow-black/40 outline-none transition hover:bg-ark-gold2 active:scale-95 focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg md:bottom-8 md:right-8" aria-label={t("backToTop")} title={t("backToTop")} > diff --git a/src/components/messageStream/hooks/usePostStream.ts b/src/components/messageStream/hooks/usePostStream.ts index 5a12617..491e963 100644 --- a/src/components/messageStream/hooks/usePostStream.ts +++ b/src/components/messageStream/hooks/usePostStream.ts @@ -112,6 +112,17 @@ function streamKey(params: PostStreamParams): string { return buildRealUrl(params); } +function cacheFirstPage( + params: PostStreamParams, + page: PostListResponse, +): void { + streamCache.set(streamKey(params), { + items: itemsOrEmpty(page.items), + cursor: page.nextCursor, + hasMore: !!page.nextCursor, + }); +} + /** * Warm the cache for a stream view before the user navigates to it, so opening * the page shows content immediately instead of starting to load on arrival. @@ -119,20 +130,31 @@ function streamKey(params: PostStreamParams): string { */ export function prefetchPostStream(params: PostStreamParams): void { if (USE_MOCK) return; + const key = streamKey(params); + if (streamCache.has(key)) return; + const url = buildRealUrl(params); - if (readJSONCache(url)) return; - getJSON(url).catch(() => {}); + const cachedPage = readJSONCache(url); + if (cachedPage) { + cacheFirstPage(params, cachedPage); + return; + } + + getJSON(url) + .then((page) => cacheFirstPage(params, page)) + .catch(() => {}); } export function usePostStream(params: PostStreamParams): PostStreamResult { - const [items, setItems] = useState([]); - const [hasMore, setHasMore] = useState(true); - const [isLoading, setIsLoading] = useState(false); + const initialCached = streamCache.get(streamKey(params)); + const [items, setItems] = useState(() => initialCached?.items ?? []); + const [hasMore, setHasMore] = useState(() => initialCached?.hasMore ?? true); + const [isLoading, setIsLoading] = useState(() => !initialCached); const [error, setError] = useState(null); const reqIdRef = useRef(0); - const cursorRef = useRef(undefined); - const hasMoreRef = useRef(true); + const cursorRef = useRef(initialCached?.cursor); + const hasMoreRef = useRef(initialCached?.hasMore ?? true); const loadingRef = useRef(false); const fetchPage = useCallback( diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx index 748fbfd..e3210d2 100644 --- a/src/layouts/PublicLayout.tsx +++ b/src/layouts/PublicLayout.tsx @@ -411,7 +411,7 @@ export function PublicLayout() { }, [mobileSearchOpen]); return ( -
+
@@ -694,7 +694,7 @@ export function PublicLayout() { ) : null}
-