From 9e5fa7d81aad7eea33a82210fe908f1e4874002d Mon Sep 17 00:00:00 2001 From: Jack <24988688+jicedtea@users.noreply.github.com> Date: Tue, 11 Mar 2025 03:53:40 +0700 Subject: [PATCH] Add history page (#897) * Add History module and integrate it into the app Introduce the History module with components for displaying chapter updates, including UI for grouped updates by date. Update routing and constants to incorporate the new /history path. * Replace "Updates" and "History" with unified "Recent" module Merged the "Updates" and "History" screens into a single unified "Recent" module with corresponding navigation updates. Adjusted GraphQL types and localization files to reflect the changes. This simplifies navigation and improves user experience. * Refactor History screen to remove unused components. Removed unused states, layout effects, and props related to last update timestamp. Renamed variables for better clarity and updated labels and error messages to align with the history context. Simplified component structure for improved maintainability. * ``` Refactor tab handling and integrate dynamic height adjustments. Renamed enums for better clarity and consistency. Implemented a `tabsMenuHeight` prop to dynamically adjust component layouts based on observed height changes using `useResizeObserver`. Updated the `Updates` component to consider the calculated heights for improved rendering. ``` * Refactor `groupByDate` parameter and add `lastReadAt` field Renamed `updates` to `histories` in `groupByDate` for clarity and added a debugging log for input data. Updated GraphQL fragment to include the `lastReadAt` field to support additional functionality. * Refactor `groupByDate` parameter and add `lastReadAt` field Renamed `updates` to `histories` in `groupByDate` for clarity and added a debugging log for input data. Updated GraphQL fragment to include the `lastReadAt` field to support additional functionality. * Refactor `groupByDate` parameter and add `lastReadAt` field Renamed `updates` to `histories` in `groupByDate` for clarity and added a debugging log for input data. Updated GraphQL fragment to include the `lastReadAt` field to support additional functionality. * Refactor `groupByDate` parameter and add `lastReadAt` field Renamed `updates` to `histories` in `groupByDate` for clarity and added a debugging log for input data. Updated GraphQL fragment to include the `lastReadAt` field to support additional functionality. * "Optimize GraphQL type definition formatting in generated file Condensed the formatting of the GetChaptersHistoryQuery type to a single line for improved readability and consistency. This change does not affect functionality but enhances maintainability of the generated code." * Remove UpdateChecker component from History screen The UpdateChecker component and its related navbar action have been removed from the History screen. This simplifies the navbar setup and eliminates unused functionality in this context. * Refactor chapter fragment and type references Updated `ChapterRecentListFieldsFragment` to `ChapterUpdateListFieldsFragment` for better alignment with naming conventions. Adjusted type imports and query structures to maintain consistency across files. No functional changes introduced. * Refactor padding calculation logic in Updates screen. Renamed state variable and refactored its logic for clarity and consistency. The updated logic now uses a more descriptive name, `listPadding`, to enhance code readability and maintainability. * Pass `tabsMenuHeight` prop to History component Added `tabsMenuHeight` prop to the `History` component to properly calculate the height for the `StyledGroupedVirtuoso` element. This ensures consistency with other components and improves layout rendering. * Revert package.json * Add last page read tracking to chapter history Introduced "lastPageRead" and "pageCount" fields to track progress in chapter history. Updated UI to display the last page read along with total pages for chapters. Adjusted localization files to support the new "page" label. * Remove last page read display from ChapterHistoryCard The "last page read" information was removed from the ChapterHistoryCard component for a cleaner UI. This change simplifies the card's design by showing only the chapter name and relevant download state. * Forcing an empty commit. * Replace "Recent" screen with separate "Updates" and "History". The "Recent" feature has been split into two distinct screens, "Updates" and "History", for improved clarity and functionality. Routes, icons, and components have been updated accordingly, and redundant code related to "Recent" has been removed. * Clean * Revert vi.json * Remove unnecessary `useLocation` hook and update state logic. The `useLocation` hook was removed as it was not used elsewhere in the component. The state property for the routing logic was updated to use `Chapters.getReaderOpenChapterLocationState` for better encapsulation and clarity. * Update history localization by removing unused "page" key Removed the "page" key from the "history" section in the localization file as it is no longer in use. This cleanup helps keep the translation file consistent and easier to maintain. --------- Co-authored-by: NamNH --- public/locales/en.json | 10 +- src/App.tsx | 2 + src/lib/graphql/fragments/ChapterFragments.ts | 14 ++ src/lib/graphql/generated/graphql.ts | 16 +- src/lib/graphql/queries/ChapterQuery.ts | 37 +++++ src/lib/requests/RequestManager.ts | 39 +++++ src/modules/core/AppRoute.constants.ts | 8 + .../history/components/ChapterHistoryCard.tsx | 148 ++++++++++++++++++ src/modules/history/screens/History.tsx | 123 +++++++++++++++ .../components/DefaultNavBar.tsx | 9 ++ 10 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 src/modules/history/components/ChapterHistoryCard.tsx create mode 100644 src/modules/history/screens/History.tsx diff --git a/public/locales/en.json b/public/locales/en.json index 8c190209..db5a3f22 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -1522,5 +1522,13 @@ } }, "title": "Updates" + }, + "history": { + "error": { + "label": { + "no_history_available": "You have not read any series yet." + } + }, + "title": "History" } -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index ee656f53..9b8487f3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,6 +39,7 @@ const { CategorySettings } = loadable( const { SourceConfigure } = loadable(() => import('@/modules/source/screens/SourceConfigure.tsx'), lazyLoadFallback); const { SourceMangas } = loadable(() => import('@/modules/source/screens/SourceMangas.tsx'), lazyLoadFallback); const { Updates } = loadable(() => import('@/modules/updates/screens/Updates.tsx'), lazyLoadFallback); +const { History } = loadable(() => import('@/modules/history/screens/History.tsx'), lazyLoadFallback); const { LibrarySettings } = loadable(() => import('@/modules/library/screens/LibrarySettings.tsx'), lazyLoadFallback); const { DownloadSettings } = loadable( () => import('@/modules/downloads/screens/DownloadSettings.tsx'), @@ -158,6 +159,7 @@ const MainApp = () => { } /> } /> + } /> } /> } /> diff --git a/src/lib/graphql/fragments/ChapterFragments.ts b/src/lib/graphql/fragments/ChapterFragments.ts index b6886b3c..73351828 100644 --- a/src/lib/graphql/fragments/ChapterFragments.ts +++ b/src/lib/graphql/fragments/ChapterFragments.ts @@ -64,6 +64,7 @@ export const CHAPTER_LIST_FIELDS = gql` fetchedAt uploadDate + lastReadAt } `; @@ -79,3 +80,16 @@ export const CHAPTER_UPDATE_LIST_FIELDS = gql` } } `; + +export const CHAPTER_HISTORY_LIST_FIELDS = gql` + ${CHAPTER_LIST_FIELDS} + ${MANGA_BASE_FIELDS} + + fragment CHAPTER_HISTORY_LIST_FIELDS on ChapterType { + ...CHAPTER_LIST_FIELDS + + manga { + ...MANGA_BASE_FIELDS + } + } +`; diff --git a/src/lib/graphql/generated/graphql.ts b/src/lib/graphql/generated/graphql.ts index 545ad400..0b8b82be 100644 --- a/src/lib/graphql/generated/graphql.ts +++ b/src/lib/graphql/generated/graphql.ts @@ -2773,6 +2773,8 @@ export type ChapterListFieldsFragment = { __typename?: 'ChapterType', fetchedAt: export type ChapterUpdateListFieldsFragment = { __typename?: 'ChapterType', fetchedAt: string, uploadDate: string, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string } }; +export type ChapterHistoryListFieldsFragment = { __typename?: 'ChapterType', lastReadAt: string, lastPageRead: number, pageCount: number, uploadDate: string, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string } }; + export type DownloadTypeFieldsFragment = { __typename?: 'DownloadType', progress: number, state: DownloadState, tries: number, chapter: { __typename?: 'ChapterType', id: number, name: string, sourceOrder: number, isDownloaded: boolean }, manga: { __typename?: 'MangaType', id: number, title: string, downloadCount: number } }; export type DownloadStatusFieldsFragment = { __typename?: 'DownloadStatus', state: DownloaderState, queue: Array<{ __typename?: 'DownloadType', progress: number, state: DownloadState, tries: number, chapter: { __typename?: 'ChapterType', id: number, name: string, sourceOrder: number, isDownloaded: boolean }, manga: { __typename?: 'MangaType', id: number, title: string, downloadCount: number } }> }; @@ -3376,9 +3378,21 @@ export type GetChaptersUpdatesQueryVariables = Exact<{ order?: InputMaybe | ChapterOrderInput>; }>; - export type GetChaptersUpdatesQuery = { __typename?: 'Query', chapters: { __typename?: 'ChapterNodeList', totalCount: number, nodes: Array<{ __typename?: 'ChapterType', fetchedAt: string, uploadDate: string, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null } } }; +export type GetChaptersHistoryQueryVariables = Exact<{ + after?: InputMaybe; + before?: InputMaybe; + condition?: InputMaybe; + filter?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + offset?: InputMaybe; + order?: InputMaybe | ChapterOrderInput>; +}>; + +export type GetChaptersHistoryQuery = { __typename?: 'Query', chapters: { __typename?: 'ChapterNodeList', totalCount: number, nodes: Array<{ __typename?: 'ChapterType', lastReadAt: string, lastPageRead: number, pageCount: number, uploadDate: string, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null } } }; + export type GetMangasChapterIdsWithStateQueryVariables = Exact<{ mangaIds: Array | Scalars['Int']['input']; isDownloaded?: InputMaybe; diff --git a/src/lib/graphql/queries/ChapterQuery.ts b/src/lib/graphql/queries/ChapterQuery.ts index e04da0a4..1c844a79 100644 --- a/src/lib/graphql/queries/ChapterQuery.ts +++ b/src/lib/graphql/queries/ChapterQuery.ts @@ -13,6 +13,7 @@ import { CHAPTER_READER_FIELDS, CHAPTER_STATE_FIELDS, CHAPTER_UPDATE_LIST_FIELDS, + CHAPTER_HISTORY_LIST_FIELDS, } from '@/lib/graphql/fragments/ChapterFragments.ts'; // returns the current chapters from the database @@ -123,6 +124,42 @@ export const GET_CHAPTERS_UPDATES = gql` } `; +// returns the current chapters from the database +export const GET_CHAPTERS_HISTORY = gql` + ${CHAPTER_HISTORY_LIST_FIELDS} + ${PAGE_INFO} + + query GET_CHAPTERS_HISTORY( + $after: Cursor + $before: Cursor + $condition: ChapterConditionInput + $filter: ChapterFilterInput + $first: Int + $last: Int + $offset: Int + $order: [ChapterOrderInput!] + ) { + chapters( + after: $after + before: $before + condition: $condition + filter: $filter + first: $first + last: $last + offset: $offset + order: $order + ) { + nodes { + ...CHAPTER_HISTORY_LIST_FIELDS + } + pageInfo { + ...PAGE_INFO + } + totalCount + } + } +`; + export const GET_MANGAS_CHAPTER_IDS_WITH_STATE = gql` ${CHAPTER_STATE_FIELDS} diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index d5947a76..0fb81eb7 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -87,6 +87,8 @@ import { GetChaptersMangaQueryVariables, GetChaptersUpdatesQuery, GetChaptersUpdatesQueryVariables, + GetChaptersHistoryQuery, + GetChaptersHistoryQueryVariables, GetDownloadStatusQuery, GetDownloadStatusQueryVariables, GetExtensionsFetchMutation, @@ -267,6 +269,7 @@ import { STOP_DOWNLOADER, } from '@/lib/graphql/mutations/DownloaderMutation.ts'; import { + GET_CHAPTERS_HISTORY, GET_CHAPTERS_MANGA, GET_CHAPTERS_UPDATES, GET_MANGAS_CHAPTER_IDS_WITH_STATE, @@ -2886,6 +2889,42 @@ export class RequestManager { } as typeof result; } + public useGetRecentlyReadChapters( + initialPages: number = 1, + options?: QueryHookOptions, + ): AbortableApolloUseQueryResponse { + const PAGE_SIZE = 50; + const CACHE_KEY = 'useGetRecentlyReadChapters'; + + const offset = this.cache.getResponseFor(CACHE_KEY, undefined) ?? 0; + const [lastOffset] = useState(offset); + + const result = this.useGetChapters( + GET_CHAPTERS_HISTORY, + { + filter: { lastReadAt: { isNull: false, notEqualToAll: ['0'] } }, + order: [ + { by: ChapterOrderBy.LastReadAt, byType: SortOrder.Desc }, + { by: ChapterOrderBy.SourceOrder, byType: SortOrder.Desc }, + ], + first: initialPages * PAGE_SIZE + lastOffset, + }, + options, + ); + + return { + ...result, + fetchMore: (...args: Parameters<(typeof result)['fetchMore']>) => { + const fetchMoreOptions = args[0] ?? {}; + this.cache.cacheResponse(CACHE_KEY, undefined, fetchMoreOptions.variables?.offset); + return result.fetchMore({ + ...fetchMoreOptions, + variables: { first: PAGE_SIZE, ...fetchMoreOptions.variables }, + }); + }, + } as typeof result; + } + public startGlobalUpdate( categories?: undefined, options?: MutationOptions, diff --git a/src/modules/core/AppRoute.constants.ts b/src/modules/core/AppRoute.constants.ts index 3964b692..5767d8b1 100644 --- a/src/modules/core/AppRoute.constants.ts +++ b/src/modules/core/AppRoute.constants.ts @@ -129,6 +129,14 @@ export const AppRoutes = { match: 'updates', path: '/updates', }, + history: { + match: 'history', + path: '/history', + }, + recent: { + match: 'recent', + path: '/recent', + }, browse: { match: 'browse', path: '/browse', diff --git a/src/modules/history/components/ChapterHistoryCard.tsx b/src/modules/history/components/ChapterHistoryCard.tsx new file mode 100644 index 00000000..2f593dee --- /dev/null +++ b/src/modules/history/components/ChapterHistoryCard.tsx @@ -0,0 +1,148 @@ +/* + * 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 DownloadIcon from '@mui/icons-material/Download'; +import Box from '@mui/material/Box'; +import CardActionArea from '@mui/material/CardActionArea'; +import Avatar from '@mui/material/Avatar'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import IconButton from '@mui/material/IconButton'; +import { Link } from 'react-router-dom'; +import Refresh from '@mui/icons-material/Refresh'; +import { useTranslation } from 'react-i18next'; +import { memo } from 'react'; +import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx'; +import { DownloadStateIndicator } from '@/modules/core/components/DownloadStateIndicator.tsx'; +import { ChapterHistoryListFieldsFragment, DownloadState } from '@/lib/graphql/generated/graphql.ts'; +import { Mangas } from '@/modules/manga/services/Mangas.ts'; +import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx'; +import { TypographyMaxLines } from '@/modules/core/components/TypographyMaxLines.tsx'; +import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { makeToast } from '@/modules/core/utils/Toast.ts'; +import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { Chapters } from '@/modules/chapter/services/Chapters.ts'; + +export const ChapterHistoryCard = memo(({ chapter }: { chapter: ChapterHistoryListFieldsFragment }) => { + const { manga } = chapter; + const download = Chapters.useDownloadStatusFromCache(chapter.id); + + const { t } = useTranslation(); + + const handleRetry = async () => { + try { + await requestManager.addChapterToDownloadQueue(chapter.id).response; + } catch (e) { + makeToast(t('download.queue.error.label.failed_to_remove'), 'error', getErrorMessage(e)); + } + }; + + const downloadChapter = () => { + requestManager + .addChapterToDownloadQueue(chapter.id) + .response.catch((e) => + makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)), + ); + }; + + return ( + + theme.palette.text[chapter.isRead ? 'disabled' : 'primary'], + }} + > + + + + + + + + + + {manga.title} + + + {chapter.name} + + + + + {download?.state === DownloadState.Error && ( + + { + e.preventDefault(); + e.stopPropagation(); + handleRetry(); + }} + size="large" + > + + + + )} + {download == null && !chapter.isDownloaded && ( + + { + e.stopPropagation(); + e.preventDefault(); + downloadChapter(); + }} + size="large" + > + + + + )} + + + + ); +}); diff --git a/src/modules/history/screens/History.tsx b/src/modules/history/screens/History.tsx new file mode 100644 index 00000000..a0dcb6cd --- /dev/null +++ b/src/modules/history/screens/History.tsx @@ -0,0 +1,123 @@ +/* + * 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 Typography from '@mui/material/Typography'; +import React, { useCallback, useLayoutEffect, useMemo } from 'react'; +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()]; +}; + +export const History: React.FC = () => { + const { t } = useTranslation(); + + const { setTitle } = useNavBarContext(); + const { + data: chapterHistoryData, + loading: isLoading, + error, + fetchMore, + refetch, + } = requestManager.useGetRecentlyReadChapters(undefined, { + fetchPolicy: 'cache-and-network', + notifyOnNetworkStatusChange: true, + }); + const hasNextPage = !!chapterHistoryData?.chapters.pageInfo.hasNextPage; + const endCursor = chapterHistoryData?.chapters.pageInfo.endCursor; + const readEntries = chapterHistoryData?.chapters.nodes ?? []; + const groupedHistory = useMemo(() => groupByDate(readEntries), [readEntries]); + const groupCounts: number[] = useMemo(() => groupedHistory.map((group) => group[1]), [groupedHistory]); + + const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey( + groupCounts, + useCallback((index) => groupedHistory[index][0], [groupedHistory]), + useCallback((index) => readEntries[index].id, [readEntries]), + ); + + useLayoutEffect(() => { + setTitle(t('history.title')); + + return () => { + setTitle(''); + }; + }, [t]); + + const loadMore = useCallback(() => { + if (!hasNextPage) { + return; + } + + fetchMore({ variables: { offset: readEntries.length } }); + }, [hasNextPage, endCursor]); + + if (error) { + return ( + refetch().catch(defaultPromiseErrorHandler('History::refetch'))} + /> + ); + } + + if (!isLoading && readEntries.length === 0) { + return ; + } + + return ( + (isLoading ? : null), + }} + overscan={window.innerHeight * 0.5} + endReached={loadMore} + groupCounts={groupCounts} + groupContent={(index) => ( + + + {groupedHistory[index][0]} + + + )} + computeItemKey={computeItemKey} + itemContent={(index) => ( + + + + )} + /> + ); +}; diff --git a/src/modules/navigation-bar/components/DefaultNavBar.tsx b/src/modules/navigation-bar/components/DefaultNavBar.tsx index ae7dd84c..f1c3d6d1 100644 --- a/src/modules/navigation-bar/components/DefaultNavBar.tsx +++ b/src/modules/navigation-bar/components/DefaultNavBar.tsx @@ -15,6 +15,8 @@ import CollectionsBookmarkIcon from '@mui/icons-material/CollectionsBookmark'; import CollectionsOutlinedBookmarkIcon from '@mui/icons-material/CollectionsBookmarkOutlined'; import NewReleasesIcon from '@mui/icons-material/NewReleases'; import NewReleasesOutlinedIcon from '@mui/icons-material/NewReleasesOutlined'; +import HistoryIcon from '@mui/icons-material/History'; +import HistoryOutlinedIcon from '@mui/icons-material/HistoryOutlined'; import ExploreIcon from '@mui/icons-material/Explore'; import ExploreOutlinedIcon from '@mui/icons-material/ExploreOutlined'; import GetAppIcon from '@mui/icons-material/GetApp'; @@ -51,6 +53,13 @@ const navbarItems: Array = [ IconComponent: NewReleasesOutlinedIcon, show: 'both', }, + { + path: AppRoutes.history.path, + title: 'history.title', + SelectedIconComponent: HistoryIcon, + IconComponent: HistoryOutlinedIcon, + show: 'both', + }, { path: AppRoutes.browse.path, title: 'global.label.browse',