From a536b21c648ca3c542e2a2a65cbb7262e5afb182 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 9 Jul 2024 19:58:13 +0800 Subject: [PATCH 1/5] Next/previous keyboard navigation for home --- .../nav-containers/HomeContainer.tsx | 140 +++++++++++++++++- 1 file changed, 137 insertions(+), 3 deletions(-) diff --git a/packages/web/components/nav-containers/HomeContainer.tsx b/packages/web/components/nav-containers/HomeContainer.tsx index 44edf00ff..92705fba0 100644 --- a/packages/web/components/nav-containers/HomeContainer.tsx +++ b/packages/web/components/nav-containers/HomeContainer.tsx @@ -29,11 +29,13 @@ import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuer import useLibraryItemActions from '../../lib/hooks/useLibraryItemActions' import { SyncLoader } from 'react-spinners' import { useGetLibraryItemsQuery } from '../../lib/networking/queries/useGetLibraryItemsQuery' +import { useRegisterActions } from 'kbar' export function HomeContainer(): JSX.Element { const router = useRouter() const homeData = useGetHomeItems() const { viewerData } = useGetViewerQuery() + const [selectedItem, setSelectedItem] = useState(null) useApplyLocalTheme() @@ -62,6 +64,130 @@ export function HomeContainer(): JSX.Element { ) } + const moveSelection = useCallback( + (direction: 'next' | 'previous') => { + const elements = + document.querySelectorAll('[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 ( + return } return ( { }) }, [searchItems]) - console.log('items: ', items) - return ( { return ( Date: Wed, 10 Jul 2024 18:59:23 +0800 Subject: [PATCH 2/5] Improve fallback when no home found, add keyboard navigation --- .../nav-containers/HomeContainer.tsx | 878 ++++++++++-------- .../components/templates/NavigationLayout.tsx | 34 +- .../components/templates/PrimaryLayout.tsx | 2 +- .../web/lib/hooks/useLibraryItemActions.tsx | 1 + .../keyboardShortcuts/navigationShortcuts.ts | 62 +- .../web/lib/networking/queries/useGetHome.tsx | 2 - .../queries/useGetLibraryItemsQuery.tsx | 113 ++- packages/web/package.json | 2 +- packages/web/pages/_app.tsx | 19 +- packages/web/pages/settings/integrations.tsx | 2 +- yarn.lock | 20 +- 11 files changed, 674 insertions(+), 461 deletions(-) diff --git a/packages/web/components/nav-containers/HomeContainer.tsx b/packages/web/components/nav-containers/HomeContainer.tsx index 92705fba0..6641a7a6f 100644 --- a/packages/web/components/nav-containers/HomeContainer.tsx +++ b/packages/web/components/nav-containers/HomeContainer.tsx @@ -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 +} + +const NavigationContext = + createContext(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(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( + `[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 ( { - const elements = - document.querySelectorAll('[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 ( - - + - {homeData.sections?.map((homeSection, idx) => { - switch (homeSection.layout) { - case 'just_added': - if (homeSection.items.length < 1) { + + + {state.home?.map((homeSection, idx) => { + switch (homeSection.layout) { + case 'just_added': + if (homeSection.items.length < 1) { + return + } + return ( + + ) + case 'top_picks': + return ( + + ) + case 'quick_links': + if (homeSection.items.length < 1) { + return + } + return ( + + ) + case 'hidden': + if (homeSection.items.length < 1) { + return + } + return ( + + ) + default: + console.log('unknown home section: ', homeSection) return - } - return ( - - ) - case 'top_picks': - if (homeSection.items.length < 1) { - return - } - return ( - - ) - case 'quick_links': - if (homeSection.items.length < 1) { - return - } - return ( - - ) - case 'hidden': - if (homeSection.items.length < 1) { - return - } - return ( - - ) - default: - console.log('unknown home section: ', homeSection) - return - } - })} + } + })} + - + ) } @@ -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 ( { itemsPerPage={10} loadMoreButtonText="Load more Top Picks" render={(homeItem) => ( - - )} - /> - - ) -} - -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 ( - - {items.length > 0 && ( - - From your library - - )} - - ( - + )} /> @@ -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 ( { + 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" > @@ -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) }} > @@ -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) }} > @@ -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) }} > diff --git a/packages/web/components/templates/NavigationLayout.tsx b/packages/web/components/templates/NavigationLayout.tsx index bc15324d1..c6364fe19 100644 --- a/packages/web/components/templates/NavigationLayout.tsx +++ b/packages/web/components/templates/NavigationLayout.tsx @@ -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) diff --git a/packages/web/components/templates/PrimaryLayout.tsx b/packages/web/components/templates/PrimaryLayout.tsx index ce661f161..fdf7b1454 100644 --- a/packages/web/components/templates/PrimaryLayout.tsx +++ b/packages/web/components/templates/PrimaryLayout.tsx @@ -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) => { diff --git a/packages/web/lib/hooks/useLibraryItemActions.tsx b/packages/web/lib/hooks/useLibraryItemActions.tsx index 6fb393eb3..18474f702 100644 --- a/packages/web/lib/hooks/useLibraryItemActions.tsx +++ b/packages/web/lib/hooks/useLibraryItemActions.tsx @@ -17,6 +17,7 @@ export default function useLibraryItemActions() { archived: true, }) + console.log('result: ', result) if (result) { showSuccessToast('Link archived', { position: 'bottom-right' }) } else { diff --git a/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts b/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts index f525c5d5f..116436d3d 100644 --- a/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts +++ b/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts @@ -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() }, }, diff --git a/packages/web/lib/networking/queries/useGetHome.tsx b/packages/web/lib/networking/queries/useGetHome.tsx index f83f4e2e9..1031da91c 100644 --- a/packages/web/lib/networking/queries/useGetHome.tsx +++ b/packages/web/lib/networking/queries/useGetHome.tsx @@ -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) => { diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index 79a061476..18df92ec3 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -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, + } +} diff --git a/packages/web/package.json b/packages/web/package.json index 861f5f2c2..e6d50b316 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -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", diff --git a/packages/web/pages/_app.tsx b/packages/web/pages/_app.tsx index 9499d084f..70f7b44a3 100644 --- a/packages/web/pages/_app.tsx +++ b/packages/web/pages/_app.tsx @@ -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: { diff --git a/packages/web/pages/settings/integrations.tsx b/packages/web/pages/settings/integrations.tsx index 071cd002f..30d677bd5 100644 --- a/packages/web/pages/settings/integrations.tsx +++ b/packages/web/pages/settings/integrations.tsx @@ -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) ? ( diff --git a/yarn.lock b/yarn.lock index 3d2b9e5b7..86984db0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From 5b86a235871f904cc30f5f7dcc8a2b25398fb891 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 10 Jul 2024 20:30:09 +0800 Subject: [PATCH 3/5] Linting fixes --- packages/web/components/nav-containers/HomeContainer.tsx | 6 +----- .../web/lib/networking/queries/useGetLibraryItemsQuery.tsx | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/web/components/nav-containers/HomeContainer.tsx b/packages/web/components/nav-containers/HomeContainer.tsx index 6641a7a6f..1a087b5d5 100644 --- a/packages/web/components/nav-containers/HomeContainer.tsx +++ b/packages/web/components/nav-containers/HomeContainer.tsx @@ -926,11 +926,7 @@ const JustAddedItemView = (props: HomeItemViewProps): JSX.Element => { ) } -type TopPicksItemViewProps = {} - -const TopPicksItemView = ( - props: HomeItemViewProps & TopPicksItemViewProps -): JSX.Element => { +const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => { const router = useRouter() const { dispatch } = useNavigation() const { archiveItem, deleteItem, moveItem, shareItem } = diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index 18df92ec3..820b36d58 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -556,8 +556,8 @@ export function useGetRawSearchItemsQuery( } ) - let responseError = error - let responseData = data as LibraryItemsData | undefined + const responseError = error + const 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, From a766da372cd1304cf9e9670916de4c665f0e8a40 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 10 Jul 2024 20:50:07 +0800 Subject: [PATCH 4/5] Fix for the keyboard commands modal --- .../components/templates/KeyboardShortcutListModal.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/web/components/templates/KeyboardShortcutListModal.tsx b/packages/web/components/templates/KeyboardShortcutListModal.tsx index 69cc4a2a1..ada10ee92 100644 --- a/packages/web/components/templates/KeyboardShortcutListModal.tsx +++ b/packages/web/components/templates/KeyboardShortcutListModal.tsx @@ -178,7 +178,14 @@ export function KeyboardShortcutListModal( > { + return { + shortcutKeys: action.shortcut ?? [], + callback: () => {}, + actionDescription: action.name, + shortcutKeyDescription: (action.shortcut ?? []).join(','), + } + })} /> Date: Wed, 10 Jul 2024 20:52:08 +0800 Subject: [PATCH 5/5] Clean up build --- packages/web/components/templates/PrimaryLayout.tsx | 2 -- packages/web/components/templates/SettingsLayout.tsx | 1 - packages/web/pages/home/debug.tsx | 3 --- 3 files changed, 6 deletions(-) diff --git a/packages/web/components/templates/PrimaryLayout.tsx b/packages/web/components/templates/PrimaryLayout.tsx index fdf7b1454..6e5156f8a 100644 --- a/packages/web/components/templates/PrimaryLayout.tsx +++ b/packages/web/components/templates/PrimaryLayout.tsx @@ -34,8 +34,6 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element { const [showKeyboardCommandsModal, setShowKeyboardCommandsModal] = useState(false) - // useKeyboardShortcuts(navigationCommands(router)) - useKeyboardShortcuts( primaryCommands((action) => { switch (action) { diff --git a/packages/web/components/templates/SettingsLayout.tsx b/packages/web/components/templates/SettingsLayout.tsx index 800996992..36859fdfd 100644 --- a/packages/web/components/templates/SettingsLayout.tsx +++ b/packages/web/components/templates/SettingsLayout.tsx @@ -61,7 +61,6 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element { const [showKeyboardCommandsModal, setShowKeyboardCommandsModal] = useState(false) - useKeyboardShortcuts(navigationCommands(router)) applyStoredTheme() const showLogout = useCallback(() => { diff --git a/packages/web/pages/home/debug.tsx b/packages/web/pages/home/debug.tsx index ff2773555..376afe110 100644 --- a/packages/web/pages/home/debug.tsx +++ b/packages/web/pages/home/debug.tsx @@ -38,9 +38,6 @@ export default function DebugHome(): JSX.Element { const refreshHome = useCallback(() => { ;(async () => { refreshHomeMutation() - if (homeData?.mutate) { - homeData.mutate() - } })() }, [])