From 27dd2b27348bf4f0b1fda0b21c23df6e85a27b76 Mon Sep 17 00:00:00 2001 From: Thomas Rogers Date: Sun, 6 Aug 2023 02:55:01 +0200 Subject: [PATCH] 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 = {