2024-06-06 09:10:55 +00:00
import { Action , createAction , useKBar , useRegisterActions } from 'kbar'
import debounce from 'lodash/debounce'
import { useRouter } from 'next/router'
import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
import TopBarProgress from 'react-topbar-progress-indicator'
import { useFetchMore } from '../../../lib/hooks/useFetchMoreScroll'
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
import { libraryListCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
import {
SearchItem ,
TypeaheadSearchItemsData ,
typeaheadSearchQuery ,
} from '../../../lib/networking/queries/typeaheadSearch'
2024-07-28 12:41:16 +00:00
import {
2024-06-06 09:10:55 +00:00
LibraryItem ,
2024-06-25 01:11:15 +00:00
LibraryItemNode ,
2024-07-28 12:41:16 +00:00
LibraryItems ,
2024-06-06 09:10:55 +00:00
LibraryItemsQueryInput ,
2024-07-29 09:30:47 +00:00
useArchiveItem ,
2024-07-30 13:00:18 +00:00
useBulkActions ,
2024-07-29 09:30:47 +00:00
useDeleteItem ,
2024-07-28 12:41:16 +00:00
useGetLibraryItems ,
2024-07-29 10:23:46 +00:00
useMoveItemToFolder ,
2024-08-12 14:27:24 +00:00
useRefreshProcessingItems ,
2024-07-29 09:30:47 +00:00
useUpdateItemReadStatus ,
2024-07-28 12:41:16 +00:00
} from '../../../lib/networking/library_items/useLibraryItems'
2024-06-06 09:10:55 +00:00
import {
useGetViewerQuery ,
UserBasicData ,
} from '../../../lib/networking/queries/useGetViewerQuery'
import { Button } from '../../elements/Button'
import { StyledText } from '../../elements/StyledText'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes'
import { LinkedItemCard } from '../../patterns/LibraryCards/LinkedItemCard'
import { Box , HStack , SpanBox , VStack } from '../../elements/LayoutPrimitives'
import { AddLinkModal } from '../AddLinkModal'
import { EditLibraryItemModal } from '../homeFeed/EditItemModals'
import { EmptyLibrary } from '../homeFeed/EmptyLibrary'
2024-06-25 03:08:56 +00:00
import { MultiSelectMode } from '../homeFeed/LibraryHeader'
2024-06-06 09:10:55 +00:00
import { UploadModal } from '../UploadModal'
2024-07-30 13:00:18 +00:00
import { BulkAction } from '../../../lib/networking/library_items/useLibraryItems'
2024-08-12 12:40:28 +00:00
import { showErrorToast , showSuccessToast } from '../../../lib/toastHelpers'
2024-06-06 09:10:55 +00:00
import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter'
import { NotebookPresenter } from '../article/NotebookPresenter'
import { PinnedButtons } from '../homeFeed/PinnedButtons'
import { PinnedSearch } from '../../../pages/settings/pinned-searches'
import { FetchItemsError } from '../homeFeed/FetchItemsError'
import { LibraryHeader } from './LibraryHeader'
2024-06-25 11:40:26 +00:00
import { TrashIcon } from '../../elements/icons/TrashIcon'
import { theme } from '../../tokens/stitches.config'
import { emptyTrashMutation } from '../../../lib/networking/mutations/emptyTrashMutation'
2024-07-28 12:41:16 +00:00
import { State } from '../../../lib/networking/fragments/articleFragment'
2024-08-12 07:03:05 +00:00
import { useHandleAddUrl } from '../../../lib/hooks/useHandleAddUrl'
2024-08-14 03:41:55 +00:00
import { useGetViewer } from '../../../lib/networking/viewer/useGetViewer'
2024-08-15 08:27:05 +00:00
import { Spinner } from '@phosphor-icons/react/dist/ssr'
2024-06-06 09:10:55 +00:00
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
const fetchSearchResults = async ( query : string , cb : any ) = > {
if ( ! query . startsWith ( '#' ) ) return
const res = await typeaheadSearchQuery ( {
limit : 10 ,
searchQuery : query.substring ( 1 ) ,
} )
cb ( res )
}
const debouncedFetchSearchResults = debounce ( ( query , cb ) = > {
fetchSearchResults ( query , cb )
} , 300 )
2024-06-11 08:00:25 +00:00
type LibraryContainerProps = {
2024-08-07 06:55:19 +00:00
folder : string | undefined
2024-06-25 01:11:15 +00:00
filterFunc : ( item : LibraryItemNode ) = > boolean
2024-06-25 03:34:22 +00:00
showNavigationMenu : boolean
2024-06-11 08:00:25 +00:00
}
export function LibraryContainer ( props : LibraryContainerProps ) : JSX . Element {
2024-06-06 09:10:55 +00:00
const router = useRouter ( )
2024-08-14 03:41:55 +00:00
const { data : viewerData } = useGetViewer ( )
2024-06-06 09:10:55 +00:00
const { queryValue } = useKBar ( ( state ) = > ( { queryValue : state.searchQuery } ) )
const [ searchResults , setSearchResults ] = useState < SearchItem [ ] > ( [ ] )
const defaultQuery = {
2024-08-15 05:20:29 +00:00
limit : 10 ,
2024-06-11 08:00:25 +00:00
folder : props.folder ,
2024-06-06 09:10:55 +00:00
sortDescending : true ,
searchQuery : undefined ,
}
const gridContainerRef = useRef < HTMLDivElement > ( null )
2024-08-07 06:55:19 +00:00
const [ labelsTarget , setLabelsTarget ] =
useState < LibraryItem | undefined > ( undefined )
2024-06-06 09:10:55 +00:00
2024-08-07 06:55:19 +00:00
const [ notebookTarget , setNotebookTarget ] =
useState < LibraryItem | undefined > ( undefined )
2024-06-06 09:10:55 +00:00
const [ showAddLinkModal , setShowAddLinkModal ] = useState ( false )
const [ showEditTitleModal , setShowEditTitleModal ] = useState ( false )
const [ linkToEdit , setLinkToEdit ] = useState < LibraryItem > ( )
const [ linkToUnsubscribe , setLinkToUnsubscribe ] = useState < LibraryItem > ( )
2024-07-29 09:30:47 +00:00
const archiveItem = useArchiveItem ( )
const deleteItem = useDeleteItem ( )
2024-07-29 10:23:46 +00:00
const moveToFolder = useMoveItemToFolder ( )
2024-07-30 13:00:18 +00:00
const bulkAction = useBulkActions ( )
2024-07-29 09:30:47 +00:00
const updateItemReadStatus = useUpdateItemReadStatus ( )
2024-06-06 09:10:55 +00:00
const [ queryInputs , setQueryInputs ] =
useState < LibraryItemsQueryInput > ( defaultQuery )
const {
2024-07-28 12:41:16 +00:00
data : itemsPages ,
isLoading ,
2024-08-16 07:55:36 +00:00
isFetchingNextPage ,
2024-08-14 03:41:55 +00:00
isFetching ,
2024-07-28 12:41:16 +00:00
fetchNextPage ,
hasNextPage ,
2024-06-06 09:10:55 +00:00
error : fetchItemsError ,
2024-08-15 07:24:02 +00:00
} = useGetLibraryItems ( props . folder ? ? 'home' , props . folder , queryInputs )
2024-07-28 12:41:16 +00:00
2024-06-06 09:10:55 +00:00
useEffect ( ( ) = > {
if ( queryValue . startsWith ( '#' ) ) {
debouncedFetchSearchResults (
queryValue ,
( data : TypeaheadSearchItemsData ) = > {
setSearchResults ( data ? . typeaheadSearch . items || [ ] )
}
)
} else setSearchResults ( [ ] )
} , [ queryValue ] )
2024-06-25 09:14:33 +00:00
useEffect ( ( ) = > {
if ( ! router . isReady ) return
const q = router . query [ 'q' ]
2024-06-26 01:13:01 +00:00
let qs = ''
2024-06-25 09:14:33 +00:00
if ( q && typeof q === 'string' ) {
qs = q
}
if ( qs !== ( queryInputs . searchQuery || '' ) ) {
setQueryInputs ( { . . . queryInputs , searchQuery : qs } )
2024-07-28 12:41:16 +00:00
// performActionOnItem('refresh', undefined as unknown as any)
2024-06-25 09:14:33 +00:00
}
// intentionally not watching queryInputs and performActionOnItem
// here to prevent infinite looping
// eslint-disable-next-line react-hooks/exhaustive-deps
} , [ setQueryInputs , router . isReady , router . query ] )
2024-06-25 09:54:30 +00:00
useEffect ( ( ) = > {
window . localStorage . setItem ( 'nav-return' , router . asPath )
} , [ router . asPath ] )
2024-06-06 09:10:55 +00:00
const libraryItems = useMemo ( ( ) = > {
const items =
2024-07-28 12:41:16 +00:00
itemsPages ? . pages
. flatMap ( ( ad : LibraryItems ) = > {
2024-08-20 10:00:38 +00:00
if ( ! ad . edges ) {
return [ ]
}
2024-07-28 12:41:16 +00:00
return ad . edges . map ( ( it ) = > ( {
2024-06-25 01:11:15 +00:00
. . . it ,
isLoading : it.node.state === 'PROCESSING' ,
} ) )
} )
. filter ( ( item ) = > props . filterFunc ( item . node ) ) || [ ]
2024-06-06 09:10:55 +00:00
return items
2024-07-28 12:41:16 +00:00
} , [ itemsPages ] )
2024-06-06 09:10:55 +00:00
useEffect ( ( ) = > {
if ( localStorage ) {
localStorage . setItem (
'library-slug-list' ,
JSON . stringify ( libraryItems . map ( ( li ) = > li . node . slug ) )
)
}
} , [ libraryItems ] )
2024-08-16 09:16:29 +00:00
const processingItems = useMemo ( ( ) = > {
return libraryItems
. filter ( ( li ) = > li . node . state === State . PROCESSING )
. map ( ( li ) = > li . node . id )
} , [ libraryItems ] )
2024-08-20 05:19:29 +00:00
const refreshProcessingItems = useRefreshProcessingItems ( )
useEffect ( ( ) = > {
if ( processingItems . length ) {
refreshProcessingItems . mutateAsync ( {
attempt : 0 ,
itemIds : processingItems ,
} )
}
} , [ processingItems ] )
2024-06-06 09:10:55 +00:00
const focusFirstItem = useCallback ( ( ) = > {
if ( libraryItems . length < 1 ) {
return
}
const firstItem = libraryItems [ 0 ]
if ( ! firstItem ) {
return
}
const firstItemElement = document . getElementById ( firstItem . node . id )
if ( ! firstItemElement ) {
return
}
activateCard ( firstItem . node . id )
} , [ libraryItems ] )
const activateCard = useCallback (
( id : string ) = > {
if ( ! document . getElementById ( id ) ) {
return
}
setActiveCardId ( id )
scrollToActiveCard ( id , true )
const newItem = getItem ( id )
if ( notebookTarget && newItem ) {
setNotebookTarget ( newItem )
}
} ,
[ libraryItems ]
)
const isVisible = function ( ele : HTMLElement ) {
const container = window . document . documentElement
const eleTop = ele . offsetTop
const eleBottom = eleTop + ele . clientHeight
const containerTop = container . scrollTop + 200
const containerBottom = containerTop + container . clientHeight
return eleTop >= containerTop && eleBottom <= containerBottom
}
const scrollToActiveCard = useCallback (
( id : string | null , isSmouth? : boolean ) : void = > {
if ( id ) {
const target = document . getElementById ( id )
if ( target ) {
try {
if ( ! isVisible ( target ) ) {
target . scrollIntoView ( {
block : 'center' ,
behavior : isSmouth ? 'smooth' : 'auto' ,
} )
}
target . focus ( {
preventScroll : true ,
} )
} catch ( error ) {
console . log ( 'Cannot Scroll' , error )
}
}
}
} ,
[ ]
)
const alreadyScrolled = useRef < boolean > ( false )
const [ activeCardId , setActiveCardId ] = usePersistedState < string | null > ( {
key : ` --library-active-card-id ` ,
initialValue : null ,
isSessionStorage : true ,
} )
const activeItem = useMemo ( ( ) = > {
if ( ! activeCardId ) {
return undefined
}
return libraryItems . find ( ( item ) = > item . node . id === activeCardId )
} , [ libraryItems , activeCardId ] )
const getItem = useCallback (
( itemId : string ) = > {
return libraryItems . find ( ( item ) = > item . node . id === itemId )
} ,
[ libraryItems ]
)
const activeItemIndex = useMemo ( ( ) = > {
if ( ! activeCardId ) {
return undefined
}
const result = libraryItems . findIndex (
( item ) = > item . node . id === activeCardId
)
return result >= 0 ? result : undefined
} , [ libraryItems , activeCardId ] )
useEffect ( ( ) = > {
2024-08-15 08:27:05 +00:00
console . log ( 'active card id: ' , activeCardId )
2024-06-06 09:10:55 +00:00
if ( activeCardId && ! alreadyScrolled . current ) {
scrollToActiveCard ( activeCardId )
alreadyScrolled . current = true
}
} , [ activeCardId , scrollToActiveCard ] )
const handleCardAction = async (
action : LinkedItemCardAction ,
item : LibraryItem | undefined
) : Promise < void > = > {
if ( ! item ) {
return
}
2024-07-29 09:30:47 +00:00
switch ( action ) {
case 'showDetail' :
2024-08-14 03:41:55 +00:00
const username = viewerData ? . profile . username
2024-07-29 09:30:47 +00:00
if ( username ) {
setActiveCardId ( item . node . id )
if ( item . node . state === State . PROCESSING ) {
router . push ( ` /article?url= ${ encodeURIComponent ( item . node . url ) } ` )
} else {
router . push ( ` / ${ username } / ${ item . node . slug } ` )
}
}
break
case 'showOriginal' :
const url = item . node . originalArticleUrl
if ( url ) {
window . open ( url , '_blank' )
}
break
case 'archive' :
case 'unarchive' :
try {
await archiveItem . mutateAsync ( {
2024-07-30 07:48:21 +00:00
itemId : item.node.id ,
slug : item.node.slug ,
input : {
linkId : item.node.id ,
archived : action == 'archive' ,
} ,
2024-07-29 09:30:47 +00:00
} )
2024-07-29 11:45:10 +00:00
} catch ( err ) {
console . log ( 'Error setting archive state: ' , err )
2024-07-29 09:30:47 +00:00
showErrorToast ( ` Error ${ action } ing item ` , {
position : 'bottom-right' ,
} )
return
}
showSuccessToast ( ` Item ${ action } d ` , {
position : 'bottom-right' ,
} )
break
case 'delete' :
try {
2024-07-30 07:48:21 +00:00
await deleteItem . mutateAsync ( {
itemId : item.node.id ,
slug : item.node.slug ,
} )
2024-07-29 11:45:10 +00:00
} catch ( err ) {
console . log ( 'Error deleting item: ' , err )
2024-07-29 09:30:47 +00:00
showErrorToast ( ` Error deleting item ` , {
position : 'bottom-right' ,
} )
return
}
2024-08-23 05:29:16 +00:00
2024-07-29 09:30:47 +00:00
showSuccessToast ( ` Item deleted ` , {
position : 'bottom-right' ,
} )
break
case 'mark-read' :
case 'mark-unread' :
const desc = action == 'mark-read' ? 'read' : 'unread'
const values =
action == 'mark-read'
? {
readingProgressPercent : 100 ,
readingProgressTopPercent : 100 ,
readingProgressAnchorIndex : 0 ,
}
: {
readingProgressPercent : 0 ,
readingProgressTopPercent : 0 ,
readingProgressAnchorIndex : 0 ,
}
try {
await updateItemReadStatus . mutateAsync ( {
2024-07-30 07:48:21 +00:00
itemId : item.node.id ,
slug : item.node.slug ,
input : {
id : item.node.id ,
force : true ,
. . . values ,
} ,
2024-07-29 09:30:47 +00:00
} )
2024-07-29 11:45:10 +00:00
} catch ( err ) {
console . log ( 'Error marking item: ' , err )
2024-07-29 09:30:47 +00:00
showErrorToast ( ` Error marking as ${ desc } ` , {
position : 'bottom-right' ,
} )
return
}
break
2024-07-29 10:23:46 +00:00
case 'move-to-inbox' :
try {
await moveToFolder . mutateAsync ( {
itemId : item.node.id ,
2024-07-30 07:48:21 +00:00
slug : item.node.slug ,
2024-07-29 10:23:46 +00:00
folder : 'inbox' ,
} )
2024-07-29 11:45:10 +00:00
} catch ( err ) {
console . log ( 'Error moving item: ' , err )
2024-07-29 10:23:46 +00:00
showErrorToast ( ` Error moving item ` , {
position : 'bottom-right' ,
} )
return
}
showSuccessToast ( ` Item moved to library ` , {
position : 'bottom-right' ,
} )
break
2024-07-29 09:30:47 +00:00
case 'set-labels' :
setLabelsTarget ( item )
break
case 'open-notebook' :
if ( ! notebookTarget ) {
setNotebookTarget ( item )
} else {
setNotebookTarget ( undefined )
}
break
case 'unsubscribe' :
2024-07-29 09:38:09 +00:00
// setLinkToUnsubscribe(item.node)
2024-07-29 09:30:47 +00:00
break
default :
console . warn ( 'unknown action: ' , action )
}
2024-06-06 09:10:55 +00:00
}
const modalTargetItem = useMemo ( ( ) = > {
return labelsTarget || linkToEdit || linkToUnsubscribe
} , [ labelsTarget , linkToEdit , linkToUnsubscribe ] )
const [ checkedItems , setCheckedItems ] = useState < string [ ] > ( [ ] )
const [ multiSelectMode , setMultiSelectMode ] = useState < MultiSelectMode > ( 'off' )
const selectActiveArticle = useCallback ( ( ) = > {
if ( activeItem ) {
if ( multiSelectMode === 'off' ) {
setMultiSelectMode ( 'some' )
}
const itemId = activeItem . node . id
const isChecked = itemIsChecked ( itemId )
setIsChecked ( itemId , ! isChecked )
}
} , [ activeItem , multiSelectMode , checkedItems ] )
useKeyboardShortcuts (
libraryListCommands ( ( action ) = > {
const columnCount = ( container : HTMLDivElement ) = > {
const gridComputedStyle = window . getComputedStyle ( container )
const gridColumnCount = gridComputedStyle
. getPropertyValue ( 'grid-template-columns' )
. split ( ' ' ) . length
return gridColumnCount
}
// If any of the modals are open we disable handling keyboard shortcuts
if ( modalTargetItem ) {
return
}
switch ( action ) {
case 'openArticle' :
if ( multiSelectMode !== 'off' && activeItem ) {
const itemId = activeItem . node . id
const isChecked = itemIsChecked ( itemId )
setIsChecked ( itemId , ! isChecked )
} else {
handleCardAction ( 'showDetail' , activeItem )
}
break
case 'selectArticle' :
selectActiveArticle ( )
break
case 'openOriginalArticle' :
handleCardAction ( 'showOriginal' , activeItem )
break
case 'showAddLinkModal' :
setTimeout ( ( ) = > setShowAddLinkModal ( true ) , 0 )
break
case 'moveFocusToNextListItem' : {
const currentItemIndex = activeItemIndex
const nextItemIndex =
currentItemIndex == undefined ? 0 : currentItemIndex + 1
const nextItem = libraryItems [ nextItemIndex ]
if ( nextItem ) {
activateCard ( nextItem . node . id )
}
break
}
case 'moveFocusToPreviousListItem' : {
const currentItemIndex = activeItemIndex
const previousItemIndex =
currentItemIndex == undefined ? 0 : currentItemIndex - 1
const previousItem = libraryItems [ previousItemIndex ]
if ( previousItem ) {
activateCard ( previousItem . node . id )
}
break
}
case 'moveFocusToNextRowItem' : {
const selectedItemIndex = activeItemIndex
if ( selectedItemIndex !== undefined && gridContainerRef ? . current ) {
const nextItemIndex = Math . min (
selectedItemIndex + columnCount ( gridContainerRef . current ) ,
libraryItems . length - 1
)
const nextItem = libraryItems [ nextItemIndex ]
if ( nextItem ) {
const nextItemElement = document . getElementById ( nextItem . node . id )
if ( nextItemElement ) {
activateCard ( nextItem . node . id )
}
}
} else {
focusFirstItem ( )
}
break
}
case 'moveFocusToPreviousRowItem' : {
const selectedItemIndex = activeItemIndex
if ( selectedItemIndex !== undefined && gridContainerRef ? . current ) {
const nextItemIndex = Math . max (
0 ,
selectedItemIndex - columnCount ( gridContainerRef . current )
)
const nextItem = libraryItems [ nextItemIndex ]
if ( nextItem ) {
const nextItemElement = document . getElementById ( nextItem . node . id )
if ( nextItemElement ) {
activateCard ( nextItem . node . id )
}
}
} else {
focusFirstItem ( )
}
break
}
case 'archiveItem' :
handleCardAction ( 'archive' , activeItem )
break
case 'removeItem' :
handleCardAction ( 'delete' , activeItem )
break
case 'markItemAsRead' :
handleCardAction ( 'mark-read' , activeItem )
break
case 'markItemAsUnread' :
handleCardAction ( 'mark-unread' , activeItem )
break
case 'showEditLabelsModal' :
handleCardAction ( 'set-labels' , activeItem )
break
case 'openNotebook' :
handleCardAction ( 'open-notebook' , activeItem )
break
case 'sortDescending' :
setQueryInputs ( { . . . queryInputs , sortDescending : true } )
break
case 'sortAscending' :
setQueryInputs ( { . . . queryInputs , sortDescending : false } )
break
case 'beginMultiSelect' :
if ( multiSelectMode == 'off' ) {
setMultiSelectMode ( 'none' )
}
break
case 'endMultiSelect' :
setMultiSelectMode ( 'off' )
break
}
} )
)
2024-07-28 12:41:16 +00:00
const ARCHIVE_ACTION =
activeItem ? . node . state !== State . ARCHIVED
? createAction ( {
section : 'Library' ,
name : 'Archive selected item' ,
shortcut : [ 'e' ] ,
perform : ( ) = > handleCardAction ( 'archive' , activeItem ) ,
} )
: createAction ( {
section : 'Library' ,
name : 'UnArchive selected item' ,
shortcut : [ 'e' ] ,
perform : ( ) = > handleCardAction ( 'unarchive' , activeItem ) ,
} )
2024-06-06 09:10:55 +00:00
const ACTIVE_ACTIONS = [
ARCHIVE_ACTION ,
createAction ( {
section : 'Library' ,
name : 'Remove item' ,
shortcut : [ '#' ] ,
perform : ( ) = > handleCardAction ( 'delete' , activeItem ) ,
} ) ,
createAction ( {
section : 'Library' ,
name : 'Edit item labels' ,
shortcut : [ 'l' ] ,
perform : ( ) = > handleCardAction ( 'set-labels' , activeItem ) ,
} ) ,
createAction ( {
section : 'Library' ,
name : 'Open Notebook' ,
shortcut : [ 't' ] ,
perform : ( ) = > handleCardAction ( 'open-notebook' , activeItem ) ,
} ) ,
createAction ( {
section : 'Library' ,
name : 'Mark item as read' ,
shortcut : [ '-' ] ,
perform : ( ) = > {
handleCardAction ( 'mark-read' , activeItem )
} ,
} ) ,
createAction ( {
section : 'Library' ,
name : 'Mark item as unread' ,
shortcut : [ '_' ] ,
perform : ( ) = > handleCardAction ( 'mark-unread' , activeItem ) ,
} ) ,
]
const UNACTIVE_ACTIONS : Action [ ] = [
// createAction({
// section: 'Library',
// name: 'Sort in ascending order',
// shortcut: ['s', 'o'],
// perform: () => setQueryInputs({ ...queryInputs, sortDescending: false }),
// }),
// createAction({
// section: 'Library',
// name: 'Sort in descending order',
// shortcut: ['s', 'n'],
// perform: () => setQueryInputs({ ...queryInputs, sortDescending: true }),
// }),
]
useRegisterActions (
searchResults . map ( ( link ) = > ( {
id : link.id ,
section : 'Search Results' ,
name : link.title ,
keywords : '#' + link . title + ' #' + link . siteName ,
perform : ( ) = > {
2024-08-14 03:41:55 +00:00
const username = viewerData ? . profile . username
2024-06-06 09:10:55 +00:00
if ( username ) {
setActiveCardId ( link . id )
router . push ( ` / ${ username } / ${ link . slug } ` )
}
} ,
} ) ) ,
[ searchResults ]
)
useRegisterActions (
activeCardId ? [ . . . ACTIVE_ACTIONS , . . . UNACTIVE_ACTIONS ] : UNACTIVE_ACTIONS ,
[ activeCardId , activeItem ]
)
2024-08-19 08:06:10 +00:00
useFetchMore ( ( ) = > {
if ( ! isFetching && ! isLoading && hasNextPage ) {
fetchNextPage ( )
2024-08-19 07:58:56 +00:00
}
2024-08-19 08:06:10 +00:00
} )
2024-08-16 07:55:36 +00:00
2024-06-06 09:10:55 +00:00
const setIsChecked = useCallback (
( itemId : string , set : boolean ) = > {
if ( set && checkedItems . indexOf ( itemId ) === - 1 ) {
checkedItems . push ( itemId )
setCheckedItems ( [ . . . checkedItems ] )
} else if ( ! set && checkedItems . indexOf ( itemId ) !== - 1 ) {
checkedItems . splice ( checkedItems . indexOf ( itemId ) , 1 )
setCheckedItems ( [ . . . checkedItems ] )
}
if ( set && multiSelectMode == 'off' ) {
setMultiSelectMode ( 'some' )
}
if ( checkedItems . length < 1 ) {
setMultiSelectMode ( 'off' )
}
} ,
[ checkedItems , multiSelectMode , setMultiSelectMode ]
)
useEffect ( ( ) = > {
switch ( multiSelectMode ) {
case 'off' :
case 'none' :
setCheckedItems ( [ ] )
break
case 'some' :
break
case 'search' :
case 'visible' :
const allIds = (
2024-07-28 12:41:16 +00:00
itemsPages ? . pages . flatMap ( ( ad ) = > {
return ad . edges
2024-06-06 09:10:55 +00:00
} ) || [ ]
) . map ( ( item ) = > item . node . id )
setCheckedItems ( allIds )
break
}
} , [ multiSelectMode ] )
const itemIsChecked = useCallback (
( itemId : string ) = > {
return checkedItems . indexOf ( itemId ) !== - 1
} ,
[ checkedItems ]
)
const performMultiSelectAction = useCallback (
( action : BulkAction , labelIds? : string [ ] ) = > {
if ( multiSelectMode === 'off' ) {
return
}
if ( multiSelectMode !== 'search' && checkedItems . length < 1 ) {
return
}
; ( async ( ) = > {
const query =
multiSelectMode === 'search'
? queryInputs . searchQuery || 'in:inbox'
: ` includes: ${ checkedItems . join ( ',' ) } `
2024-07-28 12:41:16 +00:00
const expectedCount = checkedItems . length
2024-06-06 09:10:55 +00:00
2024-07-30 13:43:02 +00:00
let bulkArguments = undefined
if ( action == BulkAction . MOVE_TO_FOLDER ) {
bulkArguments = { folder : 'inbox ' }
}
2024-06-06 09:10:55 +00:00
try {
2024-07-30 13:00:18 +00:00
const res = await bulkAction . mutateAsync ( {
2024-06-06 09:10:55 +00:00
action ,
query ,
expectedCount ,
2024-07-30 13:00:18 +00:00
labelIds ,
2024-07-30 13:43:02 +00:00
arguments : bulkArguments ,
2024-07-30 13:00:18 +00:00
} )
2024-06-06 09:10:55 +00:00
if ( res ) {
let successMessage : string | undefined = undefined
switch ( action ) {
case BulkAction . ARCHIVE :
successMessage = 'Link Archived'
break
case BulkAction . ADD_LABELS :
successMessage = 'Labels Added'
break
case BulkAction . DELETE :
successMessage = 'Items deleted'
break
case BulkAction . MARK_AS_READ :
successMessage = 'Items marked as read'
break
2024-07-30 13:43:02 +00:00
case BulkAction . MOVE_TO_FOLDER :
successMessage = 'Items moved to library'
break
2024-06-06 09:10:55 +00:00
}
if ( successMessage ) {
showSuccessToast ( successMessage , { position : 'bottom-right' } )
}
} else {
showErrorToast ( 'Error performing bulk action' , {
position : 'bottom-right' ,
} )
}
} catch ( err ) {
showErrorToast ( 'Error performing bulk action' , {
position : 'bottom-right' ,
} )
}
2024-07-28 12:41:16 +00:00
// mutate()
2024-06-06 09:10:55 +00:00
} ) ( )
setMultiSelectMode ( 'off' )
} ,
[ itemsPages , multiSelectMode , checkedItems ]
)
return (
< HomeFeedGrid
2024-06-25 11:40:26 +00:00
folder = { props . folder }
2024-06-06 09:10:55 +00:00
items = { libraryItems }
actionHandler = { handleCardAction }
setIsChecked = { setIsChecked }
itemIsChecked = { itemIsChecked }
multiSelectMode = { multiSelectMode }
2024-06-25 03:34:22 +00:00
showNavigationMenu = { props . showNavigationMenu }
2024-06-06 09:10:55 +00:00
setMultiSelectMode = { setMultiSelectMode }
performMultiSelectAction = { performMultiSelectAction }
searchTerm = { queryInputs . searchQuery }
gridContainerRef = { gridContainerRef }
applySearchQuery = { ( searchQuery : string ) = > {
setQueryInputs ( {
. . . queryInputs ,
searchQuery ,
} )
const qp = new URLSearchParams ( window . location . search )
if ( searchQuery ) {
qp . set ( 'q' , searchQuery )
} else {
qp . delete ( 'q' )
}
const href = ` ${ window . location . pathname } ? ${ qp . toString ( ) } `
router . push ( href , href , { shallow : true } )
window . sessionStorage . setItem ( 'q' , qp . toString ( ) )
} }
2024-07-28 12:41:16 +00:00
loadMore = { fetchNextPage }
hasMore = { hasNextPage ? ? false }
2024-06-06 09:10:55 +00:00
hasData = { ! ! itemsPages }
2024-08-23 05:29:16 +00:00
isValidating = { isLoading || isFetching || isFetchingNextPage }
2024-06-06 09:10:55 +00:00
fetchItemsError = { ! ! fetchItemsError }
labelsTarget = { labelsTarget }
setLabelsTarget = { setLabelsTarget }
notebookTarget = { notebookTarget }
setNotebookTarget = { setNotebookTarget }
showAddLinkModal = { showAddLinkModal }
setShowAddLinkModal = { setShowAddLinkModal }
showEditTitleModal = { showEditTitleModal }
setShowEditTitleModal = { setShowEditTitleModal }
setActiveItem = { ( item : LibraryItem ) = > {
activateCard ( item . node . id )
} }
linkToEdit = { linkToEdit }
setLinkToEdit = { setLinkToEdit }
linkToUnsubscribe = { linkToUnsubscribe }
setLinkToUnsubscribe = { setLinkToUnsubscribe }
2024-07-28 12:41:16 +00:00
numItemsSelected = { checkedItems . length }
2024-06-06 09:10:55 +00:00
/ >
)
}
export type HomeFeedContentProps = {
2024-08-07 06:55:19 +00:00
folder : string | undefined
2024-06-06 09:10:55 +00:00
items : LibraryItem [ ]
searchTerm? : string
gridContainerRef : React.RefObject < HTMLDivElement >
applySearchQuery : ( searchQuery : string ) = > void
hasMore : boolean
hasData : boolean
isValidating : boolean
fetchItemsError : boolean
loadMore : ( ) = > void
labelsTarget : LibraryItem | undefined
setLabelsTarget : ( target : LibraryItem | undefined ) = > void
notebookTarget : LibraryItem | undefined
setNotebookTarget : ( target : LibraryItem | undefined ) = > void
showAddLinkModal : boolean
setShowAddLinkModal : ( show : boolean ) = > void
showEditTitleModal : boolean
setShowEditTitleModal : ( show : boolean ) = > void
setActiveItem : ( item : LibraryItem ) = > void
linkToEdit : LibraryItem | undefined
setLinkToEdit : ( set : LibraryItem | undefined ) = > void
linkToUnsubscribe : LibraryItem | undefined
setLinkToUnsubscribe : ( set : LibraryItem | undefined ) = > void
actionHandler : (
action : LinkedItemCardAction ,
item : LibraryItem | undefined
) = > Promise < void >
2024-06-25 03:34:22 +00:00
showNavigationMenu : boolean
2024-06-06 09:10:55 +00:00
setIsChecked : ( itemId : string , set : boolean ) = > void
itemIsChecked : ( itemId : string ) = > boolean
multiSelectMode : MultiSelectMode
setMultiSelectMode : ( mode : MultiSelectMode ) = > void
numItemsSelected : number
performMultiSelectAction : ( action : BulkAction , labelIds? : string [ ] ) = > void
}
function HomeFeedGrid ( props : HomeFeedContentProps ) : JSX . Element {
2024-08-14 03:41:55 +00:00
const { data : viewerData } = useGetViewer ( )
2024-06-06 09:10:55 +00:00
const [ layout , setLayout ] = usePersistedState < LayoutType > ( {
key : 'libraryLayout' ,
initialValue : 'LIST_LAYOUT' ,
} )
const updateLayout = useCallback (
async ( newLayout : LayoutType ) = > {
if ( layout === newLayout ) return
setLayout ( newLayout )
} ,
[ layout , setLayout ]
)
const showItems = useMemo ( ( ) = > {
if ( props . fetchItemsError ) {
return false
}
if ( ! props . isValidating && props . items . length <= 0 ) {
return false
}
return true
} , [ props ] )
2024-08-12 07:03:05 +00:00
const addUrl = useHandleAddUrl ( )
2024-06-06 09:10:55 +00:00
return (
< VStack
css = { {
height : '100%' ,
px : '20px' ,
py : '20px' ,
2024-06-11 08:00:25 +00:00
width : '100%' ,
2024-06-12 13:55:36 +00:00
'@mdDown' : {
2024-06-25 07:22:30 +00:00
px : '0px' ,
2024-06-12 13:55:36 +00:00
} ,
2024-06-06 09:10:55 +00:00
} }
distribution = "start"
alignment = "start"
>
< LibraryHeader
layout = { layout }
2024-07-30 13:43:02 +00:00
folder = { props . folder }
2024-08-14 03:41:55 +00:00
viewer = { viewerData }
2024-06-06 09:10:55 +00:00
updateLayout = { updateLayout }
2024-06-25 03:34:22 +00:00
showFilterMenu = { props . showNavigationMenu }
2024-06-06 09:10:55 +00:00
searchTerm = { props . searchTerm }
applySearchQuery = { ( searchQuery : string ) = > {
props . applySearchQuery ( searchQuery )
} }
multiSelectMode = { props . multiSelectMode }
setMultiSelectMode = { props . setMultiSelectMode }
numItemsSelected = { props . numItemsSelected }
performMultiSelectAction = { props . performMultiSelectAction }
/ >
< HStack css = { { width : '100%' , height : '100%' } } >
{ ! showItems && props . fetchItemsError && < FetchItemsError / > }
{ ! showItems && ! props . fetchItemsError && props . items . length <= 0 && (
< EmptyLibrary
2024-08-27 08:05:00 +00:00
folder = { props . folder ? ? 'home' }
2024-06-06 09:10:55 +00:00
onAddLinkClicked = { ( ) = > {
props . setShowAddLinkModal ( true )
} }
/ >
) }
{ showItems && (
< LibraryItemsLayout
2024-08-14 03:41:55 +00:00
viewer = { viewerData }
2024-06-06 09:10:55 +00:00
layout = { layout }
isChecked = { props . itemIsChecked }
{ . . . props }
/ >
) }
{ props . showAddLinkModal && (
< AddLinkModal
2024-08-12 07:03:05 +00:00
handleLinkSubmission = { addUrl }
2024-06-06 09:10:55 +00:00
onOpenChange = { ( ) = > props . setShowAddLinkModal ( false ) }
/ >
) }
< / HStack >
< / VStack >
)
}
type LibraryItemsLayoutProps = {
2024-08-07 06:55:19 +00:00
folder : string | undefined
2024-06-25 11:40:26 +00:00
2024-06-06 09:10:55 +00:00
layout : LayoutType
viewer? : UserBasicData
multiSelectMode : MultiSelectMode
setMultiSelectMode : ( mode : MultiSelectMode ) = > void
isChecked : ( itemId : string ) = > boolean
setIsChecked : ( itemId : string , set : boolean ) = > void
} & HomeFeedContentProps
export function LibraryItemsLayout (
props : LibraryItemsLayoutProps
) : JSX . Element {
const [ showUnsubscribeConfirmation , setShowUnsubscribeConfirmation ] =
useState ( false )
const [ showUploadModal , setShowUploadModal ] = useState ( false )
const unsubscribe = ( ) = > {
if ( ! props . linkToUnsubscribe ) {
return
}
props . actionHandler ( 'unsubscribe' , props . linkToUnsubscribe )
props . setLinkToUnsubscribe ( undefined )
setShowUnsubscribeConfirmation ( false )
}
const [ pinnedSearches , setPinnedSearches ] = usePersistedState <
PinnedSearch [ ] | null
> ( {
key : ` --library-pinned-searches ` ,
initialValue : [ ] ,
isSessionStorage : false ,
} )
return (
< >
< VStack
alignment = "start"
distribution = "start"
css = { {
height : '100%' ,
width : '100%' ,
paddingX : '40px' ,
2024-06-25 07:22:30 +00:00
'@mdDown' : {
mx : props.layout == 'GRID_LAYOUT' ? '20px' : '0px' ,
} ,
2024-06-06 09:10:55 +00:00
} }
>
< SpanBox
css = { {
alignSelf : 'flex-start' ,
'-ms-overflow-style' : 'none' ,
scrollbarWidth : 'none' ,
'::-webkit-scrollbar' : {
display : 'none' ,
} ,
'@lgDown' : {
display : 'none' ,
} ,
2024-06-13 07:29:08 +00:00
mt : '10px' ,
2024-06-06 09:10:55 +00:00
mb : '10px' ,
2024-06-13 07:29:08 +00:00
px : '70px' ,
'@xlgDown' : {
2024-06-14 04:19:45 +00:00
px : '0px' ,
2024-06-13 07:29:08 +00:00
} ,
2024-06-06 09:10:55 +00:00
} }
>
< PinnedButtons
multiSelectMode = { props . multiSelectMode }
layout = { props . layout }
items = { pinnedSearches ? ? [ ] }
searchTerm = { props . searchTerm }
applySearchQuery = { props . applySearchQuery }
/ >
< / SpanBox >
2024-06-25 11:40:26 +00:00
{ props . folder == 'trash' && (
< VStack
css = { {
alignSelf : 'flex-start' ,
'-ms-overflow-style' : 'none' ,
scrollbarWidth : 'none' ,
'::-webkit-scrollbar' : {
display : 'none' ,
} ,
'@lgDown' : {
display : 'none' ,
} ,
fontSize : '13px' ,
color : '$readerTextSubtle' ,
mt : '10px' ,
mb : '10px' ,
px : '70px' ,
'@xlgDown' : {
px : '0px' ,
} ,
} }
distribution = "start"
>
< HStack
alignment = "center"
distribution = "start"
css = { { gap : '10px' } }
>
< SpanBox css = { { pt : '4px' } } >
< TrashIcon
size = { 18 }
color = { theme . colors . thNotebookSubtle . toString ( ) }
/ >
< / SpanBox >
< VStack >
Items that remain in your trash for 14 days will be permanently
deleted .
< Button
style = "link"
css = { { textDecoration : 'underline' } }
onClick = { async ( event ) = > {
event . preventDefault ( )
2024-08-12 05:27:43 +00:00
alert (
'Emptying trash happens in the background and could take a few minutes depending on the number of items you have in the trash. You may see old items in your trash during this time.'
)
2024-06-25 11:40:26 +00:00
await emptyTrashMutation ( )
showSuccessToast ( 'Emptying trash' )
setTimeout ( ( ) = > {
props . actionHandler ( 'refresh' , undefined )
} , 500 )
} }
>
Empty trash now
< / Button >
< / VStack >
< / HStack >
< hr / >
< / VStack >
) }
2024-08-23 03:58:11 +00:00
{ props . isValidating && < TopBarProgress / > }
2024-06-06 09:10:55 +00:00
< div
onDragEnter = { ( event ) = > {
if (
event . dataTransfer . types . find ( ( t ) = > t . toLowerCase ( ) == 'files' )
) {
setShowUploadModal ( true )
}
} }
style = { { height : '100%' , width : '100%' } }
>
2024-07-28 12:41:16 +00:00
< LibraryItemsList
2024-06-25 11:40:26 +00:00
folder = { props . folder }
2024-06-06 09:10:55 +00:00
items = { props . items }
layout = { props . layout }
viewer = { props . viewer }
isChecked = { props . isChecked }
setIsChecked = { props . setIsChecked }
gridContainerRef = { props . gridContainerRef }
setShowEditTitleModal = { props . setShowEditTitleModal }
setLinkToEdit = { props . setLinkToEdit }
setShowUnsubscribeConfirmation = { setShowUnsubscribeConfirmation }
setLinkToUnsubscribe = { props . setLinkToUnsubscribe }
actionHandler = { props . actionHandler }
multiSelectMode = { props . multiSelectMode }
/ >
< HStack
distribution = "center"
css = { { width : '100%' , mt : '$2' , mb : '$4' } }
>
{ props . hasMore ? (
2024-08-15 08:27:05 +00:00
props . isValidating ? (
< Spinner / >
) : (
< Button
style = "ctaGray"
css = { {
cursor : props.isValidating ? 'not-allowed' : 'pointer' ,
} }
onClick = { props . loadMore }
disabled = { props . isValidating }
>
{ props . isValidating ? 'Loading' : 'More search results' }
< / Button >
)
2024-06-06 09:10:55 +00:00
) : (
< StyledText style = "caption" > < / StyledText >
) }
< / HStack >
< / div >
< / VStack >
{ props . showEditTitleModal && (
< EditLibraryItemModal
onOpenChange = { ( ) = > {
props . setShowEditTitleModal ( false )
props . setLinkToEdit ( undefined )
} }
2024-07-29 09:38:09 +00:00
updateItem = { async ( ) = > {
await Promise . resolve ( )
console . log ( 'item updated' )
} }
2024-06-06 09:10:55 +00:00
item = { props . linkToEdit as LibraryItem }
/ >
) }
{ showUnsubscribeConfirmation && (
< ConfirmationModal
message = { 'Are you sure you want to unsubscribe?' }
onAccept = { unsubscribe }
onOpenChange = { ( ) = > setShowUnsubscribeConfirmation ( false ) }
/ >
) }
{ props . labelsTarget ? . node . id && (
< SetPageLabelsModalPresenter
2024-07-30 07:48:21 +00:00
libraryItem = { props . labelsTarget . node }
2024-06-06 09:10:55 +00:00
onOpenChange = { ( ) = > {
if ( props . labelsTarget ) {
const activate = props . labelsTarget
props . setActiveItem ( activate )
props . setLabelsTarget ( undefined )
}
} }
/ >
) }
{ props . viewer && props . notebookTarget ? . node . id && (
< NotebookPresenter
viewer = { props . viewer }
item = { props . notebookTarget ? . node }
open = { props . notebookTarget ? . node !== undefined }
setOpen = { ( open : boolean ) = > {
// onClose={(highlights: Highlight[]) => {
// if (props.notebookTarget?.node.highlights) {
// props.notebookTarget.node.highlights = highlights
// }
props . setNotebookTarget ( open ? props.notebookTarget : undefined )
} }
/ >
) }
{ showUploadModal && (
< UploadModal onOpenChange = { ( ) = > setShowUploadModal ( false ) } / >
) }
< / >
)
}
type LibraryItemsProps = {
2024-08-07 06:55:19 +00:00
folder : string | undefined
2024-06-06 09:10:55 +00:00
items : LibraryItem [ ]
layout : LayoutType
viewer : UserBasicData | undefined
gridContainerRef : React.RefObject < HTMLDivElement >
setShowEditTitleModal : ( show : boolean ) = > void
setLinkToEdit : ( set : LibraryItem | undefined ) = > void
setShowUnsubscribeConfirmation : ( show : true ) = > void
setLinkToUnsubscribe : ( set : LibraryItem | undefined ) = > void
isChecked : ( itemId : string ) = > boolean
setIsChecked : ( itemId : string , set : boolean ) = > void
multiSelectMode : MultiSelectMode
actionHandler : (
action : LinkedItemCardAction ,
item : LibraryItem | undefined
) = > Promise < void >
}
2024-07-28 12:41:16 +00:00
function LibraryItemsList ( props : LibraryItemsProps ) : JSX . Element {
2024-06-06 09:10:55 +00:00
return (
< Box
ref = { props . gridContainerRef }
css = { {
display : 'grid' ,
width : '100%' ,
gridAutoRows : 'auto' ,
borderRadius : '6px' ,
2024-08-07 07:45:27 +00:00
gridGap : props.layout == 'LIST_LAYOUT' ? '0px' : '20px' ,
2024-06-06 09:10:55 +00:00
marginTop : '10px' ,
marginBottom : '0px' ,
paddingTop : '0' ,
paddingBottom : '0px' ,
overflow : 'visible' ,
px : '70px' ,
2024-06-13 07:25:29 +00:00
'@xlgDown' : {
2024-06-14 04:19:45 +00:00
px : '0px' ,
2024-06-11 08:00:25 +00:00
} ,
2024-06-12 13:55:36 +00:00
'@mdDown' : {
px : '0px' ,
gap : '0px' ,
} ,
2024-06-06 09:10:55 +00:00
gridTemplateColumns :
props . layout == 'LIST_LAYOUT'
? 'none'
2024-06-13 07:25:29 +00:00
: ` repeat( auto-fit, minmax(280px, 1fr) ) ` ,
2024-06-06 09:10:55 +00:00
} }
>
{ props . items . map ( ( linkedItem ) = > (
< Box
className = "linkedItemCard"
data - testid = "linkedItemCard"
id = { linkedItem . node . id }
tabIndex = { 0 }
key = { linkedItem . node . id + linkedItem . node . image }
css = { {
width : '100%' ,
'&:focus-visible' : {
outline : 'none' ,
} ,
'&> div' : {
2024-06-13 07:15:16 +00:00
// bg: '$thLeftMenuBackground',
2024-06-06 09:10:55 +00:00
// bg: '$thLibraryBackground',
} ,
'&:focus' : {
outline : 'none' ,
'> div' : {
outline : 'none' ,
bg : '$thBackgroundActive' ,
} ,
} ,
'&:hover' : {
'> div' : {
bg : '$thBackgroundActive' ,
boxShadow : '$cardBoxShadow' ,
} ,
'> a' : {
bg : '$thBackgroundActive' ,
} ,
} ,
} }
>
{ props . viewer && (
< LinkedItemCard
layout = { props . layout }
item = { linkedItem . node }
isLoading = { linkedItem . isLoading }
viewer = { props . viewer }
isChecked = { props . isChecked ( linkedItem . node . id ) }
setIsChecked = { props . setIsChecked }
multiSelectMode = { props . multiSelectMode }
handleAction = { ( action : LinkedItemCardAction ) = > {
if ( action === 'editTitle' ) {
props . setShowEditTitleModal ( true )
props . setLinkToEdit ( linkedItem )
} else if ( action == 'unsubscribe' ) {
props . setShowUnsubscribeConfirmation ( true )
props . setLinkToUnsubscribe ( linkedItem )
} else {
props . actionHandler ( action , linkedItem )
}
document . body . style . removeProperty ( 'pointer-events' )
} }
/ >
) }
< / Box >
) ) }
< / Box >
)
}