Move download status subscription to "ChapterCard"

Only re-render chapters whose download status has changed instead of the whole list everytime a download status change was received
This commit is contained in:
schroda
2025-01-12 15:05:09 +01:00
parent f7b26767a3
commit 257c0c8e10
11 changed files with 107 additions and 205 deletions

View File

@@ -28,6 +28,7 @@ import { ChaptersToolbarMenu } from '@/modules/chapter/components/ChaptersToolba
import { SelectionFAB } from '@/modules/collection/components/SelectionFAB.tsx';
import { DEFAULT_FULL_FAB_HEIGHT } from '@/modules/core/components/buttons/StyledFab.tsx';
import {
ChapterListFieldsFragment,
GetChaptersMangaQuery,
GetChaptersMangaQueryVariables,
MangaScreenFieldsFragment,
@@ -35,7 +36,6 @@ import {
import { useSelectableCollection } from '@/modules/collection/hooks/useSelectableCollection.ts';
import { SelectableCollectionSelectAll } from '@/modules/collection/components/SelectableCollectionSelectAll.tsx';
import { Chapters } from '@/modules/chapter/services/Chapters.ts';
import { ChaptersWithMeta, ChapterWithMetaType } from '@/modules/chapter/services/ChaptersWithMeta.ts';
import { ChapterActionMenuItems } from '@/modules/chapter/components/actions/ChapterActionMenuItems.tsx';
import { ChaptersDownloadActionMenuItems } from '@/modules/chapter/components/actions/ChaptersDownloadActionMenuItems.tsx';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
@@ -75,9 +75,27 @@ const StyledVirtuoso = styled(Virtuoso, {
},
}));
export interface IChapterWithMeta extends ChapterWithMetaType<ComponentProps<typeof ChapterCard>['chapter']> {
selected: boolean | null;
}
const ChapterListFAB = ({
selectedChapters,
firstUnreadChapter,
}: {
selectedChapters: ChapterListFieldsFragment[];
firstUnreadChapter: ComponentProps<typeof ResumeFab>['chapter'] | null | undefined;
}) => {
if (selectedChapters.length) {
return (
<SelectionFAB selectedItemsCount={selectedChapters.length} title="chapter.title_one">
{(handleClose) => <ChapterActionMenuItems selectedChapters={selectedChapters} onClose={handleClose} />}
</SelectionFAB>
);
}
if (firstUnreadChapter) {
return <ResumeFab chapter={firstUnreadChapter} />;
}
return null;
};
export const ChapterList = ({
manga,
@@ -100,8 +118,6 @@ export const ChapterList = ({
const scrollbarWidth = MediaQuery.useGetScrollbarSize('width');
const downloadSubscription = requestManager.useDownloadSubscription();
const [options, dispatch] = useChapterOptions(manga.id);
const {
data: chaptersData,
@@ -128,39 +144,6 @@ export const ChapterList = ({
const noChaptersFound = chapters.length === 0;
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
const chaptersWithMeta: IChapterWithMeta[] = useMemo(
() =>
visibleChapters.map((chapter) => {
const selected = !areNoItemsSelected ? selectedItemIds.includes(chapter.id) : null;
return {
chapter,
downloadChapter: Chapters.getDownloadStatusFromCache(chapter.id),
selected,
};
}),
[downloadSubscription.data?.downloadStatusChanged, selectedItemIds, visibleChapters],
);
const chapterListFAB = useMemo(() => {
const selectedChapters = chaptersWithMeta.filter((chapter) => chapter.selected);
if (selectedChapters.length) {
return (
<SelectionFAB selectedItemsCount={selectedChapters.length} title="chapter.title_one">
{(handleClose) => (
<ChapterActionMenuItems selectedChapters={selectedChapters} onClose={handleClose} />
)}
</SelectionFAB>
);
}
if (manga.firstUnreadChapter) {
return <ResumeFab chapter={manga.firstUnreadChapter} />;
}
return null;
}, [chaptersWithMeta]);
if (isLoading || (noChaptersFound && isRefreshing)) {
return (
<Stack sx={{ justifyContent: 'center', alignItems: 'center', position: 'relative', flexGrow: 1 }}>
@@ -201,13 +184,7 @@ export const ChapterList = ({
<Tooltip title={t('chapter.action.mark_as_read.add.label.action.current')}>
<IconButton
disabled={areAllChaptersRead}
onClick={() =>
Chapters.markAsRead(
ChaptersWithMeta.getChapters(ChaptersWithMeta.getNonRead(chaptersWithMeta)),
true,
manga.id,
)
}
onClick={() => Chapters.markAsRead(Chapters.getNonRead(chapters), true, manga.id)}
>
<DoneAllIcon />
</IconButton>
@@ -259,11 +236,12 @@ export const ChapterList = ({
computeItemKey={(index) => visibleChapters[index].id}
itemContent={(index: number) => (
<ChapterCard
{...chaptersWithMeta[index]}
chapter={chapters[index]}
selected={!areNoItemsSelected ? selectedItemIds.includes(chapters[index].id) : null}
allChapters={chapters}
showChapterNumber={options.showChapterNumber}
onSelect={(selected, selectRange) =>
handleSelection(chaptersWithMeta[index].chapter.id, selected, { selectRange })
handleSelection(chapters[index].id, selected, { selectRange })
}
/>
)}
@@ -271,7 +249,12 @@ export const ChapterList = ({
overscan={window.innerHeight * 0.5}
/>
</Stack>
{chapterListFAB}
<ChapterListFAB
selectedChapters={selectedItemIds
.map((id) => chapters.find((chapter) => chapter.id === id))
.filter((chapter) => chapter != null)}
firstUnreadChapter={manga.firstUnreadChapter}
/>
</>
);
};

View File

@@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next';
import BookmarkRemove from '@mui/icons-material/BookmarkRemove';
import BookmarkAdd from '@mui/icons-material/BookmarkAdd';
import DoneAll from '@mui/icons-material/DoneAll';
import { useMemo } from 'react';
import { ComponentProps, useMemo } from 'react';
import LaunchIcon from '@mui/icons-material/Launch';
import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSelectableCollection.ts';
import {
@@ -30,8 +30,6 @@ import {
Chapters,
} from '@/modules/chapter/services/Chapters.ts';
import { MenuItem } from '@/modules/core/components/menu/MenuItem.tsx';
import { IChapterWithMeta } from '@/modules/chapter/components/ChapterList.tsx';
import { ChaptersWithMeta } from '@/modules/chapter/services/ChaptersWithMeta.ts';
import {
createGetMenuItemTitle,
createIsMenuItemDisabled,
@@ -39,6 +37,7 @@ import {
} from '@/modules/core/components/menu/Menu.utils.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
type BaseProps = { onClose: () => void; selectable?: boolean };
@@ -57,7 +56,7 @@ type SingleModeProps = {
};
type SelectModeProps = {
selectedChapters: IChapterWithMeta[];
selectedChapters: ComponentProps<typeof ChapterCard>['chapter'][];
};
type Props =
@@ -95,12 +94,12 @@ export const ChapterActionMenuItems = ({
readChapters,
} = useMemo(
() => ({
downloadableChapters: ChaptersWithMeta.getDownloadable(selectedChapters),
downloadedChapters: ChaptersWithMeta.getDownloaded(selectedChapters),
unbookmarkedChapters: ChaptersWithMeta.getNonBookmarked(selectedChapters),
bookmarkedChapters: ChaptersWithMeta.getBookmarked(selectedChapters),
unreadChapters: ChaptersWithMeta.getNonRead(selectedChapters),
readChapters: ChaptersWithMeta.getRead(selectedChapters),
downloadableChapters: Chapters.getDownloadable(selectedChapters),
downloadedChapters: Chapters.getDownloaded(selectedChapters),
unbookmarkedChapters: Chapters.getNonBookmarked(selectedChapters),
bookmarkedChapters: Chapters.getBookmarked(selectedChapters),
unreadChapters: Chapters.getNonRead(selectedChapters),
readChapters: Chapters.getRead(selectedChapters),
}),
[selectedChapters],
);
@@ -110,7 +109,7 @@ export const ChapterActionMenuItems = ({
onClose();
};
const performAction = (action: ChapterAction | 'mark_prev_as_read', chaptersWithMeta: IChapterWithMeta[]) => {
const performAction = (action: ChapterAction | 'mark_prev_as_read', chapters: TChapter[]) => {
const isMarkPrevAsRead = action === 'mark_prev_as_read';
const actualAction: ChapterAction = isMarkPrevAsRead ? 'mark_as_read' : action;
@@ -125,7 +124,7 @@ export const ChapterActionMenuItems = ({
const getChapters = (): SingleModeProps['chapter'][] => {
// select mode
if (!chapter) {
return ChaptersWithMeta.getChapters(chaptersWithMeta);
return chapters;
}
if (!isMarkPrevAsRead) {
@@ -142,17 +141,17 @@ export const ChapterActionMenuItems = ({
return allChapters.slice(index + 1);
};
const chapters = getChapters();
const chaptersToUpdate = getChapters();
if (!chapters.length) {
if (!chaptersToUpdate.length) {
onClose();
return;
}
Chapters.performAction(actualAction, Chapters.getIds(chapters), {
chapters,
Chapters.performAction(actualAction, Chapters.getIds(chaptersToUpdate), {
chapters: chaptersToUpdate,
wasManuallyMarkedAsRead: true,
trackProgressMangaId: chapters[0]?.mangaId,
trackProgressMangaId: chaptersToUpdate[0]?.mangaId,
}).catch(defaultPromiseErrorHandler('ChapterActionMenuItems::performAction'));
onClose();
};
@@ -186,10 +185,7 @@ export const ChapterActionMenuItems = ({
Icon={Delete}
disabled={isMenuItemDisabled(!downloadedChapters.length)}
onClick={() =>
performAction(
'delete',
ChaptersWithMeta.getDeletable(downloadedChapters, deleteChaptersWithBookmark),
)
performAction('delete', Chapters.getDeletable(downloadedChapters, deleteChaptersWithBookmark))
}
title={getMenuItemTitle('delete', downloadedChapters.length)}
/>

View File

@@ -38,7 +38,6 @@ import {
Chapters,
ChapterScanlatorInfo,
} from '@/modules/chapter/services/Chapters.ts';
import { ChaptersWithMeta, ChapterWithMetaType } from '@/modules/chapter/services/ChaptersWithMeta.ts';
type TChapter = ChapterIdInfo &
ChapterMangaInfo &
@@ -53,7 +52,6 @@ interface IProps {
mode?: 'manga.page' | 'reader';
chapter: TChapter;
allChapters: TChapter[];
downloadChapter: ChapterWithMetaType['downloadChapter'];
showChapterNumber: boolean;
onSelect: (selected: boolean, isShiftKey?: boolean) => void;
selected: boolean | null;
@@ -71,7 +69,6 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
mode = 'manga.page',
chapter,
allChapters,
downloadChapter: dc,
showChapterNumber,
onSelect,
selected,
@@ -175,7 +172,7 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
</Typography>
</Stack>
{dc && <DownloadStateIndicator download={dc} />}
<DownloadStateIndicator chapterId={chapter.id} />
<Stack sx={{ minHeight: '48px' }}>
{selected === null ? (
@@ -209,10 +206,7 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
chapter={chapter}
allChapters={allChapters}
handleSelection={() => onSelect(true)}
canBeDownloaded={ChaptersWithMeta.isDownloadable({
chapter,
downloadChapter: dc,
})}
canBeDownloaded={Chapters.isDownloadable(chapter)}
selectable={selectable}
/>
)}

View File

@@ -8,13 +8,14 @@
import { t as translate } from 'i18next';
import gql from 'graphql-tag';
import { DocumentNode, Unmasked } from '@apollo/client';
import { DocumentNode, MaybeMasked, Unmasked, useFragment } from '@apollo/client';
import { makeToast } from '@/modules/core/utils/Toast.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { getMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
import {
ChapterListFieldsFragment,
ChapterType,
DownloadState,
DownloadStatusFieldsFragment,
DownloadTypeFieldsFragment,
} from '@/lib/graphql/generated/graphql.ts';
@@ -138,6 +139,30 @@ export class Chapters {
});
}
static useDownloadStatusFromCache<T = DownloadTypeFieldsFragment>(
id: number,
fragment: DocumentNode = DOWNLOAD_TYPE_FIELDS,
fragmentName: string = 'DOWNLOAD_TYPE_FIELDS',
): MaybeMasked<T> | null {
const downloadStatus = useFragment<T>({
from: {
__typename: 'DownloadType',
chapter: {
__ref: requestManager.graphQLClient.client.cache.identify({ __typename: 'ChapterType', id }),
},
},
fragment,
fragmentName,
client: requestManager.graphQLClient.client,
});
if (!downloadStatus.complete || !Object.keys(downloadStatus.data ?? {}).length) {
return null;
}
return downloadStatus.data;
}
static getReaderUrl<Chapter extends ChapterMangaInfo & ChapterSourceOrderInfo>(chapter: Chapter): string {
return AppRoutes.reader.path(chapter.mangaId, chapter.sourceOrder);
}
@@ -170,6 +195,15 @@ export class Chapters {
return chapters.filter(Chapters.isDownloaded);
}
static isDownloadable<Chapter extends ChapterDownloadInfo>(chapter: Chapter): boolean {
const downloadStatus = Chapters.getDownloadStatusFromCache(chapter.id);
return !Chapters.isDownloaded(chapter) && (!downloadStatus || downloadStatus.state === DownloadState.Error);
}
static getDownloadable<Chapter extends ChapterDownloadInfo>(chapters: Chapter[]): Chapter[] {
return chapters.filter(this.isDownloadable);
}
static isDeletable(
{ isBookmarked, ...chapter }: ChapterDownloadInfo & ChapterBookmarkInfo,
canDeleteBookmarked: boolean = false,

View File

@@ -1,76 +0,0 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import {
ChapterBookmarkInfo,
ChapterDownloadInfo,
ChapterDownloadStatus,
ChapterReadInfo,
Chapters,
} from '@/modules/chapter/services/Chapters.ts';
import { DownloadState } from '@/lib/graphql/generated/graphql.ts';
export type ChapterWithMetaType<
Chapter extends ChapterDownloadInfo & ChapterReadInfo & ChapterBookmarkInfo = ChapterDownloadInfo &
ChapterReadInfo &
ChapterBookmarkInfo,
> = {
chapter: Chapter;
downloadChapter: ChapterDownloadStatus | undefined | null;
};
export class ChaptersWithMeta {
static getChapters<ChaptersWithMeta extends ChapterWithMetaType>(
chapters: ChaptersWithMeta[],
): ChaptersWithMeta['chapter'][] {
return chapters.map(({ chapter }) => chapter);
}
static getIds(chapters: ChapterWithMetaType[]): number[] {
return Chapters.getIds(ChaptersWithMeta.getChapters(chapters));
}
static getDownloaded<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
return chapters.filter(({ chapter }) => Chapters.isDownloaded(chapter));
}
static getDeletable<Chapter extends ChapterWithMetaType>(
chapters: Chapter[],
canDeleteBookmarked?: boolean,
): Chapter[] {
return chapters.filter(({ chapter }) => Chapters.isDeletable(chapter, canDeleteBookmarked));
}
static getNonDownloaded<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
return chapters.filter(({ chapter }) => !Chapters.isDownloaded(chapter));
}
static isDownloadable<Chapter extends ChapterWithMetaType>({ chapter, downloadChapter }: Chapter): boolean {
return !Chapters.isDownloaded(chapter) && (!downloadChapter || downloadChapter?.state === DownloadState.Error);
}
static getDownloadable<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
return chapters.filter(this.isDownloadable);
}
static getBookmarked<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
return chapters.filter(({ chapter }) => Chapters.isBookmarked(chapter));
}
static getNonBookmarked<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
return chapters.filter(({ chapter }) => !Chapters.isBookmarked(chapter));
}
static getRead<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
return chapters.filter(({ chapter }) => Chapters.isRead(chapter));
}
static getNonRead<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
return chapters.filter(({ chapter }) => !Chapters.isRead(chapter));
}
}

View File

@@ -11,7 +11,7 @@ import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { DownloadState } from '@/lib/graphql/generated/graphql.ts';
import { ChapterDownloadStatus } from '@/modules/chapter/services/Chapters.ts';
import { ChapterIdInfo, Chapters } from '@/modules/chapter/services/Chapters.ts';
import { TranslationKey } from '@/Base.types.ts';
const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in DownloadState]: TranslationKey } = {
@@ -21,9 +21,15 @@ const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in DownloadState]: Transla
QUEUED: 'download.state.label.queued',
} as const;
export const DownloadStateIndicator = ({ download }: { download: ChapterDownloadStatus }) => {
export const DownloadStateIndicator = ({ chapterId }: { chapterId: ChapterIdInfo['id'] }) => {
const { t } = useTranslation();
const download = Chapters.useDownloadStatusFromCache(chapterId);
if (!download) {
return null;
}
const isDownloading = download.state === DownloadState.Downloading;
const isPartiallyDownloaded = download.progress !== 0;

View File

@@ -85,7 +85,7 @@ const DownloadChapterItem = ({
{item.chapter.name}
</Typography>
</Stack>
<DownloadStateIndicator download={item} />
<DownloadStateIndicator chapterId={item.chapter.id} />
{item.state === DownloadState.Error && (
<Tooltip title={t('global.button.retry')}>
<IconButton

View File

@@ -8,53 +8,33 @@
import { Virtuoso, VirtuosoProps } from 'react-virtuoso';
import { useMemo } from 'react';
import { IChapterWithMeta } from '@/modules/chapter/components/ChapterList.tsx';
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
import { Chapters } from '@/modules/chapter/services/Chapters';
export const ReaderChapterList = ({
currentChapter,
chapters,
style,
}: Pick<ReaderStateChapters, 'chapters' | 'currentChapter'> & Pick<VirtuosoProps<any, any>, 'style'>) => {
const downloadSubscription = requestManager.useDownloadSubscription();
const currentChapterIndex = useMemo(
() => currentChapter && chapters.findIndex((chapter) => chapter.id === currentChapter.id),
[currentChapter, chapters],
);
const chaptersWithMeta: IChapterWithMeta[] = useMemo(
() =>
chapters.map((chapter) => {
const downloadChapter = Chapters.getDownloadStatusFromCache(chapter.id);
return {
chapter,
downloadChapter,
selected: null,
};
}),
[downloadSubscription.data?.downloadStatusChanged, chapters],
);
return (
<Virtuoso
style={{
height: `calc(${chaptersWithMeta.length} * 100px)`,
height: `calc(${chapters.length} * 100px)`,
...style,
}}
initialTopMostItemIndex={currentChapterIndex ?? 0}
totalCount={chaptersWithMeta.length}
computeItemKey={(index) => chaptersWithMeta[index].chapter.id}
totalCount={chapters.length}
computeItemKey={(index) => chapters[index].id}
itemContent={(index) => (
<ChapterCard
key={chaptersWithMeta[index].chapter.id}
key={chapters[index].id}
mode="reader"
chapter={chaptersWithMeta[index].chapter}
downloadChapter={chaptersWithMeta[index].downloadChapter}
chapter={chapters[index]}
allChapters={chapters}
showChapterNumber={false}
selected={null}

View File

@@ -19,20 +19,17 @@ import { memo, useMemo, useRef } from 'react';
import DeleteIcon from '@mui/icons-material/Delete';
import { actionToTranslationKey, ChapterAction, Chapters } from '@/modules/chapter/services/Chapters.ts';
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { DownloadStateIndicator } from '@/modules/core/components/DownloadStateIndicator.tsx';
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
import { ChapterWithMetaType } from '@/modules/chapter/services/ChaptersWithMeta.ts';
const DownloadButton = ({
currentChapter,
downloadChapter,
}: Required<Pick<ReaderStateChapters, 'currentChapter'>> & Pick<ChapterWithMetaType, 'downloadChapter'>) => {
const DownloadButton = ({ currentChapter }: Required<Pick<ReaderStateChapters, 'currentChapter'>>) => {
const { t } = useTranslation();
const downloadChapter = Chapters.getDownloadStatusFromCache(currentChapter?.id ?? -1);
if (currentChapter && Chapters.isDownloaded(currentChapter)) {
return (
<Tooltip title={t(actionToTranslationKey.delete.action.single)}>
@@ -44,7 +41,7 @@ const DownloadButton = ({
}
if (downloadChapter) {
return <DownloadStateIndicator download={downloadChapter} />;
return <DownloadStateIndicator chapterId={downloadChapter.chapter.id} />;
}
return (
@@ -74,16 +71,6 @@ const BaseReaderNavBarDesktopActions = memo(
const pageRetryKeyPrefix = useRef<number>(0);
const downloadSubscription = requestManager.useDownloadSubscription();
const downloadChapter = useMemo(() => {
if (!currentChapter) {
return null;
}
return Chapters.getDownloadStatusFromCache(currentChapter?.id);
}, [downloadSubscription.data?.downloadStatusChanged, id]);
const haveSomePagesFailedToLoad = useMemo(
() => pageLoadStates.some((pageLoadState) => pageLoadState.error),
[pageLoadStates],
@@ -118,7 +105,7 @@ const BaseReaderNavBarDesktopActions = memo(
<ReplayIcon />
</IconButton>
</Tooltip>
<DownloadButton currentChapter={currentChapter} downloadChapter={downloadChapter} />
<DownloadButton currentChapter={currentChapter} />
<Tooltip title={t('chapter.action.label.open_on_source')}>
<IconButton
disabled={!realUrl}

View File

@@ -30,7 +30,7 @@ import { Chapters } from '@/modules/chapter/services/Chapters.ts';
export const ChapterUpdateCard = ({ chapter }: { chapter: ChapterUpdateListFieldsFragment }) => {
const { manga } = chapter;
const download = Chapters.getDownloadStatusFromCache(chapter.id);
const download = Chapters.useDownloadStatusFromCache(chapter.id);
const { t } = useTranslation();
const location = useLocation();
@@ -112,7 +112,7 @@ export const ChapterUpdateCard = ({ chapter }: { chapter: ChapterUpdateListField
</TypographyMaxLines>
</Box>
</Box>
{download && <DownloadStateIndicator download={download} />}
<DownloadStateIndicator chapterId={chapter.id} />
{download?.state === DownloadState.Error && (
<Tooltip title={t('global.button.retry')}>
<IconButton

View File

@@ -58,8 +58,6 @@ export const Updates: React.FC = () => {
const groupedUpdates = useMemo(() => groupByDate(updateEntries), [updateEntries]);
const groupCounts: number[] = useMemo(() => groupedUpdates.map((group) => group[1]), [groupedUpdates]);
requestManager.useDownloadSubscription();
const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey(
groupCounts,
useCallback((index) => groupedUpdates[index][0], [groupedUpdates]),