mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Add Ability to Add URLs directly from the search bar.
This commit is contained in:
parent
cb85eb1c7b
commit
27dd2b2734
6 changed files with 147 additions and 49 deletions
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export type LibraryItems = {
|
|||
export type LibraryItem = {
|
||||
cursor: string
|
||||
node: LibraryItemNode
|
||||
isLoading?: boolean | undefined
|
||||
}
|
||||
|
||||
export type LibraryItemNode = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue