diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d5a856d..f0712607 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- (**Category**) Require confirmation before deleting a category
- (**Download**) Respect manga chapter filters on bulk manga download in the library
- (**History**) Show only the last read chapter per manga
+- (**Updates**) Show only the first unread chapter per manga per day
### Fixed
diff --git a/src/features/updates/components/ChapterUpdateCard.tsx b/src/features/updates/components/ChapterUpdateCard.tsx
index ec0bd0f9..61b8fd78 100644
--- a/src/features/updates/components/ChapterUpdateCard.tsx
+++ b/src/features/updates/components/ChapterUpdateCard.tsx
@@ -10,7 +10,7 @@ import Box from '@mui/material/Box';
import CardActionArea from '@mui/material/CardActionArea';
import Card from '@mui/material/Card';
import { Link } from 'react-router-dom';
-import { memo } from 'react';
+import { memo, useMemo } from 'react';
import { DownloadStateIndicator } from '@/base/components/downloads/DownloadStateIndicator.tsx';
import type { ChapterUpdateListFieldsFragment } from '@/lib/graphql/generated/graphql.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
@@ -20,36 +20,81 @@ import { ChapterDownloadButton } from '@/features/chapter/components/buttons/Cha
import { ChapterDownloadRetryButton } from '@/features/chapter/components/buttons/ChapterDownloadRetryButton.tsx';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { ListCardContent } from '@/base/components/lists/cards/ListCardContent.tsx';
+import { useLingui } from '@lingui/react/macro';
+import { plural } from '@lingui/core/macro';
-export const ChapterUpdateCard = memo(({ chapter }: { chapter: ChapterUpdateListFieldsFragment }) => {
- const { manga } = chapter;
+export const ChapterUpdateCard = memo(
+ ({
+ chapter,
+ otherChapters,
+ }: {
+ chapter: ChapterUpdateListFieldsFragment;
+ otherChapters: ChapterUpdateListFieldsFragment[];
+ }) => {
+ const { manga } = chapter;
- return (
-
- theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
- }}
- >
-
-
-
-
-
-
-
-
-
-
-
- );
-});
+ const { t } = useLingui();
+
+ const uniqueOtherChapters = useMemo(
+ () => Chapters.removeDuplicates(chapter, otherChapters),
+ [chapter, otherChapters],
+ );
+ const otherChaptersCount = uniqueOtherChapters.length;
+ const firstFewOtherChapters = uniqueOtherChapters.slice(-3);
+
+ const otherChaptersText = (() => {
+ if (!otherChaptersCount) {
+ return '';
+ }
+
+ const firstFewUpdatesString = firstFewOtherChapters
+ .map((otherChapter) => `#${otherChapter.chapterNumber}`)
+ .toReversed()
+ .join(', ');
+
+ if (otherChaptersCount > firstFewOtherChapters.length) {
+ const remainingUpdatesCount = otherChaptersCount - firstFewOtherChapters.length;
+
+ return t`Plus chapters ${firstFewUpdatesString} and ${remainingUpdatesCount} more`;
+ }
+
+ return plural(firstFewOtherChapters.length, {
+ one: `Plus chapter ${firstFewUpdatesString}`,
+ other: `Plus chapters ${firstFewUpdatesString}`,
+ });
+ })();
+
+ return (
+
+ theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ );
+ },
+);
diff --git a/src/features/updates/screens/Updates.tsx b/src/features/updates/screens/Updates.tsx
index 6936c81a..7c4d9c08 100644
--- a/src/features/updates/screens/Updates.tsx
+++ b/src/features/updates/screens/Updates.tsx
@@ -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`, );
+
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(null);
@@ -73,15 +130,23 @@ export const Updates: React.FC = () => {
const lastUpdateTimestamp = lastUpdateTimestampData?.lastUpdateTimestamp.timestamp;
const date = lastUpdateTimestamp ? dateTimeFormatter.format(+lastUpdateTimestamp) : '-';
- useAppTitleAndAction(t`Updates`, );
-
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 ;
}
@@ -120,18 +185,25 @@ export const Updates: React.FC = () => {
}}
overscan={window.innerHeight * 0.5}
endReached={loadMore}
- groupCounts={groupCounts}
+ groupCounts={firstUnreadUpdatesGroupCounts}
groupContent={(index) => (
- {groupedUpdates[index][VirtuosoUtil.GROUP]}
+ {firstUnreadUpdatesByGroup[index][VirtuosoUtil.GROUP]}
)}
- computeItemKey={computeItemKey}
+ computeItemKey={computeFirstUnreadUpdateItemKey}
itemContent={(index) => (
-
+
)}
/>
diff --git a/src/i18n/locales/en.po b/src/i18n/locales/en.po
index 2a7ae641..b314e07a 100644
--- a/src/i18n/locales/en.po
+++ b/src/i18n/locales/en.po
@@ -111,6 +111,11 @@ msgstr "{0, plural, one {1 migrating entry} other {# migrating entries}}"
msgid "{0, plural, one {1 searching} other {# searching}}"
msgstr "{0, plural, one {1 searching} other {# searching}}"
+#. placeholder {0}: firstFewOtherChapters.length
+#: src/features/updates/components/ChapterUpdateCard.tsx
+msgid "{0, plural, one {Plus chapter {firstFewUpdatesString}} other {Plus chapters {firstFewUpdatesString}}}"
+msgstr "{0, plural, one {Plus chapter {firstFewUpdatesString}} other {Plus chapters {firstFewUpdatesString}}}"
+
#. placeholder {0}: autoScroll.value
#: src/features/reader/auto-scroll/settings/quick-setting/ReaderNavBarDesktopAutoScroll.tsx
msgid "{0, plural, one {Second} other {Seconds}}"
@@ -2821,6 +2826,10 @@ msgstr "Pin source"
msgid "Pinned"
msgstr "Pinned"
+#: src/features/updates/components/ChapterUpdateCard.tsx
+msgid "Plus chapters {firstFewUpdatesString} and {remainingUpdatesCount} more"
+msgstr "Plus chapters {firstFewUpdatesString} and {remainingUpdatesCount} more"
+
#: src/features/source/browse/screens/SourceMangas.tsx
msgid "Popular"
msgstr "Popular"
diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts
index a94ad54d..cb7834a3 100644
--- a/src/lib/requests/RequestManager.ts
+++ b/src/lib/requests/RequestManager.ts
@@ -3430,7 +3430,7 @@ export class RequestManager {
initialPages: number = 1,
options?: QueryHookOptions,
): AbortableApolloUseQueryResponse {
- const PAGE_SIZE = 50;
+ const PAGE_SIZE = 150;
const CACHE_KEY = 'useGetRecentlyUpdatedChapters';
const offset = this.cache.getResponseFor(CACHE_KEY, undefined) ?? 0;