From 27dd2b27348bf4f0b1fda0b21c23df6e85a27b76 Mon Sep 17 00:00:00 2001 From: Thomas Rogers Date: Sun, 6 Aug 2023 02:55:01 +0200 Subject: [PATCH 1/6] Add Ability to Add URLs directly from the search bar. --- .../patterns/LibraryCards/LinkedItemCard.tsx | 2 + .../templates/homeFeed/AddLinkModal.tsx | 36 +------ .../templates/homeFeed/HomeFeedContainer.tsx | 95 ++++++++++++++++++- .../templates/homeFeed/LibraryHeader.tsx | 52 ++++++++-- .../networking/queries/useGetArticleQuery.tsx | 10 +- .../queries/useGetLibraryItemsQuery.tsx | 1 + 6 files changed, 147 insertions(+), 49 deletions(-) diff --git a/packages/web/components/patterns/LibraryCards/LinkedItemCard.tsx b/packages/web/components/patterns/LibraryCards/LinkedItemCard.tsx index 19caabc98..8d6a75ec0 100644 --- a/packages/web/components/patterns/LibraryCards/LinkedItemCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LinkedItemCard.tsx @@ -2,6 +2,8 @@ import type { LinkedItemCardProps } from './CardTypes' import { LibraryGridCard } from './LibraryGridCard' import { LibraryListCard } from './LibraryListCard' + +// TODO: Add something for the loading view if we are loading. export function LinkedItemCard(props: LinkedItemCardProps): JSX.Element { if (props.layout == 'LIST_LAYOUT') { return diff --git a/packages/web/components/templates/homeFeed/AddLinkModal.tsx b/packages/web/components/templates/homeFeed/AddLinkModal.tsx index 28c06595e..bb50485c9 100644 --- a/packages/web/components/templates/homeFeed/AddLinkModal.tsx +++ b/packages/web/components/templates/homeFeed/AddLinkModal.tsx @@ -16,42 +16,12 @@ import { type AddLinkModalProps = { onOpenChange: (open: boolean) => void + handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise, } export function AddLinkModal(props: AddLinkModalProps): JSX.Element { const [link, setLink] = useState('') - const handleLinkSubmission = useCallback( - async (link: string, timezone: string, locale: string) => { - const result = await saveUrlMutation(link, timezone, locale) - if (result) { - toast( - () => ( - - Link Saved - - - - ), - { position: 'bottom-right' } - ) - } else { - showErrorToast('Error saving link', { position: 'bottom-right' }) - } - }, - [link] - ) - const validateLink = useCallback( (link: string) => { try { @@ -81,7 +51,7 @@ export function AddLinkModal(props: AddLinkModalProps): JSX.Element {
{ + onSubmit={async (event) => { event.preventDefault() let submitLink = link @@ -96,7 +66,7 @@ export function AddLinkModal(props: AddLinkModalProps): JSX.Element { setLink(newLink) submitLink = newLink } - handleLinkSubmission(submitLink, timeZone, locale) + await props.handleLinkSubmission(submitLink, timeZone, locale) props.onOpenChange(false) }} > diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 039ed728f..e2c5385f9 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -1,4 +1,7 @@ import { Action, createAction, useKBar, useRegisterActions } from 'kbar' +import { + articleQuery, +} from "../../../lib/networking/queries/useGetArticleQuery" import debounce from 'lodash/debounce' import { useRouter } from 'next/router' import { @@ -9,7 +12,7 @@ import { useRef, useState, } from 'react' -import { 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' @@ -51,7 +54,7 @@ import { bulkActionMutation } from '../../../lib/networking/mutations/bulkAction import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter' import { NotebookPresenter } from '../article/NotebookPresenter' -import { Highlight } from '../../../lib/networking/fragments/highlightFragment' +import { saveUrlMutation } from "../../../lib/networking/mutations/saveUrlMutation" export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT' export type LibraryMode = 'reads' | 'highlights' @@ -69,6 +72,8 @@ const debouncedFetchSearchResults = debounce((query, cb) => { fetchSearchResults(query, cb) }, 300) +const TIMEOUT_DELAYS = [500, 750, 1000, 2000, 5000]; + export function HomeFeedContainer(): JSX.Element { const { viewerData } = useGetViewerQuery() const router = useRouter() @@ -97,6 +102,7 @@ export function HomeFeedContainer(): JSX.Element { const [linkToRemove, setLinkToRemove] = useState() const [linkToEdit, setLinkToEdit] = useState() const [linkToUnsubscribe, setLinkToUnsubscribe] = useState() + const [savedLink, setSavedLink] = useState(); const [queryInputs, setQueryInputs] = useState(defaultQuery) @@ -171,6 +177,42 @@ export function HomeFeedContainer(): JSX.Element { return items }, [itemsPages, performActionOnItem]) + useEffect(() => { + let startIdx = 1; + if (savedLink) { + const seeIfUpdated = async() => { + if (startIdx > 5) { + return + } + + const item = getItem(savedLink); + const username = viewerData?.me?.profile.username; + if (item) { + const link = await articleQuery({ username, slug: item.node.slug, includeFriendsHighlights: false }) + + if (link && link.state != "PROCESSING") { + const updatedArticle = { ...item }; + updatedArticle.node = {...item.node, ...link } + performActionOnItem('update-item', updatedArticle); + return; + } + + if (!item.isLoading) { + performActionOnItem('update-item', { ...item, isLoading: true }); + } + console.log(`Trying to get the metadata of item ${item.node.slug}... Retry ${startIdx} of 5`); + setTimeout(seeIfUpdated, TIMEOUT_DELAYS[startIdx++]) + } + + // If the item was not found, this suggests that we are not in the right search view. So we can bail early. + } + + setTimeout(seeIfUpdated, TIMEOUT_DELAYS[0]); + setSavedLink(undefined); + } + + }, [itemsPages]) + const handleFetchMore = useCallback(() => { if (isValidating || !hasMore) { return @@ -273,6 +315,13 @@ export function HomeFeedContainer(): JSX.Element { [libraryItems] ) + const getItemByUrl = useCallback( + (url: string) => { + return libraryItems.find(it => it.node.url === url); + }, + [libraryItems] + ) + const activeItemIndex = useMemo(() => { if (!activeCardId) { return undefined @@ -706,6 +755,42 @@ export function HomeFeedContainer(): JSX.Element { [itemsPages, multiSelectMode, checkedItems] ) + const queryUntilSavedOrTimeout = async (url: string, tries : number | undefined = 5)=> { + return; + } + + 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) + setSavedLink(id); + } else { + showErrorToast('Error saving link', { position: 'bottom-right' }) + } + }; + return ( { setQueryInputs({ ...queryInputs, @@ -811,6 +897,8 @@ type HomeFeedContentProps = { item: LibraryItem | undefined ) => Promise + handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise, + setIsChecked: (itemId: string, set: boolean) => void itemIsChecked: (itemId: string) => boolean @@ -853,6 +941,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { applySearchQuery={(searchQuery: string) => { props.applySearchQuery(searchQuery) }} + handleLinkSubmission={props.handleLinkSubmission} allowSelectMultiple={props.mode !== 'highlights'} alwaysShowHeader={props.mode == 'highlights'} showFilterMenu={showFilterMenu} @@ -892,7 +981,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { )} {props.showAddLinkModal && ( - props.setShowAddLinkModal(false)} /> + props.setShowAddLinkModal(false)} /> )} diff --git a/packages/web/components/templates/homeFeed/LibraryHeader.tsx b/packages/web/components/templates/homeFeed/LibraryHeader.tsx index 58f686ced..0d504a743 100644 --- a/packages/web/components/templates/homeFeed/LibraryHeader.tsx +++ b/packages/web/components/templates/homeFeed/LibraryHeader.tsx @@ -1,15 +1,16 @@ -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from "react" import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { theme } from '../../tokens/stitches.config' import { FormInput } from '../../elements/FormElements' import { searchBarCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts' import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts' +import { locale, timeZone } from '../../../lib/dateFormatting' import { Button, IconButton } from '../../elements/Button' import { - CaretDown, FunnelSimple, MagnifyingGlass, Prohibit, + Plus, X, } from 'phosphor-react' import { LayoutType } from './HomeFeedContainer' @@ -52,6 +53,8 @@ type LibraryHeaderProps = { setMultiSelectMode: (mode: MultiSelectMode) => void performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void + + handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise, } export function LibraryHeader(props: LibraryHeaderProps): JSX.Element { @@ -110,6 +113,7 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element { searchTerm={props.searchTerm} applySearchQuery={props.applySearchQuery} allowSelectMultiple={props.allowSelectMultiple} + handleLinkSubmission={props.handleLinkSubmission} /> ) @@ -148,6 +152,7 @@ function SmallHeaderLayout(props: LibraryHeaderProps): JSX.Element { <> {props.multiSelectMode === 'off' && } void + handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise, } export function SearchBox(props: SearchBoxProps): JSX.Element { const inputRef = useRef(null) const [focused, setFocused] = useState(false) const [searchTerm, setSearchTerm] = useState(props.searchTerm ?? '') + const [isAddAction, setIsAddAction] = useState(false); + const IS_URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; + useEffect(() => { setSearchTerm(props.searchTerm ?? '') }, [props.searchTerm]) + useEffect(() => { + setIsAddAction(IS_URL_REGEX.test(searchTerm)) + }, [searchTerm, props.searchTerm]) + useKeyboardShortcuts( searchBarCommands((action) => { if (action === 'focusSearchBar' && inputRef.current) { @@ -272,15 +285,36 @@ export function SearchBox(props: SearchBoxProps): JSX.Element { e.preventDefault() }} > - + { + (() => { + if (isAddAction) { + return + } + + return + })() + } + { + onSubmit={async (event) => { event.preventDefault() - props.applySearchQuery(searchTerm || '') + + if (!isAddAction) { + props.applySearchQuery(searchTerm || '') + } else { + await props.handleLinkSubmission(searchTerm, timeZone, locale) + setSearchTerm(props.searchTerm ?? "") + // This will technically (albeit kinda hackily) refresh and add the link + // I would prefer, though, for this to actually be handled better. + props.applySearchQuery(props.searchTerm ?? "") + } inputRef.current?.blur() if (props.onClose) { props.onClose() @@ -376,6 +410,8 @@ type ControlButtonBoxProps = { searchTerm: string | undefined applySearchQuery: (searchQuery: string) => void + + handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise, } function MultiSelectControls(props: ControlButtonBoxProps): JSX.Element { diff --git a/packages/web/lib/networking/queries/useGetArticleQuery.tsx b/packages/web/lib/networking/queries/useGetArticleQuery.tsx index f9f9df7cf..6fcb758ff 100644 --- a/packages/web/lib/networking/queries/useGetArticleQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleQuery.tsx @@ -1,6 +1,6 @@ import { gql } from 'graphql-request' import useSWRImmutable, { Cache } from 'swr' -import { makeGqlFetcher, RequestContext, ssrFetcher } from '../networkHelpers' +import { gqlFetcher, makeGqlFetcher, RequestContext, ssrFetcher } from "../networkHelpers" import { articleFragment, ContentReader, @@ -135,15 +135,15 @@ export function useGetArticleQuery({ } export async function articleQuery( - context: RequestContext, input: ArticleQueryInput -): Promise { - const result = (await ssrFetcher(context, query, input)) as ArticleData +): Promise { + + const result = (await gqlFetcher(query, input)) as ArticleData if (result.article) { return result.article.article } - return Promise.reject() + return undefined } export const cacheArticle = ( diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index 9b175684d..feda1a48e 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -60,6 +60,7 @@ export type LibraryItems = { export type LibraryItem = { cursor: string node: LibraryItemNode + isLoading?: boolean | undefined } export type LibraryItemNode = { From 7aa959a4c7f8e50e758548b3a32db2654b277dff Mon Sep 17 00:00:00 2001 From: Thomas Rogers Date: Sun, 6 Aug 2023 18:29:07 +0200 Subject: [PATCH 2/6] Change timeout for polling. --- .../web/components/elements/LoadingBar.tsx | 78 +++++++++++++++++++ .../patterns/LibraryCards/CardTypes.tsx | 1 + .../patterns/LibraryCards/LibraryGridCard.tsx | 18 ++++- .../patterns/LibraryCards/LibraryListCard.tsx | 51 +++++++++++- .../templates/homeFeed/HomeFeedContainer.tsx | 28 +++---- 5 files changed, 153 insertions(+), 23 deletions(-) create mode 100644 packages/web/components/elements/LoadingBar.tsx diff --git a/packages/web/components/elements/LoadingBar.tsx b/packages/web/components/elements/LoadingBar.tsx new file mode 100644 index 000000000..8231a5b19 --- /dev/null +++ b/packages/web/components/elements/LoadingBar.tsx @@ -0,0 +1,78 @@ +import { Box } from './../elements/LayoutPrimitives' +import { Dispatch, SetStateAction, useEffect, useState } from "react" + +type LoadingBarProps = { + fillColor: string + backgroundColor: string + borderRadius: string + percentFill?: number +} + +type AnimationStatus = { + position: number, + transition: string +} +export function LoadingBar(props: LoadingBarProps): JSX.Element { + // OK So, what we want to do is. + // We have two boxes. + const [leftOne, setLeftOne] = useState({ position: 0, transition: 'left 0.5s linear' }) + const [leftTwo, setLeftTwo] = useState({ position: -100, transition: 'left 0.5s linear' }) + + const calculateNewValue = (currVal: AnimationStatus, setNextVal: Dispatch>) => { + const position = currVal.position >= 100 ? -100 : currVal.position + 25; + const transition = currVal.position >= 100 ? 'left 0s linear' : 'left 0.5s linear'; + setNextVal({ position, transition }) + } + + useEffect(() => { + const interval = setTimeout(() => { + calculateNewValue(leftOne, setLeftOne) + }, 500); + + return () => { + clearTimeout(interval) + } + }, [leftOne]) + + useEffect(() => { + const interval = setTimeout(() => { + calculateNewValue(leftTwo, setLeftTwo) + }, 500); + + return () => { + clearTimeout(interval) + } + }, [leftTwo]) + + return ( + + + + + ) +} diff --git a/packages/web/components/patterns/LibraryCards/CardTypes.tsx b/packages/web/components/patterns/LibraryCards/CardTypes.tsx index 799509cb2..ab9d9dfcb 100644 --- a/packages/web/components/patterns/LibraryCards/CardTypes.tsx +++ b/packages/web/components/patterns/LibraryCards/CardTypes.tsx @@ -30,4 +30,5 @@ export type LinkedItemCardProps = { multiSelectMode: MultiSelectMode isHovered?: boolean + isLoading?: boolean } diff --git a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx index 60072a376..167fdbd05 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx @@ -5,7 +5,6 @@ import { CoverImage } from '../../elements/CoverImage' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { useCallback, useState } from 'react' -import Link from 'next/link' import { AuthorInfoStyle, CardCheckbox, @@ -28,7 +27,7 @@ import { import { CardMenu } from '../CardMenu' import { DotsThree } from 'phosphor-react' import { isTouchScreenDevice } from '../../../lib/deviceType' -import { ProgressBarOverlay } from './LibraryListCard' +import { LoadingBarOverlay, ProgressBarOverlay } from "./LibraryListCard" import { FallbackImage } from './FallbackImage' import { useRouter } from 'next/router' @@ -88,7 +87,6 @@ export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element { setIsHovered(false) }} onClick={(event) => { - console.log('click event: ', event) if (event.metaKey || event.ctrlKey) { window.open( `/${props.viewer.profile.username}/${props.item.slug}`, @@ -133,6 +131,7 @@ type GridImageProps = { src?: string title?: string readingProgress?: number + isLoading?: boolean } const GridImage = (props: GridImageProps): JSX.Element => { @@ -140,7 +139,17 @@ const GridImage = (props: GridImageProps): JSX.Element => { return ( <> - {(props.readingProgress ?? 0) > 0 && ( + { + props.isLoading && ( + + ) + } + {(props.readingProgress ?? 0) > 0 && !props.isLoading && ( { src={props.item.image} title={props.item.title} readingProgress={item.readingProgressPercent} + isLoading={props.isLoading} /> { + return ( + + + + ) } export const ProgressBarOverlay = ( @@ -164,13 +199,24 @@ type ListImageProps = { src?: string title?: string readingProgress?: number + isLoading?: boolean } const ListImage = (props: ListImageProps): JSX.Element => { const [displayFallback, setDisplayFallback] = useState(props.src == undefined) return ( - <> + <>{ + props.isLoading && ( + + ) + } {(props.readingProgress ?? 0) > 0 && ( { fetchSearchResults(query, cb) }, 300) -const TIMEOUT_DELAYS = [500, 750, 1000, 2000, 5000]; +// We set a relatively high delay for the refresh. +const TIMEOUT_DELAYS = [1000, 3000, 4000, 5000, 10000]; export function HomeFeedContainer(): JSX.Element { const { viewerData } = useGetViewerQuery() @@ -142,10 +143,11 @@ export function HomeFeedContainer(): JSX.Element { useEffect(() => { if (!router.isReady) return const q = router.query['q'] - let qs = '' + let qs = 'in:inbox' // Default to in:inbox search term. if (q && typeof q === 'string') { qs = q } + if (qs !== (queryInputs.searchQuery || '')) { setQueryInputs({ ...queryInputs, searchQuery: qs }) performActionOnItem('refresh', undefined as unknown as any) @@ -172,7 +174,7 @@ export function HomeFeedContainer(): JSX.Element { const libraryItems = useMemo(() => { const items = itemsPages?.flatMap((ad) => { - return ad.search.edges + return ad.search.edges.map(it => ({ ...it, isLoading: it.node.state === 'PROCESSING'})); }) || [] return items }, [itemsPages, performActionOnItem]) @@ -186,13 +188,16 @@ export function HomeFeedContainer(): JSX.Element { } const item = getItem(savedLink); - const username = viewerData?.me?.profile.username; + const username = viewerData?.me?.profile.username + if (item) { const link = await articleQuery({ username, slug: item.node.slug, includeFriendsHighlights: false }) if (link && link.state != "PROCESSING") { const updatedArticle = { ...item }; updatedArticle.node = {...item.node, ...link } + updatedArticle.isLoading = false; + console.log('updating') performActionOnItem('update-item', updatedArticle); return; } @@ -206,9 +211,8 @@ export function HomeFeedContainer(): JSX.Element { // If the item was not found, this suggests that we are not in the right search view. So we can bail early. } - - setTimeout(seeIfUpdated, TIMEOUT_DELAYS[0]); setSavedLink(undefined); + setTimeout(seeIfUpdated, TIMEOUT_DELAYS[0]); } }, [itemsPages]) @@ -315,13 +319,6 @@ export function HomeFeedContainer(): JSX.Element { [libraryItems] ) - const getItemByUrl = useCallback( - (url: string) => { - return libraryItems.find(it => it.node.url === url); - }, - [libraryItems] - ) - const activeItemIndex = useMemo(() => { if (!activeCardId) { return undefined @@ -755,10 +752,6 @@ export function HomeFeedContainer(): JSX.Element { [itemsPages, multiSelectMode, checkedItems] ) - const queryUntilSavedOrTimeout = async (url: string, tries : number | undefined = 5)=> { - return; - } - const handleLinkSubmission = async (link: string, timezone: string, locale: string) => { const result = await saveUrlMutation(link, timezone, locale) @@ -1283,6 +1276,7 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element { Date: Mon, 7 Aug 2023 09:08:08 +0200 Subject: [PATCH 3/6] Clear Timeout when navigating away from page. --- .../components/templates/homeFeed/HomeFeedContainer.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 2989751c6..db76086d1 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -181,6 +181,7 @@ export function HomeFeedContainer(): JSX.Element { useEffect(() => { let startIdx = 1; + let timeout : NodeJS.Timeout | undefined; if (savedLink) { const seeIfUpdated = async() => { if (startIdx > 5) { @@ -206,15 +207,18 @@ export function HomeFeedContainer(): JSX.Element { performActionOnItem('update-item', { ...item, isLoading: true }); } console.log(`Trying to get the metadata of item ${item.node.slug}... Retry ${startIdx} of 5`); - setTimeout(seeIfUpdated, TIMEOUT_DELAYS[startIdx++]) + timeout = setTimeout(seeIfUpdated, TIMEOUT_DELAYS[startIdx++]) } // If the item was not found, this suggests that we are not in the right search view. So we can bail early. } setSavedLink(undefined); - setTimeout(seeIfUpdated, TIMEOUT_DELAYS[0]); + timeout = setTimeout(seeIfUpdated, TIMEOUT_DELAYS[0]); } + return () => { + clearTimeout(timeout); + } }, [itemsPages]) const handleFetchMore = useCallback(() => { From 9be74570b670c2ffa0bfd70f3ae40f461c5a7712 Mon Sep 17 00:00:00 2001 From: Thomas Rogers Date: Mon, 7 Aug 2023 19:29:48 +0200 Subject: [PATCH 4/6] Fix up and refactor some elements of the code. Allow multiple loading items. --- .../web/components/elements/LoadingBar.tsx | 3 +- .../patterns/LibraryCards/LibraryListCard.tsx | 2 +- .../patterns/LibraryCards/LinkedItemCard.tsx | 2 - .../templates/homeFeed/HomeFeedContainer.tsx | 144 +++++++++--------- 4 files changed, 70 insertions(+), 81 deletions(-) diff --git a/packages/web/components/elements/LoadingBar.tsx b/packages/web/components/elements/LoadingBar.tsx index 8231a5b19..6b92e7bf8 100644 --- a/packages/web/components/elements/LoadingBar.tsx +++ b/packages/web/components/elements/LoadingBar.tsx @@ -12,9 +12,8 @@ type AnimationStatus = { position: number, transition: string } + export function LoadingBar(props: LoadingBarProps): JSX.Element { - // OK So, what we want to do is. - // We have two boxes. const [leftOne, setLeftOne] = useState({ position: 0, transition: 'left 0.5s linear' }) const [leftTwo, setLeftTwo] = useState({ position: -100, transition: 'left 0.5s linear' }) diff --git a/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx index 60b1d02b2..c4ff05907 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx @@ -217,7 +217,7 @@ const ListImage = (props: ListImageProps): JSX.Element => { /> ) } - {(props.readingProgress ?? 0) > 0 && ( + {(props.readingProgress ?? 0) > 0 && !props.isLoading && ( diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index db76086d1..c79201288 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -1,59 +1,40 @@ -import { Action, createAction, useKBar, useRegisterActions } from 'kbar' -import { - articleQuery, -} from "../../../lib/networking/queries/useGetArticleQuery" -import debounce from 'lodash/debounce' -import { useRouter } from 'next/router' -import { - useCallback, - useEffect, - useMemo, - useReducer, - useRef, - useState, -} from 'react' -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' -import { libraryListCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts' -import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts' -import { - PageType, - State, -} from '../../../lib/networking/fragments/articleFragment' +import { Action, createAction, useKBar, useRegisterActions } from "kbar" +import { articleQuery } from "../../../lib/networking/queries/useGetArticleQuery" +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 TopBarProgress from "react-topbar-progress-indicator" +import { useFetchMore } from "../../../lib/hooks/useFetchMoreScroll" +import { usePersistedState } from "../../../lib/hooks/usePersistedState" +import { libraryListCommands } from "../../../lib/keyboardShortcuts/navigationShortcuts" +import { useKeyboardShortcuts } from "../../../lib/keyboardShortcuts/useKeyboardShortcuts" +import { PageType, State } from "../../../lib/networking/fragments/articleFragment" import { SearchItem, TypeaheadSearchItemsData, - typeaheadSearchQuery, -} from '../../../lib/networking/queries/typeaheadSearch' -import type { - LibraryItem, - LibraryItemsQueryInput, -} from '../../../lib/networking/queries/useGetLibraryItemsQuery' -import { useGetLibraryItemsQuery } from '../../../lib/networking/queries/useGetLibraryItemsQuery' -import { - useGetViewerQuery, - UserBasicData, -} from '../../../lib/networking/queries/useGetViewerQuery' -import { Button } from '../../elements/Button' -import { StyledText } from '../../elements/StyledText' -import { ConfirmationModal } from '../../patterns/ConfirmationModal' -import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes' -import { LinkedItemCard } from '../../patterns/LibraryCards/LinkedItemCard' -import { Box, HStack, VStack } from './../../elements/LayoutPrimitives' -import { AddLinkModal } from './AddLinkModal' -import { EditLibraryItemModal } from './EditItemModals' -import { EmptyLibrary } from './EmptyLibrary' -import { HighlightItemsLayout } from './HighlightsLayout' -import { LibraryFilterMenu } from './LibraryFilterMenu' -import { LibraryHeader, MultiSelectMode } from './LibraryHeader' -import { UploadModal } from '../UploadModal' -import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation' -import { bulkActionMutation } from '../../../lib/networking/mutations/bulkActionMutation' -import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' -import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter' -import { NotebookPresenter } from '../article/NotebookPresenter' + typeaheadSearchQuery +} from "../../../lib/networking/queries/typeaheadSearch" +import type { LibraryItem, LibraryItemsQueryInput } from "../../../lib/networking/queries/useGetLibraryItemsQuery" +import { useGetLibraryItemsQuery } from "../../../lib/networking/queries/useGetLibraryItemsQuery" +import { useGetViewerQuery, UserBasicData } from "../../../lib/networking/queries/useGetViewerQuery" +import { Button } from "../../elements/Button" +import { StyledText } from "../../elements/StyledText" +import { ConfirmationModal } from "../../patterns/ConfirmationModal" +import { LinkedItemCardAction } from "../../patterns/LibraryCards/CardTypes" +import { LinkedItemCard } from "../../patterns/LibraryCards/LinkedItemCard" +import { Box, HStack, VStack } from "./../../elements/LayoutPrimitives" +import { AddLinkModal } from "./AddLinkModal" +import { EditLibraryItemModal } from "./EditItemModals" +import { EmptyLibrary } from "./EmptyLibrary" +import { HighlightItemsLayout } from "./HighlightsLayout" +import { LibraryFilterMenu } from "./LibraryFilterMenu" +import { LibraryHeader, MultiSelectMode } from "./LibraryHeader" +import { UploadModal } from "../UploadModal" +import { BulkAction, bulkActionMutation } from "../../../lib/networking/mutations/bulkActionMutation" +import { showErrorToast, showSuccessToast } from "../../../lib/toastHelpers" +import { SetPageLabelsModalPresenter } from "../article/SetLabelsModalPresenter" +import { NotebookPresenter } from "../article/NotebookPresenter" import { saveUrlMutation } from "../../../lib/networking/mutations/saveUrlMutation" export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT' @@ -72,8 +53,10 @@ const debouncedFetchSearchResults = debounce((query, cb) => { fetchSearchResults(query, cb) }, 300) -// We set a relatively high delay for the refresh. -const TIMEOUT_DELAYS = [1000, 3000, 4000, 5000, 10000]; +// 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]; export function HomeFeedContainer(): JSX.Element { const { viewerData } = useGetViewerQuery() @@ -103,7 +86,7 @@ export function HomeFeedContainer(): JSX.Element { const [linkToRemove, setLinkToRemove] = useState() const [linkToEdit, setLinkToEdit] = useState() const [linkToUnsubscribe, setLinkToUnsubscribe] = useState() - const [savedLink, setSavedLink] = useState(); + const updatingLinks = useRef>(new Set()) const [queryInputs, setQueryInputs] = useState(defaultQuery) @@ -171,6 +154,7 @@ export function HomeFeedContainer(): JSX.Element { return itemsPages[itemsPages.length - 1].search.pageInfo.hasNextPage }, [itemsPages]) + const libraryItems = useMemo(() => { const items = itemsPages?.flatMap((ad) => { @@ -180,44 +164,53 @@ export function HomeFeedContainer(): JSX.Element { }, [itemsPages, performActionOnItem]) useEffect(() => { - let startIdx = 1; - let timeout : NodeJS.Timeout | undefined; - if (savedLink) { - const seeIfUpdated = async() => { - if (startIdx > 5) { + const timeout : NodeJS.Timeout[] = [] + const itemsToUpdate = libraryItems + .filter(it => it.isLoading && !updatingLinks.current.has(it.node.slug)); + + const items = + itemsPages?.flatMap((ad) => { + return ad.search.edges.map(it => ({ ...it, isLoading: it.node.state === 'PROCESSING'})); + }) || [] + + items.map(async (item) => { + let startIdx = 0; + updatingLinks.current.add(item.node.slug) + + const seeIfUpdated = async () => { + if (startIdx > TIMEOUT_DELAYS.length) { + item.node.state = State.FAILED; + performActionOnItem('update-item', item); + return } - const item = getItem(savedLink); const username = viewerData?.me?.profile.username + const itemsToUpdate = libraryItems.filter(it => it.isLoading); - if (item) { + if (itemsToUpdate.length > 0) { const link = await articleQuery({ username, slug: item.node.slug, includeFriendsHighlights: false }) if (link && link.state != "PROCESSING") { const updatedArticle = { ...item }; - updatedArticle.node = {...item.node, ...link } + updatedArticle.node = { ...item.node, ...link } updatedArticle.isLoading = false; - console.log('updating') + console.log(`Updating Metadata of ${item.node.slug}.`) + updatingLinks.current.delete(item.node.slug) performActionOnItem('update-item', updatedArticle); return; } - if (!item.isLoading) { - performActionOnItem('update-item', { ...item, isLoading: true }); - } console.log(`Trying to get the metadata of item ${item.node.slug}... Retry ${startIdx} of 5`); - timeout = setTimeout(seeIfUpdated, TIMEOUT_DELAYS[startIdx++]) + timeout.push(setTimeout(seeIfUpdated, TIMEOUT_DELAYS[startIdx++])) } - - // If the item was not found, this suggests that we are not in the right search view. So we can bail early. } - setSavedLink(undefined); - timeout = setTimeout(seeIfUpdated, TIMEOUT_DELAYS[0]); - } + + await seeIfUpdated(); + }); return () => { - clearTimeout(timeout); + timeout.forEach(clearTimeout); } }, [itemsPages]) @@ -782,7 +775,6 @@ export function HomeFeedContainer(): JSX.Element { ) const id = result.url?.match(/[^/]+$/)?.[0] ?? ""; performActionOnItem('refresh', undefined as unknown as any) - setSavedLink(id); } else { showErrorToast('Error saving link', { position: 'bottom-right' }) } From f4c9724a99f03d7f5dc7cecb70e4be742caba9f3 Mon Sep 17 00:00:00 2001 From: Thomas Rogers Date: Mon, 7 Aug 2023 19:42:35 +0200 Subject: [PATCH 5/6] Remove Comment --- packages/web/components/templates/homeFeed/LibraryHeader.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/web/components/templates/homeFeed/LibraryHeader.tsx b/packages/web/components/templates/homeFeed/LibraryHeader.tsx index 0d504a743..f33d0c9f1 100644 --- a/packages/web/components/templates/homeFeed/LibraryHeader.tsx +++ b/packages/web/components/templates/homeFeed/LibraryHeader.tsx @@ -230,7 +230,6 @@ export function SearchBox(props: SearchBoxProps): JSX.Element { const [isAddAction, setIsAddAction] = useState(false); const IS_URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; - useEffect(() => { setSearchTerm(props.searchTerm ?? '') }, [props.searchTerm]) @@ -311,8 +310,6 @@ export function SearchBox(props: SearchBoxProps): JSX.Element { } else { await props.handleLinkSubmission(searchTerm, timeZone, locale) setSearchTerm(props.searchTerm ?? "") - // This will technically (albeit kinda hackily) refresh and add the link - // I would prefer, though, for this to actually be handled better. props.applySearchQuery(props.searchTerm ?? "") } inputRef.current?.blur() From 123081516114fdec5b557eea6bd116bea54d3557 Mon Sep 17 00:00:00 2001 From: Thomas Rogers Date: Mon, 7 Aug 2023 21:32:58 +0200 Subject: [PATCH 6/6] Fix issue where loading would stop due to ref --- .../templates/homeFeed/HomeFeedContainer.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index c79201288..4bec6daa0 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -86,7 +86,6 @@ export function HomeFeedContainer(): JSX.Element { const [linkToRemove, setLinkToRemove] = useState() const [linkToEdit, setLinkToEdit] = useState() const [linkToUnsubscribe, setLinkToUnsubscribe] = useState() - const updatingLinks = useRef>(new Set()) const [queryInputs, setQueryInputs] = useState(defaultQuery) @@ -165,23 +164,19 @@ export function HomeFeedContainer(): JSX.Element { useEffect(() => { const timeout : NodeJS.Timeout[] = [] - const itemsToUpdate = libraryItems - .filter(it => it.isLoading && !updatingLinks.current.has(it.node.slug)); const items = - itemsPages?.flatMap((ad) => { + (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; - updatingLinks.current.add(item.node.slug) const seeIfUpdated = async () => { if (startIdx > TIMEOUT_DELAYS.length) { item.node.state = State.FAILED; - performActionOnItem('update-item', item); - return } @@ -196,7 +191,6 @@ export function HomeFeedContainer(): JSX.Element { updatedArticle.node = { ...item.node, ...link } updatedArticle.isLoading = false; console.log(`Updating Metadata of ${item.node.slug}.`) - updatingLinks.current.delete(item.node.slug) performActionOnItem('update-item', updatedArticle); return; }