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"