mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Improve fallback when no home found, add keyboard navigation
This commit is contained in:
parent
a536b21c64
commit
486f0f2870
11 changed files with 674 additions and 461 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export default function useLibraryItemActions() {
|
|||
archived: true,
|
||||
})
|
||||
|
||||
console.log('result: ', result)
|
||||
if (result) {
|
||||
showSuccessToast('Link archived', { position: 'bottom-right' })
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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) ? (
|
||||
|
|
|
|||
20
yarn.lock
20
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"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue