Show only first unread chapter per manga per day in updates page
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
|
||||
@@ -16,7 +16,7 @@ import { UpdateChecker } from '@/features/updates/components/UpdateChecker.tsx';
|
||||
import { StyledGroupedVirtuoso } from '@/base/components/virtuoso/StyledGroupedVirtuoso.tsx';
|
||||
import { StyledGroupHeader } from '@/base/components/virtuoso/StyledGroupHeader.tsx';
|
||||
import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupItemWrapper.tsx';
|
||||
import { dateTimeFormatter } from '@/base/utils/DateHelper.ts';
|
||||
import { dateTimeFormatter, epochToDate, getDateString } from '@/base/utils/DateHelper.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
|
||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
@@ -26,11 +26,16 @@ import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitleAndAction.ts';
|
||||
import { GROUPED_VIRTUOSO_Z_INDEX } from '@/lib/virtuoso/Virtuoso.constants.ts';
|
||||
import { STABLE_EMPTY_ARRAY } from '@/base/Base.constants.ts';
|
||||
import mapValues from 'lodash/fp/mapValues';
|
||||
import difference from 'lodash/fp/difference';
|
||||
import uniqBy from 'lodash/fp/uniqBy';
|
||||
|
||||
export const Updates: React.FC = () => {
|
||||
const { t } = useLingui();
|
||||
const { appBarHeight } = useNavBarContext();
|
||||
|
||||
useAppTitleAndAction(t`Updates`, <UpdateChecker />);
|
||||
|
||||
const {
|
||||
data: chapterUpdateData,
|
||||
loading: isLoading,
|
||||
@@ -41,21 +46,73 @@ export const Updates: React.FC = () => {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
const hasNextPage = !!chapterUpdateData?.chapters.pageInfo.hasNextPage;
|
||||
const endCursor = chapterUpdateData?.chapters.pageInfo.endCursor;
|
||||
const updateEntries = chapterUpdateData?.chapters.nodes ?? STABLE_EMPTY_ARRAY;
|
||||
const groupedUpdates = useMemo(
|
||||
() => Object.entries(Chapters.groupByDate(updateEntries, 'fetchedAt')),
|
||||
[updateEntries],
|
||||
const allUpdateEntries = chapterUpdateData?.chapters.nodes ?? STABLE_EMPTY_ARRAY;
|
||||
|
||||
const [prevUpdateEntriesCount, setPrevUpdateEntriesCount] = useState(0);
|
||||
|
||||
const [firstUnreadUpdatesByGroup, otherUpdatesByMangaByGroup] = useMemo(() => {
|
||||
const groupedEntries = Chapters.groupByDate(allUpdateEntries, 'fetchedAt');
|
||||
|
||||
const mangaIdByGroup = mapValues(
|
||||
(groupEntries) => uniqBy('mangaId', groupEntries).map((entry) => entry.mangaId),
|
||||
groupedEntries,
|
||||
);
|
||||
|
||||
const entriesByMangaByGroup = mapValues(
|
||||
(entries) => Object.groupBy(entries!, (entry) => entry.mangaId),
|
||||
groupedEntries,
|
||||
);
|
||||
|
||||
const firstUnreadEntryByMangaByGroup = mapValues(
|
||||
(entriesByManga) =>
|
||||
mapValues(
|
||||
(mangaEntries) => [mangaEntries!.findLast((entry) => !entry.isRead) ?? mangaEntries![0]],
|
||||
entriesByManga,
|
||||
),
|
||||
entriesByMangaByGroup,
|
||||
);
|
||||
const firstUnreadEntryByGroup = mapValues(
|
||||
(firstUnreadEntryByManga) =>
|
||||
Object.values(firstUnreadEntryByManga)
|
||||
.flat()
|
||||
.toSorted((a, b) => {
|
||||
const groupMangaIds = mangaIdByGroup[getDateString(epochToDate(Number(a.fetchedAt)))];
|
||||
|
||||
return groupMangaIds.indexOf(a.mangaId) - groupMangaIds.indexOf(b.mangaId);
|
||||
}),
|
||||
firstUnreadEntryByMangaByGroup,
|
||||
);
|
||||
const remainingEntriesByMangaByGroup = mapValues(
|
||||
(entriesByManga) =>
|
||||
mapValues(
|
||||
(mangaEntries) =>
|
||||
difference(
|
||||
mangaEntries!,
|
||||
firstUnreadEntryByMangaByGroup[
|
||||
getDateString(epochToDate(Number(mangaEntries![0].fetchedAt)))
|
||||
]![mangaEntries![0].mangaId],
|
||||
),
|
||||
entriesByManga,
|
||||
),
|
||||
entriesByMangaByGroup,
|
||||
);
|
||||
|
||||
return [Object.entries(firstUnreadEntryByGroup), remainingEntriesByMangaByGroup];
|
||||
}, [allUpdateEntries]);
|
||||
|
||||
const firstUnreadUpdatesGroupCounts = useMemo(
|
||||
() => firstUnreadUpdatesByGroup.map((updatesByGroup) => updatesByGroup[VirtuosoUtil.ITEMS].length),
|
||||
[firstUnreadUpdatesByGroup],
|
||||
);
|
||||
const groupCounts: number[] = useMemo(
|
||||
() => groupedUpdates.map((group) => group[VirtuosoUtil.ITEMS].length),
|
||||
[groupedUpdates],
|
||||
const firstUnreadUpdatesEntries = useMemo(
|
||||
() => firstUnreadUpdatesByGroup.flatMap((updatesByGroup) => updatesByGroup[VirtuosoUtil.ITEMS]),
|
||||
[firstUnreadUpdatesByGroup],
|
||||
);
|
||||
|
||||
const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey(
|
||||
groupCounts,
|
||||
useCallback((index) => groupedUpdates[index][VirtuosoUtil.GROUP], [groupedUpdates]),
|
||||
useCallback((index) => updateEntries[index].id, [updateEntries]),
|
||||
const computeFirstUnreadUpdateItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey(
|
||||
firstUnreadUpdatesGroupCounts,
|
||||
useCallback((index) => firstUnreadUpdatesByGroup[index][VirtuosoUtil.GROUP], [firstUnreadUpdatesByGroup]),
|
||||
useCallback((index) => firstUnreadUpdatesEntries[index].id, [firstUnreadUpdatesEntries]),
|
||||
);
|
||||
|
||||
const lastUpdateTimestampCompRef = useRef<HTMLElement>(null);
|
||||
@@ -73,15 +130,23 @@ export const Updates: React.FC = () => {
|
||||
const lastUpdateTimestamp = lastUpdateTimestampData?.lastUpdateTimestamp.timestamp;
|
||||
const date = lastUpdateTimestamp ? dateTimeFormatter.format(+lastUpdateTimestamp) : '-';
|
||||
|
||||
useAppTitleAndAction(t`Updates`, <UpdateChecker />);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (!hasNextPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchMore({ variables: { offset: updateEntries.length } });
|
||||
}, [hasNextPage, endCursor]);
|
||||
fetchMore({ variables: { offset: allUpdateEntries.length } }).then(() =>
|
||||
setPrevUpdateEntriesCount(firstUnreadUpdatesEntries.length),
|
||||
);
|
||||
}, [hasNextPage, allUpdateEntries.length, firstUnreadUpdatesEntries.length]);
|
||||
|
||||
const filteredOutAllItemsOfFetchedPage =
|
||||
allUpdateEntries.length > 0 && prevUpdateEntriesCount === firstUnreadUpdatesEntries.length;
|
||||
useEffect(() => {
|
||||
if (filteredOutAllItemsOfFetchedPage && hasNextPage && !isLoading) {
|
||||
loadMore();
|
||||
}
|
||||
}, [isLoading, hasNextPage, filteredOutAllItemsOfFetchedPage, loadMore]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -93,7 +158,7 @@ export const Updates: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && updateEntries.length === 0) {
|
||||
if (!isLoading && firstUnreadUpdatesEntries.length === 0) {
|
||||
return <EmptyViewAbsoluteCentered message={t`You don't have any updates yet.`} />;
|
||||
}
|
||||
|
||||
@@ -120,18 +185,25 @@ export const Updates: React.FC = () => {
|
||||
}}
|
||||
overscan={window.innerHeight * 0.5}
|
||||
endReached={loadMore}
|
||||
groupCounts={groupCounts}
|
||||
groupCounts={firstUnreadUpdatesGroupCounts}
|
||||
groupContent={(index) => (
|
||||
<StyledGroupHeader isFirstItem={index === 0}>
|
||||
<Typography variant="h5" component="h2">
|
||||
{groupedUpdates[index][VirtuosoUtil.GROUP]}
|
||||
{firstUnreadUpdatesByGroup[index][VirtuosoUtil.GROUP]}
|
||||
</Typography>
|
||||
</StyledGroupHeader>
|
||||
)}
|
||||
computeItemKey={computeItemKey}
|
||||
computeItemKey={computeFirstUnreadUpdateItemKey}
|
||||
itemContent={(index) => (
|
||||
<StyledGroupItemWrapper>
|
||||
<ChapterUpdateCard chapter={updateEntries[index]} />
|
||||
<ChapterUpdateCard
|
||||
chapter={firstUnreadUpdatesEntries[index]}
|
||||
otherChapters={
|
||||
otherUpdatesByMangaByGroup[
|
||||
getDateString(epochToDate(Number(firstUnreadUpdatesEntries[index].fetchedAt)))
|
||||
][firstUnreadUpdatesEntries[index].mangaId]
|
||||
}
|
||||
/>
|
||||
</StyledGroupItemWrapper>
|
||||
)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user