Add Ability to Add URLs directly from the search bar.

This commit is contained in:
Thomas Rogers 2023-08-06 02:55:01 +02:00
parent cb85eb1c7b
commit 27dd2b2734
6 changed files with 147 additions and 49 deletions

View file

@ -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 <LibraryListCard {...props} />

View file

@ -16,42 +16,12 @@ import {
type AddLinkModalProps = {
onOpenChange: (open: boolean) => void
handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise<void>,
}
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(
() => (
<Box>
Link Saved
<span style={{ padding: '16px' }} />
<Button
style="ctaDarkYellow"
autoFocus
onClick={() => {
window.location.href = `/article?url=${encodeURIComponent(
link
)}`
}}
>
Read Now
</Button>
</Box>
),
{ 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 {
<ModalTitleBar title="Add Link" onOpenChange={props.onOpenChange} />
<Box css={{ width: '100%', py: '16px' }}>
<form
onSubmit={(event) => {
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)
}}
>

View file

@ -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<LibraryItem>()
const [linkToEdit, setLinkToEdit] = useState<LibraryItem>()
const [linkToUnsubscribe, setLinkToUnsubscribe] = useState<LibraryItem>()
const [savedLink, setSavedLink] = useState<string>();
const [queryInputs, setQueryInputs] =
useState<LibraryItemsQueryInput>(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(
() => (
<Box>
Link Saved
<span style={{ padding: '16px' }} />
<Button
style="ctaDarkYellow"
autoFocus
onClick={() => {
window.location.href = `/article?url=${encodeURIComponent(
link
)}`
}}
>
Read Now
</Button>
</Box>
),
{ 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 (
<HomeFeedGrid
items={libraryItems}
@ -720,6 +805,7 @@ export function HomeFeedContainer(): JSX.Element {
gridContainerRef={gridContainerRef}
mode={mode}
setMode={setMode}
handleLinkSubmission={handleLinkSubmission}
applySearchQuery={(searchQuery: string) => {
setQueryInputs({
...queryInputs,
@ -811,6 +897,8 @@ type HomeFeedContentProps = {
item: LibraryItem | undefined
) => Promise<void>
handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise<void>,
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 && (
<AddLinkModal onOpenChange={() => props.setShowAddLinkModal(false)} />
<AddLinkModal handleLinkSubmission={props.handleLinkSubmission} onOpenChange={() => props.setShowAddLinkModal(false)} />
)}
</HStack>
</VStack>

View file

@ -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<void>,
}
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}
/>
</HStack>
)
@ -148,6 +152,7 @@ function SmallHeaderLayout(props: LibraryHeaderProps): JSX.Element {
<>
{props.multiSelectMode === 'off' && <MenuHeaderButton {...props} />}
<ControlButtonBox
handleLinkSubmission={props.handleLinkSubmission}
layout={props.layout}
updateLayout={props.updateLayout}
setShowInlineSearch={setShowInlineSearch}
@ -215,17 +220,25 @@ export type SearchBoxProps = {
compact?: boolean
onClose?: () => void
handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise<void>,
}
export function SearchBox(props: SearchBoxProps): JSX.Element {
const inputRef = useRef<HTMLInputElement | null>(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()
}}
>
<MagnifyingGlass
size={props.compact ? 15 : 20}
color={theme.colors.graySolid.toString()}
/>
{
(() => {
if (isAddAction) {
return <Plus
size={props.compact ? 15 : 20}
color={theme.colors.graySolid.toString()}
/>
}
return <MagnifyingGlass
size={props.compact ? 15 : 20}
color={theme.colors.graySolid.toString()}
/>
})()
}
</HStack>
<form
onSubmit={(event) => {
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<void>,
}
function MultiSelectControls(props: ControlButtonBoxProps): JSX.Element {

View file

@ -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<ArticleAttributes> {
const result = (await ssrFetcher(context, query, input)) as ArticleData
): Promise<ArticleAttributes | undefined> {
const result = (await gqlFetcher(query, input)) as ArticleData
if (result.article) {
return result.article.article
}
return Promise.reject()
return undefined
}
export const cacheArticle = (

View file

@ -60,6 +60,7 @@ export type LibraryItems = {
export type LibraryItem = {
cursor: string
node: LibraryItemNode
isLoading?: boolean | undefined
}
export type LibraryItemNode = {