From 05fa3d0732d660b92ad125fd9bead7cab8a312d7 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Mon, 1 Jan 2024 22:40:39 +0100 Subject: [PATCH] Fix/chapter not getting deleted after being read (#532) * Create permanent subscriptions The subscriptions have to be permanently open, otherwise, the cache gets outdated. E.g. in case a view is open, which does not subscribe to the download updates, finished downloads are never received and thus, data of existing chapters/mangas in the cache get outdated * [Codegen][Tool] Get latest data from cache for automatic chapter deletion after reading The manga chapters query is on "standby" to prevent the reader from rerender in case the underlying query data updates, which would cause the reader to jump back to the beginning of the last read page. Thus, when checking if the chapter should be automatically deleted, the data from the query can't be used, since it is outdated. --- src/App.tsx | 16 ++++++++++++++++ src/components/chapter/ChapterList.tsx | 4 ++-- src/components/library/UpdateChecker.tsx | 4 ++-- src/lib/graphql/generated/apollo-helpers.ts | 10 +++++----- src/lib/graphql/generated/graphql.ts | 4 ++-- src/lib/requests/RequestManager.ts | 19 ++++++++++++++++--- src/lib/requests/client/GraphQLClient.ts | 17 +++++++++++++++++ src/screens/DownloadQueue.tsx | 1 - src/screens/Reader.tsx | 18 ++++++++++++++++-- src/screens/Updates.tsx | 4 ++-- src/screens/settings/About.tsx | 18 ++++++++++-------- tools/scripts/codegenFormatter.ts | 10 +++++----- 12 files changed, 93 insertions(+), 32 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7e565798..badfbd45 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,6 +35,7 @@ import { DownloadSettings } from '@/screens/settings/DownloadSettings.tsx'; import { ServerSettings } from '@/screens/settings/ServerSettings.tsx'; import { WebUISettings } from '@/screens/settings/WebUISettings.tsx'; import { ServerUpdateChecker } from '@/components/settings/ServerUpdateChecker.tsx'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; if (process.env.NODE_ENV !== 'production') { // Adds messages only in a dev environment @@ -52,10 +53,25 @@ const ScrollToTop = () => { return null; }; +/** + * Creates permanent subscriptions to always have the latest data. + * + * E.g. in case a view is open, which does not subscribe to the download updates, finished downloads are never received + * and thus, data of existing chapters/mangas in the cache get outdated + */ +const BackgroundSubscriptions = () => { + requestManager.useDownloadSubscription(); + requestManager.useUpdaterSubscription(); + requestManager.useWebUIUpdateSubscription(); + + return null; +}; + export const App: React.FC = () => ( + = ({ manga, isRefreshing }) => { const { t } = useTranslation(); - const { data: downloaderData } = requestManager.useDownloadSubscription(); - const queue = (downloaderData?.downloadChanged.queue as DownloadType[]) ?? []; + const { data: downloaderData } = requestManager.useGetDownloadStatus(); + const queue = (downloaderData?.downloadStatus.queue as DownloadType[]) ?? []; const [options, dispatch] = useChapterOptions(manga.id); const { data: chaptersData, loading: isLoading } = requestManager.useGetMangaChapters(manga.id); diff --git a/src/components/library/UpdateChecker.tsx b/src/components/library/UpdateChecker.tsx index a6955d1c..8fc718c6 100644 --- a/src/components/library/UpdateChecker.tsx +++ b/src/components/library/UpdateChecker.tsx @@ -38,8 +38,8 @@ export function UpdateChecker({ handleFinishedUpdate }: { handleFinishedUpdate?: const { data: lastUpdateTimestampData, refetch: reFetchLastTimestamp } = requestManager.useGetLastGlobalUpdateTimestamp(); const lastUpdateTimestamp = lastUpdateTimestampData?.lastUpdateTimestamp.timestamp; - const { data: updaterData } = requestManager.useUpdaterSubscription(); - const status = updaterData?.updateStatusChanged; + const { data: updaterData } = requestManager.useGetGlobalUpdateSummary(); + const status = updaterData?.updateStatus; const loading = !!status?.isRunning; const progress = useMemo( diff --git a/src/lib/graphql/generated/apollo-helpers.ts b/src/lib/graphql/generated/apollo-helpers.ts index 9a084967..7ab312c2 100644 --- a/src/lib/graphql/generated/apollo-helpers.ts +++ b/src/lib/graphql/generated/apollo-helpers.ts @@ -1,8 +1,8 @@ import {FieldPolicy, FieldReadFunction, Reference, TypePolicies, TypePolicy} from '@apollo/client/cache'; import { GetCategoryQueryVariables, GetChapterQueryVariables, - GetChaptersQuery, GetExtensionQueryVariables, GetGlobalMetadataQueryVariables, - GetMangaQueryVariables, GetSourceQueryVariables, + GetChaptersQuery, GetDownloadStatusQueryVariables, GetExtensionQueryVariables, GetGlobalMetadataQueryVariables, + GetMangaQueryVariables, GetSourceQueryVariables, GetUpdateStatusQueryVariables, GetWebuiUpdateStatusQueryVariables, } from "@/lib/graphql/generated/graphql.ts"; import {FieldFunctionOptions} from "@apollo/client/cache/inmemory/policies"; export type AboutServerPayloadKeySpecifier = ('buildTime' | 'buildType' | 'discord' | 'github' | 'name' | 'revision' | 'version' | AboutServerPayloadKeySpecifier)[]; @@ -518,10 +518,10 @@ export type QueryFieldPolicy = { chapters?: FieldPolicy | FieldReadFunction, checkForServerUpdates?: FieldPolicy | FieldReadFunction, checkForWebUIUpdate?: FieldPolicy | FieldReadFunction, - downloadStatus?: FieldPolicy | FieldReadFunction, + downloadStatus?: FieldPolicy> | FieldReadFunction>, extension?: FieldPolicy> | FieldReadFunction>, extensions?: FieldPolicy | FieldReadFunction, - getWebUIUpdateStatus?: FieldPolicy | FieldReadFunction, + getWebUIUpdateStatus?: FieldPolicy> | FieldReadFunction>, lastUpdateTimestamp?: FieldPolicy | FieldReadFunction, manga?: FieldPolicy> | FieldReadFunction>, mangas?: FieldPolicy | FieldReadFunction, @@ -531,7 +531,7 @@ export type QueryFieldPolicy = { settings?: FieldPolicy | FieldReadFunction, source?: FieldPolicy> | FieldReadFunction>, sources?: FieldPolicy | FieldReadFunction, - updateStatus?: FieldPolicy | FieldReadFunction, + updateStatus?: FieldPolicy> | FieldReadFunction>, validateBackup?: FieldPolicy | FieldReadFunction }; export type ReorderChapterDownloadPayloadKeySpecifier = ('clientMutationId' | 'downloadStatus' | ReorderChapterDownloadPayloadKeySpecifier)[]; diff --git a/src/lib/graphql/generated/graphql.ts b/src/lib/graphql/generated/graphql.ts index e6ea6ad3..aacede82 100644 --- a/src/lib/graphql/generated/graphql.ts +++ b/src/lib/graphql/generated/graphql.ts @@ -2369,14 +2369,14 @@ export type UpdateExtensionMutationVariables = Exact<{ }>; -export type UpdateExtensionMutation = { __typename?: 'Mutation', updateExtension: { __typename?: 'UpdateExtensionPayload', clientMutationId?: string | null, extension: { __typename?: 'ExtensionType', apkName: string, versionName: string, versionCode: number, isInstalled: boolean, isObsolete: boolean, hasUpdate: boolean } } }; +export type UpdateExtensionMutation = { __typename?: 'Mutation', updateExtension: { __typename?: 'UpdateExtensionPayload', clientMutationId?: string | null, extension: { __typename?: 'ExtensionType', pkgName: string, apkName: string, versionName: string, versionCode: number, isInstalled: boolean, isObsolete: boolean, hasUpdate: boolean } } }; export type UpdateExtensionsMutationVariables = Exact<{ input: UpdateExtensionsInput; }>; -export type UpdateExtensionsMutation = { __typename?: 'Mutation', updateExtensions: { __typename?: 'UpdateExtensionsPayload', clientMutationId?: string | null, extensions: Array<{ __typename?: 'ExtensionType', apkName: string, versionName: string, versionCode: number, isInstalled: boolean, isObsolete: boolean, hasUpdate: boolean }> } }; +export type UpdateExtensionsMutation = { __typename?: 'Mutation', updateExtensions: { __typename?: 'UpdateExtensionsPayload', clientMutationId?: string | null, extensions: Array<{ __typename?: 'ExtensionType', pkgName: string, apkName: string, versionName: string, versionCode: number, isInstalled: boolean, isObsolete: boolean, hasUpdate: boolean }> } }; export type InstallExternalExtensionMutationVariables = Exact<{ file: Scalars['Upload']['input']; diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index ed7fc7f2..2a59c97d 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -170,10 +170,17 @@ import { UpdateMangasCategoriesMutation, UpdateMangasCategoriesMutationVariables, UpdateMangaCategoriesPatchInput, + GetWebuiUpdateStatusQuery, + GetWebuiUpdateStatusQueryVariables, } from '@/lib/graphql/generated/graphql.ts'; import { GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.ts'; import { SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts'; -import { CHECK_FOR_SERVER_UPDATES, CHECK_FOR_WEBUI_UPDATE, GET_ABOUT } from '@/lib/graphql/queries/ServerInfoQuery.ts'; +import { + CHECK_FOR_SERVER_UPDATES, + CHECK_FOR_WEBUI_UPDATE, + GET_ABOUT, + GET_WEBUI_UPDATE_STATUS, +} from '@/lib/graphql/queries/ServerInfoQuery.ts'; import { GET_EXTENSIONS } from '@/lib/graphql/queries/ExtensionQuery.ts'; import { GET_EXTENSIONS_FETCH, @@ -2104,8 +2111,8 @@ export class RequestManager { } public useGetDownloadStatus( - options?: SubscriptionHookOptions, - ): SubscriptionResult { + options?: QueryHookOptions, + ): AbortableApolloUseQueryResponse { return this.doRequest(GQLMethod.USE_QUERY, GET_DOWNLOAD_STATUS, {}, options); } @@ -2163,6 +2170,12 @@ export class RequestManager { ): AbortableApolloMutationResponse { return this.doRequest(GQLMethod.MUTATION, RESET_WEBUI_UPDATE_STATUS, undefined, options); } + + public useGetWebUIUpdateStatus( + options?: QueryHookOptions, + ): AbortableApolloUseQueryResponse { + return this.doRequest(GQLMethod.USE_QUERY, GET_WEBUI_UPDATE_STATUS, undefined, options); + } } export const requestManager = new RequestManager(); diff --git a/src/lib/requests/client/GraphQLClient.ts b/src/lib/requests/client/GraphQLClient.ts index d5b42fd0..62c3a6bd 100644 --- a/src/lib/requests/client/GraphQLClient.ts +++ b/src/lib/requests/client/GraphQLClient.ts @@ -26,6 +26,8 @@ const typePolicies: StrictTypedTypePolicies = { SettingsType: { keyFields: [] }, DownloadStatus: { keyFields: [] }, DownloadType: { keyFields: ['chapter'] }, + WebUIUpdateStatus: { keyFields: [] }, + UpdateStatus: { keyFields: [] }, Query: { fields: { manga(_, { args, toReference }) { @@ -58,6 +60,21 @@ const typePolicies: StrictTypedTypePolicies = { key: args?.key, }); }, + downloadStatus(_, { toReference }) { + return toReference({ + __typename: 'DownloadStatus', + key: {}, + }); + }, + getWebUIUpdateStatus(_, { toReference }) { + return toReference({ + __typename: 'WebUIUpdateStatus', + key: {}, + }); + }, + updateStatus(_, { toReference }) { + return toReference({ __typename: 'UpdateStatus', key: {} }); + }, chapters: { keyArgs: ['condition', 'filter', 'orderBy', 'orderByType'], merge(existing, incoming) { diff --git a/src/screens/DownloadQueue.tsx b/src/screens/DownloadQueue.tsx index fbc07787..f19dfb0e 100644 --- a/src/screens/DownloadQueue.tsx +++ b/src/screens/DownloadQueue.tsx @@ -98,7 +98,6 @@ const DownloadChapterItem = ({ export const DownloadQueue: React.FC = () => { const { t } = useTranslation(); - requestManager.useDownloadSubscription(); const [reorderDownload, { reset: revertReorder }] = requestManager.useReorderChapterInDownloadQueue(); const { data: downloadStatusData, loading: isLoading } = requestManager.useGetDownloadStatus(); diff --git a/src/screens/Reader.tsx b/src/screens/Reader.tsx index 184cd18e..fb4a1192 100644 --- a/src/screens/Reader.tsx +++ b/src/screens/Reader.tsx @@ -31,6 +31,7 @@ import { useDebounce } from '@/util/useDebounce.ts'; import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts'; import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; +import { FULL_CHAPTER_FIELDS } from '@/lib/graphql/Fragments.ts'; const isDupChapter = async (chapterIndex: number, currentChapter: TChapter) => { const nextChapter = await requestManager.getChapter(currentChapter.manga.id, chapterIndex).response; @@ -191,9 +192,22 @@ export function Reader() { (mangaChapter) => mangaChapter.sourceOrder === chapterToDeleteSourceOrder, ); + if (!chapterToDelete) { + return -1; + } + + const chapterToDeleteUpToDateData = requestManager.graphQLClient.client.cache.readFragment({ + id: requestManager.graphQLClient.client.cache.identify({ + __typename: 'ChapterType', + id: chapterToDelete.id, + }), + fragment: FULL_CHAPTER_FIELDS, + fragmentName: 'FULL_CHAPTER_FIELDS', + }); + const shouldDeleteChapter = - chapterToDelete?.isDownloaded && - (!chapterToDelete?.isBookmarked || metadataSettings.deleteChaptersWithBookmark); + chapterToDeleteUpToDateData?.isDownloaded && + (!chapterToDeleteUpToDateData?.isBookmarked || metadataSettings.deleteChaptersWithBookmark); if (!shouldDeleteChapter) { return -1; } diff --git a/src/screens/Updates.tsx b/src/screens/Updates.tsx index 15f998ce..3fbe58aa 100644 --- a/src/screens/Updates.tsx +++ b/src/screens/Updates.tsx @@ -117,8 +117,8 @@ export const Updates: React.FC = () => { const updateEntries = chapterUpdateData?.chapters.nodes ?? []; const groupedUpdates = useMemo(() => groupByDate(updateEntries), [updateEntries]); const groupCounts: number[] = useMemo(() => groupedUpdates.map((group) => group[1]), [groupedUpdates]); - const { data: downloaderData } = requestManager.useDownloadSubscription(); - const queue = (downloaderData?.downloadChanged.queue as DownloadType[]) ?? []; + const { data: downloaderData } = requestManager.useGetDownloadStatus(); + const queue = (downloaderData?.downloadStatus.queue as DownloadType[]) ?? []; const lastUpdateTimestampCompRef = useRef(null); const [lastUpdateTimestampCompHeight, setLastUpdateTimestampCompHeight] = useState(0); diff --git a/src/screens/settings/About.tsx b/src/screens/settings/About.tsx index 1ad16842..8ce173b7 100644 --- a/src/screens/settings/About.tsx +++ b/src/screens/settings/About.tsx @@ -211,9 +211,11 @@ export function About() { } = requestManager.useCheckForWebUIUpdate({ notifyOnNetworkStatusChange: true }); const webUIUpdateCheckError = orgWebUIUpdateCheckError || webUIUpdateData?.checkForWebUIUpdate.tag === ''; - const { data: webUIUpdateStatusData } = requestManager.useWebUIUpdateSubscription(); - const { state: webUIUpdateState, progress: webUIUpdateProgress } = - webUIUpdateStatusData?.webUIUpdateStatusChange ?? { state: UpdateState.Idle, progress: 0 }; + const { data: webUIUpdateStatusData } = requestManager.useGetWebUIUpdateStatus(); + const { state: webUIUpdateState, progress: webUIUpdateProgress } = webUIUpdateStatusData?.getWebUIUpdateStatus ?? { + state: UpdateState.Idle, + progress: 0, + }; useEffect(() => { const isError = webUIUpdateState === UpdateState.Error; @@ -236,7 +238,7 @@ export function About() { makeToast( t('settings.about.webui.label.update_success', { - version: webUIUpdateStatusData!.webUIUpdateStatusChange.info.tag, + version: webUIUpdateStatusData!.getWebUIUpdateStatus.info.tag, }), 'success', ); @@ -245,16 +247,16 @@ export function About() { fragment: ABOUT_WEBUI, data: { __typename: 'AboutWebUI', - channel: webUIUpdateStatusData!.webUIUpdateStatusChange.info.channel, - tag: webUIUpdateStatusData!.webUIUpdateStatusChange.info.tag, + channel: webUIUpdateStatusData!.getWebUIUpdateStatus.info.channel, + tag: webUIUpdateStatusData!.getWebUIUpdateStatus.info.tag, }, }); requestManager.graphQLClient.client.cache.writeFragment({ fragment: WEBUI_UPDATE_CHECK, data: { __typename: 'WebUIUpdateCheck', - channel: webUIUpdateStatusData!.webUIUpdateStatusChange.info.channel, - tag: webUIUpdateStatusData!.webUIUpdateStatusChange.info.tag, + channel: webUIUpdateStatusData!.getWebUIUpdateStatus.info.channel, + tag: webUIUpdateStatusData!.getWebUIUpdateStatus.info.tag, updateAvailable: false, }, }); diff --git a/tools/scripts/codegenFormatter.ts b/tools/scripts/codegenFormatter.ts index e56e2752..7f749ac6 100644 --- a/tools/scripts/codegenFormatter.ts +++ b/tools/scripts/codegenFormatter.ts @@ -54,8 +54,8 @@ const addImports = format( `import {FieldPolicy, FieldReadFunction, Reference, TypePolicies, TypePolicy} from '@apollo/client/cache'; import { \tGetCategoryQueryVariables, GetChapterQueryVariables, -\tGetChaptersQuery, GetExtensionQueryVariables, GetGlobalMetadataQueryVariables, -\tGetMangaQueryVariables, GetSourceQueryVariables, +\tGetChaptersQuery, GetDownloadStatusQueryVariables, GetExtensionQueryVariables, GetGlobalMetadataQueryVariables, +\tGetMangaQueryVariables, GetSourceQueryVariables, GetUpdateStatusQueryVariables, GetWebuiUpdateStatusQueryVariables, } from "@/lib/graphql/generated/graphql.ts"; import {FieldFunctionOptions} from "@apollo/client/cache/inmemory/policies";`, ); @@ -95,10 +95,10 @@ const fixTypingOfQueryTypePolicies = format( \tchapters?: FieldPolicy | FieldReadFunction, \tcheckForServerUpdates?: FieldPolicy | FieldReadFunction, \tcheckForWebUIUpdate?: FieldPolicy | FieldReadFunction, -\tdownloadStatus?: FieldPolicy | FieldReadFunction, +\tdownloadStatus?: FieldPolicy> | FieldReadFunction>, \textension?: FieldPolicy> | FieldReadFunction>, \textensions?: FieldPolicy | FieldReadFunction, -\tgetWebUIUpdateStatus?: FieldPolicy | FieldReadFunction, +\tgetWebUIUpdateStatus?: FieldPolicy> | FieldReadFunction>, \tlastUpdateTimestamp?: FieldPolicy | FieldReadFunction, \tmanga?: FieldPolicy> | FieldReadFunction>, \tmangas?: FieldPolicy | FieldReadFunction, @@ -108,7 +108,7 @@ const fixTypingOfQueryTypePolicies = format( \tsettings?: FieldPolicy | FieldReadFunction, \tsource?: FieldPolicy> | FieldReadFunction>, \tsources?: FieldPolicy | FieldReadFunction, -\tupdateStatus?: FieldPolicy | FieldReadFunction, +\tupdateStatus?: FieldPolicy> | FieldReadFunction>, \tvalidateBackup?: FieldPolicy | FieldReadFunction };`, );