Improve fallback when no home found, add keyboard navigation

This commit is contained in:
Jackson Harper 2024-07-10 18:59:23 +08:00
parent a536b21c64
commit 486f0f2870
11 changed files with 674 additions and 461 deletions

View file

@ -1,7 +1,15 @@
import * as HoverCard from '@radix-ui/react-hover-card'
import { styled } from '@stitches/react'
import { useRouter } from 'next/router'
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import {
createContext,
useCallback,
useEffect,
useMemo,
useReducer,
useState,
useContext,
} from 'react'
import { Button } from '../elements/Button'
import { AddToLibraryActionIcon } from '../elements/icons/home/AddToLibraryActionIcon'
import { ArchiveActionIcon } from '../elements/icons/home/ArchiveActionIcon'
@ -14,6 +22,7 @@ import { useApplyLocalTheme } from '../../lib/hooks/useApplyLocalTheme'
import { useGetHiddenHomeSection } from '../../lib/networking/queries/useGetHiddenHomeSection'
import {
HomeItem,
HomeItemResponse,
HomeItemSource,
HomeItemSourceType,
HomeSection,
@ -28,14 +37,188 @@ import { Toaster } from 'react-hot-toast'
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
import useLibraryItemActions from '../../lib/hooks/useLibraryItemActions'
import { SyncLoader } from 'react-spinners'
import { useGetLibraryItemsQuery } from '../../lib/networking/queries/useGetLibraryItemsQuery'
import { useGetRawSearchItemsQuery } from '../../lib/networking/queries/useGetLibraryItemsQuery'
import { useRegisterActions } from 'kbar'
type HomeState = {
items: HomeItem[]
home?: HomeSection[]
libraryItems?: HomeItem[]
selectedItem: string | undefined
selectedItemIdx: number | undefined
serverHome?: HomeSection[]
serverLibraryItems?: HomeItem[]
}
type Action =
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'SET_HOME_ITEMS'; payload: HomeSection[] }
| { type: 'SET_LIBRARY_ITEMS'; payload: HomeItem[] }
| { type: 'SET_ACTIVE_ELEMENT'; payload: string | undefined }
| { type: 'SET_ACTIVE_ELEMENT_IDX'; payload: number | undefined }
const flattenItems = (
home: HomeSection[] | undefined,
libraryItems: HomeItem[] | undefined
) => {
if (!home) {
return []
}
const result = [...home]
// If we have library items, we need to find the top_picks section
// and append them there.
if (libraryItems) {
const topPicks = result.find((section) => section.layout == 'top_picks')
if (topPicks) {
topPicks.title = 'From your library'
topPicks.items = libraryItems
} else {
console.log('could not find top picks')
}
}
return result
.filter((section) => section.layout !== 'just_added')
.flatMap((section) => section.items)
}
const removeItem = (
itemId: string,
home: HomeSection[] | undefined,
libraryItems: HomeItem[] | undefined
) => {
if (home) {
home = home.map((section) => ({
...section,
items: section.items.filter((item) => item.id !== itemId),
}))
}
if (libraryItems) {
libraryItems = libraryItems.filter((item) => item.id !== itemId)
}
return { home, libraryItems }
}
const updateSelectedItem = (
currentSelectedItem: string | undefined,
previousItems: HomeItem[],
currentItems: HomeItem[]
) => {
if (!currentSelectedItem) {
return undefined
}
const currentIdx = currentItems.findIndex(
(item) => item.id === currentSelectedItem
)
if (currentIdx !== -1) {
// item has not been removed
return currentSelectedItem
}
const previousIdx = previousItems.findIndex(
(item) => item.id === currentSelectedItem
)
if (previousIdx >= 0 && previousIdx < currentItems.length - 1) {
return currentItems[previousIdx].id
}
if (previousIdx >= 1 && previousIdx > currentItems.length - 1) {
return currentItems[previousIdx - 1].id
}
return undefined
}
const initialState: HomeState = {
items: [],
selectedItem: undefined,
selectedItemIdx: undefined,
}
const reducer = (state: HomeState, action: Action): HomeState => {
console.log('action: ', action)
switch (action.type) {
case 'REMOVE_ITEM':
const items = state.items.filter((item) => item.id !== action.payload)
return {
...state,
items,
selectedItem: updateSelectedItem(
state.selectedItem,
state.items,
items
),
...removeItem(action.payload, state.home, state.libraryItems),
}
case 'SET_ACTIVE_ELEMENT':
return {
...state,
selectedItem: action.payload,
}
case 'SET_ACTIVE_ELEMENT_IDX':
return {
...state,
selectedItemIdx: action.payload,
}
case 'SET_HOME_ITEMS':
return {
...state,
home: [...action.payload],
serverHome: [...action.payload],
items: flattenItems(action.payload, undefined),
}
case 'SET_LIBRARY_ITEMS':
return {
...state,
libraryItems: [...action.payload],
serverLibraryItems: [...action.payload],
items: flattenItems(state.home, action.payload),
}
default:
console.log('hitting default return ')
return state
}
}
type NavigationContextType = {
state: HomeState
dispatch: React.Dispatch<Action>
}
const NavigationContext =
createContext<NavigationContextType | undefined>(undefined)
export const useNavigation = (): NavigationContextType => {
const context = useContext(NavigationContext)
if (!context) {
throw new Error('useNavigation must be used within a NavigationProvider')
}
return context
}
export function HomeContainer(): JSX.Element {
const router = useRouter()
const [state, dispatch] = useReducer(reducer, initialState)
const homeData = useGetHomeItems()
const router = useRouter()
const { viewerData } = useGetViewerQuery()
const [selectedItem, setSelectedItem] = useState<HTMLElement | null>(null)
const hasTopPicks = (homeData: HomeItemResponse) => {
const topPicks = homeData.sections?.find(
(section) => section.layout === 'top_picks'
)
const result = topPicks && topPicks.items.length > 0
return result
}
const shouldFallback =
homeData.error || (!homeData.isValidating && !hasTopPicks(homeData))
const { items: searchResponseItems } = useGetRawSearchItemsQuery(
{
limit: 10,
searchQuery: 'in:inbox',
includeContent: false,
sortDescending: true,
},
shouldFallback
)
useApplyLocalTheme()
@ -43,10 +226,162 @@ export function HomeContainer(): JSX.Element {
return viewerData?.me?.profile.username
}, [viewerData])
const searchItems = useMemo(() => {
return searchResponseItems.map((item) => {
return {
id: item.id,
date: item.savedAt,
title: item.title,
url: item.url,
slug: item.slug,
score: 1.0,
thumbnail: item.image,
previewContent: item.description,
source: {
name: item.folder == 'following' ? item.subscription : item.siteName,
icon: item.siteIcon,
type: 'LIBRARY',
},
canArchive: true,
canDelete: true,
canShare: true,
canMove: item.folder == 'following',
} as HomeItem
})
}, [searchResponseItems])
useEffect(() => {
window.localStorage.setItem('nav-return', router.asPath)
}, [router.asPath])
useEffect(() => {
const newSections = homeData.sections
if (
homeData.sections &&
JSON.stringify(newSections) !== JSON.stringify(state.serverHome)
) {
dispatch({
type: 'SET_HOME_ITEMS',
payload: homeData.sections,
})
}
}, [homeData, state.home, dispatch])
useEffect(() => {
if (
searchItems &&
searchItems.length > 0 &&
JSON.stringify(searchItems) !== JSON.stringify(state.serverLibraryItems)
) {
dispatch({
type: 'SET_LIBRARY_ITEMS',
payload: searchItems,
})
}
}, [searchItems, state.libraryItems, dispatch])
const moveSelectedItem = useCallback(
(direction: 'next' | 'previous') => {
const elements = document.querySelectorAll('[data-navigable]')
// this is the old index, if its less than
// the current length then its good because
// the removed item will give
let index = Array.from(elements).findIndex(
(element) =>
element.getAttribute('data-navigable') == state.selectedItem
)
if (direction == 'next') {
index = index === -1 ? 0 : Math.min(index + 1, state.items.length - 1)
} else if (direction == 'previous') {
index = index === -1 ? 0 : Math.max(index - 1, 0)
}
const selected = state.items[index]
if (state.selectedItem !== selected.id) {
dispatch({
type: 'SET_ACTIVE_ELEMENT',
payload: selected.id,
})
}
},
[state, dispatch]
)
useEffect(() => {
if (state.selectedItem) {
const element = document.querySelector<HTMLElement>(
`[data-navigable="${state.selectedItem}"]`
)
element?.focus()
localStorage.setItem('activeElementId', state.selectedItem)
}
}, [state.selectedItem])
useEffect(() => {
const selectedItem = localStorage.getItem('activeElementId')
console.log('loaded selected item: ', selectedItem)
if (selectedItem) {
dispatch({
type: 'SET_ACTIVE_ELEMENT',
payload: selectedItem,
})
}
}, [])
useRegisterActions(
[
{
id: 'move_next',
section: 'Items',
name: 'Focus next item',
shortcut: ['arrowdown'],
keywords: 'move next',
perform: () => {
moveSelectedItem('next')
},
},
{
id: 'move_previous',
section: 'Items',
name: 'Focus previous item',
shortcut: ['arrowup'],
keywords: 'move previous',
perform: () => {
moveSelectedItem('previous')
},
},
{
id: 'move_next_vim',
section: 'Items',
name: 'Focus next item',
shortcut: ['j'],
keywords: 'move next',
perform: () => {
moveSelectedItem('next')
},
},
{
id: 'move_previous_vim',
section: 'Items',
name: 'Focus previous item',
shortcut: ['k'],
keywords: 'move previous',
perform: () => {
moveSelectedItem('previous')
},
},
// {
// shortcutKeys: ['a'],
// actionDescription: 'Open Add Link dialog',
// shortcutKeyDescription: 'a',
// callback: () => actionHandler('showAddLinkModal'),
// },
],
[state.selectedItem, moveSelectedItem]
)
if (homeData.error && homeData.errorMessage == 'PENDING') {
return (
<VStack
@ -64,212 +399,87 @@ export function HomeContainer(): JSX.Element {
)
}
const moveSelection = useCallback(
(direction: 'next' | 'previous') => {
const elements =
document.querySelectorAll<HTMLElement>('[data-navigable]')
let index = Array.prototype.indexOf.call(elements, selectedItem)
if (direction == 'next') {
index = index === -1 ? 0 : Math.min(index + 1, elements.length - 1)
} else if (direction == 'previous') {
index = index === -1 ? 0 : Math.max(index - 1, 0)
}
console.log('elements: ', index, elements)
const selected = elements[index]
setSelectedItem(selected)
selected.focus()
},
[selectedItem, setSelectedItem]
)
useRegisterActions(
[
{
id: 'open_readable',
section: 'Items',
name: 'Open focused item',
shortcut: ['enter'],
keywords: 'open',
perform: () => {
console.log('open item')
},
},
{
id: 'open_original',
section: 'Items',
name: 'Open original url',
shortcut: ['o'],
keywords: 'open original',
perform: () => {
console.log('open original')
},
},
{
id: 'archive',
section: 'Items',
name: 'Archive item',
shortcut: ['e'],
keywords: 'archive item',
perform: () => {
console.log('archive')
},
},
{
id: 'mark_read',
section: 'Items',
name: 'Mark item as read',
shortcut: ['-'],
keywords: 'mark read',
perform: () => {
console.log('mark_read')
},
},
{
id: 'delete_item',
section: 'Items',
name: 'Delete item',
shortcut: ['#'],
keywords: 'delete remove',
perform: () => {
console.log('delete')
},
},
{
id: 'move_next',
section: 'Items',
name: 'Focus next item',
shortcut: ['arrowdown'],
keywords: 'move next',
perform: () => {
moveSelection('next')
},
},
{
id: 'move_previous',
section: 'Items',
name: 'Focus previous item',
shortcut: ['arrowup'],
keywords: 'move previous',
perform: () => {
moveSelection('previous')
},
},
{
id: 'move_next_vim',
section: 'Items',
name: 'Focus next item',
shortcut: ['j'],
keywords: 'move next',
perform: () => {
moveSelection('next')
},
},
{
id: 'move_previous_vim',
section: 'Items',
name: 'Focus previous item',
shortcut: ['k'],
keywords: 'move previous',
perform: () => {
moveSelection('previous')
},
},
// {
// shortcutKeys: ['a'],
// actionDescription: 'Open Add Link dialog',
// shortcutKeyDescription: 'a',
// callback: () => actionHandler('showAddLinkModal'),
// },
],
[selectedItem]
)
return (
<VStack
distribution="start"
alignment="center"
css={{
width: '100%',
bg: '$readerBg',
pt: '45px',
minHeight: '100vh',
minWidth: '320px',
'@mdDown': {
pt: '0px',
mt: '80px',
},
}}
>
<Toaster />
<NavigationContext.Provider value={{ state, dispatch }}>
<VStack
distribution="start"
alignment="center"
css={{
width: '680px',
gap: '50px',
width: '100%',
bg: '$readerBg',
pt: '45px',
minHeight: '100vh',
minWidth: '320px',
'@mdDown': {
gap: '40px',
width: '100%',
pt: '0px',
mt: '80px',
},
}}
>
{homeData.sections?.map((homeSection, idx) => {
switch (homeSection.layout) {
case 'just_added':
if (homeSection.items.length < 1) {
<Toaster />
<VStack
distribution="start"
css={{
width: '680px',
gap: '50px',
minHeight: '100vh',
'@mdDown': {
gap: '40px',
width: '100%',
},
}}
>
{state.home?.map((homeSection, idx) => {
switch (homeSection.layout) {
case 'just_added':
if (homeSection.items.length < 1) {
return <SpanBox key={`section-${idx}`}></SpanBox>
}
return (
<JustAddedHomeSection
key={`section-${idx}`}
homeSection={homeSection}
viewerUsername={viewerUsername}
/>
)
case 'top_picks':
return (
<TopPicksHomeSection
key={`section-${idx}`}
homeSection={homeSection}
viewerUsername={viewerUsername}
/>
)
case 'quick_links':
if (homeSection.items.length < 1) {
return <SpanBox key={`section-${idx}`}></SpanBox>
}
return (
<QuickLinksHomeSection
key={`section-${idx}`}
homeSection={homeSection}
viewerUsername={viewerUsername}
/>
)
case 'hidden':
if (homeSection.items.length < 1) {
return <SpanBox key={`section-${idx}`}></SpanBox>
}
return (
<HiddenHomeSection
key={`section-${idx}`}
homeSection={homeSection}
viewerUsername={viewerUsername}
/>
)
default:
console.log('unknown home section: ', homeSection)
return <SpanBox key={`section-${idx}`}></SpanBox>
}
return (
<JustAddedHomeSection
key={`section-${idx}`}
homeSection={homeSection}
viewerUsername={viewerUsername}
/>
)
case 'top_picks':
if (homeSection.items.length < 1) {
return <FromYourLibraryHomeSection key="section-from-your" />
}
return (
<TopPicksHomeSection
key={`section-${idx}`}
homeSection={homeSection}
viewerUsername={viewerUsername}
/>
)
case 'quick_links':
if (homeSection.items.length < 1) {
return <SpanBox key={`section-${idx}`}></SpanBox>
}
return (
<QuickLinksHomeSection
key={`section-${idx}`}
homeSection={homeSection}
viewerUsername={viewerUsername}
/>
)
case 'hidden':
if (homeSection.items.length < 1) {
return <SpanBox key={`section-${idx}`}></SpanBox>
}
return (
<HiddenHomeSection
key={`section-${idx}`}
homeSection={homeSection}
viewerUsername={viewerUsername}
/>
)
default:
console.log('unknown home section: ', homeSection)
return <SpanBox key={`section-${idx}`}></SpanBox>
}
})}
}
})}
</VStack>
</VStack>
</VStack>
</NavigationContext.Provider>
)
}
@ -368,38 +578,15 @@ const JustAddedHomeSection = (props: HomeSectionProps): JSX.Element => {
}
const TopPicksHomeSection = (props: HomeSectionProps): JSX.Element => {
const listReducer = (
state: HomeItem[],
action: {
type: string
itemId?: string
items?: HomeItem[]
}
) => {
switch (action.type) {
case 'RESET':
return action.items ?? []
case 'REMOVE_ITEM':
return state.filter((item) => item.id !== action.itemId)
default:
throw new Error()
}
}
const { state, dispatch } = useNavigation()
const [items, dispatchList] = useReducer(listReducer, [])
const items = useMemo(() => {
return (
state.home?.find((section) => section.layout == 'top_picks')?.items ?? []
)
}, [props, state.home])
useEffect(() => {
dispatchList({
type: 'RESET',
items: props.homeSection.items,
})
}, [props])
console.log(
'props.homeSection.items.length: ',
props.homeSection.items.length
)
if (props.homeSection.items.length < 1) {
if (items.length < 1) {
return (
<VStack
distribution="start"
@ -450,121 +637,7 @@ const TopPicksHomeSection = (props: HomeSectionProps): JSX.Element => {
itemsPerPage={10}
loadMoreButtonText="Load more Top Picks"
render={(homeItem) => (
<TopPicksItemView
key={homeItem.id}
homeItem={homeItem}
dispatchList={dispatchList}
/>
)}
/>
</VStack>
)
}
const FromYourLibraryHomeSection = (): JSX.Element => {
const { itemsPages } = useGetLibraryItemsQuery('all', {
limit: 10,
includeContent: false,
sortDescending: true,
})
const searchItems = useMemo(() => {
return (
itemsPages?.flatMap((ad) => {
return ad.search.edges.map((it) => ({
...it,
isLoading: it.node.state === 'PROCESSING',
}))
}) || []
).map((item) => {
return {
id: item.node.id,
date: item.node.savedAt,
title: item.node.title,
url: item.node.url,
slug: item.node.slug,
score: 1.0,
thumbnail: item.node.image,
source: {
name:
item.node.folder == 'following'
? item.node.subscription
: item.node.siteName,
icon: item.node.siteIcon,
type: 'LIBRARY',
},
canArchive: true,
canDelete: true,
canShare: true,
canMove: item.node.folder == 'following',
} as HomeItem
})
}, [itemsPages])
const listReducer = (
state: HomeItem[],
action: {
type: string
itemId?: string
items?: HomeItem[]
}
) => {
switch (action.type) {
case 'RESET':
return action.items ?? []
case 'REMOVE_ITEM':
return state.filter((item) => item.id !== action.itemId)
default:
throw new Error()
}
}
const [items, dispatchList] = useReducer(listReducer, [])
useEffect(() => {
dispatchList({
type: 'RESET',
items: searchItems,
})
}, [searchItems])
return (
<VStack
distribution="start"
css={{
width: '100%',
gap: '20px',
'@mdDown': {
gap: '10px',
},
}}
>
{items.length > 0 && (
<SpanBox
css={{
fontFamily: '$inter',
fontSize: '16px',
fontWeight: '600',
color: '$homeTextTitle',
'@mdDown': {
px: '20px',
},
}}
>
From your library
</SpanBox>
)}
<Pagination
items={items}
itemsPerPage={10}
loadMoreButtonText="Load more Top Picks"
render={(homeItem) => (
<TopPicksItemView
key={homeItem.id}
homeItem={homeItem}
dispatchList={dispatchList}
/>
<TopPicksItemView key={homeItem.id} homeItem={homeItem} />
)}
/>
</VStack>
@ -853,17 +926,70 @@ const JustAddedItemView = (props: HomeItemViewProps): JSX.Element => {
)
}
type TopPicksItemViewProps = {
dispatchList: (args: { type: string; itemId?: string }) => void
}
type TopPicksItemViewProps = {}
const TopPicksItemView = (
props: HomeItemViewProps & TopPicksItemViewProps
): JSX.Element => {
const router = useRouter()
const { dispatch } = useNavigation()
const { archiveItem, deleteItem, moveItem, shareItem } =
useLibraryItemActions()
const doArchiveItem = useCallback(
async (libraryItemId: string) => {
dispatch({
type: 'REMOVE_ITEM',
payload: libraryItemId,
})
if (!(await archiveItem(libraryItemId))) {
// dispatch({
// type: 'REPLACE_ITEM',
// itemId: libraryItemId,
// })
}
},
[archiveItem]
)
const doDeleteItem = useCallback(
async (libraryItemId: string) => {
dispatch({
type: 'REMOVE_ITEM',
payload: libraryItemId,
})
const undo = () => {
// props.dispatch({
// type: 'REPLACE_ITEM',
// : libraryItemId,
// })
}
if (!(await deleteItem(libraryItemId, undo))) {
// dispatch({
// type: 'REPLACE_ITEM',
// payload: libraryItemId,
// })
}
},
[deleteItem]
)
const doMoveItem = useCallback(
async (libraryItemId: string) => {
dispatch({
type: 'REMOVE_ITEM',
payload: libraryItemId,
})
if (!(await moveItem(libraryItemId))) {
// dispatch({
// type: 'REPLACE_ITEM',
// payload: libraryItemId,
// })
}
},
[moveItem]
)
return (
<VStack
tabIndex={0}
@ -898,6 +1024,25 @@ const TopPicksItemView = (
router.push(path)
}
}}
onKeyDown={(event) => {
switch (event.key.toLowerCase()) {
case 'enter':
;(event.target as HTMLElement).click()
break
case 'e':
doArchiveItem(props.homeItem.id)
break
case '#':
doDeleteItem(props.homeItem.id)
break
case 'm':
doMoveItem(props.homeItem.id)
break
case 'o':
window.open(props.homeItem.url, '_blank')
break
}
}}
alignment="start"
>
<Box css={{ width: '100%', gap: '10px', px: '20px' }}>
@ -940,16 +1085,7 @@ const TopPicksItemView = (
event.preventDefault()
event.stopPropagation()
props.dispatchList({
type: 'REMOVE_ITEM',
itemId: props.homeItem.id,
})
if (!(await moveItem(props.homeItem.id))) {
props.dispatchList({
type: 'REPLACE_ITEM',
itemId: props.homeItem.id,
})
}
await doMoveItem(props.homeItem.id)
}}
>
<AddToLibraryActionIcon />
@ -963,16 +1099,7 @@ const TopPicksItemView = (
event.preventDefault()
event.stopPropagation()
props.dispatchList({
type: 'REMOVE_ITEM',
itemId: props.homeItem.id,
})
if (!(await archiveItem(props.homeItem.id))) {
props.dispatchList({
type: 'REPLACE_ITEM',
itemId: props.homeItem.id,
})
}
await doArchiveItem(props.homeItem.id)
}}
>
<ArchiveActionIcon />
@ -986,22 +1113,7 @@ const TopPicksItemView = (
event.preventDefault()
event.stopPropagation()
props.dispatchList({
type: 'REMOVE_ITEM',
itemId: props.homeItem.id,
})
const undo = () => {
props.dispatchList({
type: 'REPLACE_ITEM',
itemId: props.homeItem.id,
})
}
if (!(await deleteItem(props.homeItem.id, undo))) {
props.dispatchList({
type: 'REPLACE_ITEM',
itemId: props.homeItem.id,
})
}
await doDeleteItem(props.homeItem.id)
}}
>
<RemoveActionIcon />

View file

@ -52,7 +52,7 @@ export function NavigationLayout(props: NavigationLayoutProps): JSX.Element {
const [showKeyboardCommandsModal, setShowKeyboardCommandsModal] =
useState(false)
useKeyboardShortcuts(navigationCommands(router))
useRegisterActions(navigationCommands(router))
useKeyboardShortcuts(
primaryCommands((action) => {
@ -64,38 +64,6 @@ export function NavigationLayout(props: NavigationLayoutProps): JSX.Element {
})
)
useRegisterActions(
[
{
id: 'home',
section: 'Navigation',
name: 'Go to Home (Library) ',
shortcut: ['g h'],
keywords: 'go home',
perform: () => router?.push('/home'),
},
{
id: 'lightTheme',
section: 'Preferences',
name: 'Change theme (light) ',
shortcut: ['v', 'l'],
keywords: 'light theme',
priority: Priority.LOW,
perform: () => updateTheme(ThemeId.Light),
},
{
id: 'darkTheme',
section: 'Preferences',
name: 'Change theme (dark) ',
shortcut: ['v', 'd'],
keywords: 'dark theme',
priority: Priority.LOW,
perform: () => updateTheme(ThemeId.Dark),
},
],
[router]
)
// Attempt to identify the user if they are logged in.
useEffect(() => {
setupAnalytics(viewerData?.me)

View file

@ -34,7 +34,7 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
const [showKeyboardCommandsModal, setShowKeyboardCommandsModal] =
useState(false)
useKeyboardShortcuts(navigationCommands(router))
// useKeyboardShortcuts(navigationCommands(router))
useKeyboardShortcuts(
primaryCommands((action) => {

View file

@ -17,6 +17,7 @@ export default function useLibraryItemActions() {
archived: true,
})
console.log('result: ', result)
if (result) {
showSuccessToast('Link archived', { position: 'bottom-right' })
} else {

View file

@ -1,21 +1,61 @@
import { Action } from 'kbar'
import type { KeyboardCommand } from './useKeyboardShortcuts'
import type { NextRouter } from 'next/router'
import { DEFAULT_HOME_PATH } from '../navigations'
export function navigationCommands(
router: NextRouter | undefined
): KeyboardCommand[] {
export function navigationCommands(router: NextRouter | undefined): Action[] {
return [
{
shortcutKeys: ['g', 'h'],
actionDescription: 'Go to Home',
shortcutKeyDescription: 'g then h',
callback: () => router?.push('/home'),
id: 'home',
section: 'Navigation',
name: 'Go to home',
shortcut: ['g', 'h'],
keywords: 'go home',
perform: () => {
console.log('go home')
router?.push(`/l/home`)
},
},
{
shortcutKeys: ['b'],
actionDescription: 'Go back',
shortcutKeyDescription: 'b',
callback: () => {
id: 'library',
section: 'Navigation',
name: 'Go to library',
shortcut: ['g', 'l'],
keywords: 'go library',
perform: () => {
console.log('go library')
router?.push(`/l/library`)
},
},
{
id: 'subscriptions',
section: 'Navigation',
name: 'Go to subscriptions',
shortcut: ['g', 's'],
keywords: 'go subscriptions',
perform: () => {
console.log('go subscriptions')
router?.push(`/l/subscriptions`)
},
},
{
id: 'highlights',
section: 'Navigation',
name: 'Go to highlights',
shortcut: ['g', 'i'],
keywords: 'go highlights',
perform: () => {
console.log('go highlights')
router?.push(`/l/highlights`)
},
},
{
id: 'goback',
section: 'Navigation',
name: 'Go back',
shortcut: ['g', 'b'],
keywords: 'go back',
perform: () => {
router?.back()
},
},

View file

@ -14,7 +14,6 @@ export type HomeItemResponse = {
isValidating: boolean
errorMessage?: string
sections?: HomeSection[]
mutate?: () => void
}
export type HomeItem = {
@ -160,7 +159,6 @@ export function useGetHomeItems(): HomeItemResponse {
if (result && result.home && result.home.edges) {
return {
mutate,
error: false,
isValidating,
sections: result.home.edges.map((edge) => {

View file

@ -11,9 +11,10 @@ import { articleReadingProgressMutation } from '../mutations/articleReadingProgr
import { deleteLinkMutation } from '../mutations/deleteLinkMutation'
import { setLinkArchivedMutation } from '../mutations/setLinkArchivedMutation'
import { updatePageMutation } from '../mutations/updatePageMutation'
import { gqlFetcher } from '../networkHelpers'
import { gqlFetcher, makeGqlFetcher } from '../networkHelpers'
import { Label } from './../fragments/labelFragment'
import { moveToFolderMutation } from '../mutations/moveToLibraryMutation'
import useSWR from 'swr'
export interface ReadableItem {
id: string
@ -43,6 +44,14 @@ type LibraryItemsQueryResponse = {
mutate: () => void
}
type LibraryItemsRawQueryResponse = {
items: LibraryItemNode[]
itemsDataError?: unknown
isLoading: boolean
isValidating: boolean
error: boolean
}
type LibraryItemAction =
| 'archive'
| 'unarchive'
@ -56,6 +65,7 @@ type LibraryItemAction =
export type LibraryItemsData = {
search: LibraryItems
errorCodes?: string[]
}
export type LibraryItems = {
@ -469,3 +479,104 @@ export function useGetLibraryItemsQuery(
error: !!error,
}
}
export function useGetRawSearchItemsQuery(
{
limit,
searchQuery,
cursor,
includeContent = false,
}: LibraryItemsQueryInput,
shouldFetch = true
): LibraryItemsRawQueryResponse {
const query = gql`
query Search(
$after: String
$first: Int
$query: String
$includeContent: Boolean
) {
search(
first: $first
after: $after
query: $query
includeContent: $includeContent
) {
... on SearchSuccess {
edges {
cursor
node {
id
title
slug
url
folder
createdAt
author
image
description
publishedAt
originalArticleUrl
siteName
siteIcon
subscription
readAt
savedAt
wordsCount
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
totalCount
}
}
... on SearchError {
errorCodes
}
}
}
`
const variables = {
after: cursor,
first: limit,
query: searchQuery,
includeContent,
}
const { data, error, isValidating, mutate } = useSWR(
shouldFetch ? [query, variables.first, variables.after] : null,
makeGqlFetcher(query, variables),
{
revalidateIfStale: false,
revalidateOnFocus: false,
}
)
let responseError = error
let responseData = data as LibraryItemsData | undefined
// We need to check the response errors here and return the error
// it will be nested in the data pages, if there is one error,
// we invalidate the data and return the error. We also zero out
// the response in the case of an error.
if (responseData?.errorCodes) {
return {
isValidating: false,
items: [],
isLoading: false,
error: true,
}
}
return {
isValidating,
items: responseData?.search.edges.map((edge) => edge.node) ?? [],
itemsDataError: responseError,
isLoading: !error && !data,
error: !!error,
}
}

View file

@ -41,7 +41,7 @@
"diff-match-patch": "^1.0.5",
"epubjs": "^0.3.93",
"graphql-request": "^3.6.1",
"kbar": "^0.1.0-beta.35",
"kbar": "^v0.1.0-beta.45",
"loadjs": "^4.3.0-rc1",
"markdown-it": "^13.0.1",
"nanoid": "^3.1.29",

View file

@ -36,22 +36,7 @@ TopBarProgress.config({
})
const generateActions = (router: NextRouter) => {
const defaultActions = [
{
id: 'home',
section: 'Navigation',
name: 'Go to Home (Library) ',
shortcut: ['g', 'h'],
keywords: 'go home',
perform: () => {
const navReturn = window.localStorage.getItem('nav-return')
if (navReturn) {
router.push(navReturn)
return
}
router?.push(DEFAULT_HOME_PATH)
},
},
return [
{
id: 'lightTheme',
section: 'Preferences',
@ -71,8 +56,6 @@ const generateActions = (router: NextRouter) => {
perform: () => updateTheme(ThemeId.Dark),
},
]
return defaultActions
}
const ConditionalCaptchaProvider = (props: {

View file

@ -245,7 +245,7 @@ export default function Integrations(): JSX.Element {
icon: '/static/icons/pocket.svg',
title: 'Pocket',
subText:
'Pocket is a place to save articles, videos, and more. Our Pocket integration allows importing your Pocket library to Omnivore. Once connected we will asyncronously import all your Pocket articles into Omnivore, as this process is resource intensive it can take some time. You will receive an email when the process is completed. Limit 20k articles per import.',
'Pocket is a place to save articles, videos, and more. Our Pocket integration allows importing your Pocket library to Omnivore. Once connected we will asyncronously import all your Pocket articles into Omnivore, as this process is resource intensive it can take some time. You will receive an email when the process is completed. Limit 20k articles per import. The import is a one-time process and can only be performed once per-account.',
button: {
text: pocket ? 'Disconnect' : 'Import',
icon: isImporting(pocket) ? (

View file

@ -12548,11 +12548,6 @@ comma-separated-tokens@^2.0.0:
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
command-score@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/command-score/-/command-score-0.1.2.tgz#b986ad7e8c0beba17552a56636c44ae38363d381"
integrity sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==
commander@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
@ -16538,6 +16533,11 @@ fuse.js@^3.6.1:
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.6.1.tgz#7de85fdd6e1b3377c23ce010892656385fd9b10c"
integrity sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==
fuse.js@^6.6.2:
version "6.6.2"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111"
integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==
gauge@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
@ -20390,14 +20390,14 @@ jws@^4.0.0:
jwa "^2.0.0"
safe-buffer "^5.0.1"
kbar@^0.1.0-beta.35:
version "0.1.0-beta.40"
resolved "https://registry.yarnpkg.com/kbar/-/kbar-0.1.0-beta.40.tgz#89747e3c1538375fef779af986b6614bb441ae7c"
integrity sha512-vEV02WuEBvKaSivO2DnNtyd3gUAbruYrZCax5fXcLcVTFV6q0/w6Ew3z6Qy+AqXxbZdWguwQ3POIwgdHevp+6A==
kbar@^v0.1.0-beta.45:
version "0.1.0-beta.45"
resolved "https://registry.yarnpkg.com/kbar/-/kbar-0.1.0-beta.45.tgz#6b0871f2860a7fe21ad4db5df2389cf3b73d344a"
integrity sha512-kXvjthqPLoWZXlxLJPrFKioskNdQv1O3Ukg5mqq2ExK3Ix1qvYT3W/ACDRIv/e/CHxPWZoTriB4oFbQ6UCSX5g==
dependencies:
"@radix-ui/react-portal" "^1.0.1"
command-score "^0.1.2"
fast-equals "^2.0.3"
fuse.js "^6.6.2"
react-virtual "^2.8.2"
tiny-invariant "^1.2.0"