From aa801a57ee61fb52a544ed77fdfddfe4c9843ffe Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Thu, 18 May 2023 13:48:01 +0200 Subject: [PATCH] Feature/updates screen use infinite swr hook (#303) * Use "RequestManager" for loading updates * [Cleanup] Fix typing * Memoize grouped updates * [TSConfig] Increase target version Makes it possible to iterate over a "IterableIterator" (e.g. [....entries()]) * Use "GroupedVirtuoso" for "Updates" screen * Optionally use "padding" instead of margin for the loading placeholder In case the loading placeholder gets used in a "react-virtuoso" list, the margin might cause issues with the calculated height. * Use "padding" instead of "margin" "Margin" breaks "react-virtuosos" height calculation * Add background color to group header With virtuoso the group headers are sticky, thus, a background color is needed * Simplify "groupByDate" object structure It's not necessary to save the items inside the group object --- src/components/util/LoadingPlaceholder.tsx | 8 +- src/screens/Updates.tsx | 272 +++++++++++---------- tsconfig.json | 2 +- 3 files changed, 150 insertions(+), 132 deletions(-) diff --git a/src/components/util/LoadingPlaceholder.tsx b/src/components/util/LoadingPlaceholder.tsx index d615ddb9..84e365ba 100644 --- a/src/components/util/LoadingPlaceholder.tsx +++ b/src/components/util/LoadingPlaceholder.tsx @@ -15,10 +15,11 @@ interface IProps { children?: React.ReactNode; component?: string | React.FunctionComponent | React.ComponentClass; componentProps?: any; + usePadding?: boolean; } export default function LoadingPlaceholder(props: IProps) { - const { children, shouldRender, component, componentProps } = props; + const { children, shouldRender, component, componentProps, usePadding } = props; let condition = true; if (shouldRender !== undefined) { @@ -38,7 +39,10 @@ export default function LoadingPlaceholder(props: IProps) { return ( ({ + // 64px header + height: 'calc(100vh - 64px)', + [theme.breakpoints.down('sm')]: { + // 64px header (margin); 64px menu (margin); + height: 'calc(100vh - 64px - 64px)', + }, +})); + +const StyledGroupHeader = styled(Typography, { shouldForwardProp: (prop) => prop !== 'isFirstItem' })<{ + isFirstItem: boolean; +}>(({ theme, isFirstItem }) => ({ + paddingLeft: '24px', + // 16px - 10px (bottom padding of the group items) + paddingTop: '6px', + paddingBottom: '16px', + fontWeight: 700, + textTransform: 'uppercase', + backgroundColor: theme.palette.background.default, + [theme.breakpoints.down('sm')]: { + // 16px - 8px (margin of header) + paddingTop: isFirstItem ? '8px' : '6px', + }, +})); + +const StyledGroupItemWrapper = styled(Box, { shouldForwardProp: (prop) => prop !== 'isLastItem' })<{ + isLastItem: boolean; +}>(({ isLastItem }) => ({ + padding: '0 10px', + paddingBottom: isLastItem ? '0' : '10px', +})); function epochToDate(epoch: number) { const date = new Date(0); // The 0 there is the key, which sets the date to the epoch @@ -49,21 +82,19 @@ function getDateString(date: Date) { return date.toLocaleDateString(); } -function groupByDate(updates: IMangaChapter[]): [string, { item: IMangaChapter; globalIdx: number }[]][] { - if (updates.length === 0) return []; +const groupByDate = (updates: IMangaChapter[]): [date: string, items: number][] => { + if (!updates.length) { + return []; + } - const groups = {}; - updates.forEach((item, globalIdx) => { - const key = getDateString(epochToDate(item.chapter.fetchedAt)); - // @ts-ignore - if (groups[key] === undefined) groups[key] = []; - // @ts-ignore - groups[key].push({ item, globalIdx }); + const dateToItemMap = new Map(); + updates.forEach((item) => { + const date = getDateString(epochToDate(item.chapter.fetchedAt)); + dateToItemMap.set(date, (dateToItemMap.get(date) ?? 0) + 1); }); - // @ts-ignore - return Object.keys(groups).map((key) => [key, groups[key]]); -} + return [...dateToItemMap.entries()]; +}; const initialQueue = { status: 'Stopped', @@ -75,10 +106,19 @@ const Updates: React.FC = () => { const history = useHistory(); const { setTitle, setAction } = useContext(NavbarContext); - const [updateEntries, setUpdateEntries] = useState([]); - const [hasNextPage, setHasNextPage] = useState(true); - const [fetched, setFetched] = useState(false); - const [lastPageNum, setLastPageNum] = useState(0); + const { + data: pages = [{ hasNextPage: false, page: [] }], + isLoading, + size: loadedPages, + setSize: setPages, + } = requestManager.useGetRecentlyUpdatedChapters(); + const { hasNextPage } = pages[pages.length - 1]; + const updateEntries = useMemo( + () => pages.map((page) => page.page).reduce((lastPageChapters, chapters) => [...lastPageChapters, ...chapters]), + [pages], + ); + const groupedUpdates = useMemo(() => groupByDate(updateEntries), [updateEntries]); + const groupCounts: number[] = useMemo(() => groupedUpdates.map((group) => group[1]), [groupedUpdates]); const [, setWsClient] = useState(); const [{ queue }, setQueueState] = useState(initialQueue); @@ -101,41 +141,7 @@ const Updates: React.FC = () => { setAction(null); }, [t]); - useEffect(() => { - if (hasNextPage) { - requestManager - .getClient() - .get(`/api/v1/update/recentChapters/${lastPageNum}`) - .then((response) => response.data) - .then(({ hasNextPage: fetchedHasNextPage, page }: PaginatedList) => { - setUpdateEntries([...updateEntries, ...page]); - setHasNextPage(fetchedHasNextPage); - setFetched(true); - }); - } - }, [lastPageNum]); - - const lastEntry = useRef(null); - - const scrollHandler = () => { - if (lastEntry.current) { - const rect = lastEntry.current.getBoundingClientRect(); - if ((rect.y + rect.height) / window.innerHeight < 2 && hasNextPage) { - setLastPageNum(lastPageNum + 1); - } - } - }; - useEffect(() => { - window.addEventListener('scroll', scrollHandler, true); - return () => { - window.removeEventListener('scroll', scrollHandler, true); - }; - }, [hasNextPage, updateEntries]); - - if (!fetched) { - return ; - } - if (fetched && updateEntries.length === 0) { + if (!isLoading && updateEntries.length === 0) { return ; } @@ -148,86 +154,94 @@ const Updates: React.FC = () => { requestManager.addChapterToDownloadQueue(chapter.mangaId, chapter.index); }; + const loadMore = useCallback(() => { + if (!hasNextPage) { + return; + } + + setPages(loadedPages + 1); + }, [hasNextPage, loadedPages]); + return ( - <> - {groupByDate(updateEntries).map((dateGroup) => ( -
- - {dateGroup[0]} - - {dateGroup[1].map(({ item: { chapter, manga }, globalIdx }) => { - const download = downloadForChapter(chapter); - return ( - (isLoading ? : null), + }} + overscan={window.innerHeight * 0.5} + endReached={loadMore} + groupCounts={groupCounts} + groupContent={(index) => ( + + {groupedUpdates[index][0]} + + )} + itemContent={(index) => { + const { chapter, manga } = updateEntries[index]; + const download = downloadForChapter(chapter); + + return ( + + + - - - - - - - {manga.title} - - - {chapter.name} - - + + + + + {manga.title} + + + {chapter.name} + - {download && } - {download == null && !chapter.downloaded && ( - { - e.stopPropagation(); - e.preventDefault(); - downloadChapter(chapter); - }} - size="large" - > - - - )} - - - - ); - })} -
- ))} - +
+ {download && } + {download == null && !chapter.downloaded && ( + { + e.stopPropagation(); + e.preventDefault(); + downloadChapter(chapter); + }} + size="large" + > + + + )} + + + + + ); + }} + /> ); }; diff --git a/tsconfig.json b/tsconfig.json index 972b640e..7e38070e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "baseUrl": "./src", - "target": "es5", + "target": "es2015", "lib": [ "dom", "dom.iterable",