diff --git a/src/UtilTypes.d.ts b/src/UtilTypes.d.ts index ddebeeb2..22ed4724 100644 --- a/src/UtilTypes.d.ts +++ b/src/UtilTypes.d.ts @@ -39,3 +39,11 @@ type TransformRecordToWithDefaultFlag> = { type MergeObjectsArray = T extends [infer F, ...infer R] ? F & MergeObjectsArray : {}; + +type OmitNotMatching = { + [P in K]: T[K]; +}; + +type ExtractCommon = { + [K in keyof T & keyof U]: T[K] extends U[K] ? T[K] : never; +}; diff --git a/src/modules/chapter/services/Chapters.ts b/src/modules/chapter/services/Chapters.ts index d62f54ba..5d1cb7d8 100644 --- a/src/modules/chapter/services/Chapters.ts +++ b/src/modules/chapter/services/Chapters.ts @@ -26,6 +26,7 @@ import { ReaderOpenChapterLocationState, ReaderResumeMode } from '@/modules/read import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { DOWNLOAD_TYPE_FIELDS } from '@/lib/graphql/fragments/DownloadFragments.ts'; +import { epochToDate, getDateString } from '@/util/DateHelper.ts'; export type ChapterAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread'; @@ -455,4 +456,19 @@ export class Chapters { updateInitialChapter, }; } + + /** + * Returns the chapters grouped by the passed key representing a timestamp. + * + * The timestamp gets mapped to a string via {@link getDateString} + */ + static groupByDate< + T extends Pick | Pick | Pick, + K extends keyof ExtractCommon, T>, + >(chapters: T[], key: K): Record { + return Object.groupBy(chapters, (chapter) => getDateString(epochToDate(Number(chapter[key])))) as Record< + string, + T[] + >; + } } diff --git a/src/modules/history/screens/History.tsx b/src/modules/history/screens/History.tsx index a0dcb6cd..9fc1d577 100644 --- a/src/modules/history/screens/History.tsx +++ b/src/modules/history/screens/History.tsx @@ -12,30 +12,15 @@ import { useTranslation } from 'react-i18next'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; -import { ChapterType } from '@/lib/graphql/generated/graphql.ts'; import { StyledGroupedVirtuoso } from '@/modules/core/components/virtuoso/StyledGroupedVirtuoso.tsx'; import { StyledGroupHeader } from '@/modules/core/components/virtuoso/StyledGroupHeader.tsx'; import { StyledGroupItemWrapper } from '@/modules/core/components/virtuoso/StyledGroupItemWrapper.tsx'; -import { epochToDate, getDateString } from '@/util/DateHelper.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { ChapterHistoryCard } from '@/modules/history/components/ChapterHistoryCard.tsx'; - -const groupByDate = (histories: Pick[]): [date: string, items: number][] => { - if (!histories.length) { - return []; - } - - const dateToItemMap = new Map(); - histories.forEach((item) => { - const date = getDateString(epochToDate(Number(item.lastReadAt))); - dateToItemMap.set(date, (dateToItemMap.get(date) ?? 0) + 1); - }); - - return [...dateToItemMap.entries()]; -}; +import { Chapters } from '@/modules/chapter/services/Chapters.ts'; export const History: React.FC = () => { const { t } = useTranslation(); @@ -54,7 +39,14 @@ export const History: React.FC = () => { const hasNextPage = !!chapterHistoryData?.chapters.pageInfo.hasNextPage; const endCursor = chapterHistoryData?.chapters.pageInfo.endCursor; const readEntries = chapterHistoryData?.chapters.nodes ?? []; - const groupedHistory = useMemo(() => groupByDate(readEntries), [readEntries]); + const groupedHistory = useMemo( + () => + Object.entries(Chapters.groupByDate(readEntries, 'lastReadAt')).map(([date, chapters]) => [ + date, + chapters.length, + ]) satisfies [date: string, itemCount: number][], + [readEntries], + ); const groupCounts: number[] = useMemo(() => groupedHistory.map((group) => group[1]), [groupedHistory]); const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey( diff --git a/src/modules/updates/screens/Updates.tsx b/src/modules/updates/screens/Updates.tsx index e0fa3fd4..dabc6a9f 100644 --- a/src/modules/updates/screens/Updates.tsx +++ b/src/modules/updates/screens/Updates.tsx @@ -12,31 +12,17 @@ import { useTranslation } from 'react-i18next'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; -import { ChapterType } from '@/lib/graphql/generated/graphql.ts'; import { UpdateChecker } from '@/modules/core/components/UpdateChecker.tsx'; import { StyledGroupedVirtuoso } from '@/modules/core/components/virtuoso/StyledGroupedVirtuoso.tsx'; import { StyledGroupHeader } from '@/modules/core/components/virtuoso/StyledGroupHeader.tsx'; import { StyledGroupItemWrapper } from '@/modules/core/components/virtuoso/StyledGroupItemWrapper.tsx'; -import { dateTimeFormatter, epochToDate, getDateString } from '@/util/DateHelper.ts'; +import { dateTimeFormatter } from '@/util/DateHelper.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { ChapterUpdateCard } from '@/modules/updates/components/ChapterUpdateCard.tsx'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; - -const groupByDate = (updates: Pick[]): [date: string, items: number][] => { - if (!updates.length) { - return []; - } - - const dateToItemMap = new Map(); - updates.forEach((item) => { - const date = getDateString(epochToDate(Number(item.fetchedAt))); - dateToItemMap.set(date, (dateToItemMap.get(date) ?? 0) + 1); - }); - - return [...dateToItemMap.entries()]; -}; +import { Chapters } from '@/modules/chapter/services/Chapters.ts'; export const Updates: React.FC = () => { const { t } = useTranslation(); @@ -55,7 +41,14 @@ export const Updates: React.FC = () => { const hasNextPage = !!chapterUpdateData?.chapters.pageInfo.hasNextPage; const endCursor = chapterUpdateData?.chapters.pageInfo.endCursor; const updateEntries = chapterUpdateData?.chapters.nodes ?? []; - const groupedUpdates = useMemo(() => groupByDate(updateEntries), [updateEntries]); + const groupedUpdates = useMemo( + () => + Object.entries(Chapters.groupByDate(updateEntries, 'fetchedAt')).map(([date, chapters]) => [ + date, + chapters.length, + ]) satisfies [date: string, itemCount: number][], + [updateEntries], + ); const groupCounts: number[] = useMemo(() => groupedUpdates.map((group) => group[1]), [groupedUpdates]); const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey(