mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
fix(artists): restore infinite scroll after initial library load (#709)
* fix(artists): attach infinite-scroll observer when sentinel mounts The Artists page only renders the bottom sentinel after getArtists finishes. The hook subscribed in an effect keyed on loadMore; unlike Albums, that callback does not depend on loading, so the observer never attached after the first paint. Use a callback ref and observe against the main scroll viewport (#app-main-scroll-viewport). * docs: changelog for Artists infinite scroll fix (PR #709)
This commit is contained in:
@@ -284,6 +284,12 @@ Foundational work: faster reviews, narrower diffs, and a safety net under the pa
|
|||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
|
### Artists — infinite scroll after first page
|
||||||
|
|
||||||
|
**By [@cucadmuh](https://github.com/cucadmuh), PR [#709](https://github.com/Psychotoxical/psysonic/pull/709)**
|
||||||
|
|
||||||
|
* The **Artists** page only mounts the bottom **`IntersectionObserver`** sentinel after **`getArtists`** finishes, while **`useArtistsInfiniteScroll`** subscribed in an effect keyed only on **`loadMore`**. Unlike **Albums**, that callback does not depend on **`loading`**, so its identity did not change when the sentinel first appeared — the observer never attached and **`visibleCount`** never grew past the first batch. The hook now uses a **callback ref** (subscribe on mount, disconnect on unmount) and sets **`root`** to **`#app-main-scroll-viewport`** so intersection matches the real scroll container.
|
||||||
|
|
||||||
### Playback — track no longer clipped at the end with gapless and crossfade off
|
### Playback — track no longer clipped at the end with gapless and crossfade off
|
||||||
|
|
||||||
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#708](https://github.com/Psychotoxical/psysonic/pull/708)**
|
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#708](https://github.com/Psychotoxical/psysonic/pull/708)**
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
|
||||||
|
|
||||||
interface UseArtistsInfiniteScrollArgs {
|
interface UseArtistsInfiniteScrollArgs {
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
@@ -8,7 +9,8 @@ interface UseArtistsInfiniteScrollArgs {
|
|||||||
interface UseArtistsInfiniteScrollResult {
|
interface UseArtistsInfiniteScrollResult {
|
||||||
visibleCount: number;
|
visibleCount: number;
|
||||||
loadingMore: boolean;
|
loadingMore: boolean;
|
||||||
observerTarget: React.RefObject<HTMLDivElement | null>;
|
/** Callback ref — attaches IntersectionObserver when the sentinel mounts (fixes first paint behind `loading`). */
|
||||||
|
observerTarget: React.RefCallback<HTMLDivElement | null>;
|
||||||
loadMore: () => void;
|
loadMore: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +27,7 @@ interface UseArtistsInfiniteScrollResult {
|
|||||||
* The observer doesn't take a `hasMore` flag — the page only renders
|
* The observer doesn't take a `hasMore` flag — the page only renders
|
||||||
* the sentinel `<div ref={observerTarget}>` while there is more data,
|
* the sentinel `<div ref={observerTarget}>` while there is more data,
|
||||||
* so the observer naturally disconnects when the last page is reached
|
* so the observer naturally disconnects when the last page is reached
|
||||||
* (the cleanup runs as the sentinel unmounts).
|
* (callback ref runs with `node === null` as the sentinel unmounts).
|
||||||
*/
|
*/
|
||||||
export function useArtistsInfiniteScroll({
|
export function useArtistsInfiniteScroll({
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -33,7 +35,8 @@ export function useArtistsInfiniteScroll({
|
|||||||
}: UseArtistsInfiniteScrollArgs): UseArtistsInfiniteScrollResult {
|
}: UseArtistsInfiniteScrollArgs): UseArtistsInfiniteScrollResult {
|
||||||
const [visibleCount, setVisibleCount] = useState(pageSize);
|
const [visibleCount, setVisibleCount] = useState(pageSize);
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
const observerTarget = useRef<HTMLDivElement>(null);
|
const loadMoreRef = useRef<() => void>(() => {});
|
||||||
|
const observerInst = useRef<IntersectionObserver | null>(null);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
if (loadingMore) return;
|
if (loadingMore) return;
|
||||||
@@ -42,20 +45,37 @@ export function useArtistsInfiniteScroll({
|
|||||||
setTimeout(() => setLoadingMore(false), 100);
|
setTimeout(() => setLoadingMore(false), 100);
|
||||||
}, [loadingMore, pageSize]);
|
}, [loadingMore, pageSize]);
|
||||||
|
|
||||||
|
loadMoreRef.current = loadMore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVisibleCount(pageSize);
|
setVisibleCount(pageSize);
|
||||||
// resetDeps is intentionally spread into the dep array.
|
// resetDeps is intentionally spread into the dep array.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [pageSize, ...resetDeps]);
|
}, [pageSize, ...resetDeps]);
|
||||||
|
|
||||||
useEffect(() => {
|
const observerTarget = useCallback((node: HTMLDivElement | null) => {
|
||||||
|
observerInst.current?.disconnect();
|
||||||
|
observerInst.current = null;
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
const rootEl = document.getElementById(APP_MAIN_SCROLL_VIEWPORT_ID);
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
entries => { if (entries[0].isIntersecting) loadMore(); },
|
entries => {
|
||||||
{ rootMargin: '200px' },
|
if (entries[0]?.isIntersecting) loadMoreRef.current();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: rootEl instanceof HTMLElement ? rootEl : null,
|
||||||
|
rootMargin: '200px',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (observerTarget.current) observer.observe(observerTarget.current);
|
observer.observe(node);
|
||||||
return () => observer.disconnect();
|
observerInst.current = observer;
|
||||||
}, [loadMore]);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
observerInst.current?.disconnect();
|
||||||
|
observerInst.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return { visibleCount, loadingMore, observerTarget, loadMore };
|
return { visibleCount, loadingMore, observerTarget, loadMore };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user