diff --git a/packages/web/components/elements/icons/ArrowRightIcon.tsx b/packages/web/components/elements/icons/ArrowRightIcon.tsx new file mode 100644 index 000000000..4077c3b28 --- /dev/null +++ b/packages/web/components/elements/icons/ArrowRightIcon.tsx @@ -0,0 +1,43 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from './IconProps' + +import React from 'react' + +export class ArrowRightIcon extends React.Component { + render() { + const size = (this.props.size || 26).toString() + const color = (this.props.color || '#2A2A2A').toString() + + return ( + + + + + + + + ) + } +} diff --git a/packages/web/components/templates/ErrorLayout.tsx b/packages/web/components/templates/ErrorLayout.tsx index 4c8881d5c..b0837c883 100644 --- a/packages/web/components/templates/ErrorLayout.tsx +++ b/packages/web/components/templates/ErrorLayout.tsx @@ -1,8 +1,4 @@ -import { - VStack, - HStack, - SpanBox, -} from '../elements/LayoutPrimitives' +import { VStack, HStack, SpanBox } from '../elements/LayoutPrimitives' import { StyledText } from '../elements/StyledText' import Link from 'next/link' import { Button } from '../elements/Button' @@ -22,11 +18,14 @@ export function ErrorLayout(props: ErrorLayoutProps): JSX.Element { return ( - + {props.statusCode} @@ -34,9 +33,11 @@ export function ErrorLayout(props: ErrorLayoutProps): JSX.Element { - - + + ) -} \ No newline at end of file +} diff --git a/packages/web/components/templates/homeFeed/EmptyLibrary.tsx b/packages/web/components/templates/homeFeed/EmptyLibrary.tsx index 95b4ff530..92e04e1da 100644 --- a/packages/web/components/templates/homeFeed/EmptyLibrary.tsx +++ b/packages/web/components/templates/homeFeed/EmptyLibrary.tsx @@ -1,51 +1,237 @@ import Link from 'next/link' import { Book } from 'phosphor-react' import { Button } from '../../elements/Button' -import { VStack } from '../../elements/LayoutPrimitives' +import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { StyledText } from '../../elements/StyledText' import { theme } from '../../tokens/stitches.config' +import { useMemo } from 'react' +import { searchQuery } from '../../../lib/networking/queries/search' +import { LIBRARY_LEFT_MENU_WIDTH } from './LibraryFilterMenu' +import { LayoutType } from './HomeFeedContainer' +import { ArrowRightIcon } from '../../elements/icons/ArrowRightIcon' type EmptyLibraryProps = { + searchTerm: string | undefined onAddLinkClicked: () => void + + layoutType: LayoutType } -export function EmptyLibrary(props: EmptyLibraryProps): JSX.Element { +type MessageType = 'feed' | 'newsletter' | 'library' + +type HelpMessageProps = { + type: MessageType +} + +const HelpMessage = (props: HelpMessageProps) => { + switch (props.type) { + case 'library': + return ( + <> + You can add a link or read more about Omnivore's{' '} + + advanced search + + . + + ) + case 'feed': + return ( + <> + You can subscribe to RSS feeds using the{' '} + + feeds page + + . Learn more about feeds at 's{' '} + + docs.omnivore.app/using/feeds.html + + . + + ) + case 'newsletter': + return ( + <> + Create email addresses that can be used to subscribe to newsletters on + the{' '} + + emails page + + . Learn more about reading newsletters in Omnivore at 's{' '} + + docs.omnivore.app/using/inbox.html + + . + + ) + } + return <> +} + +export const ErrorBox = (props: HelpMessageProps) => { + const errorTitle = useMemo(() => { + switch (props.type) { + case 'feed': + return 'You do not have any feed items matching this query.' + case 'newsletter': + return 'You do not have any newsletter item matching this query.' + } + return 'No results found for this query.' + }, [props.type]) + return ( - - - - No results found. - - - - You can add a link or read more about Omnivore's{' '} - - advanced search - - . - - - - + {errorTitle} + + ) +} + +export const SuggestionBox = (props: HelpMessageProps) => { + const helpMessage = useMemo(() => { + switch (props.type) { + case 'feed': + return 'Want to add an RSS or Atom Subscription?' + case 'newsletter': + return 'Create an Omnivore email address and subscribe to newsletters.' + } + return "Add a link or read more about Omnivore's Advanced Search." + }, [props.type]) + + const helpTarget = useMemo(() => { + switch (props.type) { + case 'feed': + return '/settings/feeds' + case 'newsletter': + return '/settings/emails' + } + return 'https://docs.omnivore.app/' + }, [props.type]) + + return ( + + {helpMessage} + + + + <>Click Here + + + + + + ) +} + +export const EmptyLibrary = (props: EmptyLibraryProps) => { + const type = useMemo(() => { + if (props.searchTerm) { + switch (props.searchTerm) { + case 'label:RSS': + return 'feed' + case 'label:Newsletter': + return 'newsletter' + } + } + return 'library' + }, [props]) + + return ( + + + + ) } diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index ae89e8f0f..338e3e833 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -2,7 +2,7 @@ import { Action, createAction, useKBar, useRegisterActions } from 'kbar' import debounce from 'lodash/debounce' import { useRouter } from 'next/router' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import toast, { Toaster } from "react-hot-toast" +import toast, { Toaster } from 'react-hot-toast' import TopBarProgress from 'react-topbar-progress-indicator' import { useFetchMore } from '../../../lib/hooks/useFetchMoreScroll' import { usePersistedState } from '../../../lib/hooks/usePersistedState' @@ -48,8 +48,9 @@ import { } from '../../../lib/toastHelpers' import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter' import { NotebookPresenter } from '../article/NotebookPresenter' -import { saveUrlMutation } from "../../../lib/networking/mutations/saveUrlMutation" -import { articleQuery } from "../../../lib/networking/queries/useGetArticleQuery" +import { saveUrlMutation } from '../../../lib/networking/mutations/saveUrlMutation' +import { articleQuery } from '../../../lib/networking/queries/useGetArticleQuery' +import { searchQuery } from '../../../lib/networking/queries/search' export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT' export type LibraryMode = 'reads' | 'highlights' @@ -70,7 +71,7 @@ const debouncedFetchSearchResults = debounce((query, cb) => { // We set a relatively high delay for the refresh at the end, as it's likely there's an issue // in processing. We give it the best attempt to be able to resolve, but if it doesn't we set // the state as Failed. On refresh it will try again if the backend sends "PROCESSING" -const TIMEOUT_DELAYS = [1000, 2000, 2500, 3500, 5000, 10000, 60000]; +const TIMEOUT_DELAYS = [1000, 2000, 2500, 3500, 5000, 10000, 60000] export function HomeFeedContainer(): JSX.Element { const { viewerData } = useGetViewerQuery() @@ -179,58 +180,69 @@ export function HomeFeedContainer(): JSX.Element { return itemsPages[itemsPages.length - 1].search.pageInfo.hasNextPage }, [itemsPages]) - const libraryItems = useMemo(() => { const items = itemsPages?.flatMap((ad) => { - return ad.search.edges.map(it => ({ ...it, isLoading: it.node.state === 'PROCESSING'})); + return ad.search.edges.map((it) => ({ + ...it, + isLoading: it.node.state === 'PROCESSING', + })) }) || [] return items }, [itemsPages, performActionOnItem]) useEffect(() => { - const timeout : NodeJS.Timeout[] = [] + const timeout: NodeJS.Timeout[] = [] - const items = - (itemsPages?.flatMap((ad) => { - return ad.search.edges.map(it => ({ ...it, isLoading: it.node.state === 'PROCESSING'})); - }) || []) - .filter(it => it.isLoading); + const items = ( + itemsPages?.flatMap((ad) => { + return ad.search.edges.map((it) => ({ + ...it, + isLoading: it.node.state === 'PROCESSING', + })) + }) || [] + ).filter((it) => it.isLoading) items.map(async (item) => { - let startIdx = 0; + let startIdx = 0 const seeIfUpdated = async () => { if (startIdx > TIMEOUT_DELAYS.length) { - item.node.state = State.FAILED; + item.node.state = State.FAILED return } const username = viewerData?.me?.profile.username - const itemsToUpdate = libraryItems.filter(it => it.isLoading); + const itemsToUpdate = libraryItems.filter((it) => it.isLoading) if (itemsToUpdate.length > 0) { - const link = await articleQuery({ username, slug: item.node.slug, includeFriendsHighlights: false }) + const link = await articleQuery({ + username, + slug: item.node.slug, + includeFriendsHighlights: false, + }) - if (link && link.state != "PROCESSING") { - const updatedArticle = { ...item }; + if (link && link.state != 'PROCESSING') { + const updatedArticle = { ...item } updatedArticle.node = { ...item.node, ...link } - updatedArticle.isLoading = false; + updatedArticle.isLoading = false console.log(`Updating Metadata of ${item.node.slug}.`) - performActionOnItem('update-item', updatedArticle); - return; + performActionOnItem('update-item', updatedArticle) + return } - console.log(`Trying to get the metadata of item ${item.node.slug}... Retry ${startIdx} of 5`); + console.log( + `Trying to get the metadata of item ${item.node.slug}... Retry ${startIdx} of 5` + ) timeout.push(setTimeout(seeIfUpdated, TIMEOUT_DELAYS[startIdx++])) } } - await seeIfUpdated(); - }); + await seeIfUpdated() + }) return () => { - timeout.forEach(clearTimeout); + timeout.forEach(clearTimeout) } }, [itemsPages]) @@ -769,36 +781,39 @@ export function HomeFeedContainer(): JSX.Element { [itemsPages, multiSelectMode, checkedItems] ) - const handleLinkSubmission = - async (link: string, timezone: string, locale: string) => { - const result = await saveUrlMutation(link, timezone, locale) - if (result) { - toast( - () => ( - - Link Saved - - - - ), - { position: 'bottom-right' } - ) - const id = result.url?.match(/[^/]+$/)?.[0] ?? ""; - performActionOnItem('refresh', undefined as unknown as any) - } else { - showErrorToast('Error saving link', { position: 'bottom-right' }) - } - }; + const handleLinkSubmission = async ( + link: string, + timezone: string, + locale: string + ) => { + const result = await saveUrlMutation(link, timezone, locale) + if (result) { + toast( + () => ( + + Link Saved + + + + ), + { position: 'bottom-right' } + ) + const id = result.url?.match(/[^/]+$/)?.[0] ?? '' + performActionOnItem('refresh', undefined as unknown as any) + } else { + showErrorToast('Error saving link', { position: 'bottom-right' }) + } + } return ( Promise - handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise, + handleLinkSubmission: ( + link: string, + timezone: string, + locale: string + ) => Promise setIsChecked: (itemId: string, set: boolean) => void itemIsChecked: (itemId: string) => boolean @@ -986,7 +1005,10 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { )} {props.showAddLinkModal && ( - props.setShowAddLinkModal(false)} /> + props.setShowAddLinkModal(false)} + /> )} @@ -1044,6 +1066,8 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element { > {!props.isValidating && props.items.length == 0 ? ( { props.setShowAddLinkModal(true) }} diff --git a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx index f3177cbcf..79fb71724 100644 --- a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx +++ b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx @@ -92,7 +92,7 @@ function SavedSearches(props: LibraryFilterMenuProps): JSX.Element { term: 'in:inbox sort:read-desc is:unread', }, { - name: 'Read Later', + name: 'Non-Feed Items', term: 'in:library', }, { @@ -170,10 +170,6 @@ function Subscriptions(props: LibraryFilterMenuProps): JSX.Element { [subscriptions] ) - if (!subscriptions || subscriptions.length < 1) { - return <> - } - return ( { window.location.href = '/settings/subscriptions' }} + viewAll={() => { + setViewAll(true) + }} > - {subscriptions.slice(0, viewAll ? undefined : 4).map((item) => { - return ( + {viewAll ? ( + <> + - ) - })} - + {(subscriptions ?? []).map((item) => { + return ( + + ) + })} + + + ) : ( + + )} ) } @@ -233,6 +244,7 @@ type MenuPanelProps = { editFunc?: () => void editTitle?: string hideBottomBorder?: boolean + viewAll?: () => void } function MenuPanel(props: MenuPanelProps): JSX.Element { @@ -258,14 +270,15 @@ function MenuPanel(props: MenuPanelProps): JSX.Element { lineHeight: '125%', color: '$thLibraryMenuPrimary', pl: '10px', - my: '20px', + mt: '20px', + mb: '10px', }} > {props.title} } > + {props.viewAll && ( + { + if (props.viewAll) { + props.viewAll() + } + }} + /> + )} { diff --git a/packages/web/pages/settings/feeds/index.tsx b/packages/web/pages/settings/feeds/index.tsx index a14f7d36c..c94a19b0e 100644 --- a/packages/web/pages/settings/feeds/index.tsx +++ b/packages/web/pages/settings/feeds/index.tsx @@ -8,7 +8,7 @@ import { ConfirmationModal } from '../../../components/patterns/ConfirmationModa import { EmptySettingsRow, SettingsTable, - SettingsTableRow + SettingsTableRow, } from '../../../components/templates/settings/SettingsTable' import { theme } from '../../../components/tokens/stitches.config' import { formattedDateTime } from '../../../lib/dateFormatting' @@ -17,7 +17,7 @@ import { updateSubscriptionMutation } from '../../../lib/networking/mutations/up import { SubscriptionStatus, SubscriptionType, - useGetSubscriptionsQuery + useGetSubscriptionsQuery, } from '../../../lib/networking/queries/useGetSubscriptionsQuery' import { applyStoredTheme } from '../../../lib/themeUpdater' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' @@ -90,17 +90,15 @@ export default function Rss(): JSX.Element { return ( { router.push('/settings/feeds/add') }} > {subscriptions.length === 0 ? ( - + ) : ( subscriptions.map((subscription, i) => { return ( @@ -218,9 +216,7 @@ export default function Rss(): JSX.Element { {onDeleteId && ( { await onDelete(onDeleteId) setOnDeleteId('') diff --git a/packages/web/stories/EmptyLibrary.stories.tsx b/packages/web/stories/EmptyLibrary.stories.tsx index 68104a528..a690ad1d8 100644 --- a/packages/web/stories/EmptyLibrary.stories.tsx +++ b/packages/web/stories/EmptyLibrary.stories.tsx @@ -12,10 +12,16 @@ export default { }, } as ComponentMeta -export const EmptyLibraryStory: ComponentStory = (args: any) => { +export const EmptyLibraryStory: ComponentStory = ( + args: any +) => { return ( - { - console.log('onAddLinkClicked') - }} /> + { + console.log('onAddLinkClicked') + }} + /> ) }