diff --git a/apple/Omnivore.xcodeproj/project.pbxproj b/apple/Omnivore.xcodeproj/project.pbxproj index d943ee073..4e0f6563b 100644 --- a/apple/Omnivore.xcodeproj/project.pbxproj +++ b/apple/Omnivore.xcodeproj/project.pbxproj @@ -1389,7 +1389,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.44.0; + MARKETING_VERSION = 1.45.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; @@ -1424,7 +1424,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.44.0; + MARKETING_VERSION = 1.45.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1479,7 +1479,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.44.0; + MARKETING_VERSION = 1.45.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1820,7 +1820,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.44.0; + MARKETING_VERSION = 1.45.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index c581f6646..92438d1ac 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -530,8 +530,13 @@ struct AnimatingCellHeight: AnimatableModifier { ) } else { HomeFeedGridView( + listTitle: $listTitle, + isListScrolled: $isListScrolled, + prefersListLayout: $prefersListLayout, + isEditMode: $isEditMode, + selection: $selection, viewModel: viewModel, - isListScrolled: $isListScrolled + showFeatureCards: showFeatureCards ) } }.sheet(isPresented: $viewModel.showLabelsSheet) { @@ -574,10 +579,6 @@ struct AnimatingCellHeight: AnimatableModifier { .dynamicTypeSize(.small ... .accessibility1) } - func menuItems(for item: Models.LibraryItem) -> some View { - libraryItemMenu(dataService: dataService, viewModel: viewModel, item: item) - } - var featureCard: some View { VStack(spacing: 0) { if Color.isDarkMode { @@ -745,7 +746,7 @@ struct AnimatingCellHeight: AnimatableModifier { .listRowSeparatorTint(Color.thBorderColor) .listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset)) .contextMenu { - menuItems(for: item) + libraryItemMenu(dataService: dataService, viewModel: viewModel, item: item) } .swipeActions(edge: .leading, allowsFullSwipe: true) { if let listConfig = viewModel.currentListConfig { @@ -938,11 +939,20 @@ struct AnimatingCellHeight: AnimatableModifier { @EnvironmentObject var dataService: DataService @EnvironmentObject var audioController: AudioController - @State var isContextMenuOpen = false + @Binding var listTitle: String + @Binding var isListScrolled: Bool + @Binding var prefersListLayout: Bool + @Binding var isEditMode: EditMode + @State private var showHideFeatureAlert = false + @Binding var selection: Set @ObservedObject var viewModel: HomeFeedViewModel - @Binding var isListScrolled: Bool + let showFeatureCards: Bool + + @State var shouldScrollToTop = false + @State var topItem: Models.LibraryItem? + @ObservedObject var networkMonitor = NetworkMonitor() func contextMenuActionHandler(item: Models.LibraryItem, action: GridCardAction) { switch action { @@ -972,10 +982,6 @@ struct AnimatingCellHeight: AnimatableModifier { .dynamicTypeSize(.small ... .accessibility1) } - func menuItems(for item: Models.LibraryItem) -> some View { - libraryItemMenu(dataService: dataService, viewModel: viewModel, item: item) - } - var body: some View { VStack(alignment: .leading) { Color.systemBackground.frame(height: 1) @@ -1018,7 +1024,7 @@ struct AnimatingCellHeight: AnimatableModifier { viewModel: viewModel ) .contextMenu { - menuItems(for: item) + libraryItemMenu(dataService: dataService, viewModel: viewModel, item: item) } .onAppear { if idx >= viewModel.fetcher.items.count - 5 { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index ceec00508..af5d6d68c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -13,7 +13,7 @@ enum LoadingBarStyle { @MainActor final class HomeFeedViewModel: NSObject, ObservableObject { let filterKey: String - @Published var fetcher: LibraryItemFetcher + @ObservedObject var fetcher: LibraryItemFetcher let folderConfigs: [String: LibraryListConfig] @Published var isLoading = false diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemMenu.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemMenu.swift index 2b68877bf..76b369d08 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemMenu.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemMenu.swift @@ -13,6 +13,19 @@ import Views action: { viewModel.itemUnderTitleEdit = item }, label: { Label("Edit Info", systemImage: "info.circle") } ) + Button( + action: { viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: !item.isArchived) }, + label: { + Label( + item.isArchived ? "Unarchive" : "Archive", + systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox" + ) + } + ) + Button( + action: { viewModel.removeLibraryItem(dataService: dataService, objectID: item.objectID) }, + label: { Label("Remove", systemImage: "trash") } + ) Button( action: { viewModel.itemUnderLabelEdit = item }, label: { Label(item.labels?.count == 0 ? "Add Labels" : "Edit Labels", systemImage: "tag") } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index b922c8298..639bb7c90 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -432,7 +432,7 @@ struct WebReaderContainerView: View { }, label: { Text(LocalText.readerSave) }) } #if os(iOS) - .sheet(item: $safariWebLink) { + .fullScreenCover(item: $safariWebLink) { SafariView(url: $0.url) .ignoresSafeArea(.all, edges: .bottom) } diff --git a/packages/api/src/jobs/integration/export_all_items.ts b/packages/api/src/jobs/integration/export_all_items.ts index fb29b20fb..930a4c8df 100644 --- a/packages/api/src/jobs/integration/export_all_items.ts +++ b/packages/api/src/jobs/integration/export_all_items.ts @@ -1,6 +1,6 @@ import { IntegrationType } from '../../entity/integration' import { findIntegration } from '../../services/integrations' -import { searchLibraryItems } from '../../services/library_item' +import { findRecentLibraryItems } from '../../services/library_item' import { findActiveUser } from '../../services/user' import { enqueueExportItem } from '../../utils/createTask' import { logger } from '../../utils/logger' @@ -39,31 +39,37 @@ export const exportAllItems = async (jobData: ExportAllItemsJobData) => { return } - // get paginated items from the database - const first = 50 - let after = 0 - for (;;) { - console.log('searching for items...', { - userId, - first, - after, - }) - const searchResult = await searchLibraryItems( - { from: after, size: first }, - userId - ) - const libraryItems = searchResult.libraryItems - const size = libraryItems.length - if (size === 0) { - break + const maxItems = 1000 + const limit = 100 + let offset = 0 + // get max 1000 most recent items from the database + while (offset < maxItems) { + const libraryItems = await findRecentLibraryItems(userId, limit, offset) + if (libraryItems.length === 0) { + logger.info('no library items found', { + userId, + }) + return } + logger.info('enqueuing export item...', { + userId, + offset, + integrationId, + }) + await enqueueExportItem({ userId, libraryItemIds: libraryItems.map((item) => item.id), integrationId, }) - after += size + offset += libraryItems.length + + logger.info('exported items', { + userId, + offset, + integrationId, + }) } } diff --git a/packages/api/src/jobs/integration/export_item.ts b/packages/api/src/jobs/integration/export_item.ts index 896ca45b4..cd99bba5e 100644 --- a/packages/api/src/jobs/integration/export_item.ts +++ b/packages/api/src/jobs/integration/export_item.ts @@ -21,7 +21,6 @@ export const exportItem = async (jobData: ExportItemJobData) => { if (libraryItems.length === 0) { logger.error('library items not found', { userId, - libraryItemIds, }) return } @@ -40,7 +39,6 @@ export const exportItem = async (jobData: ExportItemJobData) => { integrations.map(async (integration) => { const logObject = { userId, - libraryItemIds, integrationId: integration.id, } logger.info('exporting item...', logObject) diff --git a/packages/api/src/services/create_page_save_request.ts b/packages/api/src/services/create_page_save_request.ts index 084f7da3f..ce4ce137b 100644 --- a/packages/api/src/services/create_page_save_request.ts +++ b/packages/api/src/services/create_page_save_request.ts @@ -16,7 +16,7 @@ import { libraryItemToArticleSavingRequest, } from '../utils/helpers' import { logger } from '../utils/logger' -import { countByCreatedAt, createOrUpdateLibraryItem } from './library_item' +import { countBySavedAt, createOrUpdateLibraryItem } from './library_item' interface PageSaveRequest { user: User @@ -38,12 +38,12 @@ const SAVING_CONTENT = 'Your link is being saved...' const isPrivateIP = privateIpLib.default -// 5 articles added in the last minute: use low queue +// 5 items saved in the last minute: use low queue // default: use normal queue const getPriorityByRateLimit = async ( userId: string ): Promise<'low' | 'high'> => { - const count = await countByCreatedAt(userId, new Date(Date.now() - 60 * 1000)) + const count = await countBySavedAt(userId, new Date(Date.now() - 60 * 1000)) return count >= 5 ? 'low' : 'high' } diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 4cb581784..cf0f080fb 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -652,6 +652,28 @@ export const searchLibraryItems = async ( ) } +export const findRecentLibraryItems = async ( + userId: string, + limit = 1000, + offset?: number +) => { + return authTrx( + async (tx) => + tx + .createQueryBuilder(LibraryItem, 'library_item') + .where('library_item.user_id = :userId', { userId }) + .andWhere('library_item.state = :state', { + state: LibraryItemState.Succeeded, + }) + .orderBy('library_item.saved_at', 'DESC', 'NULLS LAST') + .take(limit) + .skip(offset) + .getMany(), + undefined, + userId + ) +} + export const findLibraryItemsByIds = async (ids: string[], userId: string) => { return authTrx( async (tx) => @@ -659,7 +681,6 @@ export const findLibraryItemsByIds = async (ids: string[], userId: string) => { .createQueryBuilder(LibraryItem, 'library_item') .leftJoinAndSelect('library_item.labels', 'labels') .leftJoinAndSelect('library_item.highlights', 'highlights') - .leftJoinAndSelect('highlights.user', 'user') .where('library_item.id IN (:...ids)', { ids }) .getMany(), undefined, @@ -983,7 +1004,7 @@ export const findLibraryItemsByPrefix = async ( ) } -export const countByCreatedAt = async ( +export const countBySavedAt = async ( userId: string, startDate = new Date(0), endDate = new Date() @@ -993,7 +1014,7 @@ export const countByCreatedAt = async ( tx .createQueryBuilder(LibraryItem, 'library_item') .where('library_item.user_id = :userId', { userId }) - .andWhere('library_item.created_at between :startDate and :endDate', { + .andWhere('library_item.saved_at between :startDate and :endDate', { startDate, endDate, }) diff --git a/packages/api/src/services/update_pdf_content.ts b/packages/api/src/services/update_pdf_content.ts index ed9822ce2..75d3e8438 100644 --- a/packages/api/src/services/update_pdf_content.ts +++ b/packages/api/src/services/update_pdf_content.ts @@ -40,8 +40,7 @@ export const updateContentForFileItem = async (msg: UpdateContentMessage) => { .withRepository(libraryItemRepository) .createQueryBuilder('item') .innerJoinAndSelect('item.uploadFile', 'file') - .where('item.user = :userId', { userId: uploadFile.user.id }) - .andWhere('file.id = :fileId', { fileId }) + .where('file.id = :fileId', { fileId }) .getOne(), undefined, uploadFile.user.id diff --git a/packages/db/migrations/0162.do.library_item_user_id_state_index.sql b/packages/db/migrations/0162.do.library_item_user_id_state_index.sql new file mode 100755 index 000000000..942896ca1 --- /dev/null +++ b/packages/db/migrations/0162.do.library_item_user_id_state_index.sql @@ -0,0 +1,5 @@ +-- Type: DO +-- Name: library_item_user_id_state_index +-- Description: Create an index on omnivore.library_item table for querying by user_id and state + +CREATE INDEX CONCURRENTLY IF NOT EXISTS library_item_user_id_state_idx ON omnivore.library_item (user_id, state); diff --git a/packages/db/migrations/0162.undo.library_item_user_id_state_index.sql b/packages/db/migrations/0162.undo.library_item_user_id_state_index.sql new file mode 100755 index 000000000..b3d612b9b --- /dev/null +++ b/packages/db/migrations/0162.undo.library_item_user_id_state_index.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: library_item_user_id_state_index +-- Description: Create an index on omnivore.library_item table for querying by user_id and state + +BEGIN; + +DROP INDEX IF EXISTS omnivore.library_item_user_id_state_idx; + +COMMIT; diff --git a/packages/db/migrations/0163.do.api_key_index.sql b/packages/db/migrations/0163.do.api_key_index.sql new file mode 100755 index 000000000..03db73c1d --- /dev/null +++ b/packages/db/migrations/0163.do.api_key_index.sql @@ -0,0 +1,5 @@ +-- Type: DO +-- Name: api_key_index +-- Description: Create an index for checking key in api_key table + +CREATE INDEX CONCURRENTLY IF NOT EXISTS api_key_key_idx ON omnivore.api_key (key); diff --git a/packages/db/migrations/0163.undo.api_key_index.sql b/packages/db/migrations/0163.undo.api_key_index.sql new file mode 100755 index 000000000..46dfa8cf6 --- /dev/null +++ b/packages/db/migrations/0163.undo.api_key_index.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: api_key_index +-- Description: Create an index for checking key in api_key table + +BEGIN; + +DROP INDEX IF EXISTS omnivore.api_key_key_idx; + +COMMIT; diff --git a/packages/db/migrations/0164.do.library_item_file_id_index.sql b/packages/db/migrations/0164.do.library_item_file_id_index.sql new file mode 100755 index 000000000..f962642dd --- /dev/null +++ b/packages/db/migrations/0164.do.library_item_file_id_index.sql @@ -0,0 +1,5 @@ +-- Type: DO +-- Name: library_item_file_id_index +-- Description: create an index for upload_file_id column on library_item table + +CREATE INDEX CONCURRENTLY IF NOT EXISTS library_item_upload_file_id_idx ON omnivore.library_item (upload_file_id); diff --git a/packages/db/migrations/0164.undo.library_item_file_id_index.sql b/packages/db/migrations/0164.undo.library_item_file_id_index.sql new file mode 100755 index 000000000..e2db369e7 --- /dev/null +++ b/packages/db/migrations/0164.undo.library_item_file_id_index.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: library_item_file_id_index +-- Description: create an index for upload_file_id column on library_item table + +BEGIN; + +DROP INDEX IF EXISTS omnivore.library_item_upload_file_id_idx; + +COMMIT; diff --git a/packages/web/components/elements/icons/ConfusedSlothIcon.tsx b/packages/web/components/elements/icons/ConfusedSlothIcon.tsx new file mode 100644 index 000000000..47f300a3e --- /dev/null +++ b/packages/web/components/elements/icons/ConfusedSlothIcon.tsx @@ -0,0 +1,323 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { useCurrentTheme } from '../../../lib/hooks/useCurrentTheme' +import { useDarkModeListener } from '../../../lib/hooks/useDarkModeListener' +import { IconProps } from './IconProps' + +import React from 'react' + +export function ConfusedSlothIcon(): JSX.Element { + const { currentThemeIsDark } = useCurrentTheme() + console.log('is dark mdoe: ', currentThemeIsDark) + return currentThemeIsDark ? ( + + ) : ( + + ) +} + +class ConfusedSlothIconDark extends React.Component { + render() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) + } +} + +class ConfusedSlothIconLight extends React.Component { + render() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/ErrorSlothIcon.tsx b/packages/web/components/elements/icons/ErrorSlothIcon.tsx new file mode 100644 index 000000000..5d986c30c --- /dev/null +++ b/packages/web/components/elements/icons/ErrorSlothIcon.tsx @@ -0,0 +1,261 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { useCurrentTheme } from '../../../lib/hooks/useCurrentTheme' +import { IconProps } from './IconProps' + +import React from 'react' + +export function ErrorSlothIcon(): JSX.Element { + const { currentThemeIsDark } = useCurrentTheme() + return currentThemeIsDark ? : +} + +class ErrorSlothIconDark extends React.Component { + render() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) + } +} + +class ErrorSlothIconLight extends React.Component { + render() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) + } +} diff --git a/packages/web/components/templates/PrimaryDropdown.tsx b/packages/web/components/templates/PrimaryDropdown.tsx index 98d1a4b5c..e7f2d727a 100644 --- a/packages/web/components/templates/PrimaryDropdown.tsx +++ b/packages/web/components/templates/PrimaryDropdown.tsx @@ -1,8 +1,7 @@ import { useRouter } from 'next/router' import { Moon, Sun } from 'phosphor-react' -import { ReactNode, useCallback, useState } from 'react' +import { ReactNode, useCallback } from 'react' import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery' -import { getCurrentLocalTheme, updateTheme } from '../../lib/themeUpdater' import { Avatar } from '../elements/Avatar' import { AvatarDropdown } from '../elements/AvatarDropdown' import { @@ -16,7 +15,7 @@ import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives' import { StyledText } from '../elements/StyledText' import { styled, theme, ThemeId } from '../tokens/stitches.config' import { LayoutType } from './homeFeed/HomeFeedContainer' -import { DropdownMenu } from '@radix-ui/react-dropdown-menu' +import { useCurrentTheme } from '../../lib/hooks/useCurrentTheme' type PrimaryDropdownProps = { children?: ReactNode @@ -42,7 +41,7 @@ export type HeaderDropdownAction = | 'logout' type TriggerButtonProps = { - name: string + name?: string } const TriggerButton = (props: TriggerButtonProps): JSX.Element => { @@ -61,7 +60,7 @@ const TriggerButton = (props: TriggerButtonProps): JSX.Element => { }, }} > - + { whiteSpace: 'nowrap', }} > - {props.name} + {props.name ?? 'Settings'} ) @@ -125,15 +124,11 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element { [router] ) - if (!viewerData?.me) { - return <> - } - return ( + props.children ?? } css={{ width: '240px', ml: '15px' }} > @@ -154,7 +149,7 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element { }} > @@ -163,7 +158,7 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element { alignment="start" distribution="around" > - {viewerData.me && ( + {viewerData?.me && ( <> { - updateTheme(newTheme) - setDisplayTheme(newTheme) - }, - [setDisplayTheme] - ) + const { currentTheme, setCurrentTheme, currentThemeIsDark } = + useCurrentTheme() return ( <> @@ -311,18 +299,18 @@ function ThemeSection(props: PrimaryDropdownProps): JSX.Element { }} > { - doUpdateTheme(ThemeId.Light) + setCurrentTheme(ThemeId.Light) }} > Light { - doUpdateTheme(ThemeId.Dark) + setCurrentTheme(ThemeId.Dark) }} > Dark diff --git a/packages/web/components/templates/homeFeed/EmptyLibrary.tsx b/packages/web/components/templates/homeFeed/EmptyLibrary.tsx index 361f24f11..058fb8f48 100644 --- a/packages/web/components/templates/homeFeed/EmptyLibrary.tsx +++ b/packages/web/components/templates/homeFeed/EmptyLibrary.tsx @@ -1,14 +1,12 @@ -import { Box } from '../../elements/LayoutPrimitives' +import { Box, VStack } from '../../elements/LayoutPrimitives' import { useMemo } from 'react' -import { LIBRARY_LEFT_MENU_WIDTH } from '../navMenu/LibraryMenu' -import { LayoutType } from './HomeFeedContainer' import { SuggestionBox, SuggestionAction } from '../../elements/SuggestionBox' +import { ConfusedSlothIcon } from '../../elements/icons/ConfusedSlothIcon' +import { DEFAULT_HEADER_HEIGHT } from './HeaderSpacer' type EmptyLibraryProps = { searchTerm: string | undefined onAddLinkClicked: () => void - - layoutType: LayoutType } type MessageType = @@ -58,21 +56,13 @@ export const ErrorBox = (props: HelpMessageProps) => { return ( {errorTitle} @@ -181,6 +171,7 @@ export const EmptyLibrary = (props: EmptyLibraryProps) => { return 'files' case 'in:archive': return 'archive' + case 'in:following use:folders': case 'label:RSS': return 'feed' case 'has:subscriptions': @@ -193,40 +184,20 @@ export const EmptyLibrary = (props: EmptyLibraryProps) => { }, [props]) return ( - + - + ) } diff --git a/packages/web/components/templates/homeFeed/FetchItemsError.tsx b/packages/web/components/templates/homeFeed/FetchItemsError.tsx new file mode 100644 index 000000000..7a6f1c2d9 --- /dev/null +++ b/packages/web/components/templates/homeFeed/FetchItemsError.tsx @@ -0,0 +1,56 @@ +import { VStack } from '../../elements/LayoutPrimitives' +import { StyledText } from '../../elements/StyledText' +import { ErrorSlothIcon } from '../../elements/icons/ErrorSlothIcon' +import { DEFAULT_HEADER_HEIGHT } from './HeaderSpacer' + +export const FetchItemsError = (): JSX.Element => { + return ( + + + + Something has gone wrong. + + + We have encountered unexpected problems.{' '} + + Get help + + + + ) +} diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index f64700bca..403656aa0 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -49,6 +49,9 @@ import { saveUrlMutation } from '../../../lib/networking/mutations/saveUrlMutati import { articleQuery } from '../../../lib/networking/queries/useGetArticleQuery' import { PinnedButtons } from './PinnedButtons' import { PinnedSearch } from '../../../pages/settings/pinned-searches' +import { ErrorSlothIcon } from '../../elements/icons/ErrorSlothIcon' +import { DEFAULT_HEADER_HEIGHT } from './HeaderSpacer' +import { FetchItemsError } from './FetchItemsError' export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT' export type LibraryMode = 'reads' | 'highlights' @@ -109,8 +112,11 @@ export function HomeFeedContainer(): JSX.Element { isValidating, performActionOnItem, mutate, + error: fetchItemsError, } = useGetLibraryItemsQuery(queryInputs) + console.log('fetchItemsError fetchItemsError: ', fetchItemsError) + useEffect(() => { const handleRevalidate = () => { ;(async () => { @@ -856,6 +862,7 @@ export function HomeFeedContainer(): JSX.Element { hasData={!!itemsPages} totalItems={itemsPages?.[0].search.pageInfo.totalCount || 0} isValidating={isValidating} + fetchItemsError={!!fetchItemsError} labelsTarget={labelsTarget} setLabelsTarget={setLabelsTarget} notebookTarget={notebookTarget} @@ -890,6 +897,8 @@ export type HomeFeedContentProps = { hasData: boolean totalItems: number isValidating: boolean + fetchItemsError: boolean + loadMore: () => void labelsTarget: LibraryItem | undefined setLabelsTarget: (target: LibraryItem | undefined) => void @@ -954,6 +963,16 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { const [showFilterMenu, setShowFilterMenu] = useState(false) + const showItems = useMemo(() => { + if (props.fetchItemsError) { + return false + } + if (!props.isValidating && props.items.length <= 0) { + return false + } + return true + }, [props]) + return ( )} - {!props.isValidating && props.mode == 'highlights' && ( + + {!showItems && props.fetchItemsError && } + {!showItems && !props.fetchItemsError && props.items.length <= 0 && ( + { + props.setShowAddLinkModal(true) + }} + /> + )} + + {showItems && props.mode == 'highlights' && ( )} - {props.mode == 'reads' && ( + {showItems && props.mode == 'reads' && ( - {!props.isValidating && props.items.length == 0 ? ( - { - props.setShowAddLinkModal(true) - }} - /> - ) : ( - - )} + { + if ( + themeId === 'Dark' || + themeId === 'Darker' || + themeId === 'Apollo' || + themeId == 'Black' + ) { + return true + } + return false +} + +export const isLightTheme = (themeId: string): boolean => { + if (themeId === 'Sepia' || themeId == 'Light') { + return true + } + return false +} + export function useCurrentTheme() { const isDarkMode = useDarkModeListener() const [currentThemeInternal, setCurrentThemeInternal] = usePersistedState< @@ -29,25 +48,6 @@ export function useCurrentTheme() { } ) - const isDarkTheme = (themeId: string): boolean => { - if ( - themeId === 'Dark' || - themeId === 'Darker' || - themeId === 'Apollo' || - themeId == 'Black' - ) { - return true - } - return false - } - - const isLightTheme = (themeId: string): boolean => { - if (themeId === 'Sepia' || themeId == 'Light') { - return true - } - return false - } - const currentTheme = useMemo(() => { return currentThemeInternal }, [currentThemeInternal]) @@ -94,9 +94,17 @@ export function useCurrentTheme() { setCurrentThemeInternal, ]) + const currentThemeIsDark = useMemo(() => { + if (currentTheme) { + return isDarkTheme(currentTheme) + } + return false + }, [currentTheme]) + return { currentTheme, setCurrentTheme, resetSystemTheme, + currentThemeIsDark, } } diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index c289b3f7c..9f9257fcf 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -34,6 +34,7 @@ type LibraryItemsQueryResponse = { itemsDataError?: unknown isLoading: boolean isValidating: boolean + error: boolean size: number setSize: ( size: number | ((_size: number) => number) @@ -447,5 +448,6 @@ export function useGetLibraryItemsQuery({ size, setSize, mutate, + error: !!error, } } diff --git a/packages/web/stories/EmptyLibrary.stories.tsx b/packages/web/stories/EmptyLibrary.stories.tsx deleted file mode 100644 index a690ad1d8..000000000 --- a/packages/web/stories/EmptyLibrary.stories.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react' -import { EmptyLibrary } from '../components/templates/homeFeed/EmptyLibrary' - -export default { - title: 'Components/EmptyLibraryStory', - component: EmptyLibrary, - argTypes: { - position: { - description: 'The empty library component', - control: { type: 'select' }, - }, - }, -} as ComponentMeta - -export const EmptyLibraryStory: ComponentStory = ( - args: any -) => { - return ( - { - console.log('onAddLinkClicked') - }} - /> - ) -} diff --git a/yarn.lock b/yarn.lock index e151a2691..ffa3ed7f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16556,9 +16556,9 @@ graphql-fields@^2.0.3: integrity sha512-x3VE5lUcR4XCOxPIqaO4CE+bTK8u6gVouOdpQX9+EKHr+scqtK5Pp/l8nIGqIpN1TUlkKE6jDCCycm/WtLRAwA== graphql-middleware@^6.0.10: - version "6.1.32" - resolved "https://registry.yarnpkg.com/graphql-middleware/-/graphql-middleware-6.1.32.tgz#1b8a121d615efcf6d4b21d63f0a4818189700e0a" - integrity sha512-4vL+o9lljc1JavXNtMZg+gwDiZ9ORhgzAxf+7ef8r9/gn6D5sWxm2w8SdG6PLkKOsSM/tDunwSpfSJinn0XPCw== + version "6.1.35" + resolved "https://registry.yarnpkg.com/graphql-middleware/-/graphql-middleware-6.1.35.tgz#7297d41ceb35742b80640cca5cd85d1e47e8ab24" + integrity sha512-azawK7ApUYtcuPGRGBR9vDZu795pRuaFhO5fgomdJppdfKRt7jwncuh0b7+D3i574/4B+16CNWgVpnGVlg3ZCg== dependencies: "@graphql-tools/delegate" "^8.8.1" "@graphql-tools/schema" "^8.5.1"