diff --git a/packages/web/components/elements/icons/EmptyHighlightsIcon.tsx b/packages/web/components/elements/icons/EmptyHighlightsIcon.tsx new file mode 100644 index 000000000..7cc465a81 --- /dev/null +++ b/packages/web/components/elements/icons/EmptyHighlightsIcon.tsx @@ -0,0 +1,110 @@ +/* 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 EmptyHighlightsIcon(): JSX.Element { + const { currentTheme } = useCurrentTheme() + switch (currentTheme) { + case 'Sepia': + return + case 'Apollo': + case 'Dark': + return + case 'Light': + return + } + return +} + +class EmptyHighlightsIconDark extends React.Component { + render() { + return ( + + + + + + + + + ) + } +} + +class EmptyHighlightsIconLight extends React.Component { + render() { + return ( + + + + + + + + + ) + } +} + +class EmptyHighlightsIconSepia extends React.Component { + render() { + return ( + + + + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/EmptyLibraryNotes.tsx b/packages/web/components/elements/icons/EmptyLibraryNotes.tsx new file mode 100644 index 000000000..e8437f301 --- /dev/null +++ b/packages/web/components/elements/icons/EmptyLibraryNotes.tsx @@ -0,0 +1,175 @@ +/* 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 EmptyLibraryIcon(): JSX.Element { + const { currentTheme } = useCurrentTheme() + switch (currentTheme) { + case 'Sepia': + return + case 'Apollo': + case 'Dark': + return + case 'Light': + return + } + return +} + +class EmptyLibraryIconDark extends React.Component { + render() { + return ( + + + + + + + + + ) + } +} + +class EmptyLibraryIconLight extends React.Component { + render() { + return ( + + + + + + + + + ) + } +} + +class EmptyLibraryIconSepia extends React.Component { + render() { + console.log('rendering sepia icon') + + return ( + + + + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/EmptyTrashIcon.tsx b/packages/web/components/elements/icons/EmptyTrashIcon.tsx new file mode 100644 index 000000000..f79980909 --- /dev/null +++ b/packages/web/components/elements/icons/EmptyTrashIcon.tsx @@ -0,0 +1,159 @@ +/* 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 EmptyTrashIcon(): JSX.Element { + const { currentTheme } = useCurrentTheme() + switch (currentTheme) { + case 'Sepia': + return + case 'Apollo': + case 'Dark': + return + case 'Light': + return + } + return +} + +class EmptyTrashIconDark extends React.Component { + render() { + return ( + + + + + + + + + ) + } +} + +class EmptyTrashIconLight extends React.Component { + render() { + return ( + + + + + + + + + ) + } +} + +class EmptyTrashIconSepia extends React.Component { + render() { + return ( + + + + + + ) + } +} diff --git a/packages/web/components/nav-containers/HighlightsContainer.tsx b/packages/web/components/nav-containers/HighlightsContainer.tsx index 84792e30e..a6a03716c 100644 --- a/packages/web/components/nav-containers/HighlightsContainer.tsx +++ b/packages/web/components/nav-containers/HighlightsContainer.tsx @@ -32,14 +32,13 @@ import { highlightColor } from '../../lib/themeUpdater' import { HighlightViewNote } from '../patterns/HighlightNotes' import { theme } from '../tokens/stitches.config' import { useDeleteHighlight } from '../../lib/networking/highlights/useItemHighlights' +import { EmptyLibrary } from '../templates/homeFeed/EmptyLibrary' const PAGE_SIZE = 10 export function HighlightsContainer(): JSX.Element { const router = useRouter() const viewer = useGetViewerQuery() - const [showFilterMenu, setShowFilterMenu] = useState(false) - const [_, setShowAddLinkModal] = useState(false) const { isLoading, setSize, size, data, mutate } = useGetHighlights({ first: PAGE_SIZE, @@ -73,12 +72,18 @@ export function HighlightsContainer(): JSX.Element { css={{ padding: '20px', margin: '30px', + width: '100%', '@mdDown': { margin: '0px', marginTop: '50px', }, }} > + {!isLoading && highlights.length < 1 && ( + + + + )} {highlights.map((highlight) => { return ( viewer.viewerData?.me && ( @@ -132,9 +137,8 @@ function HighlightCard(props: HighlightCardProps): JSX.Element { const [isOpen, setIsOpen] = useState(false) const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] = useState(undefined) - const [labelsTarget, setLabelsTarget] = useState( - undefined - ) + const [labelsTarget, setLabelsTarget] = + useState(undefined) const deleteHighlight = useDeleteHighlight() const viewInReader = useCallback( diff --git a/packages/web/components/templates/homeFeed/EmptyLibrary.tsx b/packages/web/components/templates/homeFeed/EmptyLibrary.tsx index 058fb8f48..1fec280de 100644 --- a/packages/web/components/templates/homeFeed/EmptyLibrary.tsx +++ b/packages/web/components/templates/homeFeed/EmptyLibrary.tsx @@ -1,58 +1,55 @@ import { Box, VStack } from '../../elements/LayoutPrimitives' import { useMemo } from 'react' import { SuggestionBox, SuggestionAction } from '../../elements/SuggestionBox' -import { ConfusedSlothIcon } from '../../elements/icons/ConfusedSlothIcon' import { DEFAULT_HEADER_HEIGHT } from './HeaderSpacer' +import { EmptyLibraryIcon } from '../../elements/icons/EmptyLibraryNotes' +import { EmptyHighlightsIcon } from '../../elements/icons/EmptyHighlightsIcon' +import { EmptyTrashIcon } from '../../elements/icons/EmptyTrashIcon' +import { useGetSubscriptions } from '../../../lib/networking/subscriptions/useGetSubscriptions' type EmptyLibraryProps = { - searchTerm: string | undefined - onAddLinkClicked: () => void + folder: string | undefined + onAddLinkClicked?: () => void | undefined } -type MessageType = - | 'inbox' - | 'continue' - | 'non-feed' - | 'highlight' - | 'unlabeled' - | 'files' - | 'archive' - | 'feed' - | 'subscription' - | 'newsletter' - | 'library' - -type HelpMessageProps = { - type: MessageType +const Icon = (props: EmptyLibraryProps) => { + console.log('empty icon:', props.folder) + switch (props.folder) { + case 'highlights': + return + case 'trash': + return + default: + return + } } -export const ErrorBox = (props: HelpMessageProps) => { - const errorTitle = useMemo(() => { - switch (props.type) { - case 'inbox': - return 'Your inbox is empty. The inbox will contain all your non-archived saved items.' - case 'continue': - return "No continue reading items. Continue Reading items are items you have started but haven't finished reading." - case 'non-feed': - return "No non-feed items found. Non-feed items are items you've add to the library using the mobile apps, browser extensions, or Add Link button. Not newsletter or feed items." - case 'highlight': - return 'No highlights found. Add highlights to your library by highlighting text in the reader view.' - case 'unlabeled': - return 'No unlabeled items found. Items without labels can be found here. Use this query to easily triage your library.' +const Title = (props: EmptyLibraryProps) => { + // If its the subscriptions folder we fetch the subscriptions + // to prevent a better message + const { data: subscriptions, isLoading } = useGetSubscriptions( + {}, + props.folder == 'subscriptions' + ) + + const titleText = (folder: string | undefined) => { + switch (folder) { + case 'highlights': + return 'You do not have any highlights yet.' + case 'trash': + return 'Your trash is empty.' case 'archive': return 'You do not have any archived items.' - case 'files': - return 'No files found.' - case 'feed': - return 'You do not have any feed items matching this query.' - case 'subscription': + case 'following': + if (isLoading || (subscriptions?.length ?? 0) > 0) { + return 'No subscription items found.' + } return 'You do not have any subscriptions.' - case 'newsletter': - return 'You do not have any newsletter items matching this query.' + case 'home': + return 'Your homepage is empty.' } - return 'No results found for this query.' - }, [props.type]) - + return 'No results found.' + } return ( { marginBlockStart: '0px', marginBlockEnd: '10px', lineHeight: '125%', - fontSize: '20px', + fontSize: '23px', fontFamily: '$inter', fontWeight: 'bold', + textAlign: 'center', }} > - {errorTitle} + {titleText(props.folder)} ) } -type SuggestionMessage = { - message: string - actions: SuggestionAction[] -} - -export const Suggestion = (props: HelpMessageProps) => { - const helpMessage = useMemo(() => { - switch (props.type) { - case 'feed': - return { - message: 'Want to add an RSS or Atom Subscription?', - actions: [ - { text: 'Add an RSS or Atom feed', url: '/settings/feeds' }, - ], - } +const Subtitle = (props: EmptyLibraryProps) => { + // If its the subscriptions folder we fetch the subscriptions + // to prevent a better message + const { data: subscriptions, isLoading } = useGetSubscriptions( + {}, + props.folder == 'subscriptions' + ) + const titleText = (folder: string | undefined) => { + switch (folder) { + case 'highlights': + return 'Highlight text while reading to start building your highlights library.' + case 'trash': + return 'Deleted items will appear here before they are permanently deleted.' case 'archive': - return { - message: - 'When you are done reading something archive it and it will be saved in Omnivore forever.', - actions: [ - { - text: 'Read the docs', - url: 'https://docs.omnivore.app/using/saving', - }, - ], - } - case 'files': - return { - message: - 'Drag PDFs into the library to add them to your Omnivore account.', - actions: [], - } - case 'newsletter': - return { - message: - 'Create an Omnivore email address and subscribe to newsletters.', - actions: [ - { - text: 'Create an email address for newsletters', - url: '/settings/emails', - }, - ], - } - case 'subscription': - return { - message: - 'Create an Omnivore email address and subscribe to newsletters or add a feed from the Feeds page.', - actions: [ - { text: 'Add an RSS or Atom feed', url: '/settings/feeds' }, - { - text: 'Create an email address for newsletters', - url: '/settings/emails', - }, - ], + return 'Archived items are hidden from your library but saved forever. You can always access them here.' + case 'following': + if (isLoading || (subscriptions?.length ?? 0) > 0) { + return 'No subscription items found.' } + return 'Get started by subscribing to newsletters or feeds from the settings menu.' } - return { - message: "Add a link or read more about Omnivore's Advanced Search.", - actions: [ - { - text: 'Read the Docs', - url: 'https://docs.omnivore.app/using/search.html', - }, - ], - } - }, [props.type]) - + return 'No results found.' + } return ( - <> - {helpMessage ? ( - - ) : ( - <> - )} - + + {titleText(props.folder)} + ) } export const EmptyLibrary = (props: EmptyLibraryProps) => { - const type = useMemo(() => { - if (props.searchTerm) { - switch (props.searchTerm) { - case 'in:inbox': - return 'inbox' - case 'in:inbox sort:read-desc is:reading': - return 'continue' - case 'no:subscription': - return 'non-feed' - // Handle both cases while we migrate - case 'has:highlights mode:highlights': - case 'in:all has:highlights mode:highlights': - return 'highlight' - case 'no:label': - return 'unlabeled' - case 'type:file': - return 'files' - case 'in:archive': - return 'archive' - case 'in:following use:folders': - case 'label:RSS': - return 'feed' - case 'has:subscriptions': - return 'subscription' - case 'label:Newsletter': - return 'newsletter' - } - } - return 'library' - }, [props]) - return ( { width: '100%', height: '100%', pb: '100px', + px: '25px', + color: '$thLibraryMenuSecondary', minHeight: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`, }} > - - - + + + <Subtitle {...props} /> </VStack> ) } diff --git a/packages/web/components/templates/library/LibraryContainer.tsx b/packages/web/components/templates/library/LibraryContainer.tsx index 74fd822d8..64d1f8ce4 100644 --- a/packages/web/components/templates/library/LibraryContainer.tsx +++ b/packages/web/components/templates/library/LibraryContainer.tsx @@ -945,7 +945,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { {!showItems && props.fetchItemsError && <FetchItemsError />} {!showItems && !props.fetchItemsError && props.items.length <= 0 && ( <EmptyLibrary - searchTerm={props.searchTerm} + folder={props.folder} onAddLinkClicked={() => { props.setShowAddLinkModal(true) }} diff --git a/packages/web/lib/networking/subscriptions/useGetSubscriptions.tsx b/packages/web/lib/networking/subscriptions/useGetSubscriptions.tsx new file mode 100644 index 000000000..6f8d8a694 --- /dev/null +++ b/packages/web/lib/networking/subscriptions/useGetSubscriptions.tsx @@ -0,0 +1,71 @@ +import { useQuery } from '@tanstack/react-query' +import { gqlFetcher } from '../networkHelpers' +import { gql } from 'graphql-request' +import { + Subscription, + SubscriptionType, +} from '../queries/useGetSubscriptionsQuery' + +export function useGetSubscriptions( + variables: { + type?: SubscriptionType | undefined + sortBy?: string | undefined + }, + enabled = true +) { + return useQuery({ + queryKey: ['subscriptions'], + queryFn: async () => { + const response = (await gqlFetcher(GQL_GET_SUBSCRIPTIONS, { + type: variables.type, + sort: { + by: variables.sortBy ?? 'UPDATED_TIME', + }, + })) as SubscriptionsData + if (response.subscriptions?.errorCodes?.length) { + throw new Error(response.subscriptions.errorCodes[0]) + } + return response.subscriptions.subscriptions + }, + enabled, + }) +} + +type SubscriptionsResult = { + errorCodes?: string[] + subscriptions: Subscription[] +} + +type SubscriptionsData = { + subscriptions: SubscriptionsResult +} + +const GQL_GET_SUBSCRIPTIONS = gql` + query GetSubscriptions($type: SubscriptionType, $sort: SortParams) { + subscriptions(type: $type, sort: $sort) { + ... on SubscriptionsSuccess { + subscriptions { + id + name + type + newsletterEmail + url + icon + description + status + unsubscribeMailTo + unsubscribeHttpUrl + createdAt + updatedAt + lastFetchedAt + fetchContentType + mostRecentItemDate + failedAt + } + } + ... on SubscriptionsError { + errorCodes + } + } + } +`