mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #4223 from omnivore-app/fix/web-library-fixes
Web library cleanups
This commit is contained in:
commit
d56f4f9234
124 changed files with 4469 additions and 5802 deletions
|
|
@ -2,16 +2,17 @@ import AutosizeInput_, { AutosizeInputProps } from 'react-input-autosize'
|
|||
import { Box, SpanBox } from './LayoutPrimitives'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Label } from '../../lib/networking/fragments/labelFragment'
|
||||
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { isTouchScreenDevice } from '../../lib/deviceType'
|
||||
import { EditLabelChip } from './EditLabelChip'
|
||||
import { LabelsDispatcher } from '../../lib/hooks/useSetPageLabels'
|
||||
import { EditLabelChipStack } from './EditLabelChipStack'
|
||||
import { useGetLabels } from '../../lib/networking/labels/useLabels'
|
||||
|
||||
// AutosizeInput is a Class component, but the types are broken in React 18.
|
||||
// TODO: Maybe move away from this component, since it hasn't been updated for 3 years.
|
||||
// https://github.com/JedWatson/react-input-autosize/issues
|
||||
const AutosizeInput = AutosizeInput_ as unknown as React.FunctionComponent<AutosizeInputProps>
|
||||
const AutosizeInput =
|
||||
AutosizeInput_ as unknown as React.FunctionComponent<AutosizeInputProps>
|
||||
|
||||
const MaxUnstackedLabels = 7
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ type LabelsPickerProps = {
|
|||
|
||||
export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
||||
const inputRef = useRef<HTMLInputElement | null>()
|
||||
const availableLabels = useGetLabelsQuery()
|
||||
const { data: availableLabels } = useGetLabels()
|
||||
const [isStackExpanded, setIsStackExpanded] = useState(false)
|
||||
const {
|
||||
focused,
|
||||
|
|
@ -80,9 +81,10 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
|||
setTabCount(_tabCount)
|
||||
}
|
||||
|
||||
const matches = availableLabels.labels.filter((l) =>
|
||||
l.name.toLowerCase().startsWith(_tabStartValue)
|
||||
)
|
||||
const matches =
|
||||
availableLabels?.filter((l) =>
|
||||
l.name.toLowerCase().startsWith(_tabStartValue)
|
||||
) ?? []
|
||||
|
||||
if (_tabCount < matches.length) {
|
||||
setInputValue(matches[_tabCount].name)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import React from 'react'
|
|||
|
||||
export function ConfusedSlothIcon(): JSX.Element {
|
||||
const { currentThemeIsDark } = useCurrentTheme()
|
||||
console.log('is dark mdoe: ', currentThemeIsDark)
|
||||
return currentThemeIsDark ? (
|
||||
<ConfusedSlothIconDark />
|
||||
) : (
|
||||
|
|
|
|||
35
packages/web/components/elements/icons/UntrashIcon.tsx
Normal file
35
packages/web/components/elements/icons/UntrashIcon.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* eslint-disable functional/no-class */
|
||||
/* eslint-disable functional/no-this-expression */
|
||||
import { IconProps } from './IconProps'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export class UntrashIcon extends React.Component<IconProps> {
|
||||
render() {
|
||||
const size = (this.props.size || 26).toString()
|
||||
const color = (this.props.color || '#2A2A2A').toString()
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
strokeWidth="1.5"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 3l18 18" />
|
||||
<path d="M4 7h3m4 0h9" />
|
||||
<path d="M10 11l0 6" />
|
||||
<path d="M14 14l0 3" />
|
||||
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l.077 -.923" />
|
||||
<path d="M18.384 14.373l.616 -7.373" />
|
||||
<path d="M9 5v-1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { DEFAULT_HOME_PATH } from '../../../lib/navigations'
|
||||
import { Box } from '../LayoutPrimitives'
|
||||
export type OmnivoreLogoBaseProps = {
|
||||
color?: string
|
||||
href?: string
|
||||
|
|
@ -9,13 +10,8 @@ export type OmnivoreLogoBaseProps = {
|
|||
}
|
||||
|
||||
export function OmnivoreLogoBase(props: OmnivoreLogoBaseProps): JSX.Element {
|
||||
const href = props.href || '/home'
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<Link
|
||||
passHref
|
||||
href={href}
|
||||
<Box
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
|
|
@ -38,6 +34,6 @@ export function OmnivoreLogoBase(props: OmnivoreLogoBaseProps): JSX.Element {
|
|||
aria-label="Omnivore logo"
|
||||
>
|
||||
{props.children}
|
||||
</Link>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { highlightColor } from '../../lib/themeUpdater'
|
|||
|
||||
import { HighlightViewNote } from '../patterns/HighlightNotes'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
import { useDeleteHighlight } from '../../lib/networking/highlights/useItemHighlights'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
|
|
@ -131,8 +132,10 @@ function HighlightCard(props: HighlightCardProps): JSX.Element {
|
|||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] =
|
||||
useState<undefined | string>(undefined)
|
||||
const [labelsTarget, setLabelsTarget] =
|
||||
useState<Highlight | undefined>(undefined)
|
||||
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
|
||||
undefined
|
||||
)
|
||||
const deleteHighlight = useDeleteHighlight()
|
||||
|
||||
const viewInReader = useCallback(
|
||||
(highlightId: string) => {
|
||||
|
|
@ -283,24 +286,22 @@ function HighlightCard(props: HighlightCardProps): JSX.Element {
|
|||
message={'Are you sure you want to delete this highlight?'}
|
||||
onAccept={() => {
|
||||
;(async () => {
|
||||
const highlightId = showConfirmDeleteHighlightId
|
||||
const success = await deleteHighlightMutation(
|
||||
props.highlight.libraryItem?.id || '',
|
||||
showConfirmDeleteHighlightId
|
||||
)
|
||||
props.mutate()
|
||||
if (success) {
|
||||
showSuccessToast('Highlight deleted.', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
const event = new CustomEvent('deleteHighlightbyId', {
|
||||
detail: highlightId,
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
} else {
|
||||
showErrorToast('Error deleting highlight', {
|
||||
position: 'bottom-right',
|
||||
if (props.highlight.libraryItem) {
|
||||
const success = await deleteHighlight.mutateAsync({
|
||||
itemId: props.highlight.libraryItem?.id,
|
||||
slug: props.highlight.libraryItem?.slug,
|
||||
highlightId: showConfirmDeleteHighlightId,
|
||||
})
|
||||
|
||||
if (success) {
|
||||
showSuccessToast('Highlight deleted.', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
} else {
|
||||
showErrorToast('Error deleting highlight', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
}
|
||||
})()
|
||||
setShowConfirmDeleteHighlightId(undefined)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ 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 { useGetRawSearchItemsQuery } from '../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { useGetLibraryItems } from '../../lib/networking/library_items/useLibraryItems'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
|
||||
type HomeState = {
|
||||
|
|
@ -182,8 +182,9 @@ type NavigationContextType = {
|
|||
dispatch: React.Dispatch<Action>
|
||||
}
|
||||
|
||||
const NavigationContext =
|
||||
createContext<NavigationContextType | undefined>(undefined)
|
||||
const NavigationContext = createContext<NavigationContextType | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export const useNavigation = (): NavigationContextType => {
|
||||
const context = useContext(NavigationContext)
|
||||
|
|
@ -210,13 +211,15 @@ export function HomeContainer(): JSX.Element {
|
|||
|
||||
const shouldFallback =
|
||||
homeData.error || (!homeData.isValidating && !hasTopPicks(homeData))
|
||||
const searchData = useGetRawSearchItemsQuery(
|
||||
const searchData = useGetLibraryItems(
|
||||
undefined,
|
||||
{
|
||||
limit: 10,
|
||||
searchQuery: 'in:inbox',
|
||||
includeContent: false,
|
||||
sortDescending: true,
|
||||
},
|
||||
// only enable this search if we didn't get home data
|
||||
shouldFallback
|
||||
)
|
||||
|
||||
|
|
@ -227,27 +230,28 @@ export function HomeContainer(): JSX.Element {
|
|||
}, [viewerData])
|
||||
|
||||
const searchItems = useMemo(() => {
|
||||
return searchData.items.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
|
||||
})
|
||||
return []
|
||||
// return searchData.items.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
|
||||
// })
|
||||
}, [searchData])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -383,7 +387,7 @@ export function HomeContainer(): JSX.Element {
|
|||
)
|
||||
|
||||
const dataReady =
|
||||
!homeData.isValidating && (!shouldFallback || !searchData.isValidating)
|
||||
!homeData.isValidating && (!shouldFallback || !searchData.isLoading)
|
||||
if (!dataReady || (homeData.error && homeData.errorMessage == 'PENDING')) {
|
||||
console.log('showing pending')
|
||||
return (
|
||||
|
|
@ -544,7 +548,7 @@ const JustAddedHomeSection = (props: HomeSectionProps): JSX.Element => {
|
|||
<Button
|
||||
style="link"
|
||||
onClick={(event) => {
|
||||
router.push('/l/library')
|
||||
router.push('/library')
|
||||
event.preventDefault()
|
||||
}}
|
||||
css={{
|
||||
|
|
@ -946,12 +950,12 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
useLibraryItemActions()
|
||||
|
||||
const doArchiveItem = useCallback(
|
||||
async (libraryItemId: string) => {
|
||||
async (libraryItemId: string, slug: string) => {
|
||||
dispatch({
|
||||
type: 'REMOVE_ITEM',
|
||||
payload: libraryItemId,
|
||||
})
|
||||
if (!(await archiveItem(libraryItemId))) {
|
||||
if (!(await archiveItem(libraryItemId, slug))) {
|
||||
// dispatch({
|
||||
// type: 'REPLACE_ITEM',
|
||||
// itemId: libraryItemId,
|
||||
|
|
@ -962,7 +966,7 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
)
|
||||
|
||||
const doDeleteItem = useCallback(
|
||||
async (libraryItemId: string) => {
|
||||
async (libraryItemId: string, slug: string) => {
|
||||
dispatch({
|
||||
type: 'REMOVE_ITEM',
|
||||
payload: libraryItemId,
|
||||
|
|
@ -973,7 +977,7 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
// : libraryItemId,
|
||||
// })
|
||||
}
|
||||
if (!(await deleteItem(libraryItemId, undo))) {
|
||||
if (!(await deleteItem(libraryItemId, slug, undo))) {
|
||||
// dispatch({
|
||||
// type: 'REPLACE_ITEM',
|
||||
// payload: libraryItemId,
|
||||
|
|
@ -984,12 +988,12 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
)
|
||||
|
||||
const doMoveItem = useCallback(
|
||||
async (libraryItemId: string) => {
|
||||
async (libraryItemId: string, slug: string) => {
|
||||
dispatch({
|
||||
type: 'REMOVE_ITEM',
|
||||
payload: libraryItemId,
|
||||
})
|
||||
if (!(await moveItem(libraryItemId))) {
|
||||
if (!(await moveItem(libraryItemId, slug))) {
|
||||
// dispatch({
|
||||
// type: 'REPLACE_ITEM',
|
||||
// payload: libraryItemId,
|
||||
|
|
@ -1039,13 +1043,13 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
;(event.target as HTMLElement).click()
|
||||
break
|
||||
case 'e':
|
||||
doArchiveItem(props.homeItem.id)
|
||||
doArchiveItem(props.homeItem.id, props.homeItem.slug)
|
||||
break
|
||||
case '#':
|
||||
doDeleteItem(props.homeItem.id)
|
||||
doDeleteItem(props.homeItem.id, props.homeItem.slug)
|
||||
break
|
||||
case 'm':
|
||||
doMoveItem(props.homeItem.id)
|
||||
doMoveItem(props.homeItem.id, props.homeItem.slug)
|
||||
break
|
||||
case 'o':
|
||||
window.open(props.homeItem.url, '_blank')
|
||||
|
|
@ -1093,8 +1097,7 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
onClick={async (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
await doMoveItem(props.homeItem.id)
|
||||
await doMoveItem(props.homeItem.id, props.homeItem.slug)
|
||||
}}
|
||||
>
|
||||
<AddToLibraryActionIcon />
|
||||
|
|
@ -1107,8 +1110,7 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
onClick={async (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
await doArchiveItem(props.homeItem.id)
|
||||
await doArchiveItem(props.homeItem.id, props.homeItem.slug)
|
||||
}}
|
||||
>
|
||||
<ArchiveActionIcon />
|
||||
|
|
@ -1121,8 +1123,7 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
onClick={async (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
await doDeleteItem(props.homeItem.id)
|
||||
await doDeleteItem(props.homeItem.id, props.homeItem.slug)
|
||||
}}
|
||||
>
|
||||
<RemoveActionIcon />
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
|
|||
const saveText = useCallback(
|
||||
(text: string) => {
|
||||
;(async () => {
|
||||
console.log('saving text: ', text)
|
||||
const success = await updateHighlightMutation({
|
||||
annotation: text,
|
||||
libraryItemId: props.targetId,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import { Dropdown, DropdownOption } from '../elements/DropdownElements'
|
||||
import { LibraryItemNode } from '../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import {
|
||||
LibraryItemNode,
|
||||
useUpdateItemReadStatus,
|
||||
} from '../../lib/networking/library_items/useLibraryItems'
|
||||
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import { State } from '../../lib/networking/fragments/articleFragment'
|
||||
|
||||
export type CardMenuDropdownAction =
|
||||
| 'mark-read'
|
||||
| 'mark-unread'
|
||||
| 'archive'
|
||||
| 'unarchive'
|
||||
| 'delete'
|
||||
|
|
@ -24,12 +26,15 @@ type CardMenuProps = {
|
|||
}
|
||||
|
||||
export function CardMenu(props: CardMenuProps): JSX.Element {
|
||||
const updateItemReadStatus = useUpdateItemReadStatus()
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
triggerElement={props.triggerElement}
|
||||
onOpenChange={props.onOpenChange}
|
||||
css={{ bg: '$thNavMenuFooter' }}
|
||||
>
|
||||
{!props.item.isArchived ? (
|
||||
{props.item.state != State.ARCHIVED ? (
|
||||
<DropdownOption
|
||||
onSelect={() => props.actionHandler('archive')}
|
||||
title="Archive"
|
||||
|
|
@ -62,15 +67,31 @@ export function CardMenu(props: CardMenuProps): JSX.Element {
|
|||
/>
|
||||
{props.item.readingProgressPercent < 98 ? (
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
props.actionHandler('mark-read')
|
||||
onSelect={async () => {
|
||||
await updateItemReadStatus.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
input: {
|
||||
id: props.item.id,
|
||||
readingProgressPercent: 100,
|
||||
force: true,
|
||||
},
|
||||
})
|
||||
}}
|
||||
title="Mark read"
|
||||
/>
|
||||
) : (
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
props.actionHandler('mark-unread')
|
||||
onSelect={async () => {
|
||||
await updateItemReadStatus.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
input: {
|
||||
id: props.item.id,
|
||||
readingProgressPercent: 0,
|
||||
force: true,
|
||||
},
|
||||
})
|
||||
}}
|
||||
title="Mark unread"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
|
|||
const saveText = useCallback(
|
||||
(text: string, updateTime: Date, interactive: boolean) => {
|
||||
;(async () => {
|
||||
console.log('updating highlight text')
|
||||
const success = await updateHighlightMutation({
|
||||
annotation: text,
|
||||
libraryItemId: props.targetId,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { HighlightViewNote } from './HighlightNotes'
|
|||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { highlightColorVar } from '../../lib/themeUpdater'
|
||||
import { ReadableItem } from '../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../lib/networking/library_items/useLibraryItems'
|
||||
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import {
|
||||
autoUpdate,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { LayoutType } from '../../templates/homeFeed/HomeFeedContainer'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import type { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import type { LibraryItemNode } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { MultiSelectMode } from '../../templates/homeFeed/LibraryHeader'
|
||||
|
||||
export type LinkedItemCardAction =
|
||||
|
|
@ -18,6 +18,7 @@ export type LinkedItemCardAction =
|
|||
| 'update-item'
|
||||
| 'move-to-inbox'
|
||||
| 'refresh'
|
||||
| 'restore'
|
||||
|
||||
export type LinkedItemCardProps = {
|
||||
item: LibraryItemNode
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useMemo } from 'react'
|
||||
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { LibraryItemNode } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { HStack, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { RecommendedFlairIcon } from '../../elements/icons/RecommendedFlairIcon'
|
||||
import { PinnedFlairIcon } from '../../elements/icons/PinnedFlairIcon'
|
||||
|
|
@ -140,9 +140,7 @@ type FlairIconProps = {
|
|||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function FlairIcon(
|
||||
props: FlairIconProps
|
||||
): JSX.Element {
|
||||
export function FlairIcon(props: FlairIconProps): JSX.Element {
|
||||
return (
|
||||
<SpanBox title={props.title} css={{ lineHeight: '1' }}>
|
||||
{props.children}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { CaretDown, CaretUp } from '@phosphor-icons/react'
|
|||
import { MetaStyle, timeAgo, TitleStyle } from './LibraryCardStyles'
|
||||
import { styled } from '@stitches/react'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { LibraryItemNode } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { useState } from 'react'
|
||||
import { Box, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import {
|
||||
LibraryItemNode,
|
||||
useArchiveItem,
|
||||
useDeleteItem,
|
||||
useRestoreItem,
|
||||
} from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { LinkedItemCardAction } from './CardTypes'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
|
|
@ -14,6 +19,8 @@ import { LabelIcon } from '../../elements/icons/LabelIcon'
|
|||
import { UnarchiveIcon } from '../../elements/icons/UnarchiveIcon'
|
||||
import { BrowserIcon } from '../../elements/icons/BrowserIcon'
|
||||
import { MoveToInboxIcon } from '../../elements/icons/MoveToInboxIcon'
|
||||
import { UntrashIcon } from '../../elements/icons/UntrashIcon'
|
||||
import { State } from '../../../lib/networking/fragments/articleFragment'
|
||||
|
||||
type LibraryHoverActionsProps = {
|
||||
viewer: UserBasicData
|
||||
|
|
@ -26,6 +33,9 @@ type LibraryHoverActionsProps = {
|
|||
|
||||
export const LibraryHoverActions = (props: LibraryHoverActionsProps) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const archiveItem = useArchiveItem()
|
||||
const deleteItem = useDeleteItem()
|
||||
const restoreItem = useRestoreItem()
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
|
@ -89,16 +99,26 @@ export const LibraryHoverActions = (props: LibraryHoverActionsProps) => {
|
|||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
title={props.item.isArchived ? 'Unarchive (e)' : 'Archive (e)'}
|
||||
title={
|
||||
props.item.state === State.ARCHIVED
|
||||
? 'Unarchive (e)'
|
||||
: 'Archive (e)'
|
||||
}
|
||||
style="hoverActionIcon"
|
||||
onClick={(event) => {
|
||||
const action = props.item.isArchived ? 'unarchive' : 'archive'
|
||||
props.handleAction(action)
|
||||
onClick={async (event) => {
|
||||
await archiveItem.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
input: {
|
||||
linkId: props.item.id,
|
||||
archived: props.item.state !== State.ARCHIVED,
|
||||
},
|
||||
})
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{props.item.isArchived ? (
|
||||
{props.item.state === State.ARCHIVED ? (
|
||||
<UnarchiveIcon
|
||||
size={21}
|
||||
color={theme.colors.thNotebookSubtle.toString()}
|
||||
|
|
@ -112,15 +132,35 @@ export const LibraryHoverActions = (props: LibraryHoverActionsProps) => {
|
|||
</Button>
|
||||
)}
|
||||
<Button
|
||||
title="Remove (#)"
|
||||
title={props.item.state == State.DELETED ? 'Restore' : 'Remove (#)'}
|
||||
style="hoverActionIcon"
|
||||
onClick={(event) => {
|
||||
props.handleAction('delete')
|
||||
onClick={async (event) => {
|
||||
if (props.item.state == State.DELETED) {
|
||||
await restoreItem.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
})
|
||||
} else {
|
||||
await deleteItem.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
})
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={21} color={theme.colors.thNotebookSubtle.toString()} />
|
||||
{props.item.state == State.DELETED ? (
|
||||
<UntrashIcon
|
||||
size={21}
|
||||
color={theme.colors.thNotebookSubtle.toString()}
|
||||
/>
|
||||
) : (
|
||||
<TrashIcon
|
||||
size={21}
|
||||
color={theme.colors.thNotebookSubtle.toString()}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
title="Edit labels (l)"
|
||||
|
|
|
|||
|
|
@ -85,12 +85,15 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
|
|||
height: '100%',
|
||||
cursor: 'pointer',
|
||||
gap: '10px',
|
||||
borderStyle: 'none',
|
||||
borderBottom: 'none',
|
||||
borderRadius: '6px',
|
||||
borderBottom: props.legacyLayout
|
||||
? 'unset'
|
||||
: '1px solid $thLeftMenuBackground',
|
||||
'@media (max-width: 930px)': {
|
||||
borderRadius: '0px',
|
||||
},
|
||||
'&:hover': {
|
||||
borderBottom: 'unset',
|
||||
},
|
||||
...layoutWidths,
|
||||
}}
|
||||
alignment="start"
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import {
|
|||
DropdownOption,
|
||||
DropdownSeparator,
|
||||
} from '../elements/DropdownElements'
|
||||
import { ArticleAttributes } from '../../lib/networking/library_items/useLibraryItems'
|
||||
import { State } from '../../lib/networking/fragments/articleFragment'
|
||||
|
||||
type DropdownMenuProps = {
|
||||
triggerElement: ReactNode
|
||||
libraryItem?: ArticleAttributes
|
||||
articleActionHandler: (action: string, arg?: unknown) => void
|
||||
}
|
||||
|
||||
|
|
@ -14,8 +17,18 @@ export function ReaderDropdownMenu(props: DropdownMenuProps): JSX.Element {
|
|||
return (
|
||||
<Dropdown triggerElement={props.triggerElement}>
|
||||
<DropdownOption
|
||||
onSelect={() => props.articleActionHandler('archive')}
|
||||
title="Archive (e)"
|
||||
onSelect={async () => {
|
||||
if (props.libraryItem?.state === State.ARCHIVED) {
|
||||
props.articleActionHandler('unarchive')
|
||||
} else {
|
||||
props.articleActionHandler('archive')
|
||||
}
|
||||
}}
|
||||
title={
|
||||
props.libraryItem?.state === State.ARCHIVED
|
||||
? 'Unarchive (e)'
|
||||
: 'Archive (e)'
|
||||
}
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.articleActionHandler('setLabels')}
|
||||
|
|
@ -26,7 +39,9 @@ export function ReaderDropdownMenu(props: DropdownMenuProps): JSX.Element {
|
|||
title="Edit info (i)"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.articleActionHandler('delete')}
|
||||
onSelect={async () => {
|
||||
props.articleActionHandler('delete')
|
||||
}}
|
||||
title="Remove (#)"
|
||||
/>
|
||||
<DropdownSeparator />
|
||||
|
|
|
|||
|
|
@ -11,20 +11,21 @@ import { setupAnalytics } from '../../lib/analytics'
|
|||
import { primaryCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { logout } from '../../lib/logout'
|
||||
import { useApplyLocalTheme } from '../../lib/hooks/useApplyLocalTheme'
|
||||
import { updateTheme } from '../../lib/themeUpdater'
|
||||
import { Priority, useRegisterActions } from 'kbar'
|
||||
import { ThemeId, theme } from '../tokens/stitches.config'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
import { NavigationMenu } from './navMenu/NavigationMenu'
|
||||
import { Button } from '../elements/Button'
|
||||
import { List } from '@phosphor-icons/react'
|
||||
import { LIBRARY_LEFT_MENU_WIDTH } from './navMenu/LibraryLegacyMenu'
|
||||
import { AddLinkModal } from './AddLinkModal'
|
||||
import { saveUrlMutation } from '../../lib/networking/mutations/saveUrlMutation'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {
|
||||
showErrorToast,
|
||||
showSuccessToastWithAction,
|
||||
} from '../../lib/toastHelpers'
|
||||
import useWindowDimensions from '../../lib/hooks/useGetWindowDimensions'
|
||||
import { useAddItem } from '../../lib/networking/library_items/useLibraryItems'
|
||||
import { useHandleAddUrl } from '../../lib/hooks/useHandleAddUrl'
|
||||
|
||||
export type NavigationSection =
|
||||
| 'home'
|
||||
|
|
@ -52,6 +53,7 @@ export function NavigationLayout(props: NavigationLayoutProps): JSX.Element {
|
|||
const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false)
|
||||
const [showKeyboardCommandsModal, setShowKeyboardCommandsModal] =
|
||||
useState(false)
|
||||
const addItem = useAddItem()
|
||||
|
||||
useRegisterActions(navigationCommands(router))
|
||||
|
||||
|
|
@ -84,22 +86,7 @@ export function NavigationLayout(props: NavigationLayoutProps): JSX.Element {
|
|||
|
||||
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
|
||||
|
||||
const handleLinkAdded = useCallback(
|
||||
async (link: string, timezone: string, locale: string) => {
|
||||
const result = await saveUrlMutation(link, timezone, locale)
|
||||
if (result) {
|
||||
showSuccessToastWithAction('Link saved', 'Read now', async () => {
|
||||
window.location.href = `/article?url=${encodeURIComponent(link)}`
|
||||
return Promise.resolve()
|
||||
})
|
||||
// const id = result.url?.match(/[^/]+$/)?.[0] ?? ''
|
||||
// performActionOnItem('refresh', undefined as unknown as any)
|
||||
} else {
|
||||
showErrorToast('Error saving link', { position: 'bottom-right' })
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
const handleLinkAdded = useHandleAddUrl()
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('logout', showLogout)
|
||||
|
|
|
|||
443
packages/web/components/templates/ShortcutsTree.tsx
Normal file
443
packages/web/components/templates/ShortcutsTree.tsx
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { NodeApi, SimpleTree, Tree, TreeApi } from 'react-arborist'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
import {
|
||||
Shortcut,
|
||||
useGetShortcuts,
|
||||
useSetShortcuts,
|
||||
} from '../../lib/networking/shortcuts/useShortcuts'
|
||||
import { usePersistedState } from '../../lib/hooks/usePersistedState'
|
||||
import { CSSProperties, useCallback, useMemo, useState } from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { Box, HStack, SpanBox } from '../elements/LayoutPrimitives'
|
||||
import { Dropdown, DropdownOption } from '../elements/DropdownElements'
|
||||
import { DotsThree, ListMagnifyingGlass, Tag } from '@phosphor-icons/react'
|
||||
import { ShortcutFolderClosed } from '../elements/icons/ShortcutFolderClosed'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
import { ShortcutFolderOpen } from '../elements/icons/ShortcutFolderOpen'
|
||||
import { CoverImage } from '../elements/CoverImage'
|
||||
import { NewsletterIcon } from '../elements/icons/NewsletterIcon'
|
||||
import { FollowingIcon } from '../elements/icons/FollowingIcon'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { OpenMap } from 'react-arborist/dist/module/state/open-slice'
|
||||
|
||||
type ShortcutsTreeProps = {
|
||||
treeRef: React.MutableRefObject<TreeApi<Shortcut> | undefined>
|
||||
}
|
||||
|
||||
export const ShortcutsTree = (props: ShortcutsTreeProps): JSX.Element => {
|
||||
const router = useRouter()
|
||||
const { ref, width, height } = useResizeObserver()
|
||||
const { data, isLoading } = useGetShortcuts()
|
||||
const setShorcuts = useSetShortcuts()
|
||||
|
||||
const [folderOpenState, setFolderOpenState] = usePersistedState<
|
||||
Record<string, boolean>
|
||||
>({
|
||||
key: 'nav-menu-open-state',
|
||||
isSessionStorage: false,
|
||||
initialValue: {},
|
||||
})
|
||||
const tree = useMemo(() => {
|
||||
const result = new SimpleTree<Shortcut>((data ?? []) as Shortcut[])
|
||||
return result
|
||||
}, [data])
|
||||
|
||||
const syncTreeData = async (data: Shortcut[]) => {
|
||||
await setShorcuts.mutateAsync({ shortcuts: data })
|
||||
}
|
||||
|
||||
const onMove = useCallback(
|
||||
async (args: {
|
||||
dragIds: string[]
|
||||
parentId: null | string
|
||||
index: number
|
||||
}) => {
|
||||
for (const id of args.dragIds) {
|
||||
tree?.move({ id, parentId: args.parentId, index: args.index })
|
||||
}
|
||||
await syncTreeData(tree.data)
|
||||
},
|
||||
[tree, data]
|
||||
)
|
||||
|
||||
const onCreate = useCallback(
|
||||
async (args: { parentId: string | null; index: number; type: string }) => {
|
||||
const data = { id: uuidv4(), name: '', type: 'folder' } as any
|
||||
if (args.type === 'internal') {
|
||||
data.children = []
|
||||
}
|
||||
tree.create({ parentId: args.parentId, index: args.index, data })
|
||||
await syncTreeData(tree.data)
|
||||
return data
|
||||
},
|
||||
[tree, data]
|
||||
)
|
||||
|
||||
const onDelete = useCallback(
|
||||
async (args: { ids: string[] }) => {
|
||||
args.ids.forEach((id) => tree.drop({ id }))
|
||||
await syncTreeData(tree.data)
|
||||
},
|
||||
[tree, data]
|
||||
)
|
||||
|
||||
const onRename = useCallback(
|
||||
async (args: { name: string; id: string }) => {
|
||||
tree.update({ id: args.id, changes: { name: args.name } as any })
|
||||
await syncTreeData(tree.data)
|
||||
},
|
||||
[tree, data]
|
||||
)
|
||||
|
||||
const onToggle = useCallback(
|
||||
(id: string) => {
|
||||
if (id && props.treeRef.current) {
|
||||
const isOpen = props.treeRef.current?.isOpen(id)
|
||||
const newItem: OpenMap = {}
|
||||
newItem[id] = isOpen
|
||||
setFolderOpenState({ ...folderOpenState, ...newItem })
|
||||
}
|
||||
},
|
||||
[props, folderOpenState, setFolderOpenState]
|
||||
)
|
||||
|
||||
const onActivate = useCallback(
|
||||
(node: NodeApi<Shortcut>) => {
|
||||
if (node.data.type == 'folder') {
|
||||
const join = node.data.join
|
||||
if (join == 'or') {
|
||||
const query = node.children
|
||||
?.map((child) => {
|
||||
return `(${child.data.filter})`
|
||||
})
|
||||
.join(' OR ')
|
||||
}
|
||||
} else if (node.data.section != null && node.data.filter != null) {
|
||||
router.push(`/${node.data.section}?q=${node.data.filter}`)
|
||||
}
|
||||
},
|
||||
[tree, router]
|
||||
)
|
||||
|
||||
function countTotalShortcuts(shortcuts: Shortcut[]): number {
|
||||
let total = 0
|
||||
|
||||
for (const shortcut of shortcuts) {
|
||||
// Count the current shortcut
|
||||
total++
|
||||
|
||||
// If the shortcut has children, recursively count them
|
||||
if (shortcut.children && shortcut.children.length > 0) {
|
||||
total += countTotalShortcuts(shortcut.children)
|
||||
}
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
const maximumHeight = useMemo(() => {
|
||||
if (!data) {
|
||||
return 320
|
||||
}
|
||||
return countTotalShortcuts(data as Shortcut[]) * 36
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
css={{
|
||||
height: maximumHeight,
|
||||
flexGrow: 1,
|
||||
minBlockSize: 0,
|
||||
}}
|
||||
>
|
||||
{!isLoading && (
|
||||
<Tree
|
||||
ref={props.treeRef}
|
||||
data={data as Shortcut[]}
|
||||
onCreate={onCreate}
|
||||
onMove={onMove}
|
||||
onDelete={onDelete}
|
||||
onRename={onRename}
|
||||
onToggle={onToggle}
|
||||
onActivate={onActivate}
|
||||
rowHeight={36}
|
||||
initialOpenState={folderOpenState}
|
||||
width={width}
|
||||
height={maximumHeight}
|
||||
>
|
||||
{NodeRenderer}
|
||||
</Tree>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function NodeRenderer(args: {
|
||||
style: CSSProperties
|
||||
node: NodeApi<Shortcut>
|
||||
tree: TreeApi<Shortcut>
|
||||
dragHandle?: (el: HTMLDivElement | null) => void
|
||||
preview?: boolean
|
||||
}) {
|
||||
const isSelected = false
|
||||
const [menuVisible, setMenuVisible] = useState(false)
|
||||
const [menuOpened, setMenuOpened] = useState(false)
|
||||
|
||||
return (
|
||||
<HStack
|
||||
ref={args.dragHandle}
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
pl: `${20 + args.node.level * 15}px`,
|
||||
mb: '2px',
|
||||
gap: '10px',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
height: '34px',
|
||||
|
||||
backgroundColor: isSelected ? '$thLibrarySelectionColor' : 'unset',
|
||||
fontSize: '15px',
|
||||
fontWeight: 'regular',
|
||||
fontFamily: '$display',
|
||||
color: isSelected
|
||||
? '$thLibraryMenuSecondary'
|
||||
: '$thLibraryMenuUnselected',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
'&:hover': {
|
||||
backgroundColor: isSelected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
'&:active': {
|
||||
outline: 'unset',
|
||||
backgroundColor: isSelected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
'&:hover [role="hover-menu"]': {
|
||||
opacity: '1',
|
||||
},
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setMenuVisible(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setMenuVisible(false)
|
||||
}}
|
||||
title={args.node.data.name}
|
||||
onClick={(e) => {
|
||||
// router.push(`/` + props.section)
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
>
|
||||
<NodeItemContents node={args.node} />
|
||||
<SpanBox
|
||||
role="hover-menu"
|
||||
css={{
|
||||
display: 'flex',
|
||||
ml: 'auto',
|
||||
mr: '15px',
|
||||
opacity: menuVisible || menuOpened ? '1' : '0',
|
||||
}}
|
||||
>
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
triggerElement={<DotsThree size={20} />}
|
||||
css={{ ml: 'auto' }}
|
||||
onOpenChange={(open) => {
|
||||
setMenuOpened(open)
|
||||
}}
|
||||
>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
args.tree.delete(args.node)
|
||||
}}
|
||||
title="Remove"
|
||||
/>
|
||||
{/* {args.node.data.type == 'folder' && (
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
args.node.data.join = 'or'
|
||||
}}
|
||||
title="Folder query: OR"
|
||||
/>
|
||||
)} */}
|
||||
</Dropdown>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type NodeItemContentsProps = {
|
||||
node: NodeApi<Shortcut>
|
||||
}
|
||||
|
||||
const NodeItemContents = (props: NodeItemContentsProps): JSX.Element => {
|
||||
if (props.node.isEditing) {
|
||||
return (
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
defaultValue={props.node.data.name}
|
||||
onFocus={(e) => e.currentTarget.select()}
|
||||
onBlur={() => props.node.reset()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
props.node.reset()
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
// props.node.data = {
|
||||
// id: 'new-folder',
|
||||
// type: 'folder',
|
||||
// name: e.currentTarget.value,
|
||||
// }
|
||||
props.node.submit(e.currentTarget.value)
|
||||
props.node.activate()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (props.node.isLeaf) {
|
||||
const shortcut = props.node.data
|
||||
if (shortcut) {
|
||||
switch (shortcut.type) {
|
||||
case 'feed':
|
||||
case 'newsletter':
|
||||
return (
|
||||
<SpanBox>
|
||||
<FeedOrNewsletterShortcut shortcut={shortcut} />
|
||||
</SpanBox>
|
||||
)
|
||||
case 'label':
|
||||
return (
|
||||
<Box>
|
||||
<LabelShortcut shortcut={shortcut} />
|
||||
</Box>
|
||||
)
|
||||
case 'search':
|
||||
return (
|
||||
<Box>
|
||||
<SearchShortcut shortcut={shortcut} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ gap: '10px', width: '100%' }}
|
||||
onClick={(event) => {
|
||||
props.node.toggle()
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{props.node.isClosed ? (
|
||||
<ShortcutFolderClosed
|
||||
color={theme.colors.thLibraryMenuPrimary.toString()}
|
||||
/>
|
||||
) : (
|
||||
<ShortcutFolderOpen
|
||||
color={theme.colors.thLibraryMenuPrimary.toString()}
|
||||
/>
|
||||
)}
|
||||
{props.node.data.name}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
|
||||
type ShortcutItemProps = {
|
||||
shortcut: Shortcut
|
||||
}
|
||||
|
||||
const FeedOrNewsletterShortcut = (props: ShortcutItemProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ pl: '10px', width: '100%', gap: '10px' }}
|
||||
key={`search-${props.shortcut.id}`}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ minWidth: '20px' }}
|
||||
>
|
||||
{props.shortcut.icon ? (
|
||||
<CoverImage
|
||||
src={props.shortcut.icon}
|
||||
width={20}
|
||||
height={20}
|
||||
css={{ borderRadius: '20px' }}
|
||||
/>
|
||||
) : props.shortcut.type == 'newsletter' ? (
|
||||
<NewsletterIcon color="#F59932" size={18} />
|
||||
) : (
|
||||
<FollowingIcon color="#F59932" size={21} />
|
||||
)}
|
||||
</HStack>
|
||||
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const SearchShortcut = (props: ShortcutItemProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ pl: '10px', width: '100%', gap: '7px' }}
|
||||
key={`search-${props.shortcut.id}`}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ minWidth: '20px' }}
|
||||
>
|
||||
<ListMagnifyingGlass size={17} />
|
||||
</HStack>
|
||||
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const LabelShortcut = (props: ShortcutItemProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ width: '100%', gap: '7px' }}
|
||||
key={`search-${props.shortcut.id}`}
|
||||
>
|
||||
<Tag
|
||||
size={15}
|
||||
color={props.shortcut.label?.color ?? 'gray'}
|
||||
weight="fill"
|
||||
/>
|
||||
<StyledText style="settingsItem" css={{ pb: '1px' }}>
|
||||
{props.shortcut.name}
|
||||
</StyledText>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,13 +8,15 @@ import {
|
|||
ModalTitleBar,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { SetLabelsControl } from './SetLabelsControl'
|
||||
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
import { LabelAction } from '../../../lib/hooks/useSetPageLabels'
|
||||
import { Button } from '../../elements/Button'
|
||||
import {
|
||||
useCreateLabel,
|
||||
useGetLabels,
|
||||
} from '../../../lib/networking/labels/useLabels'
|
||||
|
||||
type AddBulkLabelsModalProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
|
|
@ -24,12 +26,14 @@ type AddBulkLabelsModalProps = {
|
|||
export function AddBulkLabelsModal(
|
||||
props: AddBulkLabelsModalProps
|
||||
): JSX.Element {
|
||||
const availableLabels = useGetLabelsQuery()
|
||||
const { data: availableLabels } = useGetLabels()
|
||||
const createLabel = useCreateLabel()
|
||||
const [tabCount, setTabCount] = useState(-1)
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [tabStartValue, setTabStartValue] = useState('')
|
||||
const [errorMessage, setErrorMessage] =
|
||||
useState<string | undefined>(undefined)
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const errorTimeoutRef = useRef<NodeJS.Timeout | undefined>()
|
||||
const [highlightLastLabel, setHighlightLastLabel] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
|
@ -97,10 +101,11 @@ export function AddBulkLabelsModal(
|
|||
(newLabels: Label[], tempLabel: Label) => {
|
||||
;(async () => {
|
||||
const currentLabels = newLabels
|
||||
const newLabel = await createLabelMutation(
|
||||
tempLabel.name,
|
||||
tempLabel.color
|
||||
)
|
||||
const newLabel = await createLabel.mutateAsync({
|
||||
name: tempLabel.name,
|
||||
color: tempLabel.color,
|
||||
description: undefined,
|
||||
})
|
||||
const idx = currentLabels.findIndex((l) => l.id === tempLabel.id)
|
||||
if (newLabel) {
|
||||
showSuccessToast(`Created label ${newLabel.name}`, {
|
||||
|
|
@ -132,7 +137,7 @@ export function AddBulkLabelsModal(
|
|||
const trimmedValue = value.trim()
|
||||
const current = selectedLabels.labels ?? []
|
||||
const lowerCasedValue = trimmedValue.toLowerCase()
|
||||
const existing = availableLabels.labels.find(
|
||||
const existing = availableLabels?.find(
|
||||
(l) => l.name.toLowerCase() == lowerCasedValue
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Separator } from '@radix-ui/react-separator'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { ArticleAttributes } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { Box, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
|
|
@ -11,6 +11,7 @@ import { TrashIcon } from '../../elements/icons/TrashIcon'
|
|||
import { LabelIcon } from '../../elements/icons/LabelIcon'
|
||||
import { EditInfoIcon } from '../../elements/icons/EditInfoIcon'
|
||||
import { UnarchiveIcon } from '../../elements/icons/UnarchiveIcon'
|
||||
import { State } from '../../../lib/networking/fragments/articleFragment'
|
||||
|
||||
export type ArticleActionsMenuLayout = 'top' | 'side'
|
||||
|
||||
|
|
@ -163,7 +164,7 @@ export function ArticleActionsMenu(
|
|||
<TrashIcon size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</Button>
|
||||
|
||||
{!props.article?.isArchived ? (
|
||||
{props.article?.state !== State.ARCHIVED ? (
|
||||
<Button
|
||||
title="Archive (e)"
|
||||
style="articleActionIcon"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
import {
|
||||
ArticleAttributes,
|
||||
TextDirection,
|
||||
} from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { Article } from './../../../components/templates/article/Article'
|
||||
import { Box, HStack, SpanBox, VStack } from './../../elements/LayoutPrimitives'
|
||||
import { StyledText } from './../../elements/StyledText'
|
||||
|
|
@ -19,11 +15,14 @@ import { updateTheme, updateThemeLocally } from '../../../lib/themeUpdater'
|
|||
import { ArticleMutations } from '../../../lib/articleActions'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { Recommendation } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import {
|
||||
ArticleAttributes,
|
||||
Recommendation,
|
||||
TextDirection,
|
||||
useUpdateItemReadStatus,
|
||||
} from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Avatar } from '../../elements/Avatar'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { AISummary } from './AISummary'
|
||||
import { userHasFeature } from '../../../lib/featureFlag'
|
||||
|
||||
type ArticleContainerProps = {
|
||||
viewer: UserBasicData
|
||||
|
|
@ -117,23 +116,28 @@ const RecommendationComments = (
|
|||
|
||||
export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
const [labels, setLabels] = useState(props.labels)
|
||||
const [title, setTitle] = useState(props.article.title)
|
||||
const [title, setTitle] = useState<string | undefined>(undefined)
|
||||
const [showReportIssuesModal, setShowReportIssuesModal] = useState(false)
|
||||
const [fontSize, setFontSize] = useState(props.fontSize ?? 20)
|
||||
const [highlightOnRelease, setHighlightOnRelease] = useState(
|
||||
props.highlightOnRelease
|
||||
)
|
||||
// iOS app embed can overide the original margin and line height
|
||||
const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] =
|
||||
useState<number | null>(null)
|
||||
const [lineHeightOverride, setLineHeightOverride] =
|
||||
useState<number | null>(null)
|
||||
const [fontFamilyOverride, setFontFamilyOverride] =
|
||||
useState<string | null>(null)
|
||||
const [highContrastTextOverride, setHighContrastTextOverride] =
|
||||
useState<boolean | undefined>(undefined)
|
||||
const [justifyTextOverride, setJustifyTextOverride] =
|
||||
useState<boolean | undefined>(undefined)
|
||||
const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] = useState<
|
||||
number | null
|
||||
>(null)
|
||||
const [lineHeightOverride, setLineHeightOverride] = useState<number | null>(
|
||||
null
|
||||
)
|
||||
const [fontFamilyOverride, setFontFamilyOverride] = useState<string | null>(
|
||||
null
|
||||
)
|
||||
const [highContrastTextOverride, setHighContrastTextOverride] = useState<
|
||||
boolean | undefined
|
||||
>(undefined)
|
||||
const [justifyTextOverride, setJustifyTextOverride] = useState<
|
||||
boolean | undefined
|
||||
>(undefined)
|
||||
const highlightHref = useRef(
|
||||
window.location.hash ? window.location.hash.split('#')[1] : null
|
||||
)
|
||||
|
|
@ -444,9 +448,9 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
'-webkit-line-clamp': '6',
|
||||
},
|
||||
}}
|
||||
title={title}
|
||||
title={title ?? props.article.title}
|
||||
>
|
||||
{title}
|
||||
{title ?? props.article.title}
|
||||
</StyledText>
|
||||
<ArticleSubtitle
|
||||
author={props.article.author}
|
||||
|
|
@ -520,9 +524,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
item={props.article}
|
||||
scrollToHighlight={highlightHref}
|
||||
highlights={props.article.highlights}
|
||||
articleTitle={title}
|
||||
articleAuthor={props.article.author ?? ''}
|
||||
articleId={props.article.id}
|
||||
isAppleAppEmbed={props.isAppleAppEmbed}
|
||||
highlightBarDisabled={props.highlightBarDisabled}
|
||||
showHighlightsModal={props.showHighlightsModal}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { ArticleAttributes } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Box, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
|
@ -11,10 +11,6 @@ import {
|
|||
import PSPDFKit from 'pspdfkit'
|
||||
import { Instance, HighlightAnnotation, List, Annotation, Rect } from 'pspdfkit'
|
||||
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation'
|
||||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation'
|
||||
import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation'
|
||||
import { useCanShareNative } from '../../../lib/hooks/useCanShareNative'
|
||||
import { pspdfKitKey } from '../../../lib/appConfig'
|
||||
import { NotebookModal } from './NotebookModal'
|
||||
|
|
@ -42,13 +38,15 @@ type EpubPatch = {
|
|||
export default function EpubContainer(props: EpubContainerProps): JSX.Element {
|
||||
const epubRef = useRef<HTMLDivElement | null>(null)
|
||||
const renditionRef = useRef<Rendition | undefined>(undefined)
|
||||
const [shareTarget, setShareTarget] =
|
||||
useState<Highlight | undefined>(undefined)
|
||||
const [shareTarget, setShareTarget] = useState<Highlight | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [touchStart, setTouchStart] = useState(0)
|
||||
const [notebookKey, setNotebookKey] = useState<string>(uuidv4())
|
||||
const [noteTarget, setNoteTarget] = useState<Highlight | undefined>(undefined)
|
||||
const [noteTargetPageIndex, setNoteTargetPageIndex] =
|
||||
useState<number | undefined>(undefined)
|
||||
const [noteTargetPageIndex, setNoteTargetPageIndex] = useState<
|
||||
number | undefined
|
||||
>(undefined)
|
||||
const highlightsRef = useRef<Highlight[]>([])
|
||||
|
||||
const book = useMemo(() => {
|
||||
|
|
@ -309,56 +307,6 @@ export default function EpubContainer(props: EpubContainerProps): JSX.Element {
|
|||
{/* EPUB CONTAINER
|
||||
<div ></div> */}
|
||||
</Box>
|
||||
{noteTarget && (
|
||||
<HighlightNoteModal
|
||||
highlight={noteTarget}
|
||||
libraryItemId={props.article.id}
|
||||
author={props.article.author ?? ''}
|
||||
title={props.article.title}
|
||||
onUpdate={(highlight: Highlight) => {
|
||||
const savedHighlight = highlightsRef.current.find(
|
||||
(other: Highlight) => {
|
||||
return other.id == highlight.id
|
||||
}
|
||||
)
|
||||
|
||||
if (savedHighlight) {
|
||||
savedHighlight.annotation = highlight.annotation
|
||||
}
|
||||
}}
|
||||
onOpenChange={() => {
|
||||
setNoteTarget(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{props.showHighlightsModal && (
|
||||
<NotebookModal
|
||||
key={notebookKey}
|
||||
viewer={props.viewer}
|
||||
item={props.article}
|
||||
onClose={(updatedHighlights, deletedAnnotations) => {
|
||||
console.log(
|
||||
'closed PDF notebook: ',
|
||||
updatedHighlights,
|
||||
deletedAnnotations
|
||||
)
|
||||
deletedAnnotations.forEach((highlight) => {
|
||||
const event = new CustomEvent('deleteHighlightbyId', {
|
||||
detail: highlight.id,
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
})
|
||||
props.setShowHighlightsModal(false)
|
||||
}}
|
||||
viewHighlightInReader={(highlightId) => {
|
||||
const event = new CustomEvent('scrollToHighlightId', {
|
||||
detail: highlightId,
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
props.setShowHighlightsModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,13 @@ import { VStack } from '../../elements/LayoutPrimitives'
|
|||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { StyledTextArea } from '../../elements/StyledTextArea'
|
||||
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
|
||||
import { showErrorToast } from '../../../lib/toastHelpers'
|
||||
import { useUpdateHighlight } from '../../../lib/networking/highlights/useItemHighlights'
|
||||
|
||||
type HighlightNoteModalProps = {
|
||||
author: string
|
||||
title: string
|
||||
highlight?: Highlight
|
||||
libraryItemId: string
|
||||
libraryItemSlug: string
|
||||
onUpdate: (updatedHighlight: Highlight) => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
createHighlightForNote?: (note?: string) => Promise<Highlight | undefined>
|
||||
|
|
@ -25,6 +24,7 @@ type HighlightNoteModalProps = {
|
|||
export function HighlightNoteModal(
|
||||
props: HighlightNoteModalProps
|
||||
): JSX.Element {
|
||||
const updateHighlight = useUpdateHighlight()
|
||||
const [noteContent, setNoteContent] = useState(
|
||||
props.highlight?.annotation ?? ''
|
||||
)
|
||||
|
|
@ -38,20 +38,25 @@ export function HighlightNoteModal(
|
|||
|
||||
const saveNoteChanges = useCallback(async () => {
|
||||
if (noteContent != props.highlight?.annotation && props.highlight?.id) {
|
||||
const result = await updateHighlightMutation({
|
||||
libraryItemId: props.libraryItemId,
|
||||
highlightId: props.highlight?.id,
|
||||
annotation: noteContent,
|
||||
color: props.highlight?.color,
|
||||
})
|
||||
|
||||
if (result) {
|
||||
console.log('updating highlight textsdsdfsd')
|
||||
try {
|
||||
const result = await updateHighlight.mutateAsync({
|
||||
itemId: props.libraryItemId,
|
||||
slug: props.libraryItemSlug,
|
||||
input: {
|
||||
libraryItemId: props.libraryItemId,
|
||||
highlightId: props.highlight?.id,
|
||||
annotation: noteContent,
|
||||
color: props.highlight?.color,
|
||||
},
|
||||
})
|
||||
props.onUpdate({ ...props.highlight, annotation: noteContent })
|
||||
props.onOpenChange(false)
|
||||
} else {
|
||||
return result?.id
|
||||
} catch (err) {
|
||||
showErrorToast('Error updating your note', { position: 'bottom-right' })
|
||||
return undefined
|
||||
}
|
||||
document.dispatchEvent(new Event('highlightsUpdated'))
|
||||
}
|
||||
if (!props.highlight && props.createHighlightForNote) {
|
||||
const result = await props.createHighlightForNote(noteContent)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { HighlightView } from '../../patterns/HighlightView'
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
|||
import { ArticleMutations } from '../../../lib/articleActions'
|
||||
import { isTouchScreenDevice } from '../../../lib/deviceType'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter'
|
||||
import 'react-sliding-pane/dist/react-sliding-pane.css'
|
||||
import { NotebookContent } from './Notebook'
|
||||
|
|
@ -39,9 +39,6 @@ type HighlightsLayerProps = {
|
|||
item: ReadableItem
|
||||
highlights: Highlight[]
|
||||
|
||||
articleId: string
|
||||
articleTitle: string
|
||||
articleAuthor: string
|
||||
isAppleAppEmbed: boolean
|
||||
highlightBarDisabled: boolean
|
||||
showHighlightsModal: boolean
|
||||
|
|
@ -105,7 +102,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
const result = await createHighlight(
|
||||
{
|
||||
selection: selection,
|
||||
articleId: props.articleId,
|
||||
articleId: props.item.id,
|
||||
existingHighlights: highlights,
|
||||
color: options?.color,
|
||||
highlightStartEndOffsets: highlightLocations,
|
||||
|
|
@ -141,7 +138,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
[
|
||||
highlightLocations,
|
||||
highlights,
|
||||
props.articleId,
|
||||
props.item.id,
|
||||
props.articleMutations,
|
||||
setSelectionData,
|
||||
]
|
||||
|
|
@ -189,7 +186,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
|
||||
const didDeleteHighlight =
|
||||
await props.articleMutations.deleteHighlightMutation(
|
||||
props.articleId,
|
||||
props.item.id,
|
||||
highlightId
|
||||
)
|
||||
|
||||
|
|
@ -226,7 +223,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
updateHighlightsCallback(highlight)
|
||||
;(async () => {
|
||||
const update = await props.articleMutations.updateHighlightMutation({
|
||||
libraryItemId: props.articleId,
|
||||
libraryItemId: props.item.id,
|
||||
highlightId: highlight.id,
|
||||
color: color,
|
||||
})
|
||||
|
|
@ -718,7 +715,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
const annotation = event.annotation ?? ''
|
||||
|
||||
const result = await props.articleMutations.updateHighlightMutation({
|
||||
libraryItemId: props.articleId,
|
||||
libraryItemId: props.item.id,
|
||||
highlightId: focusedHighlight.id,
|
||||
annotation: event.annotation ?? '',
|
||||
})
|
||||
|
|
@ -800,9 +797,8 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
{highlightModalAction?.highlightModalAction == 'addComment' && (
|
||||
<HighlightNoteModal
|
||||
highlight={highlightModalAction.highlight}
|
||||
author={props.articleAuthor}
|
||||
title={props.articleTitle}
|
||||
libraryItemId={props.articleId}
|
||||
libraryItemId={props.item.id}
|
||||
libraryItemSlug={props.item.slug}
|
||||
onUpdate={updateHighlightsCallback}
|
||||
onOpenChange={() =>
|
||||
setHighlightModalAction({ highlightModalAction: 'none' })
|
||||
|
|
|
|||
|
|
@ -5,21 +5,24 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import 'react-markdown-editor-lite/lib/index.css'
|
||||
import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { HighlightViewItem } from './HighlightViewItem'
|
||||
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
|
||||
import { TrashIcon } from '../../elements/icons/TrashIcon'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter'
|
||||
import { ArticleNotes } from '../../patterns/ArticleNotes'
|
||||
import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { formattedShortTime } from '../../../lib/dateFormatting'
|
||||
import { isDarkTheme } from '../../../lib/themeUpdater'
|
||||
import { sortHighlights } from '../../../lib/highlights/sortHighlights'
|
||||
import { useGetLibraryItemContent } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import {
|
||||
useCreateHighlight,
|
||||
useDeleteHighlight,
|
||||
useUpdateHighlight,
|
||||
} from '../../../lib/networking/highlights/useItemHighlights'
|
||||
|
||||
type NotebookContentProps = {
|
||||
viewer: UserBasicData
|
||||
|
|
@ -42,12 +45,14 @@ type NoteState = {
|
|||
|
||||
export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
||||
const isDark = isDarkTheme()
|
||||
const createHighlight = useCreateHighlight()
|
||||
const deleteHighlight = useDeleteHighlight()
|
||||
const updateHighlight = useUpdateHighlight()
|
||||
|
||||
const { articleData, mutate } = useGetArticleQuery({
|
||||
slug: props.item.slug,
|
||||
username: props.viewer.profile.username,
|
||||
includeFriendsHighlights: false,
|
||||
})
|
||||
const { data: article } = useGetLibraryItemContent(
|
||||
props.viewer.profile.username as string,
|
||||
props.item.slug as string
|
||||
)
|
||||
const [noteText, setNoteText] = useState<string>('')
|
||||
const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] =
|
||||
useState<undefined | string>(undefined)
|
||||
|
|
@ -88,12 +93,16 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
noteState.current.createStarted = new Date()
|
||||
;(async () => {
|
||||
try {
|
||||
const success = await createHighlightMutation({
|
||||
id: newNoteId,
|
||||
shortId: nanoid(8),
|
||||
type: 'NOTE',
|
||||
articleId: props.item.id,
|
||||
annotation: text,
|
||||
const success = await createHighlight.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
input: {
|
||||
id: newNoteId,
|
||||
shortId: nanoid(8),
|
||||
type: 'NOTE',
|
||||
articleId: props.item.id,
|
||||
annotation: text,
|
||||
},
|
||||
})
|
||||
if (success) {
|
||||
noteState.current.note = success
|
||||
|
|
@ -112,7 +121,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
)
|
||||
|
||||
const highlights = useMemo(() => {
|
||||
const result = articleData?.article.article.highlights
|
||||
const result = article?.highlights
|
||||
const note = result?.find((h) => h.type === 'NOTE')
|
||||
if (note) {
|
||||
noteState.current.note = note
|
||||
|
|
@ -122,7 +131,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
setNoteText('')
|
||||
}
|
||||
return result
|
||||
}, [articleData])
|
||||
}, [article])
|
||||
|
||||
useEffect(() => {
|
||||
if (highlights && props.onAnnotationsChanged) {
|
||||
|
|
@ -165,7 +174,11 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
highlights
|
||||
?.filter((h) => h.type === 'NOTE')
|
||||
.forEach(async (h) => {
|
||||
const result = await deleteHighlightMutation(props.item.id, h.id)
|
||||
const result = await deleteHighlight.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
highlightId: h.id,
|
||||
})
|
||||
if (!result) {
|
||||
showErrorToast('Error deleting note')
|
||||
}
|
||||
|
|
@ -179,16 +192,6 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
|
||||
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
const highlightsUpdated = () => {
|
||||
mutate()
|
||||
}
|
||||
document.addEventListener('highlightsUpdated', highlightsUpdated)
|
||||
return () => {
|
||||
document.removeEventListener('highlightsUpdated', highlightsUpdated)
|
||||
}
|
||||
}, [mutate])
|
||||
|
||||
return (
|
||||
<VStack
|
||||
tabIndex={-1}
|
||||
|
|
@ -257,7 +260,8 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
setSetLabelsTarget={setLabelsTarget}
|
||||
setShowConfirmDeleteHighlightId={setShowConfirmDeleteHighlightId}
|
||||
updateHighlight={() => {
|
||||
mutate()
|
||||
// nothing should be needed here anymore with new caching
|
||||
console.log('update highlight')
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -294,11 +298,11 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
onAccept={() => {
|
||||
;(async () => {
|
||||
const highlightId = showConfirmDeleteHighlightId
|
||||
const success = await deleteHighlightMutation(
|
||||
props.item.id,
|
||||
showConfirmDeleteHighlightId
|
||||
)
|
||||
mutate()
|
||||
const success = await deleteHighlight.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
highlightId: showConfirmDeleteHighlightId,
|
||||
})
|
||||
if (success) {
|
||||
showSuccessToast('Highlight deleted.', {
|
||||
position: 'bottom-right',
|
||||
|
|
@ -333,7 +337,6 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
console.log('update highlight: ', highlight)
|
||||
}}
|
||||
onOpenChange={() => {
|
||||
mutate()
|
||||
setLabelsTarget(undefined)
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Button } from '../../elements/Button'
|
|||
import { ExportIcon } from '../../elements/icons/ExportIcon'
|
||||
import { useCallback } from 'react'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { highlightsAsMarkdown } from '../homeFeed/HighlightItem'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { highlightsAsMarkdown } from '../homeFeed/HighlightItem'
|
|||
import 'react-markdown-editor-lite/lib/index.css'
|
||||
import { NotebookContent } from './Notebook'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
|
||||
type NotebookModalProps = {
|
||||
viewer: UserBasicData
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import 'react-sliding-pane/dist/react-sliding-pane.css'
|
||||
import { NotebookContent } from './Notebook'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import {
|
||||
ArticleAttributes,
|
||||
useUpdateItemReadStatus,
|
||||
} from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
|
@ -7,10 +10,6 @@ import { isDarkTheme } from '../../../lib/themeUpdater'
|
|||
import PSPDFKit from 'pspdfkit'
|
||||
import { Instance, HighlightAnnotation, List, Annotation, Rect } from 'pspdfkit'
|
||||
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation'
|
||||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation'
|
||||
import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation'
|
||||
import { pspdfKitKey } from '../../../lib/appConfig'
|
||||
import { HighlightNoteModal } from './HighlightNoteModal'
|
||||
import { showErrorToast } from '../../../lib/toastHelpers'
|
||||
|
|
@ -22,6 +21,12 @@ import { NotebookHeader } from './NotebookHeader'
|
|||
import useWindowDimensions from '../../../lib/hooks/useGetWindowDimensions'
|
||||
import { ResizableSidebar } from './ResizableSidebar'
|
||||
import { DEFAULT_HOME_PATH } from '../../../lib/navigations'
|
||||
import {
|
||||
useCreateHighlight,
|
||||
useDeleteHighlight,
|
||||
useMergeHighlight,
|
||||
useUpdateHighlight,
|
||||
} from '../../../lib/networking/highlights/useItemHighlights'
|
||||
|
||||
export type PdfArticleContainerProps = {
|
||||
viewer: UserBasicData
|
||||
|
|
@ -36,9 +41,15 @@ export default function PdfArticleContainer(
|
|||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const [notebookKey, setNotebookKey] = useState<string>(uuidv4())
|
||||
const [noteTarget, setNoteTarget] = useState<Highlight | undefined>(undefined)
|
||||
const [noteTargetPageIndex, setNoteTargetPageIndex] =
|
||||
useState<number | undefined>(undefined)
|
||||
const [noteTargetPageIndex, setNoteTargetPageIndex] = useState<
|
||||
number | undefined
|
||||
>(undefined)
|
||||
const highlightsRef = useRef<Highlight[]>([])
|
||||
const createHighlight = useCreateHighlight()
|
||||
const deleteHighlight = useDeleteHighlight()
|
||||
const mergeHighlight = useMergeHighlight()
|
||||
const updateHighlight = useUpdateHighlight()
|
||||
const updateItemReadStatus = useUpdateItemReadStatus()
|
||||
|
||||
const annotationOmnivoreId = (annotation: Annotation): string | undefined => {
|
||||
if (
|
||||
|
|
@ -113,7 +124,11 @@ export default function PdfArticleContainer(
|
|||
.delete(annotation)
|
||||
.then(() => {
|
||||
if (annotationId) {
|
||||
return deleteHighlightMutation(props.article.id, annotationId)
|
||||
return deleteHighlight.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
highlightId: annotationId,
|
||||
})
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
|
|
@ -214,8 +229,6 @@ export default function PdfArticleContainer(
|
|||
}),
|
||||
}
|
||||
|
||||
console.log('instnace config: ', config)
|
||||
|
||||
instance = await PSPDFKit.load(config)
|
||||
console.log('created PDF instance', instance)
|
||||
|
||||
|
|
@ -229,7 +242,11 @@ export default function PdfArticleContainer(
|
|||
}
|
||||
const annotationId = annotationOmnivoreId(annotation)
|
||||
if (annotationId) {
|
||||
await deleteHighlightMutation(props.article.id, annotationId)
|
||||
await deleteHighlight.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
highlightId: annotationId,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -339,16 +356,21 @@ export default function PdfArticleContainer(
|
|||
|
||||
if (overlapping.size === 0) {
|
||||
const positionPercent = positionPercentForAnnotation(annotation)
|
||||
const result = await createHighlightMutation({
|
||||
id: id,
|
||||
shortId: shortId,
|
||||
quote: quote,
|
||||
articleId: props.article.id,
|
||||
prefix: surroundingText.prefix,
|
||||
suffix: surroundingText.suffix,
|
||||
patch: JSON.stringify(serialized),
|
||||
highlightPositionPercent: positionPercent * 100,
|
||||
highlightPositionAnchorIndex: annotation.pageIndex,
|
||||
|
||||
const result = await createHighlight.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
input: {
|
||||
id: id,
|
||||
shortId: shortId,
|
||||
quote: quote,
|
||||
articleId: props.article.id,
|
||||
prefix: surroundingText.prefix,
|
||||
suffix: surroundingText.suffix,
|
||||
patch: JSON.stringify(serialized),
|
||||
highlightPositionPercent: positionPercent * 100,
|
||||
highlightPositionAnchorIndex: annotation.pageIndex,
|
||||
},
|
||||
})
|
||||
if (result) {
|
||||
highlightsRef.current.push(result)
|
||||
|
|
@ -384,20 +406,24 @@ export default function PdfArticleContainer(
|
|||
(ha) => (ha.customData?.omnivoreHighlight as Highlight).id
|
||||
)
|
||||
const positionPercent = positionPercentForAnnotation(annotation)
|
||||
const result = await mergeHighlightMutation({
|
||||
quote,
|
||||
id,
|
||||
shortId,
|
||||
patch: JSON.stringify(serialized),
|
||||
prefix: surroundingText.prefix,
|
||||
suffix: surroundingText.suffix,
|
||||
articleId: props.article.id,
|
||||
overlapHighlightIdList: mergedIds.toArray(),
|
||||
highlightPositionPercent: positionPercent * 100,
|
||||
highlightPositionAnchorIndex: annotation.pageIndex,
|
||||
const result = await mergeHighlight.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
input: {
|
||||
quote,
|
||||
id,
|
||||
shortId,
|
||||
patch: JSON.stringify(serialized),
|
||||
prefix: surroundingText.prefix,
|
||||
suffix: surroundingText.suffix,
|
||||
articleId: props.article.id,
|
||||
overlapHighlightIdList: mergedIds.toArray(),
|
||||
highlightPositionPercent: positionPercent * 100,
|
||||
highlightPositionAnchorIndex: annotation.pageIndex,
|
||||
},
|
||||
})
|
||||
if (result) {
|
||||
highlightsRef.current.push(result)
|
||||
if (result && result.highlight) {
|
||||
highlightsRef.current.push(result.highlight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -410,11 +436,15 @@ export default function PdfArticleContainer(
|
|||
100,
|
||||
Math.max(0, ((pageIndex + 1) / instance.totalPageCount) * 100)
|
||||
)
|
||||
await articleReadingProgressMutation({
|
||||
id: props.article.id,
|
||||
force: true,
|
||||
readingProgressPercent: percent,
|
||||
readingProgressAnchorIndex: pageIndex,
|
||||
await updateItemReadStatus.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
input: {
|
||||
id: props.article.id,
|
||||
force: true,
|
||||
readingProgressPercent: percent,
|
||||
readingProgressAnchorIndex: pageIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
|
@ -517,7 +547,11 @@ export default function PdfArticleContainer(
|
|||
const storedId = annotationOmnivoreId(annotation)
|
||||
if (storedId == annotationId) {
|
||||
await instance.delete(annotation)
|
||||
await deleteHighlightMutation(props.article.id, annotationId)
|
||||
await deleteHighlight.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
highlightId: annotationId,
|
||||
})
|
||||
|
||||
const highlightIdx = highlightsRef.current.findIndex((value) => {
|
||||
return value.id == annotationId
|
||||
|
|
@ -582,8 +616,7 @@ export default function PdfArticleContainer(
|
|||
<HighlightNoteModal
|
||||
highlight={noteTarget}
|
||||
libraryItemId={props.article.id}
|
||||
author={props.article.author ?? ''}
|
||||
title={props.article.title}
|
||||
libraryItemSlug={props.article.slug}
|
||||
onUpdate={(highlight: Highlight) => {
|
||||
const savedHighlight = highlightsRef.current.find(
|
||||
(other: Highlight) => {
|
||||
|
|
|
|||
|
|
@ -4,14 +4,16 @@ import { Button } from '../../elements/Button'
|
|||
import { StyledText } from '../../elements/StyledText'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { Check, Circle, Plus, WarningCircle } from '@phosphor-icons/react'
|
||||
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
import { useRouter } from 'next/router'
|
||||
import { LabelsPicker } from '../../elements/LabelsPicker'
|
||||
import { LabelsDispatcher } from '../../../lib/hooks/useSetPageLabels'
|
||||
import {
|
||||
useCreateLabel,
|
||||
useGetLabels,
|
||||
} from '../../../lib/networking/labels/useLabels'
|
||||
|
||||
export interface LabelsProvider {
|
||||
labels?: Label[]
|
||||
|
|
@ -282,10 +284,10 @@ function Footer(props: FooterProps): JSX.Element {
|
|||
}
|
||||
|
||||
export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
||||
const router = useRouter()
|
||||
const { inputValue, setInputValue, selectedLabels, setHighlightLastLabel } =
|
||||
props
|
||||
const { labels, revalidate } = useGetLabelsQuery()
|
||||
const { data: labels } = useGetLabels()
|
||||
const createLabel = useCreateLabel()
|
||||
// Move focus through the labels list on tab or arrow up/down keys
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(0)
|
||||
|
||||
|
|
@ -321,9 +323,8 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
props.dispatchLabels({ type: 'SAVE', labels: newSelectedLabels })
|
||||
|
||||
props.clearInputState()
|
||||
revalidate()
|
||||
},
|
||||
[isSelected, props, revalidate]
|
||||
[isSelected, props]
|
||||
)
|
||||
|
||||
const filteredLabels = useMemo(() => {
|
||||
|
|
@ -342,11 +343,11 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
const createLabelFromFilterText = useCallback(
|
||||
async (text: string) => {
|
||||
const trimmedLabelName = text.trim()
|
||||
const label = await createLabelMutation(
|
||||
trimmedLabelName,
|
||||
randomLabelColorHex(),
|
||||
''
|
||||
)
|
||||
const label = await createLabel.mutateAsync({
|
||||
name: trimmedLabelName,
|
||||
color: randomLabelColorHex(),
|
||||
description: undefined,
|
||||
})
|
||||
if (label) {
|
||||
showSuccessToast(`Created label ${label.name}`, {
|
||||
position: 'bottom-right',
|
||||
|
|
@ -425,7 +426,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
}, [inputValue, setInputValue, createLabelFromFilterText])
|
||||
|
||||
const selectEnteredLabel = useCallback(() => {
|
||||
const label = labels.find(
|
||||
const label = labels?.find(
|
||||
(l: Label) => l.name.toLowerCase() == inputValue.toLowerCase()
|
||||
)
|
||||
if (!label) {
|
||||
|
|
@ -509,7 +510,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
<Footer
|
||||
filterText={inputValue}
|
||||
selectedLabels={props.selectedLabels}
|
||||
availableLabels={labels}
|
||||
availableLabels={labels ?? []}
|
||||
focused={focusedIndex === filteredLabels.length + 1}
|
||||
createEnteredLabel={createEnteredLabel}
|
||||
selectEnteredLabel={selectEnteredLabel}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@ import {
|
|||
ModalTitleBar,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { LabelsProvider, SetLabelsControl } from './SetLabelsControl'
|
||||
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
import { LabelsDispatcher } from '../../../lib/hooks/useSetPageLabels'
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import {
|
||||
useCreateLabel,
|
||||
useGetLabels,
|
||||
} from '../../../lib/networking/labels/useLabels'
|
||||
|
||||
type SetLabelsModalProps = {
|
||||
provider: LabelsProvider
|
||||
|
|
@ -28,7 +30,7 @@ type SetLabelsModalProps = {
|
|||
export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const { selectedLabels, dispatchLabels } = props
|
||||
const availableLabels = useGetLabelsQuery()
|
||||
const { data: availableLabels } = useGetLabels()
|
||||
const [tabCount, setTabCount] = useState(-1)
|
||||
const [tabStartValue, setTabStartValue] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
|
|
@ -37,6 +39,8 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
const errorTimeoutRef = useRef<NodeJS.Timeout | undefined>()
|
||||
const [highlightLastLabel, setHighlightLastLabel] = useState(false)
|
||||
|
||||
const createLabel = useCreateLabel()
|
||||
|
||||
const showMessage = useCallback(
|
||||
(msg: string, timeout?: number) => {
|
||||
if (errorTimeoutRef.current) {
|
||||
|
|
@ -82,10 +86,11 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
(newLabels: Label[], tempLabel: Label) => {
|
||||
;(async () => {
|
||||
const currentLabels = newLabels
|
||||
const newLabel = await createLabelMutation(
|
||||
tempLabel.name,
|
||||
tempLabel.color
|
||||
)
|
||||
const newLabel = await createLabel.mutateAsync({
|
||||
name: tempLabel.name,
|
||||
color: tempLabel.color,
|
||||
description: undefined,
|
||||
})
|
||||
const idx = currentLabels.findIndex((l) => l.id === tempLabel.id)
|
||||
if (newLabel) {
|
||||
showSuccessToast(`Created label ${newLabel.name}`, {
|
||||
|
|
@ -116,7 +121,7 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
(value: string) => {
|
||||
const current = selectedLabels ?? []
|
||||
const lowerCasedValue = value.toLowerCase()
|
||||
const existing = availableLabels.labels.find(
|
||||
const existing = availableLabels?.find(
|
||||
(l) => l.name.toLowerCase() == lowerCasedValue
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,21 +4,24 @@ import { LabelsProvider } from './SetLabelsControl'
|
|||
import { SetLabelsModal } from './SetLabelsModal'
|
||||
import { useSetHighlightLabels } from '../../../lib/hooks/useSetHighlightLabels'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { LibraryItemNode } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
|
||||
type SetPageLabelsModalPresenterProps = {
|
||||
articleId: string
|
||||
article: LabelsProvider
|
||||
libraryItem: LibraryItemNode
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function SetPageLabelsModalPresenter(
|
||||
props: SetPageLabelsModalPresenterProps
|
||||
): JSX.Element {
|
||||
const [labels, dispatchLabels] = useSetPageLabels(props.articleId)
|
||||
const [labels, dispatchLabels] = useSetPageLabels(
|
||||
props.libraryItem.id,
|
||||
props.libraryItem.slug
|
||||
)
|
||||
|
||||
const onOpenChange = useCallback(() => {
|
||||
if (props.article) {
|
||||
props.article.labels = labels.labels
|
||||
if (props.libraryItem) {
|
||||
props.libraryItem.labels = labels.labels
|
||||
}
|
||||
props.onOpenChange(true)
|
||||
}, [props, labels])
|
||||
|
|
@ -26,13 +29,13 @@ export function SetPageLabelsModalPresenter(
|
|||
useEffect(() => {
|
||||
dispatchLabels({
|
||||
type: 'RESET',
|
||||
labels: props.article.labels ?? [],
|
||||
labels: props.libraryItem.labels ?? [],
|
||||
})
|
||||
}, [props.article, dispatchLabels])
|
||||
}, [props.libraryItem, dispatchLabels])
|
||||
|
||||
return (
|
||||
<SetLabelsModal
|
||||
provider={props.article}
|
||||
provider={props.libraryItem}
|
||||
selectedLabels={labels.labels}
|
||||
dispatchLabels={dispatchLabels}
|
||||
onOpenChange={onOpenChange}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { ArticleAttributes } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { HStack } from '../../elements/LayoutPrimitives'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
|
|
@ -11,6 +11,7 @@ import { EditInfoIcon } from '../../elements/icons/EditInfoIcon'
|
|||
import { ReaderSettingsIcon } from '../../elements/icons/ReaderSettingsIcon'
|
||||
import { CircleUtilityMenuIcon } from '../../elements/icons/CircleUtilityMenuIcon'
|
||||
import { UnarchiveIcon } from '../../elements/icons/UnarchiveIcon'
|
||||
import { State } from '../../../lib/networking/fragments/articleFragment'
|
||||
|
||||
export type ArticleActionsMenuLayout = 'top' | 'side'
|
||||
|
||||
|
|
@ -94,15 +95,12 @@ export function VerticalArticleActionsMenu(
|
|||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</Button>
|
||||
|
||||
{!props.article?.isArchived ? (
|
||||
{props.article?.state !== State.ARCHIVED ? (
|
||||
<Button
|
||||
title="Archive (e)"
|
||||
style="articleActionIcon"
|
||||
|
|
@ -155,6 +153,7 @@ export function VerticalArticleActionsMenu(
|
|||
</Button>
|
||||
|
||||
<ReaderDropdownMenu
|
||||
libraryItem={props.article}
|
||||
triggerElement={
|
||||
<CircleUtilityMenuIcon
|
||||
size={24}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import dayjs, { Dayjs } from 'dayjs'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { updatePageMutation } from '../../../lib/networking/mutations/updatePageMutation'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import {
|
||||
ArticleAttributes,
|
||||
useUpdateItem,
|
||||
} from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { LibraryItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { CloseButton } from '../../elements/CloseButton'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
|
|
@ -24,23 +26,28 @@ type EditLibraryItemModalProps = {
|
|||
export function EditLibraryItemModal(
|
||||
props: EditLibraryItemModalProps
|
||||
): JSX.Element {
|
||||
const updateItem = useUpdateItem()
|
||||
const onSave = useCallback(
|
||||
(
|
||||
title: string,
|
||||
author: string | undefined,
|
||||
description: string,
|
||||
description: string | undefined,
|
||||
savedAt: Dayjs,
|
||||
publishedAt: Dayjs | undefined
|
||||
) => {
|
||||
;(async () => {
|
||||
if (title !== '') {
|
||||
const res = await updatePageMutation({
|
||||
pageId: props.item.node.id,
|
||||
title,
|
||||
description,
|
||||
byline: author,
|
||||
savedAt: savedAt.toISOString(),
|
||||
publishedAt: publishedAt ? publishedAt.toISOString() : undefined,
|
||||
const res = await updateItem.mutateAsync({
|
||||
itemId: props.item.node.id,
|
||||
slug: props.item.node.slug,
|
||||
input: {
|
||||
pageId: props.item.node.id,
|
||||
title,
|
||||
description,
|
||||
byline: author,
|
||||
savedAt: savedAt.toISOString(),
|
||||
publishedAt: publishedAt ? publishedAt.toISOString() : undefined,
|
||||
},
|
||||
})
|
||||
|
||||
if (res) {
|
||||
|
|
@ -95,30 +102,35 @@ type EditArticleModalProps = {
|
|||
updateArticle: (
|
||||
title: string,
|
||||
author: string | undefined,
|
||||
description: string,
|
||||
description: string | undefined,
|
||||
savedAt: string,
|
||||
publishedAt: string | undefined
|
||||
) => void
|
||||
}
|
||||
|
||||
export function EditArticleModal(props: EditArticleModalProps): JSX.Element {
|
||||
const updateItem = useUpdateItem()
|
||||
const onSave = useCallback(
|
||||
(
|
||||
title: string,
|
||||
author: string | undefined,
|
||||
description: string,
|
||||
description: string | undefined,
|
||||
savedAt: Dayjs,
|
||||
publishedAt: Dayjs | undefined
|
||||
) => {
|
||||
;(async () => {
|
||||
if (title !== '') {
|
||||
const res = await updatePageMutation({
|
||||
pageId: props.article.id,
|
||||
title,
|
||||
description,
|
||||
byline: author,
|
||||
savedAt: savedAt.toISOString(),
|
||||
publishedAt: publishedAt ? publishedAt.toISOString() : undefined,
|
||||
const res = await updateItem.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
input: {
|
||||
pageId: props.article.id,
|
||||
title,
|
||||
description,
|
||||
byline: author,
|
||||
savedAt: savedAt.toISOString(),
|
||||
publishedAt: publishedAt ? publishedAt.toISOString() : undefined,
|
||||
},
|
||||
})
|
||||
if (res) {
|
||||
props.updateArticle(
|
||||
|
|
@ -165,7 +177,7 @@ export function EditArticleModal(props: EditArticleModalProps): JSX.Element {
|
|||
type EditItemModalProps = {
|
||||
title: string
|
||||
author: string | undefined
|
||||
description: string
|
||||
description: string | undefined
|
||||
|
||||
savedAt: Dayjs
|
||||
publishedAt: Dayjs | undefined
|
||||
|
|
@ -174,7 +186,7 @@ type EditItemModalProps = {
|
|||
onSave: (
|
||||
title: string,
|
||||
author: string | undefined,
|
||||
description: string,
|
||||
description: string | undefined,
|
||||
savedAt: Dayjs,
|
||||
publishedAt: Dayjs | undefined
|
||||
) => void
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import Link from 'next/link'
|
|||
import { DotsThreeVertical } from '@phosphor-icons/react'
|
||||
import { useCallback } from 'react'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
|||
import {
|
||||
LibraryItem,
|
||||
LibraryItemNode,
|
||||
} from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
} from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,7 @@ import { LayoutType, LibraryMode } from './HomeFeedContainer'
|
|||
import { OmnivoreSmallLogo } from '../../elements/images/OmnivoreNameLogo'
|
||||
import { DEFAULT_HEADER_HEIGHT, HeaderSpacer } from './HeaderSpacer'
|
||||
import { LIBRARY_LEFT_MENU_WIDTH } from '../navMenu/LibraryMenu'
|
||||
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
|
||||
import { BulkAction } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { HeaderToggleGridIcon } from '../../elements/icons/HeaderToggleGridIcon'
|
||||
import { HeaderToggleListIcon } from '../../elements/icons/HeaderToggleListIcon'
|
||||
import { HeaderToggleTLDRIcon } from '../../elements/icons/HeaderToggleTLDRIcon'
|
||||
|
|
@ -123,7 +123,7 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
|
|||
>
|
||||
{props.multiSelectMode !== 'off' ? (
|
||||
<>
|
||||
<MultiSelectControls {...props} />
|
||||
<MultiSelectControls {...props} folder={'library'} />
|
||||
</>
|
||||
) : (
|
||||
<HeaderControls {...props} />
|
||||
|
|
@ -310,7 +310,7 @@ export function SearchBox(props: SearchBoxProps): JSX.Element {
|
|||
},
|
||||
}}
|
||||
>
|
||||
<CheckBoxButton {...props} />
|
||||
<CheckBoxButton {...props} folder={'library'} />
|
||||
</HStack>
|
||||
<HStack
|
||||
alignment="center"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useState } from 'react'
|
|||
import { theme } from '../../tokens/stitches.config'
|
||||
import { Box, HStack, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
|
||||
import { BulkAction } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { ArchiveIcon } from '../../elements/icons/ArchiveIcon'
|
||||
import { LabelIcon } from '../../elements/icons/LabelIcon'
|
||||
import { TrashIcon } from '../../elements/icons/TrashIcon'
|
||||
|
|
@ -14,10 +14,13 @@ import { HeaderCheckboxIcon } from '../../elements/icons/HeaderCheckboxIcon'
|
|||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { MarkAsReadIcon } from '../../elements/icons/MarkAsReadIcon'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { UnarchiveIcon } from '../../elements/icons/UnarchiveIcon'
|
||||
import { MoveToInboxIcon } from '../../elements/icons/MoveToInboxIcon'
|
||||
|
||||
export type MultiSelectProps = {
|
||||
viewer: UserBasicData | undefined
|
||||
|
||||
folder: string | undefined
|
||||
searchTerm: string | undefined
|
||||
applySearchQuery: (searchQuery: string) => void
|
||||
|
||||
|
|
@ -116,7 +119,7 @@ export const MultiSelectControls = (props: MultiSelectProps): JSX.Element => {
|
|||
<SpanBox
|
||||
css={{
|
||||
display: 'none',
|
||||
fontSize: '14px',
|
||||
fontSize: '11px',
|
||||
fontFamily: '$display',
|
||||
marginRight: 'auto',
|
||||
'@mdDown': {
|
||||
|
|
@ -126,9 +129,14 @@ export const MultiSelectControls = (props: MultiSelectProps): JSX.Element => {
|
|||
>
|
||||
{props.numItemsSelected} items
|
||||
</SpanBox>
|
||||
<ArchiveButton {...props} />
|
||||
{props.folder !== 'archive' && <ArchiveButton {...props} />}
|
||||
<AddLabelsButton setShowLabelsModal={setShowLabelsModal} />
|
||||
<RemoveItemsButton setShowConfirmDelete={setShowConfirmDelete} />
|
||||
{props.folder == 'subscriptions' && (
|
||||
<MoveToLibraryButton {...props} />
|
||||
)}
|
||||
{props.folder !== 'trash' && (
|
||||
<RemoveItemsButton setShowConfirmDelete={setShowConfirmDelete} />
|
||||
)}
|
||||
<MarkAsReadButton {...props} />
|
||||
{showConfirmDelete && (
|
||||
<ConfirmationModal
|
||||
|
|
@ -255,6 +263,41 @@ export const MarkAsReadButton = (props: MultiSelectProps): JSX.Element => {
|
|||
)
|
||||
}
|
||||
|
||||
export const MoveToLibraryButton = (props: MultiSelectProps): JSX.Element => {
|
||||
const [color, setColor] = useState<string>(
|
||||
theme.colors.thTextContrast2.toString()
|
||||
)
|
||||
return (
|
||||
<Button
|
||||
title="Move to library"
|
||||
css={{
|
||||
p: '5px',
|
||||
display: 'flex',
|
||||
'&:hover': {
|
||||
bg: '$ctaBlue',
|
||||
borderRadius: '100px',
|
||||
opacity: 1.0,
|
||||
},
|
||||
}}
|
||||
onMouseEnter={(event) => {
|
||||
setColor('white')
|
||||
event.preventDefault()
|
||||
}}
|
||||
onMouseLeave={(event) => {
|
||||
setColor(theme.colors.thTextContrast2.toString())
|
||||
event.preventDefault()
|
||||
}}
|
||||
style="plainIcon"
|
||||
onClick={(e) => {
|
||||
props.performMultiSelectAction(BulkAction.MOVE_TO_FOLDER)
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<MoveToInboxIcon size={20} color={color} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
type AddLabelsButtonProps = {
|
||||
setShowLabelsModal: (set: boolean) => void
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { LayoutType } from './HomeFeedContainer'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { LibraryItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import TopBarProgress from 'react-topbar-progress-indicator'
|
||||
|
|
|
|||
|
|
@ -8,21 +8,24 @@ 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 {
|
||||
PageType,
|
||||
State,
|
||||
} from '../../../lib/networking/fragments/articleFragment'
|
||||
import {
|
||||
SearchItem,
|
||||
TypeaheadSearchItemsData,
|
||||
typeaheadSearchQuery,
|
||||
} from '../../../lib/networking/queries/typeaheadSearch'
|
||||
import type {
|
||||
import {
|
||||
LibraryItem,
|
||||
LibraryItemNode,
|
||||
LibraryItems,
|
||||
LibraryItemsQueryInput,
|
||||
} from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { useGetLibraryItemsQuery } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
useArchiveItem,
|
||||
useBulkActions,
|
||||
useDeleteItem,
|
||||
useGetLibraryItems,
|
||||
useMoveItemToFolder,
|
||||
useRefreshProcessingItems,
|
||||
useUpdateItemReadStatus,
|
||||
} from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import {
|
||||
useGetViewerQuery,
|
||||
UserBasicData,
|
||||
|
|
@ -38,17 +41,10 @@ import { EditLibraryItemModal } from '../homeFeed/EditItemModals'
|
|||
import { EmptyLibrary } from '../homeFeed/EmptyLibrary'
|
||||
import { MultiSelectMode } from '../homeFeed/LibraryHeader'
|
||||
import { UploadModal } from '../UploadModal'
|
||||
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
|
||||
import { bulkActionMutation } from '../../../lib/networking/mutations/bulkActionMutation'
|
||||
import {
|
||||
showErrorToast,
|
||||
showSuccessToast,
|
||||
showSuccessToastWithAction,
|
||||
} from '../../../lib/toastHelpers'
|
||||
import { BulkAction } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter'
|
||||
import { NotebookPresenter } from '../article/NotebookPresenter'
|
||||
import { saveUrlMutation } from '../../../lib/networking/mutations/saveUrlMutation'
|
||||
import { articleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { PinnedButtons } from '../homeFeed/PinnedButtons'
|
||||
import { PinnedSearch } from '../../../pages/settings/pinned-searches'
|
||||
import { FetchItemsError } from '../homeFeed/FetchItemsError'
|
||||
|
|
@ -56,6 +52,9 @@ import { LibraryHeader } from './LibraryHeader'
|
|||
import { TrashIcon } from '../../elements/icons/TrashIcon'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { emptyTrashMutation } from '../../../lib/networking/mutations/emptyTrashMutation'
|
||||
import { State } from '../../../lib/networking/fragments/articleFragment'
|
||||
import { useHandleAddUrl } from '../../../lib/hooks/useHandleAddUrl'
|
||||
import { QueryClient, useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
|
||||
|
||||
|
|
@ -72,21 +71,16 @@ const debouncedFetchSearchResults = debounce((query, cb) => {
|
|||
fetchSearchResults(query, cb)
|
||||
}, 300)
|
||||
|
||||
// We set a relatively high delay for the refresh at the end, as it's likely there's an issue
|
||||
// in processing. We give it the best attempt to be able to resolve, but if it doesn't we set
|
||||
// the state as Failed. On refresh it will try again if the backend sends "PROCESSING"
|
||||
const TIMEOUT_DELAYS = [2000, 3500, 5000]
|
||||
|
||||
type LibraryContainerProps = {
|
||||
folder: string
|
||||
folder: string | undefined
|
||||
filterFunc: (item: LibraryItemNode) => boolean
|
||||
|
||||
showNavigationMenu: boolean
|
||||
}
|
||||
|
||||
export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const router = useRouter()
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const { queryValue } = useKBar((state) => ({ queryValue: state.searchQuery }))
|
||||
const [searchResults, setSearchResults] = useState<SearchItem[]>([])
|
||||
|
||||
|
|
@ -110,31 +104,22 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
const [linkToEdit, setLinkToEdit] = useState<LibraryItem>()
|
||||
const [linkToUnsubscribe, setLinkToUnsubscribe] = useState<LibraryItem>()
|
||||
|
||||
const archiveItem = useArchiveItem()
|
||||
const deleteItem = useDeleteItem()
|
||||
const moveToFolder = useMoveItemToFolder()
|
||||
const bulkAction = useBulkActions()
|
||||
const updateItemReadStatus = useUpdateItemReadStatus()
|
||||
|
||||
const [queryInputs, setQueryInputs] =
|
||||
useState<LibraryItemsQueryInput>(defaultQuery)
|
||||
|
||||
const {
|
||||
itemsPages,
|
||||
size,
|
||||
setSize,
|
||||
isValidating,
|
||||
performActionOnItem,
|
||||
mutate,
|
||||
data: itemsPages,
|
||||
isLoading,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
error: fetchItemsError,
|
||||
} = useGetLibraryItemsQuery(props.folder, queryInputs)
|
||||
|
||||
useEffect(() => {
|
||||
const handleRevalidate = () => {
|
||||
;(async () => {
|
||||
console.log('revalidating library')
|
||||
await mutate()
|
||||
})()
|
||||
}
|
||||
document.addEventListener('revalidateLibrary', handleRevalidate)
|
||||
return () => {
|
||||
document.removeEventListener('revalidateLibrary', handleRevalidate)
|
||||
}
|
||||
}, [mutate])
|
||||
} = useGetLibraryItems(props.folder, queryInputs)
|
||||
|
||||
useEffect(() => {
|
||||
if (queryValue.startsWith('#')) {
|
||||
|
|
@ -157,7 +142,7 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
|
||||
if (qs !== (queryInputs.searchQuery || '')) {
|
||||
setQueryInputs({ ...queryInputs, searchQuery: qs })
|
||||
performActionOnItem('refresh', undefined as unknown as any)
|
||||
// performActionOnItem('refresh', undefined as unknown as any)
|
||||
}
|
||||
|
||||
// intentionally not watching queryInputs and performActionOnItem
|
||||
|
|
@ -169,25 +154,18 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
window.localStorage.setItem('nav-return', router.asPath)
|
||||
}, [router.asPath])
|
||||
|
||||
const hasMore = useMemo(() => {
|
||||
if (!itemsPages) {
|
||||
return false
|
||||
}
|
||||
return itemsPages[itemsPages.length - 1].search.pageInfo.hasNextPage
|
||||
}, [itemsPages])
|
||||
|
||||
const libraryItems = useMemo(() => {
|
||||
const items =
|
||||
itemsPages
|
||||
?.flatMap((ad) => {
|
||||
return ad.search.edges.map((it) => ({
|
||||
itemsPages?.pages
|
||||
.flatMap((ad: LibraryItems) => {
|
||||
return ad.edges.map((it) => ({
|
||||
...it,
|
||||
isLoading: it.node.state === 'PROCESSING',
|
||||
}))
|
||||
})
|
||||
.filter((item) => props.filterFunc(item.node)) || []
|
||||
return items
|
||||
}, [itemsPages, performActionOnItem])
|
||||
}, [itemsPages])
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage) {
|
||||
|
|
@ -198,78 +176,22 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
}
|
||||
}, [libraryItems])
|
||||
|
||||
useEffect(() => {
|
||||
const timeout: NodeJS.Timeout[] = []
|
||||
const processingItems = useMemo(() => {
|
||||
return libraryItems
|
||||
.filter((li) => li.node.state === State.PROCESSING)
|
||||
.map((li) => li.node.id)
|
||||
}, [libraryItems])
|
||||
|
||||
const items = (
|
||||
itemsPages?.flatMap((ad) => {
|
||||
return ad.search.edges.map((it) => ({
|
||||
...it,
|
||||
isLoading: it.node.state === 'PROCESSING',
|
||||
}))
|
||||
}) || []
|
||||
).filter((it) => it.isLoading)
|
||||
|
||||
items.map(async (item) => {
|
||||
let startIdx = 0
|
||||
|
||||
const seeIfUpdated = async () => {
|
||||
if (startIdx >= TIMEOUT_DELAYS.length) {
|
||||
item.node.state = State.FAILED
|
||||
const updatedArticle = { ...item }
|
||||
updatedArticle.node = { ...item.node }
|
||||
updatedArticle.isLoading = false
|
||||
performActionOnItem('update-item', updatedArticle)
|
||||
return
|
||||
}
|
||||
|
||||
const username = viewerData?.me?.profile.username
|
||||
const itemsToUpdate = libraryItems.filter((it) => it.isLoading)
|
||||
|
||||
if (itemsToUpdate.length > 0) {
|
||||
const link = await articleQuery({
|
||||
username,
|
||||
slug: item.node.id,
|
||||
includeFriendsHighlights: false,
|
||||
})
|
||||
|
||||
if (link && link.state != 'PROCESSING') {
|
||||
const updatedArticle = { ...item }
|
||||
updatedArticle.node = { ...item.node, ...link }
|
||||
updatedArticle.isLoading = false
|
||||
console.log(`Updating Metadata of ${item.node.slug}.`)
|
||||
performActionOnItem('update-item', updatedArticle)
|
||||
return
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Trying to get the metadata of item ${item.node.slug}... Retry ${startIdx} of 5`
|
||||
)
|
||||
timeout.push(setTimeout(seeIfUpdated, TIMEOUT_DELAYS[startIdx++]))
|
||||
}
|
||||
}
|
||||
|
||||
await seeIfUpdated()
|
||||
})
|
||||
|
||||
return () => {
|
||||
timeout.forEach(clearTimeout)
|
||||
}
|
||||
}, [itemsPages])
|
||||
|
||||
const handleFetchMore = useCallback(() => {
|
||||
if (isValidating || !hasMore) {
|
||||
return
|
||||
}
|
||||
setSize(size + 1)
|
||||
}, [size, isValidating])
|
||||
const refreshProcessingItems = useRefreshProcessingItems()
|
||||
|
||||
useEffect(() => {
|
||||
if (isValidating || !hasMore || size !== 1) {
|
||||
return
|
||||
if (processingItems.length) {
|
||||
refreshProcessingItems.mutateAsync({
|
||||
attempt: 0,
|
||||
itemIds: processingItems,
|
||||
})
|
||||
}
|
||||
setSize(size + 1)
|
||||
}, [size, isValidating])
|
||||
}, [processingItems])
|
||||
|
||||
const focusFirstItem = useCallback(() => {
|
||||
if (libraryItems.length < 1) {
|
||||
|
|
@ -374,10 +296,6 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
if (activeCardId && !alreadyScrolled.current) {
|
||||
scrollToActiveCard(activeCardId)
|
||||
alreadyScrolled.current = true
|
||||
|
||||
if (activeItem) {
|
||||
performActionOnItem('refresh', activeItem)
|
||||
}
|
||||
}
|
||||
}, [activeCardId, scrollToActiveCard])
|
||||
|
||||
|
|
@ -397,11 +315,7 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
if (item.node.state === State.PROCESSING) {
|
||||
router.push(`/article?url=${encodeURIComponent(item.node.url)}`)
|
||||
} else {
|
||||
const dl =
|
||||
item.node.pageType === PageType.HIGHLIGHTS
|
||||
? `#${item.node.id}`
|
||||
: ''
|
||||
router.push(`/${username}/${item.node.slug}` + dl)
|
||||
router.push(`/${username}/${item.node.slug}`)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
|
@ -412,19 +326,94 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
}
|
||||
break
|
||||
case 'archive':
|
||||
performActionOnItem('archive', item)
|
||||
break
|
||||
case 'unarchive':
|
||||
performActionOnItem('unarchive', item)
|
||||
try {
|
||||
await archiveItem.mutateAsync({
|
||||
itemId: item.node.id,
|
||||
slug: item.node.slug,
|
||||
input: {
|
||||
linkId: item.node.id,
|
||||
archived: action == 'archive',
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.log('Error setting archive state: ', err)
|
||||
showErrorToast(`Error ${action}ing item`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
return
|
||||
}
|
||||
showSuccessToast(`Item ${action}d`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
break
|
||||
case 'delete':
|
||||
performActionOnItem('delete', item)
|
||||
try {
|
||||
await deleteItem.mutateAsync({
|
||||
itemId: item.node.id,
|
||||
slug: item.node.slug,
|
||||
})
|
||||
} catch (err) {
|
||||
console.log('Error deleting item: ', err)
|
||||
showErrorToast(`Error deleting item`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
return
|
||||
}
|
||||
showSuccessToast(`Item deleted`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
break
|
||||
case 'mark-read':
|
||||
performActionOnItem('mark-read', item)
|
||||
break
|
||||
case 'mark-unread':
|
||||
performActionOnItem('mark-unread', item)
|
||||
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({
|
||||
itemId: item.node.id,
|
||||
slug: item.node.slug,
|
||||
input: {
|
||||
id: item.node.id,
|
||||
force: true,
|
||||
...values,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.log('Error marking item: ', err)
|
||||
showErrorToast(`Error marking as ${desc}`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
return
|
||||
}
|
||||
break
|
||||
case 'move-to-inbox':
|
||||
try {
|
||||
await moveToFolder.mutateAsync({
|
||||
itemId: item.node.id,
|
||||
slug: item.node.slug,
|
||||
folder: 'inbox',
|
||||
})
|
||||
} catch (err) {
|
||||
console.log('Error moving item: ', err)
|
||||
showErrorToast(`Error moving item`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
return
|
||||
}
|
||||
showSuccessToast(`Item moved to library`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
break
|
||||
case 'set-labels':
|
||||
setLabelsTarget(item)
|
||||
|
|
@ -437,10 +426,10 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
}
|
||||
break
|
||||
case 'unsubscribe':
|
||||
performActionOnItem('unsubscribe', item)
|
||||
case 'update-item':
|
||||
performActionOnItem('update-item', item)
|
||||
// setLinkToUnsubscribe(item.node)
|
||||
break
|
||||
default:
|
||||
console.warn('unknown action: ', action)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -590,19 +579,20 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
})
|
||||
)
|
||||
|
||||
const ARCHIVE_ACTION = !activeItem?.node.isArchived
|
||||
? 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),
|
||||
})
|
||||
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),
|
||||
})
|
||||
|
||||
const ACTIVE_ACTIONS = [
|
||||
ARCHIVE_ACTION,
|
||||
|
|
@ -676,7 +666,7 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
activeCardId ? [...ACTIVE_ACTIONS, ...UNACTIVE_ACTIONS] : UNACTIVE_ACTIONS,
|
||||
[activeCardId, activeItem]
|
||||
)
|
||||
useFetchMore(handleFetchMore)
|
||||
useFetchMore(fetchNextPage)
|
||||
|
||||
const setIsChecked = useCallback(
|
||||
(itemId: string, set: boolean) => {
|
||||
|
|
@ -710,8 +700,8 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
case 'search':
|
||||
case 'visible':
|
||||
const allIds = (
|
||||
itemsPages?.flatMap((ad) => {
|
||||
return ad.search.edges
|
||||
itemsPages?.pages.flatMap((ad) => {
|
||||
return ad.edges
|
||||
}) || []
|
||||
).map((item) => item.node.id)
|
||||
setCheckedItems(allIds)
|
||||
|
|
@ -739,21 +729,23 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
multiSelectMode === 'search'
|
||||
? queryInputs.searchQuery || 'in:inbox'
|
||||
: `includes:${checkedItems.join(',')}`
|
||||
const expectedCount =
|
||||
multiSelectMode === 'search'
|
||||
? itemsPages?.[0].search.pageInfo.totalCount || 0
|
||||
: checkedItems.length
|
||||
const expectedCount = checkedItems.length
|
||||
|
||||
let bulkArguments = undefined
|
||||
if (action == BulkAction.MOVE_TO_FOLDER) {
|
||||
bulkArguments = { folder: 'inbox ' }
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await bulkActionMutation(
|
||||
const res = await bulkAction.mutateAsync({
|
||||
action,
|
||||
query,
|
||||
expectedCount,
|
||||
labelIds
|
||||
)
|
||||
labelIds,
|
||||
arguments: bulkArguments,
|
||||
})
|
||||
if (res) {
|
||||
let successMessage: string | undefined = undefined
|
||||
console.log(action)
|
||||
switch (action) {
|
||||
case BulkAction.ARCHIVE:
|
||||
successMessage = 'Link Archived'
|
||||
|
|
@ -767,6 +759,9 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
case BulkAction.MARK_AS_READ:
|
||||
successMessage = 'Items marked as read'
|
||||
break
|
||||
case BulkAction.MOVE_TO_FOLDER:
|
||||
successMessage = 'Items moved to library'
|
||||
break
|
||||
}
|
||||
if (successMessage) {
|
||||
showSuccessToast(successMessage, { position: 'bottom-right' })
|
||||
|
|
@ -781,37 +776,18 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
mutate()
|
||||
// mutate()
|
||||
})()
|
||||
setMultiSelectMode('off')
|
||||
},
|
||||
[itemsPages, multiSelectMode, checkedItems]
|
||||
)
|
||||
|
||||
const handleLinkSubmission = async (
|
||||
link: string,
|
||||
timezone: string,
|
||||
locale: string
|
||||
) => {
|
||||
const result = await saveUrlMutation(link, timezone, locale)
|
||||
if (result) {
|
||||
showSuccessToastWithAction('Link saved', 'Read now', async () => {
|
||||
window.location.href = `/article?url=${encodeURIComponent(link)}`
|
||||
return Promise.resolve()
|
||||
})
|
||||
const id = result.url?.match(/[^/]+$/)?.[0] ?? ''
|
||||
performActionOnItem('refresh', undefined as unknown as any)
|
||||
} else {
|
||||
showErrorToast('Error saving link', { position: 'bottom-right' })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<HomeFeedGrid
|
||||
folder={props.folder}
|
||||
items={libraryItems}
|
||||
actionHandler={handleCardAction}
|
||||
reloadItems={mutate}
|
||||
setIsChecked={setIsChecked}
|
||||
itemIsChecked={itemIsChecked}
|
||||
multiSelectMode={multiSelectMode}
|
||||
|
|
@ -820,7 +796,6 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
performMultiSelectAction={performMultiSelectAction}
|
||||
searchTerm={queryInputs.searchQuery}
|
||||
gridContainerRef={gridContainerRef}
|
||||
handleLinkSubmission={handleLinkSubmission}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
setQueryInputs({
|
||||
...queryInputs,
|
||||
|
|
@ -836,18 +811,11 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
const href = `${window.location.pathname}?${qp.toString()}`
|
||||
router.push(href, href, { shallow: true })
|
||||
window.sessionStorage.setItem('q', qp.toString())
|
||||
performActionOnItem('refresh', undefined as unknown as any)
|
||||
}}
|
||||
loadMore={() => {
|
||||
if (isValidating) {
|
||||
return
|
||||
}
|
||||
setSize(size + 1)
|
||||
}}
|
||||
hasMore={hasMore}
|
||||
loadMore={fetchNextPage}
|
||||
hasMore={hasNextPage ?? false}
|
||||
hasData={!!itemsPages}
|
||||
totalItems={itemsPages?.[0].search.pageInfo.totalCount || 0}
|
||||
isValidating={isValidating}
|
||||
isValidating={isLoading}
|
||||
fetchItemsError={!!fetchItemsError}
|
||||
labelsTarget={labelsTarget}
|
||||
setLabelsTarget={setLabelsTarget}
|
||||
|
|
@ -864,25 +832,19 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
|||
setLinkToEdit={setLinkToEdit}
|
||||
linkToUnsubscribe={linkToUnsubscribe}
|
||||
setLinkToUnsubscribe={setLinkToUnsubscribe}
|
||||
numItemsSelected={
|
||||
multiSelectMode == 'search'
|
||||
? itemsPages?.[0].search.pageInfo.totalCount || 0
|
||||
: checkedItems.length
|
||||
}
|
||||
numItemsSelected={checkedItems.length}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export type HomeFeedContentProps = {
|
||||
folder: string
|
||||
folder: string | undefined
|
||||
items: LibraryItem[]
|
||||
searchTerm?: string
|
||||
reloadItems: () => void
|
||||
gridContainerRef: React.RefObject<HTMLDivElement>
|
||||
applySearchQuery: (searchQuery: string) => void
|
||||
hasMore: boolean
|
||||
hasData: boolean
|
||||
totalItems: number
|
||||
isValidating: boolean
|
||||
fetchItemsError: boolean
|
||||
|
||||
|
|
@ -909,12 +871,6 @@ export type HomeFeedContentProps = {
|
|||
item: LibraryItem | undefined
|
||||
) => Promise<void>
|
||||
|
||||
handleLinkSubmission: (
|
||||
link: string,
|
||||
timezone: string,
|
||||
locale: string
|
||||
) => Promise<void>
|
||||
|
||||
showNavigationMenu: boolean
|
||||
|
||||
setIsChecked: (itemId: string, set: boolean) => void
|
||||
|
|
@ -953,6 +909,8 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
return true
|
||||
}, [props])
|
||||
|
||||
const addUrl = useHandleAddUrl()
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
|
|
@ -969,6 +927,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
>
|
||||
<LibraryHeader
|
||||
layout={layout}
|
||||
folder={props.folder}
|
||||
viewer={viewerData?.me}
|
||||
updateLayout={updateLayout}
|
||||
showFilterMenu={props.showNavigationMenu}
|
||||
|
|
@ -1004,7 +963,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
|
||||
{props.showAddLinkModal && (
|
||||
<AddLinkModal
|
||||
handleLinkSubmission={props.handleLinkSubmission}
|
||||
handleLinkSubmission={addUrl}
|
||||
onOpenChange={() => props.setShowAddLinkModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -1014,7 +973,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
}
|
||||
|
||||
type LibraryItemsLayoutProps = {
|
||||
folder: string
|
||||
folder: string | undefined
|
||||
|
||||
layout: LayoutType
|
||||
viewer?: UserBasicData
|
||||
|
|
@ -1136,6 +1095,9 @@ export function LibraryItemsLayout(
|
|||
css={{ textDecoration: 'underline' }}
|
||||
onClick={async (event) => {
|
||||
event.preventDefault()
|
||||
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.'
|
||||
)
|
||||
await emptyTrashMutation()
|
||||
showSuccessToast('Emptying trash')
|
||||
setTimeout(() => {
|
||||
|
|
@ -1162,7 +1124,7 @@ export function LibraryItemsLayout(
|
|||
}}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<LibraryItems
|
||||
<LibraryItemsList
|
||||
folder={props.folder}
|
||||
items={props.items}
|
||||
layout={props.layout}
|
||||
|
|
@ -1200,13 +1162,14 @@ export function LibraryItemsLayout(
|
|||
</VStack>
|
||||
{props.showEditTitleModal && (
|
||||
<EditLibraryItemModal
|
||||
updateItem={(item: LibraryItem) =>
|
||||
props.actionHandler('update-item', item)
|
||||
}
|
||||
onOpenChange={() => {
|
||||
props.setShowEditTitleModal(false)
|
||||
props.setLinkToEdit(undefined)
|
||||
}}
|
||||
updateItem={async () => {
|
||||
await Promise.resolve()
|
||||
console.log('item updated')
|
||||
}}
|
||||
item={props.linkToEdit as LibraryItem}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -1219,8 +1182,7 @@ export function LibraryItemsLayout(
|
|||
)}
|
||||
{props.labelsTarget?.node.id && (
|
||||
<SetPageLabelsModalPresenter
|
||||
articleId={props.labelsTarget.node.id}
|
||||
article={props.labelsTarget.node}
|
||||
libraryItem={props.labelsTarget.node}
|
||||
onOpenChange={() => {
|
||||
if (props.labelsTarget) {
|
||||
const activate = props.labelsTarget
|
||||
|
|
@ -1252,7 +1214,7 @@ export function LibraryItemsLayout(
|
|||
}
|
||||
|
||||
type LibraryItemsProps = {
|
||||
folder: string
|
||||
folder: string | undefined
|
||||
items: LibraryItem[]
|
||||
layout: LayoutType
|
||||
viewer: UserBasicData | undefined
|
||||
|
|
@ -1274,7 +1236,7 @@ type LibraryItemsProps = {
|
|||
) => Promise<void>
|
||||
}
|
||||
|
||||
function LibraryItems(props: LibraryItemsProps): JSX.Element {
|
||||
function LibraryItemsList(props: LibraryItemsProps): JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
ref={props.gridContainerRef}
|
||||
|
|
@ -1283,7 +1245,7 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element {
|
|||
width: '100%',
|
||||
gridAutoRows: 'auto',
|
||||
borderRadius: '6px',
|
||||
gridGap: props.layout == 'LIST_LAYOUT' ? '10px' : '20px',
|
||||
gridGap: props.layout == 'LIST_LAYOUT' ? '0px' : '20px',
|
||||
marginTop: '10px',
|
||||
marginBottom: '0px',
|
||||
paddingTop: '0',
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { FunnelSimple, X } from '@phosphor-icons/react'
|
|||
import { LayoutType } from '../homeFeed/HomeFeedContainer'
|
||||
import { OmnivoreSmallLogo } from '../../elements/images/OmnivoreNameLogo'
|
||||
import { LIBRARY_LEFT_MENU_WIDTH } from '../navMenu/LibraryMenu'
|
||||
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
|
||||
import { BulkAction } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { HeaderToggleGridIcon } from '../../elements/icons/HeaderToggleGridIcon'
|
||||
import { HeaderToggleListIcon } from '../../elements/icons/HeaderToggleListIcon'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
|
|
@ -26,6 +26,7 @@ export type LibraryHeaderProps = {
|
|||
layout: LayoutType
|
||||
updateLayout: (layout: LayoutType) => void
|
||||
|
||||
folder: string | undefined
|
||||
searchTerm: string | undefined
|
||||
applySearchQuery: (searchQuery: string) => void
|
||||
|
||||
|
|
@ -250,26 +251,28 @@ export function SearchBox(props: SearchBoxProps): JSX.Element {
|
|||
distribution="start"
|
||||
css={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
css={{
|
||||
width: '53px',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
bg: props.multiSelectMode !== 'off' ? '$ctaBlue' : 'transparent',
|
||||
borderTopLeftRadius: '6px',
|
||||
borderBottomLeftRadius: '6px',
|
||||
'--checkbox-color': 'var(--colors-thLibraryMultiselectCheckbox)',
|
||||
'&:hover': {
|
||||
bg: '$thLibraryMultiselectHover',
|
||||
'--checkbox-color':
|
||||
'var(--colors-thLibraryMultiselectCheckboxHover)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CheckBoxButton {...props} />
|
||||
</HStack>
|
||||
{props.folder !== 'trash' && (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
css={{
|
||||
width: '53px',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
bg: props.multiSelectMode !== 'off' ? '$ctaBlue' : 'transparent',
|
||||
borderTopLeftRadius: '6px',
|
||||
borderBottomLeftRadius: '6px',
|
||||
'--checkbox-color': 'var(--colors-thLibraryMultiselectCheckbox)',
|
||||
'&:hover': {
|
||||
bg: '$thLibraryMultiselectHover',
|
||||
'--checkbox-color':
|
||||
'var(--colors-thLibraryMultiselectCheckboxHover)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CheckBoxButton {...props} />
|
||||
</HStack>
|
||||
)}
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
|
|
|
|||
|
|
@ -1,57 +1,4 @@
|
|||
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 { Toaster } from 'react-hot-toast'
|
||||
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 {
|
||||
PageType,
|
||||
State,
|
||||
} from '../../../lib/networking/fragments/articleFragment'
|
||||
import {
|
||||
SearchItem,
|
||||
TypeaheadSearchItemsData,
|
||||
typeaheadSearchQuery,
|
||||
} from '../../../lib/networking/queries/typeaheadSearch'
|
||||
import type {
|
||||
LibraryItem,
|
||||
LibraryItemsQueryInput,
|
||||
} from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { useGetLibraryItemsQuery } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
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'
|
||||
import { LegacyLibraryHeader, MultiSelectMode } from '../homeFeed/LibraryHeader'
|
||||
import { UploadModal } from '../UploadModal'
|
||||
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
|
||||
import { bulkActionMutation } from '../../../lib/networking/mutations/bulkActionMutation'
|
||||
import {
|
||||
showErrorToast,
|
||||
showSuccessToast,
|
||||
showSuccessToastWithAction,
|
||||
} from '../../../lib/toastHelpers'
|
||||
import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter'
|
||||
import { NotebookPresenter } from '../article/NotebookPresenter'
|
||||
import { saveUrlMutation } from '../../../lib/networking/mutations/saveUrlMutation'
|
||||
import { articleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { PinnedButtons } from '../homeFeed/PinnedButtons'
|
||||
import { PinnedSearch } from '../../../pages/settings/pinned-searches'
|
||||
import { FetchItemsError } from '../homeFeed/FetchItemsError'
|
||||
import { LibraryHeader } from './LibraryHeader'
|
||||
import { VStack } from '../../elements/LayoutPrimitives'
|
||||
|
||||
type LibrarySideBarProps = {
|
||||
text: string
|
||||
|
|
|
|||
|
|
@ -8,19 +8,19 @@ import {
|
|||
SubscriptionType,
|
||||
useGetSubscriptionsQuery,
|
||||
} from '../../../lib/networking/queries/useGetSubscriptionsQuery'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
import { LogoBox } from '../../elements/LogoBox'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
import { useGetSavedSearchQuery } from '../../../lib/networking/queries/useGetSavedSearchQuery'
|
||||
import { SavedSearch } from '../../../lib/networking/fragments/savedSearchFragment'
|
||||
import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon'
|
||||
import Link from 'next/link'
|
||||
import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon'
|
||||
import { NavMenuFooter } from './Footer'
|
||||
import { escapeQuotes } from '../../../utils/helper'
|
||||
import { useGetLabels } from '../../../lib/networking/labels/useLabels'
|
||||
import { useGetSavedSearches } from '../../../lib/networking/savedsearches/useSavedSearches'
|
||||
|
||||
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
|
||||
|
||||
|
|
@ -50,17 +50,17 @@ export function LibraryLegacyMenu(props: LibraryFilterMenuProps): JSX.Element {
|
|||
isSessionStorage: false,
|
||||
initialValue: [],
|
||||
})
|
||||
const labelsResponse = useGetLabelsQuery()
|
||||
const searchesResponse = useGetSavedSearchQuery()
|
||||
const labelsResponse = useGetLabels()
|
||||
const searchesResponse = useGetSavedSearches()
|
||||
const subscriptionsResponse = useGetSubscriptionsQuery()
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!labelsResponse.error &&
|
||||
!labelsResponse.isLoading &&
|
||||
labelsResponse.labels
|
||||
labelsResponse.data
|
||||
) {
|
||||
setLabels(labelsResponse.labels)
|
||||
setLabels(labelsResponse.data)
|
||||
}
|
||||
}, [setLabels, labelsResponse])
|
||||
|
||||
|
|
@ -78,9 +78,9 @@ export function LibraryLegacyMenu(props: LibraryFilterMenuProps): JSX.Element {
|
|||
if (
|
||||
!searchesResponse.error &&
|
||||
!searchesResponse.isLoading &&
|
||||
searchesResponse.savedSearches
|
||||
searchesResponse?.data
|
||||
) {
|
||||
setSavedSearches(searchesResponse.savedSearches)
|
||||
setSavedSearches(searchesResponse.data)
|
||||
}
|
||||
}, [setSavedSearches, searchesResponse])
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,12 @@ import { Circle, DotsThree, MagnifyingGlass, X } from '@phosphor-icons/react'
|
|||
import {
|
||||
Subscription,
|
||||
SubscriptionType,
|
||||
useGetSubscriptionsQuery,
|
||||
} from '../../../lib/networking/queries/useGetSubscriptionsQuery'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
import { LogoBox } from '../../elements/LogoBox'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
import { useGetSavedSearchQuery } from '../../../lib/networking/queries/useGetSavedSearchQuery'
|
||||
import { SavedSearch } from '../../../lib/networking/fragments/savedSearchFragment'
|
||||
import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon'
|
||||
import Link from 'next/link'
|
||||
|
|
@ -25,13 +22,13 @@ import { HomeIcon } from '../../elements/icons/HomeIcon'
|
|||
import { LibraryIcon } from '../../elements/icons/LibraryIcon'
|
||||
import { HighlightsIcon } from '../../elements/icons/HighlightsIcon'
|
||||
import { CoverImage } from '../../elements/CoverImage'
|
||||
import { Shortcut } from './NavigationMenu'
|
||||
import { OutlinedLabelChip } from '../../elements/OutlinedLabelChip'
|
||||
import { NewsletterIcon } from '../../elements/icons/NewsletterIcon'
|
||||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
import { useRouter } from 'next/router'
|
||||
import { DiscoverIcon } from '../../elements/icons/DiscoverIcon'
|
||||
import { escapeQuotes } from '../../../utils/helper'
|
||||
import { Shortcut } from '../../../lib/networking/shortcuts/useShortcuts'
|
||||
|
||||
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
|
||||
|
||||
|
|
@ -185,59 +182,6 @@ const Shortcuts = (props: LibraryFilterMenuProps): JSX.Element => {
|
|||
initialValue: [],
|
||||
})
|
||||
|
||||
console.log('got shortcuts: ', shortcuts)
|
||||
|
||||
// const shortcuts: Shortcut[] = [
|
||||
// {
|
||||
// id: '12asdfasdf',
|
||||
// name: 'Omnivore Blog',
|
||||
// icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
|
||||
// filter: 'subscription:"Money Talk"',
|
||||
// type: 'feed',
|
||||
// },
|
||||
// {
|
||||
// id: 'sdfsdfgdsfg',
|
||||
// name: 'Follow the Money | Arne & Harr',
|
||||
// filter: 'subscription:"Money Talk"',
|
||||
// type: 'feed',
|
||||
// },
|
||||
// {
|
||||
// id: 'sdfasdfasdfsdfsdfsgasdfg',
|
||||
// name: 'Andrew Kenneson from Center for the Study of Partisanship and Ideology',
|
||||
// // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
|
||||
// filter: 'in:all label:"Hockey"',
|
||||
// type: 'newsletter',
|
||||
// },
|
||||
// {
|
||||
// id: 'sdfasdfasdfsdfsdfsgasdfg',
|
||||
// name: 'Robert的博客',
|
||||
// // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
|
||||
// filter: 'in:all label:"Hockey"',
|
||||
// type: 'feed',
|
||||
// },
|
||||
// {
|
||||
// id: 'sdfasdfasdfasdfasf',
|
||||
// name: 'Oldest First',
|
||||
// // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
|
||||
// filter: 'in:all label:"Hockey"',
|
||||
// type: 'search',
|
||||
// },
|
||||
// {
|
||||
// id: 'sdfasdfasdfgasdfg',
|
||||
// name: 'Hockey',
|
||||
// // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
|
||||
// filter: 'in:all label:"Hockey"',
|
||||
// type: 'label',
|
||||
// label: {
|
||||
// id: 'sdfsdfsdf',
|
||||
// name: 'Hockey',
|
||||
// color: '#E98B8B',
|
||||
// createdAt: new Date(),
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
//
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
|
|
|
|||
|
|
@ -1,16 +1,8 @@
|
|||
import {
|
||||
CSSProperties,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { ReactNode, useCallback, useRef, useState } from 'react'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { DotsThree, List, X, Tag } from '@phosphor-icons/react'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { DotsThree } from '@phosphor-icons/react'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
import { NavMenuFooter } from './Footer'
|
||||
|
|
@ -18,49 +10,23 @@ import { FollowingIcon } from '../../elements/icons/FollowingIcon'
|
|||
import { HomeIcon } from '../../elements/icons/HomeIcon'
|
||||
import { LibraryIcon } from '../../elements/icons/LibraryIcon'
|
||||
import { HighlightsIcon } from '../../elements/icons/HighlightsIcon'
|
||||
import { CoverImage } from '../../elements/CoverImage'
|
||||
import { NewsletterIcon } from '../../elements/icons/NewsletterIcon'
|
||||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
import { useRouter } from 'next/router'
|
||||
import { NavigationSection } from '../NavigationLayout'
|
||||
import { NodeApi, SimpleTree, Tree, TreeApi } from 'react-arborist'
|
||||
import { ListMagnifyingGlass } from '@phosphor-icons/react'
|
||||
import { TreeApi } from 'react-arborist'
|
||||
import React from 'react'
|
||||
import useSWR from 'swr'
|
||||
import useSWRMutation from 'swr/mutation'
|
||||
import { fetchEndpoint } from '../../../lib/appConfig'
|
||||
import { requestHeaders } from '../../../lib/networking/networkHelpers'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { showErrorToast } from '../../../lib/toastHelpers'
|
||||
import { OpenMap } from 'react-arborist/dist/module/state/open-slice'
|
||||
import { ArchiveSectionIcon } from '../../elements/icons/ArchiveSectionIcon'
|
||||
import { NavMoreButtonDownIcon } from '../../elements/icons/NavMoreButtonDown'
|
||||
import { NavMoreButtonUpIcon } from '../../elements/icons/NavMoreButtonUp'
|
||||
import { ShortcutFolderClosed } from '../../elements/icons/ShortcutFolderClosed'
|
||||
import { TrashSectionIcon } from '../../elements/icons/TrashSectionIcon'
|
||||
import { ShortcutFolderOpen } from '../../elements/icons/ShortcutFolderOpen'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
import {
|
||||
Shortcut,
|
||||
useResetShortcuts,
|
||||
} from '../../../lib/networking/shortcuts/useShortcuts'
|
||||
import { ShortcutsTree } from '../ShortcutsTree'
|
||||
|
||||
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
|
||||
|
||||
export type ShortcutType = 'search' | 'label' | 'newsletter' | 'feed' | 'folder'
|
||||
|
||||
export type Shortcut = {
|
||||
type: ShortcutType
|
||||
|
||||
id: string
|
||||
name: string
|
||||
section: string
|
||||
filter: string
|
||||
|
||||
icon?: string
|
||||
label?: Label
|
||||
|
||||
join?: string
|
||||
|
||||
children?: Shortcut[]
|
||||
}
|
||||
|
||||
type NavigationMenuProps = {
|
||||
section: NavigationSection
|
||||
|
||||
|
|
@ -101,12 +67,12 @@ export function NavigationMenu(props: NavigationMenuProps): JSX.Element {
|
|||
}}
|
||||
onClick={(event) => {
|
||||
// on small screens we want to dismiss the menu after click
|
||||
if (window.innerWidth <= 768) {
|
||||
setDismissed(true)
|
||||
setTimeout(() => {
|
||||
props.setShowMenu(false)
|
||||
}, 100)
|
||||
}
|
||||
// if (window.innerWidth <= 768) {
|
||||
// setDismissed(true)
|
||||
// setTimeout(() => {
|
||||
// props.setShowMenu(false)
|
||||
// }, 100)
|
||||
// }
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
|
|
@ -217,6 +183,7 @@ const LibraryNav = (props: NavigationMenuProps): JSX.Element => {
|
|||
onClick={(event) => {
|
||||
setMoreFolderSectionOpen(!moreFolderSectionOpen)
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
|
|
@ -267,11 +234,9 @@ const LibraryNav = (props: NavigationMenuProps): JSX.Element => {
|
|||
}
|
||||
|
||||
const Shortcuts = (props: NavigationMenuProps): JSX.Element => {
|
||||
const router = useRouter()
|
||||
const treeRef = useRef<TreeApi<Shortcut> | undefined>(undefined)
|
||||
const { trigger: resetShortcutsTrigger } = useSWRMutation(
|
||||
'/api/shortcuts',
|
||||
resetShortcuts
|
||||
)
|
||||
const resetShortcuts = useResetShortcuts()
|
||||
|
||||
const createNewFolder = useCallback(async () => {
|
||||
if (treeRef.current) {
|
||||
|
|
@ -283,9 +248,7 @@ const Shortcuts = (props: NavigationMenuProps): JSX.Element => {
|
|||
}, [treeRef])
|
||||
|
||||
const resetShortcutsToDefault = useCallback(async () => {
|
||||
resetShortcutsTrigger(null, {
|
||||
revalidate: true,
|
||||
})
|
||||
await resetShortcuts.mutateAsync()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
|
@ -323,6 +286,12 @@ const Shortcuts = (props: NavigationMenuProps): JSX.Element => {
|
|||
triggerElement={<DotsThree size={20} />}
|
||||
css={{ ml: 'auto' }}
|
||||
>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
router.push(`/settings/shortcuts`)
|
||||
}}
|
||||
title="Edit shortcuts"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={resetShortcutsToDefault}
|
||||
title="Reset to default"
|
||||
|
|
@ -345,6 +314,10 @@ const Shortcuts = (props: NavigationMenuProps): JSX.Element => {
|
|||
outline: 'none',
|
||||
},
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<ShortcutsTree treeRef={treeRef} />
|
||||
</Box>
|
||||
|
|
@ -352,525 +325,6 @@ const Shortcuts = (props: NavigationMenuProps): JSX.Element => {
|
|||
)
|
||||
}
|
||||
|
||||
type ShortcutsTreeProps = {
|
||||
treeRef: React.MutableRefObject<TreeApi<Shortcut> | undefined>
|
||||
}
|
||||
|
||||
async function getShortcuts(path: string): Promise<Shortcut[]> {
|
||||
const url = new URL(path, fetchEndpoint)
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: requestHeaders(),
|
||||
credentials: 'include',
|
||||
mode: 'cors',
|
||||
})
|
||||
const payload = await response.json()
|
||||
if ('shortcuts' in payload) {
|
||||
return payload['shortcuts'] as Shortcut[]
|
||||
}
|
||||
return []
|
||||
} catch (err) {
|
||||
console.log('error getting shortcuts: ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function setShortcuts(
|
||||
path: string,
|
||||
{ arg }: { arg: { shortcuts: Shortcut[] } }
|
||||
): Promise<Shortcut[]> {
|
||||
const url = new URL(path, fetchEndpoint)
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...requestHeaders(),
|
||||
},
|
||||
credentials: 'include',
|
||||
mode: 'cors',
|
||||
body: JSON.stringify(arg),
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!('shortcuts' in payload)) {
|
||||
throw new Error('Error syncing shortcuts')
|
||||
}
|
||||
return payload['shortcuts'] as Shortcut[]
|
||||
} catch (err) {
|
||||
showErrorToast('Error syncing shortcut changes.')
|
||||
}
|
||||
return arg.shortcuts
|
||||
}
|
||||
|
||||
async function resetShortcuts(path: string): Promise<Shortcut[]> {
|
||||
const url = new URL(path, fetchEndpoint)
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...requestHeaders(),
|
||||
},
|
||||
credentials: 'include',
|
||||
mode: 'cors',
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!('shortcuts' in payload)) {
|
||||
throw new Error('Error syncing shortcuts')
|
||||
}
|
||||
return payload['shortcuts'] as Shortcut[]
|
||||
} catch (err) {
|
||||
showErrorToast('Error syncing shortcut changes.')
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const cachedShortcutsData = (): Shortcut[] | undefined => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const str = localStorage.getItem('/api/shortcuts')
|
||||
if (str) {
|
||||
return JSON.parse(str) as Shortcut[]
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const ShortcutsTree = (props: ShortcutsTreeProps): JSX.Element => {
|
||||
const router = useRouter()
|
||||
const { ref, width, height } = useResizeObserver()
|
||||
|
||||
const { isValidating, data } = useSWR('/api/shortcuts', getShortcuts, {
|
||||
revalidateOnFocus: false,
|
||||
fallbackData: cachedShortcutsData(),
|
||||
onSuccess(data) {
|
||||
localStorage.setItem('/api/shortcuts', JSON.stringify(data))
|
||||
},
|
||||
})
|
||||
const { trigger, isMutating } = useSWRMutation('/api/shortcuts', setShortcuts)
|
||||
const [folderOpenState, setFolderOpenState] = usePersistedState<
|
||||
Record<string, boolean>
|
||||
>({
|
||||
key: 'nav-menu-open-state',
|
||||
isSessionStorage: false,
|
||||
initialValue: {},
|
||||
})
|
||||
const tree = useMemo(() => {
|
||||
const result = new SimpleTree<Shortcut>((data ?? []) as Shortcut[])
|
||||
return result
|
||||
}, [data])
|
||||
|
||||
const syncTreeData = (data: Shortcut[]) => {
|
||||
trigger(
|
||||
{ shortcuts: data },
|
||||
{
|
||||
optimisticData: data,
|
||||
rollbackOnError: true,
|
||||
populateCache: (updatedShortcuts) => {
|
||||
return updatedShortcuts
|
||||
},
|
||||
revalidate: false,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const onMove = useCallback(
|
||||
(args: { dragIds: string[]; parentId: null | string; index: number }) => {
|
||||
for (const id of args.dragIds) {
|
||||
tree?.move({ id, parentId: args.parentId, index: args.index })
|
||||
}
|
||||
syncTreeData(tree.data)
|
||||
},
|
||||
[tree, data]
|
||||
)
|
||||
|
||||
const onCreate = useCallback(
|
||||
(args: { parentId: string | null; index: number; type: string }) => {
|
||||
const data = { id: uuidv4(), name: '', type: 'folder' } as any
|
||||
if (args.type === 'internal') {
|
||||
data.children = []
|
||||
}
|
||||
tree.create({ parentId: args.parentId, index: args.index, data })
|
||||
syncTreeData(tree.data)
|
||||
return data
|
||||
},
|
||||
[tree, data]
|
||||
)
|
||||
|
||||
const onDelete = useCallback(
|
||||
(args: { ids: string[] }) => {
|
||||
args.ids.forEach((id) => tree.drop({ id }))
|
||||
syncTreeData(tree.data)
|
||||
},
|
||||
[tree, data]
|
||||
)
|
||||
|
||||
const onRename = useCallback(
|
||||
(args: { name: string; id: string }) => {
|
||||
tree.update({ id: args.id, changes: { name: args.name } as any })
|
||||
syncTreeData(tree.data)
|
||||
},
|
||||
[tree, data]
|
||||
)
|
||||
|
||||
const onToggle = useCallback(
|
||||
(id: string) => {
|
||||
if (id && props.treeRef.current) {
|
||||
const isOpen = props.treeRef.current?.isOpen(id)
|
||||
const newItem: OpenMap = {}
|
||||
newItem[id] = isOpen
|
||||
setFolderOpenState({ ...folderOpenState, ...newItem })
|
||||
}
|
||||
},
|
||||
[props, folderOpenState, setFolderOpenState]
|
||||
)
|
||||
|
||||
const onActivate = useCallback(
|
||||
(node: NodeApi<Shortcut>) => {
|
||||
if (node.data.type == 'folder') {
|
||||
const join = node.data.join
|
||||
if (join == 'or') {
|
||||
const query = node.children
|
||||
?.map((child) => {
|
||||
return `(${child.data.filter})`
|
||||
})
|
||||
.join(' OR ')
|
||||
}
|
||||
} else if (node.data.section != null && node.data.filter != null) {
|
||||
router.push(`/l/${node.data.section}?q=${node.data.filter}`)
|
||||
}
|
||||
},
|
||||
[tree, router]
|
||||
)
|
||||
|
||||
function countTotalShortcuts(shortcuts: Shortcut[]): number {
|
||||
let total = 0
|
||||
|
||||
for (const shortcut of shortcuts) {
|
||||
// Count the current shortcut
|
||||
total++
|
||||
|
||||
// If the shortcut has children, recursively count them
|
||||
if (shortcut.children && shortcut.children.length > 0) {
|
||||
total += countTotalShortcuts(shortcut.children)
|
||||
}
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
const maximumHeight = useMemo(() => {
|
||||
if (!data) {
|
||||
return 320
|
||||
}
|
||||
return countTotalShortcuts(data as Shortcut[]) * 36
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
css={{
|
||||
height: maximumHeight,
|
||||
flexGrow: 1,
|
||||
minBlockSize: 0,
|
||||
}}
|
||||
>
|
||||
{!isValidating && (
|
||||
<Tree
|
||||
ref={props.treeRef}
|
||||
data={data as Shortcut[]}
|
||||
onCreate={onCreate}
|
||||
onMove={onMove}
|
||||
onDelete={onDelete}
|
||||
onRename={onRename}
|
||||
onToggle={onToggle}
|
||||
onActivate={onActivate}
|
||||
rowHeight={36}
|
||||
initialOpenState={folderOpenState}
|
||||
width={width}
|
||||
height={maximumHeight}
|
||||
>
|
||||
{NodeRenderer}
|
||||
</Tree>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function NodeRenderer(args: {
|
||||
style: CSSProperties
|
||||
node: NodeApi<Shortcut>
|
||||
tree: TreeApi<Shortcut>
|
||||
dragHandle?: (el: HTMLDivElement | null) => void
|
||||
preview?: boolean
|
||||
}) {
|
||||
const isSelected = false
|
||||
const [menuVisible, setMenuVisible] = useState(false)
|
||||
const [menuOpened, setMenuOpened] = useState(false)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<HStack
|
||||
ref={args.dragHandle}
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
pl: `${20 + args.node.level * 15}px`,
|
||||
mb: '2px',
|
||||
gap: '10px',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
height: '34px',
|
||||
|
||||
backgroundColor: isSelected ? '$thLibrarySelectionColor' : 'unset',
|
||||
fontSize: '15px',
|
||||
fontWeight: 'regular',
|
||||
fontFamily: '$display',
|
||||
color: isSelected
|
||||
? '$thLibraryMenuSecondary'
|
||||
: '$thLibraryMenuUnselected',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
'&:hover': {
|
||||
backgroundColor: isSelected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
'&:active': {
|
||||
outline: 'unset',
|
||||
backgroundColor: isSelected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
'&:hover [role="hover-menu"]': {
|
||||
opacity: '1',
|
||||
},
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setMenuVisible(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setMenuVisible(false)
|
||||
}}
|
||||
title={args.node.data.name}
|
||||
onClick={(e) => {
|
||||
// router.push(`/` + props.section)
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
>
|
||||
<NodeItemContents node={args.node} />
|
||||
<SpanBox
|
||||
role="hover-menu"
|
||||
css={{
|
||||
display: 'flex',
|
||||
ml: 'auto',
|
||||
mr: '15px',
|
||||
opacity: menuVisible || menuOpened ? '1' : '0',
|
||||
}}
|
||||
>
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
triggerElement={<DotsThree size={20} />}
|
||||
css={{ ml: 'auto' }}
|
||||
onOpenChange={(open) => {
|
||||
setMenuOpened(open)
|
||||
}}
|
||||
>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
args.tree.delete(args.node)
|
||||
}}
|
||||
title="Remove"
|
||||
/>
|
||||
{/* {args.node.data.type == 'folder' && (
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
args.node.data.join = 'or'
|
||||
}}
|
||||
title="Folder query: OR"
|
||||
/>
|
||||
)} */}
|
||||
</Dropdown>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type NodeItemContentsProps = {
|
||||
node: NodeApi<Shortcut>
|
||||
}
|
||||
|
||||
const NodeItemContents = (props: NodeItemContentsProps): JSX.Element => {
|
||||
if (props.node.isEditing) {
|
||||
return (
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
defaultValue={props.node.data.name}
|
||||
onFocus={(e) => e.currentTarget.select()}
|
||||
onBlur={() => props.node.reset()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
props.node.reset()
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
// props.node.data = {
|
||||
// id: 'new-folder',
|
||||
// type: 'folder',
|
||||
// name: e.currentTarget.value,
|
||||
// }
|
||||
props.node.submit(e.currentTarget.value)
|
||||
props.node.activate()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (props.node.isLeaf) {
|
||||
const shortcut = props.node.data
|
||||
if (shortcut) {
|
||||
switch (shortcut.type) {
|
||||
case 'feed':
|
||||
case 'newsletter':
|
||||
return (
|
||||
<SpanBox>
|
||||
<FeedOrNewsletterShortcut shortcut={shortcut} />
|
||||
</SpanBox>
|
||||
)
|
||||
case 'label':
|
||||
return (
|
||||
<Box>
|
||||
<LabelShortcut shortcut={shortcut} />
|
||||
</Box>
|
||||
)
|
||||
case 'search':
|
||||
return (
|
||||
<Box>
|
||||
<SearchShortcut shortcut={shortcut} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ gap: '10px', width: '100%' }}
|
||||
onClick={(event) => {
|
||||
props.node.toggle()
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{props.node.isClosed ? (
|
||||
<ShortcutFolderClosed
|
||||
color={theme.colors.thLibraryMenuPrimary.toString()}
|
||||
/>
|
||||
) : (
|
||||
<ShortcutFolderOpen
|
||||
color={theme.colors.thLibraryMenuPrimary.toString()}
|
||||
/>
|
||||
)}
|
||||
{props.node.data.name}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
|
||||
type ShortcutItemProps = {
|
||||
shortcut: Shortcut
|
||||
}
|
||||
|
||||
const FeedOrNewsletterShortcut = (props: ShortcutItemProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ pl: '10px', width: '100%', gap: '10px' }}
|
||||
key={`search-${props.shortcut.id}`}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ minWidth: '20px' }}
|
||||
>
|
||||
{props.shortcut.icon ? (
|
||||
<CoverImage
|
||||
src={props.shortcut.icon}
|
||||
width={20}
|
||||
height={20}
|
||||
css={{ borderRadius: '20px' }}
|
||||
/>
|
||||
) : props.shortcut.type == 'newsletter' ? (
|
||||
<NewsletterIcon color="#F59932" size={18} />
|
||||
) : (
|
||||
<FollowingIcon color="#F59932" size={21} />
|
||||
)}
|
||||
</HStack>
|
||||
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const SearchShortcut = (props: ShortcutItemProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ pl: '10px', width: '100%', gap: '7px' }}
|
||||
key={`search-${props.shortcut.id}`}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ minWidth: '20px' }}
|
||||
>
|
||||
<ListMagnifyingGlass size={17} />
|
||||
</HStack>
|
||||
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const LabelShortcut = (props: ShortcutItemProps): JSX.Element => {
|
||||
// <OutlinedLabelChip
|
||||
// text={props.shortcut.name}
|
||||
// color={props.shortcut.label?.color ?? 'gray'}
|
||||
// />
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ width: '100%', gap: '7px' }}
|
||||
key={`search-${props.shortcut.id}`}
|
||||
>
|
||||
<Tag
|
||||
size={15}
|
||||
color={props.shortcut.label?.color ?? 'gray'}
|
||||
weight="fill"
|
||||
/>
|
||||
<StyledText style="settingsItem" css={{ pb: '1px' }}>
|
||||
{props.shortcut.name}
|
||||
</StyledText>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type NavButtonProps = {
|
||||
text: string
|
||||
icon: ReactNode
|
||||
|
|
@ -925,8 +379,7 @@ function NavButton(props: NavButtonProps): JSX.Element {
|
|||
}}
|
||||
title={props.text}
|
||||
onClick={(e) => {
|
||||
// console.log('clicked navigation menu')
|
||||
router.push(`/l/` + props.section)
|
||||
router.push(`/` + props.section)
|
||||
}}
|
||||
>
|
||||
{props.icon}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
|||
import { StyledText } from '../../elements/StyledText'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { SettingsLayout } from '../SettingsLayout'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
import { FeatureHelpBox } from '../../elements/FeatureHelpBox'
|
||||
|
||||
// Styles
|
||||
export const Header = styled(Box, {
|
||||
|
|
@ -26,8 +24,6 @@ type SettingsTableProps = {
|
|||
createTitle?: string
|
||||
createAction?: () => void
|
||||
|
||||
suggestionInfo: SuggestionInfo
|
||||
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
|
|
@ -63,16 +59,6 @@ type MoreOptionsProps = {
|
|||
onEdit?: () => void
|
||||
}
|
||||
|
||||
type SuggestionInfo = {
|
||||
title: string
|
||||
message: string
|
||||
docs: string
|
||||
key: string
|
||||
|
||||
CTAText?: string
|
||||
onClickCTA?: () => void
|
||||
}
|
||||
|
||||
const MoreOptions = (props: MoreOptionsProps) => (
|
||||
<Dropdown
|
||||
align={'end'}
|
||||
|
|
@ -294,11 +280,6 @@ const CreateButton = (props: CreateButtonProps): JSX.Element => {
|
|||
}
|
||||
|
||||
export const SettingsTable = (props: SettingsTableProps): JSX.Element => {
|
||||
const [showSuggestion, setShowSuggestion] = usePersistedState<boolean>({
|
||||
key: props.suggestionInfo.key,
|
||||
initialValue: !!props.suggestionInfo,
|
||||
})
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<Toaster
|
||||
|
|
@ -327,19 +308,6 @@ export const SettingsTable = (props: SettingsTableProps): JSX.Element => {
|
|||
},
|
||||
}}
|
||||
>
|
||||
{props.suggestionInfo && showSuggestion && (
|
||||
<FeatureHelpBox
|
||||
helpTitle={props.suggestionInfo.title}
|
||||
helpMessage={props.suggestionInfo.message}
|
||||
docsMessage={'Read the Docs'}
|
||||
docsDestination={props.suggestionInfo.docs}
|
||||
onDismiss={() => {
|
||||
setShowSuggestion(false)
|
||||
}}
|
||||
helpCTAText={props.suggestionInfo.CTAText}
|
||||
onClickCTA={props.suggestionInfo.onClickCTA}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
|
|
|
|||
|
|
@ -453,10 +453,10 @@ const sepiaThemeSpec = {
|
|||
readerFontHighContrast: '#0A0806',
|
||||
readerTableHeader: '#FFFFFF',
|
||||
|
||||
thLeftMenuBackground: '#EEE8D5',
|
||||
thNavMenuFooter: '#DDD6C1',
|
||||
thLeftMenuBackground: '#F8F1E0',
|
||||
thNavMenuFooter: '#EEE8D5',
|
||||
|
||||
thLibrarySelectionColor: '#DDD6C1',
|
||||
thLibrarySelectionColor: '#EEE8D5',
|
||||
thLabelChipBackground: '#EEE8D5',
|
||||
thBackground4: '#DDD6C166', // used on hover of menu items
|
||||
thBorderColor: '#DDD6C1',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Highlight } from './networking/fragments/highlightFragment'
|
||||
import { ArticleReadingProgressMutationInput } from './networking/mutations/articleReadingProgressMutation'
|
||||
import { CreateHighlightInput } from './networking/mutations/createHighlightMutation'
|
||||
import { MergeHighlightInput } from './networking/mutations/mergeHighlightMutation'
|
||||
import { UpdateHighlightInput } from './networking/mutations/updateHighlightMutation'
|
||||
import { CreateHighlightInput } from './networking/highlights/useItemHighlights'
|
||||
|
||||
export type ArticleMutations = {
|
||||
createHighlightMutation: (
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export async function createHighlight(
|
|||
if (!input.selection.selection) {
|
||||
return {}
|
||||
}
|
||||
|
||||
console.log(' overlapping: ', input.selection.overlapHighlights)
|
||||
const shouldMerge = input.selection.overlapHighlights.length > 0
|
||||
|
||||
const { range, selection } = input.selection
|
||||
|
|
|
|||
|
|
@ -8,19 +8,20 @@ import type { SelectionAttributes } from './highlightHelpers'
|
|||
|
||||
/**
|
||||
* Get the range of text with {@link SelectionAttributes} that user has selected
|
||||
*
|
||||
*
|
||||
* Event Handlers for detecting/using new highlight selection are registered
|
||||
*
|
||||
*
|
||||
* If the new highlight selection overlaps with existing highlights, the new selection is merged.
|
||||
*
|
||||
*
|
||||
* @param highlightLocations existing highlights
|
||||
* @returns selection range and its setter
|
||||
*/
|
||||
export function useSelection(
|
||||
highlightLocations: HighlightLocation[]
|
||||
): [SelectionAttributes | null, (x: SelectionAttributes | null) => void] {
|
||||
const [touchStartPos, setTouchStartPos] =
|
||||
useState<{ x: number; y: number } | undefined>(undefined)
|
||||
const [touchStartPos, setTouchStartPos] = useState<
|
||||
{ x: number; y: number } | undefined
|
||||
>(undefined)
|
||||
const [selectionAttributes, setSelectionAttributes] =
|
||||
useState<SelectionAttributes | null>(null)
|
||||
|
||||
|
|
@ -246,32 +247,38 @@ async function makeSelectionRange(): Promise<
|
|||
|
||||
/**
|
||||
* Edge case:
|
||||
* If the selection ends on range endContainer (or startContainer in reverse select) but no text is selected (i.e. selection ends at
|
||||
* an empty area), the preceding text is highlighted due to range normalizing.
|
||||
* If the selection ends on range endContainer (or startContainer in reverse select) but no text is selected (i.e. selection ends at
|
||||
* an empty area), the preceding text is highlighted due to range normalizing.
|
||||
* This is a visual bug and would sometimes lead to weird highlight behavior during removal.
|
||||
*/
|
||||
const selectionEndNode = selection.focusNode
|
||||
const selectionEndOffset = selection.focusOffset
|
||||
const selectionStartNode = isReverseSelected ? range.endContainer : range.startContainer
|
||||
const selectionStartNode = isReverseSelected
|
||||
? range.endContainer
|
||||
: range.startContainer
|
||||
|
||||
if (selectionEndNode?.nodeType === Node.TEXT_NODE) {
|
||||
const selectionEndNodeEdgeIndex = isReverseSelected ? selectionEndNode.textContent?.length : 0
|
||||
const selectionEndNodeEdgeIndex = isReverseSelected
|
||||
? selectionEndNode.textContent?.length
|
||||
: 0
|
||||
|
||||
if (selectionStartNode !== selectionEndNode &&
|
||||
selectionEndOffset == selectionEndNodeEdgeIndex) {
|
||||
clipRangeToNearestAnchor(range, selectionEndNode, isReverseSelected)
|
||||
if (
|
||||
selectionStartNode !== selectionEndNode &&
|
||||
selectionEndOffset == selectionEndNodeEdgeIndex
|
||||
) {
|
||||
clipRangeToNearestAnchor(range, selectionEndNode, isReverseSelected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return isRangeAllowed ? { range, isReverseSelected, selection } : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Clip selection range to the beginning/end of the adjacent anchor element
|
||||
*
|
||||
*
|
||||
* @param range selection range
|
||||
* @param selectionEndNode the node where the selection ended at
|
||||
* @param isReverseSelected
|
||||
* @param isReverseSelected
|
||||
*/
|
||||
const clipRangeToNearestAnchor = (
|
||||
range: Range,
|
||||
|
|
@ -279,30 +286,44 @@ const clipRangeToNearestAnchor = (
|
|||
isReverseSelected: boolean
|
||||
) => {
|
||||
let nearestAnchorElement = selectionEndNode.parentElement
|
||||
while (nearestAnchorElement !== null && !nearestAnchorElement.hasAttribute('data-omnivore-anchor-idx')) {
|
||||
nearestAnchorElement = nearestAnchorElement.parentElement;
|
||||
while (
|
||||
nearestAnchorElement !== null &&
|
||||
!nearestAnchorElement.hasAttribute('data-omnivore-anchor-idx')
|
||||
) {
|
||||
nearestAnchorElement = nearestAnchorElement.parentElement
|
||||
}
|
||||
if (!nearestAnchorElement) {
|
||||
throw Error('Unable to find nearest anchor element for node: ' + selectionEndNode)
|
||||
throw Error(
|
||||
'Unable to find nearest anchor element for node: ' + selectionEndNode
|
||||
)
|
||||
}
|
||||
let anchorId = Number(nearestAnchorElement.getAttribute('data-omnivore-anchor-idx')!)
|
||||
let anchorId = Number(
|
||||
nearestAnchorElement.getAttribute('data-omnivore-anchor-idx')!
|
||||
)
|
||||
let adjacentAnchorId, adjacentAnchor, adjacentAnchorOffset
|
||||
if (isReverseSelected) {
|
||||
// move down to find adjacent anchor node and clip at its beginning
|
||||
adjacentAnchorId = anchorId + 1
|
||||
adjacentAnchor = document.querySelectorAll(`[data-omnivore-anchor-idx='${adjacentAnchorId}']`)[0]
|
||||
adjacentAnchor = document.querySelectorAll(
|
||||
`[data-omnivore-anchor-idx='${adjacentAnchorId}']`
|
||||
)[0]
|
||||
adjacentAnchorOffset = 0
|
||||
range.setStart(adjacentAnchor, adjacentAnchorOffset)
|
||||
} else {
|
||||
// move up to find adjacent anchor node and clip at its end
|
||||
do {
|
||||
adjacentAnchorId = --anchorId
|
||||
adjacentAnchor = document.querySelectorAll(`[data-omnivore-anchor-idx='${adjacentAnchorId}']`)[0]
|
||||
adjacentAnchor = document.querySelectorAll(
|
||||
`[data-omnivore-anchor-idx='${adjacentAnchorId}']`
|
||||
)[0]
|
||||
} while (adjacentAnchor.contains(selectionEndNode))
|
||||
if (adjacentAnchor.textContent) {
|
||||
let lastTextNodeChild = adjacentAnchor.lastChild
|
||||
while (!!lastTextNodeChild && lastTextNodeChild.nodeType !== Node.TEXT_NODE) {
|
||||
lastTextNodeChild = lastTextNodeChild.previousSibling;
|
||||
while (
|
||||
!!lastTextNodeChild &&
|
||||
lastTextNodeChild.nodeType !== Node.TEXT_NODE
|
||||
) {
|
||||
lastTextNodeChild = lastTextNodeChild.previousSibling
|
||||
}
|
||||
adjacentAnchor = lastTextNodeChild
|
||||
adjacentAnchorOffset = adjacentAnchor?.nodeValue?.length ?? 0
|
||||
|
|
@ -326,7 +347,7 @@ export type RangeEndPos = {
|
|||
|
||||
/**
|
||||
* Return coordinates of the screen area occupied by the last line of user selection
|
||||
*
|
||||
*
|
||||
* @param range range of user selection
|
||||
* @param getFirst whether to get first line of user selection. Get last if false (default)
|
||||
* @returns {RangeEndPos} selection coordinates
|
||||
|
|
|
|||
26
packages/web/lib/hooks/useHandleAddUrl.ts
Normal file
26
packages/web/lib/hooks/useHandleAddUrl.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { useCallback } from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useAddItem } from '../networking/library_items/useLibraryItems'
|
||||
import { showErrorToast, showSuccessToastWithAction } from '../toastHelpers'
|
||||
|
||||
export const useHandleAddUrl = () => {
|
||||
const addItem = useAddItem()
|
||||
return useCallback(async (url: string, timezone: string, locale: string) => {
|
||||
const itemId = uuidv4()
|
||||
const result = await addItem.mutateAsync({
|
||||
itemId,
|
||||
url,
|
||||
timezone,
|
||||
locale,
|
||||
})
|
||||
console.log('result: ', result)
|
||||
if (result) {
|
||||
showSuccessToastWithAction('Item saving', 'Read now', async () => {
|
||||
window.location.href = `/article?url=${encodeURIComponent(url)}`
|
||||
return Promise.resolve()
|
||||
})
|
||||
} else {
|
||||
showErrorToast('Error saving url', { position: 'bottom-right' })
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
|
@ -1,20 +1,30 @@
|
|||
import { useCallback } from 'react'
|
||||
import { setLinkArchivedMutation } from '../networking/mutations/setLinkArchivedMutation'
|
||||
import {
|
||||
showErrorToast,
|
||||
showSuccessToast,
|
||||
showSuccessToastWithUndo,
|
||||
} from '../toastHelpers'
|
||||
import { deleteLinkMutation } from '../networking/mutations/deleteLinkMutation'
|
||||
import { updatePageMutation } from '../networking/mutations/updatePageMutation'
|
||||
import { State } from '../networking/fragments/articleFragment'
|
||||
import { moveToFolderMutation } from '../networking/mutations/moveToLibraryMutation'
|
||||
import {
|
||||
useArchiveItem,
|
||||
useDeleteItem,
|
||||
useMoveItemToFolder,
|
||||
useRestoreItem,
|
||||
} from '../networking/library_items/useLibraryItems'
|
||||
|
||||
export default function useLibraryItemActions() {
|
||||
const archiveItem = useCallback(async (itemId: string) => {
|
||||
const result = await setLinkArchivedMutation({
|
||||
linkId: itemId,
|
||||
archived: true,
|
||||
const archiveItem = useArchiveItem()
|
||||
const deleteItem = useDeleteItem()
|
||||
const moveItem = useMoveItemToFolder()
|
||||
const restoreItem = useRestoreItem()
|
||||
|
||||
const doArchiveItem = useCallback(async (itemId: string, slug: string) => {
|
||||
const result = await archiveItem.mutateAsync({
|
||||
itemId: itemId,
|
||||
slug: slug,
|
||||
input: {
|
||||
linkId: itemId,
|
||||
archived: true,
|
||||
},
|
||||
})
|
||||
|
||||
console.log('result: ', result)
|
||||
|
|
@ -27,33 +37,37 @@ export default function useLibraryItemActions() {
|
|||
return !!result
|
||||
}, [])
|
||||
|
||||
const deleteItem = useCallback(async (itemId: string, undo: () => void) => {
|
||||
const result = await deleteLinkMutation(itemId)
|
||||
const doDeleteItem = useCallback(
|
||||
async (itemId: string, slug: string, undo: () => void) => {
|
||||
const result = await deleteItem.mutateAsync({ itemId, slug })
|
||||
|
||||
if (result) {
|
||||
showSuccessToastWithUndo('Item removed', async () => {
|
||||
const result = await updatePageMutation({
|
||||
pageId: itemId,
|
||||
state: State.SUCCEEDED,
|
||||
if (result) {
|
||||
showSuccessToastWithUndo('Item removed', async () => {
|
||||
const result = await restoreItem.mutateAsync({ itemId, slug })
|
||||
|
||||
undo()
|
||||
|
||||
if (result) {
|
||||
showSuccessToast('Item recovered')
|
||||
} else {
|
||||
showErrorToast('Error recovering, check your deleted items')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
showErrorToast('Error removing item', { position: 'bottom-right' })
|
||||
}
|
||||
|
||||
undo()
|
||||
return !!result
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
if (result) {
|
||||
showSuccessToast('Item recovered')
|
||||
} else {
|
||||
showErrorToast('Error recovering, check your deleted items')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
showErrorToast('Error removing item', { position: 'bottom-right' })
|
||||
}
|
||||
|
||||
return !!result
|
||||
}, [])
|
||||
|
||||
const moveItem = useCallback(async (itemId: string) => {
|
||||
const result = await moveToFolderMutation(itemId, 'inbox')
|
||||
const doMoveItem = useCallback(async (itemId: string, slug: string) => {
|
||||
const result = await moveItem.mutateAsync({
|
||||
itemId,
|
||||
slug,
|
||||
folder: 'inbox',
|
||||
})
|
||||
if (result) {
|
||||
showSuccessToast('Moved to library', { position: 'bottom-right' })
|
||||
} else {
|
||||
|
|
@ -85,5 +99,10 @@ export default function useLibraryItemActions() {
|
|||
[]
|
||||
)
|
||||
|
||||
return { archiveItem, deleteItem, moveItem, shareItem }
|
||||
return {
|
||||
archiveItem: doArchiveItem,
|
||||
deleteItem: doDeleteItem,
|
||||
moveItem: doMoveItem,
|
||||
shareItem,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useRegisterActions } from 'kbar'
|
|||
import { useCallback, useState } from 'react'
|
||||
import { applyStoredTheme } from '../themeUpdater'
|
||||
import { usePersistedState } from './usePersistedState'
|
||||
import { TextDirection } from '../networking/queries/useGetArticleQuery'
|
||||
import { TextDirection } from '../networking/library_items/useLibraryItems'
|
||||
|
||||
const DEFAULT_FONT = 'Inter'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useCallback, useEffect, useReducer } from 'react'
|
||||
import { setLabelsMutation } from '../networking/mutations/setLabelsMutation'
|
||||
import { Label } from '../networking/fragments/labelFragment'
|
||||
import { showErrorToast } from '../toastHelpers'
|
||||
import throttle from 'lodash/throttle'
|
||||
import { useSetItemLabels } from '../networking/library_items/useLibraryItems'
|
||||
|
||||
export type LabelAction = 'RESET' | 'TEMP' | 'SAVE'
|
||||
export type LabelsDispatcher = (action: {
|
||||
|
|
@ -11,13 +11,22 @@ export type LabelsDispatcher = (action: {
|
|||
}) => void
|
||||
|
||||
export const useSetPageLabels = (
|
||||
articleId?: string
|
||||
libraryItemId?: string,
|
||||
libraryItemSlug?: string
|
||||
): [{ labels: Label[] }, LabelsDispatcher] => {
|
||||
const saveLabels = (labels: Label[], articleId: string) => {
|
||||
const setItemLabels = useSetItemLabels()
|
||||
const saveLabels = (
|
||||
labels: Label[],
|
||||
libraryItemId: string,
|
||||
libraryItemSlug: string
|
||||
) => {
|
||||
;(async () => {
|
||||
const labelIds = labels.map((l) => l.id)
|
||||
if (articleId) {
|
||||
const result = await setLabelsMutation(articleId, labelIds)
|
||||
if (libraryItemId) {
|
||||
const result = await setItemLabels.mutateAsync({
|
||||
itemId: libraryItemId,
|
||||
slug: libraryItemSlug,
|
||||
labels,
|
||||
})
|
||||
if (!result) {
|
||||
showErrorToast('Error saving labels', {
|
||||
position: 'bottom-right',
|
||||
|
|
@ -31,12 +40,14 @@ export const useSetPageLabels = (
|
|||
state: {
|
||||
labels: Label[]
|
||||
articleId: string | undefined
|
||||
throttledSave: (labels: Label[], articleId: string) => void
|
||||
slug: string | undefined
|
||||
throttledSave: (labels: Label[], articleId: string, slug: string) => void
|
||||
},
|
||||
action: {
|
||||
type: string
|
||||
labels: Label[]
|
||||
articleId?: string
|
||||
slug?: string
|
||||
}
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
|
@ -53,8 +64,8 @@ export const useSetPageLabels = (
|
|||
}
|
||||
}
|
||||
case 'SAVE': {
|
||||
if (state.articleId) {
|
||||
state.throttledSave(action.labels, state.articleId)
|
||||
if (state.articleId && state.slug) {
|
||||
state.throttledSave(action.labels, state.articleId, state.slug)
|
||||
} else {
|
||||
showErrorToast('Unable to update labels', {
|
||||
position: 'bottom-right',
|
||||
|
|
@ -68,6 +79,7 @@ export const useSetPageLabels = (
|
|||
case 'UPDATE_ARTICLE_ID': {
|
||||
return {
|
||||
...state,
|
||||
slug: action.slug,
|
||||
articleId: action.articleId,
|
||||
}
|
||||
}
|
||||
|
|
@ -78,7 +90,8 @@ export const useSetPageLabels = (
|
|||
|
||||
const debouncedSave = useCallback(
|
||||
throttle(
|
||||
(labels: Label[], articleId: string) => saveLabels(labels, articleId),
|
||||
(labels: Label[], articleId: string, slug: string) =>
|
||||
saveLabels(labels, articleId, slug),
|
||||
2000
|
||||
),
|
||||
[]
|
||||
|
|
@ -88,13 +101,15 @@ export const useSetPageLabels = (
|
|||
dispatchLabels({
|
||||
type: 'UPDATE_ARTICLE_ID',
|
||||
labels: [],
|
||||
articleId: articleId,
|
||||
slug: libraryItemSlug,
|
||||
articleId: libraryItemId,
|
||||
})
|
||||
}, [articleId])
|
||||
}, [libraryItemId])
|
||||
|
||||
const [labels, dispatchLabels] = useReducer(labelsReducer, {
|
||||
labels: [],
|
||||
articleId: articleId,
|
||||
articleId: libraryItemId,
|
||||
slug: libraryItemSlug,
|
||||
throttledSave: debouncedSave,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function navigationCommands(router: NextRouter | undefined): Action[] {
|
|||
keywords: 'go home',
|
||||
perform: () => {
|
||||
console.log('go home')
|
||||
router?.push(`/l/home`)
|
||||
router?.push(`/home`)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -24,7 +24,7 @@ export function navigationCommands(router: NextRouter | undefined): Action[] {
|
|||
keywords: 'go library',
|
||||
perform: () => {
|
||||
console.log('go library')
|
||||
router?.push(`/l/library`)
|
||||
router?.push(`/library`)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -35,7 +35,7 @@ export function navigationCommands(router: NextRouter | undefined): Action[] {
|
|||
keywords: 'go subscriptions',
|
||||
perform: () => {
|
||||
console.log('go subscriptions')
|
||||
router?.push(`/l/subscriptions`)
|
||||
router?.push(`/subscriptions`)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -46,7 +46,7 @@ export function navigationCommands(router: NextRouter | undefined): Action[] {
|
|||
keywords: 'go highlights',
|
||||
perform: () => {
|
||||
console.log('go highlights')
|
||||
router?.push(`/l/highlights`)
|
||||
router?.push(`/highlights`)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export const articleFragment = gql`
|
|||
readingProgressAnchorIndex
|
||||
slug
|
||||
folder
|
||||
isArchived
|
||||
description
|
||||
linkId
|
||||
state
|
||||
|
|
@ -60,7 +59,6 @@ export type ArticleFragmentData = {
|
|||
readingProgressTopPercent?: number
|
||||
readingProgressAnchorIndex: number
|
||||
slug: string
|
||||
isArchived: boolean
|
||||
description: string
|
||||
linkId?: string
|
||||
state?: State
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { LibraryItemNode } from '../queries/useGetLibraryItemsQuery'
|
||||
import { LibraryItemNode } from '../library_items/useLibraryItems'
|
||||
import { Label } from './labelFragment'
|
||||
|
||||
export const highlightFragment = gql`
|
||||
|
|
|
|||
77
packages/web/lib/networking/highlights/gql.tsx
Normal file
77
packages/web/lib/networking/highlights/gql.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { highlightFragment } from '../fragments/highlightFragment'
|
||||
|
||||
export const GQL_CREATE_HIGHLIGHT = gql`
|
||||
mutation CreateHighlight($input: CreateHighlightInput!) {
|
||||
createHighlight(input: $input) {
|
||||
... on CreateHighlightSuccess {
|
||||
highlight {
|
||||
...HighlightFields
|
||||
}
|
||||
}
|
||||
|
||||
... on CreateHighlightError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${highlightFragment}
|
||||
`
|
||||
|
||||
export const GQL_DELETE_HIGHLIGHT = gql`
|
||||
mutation DeleteHighlight($highlightId: ID!) {
|
||||
deleteHighlight(highlightId: $highlightId) {
|
||||
... on DeleteHighlightSuccess {
|
||||
highlight {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on DeleteHighlightError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_UPDATE_HIGHLIGHT = gql`
|
||||
mutation UpdateHighlight($input: UpdateHighlightInput!) {
|
||||
updateHighlight(input: $input) {
|
||||
... on UpdateHighlightSuccess {
|
||||
highlight {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
... on UpdateHighlightError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_MERGE_HIGHLIGHT = gql`
|
||||
mutation MergeHighlight($input: MergeHighlightInput!) {
|
||||
mergeHighlight(input: $input) {
|
||||
... on MergeHighlightSuccess {
|
||||
highlight {
|
||||
id
|
||||
shortId
|
||||
quote
|
||||
prefix
|
||||
suffix
|
||||
patch
|
||||
color
|
||||
createdAt
|
||||
updatedAt
|
||||
annotation
|
||||
sharedAt
|
||||
createdByMe
|
||||
}
|
||||
overlapHighlightIdList
|
||||
}
|
||||
... on MergeHighlightError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
233
packages/web/lib/networking/highlights/useItemHighlights.tsx
Normal file
233
packages/web/lib/networking/highlights/useItemHighlights.tsx
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import {
|
||||
GQL_CREATE_HIGHLIGHT,
|
||||
GQL_DELETE_HIGHLIGHT,
|
||||
GQL_MERGE_HIGHLIGHT,
|
||||
GQL_UPDATE_HIGHLIGHT,
|
||||
} from './gql'
|
||||
import { updateItemProperty } from '../library_items/useLibraryItems'
|
||||
import { Highlight, HighlightType } from '../fragments/highlightFragment'
|
||||
import { UpdateHighlightInput } from '../mutations/updateHighlightMutation'
|
||||
import { MergeHighlightInput } from '../mutations/mergeHighlightMutation'
|
||||
|
||||
export const useCreateHighlight = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const createHighlight = async (variables: {
|
||||
itemId: string
|
||||
slug: string | undefined
|
||||
input: CreateHighlightInput
|
||||
}) => {
|
||||
const result = (await gqlFetcher(GQL_CREATE_HIGHLIGHT, {
|
||||
input: variables.input,
|
||||
})) as CreateHighlightData
|
||||
if (result.createHighlight.errorCodes?.length) {
|
||||
throw new Error(result.createHighlight.errorCodes[0])
|
||||
}
|
||||
return result.createHighlight.highlight
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: createHighlight,
|
||||
onSuccess: (newHighlight, variables) => {
|
||||
if (newHighlight) {
|
||||
updateItemProperty(
|
||||
queryClient,
|
||||
variables.itemId,
|
||||
variables.slug,
|
||||
(item) => {
|
||||
return {
|
||||
...item,
|
||||
highlights: [...item.highlights, newHighlight],
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteHighlight = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const deleteHighlight = async (variables: {
|
||||
itemId: string
|
||||
slug: string
|
||||
highlightId: string
|
||||
}) => {
|
||||
const result = (await gqlFetcher(GQL_DELETE_HIGHLIGHT, {
|
||||
highlightId: variables.highlightId,
|
||||
})) as DeleteHighlightData
|
||||
if (result.deleteHighlight.errorCodes?.length) {
|
||||
throw new Error(result.deleteHighlight.errorCodes[0])
|
||||
}
|
||||
return result.deleteHighlight.highlight
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: deleteHighlight,
|
||||
onSuccess: (deletedHighlight, variables) => {
|
||||
if (deletedHighlight) {
|
||||
updateItemProperty(
|
||||
queryClient,
|
||||
variables.itemId,
|
||||
variables.slug,
|
||||
(item) => {
|
||||
return {
|
||||
...item,
|
||||
highlights: item.highlights.filter(
|
||||
(h) => h.id != deletedHighlight.id
|
||||
),
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateHighlight = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const updateHighlight = async (variables: {
|
||||
itemId: string
|
||||
slug: string | undefined
|
||||
input: UpdateHighlightInput
|
||||
}) => {
|
||||
const result = (await gqlFetcher(GQL_UPDATE_HIGHLIGHT, {
|
||||
input: variables.input,
|
||||
})) as UpdateHighlightData
|
||||
if (result.updateHighlight.errorCodes?.length) {
|
||||
throw new Error(result.updateHighlight.errorCodes[0])
|
||||
}
|
||||
return result.updateHighlight.highlight
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: updateHighlight,
|
||||
onSuccess: (updatedHighlight, variables) => {
|
||||
if (updatedHighlight) {
|
||||
updateItemProperty(
|
||||
queryClient,
|
||||
variables.itemId,
|
||||
variables.slug,
|
||||
(item) => {
|
||||
return {
|
||||
...item,
|
||||
highlights: [
|
||||
...item.highlights.filter((h) => h.id != updatedHighlight.id),
|
||||
updatedHighlight,
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useMergeHighlight = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const mergeHighlight = async (variables: {
|
||||
itemId: string
|
||||
slug: string
|
||||
input: MergeHighlightInput
|
||||
}) => {
|
||||
const result = (await gqlFetcher(GQL_MERGE_HIGHLIGHT, {
|
||||
input: {
|
||||
id: variables.input.id,
|
||||
shortId: variables.input.shortId,
|
||||
articleId: variables.input.articleId,
|
||||
patch: variables.input.patch,
|
||||
quote: variables.input.quote,
|
||||
prefix: variables.input.prefix,
|
||||
suffix: variables.input.suffix,
|
||||
html: variables.input.html,
|
||||
annotation: variables.input.annotation,
|
||||
overlapHighlightIdList: variables.input.overlapHighlightIdList,
|
||||
highlightPositionPercent: variables.input.highlightPositionPercent,
|
||||
highlightPositionAnchorIndex:
|
||||
variables.input.highlightPositionAnchorIndex,
|
||||
},
|
||||
})) as MergeHighlightData
|
||||
if (result.mergeHighlight.errorCodes?.length) {
|
||||
throw new Error(result.mergeHighlight.errorCodes[0])
|
||||
}
|
||||
return result.mergeHighlight
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: mergeHighlight,
|
||||
onSuccess: (mergeHighlights, variables) => {
|
||||
if (mergeHighlights && mergeHighlights.highlight) {
|
||||
const newHighlight = mergeHighlights.highlight
|
||||
const mergedIds = mergeHighlights.overlapHighlightIdList ?? []
|
||||
updateItemProperty(
|
||||
queryClient,
|
||||
variables.itemId,
|
||||
variables.slug,
|
||||
(item) => {
|
||||
return {
|
||||
...item,
|
||||
highlights: [
|
||||
...item.highlights.filter((h) => mergedIds.indexOf(h.id) == -1),
|
||||
newHighlight,
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type MergeHighlightData = {
|
||||
mergeHighlight: MergeHighlightResult
|
||||
}
|
||||
|
||||
type MergeHighlightResult = {
|
||||
highlight?: Highlight
|
||||
overlapHighlightIdList?: string[]
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type UpdateHighlightData = {
|
||||
updateHighlight: UpdateHighlightResult
|
||||
}
|
||||
|
||||
type UpdateHighlightResult = {
|
||||
highlight?: Highlight
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type DeleteHighlightData = {
|
||||
deleteHighlight: DeleteHighlightResult
|
||||
}
|
||||
|
||||
type DeleteHighlightResult = {
|
||||
highlight?: Highlight
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type CreateHighlightData = {
|
||||
createHighlight: CreateHighlightResult
|
||||
}
|
||||
|
||||
type CreateHighlightResult = {
|
||||
highlight?: Highlight
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
export type CreateHighlightInput = {
|
||||
id: string
|
||||
shortId: string
|
||||
articleId: string
|
||||
|
||||
prefix?: string
|
||||
suffix?: string
|
||||
quote?: string
|
||||
html?: string
|
||||
color?: string
|
||||
annotation?: string
|
||||
|
||||
patch?: string
|
||||
|
||||
highlightPositionPercent?: number
|
||||
highlightPositionAnchorIndex?: number
|
||||
|
||||
type?: HighlightType
|
||||
}
|
||||
71
packages/web/lib/networking/labels/gql.tsx
Normal file
71
packages/web/lib/networking/labels/gql.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { labelFragment } from '../fragments/labelFragment'
|
||||
|
||||
export const GQL_GET_LABELS = gql`
|
||||
query GetLabels {
|
||||
labels {
|
||||
... on LabelsSuccess {
|
||||
labels {
|
||||
...LabelFields
|
||||
}
|
||||
}
|
||||
... on LabelsError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${labelFragment}
|
||||
`
|
||||
|
||||
export const GQL_CREATE_LABEL = gql`
|
||||
mutation CreateLabel($input: CreateLabelInput!) {
|
||||
createLabel(input: $input) {
|
||||
... on CreateLabelSuccess {
|
||||
label {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
... on CreateLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_DELETE_LABEL = gql`
|
||||
mutation DeleteLabel($id: ID!) {
|
||||
deleteLabel(id: $id) {
|
||||
... on DeleteLabelSuccess {
|
||||
label {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on DeleteLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_UPDATE_LABEL = gql`
|
||||
mutation UpdateLabel($input: UpdateLabelInput!) {
|
||||
updateLabel(input: $input) {
|
||||
... on UpdateLabelSuccess {
|
||||
label {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
... on UpdateLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
159
packages/web/lib/networking/labels/useLabels.tsx
Normal file
159
packages/web/lib/networking/labels/useLabels.tsx
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import {
|
||||
GQL_CREATE_LABEL,
|
||||
GQL_DELETE_LABEL,
|
||||
GQL_GET_LABELS,
|
||||
GQL_UPDATE_LABEL,
|
||||
} from './gql'
|
||||
import { Label } from '../fragments/labelFragment'
|
||||
|
||||
export function useGetLabels() {
|
||||
return useQuery({
|
||||
queryKey: ['labels'],
|
||||
queryFn: async () => {
|
||||
const response = (await gqlFetcher(GQL_GET_LABELS)) as LabelsData
|
||||
if (response.labels?.errorCodes?.length) {
|
||||
throw new Error(response.labels.errorCodes[0])
|
||||
}
|
||||
return response.labels?.labels
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateLabel = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const createLabel = async (variables: {
|
||||
name: string
|
||||
color: string
|
||||
description: string | undefined
|
||||
}) => {
|
||||
const result = (await gqlFetcher(GQL_CREATE_LABEL, {
|
||||
input: {
|
||||
name: variables.name,
|
||||
color: variables.color,
|
||||
description: variables.description,
|
||||
},
|
||||
})) as CreateLabelData
|
||||
if (result.createLabel.errorCodes?.length) {
|
||||
throw new Error(result.createLabel.errorCodes[0])
|
||||
}
|
||||
return result.createLabel.label
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: createLabel,
|
||||
onSuccess: (newLabel) => {
|
||||
const keys = queryClient.getQueryCache().findAll({ queryKey: ['labels'] })
|
||||
keys.forEach((query) => {
|
||||
queryClient.setQueryData(query.queryKey, (data: Label[]) => {
|
||||
return [...data, newLabel]
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteLabel = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const deleteLabel = async (variables: { labelId: string }) => {
|
||||
const result = (await gqlFetcher(GQL_DELETE_LABEL, {
|
||||
id: variables.labelId,
|
||||
})) as DeleteLabelData
|
||||
if (result.deleteLabel.errorCodes?.length) {
|
||||
throw new Error(result.deleteLabel.errorCodes[0])
|
||||
}
|
||||
return result.deleteLabel?.label?.id
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: deleteLabel,
|
||||
onSuccess: (deletedId) => {
|
||||
if (deletedId) {
|
||||
const keys = queryClient
|
||||
.getQueryCache()
|
||||
.findAll({ queryKey: ['labels'] })
|
||||
keys.forEach((query) => {
|
||||
queryClient.setQueryData(query.queryKey, (data: Label[]) => {
|
||||
return data.filter((label) => label.id !== deletedId)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateLabel = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const updateLabel = async (variables: {
|
||||
labelId: string
|
||||
name: string
|
||||
color: string
|
||||
description: string
|
||||
}) => {
|
||||
const result = (await gqlFetcher(GQL_UPDATE_LABEL, {
|
||||
input: {
|
||||
labelId: variables.labelId,
|
||||
name: variables.name,
|
||||
color: variables.color,
|
||||
description: variables.description,
|
||||
},
|
||||
})) as UpdateLabelData
|
||||
if (result.updateLabel.errorCodes?.length) {
|
||||
throw new Error(result.updateLabel.errorCodes[0])
|
||||
}
|
||||
return result.updateLabel?.label
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: updateLabel,
|
||||
onSuccess: (updatedLabel) => {
|
||||
if (updatedLabel) {
|
||||
const keys = queryClient
|
||||
.getQueryCache()
|
||||
.findAll({ queryKey: ['labels'] })
|
||||
keys.forEach((query) => {
|
||||
queryClient.setQueryData(query.queryKey, (data: Label[]) => {
|
||||
return [
|
||||
...data.filter((label) => label.id !== updatedLabel.id),
|
||||
updatedLabel,
|
||||
]
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type LabelsResult = {
|
||||
labels?: Label[]
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type LabelsData = {
|
||||
labels?: LabelsResult
|
||||
}
|
||||
|
||||
type CreateLabelResult = {
|
||||
label?: Label
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type CreateLabelData = {
|
||||
createLabel: CreateLabelResult
|
||||
}
|
||||
|
||||
type DeleteLabelResult = {
|
||||
label?: Label
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type DeleteLabelData = {
|
||||
deleteLabel: DeleteLabelResult
|
||||
}
|
||||
|
||||
type UpdateLabelResult = {
|
||||
label?: Label
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type UpdateLabelData = {
|
||||
updateLabel: UpdateLabelResult
|
||||
}
|
||||
307
packages/web/lib/networking/library_items/gql.tsx
Normal file
307
packages/web/lib/networking/library_items/gql.tsx
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { highlightFragment } from '../fragments/highlightFragment'
|
||||
import { articleFragment } from '../fragments/articleFragment'
|
||||
import { labelFragment } from '../fragments/labelFragment'
|
||||
|
||||
export const recommendationFragment = gql`
|
||||
fragment RecommendationFields on Recommendation {
|
||||
id
|
||||
name
|
||||
note
|
||||
user {
|
||||
userId
|
||||
name
|
||||
username
|
||||
profileImageURL
|
||||
}
|
||||
recommendedAt
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_SEARCH_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
|
||||
pageType
|
||||
contentReader
|
||||
createdAt
|
||||
readingProgressPercent
|
||||
readingProgressTopPercent
|
||||
readingProgressAnchorIndex
|
||||
author
|
||||
image
|
||||
description
|
||||
publishedAt
|
||||
ownedByViewer
|
||||
originalArticleUrl
|
||||
uploadFileId
|
||||
labels {
|
||||
id
|
||||
name
|
||||
color
|
||||
}
|
||||
pageId
|
||||
shortId
|
||||
quote
|
||||
annotation
|
||||
state
|
||||
siteName
|
||||
siteIcon
|
||||
subscription
|
||||
readAt
|
||||
savedAt
|
||||
wordsCount
|
||||
recommendations {
|
||||
id
|
||||
name
|
||||
note
|
||||
user {
|
||||
userId
|
||||
name
|
||||
username
|
||||
profileImageURL
|
||||
}
|
||||
recommendedAt
|
||||
}
|
||||
highlights {
|
||||
...HighlightFields
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
... on SearchError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${highlightFragment}
|
||||
`
|
||||
|
||||
export const GQL_SET_LINK_ARCHIVED = gql`
|
||||
mutation SetLinkArchived($input: ArchiveLinkInput!) {
|
||||
setLinkArchived(input: $input) {
|
||||
... on ArchiveLinkSuccess {
|
||||
linkId
|
||||
message
|
||||
}
|
||||
... on ArchiveLinkError {
|
||||
message
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_DELETE_LIBRARY_ITEM = gql`
|
||||
mutation SetBookmarkArticle($input: SetBookmarkArticleInput!) {
|
||||
setBookmarkArticle(input: $input) {
|
||||
... on SetBookmarkArticleSuccess {
|
||||
bookmarkedArticle {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on SetBookmarkArticleError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_MOVE_ITEM_TO_FOLDER = gql`
|
||||
mutation MoveToFolder($id: ID!, $folder: String!) {
|
||||
moveToFolder(id: $id, folder: $folder) {
|
||||
... on MoveToFolderSuccess {
|
||||
success
|
||||
}
|
||||
... on MoveToFolderError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_SET_LABELS = gql`
|
||||
mutation SetLabels($input: SetLabelsInput!) {
|
||||
setLabels(input: $input) {
|
||||
... on SetLabelsSuccess {
|
||||
labels {
|
||||
...LabelFields
|
||||
}
|
||||
}
|
||||
... on SetLabelsError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${labelFragment}
|
||||
`
|
||||
|
||||
export const GQL_SAVE_ARTICLE_READING_PROGRESS = gql`
|
||||
mutation SaveArticleReadingProgress(
|
||||
$input: SaveArticleReadingProgressInput!
|
||||
) {
|
||||
saveArticleReadingProgress(input: $input) {
|
||||
... on SaveArticleReadingProgressSuccess {
|
||||
updatedArticle {
|
||||
id
|
||||
readingProgressPercent
|
||||
readingProgressAnchorIndex
|
||||
}
|
||||
}
|
||||
... on SaveArticleReadingProgressError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_UPDATE_LIBRARY_ITEM = gql`
|
||||
mutation UpdatePage($input: UpdatePageInput!) {
|
||||
updatePage(input: $input) {
|
||||
... on UpdatePageSuccess {
|
||||
updatedPage {
|
||||
id
|
||||
title
|
||||
url
|
||||
createdAt
|
||||
author
|
||||
image
|
||||
description
|
||||
savedAt
|
||||
publishedAt
|
||||
}
|
||||
}
|
||||
... on UpdatePageError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_GET_LIBRARY_ITEM = gql`
|
||||
query GetArticle(
|
||||
$username: String!
|
||||
$slug: String!
|
||||
$includeFriendsHighlights: Boolean
|
||||
) {
|
||||
article(username: $username, slug: $slug) {
|
||||
... on ArticleSuccess {
|
||||
article {
|
||||
...ArticleFields
|
||||
highlights(input: { includeFriends: $includeFriendsHighlights }) {
|
||||
...HighlightFields
|
||||
}
|
||||
labels {
|
||||
...LabelFields
|
||||
}
|
||||
recommendations {
|
||||
...RecommendationFields
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ArticleError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${articleFragment}
|
||||
${highlightFragment}
|
||||
${labelFragment}
|
||||
${recommendationFragment}
|
||||
`
|
||||
|
||||
export const GQL_GET_LIBRARY_ITEM_CONTENT = gql`
|
||||
query GetArticle(
|
||||
$username: String!
|
||||
$slug: String!
|
||||
$includeFriendsHighlights: Boolean
|
||||
) {
|
||||
article(username: $username, slug: $slug) {
|
||||
... on ArticleSuccess {
|
||||
article {
|
||||
...ArticleFields
|
||||
content
|
||||
highlights(input: { includeFriends: $includeFriendsHighlights }) {
|
||||
...HighlightFields
|
||||
}
|
||||
labels {
|
||||
...LabelFields
|
||||
}
|
||||
recommendations {
|
||||
...RecommendationFields
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ArticleError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${articleFragment}
|
||||
${highlightFragment}
|
||||
${labelFragment}
|
||||
${recommendationFragment}
|
||||
`
|
||||
|
||||
export const GQL_BULK_ACTION = gql`
|
||||
mutation BulkAction(
|
||||
$action: BulkActionType!
|
||||
$query: String!
|
||||
$expectedCount: Int
|
||||
$labelIds: [ID!]
|
||||
) {
|
||||
bulkAction(
|
||||
query: $query
|
||||
action: $action
|
||||
labelIds: $labelIds
|
||||
expectedCount: $expectedCount
|
||||
) {
|
||||
... on BulkActionSuccess {
|
||||
success
|
||||
}
|
||||
... on BulkActionError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_SAVE_URL = gql`
|
||||
mutation SaveUrl($input: SaveUrlInput!) {
|
||||
saveUrl(input: $input) {
|
||||
... on SaveSuccess {
|
||||
url
|
||||
clientRequestId
|
||||
}
|
||||
... on SaveError {
|
||||
errorCodes
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
1055
packages/web/lib/networking/library_items/useLibraryItems.tsx
Normal file
1055
packages/web/lib/networking/library_items/useLibraryItems.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -9,32 +9,32 @@ export type ArticleReadingProgressMutationInput = {
|
|||
readingProgressAnchorIndex?: number
|
||||
}
|
||||
|
||||
export async function articleReadingProgressMutation(
|
||||
input: ArticleReadingProgressMutationInput
|
||||
): Promise<boolean> {
|
||||
const mutation = gql`
|
||||
mutation SaveArticleReadingProgress(
|
||||
$input: SaveArticleReadingProgressInput!
|
||||
) {
|
||||
saveArticleReadingProgress(input: $input) {
|
||||
... on SaveArticleReadingProgressSuccess {
|
||||
updatedArticle {
|
||||
id
|
||||
readingProgressPercent
|
||||
readingProgressAnchorIndex
|
||||
}
|
||||
}
|
||||
... on SaveArticleReadingProgressError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
// export async function articleReadingProgressMutation(
|
||||
// input: ArticleReadingProgressMutationInput
|
||||
// ): Promise<boolean> {
|
||||
// const mutation = gql`
|
||||
// mutation SaveArticleReadingProgress(
|
||||
// $input: SaveArticleReadingProgressInput!
|
||||
// ) {
|
||||
// saveArticleReadingProgress(input: $input) {
|
||||
// ... on SaveArticleReadingProgressSuccess {
|
||||
// updatedArticle {
|
||||
// id
|
||||
// readingProgressPercent
|
||||
// readingProgressAnchorIndex
|
||||
// }
|
||||
// }
|
||||
// ... on SaveArticleReadingProgressError {
|
||||
// errorCodes
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// `
|
||||
|
||||
try {
|
||||
await gqlFetcher(mutation, { input })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// try {
|
||||
// await gqlFetcher(mutation, { input })
|
||||
// return true
|
||||
// } catch {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,62 +1,2 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
export enum BulkAction {
|
||||
ARCHIVE = 'ARCHIVE',
|
||||
DELETE = 'DELETE',
|
||||
ADD_LABELS = 'ADD_LABELS',
|
||||
MARK_AS_READ = 'MARK_AS_READ',
|
||||
}
|
||||
|
||||
type BulkActionResponseData = {
|
||||
success: boolean
|
||||
}
|
||||
|
||||
type BulkActionResponse = {
|
||||
errorCodes?: string[]
|
||||
bulkAction?: BulkActionResponseData
|
||||
}
|
||||
|
||||
export async function bulkActionMutation(
|
||||
action: BulkAction,
|
||||
query: string,
|
||||
expectedCount: number,
|
||||
labelIds?: string[]
|
||||
): Promise<boolean> {
|
||||
const mutation = gql`
|
||||
mutation BulkAction(
|
||||
$action: BulkActionType!
|
||||
$query: String!
|
||||
$expectedCount: Int
|
||||
$labelIds: [ID!]
|
||||
) {
|
||||
bulkAction(
|
||||
query: $query
|
||||
action: $action
|
||||
labelIds: $labelIds
|
||||
expectedCount: $expectedCount
|
||||
) {
|
||||
... on BulkActionSuccess {
|
||||
success
|
||||
}
|
||||
... on BulkActionError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const response = await gqlFetcher(mutation, {
|
||||
action,
|
||||
query,
|
||||
labelIds,
|
||||
expectedCount,
|
||||
})
|
||||
const data = response as BulkActionResponse | undefined
|
||||
return data?.bulkAction?.success ?? false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import {
|
||||
Highlight,
|
||||
highlightFragment,
|
||||
HighlightType,
|
||||
} from './../fragments/highlightFragment'
|
||||
|
||||
export type CreateHighlightInput = {
|
||||
id: string
|
||||
shortId: string
|
||||
articleId: string
|
||||
|
||||
prefix?: string
|
||||
suffix?: string
|
||||
quote?: string
|
||||
html?: string
|
||||
color?: string
|
||||
annotation?: string
|
||||
|
||||
patch?: string
|
||||
|
||||
highlightPositionPercent?: number
|
||||
highlightPositionAnchorIndex?: number
|
||||
|
||||
type?: HighlightType
|
||||
}
|
||||
|
||||
type CreateHighlightOutput = {
|
||||
createHighlight: InnerCreateHighlightOutput
|
||||
}
|
||||
|
||||
type InnerCreateHighlightOutput = {
|
||||
highlight: Highlight
|
||||
}
|
||||
|
||||
export async function createHighlightMutation(
|
||||
input: CreateHighlightInput
|
||||
): Promise<Highlight | undefined> {
|
||||
const mutation = gql`
|
||||
mutation CreateHighlight($input: CreateHighlightInput!) {
|
||||
createHighlight(input: $input) {
|
||||
... on CreateHighlightSuccess {
|
||||
highlight {
|
||||
...HighlightFields
|
||||
}
|
||||
}
|
||||
|
||||
... on CreateHighlightError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${highlightFragment}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, { input })
|
||||
const output = data as CreateHighlightOutput | undefined
|
||||
return output?.createHighlight.highlight
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { Label } from '../fragments/labelFragment'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type CreateLabelResult = {
|
||||
createLabel: CreateLabel
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type CreateLabel = {
|
||||
label: Label
|
||||
}
|
||||
|
||||
export async function createLabelMutation(
|
||||
name: string,
|
||||
color: string,
|
||||
description?: string
|
||||
): Promise<any | undefined> {
|
||||
const mutation = gql`
|
||||
mutation CreateLabel($input: CreateLabelInput!) {
|
||||
createLabel(input: $input) {
|
||||
... on CreateLabelSuccess {
|
||||
label {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
... on CreateLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = (await gqlFetcher(mutation, {
|
||||
input: {
|
||||
name,
|
||||
color,
|
||||
description,
|
||||
},
|
||||
})) as CreateLabelResult
|
||||
return data.errorCodes ? undefined : data.createLabel.label
|
||||
} catch (error) {
|
||||
console.log('createLabelMutation error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
export enum ReminderType {
|
||||
Tonight = 'TONIGHT',
|
||||
Tomorrow = 'TOMORROW',
|
||||
ThisWeekend = 'THIS_WEEKEND',
|
||||
NextWeek = 'NEXT_WEEK',
|
||||
}
|
||||
|
||||
export async function createReminderMutation(
|
||||
linkId: string,
|
||||
reminderType: ReminderType,
|
||||
archiveUntil: boolean,
|
||||
sendNotification: boolean
|
||||
): Promise<string | undefined> {
|
||||
const mutation = gql`
|
||||
mutation createReminderMutation($input: CreateReminderInput!) {
|
||||
createReminder(input: $input) {
|
||||
... on CreateReminderSuccess {
|
||||
reminder {
|
||||
id
|
||||
remindAt
|
||||
}
|
||||
}
|
||||
... on CreateReminderError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const input = {
|
||||
linkId,
|
||||
reminderType,
|
||||
archiveUntil,
|
||||
sendNotification,
|
||||
scheduledAt: new Date(),
|
||||
}
|
||||
const data = await gqlFetcher(mutation, { input })
|
||||
return 'data'
|
||||
} catch (error) {
|
||||
console.log('createReminder error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { SavedSearch } from "../fragments/savedSearchFragment"
|
||||
|
||||
export type DeleteFilterInput = string
|
||||
|
||||
type DeleteFilterOutput = {
|
||||
deleteFilter: { filter: SavedSearch }
|
||||
}
|
||||
|
||||
export async function deleteFilterMutation (
|
||||
id: DeleteFilterInput
|
||||
): Promise<SavedSearch | undefined> {
|
||||
const mutation = gql`
|
||||
mutation DeleteFilter($id: ID!) {
|
||||
deleteFilter(id: $id) {
|
||||
... on DeleteFilterSuccess {
|
||||
filter {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on DeleteFilterError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, { id })
|
||||
const output = data as DeleteFilterOutput | undefined
|
||||
return output?.deleteFilter.filter
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { Label } from '../fragments/labelFragment'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type DeleteLabelResult = {
|
||||
deleteLabel: DeleteLabel
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type DeleteLabel = {
|
||||
label: Label
|
||||
}
|
||||
|
||||
export async function deleteLabelMutation(
|
||||
labelId: string
|
||||
): Promise<any | undefined> {
|
||||
const mutation = gql`
|
||||
mutation DeleteLabel($id: ID!) {
|
||||
deleteLabel(id: $id) {
|
||||
... on DeleteLabelSuccess {
|
||||
label {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
... on DeleteLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = (await gqlFetcher(mutation, {
|
||||
id: labelId,
|
||||
})) as DeleteLabelResult
|
||||
return data.errorCodes ? undefined : data.deleteLabel.label.id
|
||||
} catch (error) {
|
||||
console.log('deleteLabelMutation error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
export async function deleteLinkMutation(
|
||||
linkId: string
|
||||
): Promise<unknown> {
|
||||
const mutation = gql`
|
||||
mutation SetBookmarkArticle($input: SetBookmarkArticleInput!) {
|
||||
setBookmarkArticle(input: $input) {
|
||||
... on SetBookmarkArticleSuccess {
|
||||
bookmarkedArticle {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on SetBookmarkArticleError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, { input: { articleID: linkId, bookmark: false }})
|
||||
return data
|
||||
} catch (error) {
|
||||
console.log('SetBookmarkArticleOutput error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type MoveToFolderResponseData = {
|
||||
success?: boolean
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type MoveToFolderResponse = {
|
||||
moveToFolder?: MoveToFolderResponseData
|
||||
}
|
||||
|
||||
export async function moveToFolderMutation(
|
||||
itemId: string,
|
||||
folder: string
|
||||
): Promise<boolean> {
|
||||
const mutation = gql`
|
||||
mutation MoveToFolder($id: ID!, $folder: String!) {
|
||||
moveToFolder(id: $id, folder: $folder) {
|
||||
... on MoveToFolderSuccess {
|
||||
success
|
||||
}
|
||||
... on MoveToFolderError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const response = await gqlFetcher(mutation, { id: itemId, folder })
|
||||
const data = response as MoveToFolderResponse | undefined
|
||||
if (data?.moveToFolder?.errorCodes) {
|
||||
return false
|
||||
}
|
||||
return data?.moveToFolder?.success ?? false
|
||||
} catch (error) {
|
||||
console.log('MoveToFolder error', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { SavedSearch } from "../fragments/savedSearchFragment"
|
||||
|
||||
export type AddFilterInput = {
|
||||
name: string
|
||||
filter: string
|
||||
category: string
|
||||
position: number
|
||||
folder?: string
|
||||
}
|
||||
|
||||
type AddFilterOutput = {
|
||||
saveFilter: { filter: SavedSearch }
|
||||
}
|
||||
|
||||
export async function saveFilterMutation (
|
||||
input: AddFilterInput
|
||||
): Promise<SavedSearch | undefined> {
|
||||
const mutation = gql`
|
||||
mutation SaveFilter($input: SaveFilterInput!) {
|
||||
saveFilter(input: $input) {
|
||||
... on SaveFilterSuccess {
|
||||
filter {
|
||||
id
|
||||
name
|
||||
filter
|
||||
position
|
||||
visible
|
||||
defaultFilter
|
||||
folder
|
||||
category
|
||||
}
|
||||
}
|
||||
|
||||
... on SaveFilterError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const data = await gqlFetcher(mutation, { input })
|
||||
const output = data as AddFilterOutput | undefined
|
||||
return output?.saveFilter.filter
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { Label, labelFragment } from '../fragments/labelFragment'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type SetLabelsResult = {
|
||||
setLabels: SetLabels
|
||||
}
|
||||
|
||||
type SetLabels = {
|
||||
labels: Label[]
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
export async function setLabelsMutation(
|
||||
pageId: string,
|
||||
labelIds: string[]
|
||||
): Promise<Label[] | undefined> {
|
||||
const mutation = gql`
|
||||
mutation SetLabels($input: SetLabelsInput!) {
|
||||
setLabels(input: $input) {
|
||||
... on SetLabelsSuccess {
|
||||
labels {
|
||||
...LabelFields
|
||||
}
|
||||
}
|
||||
... on SetLabelsError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${labelFragment}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = (await gqlFetcher(mutation, {
|
||||
input: { pageId, labelIds },
|
||||
})) as SetLabelsResult
|
||||
return data.setLabels.errorCodes ? undefined : data.setLabels.labels
|
||||
} catch (error) {
|
||||
console.log(' -- SetLabelsOutput error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type SetLinkArchivedInput = {
|
||||
linkId: string
|
||||
archived: boolean
|
||||
}
|
||||
|
||||
export async function setLinkArchivedMutation(
|
||||
input: SetLinkArchivedInput
|
||||
): Promise<Record<string, never> | undefined> {
|
||||
const mutation = gql`
|
||||
mutation SetLinkArchived($input: ArchiveLinkInput!) {
|
||||
setLinkArchived(input: $input) {
|
||||
... on ArchiveLinkSuccess {
|
||||
linkId
|
||||
message
|
||||
}
|
||||
... on ArchiveLinkError {
|
||||
message
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, { input })
|
||||
return data as Record<string, never> | undefined
|
||||
} catch (error) {
|
||||
console.log('SetLinkArchivedInput error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type ShareHighlightToFeedMutationInput = {
|
||||
id: string
|
||||
share: boolean
|
||||
}
|
||||
|
||||
export async function shareHighlightToFeedMutation(
|
||||
input: ShareHighlightToFeedMutationInput
|
||||
): Promise<boolean> {
|
||||
const mutation = gql`
|
||||
mutation SetShareHighlight($input: SetShareHighlightInput!) {
|
||||
setShareHighlight(input: $input) {
|
||||
... on SetShareHighlightSuccess {
|
||||
highlight {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on SetShareHighlightError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
await gqlFetcher(mutation, { input })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { SavedSearch } from "../fragments/savedSearchFragment"
|
||||
|
||||
export type UpdateFilterInput = {
|
||||
id?: string
|
||||
name?: string
|
||||
filter?: string
|
||||
position?: number
|
||||
category?: string
|
||||
description?: string
|
||||
visible?: boolean
|
||||
folder?: string
|
||||
}
|
||||
|
||||
type UpdateFilterOutput = {
|
||||
filter: SavedSearch
|
||||
}
|
||||
|
||||
export async function updateFilterMutation (
|
||||
input: UpdateFilterInput
|
||||
): Promise<string | undefined> {
|
||||
const mutation = gql`
|
||||
mutation UpdateFilter($input: UpdateFilterInput!) {
|
||||
updateFilter(input: $input) {
|
||||
... on UpdateFilterSuccess {
|
||||
filter {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
... on UpdateFilterError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const { id, name, visible, filter, position } = input
|
||||
const data = await gqlFetcher(mutation, { input: {id, name, filter, position, visible }})
|
||||
const output = data as UpdateFilterOutput | undefined
|
||||
return output?.filter?.id
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
export type UpdateLabelInput = {
|
||||
labelId: string
|
||||
name: string
|
||||
color: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export async function updateLabelMutation(
|
||||
input: UpdateLabelInput
|
||||
): Promise<string | undefined> {
|
||||
const mutation = gql`
|
||||
mutation UpdateLabel($input: UpdateLabelInput!) {
|
||||
updateLabel(input: $input) {
|
||||
... on UpdateLabelSuccess {
|
||||
label {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
... on UpdateLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, {
|
||||
input,
|
||||
})
|
||||
const output = data as any
|
||||
return output?.updatedLabel
|
||||
} catch (err) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { State } from '../fragments/articleFragment'
|
||||
|
||||
export type UpdatePageInput = {
|
||||
pageId: string
|
||||
title?: string
|
||||
byline?: string | undefined
|
||||
description?: string
|
||||
savedAt?: string
|
||||
publishedAt?: string
|
||||
state?: State
|
||||
}
|
||||
|
||||
export async function updatePageMutation(
|
||||
input: UpdatePageInput
|
||||
): Promise<string | undefined> {
|
||||
const mutation = gql`
|
||||
mutation UpdatePage($input: UpdatePageInput!) {
|
||||
updatePage(input: $input) {
|
||||
... on UpdatePageSuccess {
|
||||
updatedPage {
|
||||
id
|
||||
title
|
||||
url
|
||||
createdAt
|
||||
author
|
||||
image
|
||||
description
|
||||
savedAt
|
||||
publishedAt
|
||||
}
|
||||
}
|
||||
... on UpdatePageError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, {
|
||||
input,
|
||||
})
|
||||
const output = data as any
|
||||
return output.updatePage
|
||||
} catch (err) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type ShareHighlightCommentMutationInput = {
|
||||
highlightId: string
|
||||
annotation?: string
|
||||
}
|
||||
|
||||
export async function shareHighlightCommentMutation(
|
||||
input: ShareHighlightCommentMutationInput
|
||||
): Promise<boolean> {
|
||||
const mutation = gql`
|
||||
mutation UpdateHighlight($input: UpdateHighlightInput!) {
|
||||
updateHighlight(input: $input) {
|
||||
... on UpdateHighlightSuccess {
|
||||
highlight {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
... on UpdateHighlightError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
await gqlFetcher(mutation, { input })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +1,96 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { LibraryItemsData } from './useGetLibraryItemsQuery'
|
||||
// import { gqlFetcher } from '../networkHelpers'
|
||||
// import { LibraryItems } from '../library_items/useLibraryItems'
|
||||
|
||||
export type LibraryItemsQueryInput = {
|
||||
limit?: number
|
||||
searchQuery?: string
|
||||
includeContent?: boolean
|
||||
const foo = () => {
|
||||
return 'bar'
|
||||
}
|
||||
// export type LibraryItemsQueryInput = {
|
||||
// limit?: number
|
||||
// searchQuery?: string
|
||||
// includeContent?: boolean
|
||||
// }
|
||||
|
||||
export async function searchQuery({
|
||||
limit = 10,
|
||||
searchQuery,
|
||||
includeContent = false,
|
||||
}: LibraryItemsQueryInput): Promise<LibraryItemsData | undefined> {
|
||||
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
|
||||
pageType
|
||||
contentReader
|
||||
createdAt
|
||||
isArchived
|
||||
readingProgressPercent
|
||||
readingProgressTopPercent
|
||||
readingProgressAnchorIndex
|
||||
author
|
||||
image
|
||||
description
|
||||
publishedAt
|
||||
ownedByViewer
|
||||
originalArticleUrl
|
||||
uploadFileId
|
||||
labels {
|
||||
id
|
||||
name
|
||||
color
|
||||
}
|
||||
pageId
|
||||
shortId
|
||||
quote
|
||||
annotation
|
||||
state
|
||||
siteName
|
||||
subscription
|
||||
readAt
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
... on SearchError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
// export async function searchQuery({
|
||||
// limit = 10,
|
||||
// searchQuery,
|
||||
// includeContent = false,
|
||||
// }: LibraryItemsQueryInput): Promise<LibraryItemsData | undefined> {
|
||||
// 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
|
||||
// pageType
|
||||
// contentReader
|
||||
// createdAt
|
||||
// readingProgressPercent
|
||||
// readingProgressTopPercent
|
||||
// readingProgressAnchorIndex
|
||||
// author
|
||||
// image
|
||||
// description
|
||||
// publishedAt
|
||||
// ownedByViewer
|
||||
// originalArticleUrl
|
||||
// uploadFileId
|
||||
// labels {
|
||||
// id
|
||||
// name
|
||||
// color
|
||||
// }
|
||||
// pageId
|
||||
// shortId
|
||||
// quote
|
||||
// annotation
|
||||
// state
|
||||
// siteName
|
||||
// subscription
|
||||
// readAt
|
||||
// }
|
||||
// }
|
||||
// pageInfo {
|
||||
// hasNextPage
|
||||
// hasPreviousPage
|
||||
// startCursor
|
||||
// endCursor
|
||||
// totalCount
|
||||
// }
|
||||
// }
|
||||
// ... on SearchError {
|
||||
// errorCodes
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// `
|
||||
|
||||
const variables = {
|
||||
first: limit,
|
||||
query: searchQuery,
|
||||
includeContent,
|
||||
}
|
||||
// const variables = {
|
||||
// first: limit,
|
||||
// query: searchQuery,
|
||||
// includeContent,
|
||||
// }
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(query, { ...variables })
|
||||
return (data as LibraryItemsData) || undefined
|
||||
} catch (error) {
|
||||
console.log('search error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
// try {
|
||||
// const data = await gqlFetcher(query, { ...variables })
|
||||
// return (data as LibraryItemsData) || undefined
|
||||
// } catch (error) {
|
||||
// console.log('search error', error)
|
||||
// return undefined
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import useSWRImmutable from 'swr'
|
||||
import { makeGqlFetcher, RequestContext, ssrFetcher } from '../networkHelpers'
|
||||
import { ArticleAttributes } from '../library_items/useLibraryItems'
|
||||
|
||||
type ArticleQueryInput = {
|
||||
username?: string
|
||||
|
|
@ -17,11 +18,6 @@ type NestedArticleData = {
|
|||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
export type ArticleAttributes = {
|
||||
id: string
|
||||
originalHtml: string
|
||||
}
|
||||
|
||||
const query = gql`
|
||||
query GetArticle($username: String!, $slug: String!) {
|
||||
article(username: $username, slug: $slug) {
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ import { Highlight, highlightFragment } from '../fragments/highlightFragment'
|
|||
import { ScopedMutator } from 'swr/dist/_internal'
|
||||
import { Label, labelFragment } from '../fragments/labelFragment'
|
||||
import {
|
||||
ArticleAttributes,
|
||||
LibraryItems,
|
||||
Recommendation,
|
||||
recommendationFragment,
|
||||
} from './useGetLibraryItemsQuery'
|
||||
} from '../library_items/useLibraryItems'
|
||||
import useSWR from 'swr'
|
||||
import { recommendationFragment } from '../library_items/gql'
|
||||
|
||||
type ArticleQueryInput = {
|
||||
username?: string
|
||||
|
|
@ -39,37 +40,6 @@ type NestedArticleData = {
|
|||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
export type TextDirection = 'RTL' | 'LTR'
|
||||
|
||||
export type ArticleAttributes = {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
originalArticleUrl: string
|
||||
author?: string
|
||||
image?: string
|
||||
savedAt: string
|
||||
isArchived: boolean
|
||||
createdAt: string
|
||||
publishedAt?: string
|
||||
description?: string
|
||||
wordsCount?: number
|
||||
contentReader: ContentReader
|
||||
readingProgressPercent: number
|
||||
readingProgressTopPercent?: number
|
||||
readingProgressAnchorIndex: number
|
||||
slug: string
|
||||
folder: string
|
||||
savedByViewer?: boolean
|
||||
content: string
|
||||
highlights: Highlight[]
|
||||
linkId: string
|
||||
labels?: Label[]
|
||||
state?: State
|
||||
directionality?: TextDirection
|
||||
recommendations?: Recommendation[]
|
||||
}
|
||||
|
||||
const query = gql`
|
||||
query GetArticle(
|
||||
$username: String!
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import useSWR from 'swr'
|
|||
import { articleFragment } from '../fragments/articleFragment'
|
||||
import { highlightFragment } from '../fragments/highlightFragment'
|
||||
import { makeGqlFetcher } from '../networkHelpers'
|
||||
import { ArticleAttributes } from './useGetArticleQuery'
|
||||
import { ArticleAttributes } from '../library_items/useLibraryItems'
|
||||
|
||||
type ArticleSavingStatusInput = {
|
||||
id?: string
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { gql } from 'graphql-request'
|
|||
import useSWRInfinite from 'swr/infinite'
|
||||
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { PageInfo } from './useGetLibraryItemsQuery'
|
||||
import { PageInfo } from '../library_items/useLibraryItems'
|
||||
|
||||
interface HighlightsResponse {
|
||||
data?: Array<HighlightsData>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import useSWR from 'swr'
|
||||
import { gqlFetcher, makeGqlFetcher, publicGqlFetcher } from '../networkHelpers'
|
||||
import { makeGqlFetcher } from '../networkHelpers'
|
||||
|
||||
type HomeResult = {
|
||||
home: {
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import useSWR from 'swr'
|
||||
import { Label, labelFragment } from '../fragments/labelFragment'
|
||||
import { publicGqlFetcher } from '../networkHelpers'
|
||||
|
||||
type LabelsQueryResponse = {
|
||||
error: any
|
||||
isLoading: boolean
|
||||
isValidating: boolean
|
||||
labels: Label[]
|
||||
revalidate: () => void
|
||||
}
|
||||
|
||||
type LabelsResponseData = {
|
||||
labels?: LabelsData
|
||||
}
|
||||
|
||||
type LabelsData = {
|
||||
labels?: unknown
|
||||
}
|
||||
|
||||
export function useGetLabelsQuery(): LabelsQueryResponse {
|
||||
const query = gql`
|
||||
query GetLabels {
|
||||
labels {
|
||||
... on LabelsSuccess {
|
||||
labels {
|
||||
...LabelFields
|
||||
}
|
||||
}
|
||||
... on LabelsError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${labelFragment}
|
||||
`
|
||||
|
||||
const { data, error, mutate, isValidating } = useSWR(query, publicGqlFetcher)
|
||||
|
||||
try {
|
||||
if (data && !error) {
|
||||
const result = data as LabelsResponseData
|
||||
const labels = result.labels?.labels as Label[]
|
||||
return {
|
||||
error,
|
||||
isLoading: !error && !data,
|
||||
isValidating,
|
||||
labels,
|
||||
revalidate: () => {
|
||||
mutate()
|
||||
},
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
return {
|
||||
error,
|
||||
isLoading: !error && !data,
|
||||
isValidating: false,
|
||||
labels: [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
revalidate: () => {},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,582 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import {
|
||||
showErrorToast,
|
||||
showSuccessToast,
|
||||
showSuccessToastWithUndo,
|
||||
} from '../../toastHelpers'
|
||||
import { ContentReader, PageType, State } from '../fragments/articleFragment'
|
||||
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
|
||||
import { articleReadingProgressMutation } from '../mutations/articleReadingProgressMutation'
|
||||
import { deleteLinkMutation } from '../mutations/deleteLinkMutation'
|
||||
import { setLinkArchivedMutation } from '../mutations/setLinkArchivedMutation'
|
||||
import { updatePageMutation } from '../mutations/updatePageMutation'
|
||||
import { gqlFetcher, makeGqlFetcher } from '../networkHelpers'
|
||||
import { Label } from './../fragments/labelFragment'
|
||||
import { moveToFolderMutation } from '../mutations/moveToLibraryMutation'
|
||||
import useSWR from 'swr'
|
||||
|
||||
export interface ReadableItem {
|
||||
id: string
|
||||
title: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
export type LibraryItemsQueryInput = {
|
||||
limit: number
|
||||
sortDescending: boolean
|
||||
searchQuery?: string
|
||||
cursor?: string
|
||||
includeContent?: boolean
|
||||
}
|
||||
|
||||
type LibraryItemsQueryResponse = {
|
||||
itemsPages?: LibraryItemsData[]
|
||||
itemsDataError?: unknown
|
||||
isLoading: boolean
|
||||
isValidating: boolean
|
||||
error: boolean
|
||||
size: number
|
||||
setSize: (
|
||||
size: number | ((_size: number) => number)
|
||||
) => Promise<unknown[] | undefined>
|
||||
performActionOnItem: (action: LibraryItemAction, item: LibraryItem) => void
|
||||
mutate: () => void
|
||||
}
|
||||
|
||||
type LibraryItemsRawQueryResponse = {
|
||||
items: LibraryItemNode[]
|
||||
itemsDataError?: unknown
|
||||
isLoading: boolean
|
||||
isValidating: boolean
|
||||
error: boolean
|
||||
}
|
||||
|
||||
type LibraryItemAction =
|
||||
| 'archive'
|
||||
| 'unarchive'
|
||||
| 'delete'
|
||||
| 'mark-read'
|
||||
| 'mark-unread'
|
||||
| 'refresh'
|
||||
| 'unsubscribe'
|
||||
| 'update-item'
|
||||
| 'move-to-inbox'
|
||||
|
||||
export type LibraryItemsData = {
|
||||
search: LibraryItems
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
export type LibraryItems = {
|
||||
edges: LibraryItem[]
|
||||
pageInfo: PageInfo
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
export type LibraryItem = {
|
||||
cursor: string
|
||||
node: LibraryItemNode
|
||||
isLoading?: boolean | undefined
|
||||
}
|
||||
|
||||
export type LibraryItemNode = {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
author?: string
|
||||
image?: string
|
||||
createdAt: string
|
||||
publishedAt?: string
|
||||
contentReader?: ContentReader
|
||||
originalArticleUrl: string
|
||||
readingProgressPercent: number
|
||||
readingProgressTopPercent?: number
|
||||
readingProgressAnchorIndex: number
|
||||
slug: string
|
||||
folder?: string
|
||||
isArchived: boolean
|
||||
description: string
|
||||
ownedByViewer: boolean
|
||||
uploadFileId: string
|
||||
labels?: Label[]
|
||||
pageId: string
|
||||
shortId: string
|
||||
quote: string
|
||||
annotation: string
|
||||
state: State
|
||||
pageType: PageType
|
||||
siteName?: string
|
||||
siteIcon?: string
|
||||
subscription?: string
|
||||
readAt?: string
|
||||
savedAt?: string
|
||||
wordsCount?: number
|
||||
aiSummary?: string
|
||||
recommendations?: Recommendation[]
|
||||
highlights?: Highlight[]
|
||||
}
|
||||
|
||||
export type Recommendation = {
|
||||
id: string
|
||||
name: string
|
||||
note?: string
|
||||
user?: RecommendingUser
|
||||
recommendedAt: Date
|
||||
}
|
||||
|
||||
export type RecommendingUser = {
|
||||
userId: string
|
||||
name: string
|
||||
username: string
|
||||
profileImageURL?: string
|
||||
}
|
||||
|
||||
export type PageInfo = {
|
||||
hasNextPage: boolean
|
||||
hasPreviousPage: boolean
|
||||
startCursor: string
|
||||
endCursor: string
|
||||
totalCount: number
|
||||
}
|
||||
|
||||
export const recommendationFragment = gql`
|
||||
fragment RecommendationFields on Recommendation {
|
||||
id
|
||||
name
|
||||
note
|
||||
user {
|
||||
userId
|
||||
name
|
||||
username
|
||||
profileImageURL
|
||||
}
|
||||
recommendedAt
|
||||
}
|
||||
`
|
||||
|
||||
export function useGetLibraryItemsQuery(
|
||||
folder: string,
|
||||
{ limit, searchQuery, cursor, includeContent = false }: LibraryItemsQueryInput
|
||||
): LibraryItemsQueryResponse {
|
||||
const fullQuery = (`in:${folder} use:folders ` + (searchQuery ?? '')).trim()
|
||||
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
|
||||
pageType
|
||||
contentReader
|
||||
createdAt
|
||||
isArchived
|
||||
readingProgressPercent
|
||||
readingProgressTopPercent
|
||||
readingProgressAnchorIndex
|
||||
author
|
||||
image
|
||||
description
|
||||
publishedAt
|
||||
ownedByViewer
|
||||
originalArticleUrl
|
||||
uploadFileId
|
||||
labels {
|
||||
id
|
||||
name
|
||||
color
|
||||
}
|
||||
pageId
|
||||
shortId
|
||||
quote
|
||||
annotation
|
||||
state
|
||||
siteName
|
||||
siteIcon
|
||||
subscription
|
||||
readAt
|
||||
savedAt
|
||||
wordsCount
|
||||
recommendations {
|
||||
id
|
||||
name
|
||||
note
|
||||
user {
|
||||
userId
|
||||
name
|
||||
username
|
||||
profileImageURL
|
||||
}
|
||||
recommendedAt
|
||||
}
|
||||
highlights {
|
||||
...HighlightFields
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
... on SearchError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${highlightFragment}
|
||||
`
|
||||
|
||||
const variables = {
|
||||
after: cursor,
|
||||
first: limit,
|
||||
query: fullQuery,
|
||||
includeContent,
|
||||
}
|
||||
|
||||
const { data, error, mutate, size, setSize, isValidating } = useSWRInfinite(
|
||||
(pageIndex, previousPageData) => {
|
||||
const key = [query, variables.first, variables.query, undefined]
|
||||
const previousResult = previousPageData as LibraryItemsData
|
||||
if (pageIndex === 0) {
|
||||
return key
|
||||
}
|
||||
return [
|
||||
query,
|
||||
limit,
|
||||
searchQuery,
|
||||
pageIndex === 0 ? undefined : previousResult.search.pageInfo.endCursor,
|
||||
]
|
||||
},
|
||||
(args: any[]) => {
|
||||
const pageIndex = args[3] as number
|
||||
return gqlFetcher(query, { ...variables, after: pageIndex }, true)
|
||||
},
|
||||
{ revalidateFirstPage: false }
|
||||
)
|
||||
|
||||
let responseError = error
|
||||
let responsePages = 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 (!error && responsePages) {
|
||||
const errors = responsePages.filter(
|
||||
(d) => d.search.errorCodes && d.search.errorCodes.length > 0
|
||||
)
|
||||
if (errors?.length > 0) {
|
||||
responseError = errors
|
||||
responsePages = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const getIndexOf = (page: LibraryItems, item: LibraryItem) => {
|
||||
return page.edges.findIndex((i) => i.node.id === item.node.id)
|
||||
}
|
||||
|
||||
const performActionOnItem = async (
|
||||
action: LibraryItemAction,
|
||||
item: LibraryItem
|
||||
) => {
|
||||
console.log('performing action on items: ', action)
|
||||
if (!responsePages) {
|
||||
return
|
||||
}
|
||||
|
||||
const updateData = (mutatedItem: LibraryItem | undefined) => {
|
||||
if (!responsePages) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const searchResults of responsePages) {
|
||||
const itemIndex = getIndexOf(searchResults.search, item)
|
||||
if (itemIndex !== -1) {
|
||||
if (typeof mutatedItem === 'undefined') {
|
||||
searchResults.search.edges.splice(itemIndex, 1)
|
||||
} else {
|
||||
searchResults.search.edges.splice(itemIndex, 1, mutatedItem)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
mutate(responsePages, false)
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'move-to-inbox':
|
||||
updateData({
|
||||
cursor: item.cursor,
|
||||
node: {
|
||||
...item.node,
|
||||
folder: 'inbox',
|
||||
},
|
||||
})
|
||||
|
||||
moveToFolderMutation(item.cursor, 'inbox').then((res) => {
|
||||
if (res) {
|
||||
showSuccessToast('Link moved', { position: 'bottom-right' })
|
||||
} else {
|
||||
showErrorToast('Error moving link', { position: 'bottom-right' })
|
||||
}
|
||||
})
|
||||
|
||||
mutate()
|
||||
break
|
||||
case 'archive':
|
||||
updateData({
|
||||
cursor: item.cursor,
|
||||
node: {
|
||||
...item.node,
|
||||
isArchived: true,
|
||||
},
|
||||
})
|
||||
|
||||
setLinkArchivedMutation({
|
||||
linkId: item.node.id,
|
||||
archived: true,
|
||||
}).then((res) => {
|
||||
if (res) {
|
||||
showSuccessToast('Link archived', { position: 'bottom-right' })
|
||||
} else {
|
||||
showErrorToast('Error archiving link', { position: 'bottom-right' })
|
||||
}
|
||||
})
|
||||
|
||||
mutate()
|
||||
|
||||
break
|
||||
case 'unarchive':
|
||||
updateData({
|
||||
cursor: item.cursor,
|
||||
node: {
|
||||
...item.node,
|
||||
isArchived: false,
|
||||
},
|
||||
})
|
||||
|
||||
setLinkArchivedMutation({
|
||||
linkId: item.node.id,
|
||||
archived: false,
|
||||
}).then((res) => {
|
||||
if (res) {
|
||||
showSuccessToast('Link unarchived', { position: 'bottom-right' })
|
||||
} else {
|
||||
showErrorToast('Error unarchiving link', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
})
|
||||
mutate()
|
||||
break
|
||||
case 'delete':
|
||||
updateData({
|
||||
cursor: item.cursor,
|
||||
node: {
|
||||
...item.node,
|
||||
state: State.DELETED,
|
||||
},
|
||||
})
|
||||
|
||||
const pageId = item.node.id
|
||||
deleteLinkMutation(pageId).then((res) => {
|
||||
if (res) {
|
||||
showSuccessToastWithUndo('Page deleted', async () => {
|
||||
const result = await updatePageMutation({
|
||||
pageId: pageId,
|
||||
state: State.SUCCEEDED,
|
||||
})
|
||||
|
||||
mutate()
|
||||
|
||||
if (result) {
|
||||
showSuccessToast('Page recovered')
|
||||
} else {
|
||||
showErrorToast(
|
||||
'Error recovering page, check your deleted items'
|
||||
)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
showErrorToast('Error removing link', { position: 'bottom-right' })
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'mark-read':
|
||||
updateData({
|
||||
cursor: item.cursor,
|
||||
node: {
|
||||
...item.node,
|
||||
readingProgressPercent: 100,
|
||||
readingProgressTopPercent: 100,
|
||||
},
|
||||
})
|
||||
articleReadingProgressMutation({
|
||||
id: item.node.id,
|
||||
force: true,
|
||||
readingProgressPercent: 100,
|
||||
readingProgressTopPercent: 100,
|
||||
readingProgressAnchorIndex: 0,
|
||||
})
|
||||
mutate()
|
||||
break
|
||||
case 'mark-unread':
|
||||
updateData({
|
||||
cursor: item.cursor,
|
||||
node: {
|
||||
...item.node,
|
||||
readingProgressPercent: 0,
|
||||
readingProgressTopPercent: 0,
|
||||
readingProgressAnchorIndex: 0,
|
||||
},
|
||||
})
|
||||
articleReadingProgressMutation({
|
||||
id: item.node.id,
|
||||
force: true,
|
||||
readingProgressPercent: 0,
|
||||
readingProgressTopPercent: 0,
|
||||
readingProgressAnchorIndex: 0,
|
||||
})
|
||||
mutate()
|
||||
break
|
||||
case 'update-item':
|
||||
updateData(item)
|
||||
mutate()
|
||||
break
|
||||
case 'refresh':
|
||||
await mutate()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValidating,
|
||||
itemsPages: responsePages || undefined,
|
||||
itemsDataError: responseError,
|
||||
isLoading: !error && !data,
|
||||
performActionOnItem,
|
||||
size,
|
||||
setSize,
|
||||
mutate,
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
const responseError = error
|
||||
const responseData = data as LibraryItemsData | undefined
|
||||
|
||||
// We need to check the response errors here and return the error
|
||||
// it will be nested in the data pages, if there is one error,
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import useSWR from 'swr'
|
||||
import { publicGqlFetcher } from '../networkHelpers'
|
||||
import {
|
||||
SavedSearch,
|
||||
savedSearchFragment,
|
||||
} from '../fragments/savedSearchFragment'
|
||||
|
||||
type SavedSearchResponse = {
|
||||
error: any
|
||||
savedSearches?: SavedSearch[]
|
||||
savedSearchErrors?: unknown
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
type SavedSearchResponseData = {
|
||||
filters: { filters: SavedSearch[] }
|
||||
}
|
||||
|
||||
export function useGetSavedSearchQuery(): SavedSearchResponse {
|
||||
const query = gql`
|
||||
query SavedSearches {
|
||||
filters {
|
||||
... on FiltersSuccess {
|
||||
filters {
|
||||
...FiltersFragment
|
||||
}
|
||||
}
|
||||
... on FiltersError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${savedSearchFragment}
|
||||
`
|
||||
|
||||
const { data, error } = useSWR(query, publicGqlFetcher)
|
||||
|
||||
if (data) {
|
||||
const { filters } = data as SavedSearchResponseData
|
||||
|
||||
return {
|
||||
error,
|
||||
savedSearches: filters?.filters ?? [],
|
||||
savedSearchErrors: error ?? {},
|
||||
isLoading: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
savedSearches: [],
|
||||
savedSearchErrors: null,
|
||||
isLoading: !error && !data,
|
||||
}
|
||||
}
|
||||
68
packages/web/lib/networking/savedsearches/gql.tsx
Normal file
68
packages/web/lib/networking/savedsearches/gql.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { savedSearchFragment } from '../fragments/savedSearchFragment'
|
||||
|
||||
export const GQL_GET_SAVED_SEARCHES = gql`
|
||||
query SavedSearches {
|
||||
filters {
|
||||
... on FiltersSuccess {
|
||||
filters {
|
||||
...FiltersFragment
|
||||
}
|
||||
}
|
||||
... on FiltersError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${savedSearchFragment}
|
||||
`
|
||||
|
||||
export const GQL_DELETE_SAVED_SEARCH = gql`
|
||||
mutation DeleteFilter($id: ID!) {
|
||||
deleteFilter(id: $id) {
|
||||
... on DeleteFilterSuccess {
|
||||
filter {
|
||||
...FiltersFragment
|
||||
}
|
||||
}
|
||||
... on DeleteFilterError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${savedSearchFragment}
|
||||
`
|
||||
|
||||
export const GQL_CREATE_SAVED_SEARCH = gql`
|
||||
mutation SaveFilter($input: SaveFilterInput!) {
|
||||
saveFilter(input: $input) {
|
||||
... on SaveFilterSuccess {
|
||||
filter {
|
||||
...FiltersFragment
|
||||
}
|
||||
}
|
||||
|
||||
... on SaveFilterError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${savedSearchFragment}
|
||||
`
|
||||
|
||||
export const GQL_UPDATE_SAVED_SEARCH = gql`
|
||||
mutation UpdateFilter($input: UpdateFilterInput!) {
|
||||
updateFilter(input: $input) {
|
||||
... on UpdateFilterSuccess {
|
||||
filter {
|
||||
...FiltersFragment
|
||||
}
|
||||
}
|
||||
|
||||
... on UpdateFilterError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${savedSearchFragment}
|
||||
`
|
||||
173
packages/web/lib/networking/savedsearches/useSavedSearches.tsx
Normal file
173
packages/web/lib/networking/savedsearches/useSavedSearches.tsx
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import {
|
||||
GQL_CREATE_SAVED_SEARCH,
|
||||
GQL_DELETE_SAVED_SEARCH,
|
||||
GQL_GET_SAVED_SEARCHES,
|
||||
GQL_UPDATE_SAVED_SEARCH,
|
||||
} from './gql'
|
||||
import { SavedSearch } from '../fragments/savedSearchFragment'
|
||||
|
||||
export function useGetSavedSearches() {
|
||||
return useQuery({
|
||||
queryKey: ['filters'],
|
||||
queryFn: async () => {
|
||||
const response = (await gqlFetcher(
|
||||
GQL_GET_SAVED_SEARCHES
|
||||
)) as SavedSearchData
|
||||
if (response.filters?.errorCodes?.length) {
|
||||
throw new Error(response.filters.errorCodes[0])
|
||||
}
|
||||
return response.filters.filters
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateSavedSearch = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const createSavedSearch = async (variables: {
|
||||
name: string
|
||||
filter: string
|
||||
category: string
|
||||
position: number
|
||||
}) => {
|
||||
const result = (await gqlFetcher(GQL_CREATE_SAVED_SEARCH, {
|
||||
input: {
|
||||
name: variables.name,
|
||||
filter: variables.filter,
|
||||
category: variables.category,
|
||||
position: variables.position,
|
||||
},
|
||||
})) as CreateSavedSearchData
|
||||
if (result.saveFilter.errorCodes?.length) {
|
||||
throw new Error(result.saveFilter.errorCodes[0])
|
||||
}
|
||||
return result.saveFilter?.filter
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: createSavedSearch,
|
||||
onSuccess: (newSavedSearch) => {
|
||||
const keys = queryClient
|
||||
.getQueryCache()
|
||||
.findAll({ queryKey: ['filters'] })
|
||||
keys.forEach((query) => {
|
||||
queryClient.setQueryData(query.queryKey, (data: SavedSearch[]) => {
|
||||
return [...data, newSavedSearch]
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateSavedSearch = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const updateSavedSearch = async (variables: {
|
||||
input: UpdateSavedSearchInput
|
||||
}) => {
|
||||
const result = (await gqlFetcher(GQL_UPDATE_SAVED_SEARCH, {
|
||||
input: {
|
||||
id: variables.input.id,
|
||||
name: variables.input.name,
|
||||
visible: variables.input.visible,
|
||||
filter: variables.input.filter,
|
||||
position: variables.input.position,
|
||||
},
|
||||
})) as UpdateSavedSearchData
|
||||
if (result.updateFilter.errorCodes?.length) {
|
||||
throw new Error(result.updateFilter.errorCodes[0])
|
||||
}
|
||||
return result.updateFilter?.filter
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: updateSavedSearch,
|
||||
onSuccess: (updatedSavedSearch) => {
|
||||
if (updatedSavedSearch) {
|
||||
const keys = queryClient
|
||||
.getQueryCache()
|
||||
.findAll({ queryKey: ['filters'] })
|
||||
keys.forEach((query) => {
|
||||
queryClient.setQueryData(query.queryKey, (data: SavedSearch[]) => {
|
||||
return [
|
||||
...data.filter(
|
||||
(savedSearch) => savedSearch.id !== updatedSavedSearch.id
|
||||
),
|
||||
updatedSavedSearch,
|
||||
]
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteSavedSearch = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const deleteSavedSearch = async (variables: { searchId: string }) => {
|
||||
const result = (await gqlFetcher(GQL_DELETE_SAVED_SEARCH, {
|
||||
id: variables.searchId,
|
||||
})) as DeleteSavedSearchData
|
||||
if (result.deleteFilter.errorCodes?.length) {
|
||||
throw new Error(result.deleteFilter.errorCodes[0])
|
||||
}
|
||||
return result.deleteFilter?.filter?.id
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: deleteSavedSearch,
|
||||
onSuccess: (deletedId) => {
|
||||
if (deletedId) {
|
||||
const keys = queryClient
|
||||
.getQueryCache()
|
||||
.findAll({ queryKey: ['filters'] })
|
||||
keys.forEach((query) => {
|
||||
queryClient.setQueryData(query.queryKey, (data: SavedSearch[]) => {
|
||||
return data.filter((filter) => filter.id !== deletedId)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type UpdateSavedSearchResult = {
|
||||
filter?: SavedSearch
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type UpdateSavedSearchData = {
|
||||
updateFilter: UpdateSavedSearchResult
|
||||
}
|
||||
|
||||
type CreateSavedSearchResult = {
|
||||
filter?: SavedSearch
|
||||
errorCodes?: string[]
|
||||
}
|
||||
type CreateSavedSearchData = {
|
||||
saveFilter: CreateSavedSearchResult
|
||||
}
|
||||
|
||||
type DeleteSavedSearchResult = {
|
||||
filter?: SavedSearch
|
||||
errorCodes?: string[]
|
||||
}
|
||||
type DeleteSavedSearchData = {
|
||||
deleteFilter: DeleteSavedSearchResult
|
||||
}
|
||||
|
||||
type FiltersResult = {
|
||||
filters?: SavedSearch[]
|
||||
errorCodes?: string[]
|
||||
}
|
||||
type SavedSearchData = {
|
||||
filters: FiltersResult
|
||||
}
|
||||
|
||||
export type UpdateSavedSearchInput = {
|
||||
id?: string
|
||||
name?: string
|
||||
filter?: string
|
||||
position?: number
|
||||
category?: string
|
||||
description?: string
|
||||
visible?: boolean
|
||||
folder?: string
|
||||
}
|
||||
135
packages/web/lib/networking/shortcuts/useShortcuts.tsx
Normal file
135
packages/web/lib/networking/shortcuts/useShortcuts.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { requestHeaders } from '../networkHelpers'
|
||||
import { fetchEndpoint } from '../../appConfig'
|
||||
import { Label } from '../fragments/labelFragment'
|
||||
|
||||
export type ShortcutType = 'search' | 'label' | 'newsletter' | 'feed' | 'folder'
|
||||
|
||||
export type Shortcut = {
|
||||
type: ShortcutType
|
||||
|
||||
id: string
|
||||
name: string
|
||||
section: string
|
||||
filter: string
|
||||
|
||||
icon?: string
|
||||
label?: Label
|
||||
|
||||
join?: string
|
||||
|
||||
children?: Shortcut[]
|
||||
}
|
||||
|
||||
export function useGetShortcuts() {
|
||||
return useQuery({
|
||||
queryKey: ['shortcuts'],
|
||||
queryFn: async () => {
|
||||
return await getShortcuts()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useSetShortcuts = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (variables: { shortcuts: Shortcut[] }) => {
|
||||
return await setShortcuts(variables)
|
||||
},
|
||||
onMutate: async (variables: { shortcuts: Shortcut[] }) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['shortcuts'] })
|
||||
queryClient.setQueryData(['shortcuts'], variables.shortcuts)
|
||||
const previousState = {
|
||||
previousItems: queryClient.getQueryData(['shortcuts']),
|
||||
}
|
||||
return previousState
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (context?.previousItems) {
|
||||
queryClient.setQueryData(['shortcuts'], context.previousItems)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useResetShortcuts = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
return await resetShortcuts()
|
||||
},
|
||||
onMutate: async () => {
|
||||
const previousState = {
|
||||
previousItems: queryClient.getQueryData(['shortcuts']),
|
||||
}
|
||||
return previousState
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (context?.previousItems) {
|
||||
queryClient.setQueryData(['shortcuts'], context.previousItems)
|
||||
}
|
||||
},
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.setQueryData(['shortcuts'], data)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function getShortcuts(): Promise<Shortcut[]> {
|
||||
const url = new URL(`/api/shortcuts`, fetchEndpoint)
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: requestHeaders(),
|
||||
credentials: 'include',
|
||||
mode: 'cors',
|
||||
})
|
||||
const payload = await response.json()
|
||||
if ('shortcuts' in payload) {
|
||||
return payload['shortcuts'] as Shortcut[]
|
||||
}
|
||||
return []
|
||||
} catch (err) {
|
||||
console.log('error getting shortcuts: ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function setShortcuts(variables: {
|
||||
shortcuts: Shortcut[]
|
||||
}): Promise<Shortcut[]> {
|
||||
const url = new URL(`/api/shortcuts`, fetchEndpoint)
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...requestHeaders(),
|
||||
},
|
||||
credentials: 'include',
|
||||
mode: 'cors',
|
||||
body: JSON.stringify({ shortcuts: variables.shortcuts }),
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!('shortcuts' in payload)) {
|
||||
throw new Error('Error syncing shortcuts')
|
||||
}
|
||||
return payload['shortcuts'] as Shortcut[]
|
||||
}
|
||||
|
||||
async function resetShortcuts(): Promise<Shortcut[]> {
|
||||
const url = new URL(`/api/shortcuts`, fetchEndpoint)
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...requestHeaders(),
|
||||
},
|
||||
credentials: 'include',
|
||||
mode: 'cors',
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!('shortcuts' in payload)) {
|
||||
throw new Error('Error syncing shortcuts')
|
||||
}
|
||||
return payload['shortcuts'] as Shortcut[]
|
||||
}
|
||||
|
|
@ -114,6 +114,8 @@ const showToastWithAction = (
|
|||
action: () => Promise<void>,
|
||||
options?: ToastOptions
|
||||
) => {
|
||||
console.trace('show success: ', message)
|
||||
|
||||
return toast(
|
||||
({ id }) => (
|
||||
<FullWidthContainer alignment="center">
|
||||
|
|
@ -124,7 +126,6 @@ const showToastWithAction = (
|
|||
style="ctaLightGray"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
|
||||
toast.dismiss(id)
|
||||
;(async () => {
|
||||
await action()
|
||||
|
|
|
|||
|
|
@ -49,6 +49,34 @@ const moduleExports = {
|
|||
source: '/collect/:match*',
|
||||
destination: 'https://app.posthog.com/:match*',
|
||||
})
|
||||
rewrites.push({
|
||||
source: '/home',
|
||||
destination: '/l/home',
|
||||
})
|
||||
rewrites.push({
|
||||
source: '/library',
|
||||
destination: '/l/library',
|
||||
})
|
||||
rewrites.push({
|
||||
source: '/subscriptions',
|
||||
destination: '/l/subscriptions',
|
||||
})
|
||||
rewrites.push({
|
||||
source: '/highlights',
|
||||
destination: '/l/highlights',
|
||||
})
|
||||
rewrites.push({
|
||||
source: '/subscriptions',
|
||||
destination: '/l/subscriptions',
|
||||
})
|
||||
rewrites.push({
|
||||
source: '/archive',
|
||||
destination: '/l/archive',
|
||||
})
|
||||
rewrites.push({
|
||||
source: '/trash',
|
||||
destination: '/l/trash',
|
||||
})
|
||||
return rewrites
|
||||
},
|
||||
async headers() {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"@radix-ui/react-switch": "^1.0.1",
|
||||
"@sentry/nextjs": "^7.42.0",
|
||||
"@stitches/react": "^1.2.5",
|
||||
"@tanstack/react-query": "^5.51.21",
|
||||
"allotment": "^1.20.2",
|
||||
"antd": "4.24.3",
|
||||
"axios": "^1.2.0",
|
||||
|
|
@ -108,4 +109,4 @@
|
|||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { applyStoredTheme } from '../../../lib/themeUpdater'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
|
|
@ -7,6 +6,7 @@ import {
|
|||
HStack,
|
||||
SpanBox,
|
||||
} from '../../../components/elements/LayoutPrimitives'
|
||||
import { useGetLibraryItemContent } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
|
||||
type ArticleAttribute = {
|
||||
name: string
|
||||
|
|
@ -15,11 +15,10 @@ type ArticleAttribute = {
|
|||
|
||||
export default function Debug(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const { articleData, articleFetchError, isLoading } = useGetArticleQuery({
|
||||
username: router.query.username as string,
|
||||
slug: router.query.slug as string,
|
||||
includeFriendsHighlights: false,
|
||||
})
|
||||
const { data: article } = useGetLibraryItemContent(
|
||||
router.query.username as string,
|
||||
router.query.slug as string
|
||||
)
|
||||
|
||||
applyStoredTheme()
|
||||
|
||||
|
|
@ -30,13 +29,11 @@ export default function Debug(): JSX.Element {
|
|||
// return sortedAttributes.sort((a, b) =>
|
||||
// a.createdAt.localeCompare(b.createdAt)
|
||||
// )
|
||||
if (!articleData?.article.article) {
|
||||
if (!article) {
|
||||
return []
|
||||
}
|
||||
const result: ArticleAttribute[] = []
|
||||
|
||||
const article = articleData.article.article
|
||||
|
||||
result.push({ name: 'id', value: article.id })
|
||||
result.push({ name: 'linkId', value: article.linkId })
|
||||
|
||||
|
|
@ -57,8 +54,6 @@ export default function Debug(): JSX.Element {
|
|||
result.push({ name: 'savedAt', value: article.savedAt })
|
||||
result.push({ name: 'createdAt', value: article.createdAt })
|
||||
result.push({ name: 'publishedAt', value: article.publishedAt ?? 'null' })
|
||||
|
||||
result.push({ name: 'isArchived', value: article.isArchived.toString() })
|
||||
result.push({ name: 'description', value: article.description ?? 'null' })
|
||||
|
||||
result.push({
|
||||
|
|
@ -173,7 +168,7 @@ export default function Debug(): JSX.Element {
|
|||
// recommendations?: Recommendation[]
|
||||
|
||||
return result
|
||||
}, [articleData])
|
||||
}, [article])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import { PrimaryLayout } from '../../../components/templates/PrimaryLayout'
|
||||
import { LoadingView } from '../../../components/patterns/LoadingView'
|
||||
import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import {
|
||||
removeItemFromCache,
|
||||
useGetArticleQuery,
|
||||
} from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { useRouter } from 'next/router'
|
||||
import { VStack } from './../../../components/elements/LayoutPrimitives'
|
||||
import {
|
||||
|
|
@ -15,37 +11,37 @@ import { PdfArticleContainerProps } from './../../../components/templates/articl
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation'
|
||||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation'
|
||||
import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation'
|
||||
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
|
||||
import Script from 'next/script'
|
||||
import { ArticleActionsMenu } from '../../../components/templates/article/ArticleActionsMenu'
|
||||
import { setLinkArchivedMutation } from '../../../lib/networking/mutations/setLinkArchivedMutation'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { useSWRConfig } from 'swr'
|
||||
import {
|
||||
showErrorToast,
|
||||
showSuccessToast,
|
||||
showSuccessToastWithUndo,
|
||||
} from '../../../lib/toastHelpers'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { SetLabelsModal } from '../../../components/templates/article/SetLabelsModal'
|
||||
import { DisplaySettingsModal } from '../../../components/templates/article/DisplaySettingsModal'
|
||||
import { useReaderSettings } from '../../../lib/hooks/useReaderSettings'
|
||||
import { SkeletonArticleContainer } from '../../../components/templates/article/SkeletonArticleContainer'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
import { deleteLinkMutation } from '../../../lib/networking/mutations/deleteLinkMutation'
|
||||
import { ReaderHeader } from '../../../components/templates/reader/ReaderHeader'
|
||||
import { EditArticleModal } from '../../../components/templates/homeFeed/EditItemModals'
|
||||
import { VerticalArticleActionsMenu } from '../../../components/templates/article/VerticalArticleActions'
|
||||
import { PdfHeaderSpacer } from '../../../components/templates/article/PdfHeaderSpacer'
|
||||
import { EpubContainerProps } from '../../../components/templates/article/EpubContainer'
|
||||
import { useSetPageLabels } from '../../../lib/hooks/useSetPageLabels'
|
||||
import { updatePageMutation } from '../../../lib/networking/mutations/updatePageMutation'
|
||||
import { State } from '../../../lib/networking/fragments/articleFragment'
|
||||
import { posthog } from 'posthog-js'
|
||||
import { PDFDisplaySettingsModal } from '../../../components/templates/article/PDFDisplaySettingsModal'
|
||||
import {
|
||||
ArticleReadingProgressMutationInput,
|
||||
useArchiveItem,
|
||||
useDeleteItem,
|
||||
useGetLibraryItemContent,
|
||||
useUpdateItemReadStatus,
|
||||
} from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import {
|
||||
CreateHighlightInput,
|
||||
useCreateHighlight,
|
||||
useDeleteHighlight,
|
||||
useMergeHighlight,
|
||||
useUpdateHighlight,
|
||||
} from '../../../lib/networking/highlights/useItemHighlights'
|
||||
|
||||
const PdfArticleContainerNoSSR = dynamic<PdfArticleContainerProps>(
|
||||
() => import('./../../../components/templates/article/PdfArticleContainer'),
|
||||
|
|
@ -57,26 +53,31 @@ const EpubContainerNoSSR = dynamic<EpubContainerProps>(
|
|||
{ ssr: false }
|
||||
)
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
export default function Reader(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const { cache, mutate } = useSWRConfig()
|
||||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
const [showHighlightsModal, setShowHighlightsModal] = useState(false)
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const readerSettings = useReaderSettings()
|
||||
const archiveItem = useArchiveItem()
|
||||
const deleteItem = useDeleteItem()
|
||||
const updateItemReadStatus = useUpdateItemReadStatus()
|
||||
const createHighlight = useCreateHighlight()
|
||||
const deleteHighlight = useDeleteHighlight()
|
||||
const updateHighlight = useUpdateHighlight()
|
||||
const mergeHighlight = useMergeHighlight()
|
||||
|
||||
const { articleData, articleFetchError } = useGetArticleQuery({
|
||||
username: router.query.username as string,
|
||||
slug: router.query.slug as string,
|
||||
includeFriendsHighlights: false,
|
||||
})
|
||||
const article = articleData?.article.article
|
||||
const { data: libraryItem, error: articleFetchError } =
|
||||
useGetLibraryItemContent(
|
||||
router.query.username as string,
|
||||
router.query.slug as string
|
||||
)
|
||||
useEffect(() => {
|
||||
dispatchLabels({
|
||||
type: 'RESET',
|
||||
labels: article?.labels ?? [],
|
||||
labels: libraryItem?.labels ?? [],
|
||||
})
|
||||
}, [articleData?.article.article])
|
||||
}, [libraryItem])
|
||||
|
||||
const goNextOrHome = useCallback(() => {
|
||||
// const listStr = localStorage.getItem('library-slug-list')
|
||||
|
|
@ -97,7 +98,7 @@ export default function Home(): JSX.Element {
|
|||
|
||||
const query = window.sessionStorage.getItem('q')
|
||||
router.push(`/home?${query}`)
|
||||
}, [router, viewerData, article])
|
||||
}, [router, viewerData, libraryItem])
|
||||
|
||||
const goPreviousOrHome = useCallback(() => {
|
||||
// const listStr = localStorage.getItem('library-slug-list')
|
||||
|
|
@ -113,78 +114,88 @@ export default function Home(): JSX.Element {
|
|||
const query = window.sessionStorage.getItem('q')
|
||||
router.push(`/home?${query}`)
|
||||
// router.push(`/home`)
|
||||
}, [router, viewerData, article])
|
||||
}, [router, viewerData, libraryItem])
|
||||
|
||||
const actionHandler = useCallback(
|
||||
async (action: string, arg?: unknown) => {
|
||||
if (!libraryItem) {
|
||||
return
|
||||
}
|
||||
switch (action) {
|
||||
case 'unarchive':
|
||||
if (article) {
|
||||
removeItemFromCache(cache, mutate, article.id)
|
||||
|
||||
setLinkArchivedMutation({
|
||||
linkId: article.id,
|
||||
archived: false,
|
||||
}).then((res) => {
|
||||
if (res) {
|
||||
showSuccessToast('Link unarchived', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
} else {
|
||||
showErrorToast('Error unarchiving link', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
})
|
||||
goNextOrHome()
|
||||
}
|
||||
break
|
||||
case 'archive':
|
||||
if (article) {
|
||||
removeItemFromCache(cache, mutate, article.id)
|
||||
|
||||
await setLinkArchivedMutation({
|
||||
linkId: article.id,
|
||||
archived: true,
|
||||
}).then((res) => {
|
||||
if (!res) {
|
||||
showErrorToast('Error archiving', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
} else {
|
||||
goNextOrHome()
|
||||
showSuccessToast('Page archived', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
case 'unarchive':
|
||||
try {
|
||||
await archiveItem.mutateAsync({
|
||||
itemId: libraryItem.id,
|
||||
slug: libraryItem.slug,
|
||||
input: {
|
||||
linkId: libraryItem.id,
|
||||
archived: action == 'archive',
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
showErrorToast(`Error ${action}ing item`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
return
|
||||
}
|
||||
showSuccessToast(`Item ${action}d`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
goNextOrHome()
|
||||
break
|
||||
case 'mark-read':
|
||||
if (article) {
|
||||
articleReadingProgressMutation({
|
||||
id: article.id,
|
||||
force: true,
|
||||
readingProgressPercent: 100,
|
||||
readingProgressTopPercent: 100,
|
||||
readingProgressAnchorIndex: 0,
|
||||
}).then((res) => {
|
||||
if (!res) {
|
||||
// todo: revalidate or put back in cache?
|
||||
showErrorToast('Error marking as read', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
} else {
|
||||
goNextOrHome()
|
||||
}
|
||||
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({
|
||||
itemId: libraryItem.id,
|
||||
slug: libraryItem.slug,
|
||||
input: {
|
||||
id: libraryItem.id,
|
||||
force: true,
|
||||
...values,
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
showErrorToast(`Error marking as ${desc}`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
return
|
||||
}
|
||||
goNextOrHome()
|
||||
break
|
||||
case 'delete':
|
||||
await deleteCurrentItem()
|
||||
try {
|
||||
await deleteItem.mutateAsync({
|
||||
itemId: libraryItem.id,
|
||||
slug: libraryItem.slug,
|
||||
})
|
||||
} catch {
|
||||
showErrorToast(`Error deleting item`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
return
|
||||
}
|
||||
showSuccessToast(`Item deleted`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
goNextOrHome()
|
||||
break
|
||||
case 'openOriginalArticle':
|
||||
const url = article?.url
|
||||
const url = libraryItem?.url
|
||||
if (url) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
|
@ -206,7 +217,15 @@ export default function Home(): JSX.Element {
|
|||
break
|
||||
}
|
||||
},
|
||||
[article, viewerData, cache, mutate, router, readerSettings]
|
||||
[
|
||||
libraryItem,
|
||||
viewerData,
|
||||
router,
|
||||
readerSettings,
|
||||
archiveItem,
|
||||
deleteItem,
|
||||
updateItemReadStatus,
|
||||
]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -224,6 +243,10 @@ export default function Home(): JSX.Element {
|
|||
actionHandler('mark-read')
|
||||
}
|
||||
|
||||
const markUnread = () => {
|
||||
actionHandler('mark-unread')
|
||||
}
|
||||
|
||||
const showEditModal = () => {
|
||||
actionHandler('showEditModal')
|
||||
}
|
||||
|
|
@ -231,6 +254,7 @@ export default function Home(): JSX.Element {
|
|||
document.addEventListener('archive', archive)
|
||||
document.addEventListener('delete', deletePage)
|
||||
document.addEventListener('mark-read', markRead)
|
||||
document.addEventListener('mark-unread', markUnread)
|
||||
document.addEventListener('openOriginalArticle', openOriginalArticle)
|
||||
document.addEventListener('showEditModal', showEditModal)
|
||||
|
||||
|
|
@ -240,6 +264,8 @@ export default function Home(): JSX.Element {
|
|||
return () => {
|
||||
document.removeEventListener('archive', archive)
|
||||
document.removeEventListener('mark-read', markRead)
|
||||
document.removeEventListener('mark-unread', markUnread)
|
||||
|
||||
document.removeEventListener('delete', deletePage)
|
||||
document.removeEventListener('openOriginalArticle', openOriginalArticle)
|
||||
document.removeEventListener('showEditModal', showEditModal)
|
||||
|
|
@ -249,42 +275,15 @@ export default function Home(): JSX.Element {
|
|||
}, [actionHandler, goNextOrHome, goPreviousOrHome])
|
||||
|
||||
useEffect(() => {
|
||||
if (article && viewerData?.me) {
|
||||
if (libraryItem && viewerData?.me) {
|
||||
posthog.capture('link_read', {
|
||||
link: article.id,
|
||||
slug: article.slug,
|
||||
reader: article.contentReader,
|
||||
url: article.originalArticleUrl,
|
||||
link: libraryItem.id,
|
||||
slug: libraryItem.slug,
|
||||
reader: libraryItem.contentReader,
|
||||
url: libraryItem.originalArticleUrl,
|
||||
})
|
||||
}
|
||||
}, [article, viewerData])
|
||||
|
||||
const deleteCurrentItem = useCallback(async () => {
|
||||
if (article) {
|
||||
const pageId = article.id
|
||||
removeItemFromCache(cache, mutate, pageId)
|
||||
await deleteLinkMutation(pageId).then((res) => {
|
||||
if (res) {
|
||||
showSuccessToastWithUndo('Page deleted', async () => {
|
||||
const result = await updatePageMutation({
|
||||
pageId: pageId,
|
||||
state: State.SUCCEEDED,
|
||||
})
|
||||
document.dispatchEvent(new Event('revalidateLibrary'))
|
||||
if (result) {
|
||||
showSuccessToast('Page recovered')
|
||||
} else {
|
||||
showErrorToast('Error recovering page, check your deleted items')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// todo: revalidate or put back in cache?
|
||||
showErrorToast('Error deleting page', { position: 'bottom-right' })
|
||||
}
|
||||
})
|
||||
goNextOrHome()
|
||||
}
|
||||
}, [article, cache, mutate, router])
|
||||
}, [libraryItem, viewerData])
|
||||
|
||||
useRegisterActions(
|
||||
[
|
||||
|
|
@ -360,6 +359,15 @@ export default function Home(): JSX.Element {
|
|||
document.dispatchEvent(new Event('mark-read'))
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mark_unread',
|
||||
section: 'Article',
|
||||
name: 'Mark current item as unread',
|
||||
shortcut: ['-'],
|
||||
perform: () => {
|
||||
document.dispatchEvent(new Event('mark-unread'))
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'full_screen',
|
||||
section: 'Article',
|
||||
|
|
@ -457,10 +465,16 @@ export default function Home(): JSX.Element {
|
|||
)
|
||||
|
||||
const [labels, dispatchLabels] = useSetPageLabels(
|
||||
articleData?.article.article?.id
|
||||
libraryItem?.id,
|
||||
libraryItem?.slug
|
||||
)
|
||||
|
||||
if (articleFetchError && articleFetchError.indexOf('NOT_FOUND') > -1) {
|
||||
new Error()
|
||||
if (
|
||||
articleFetchError &&
|
||||
'message' in articleFetchError &&
|
||||
articleFetchError['message'] === 'NOT_FOUND'
|
||||
) {
|
||||
router.push('/404')
|
||||
return <LoadingView />
|
||||
}
|
||||
|
|
@ -470,18 +484,18 @@ export default function Home(): JSX.Element {
|
|||
pageTestId="home-page-tag"
|
||||
headerToolbarControl={
|
||||
<ArticleActionsMenu
|
||||
article={article}
|
||||
article={libraryItem}
|
||||
layout="top"
|
||||
showReaderDisplaySettings={article?.contentReader != 'PDF'}
|
||||
showReaderDisplaySettings={libraryItem?.contentReader != 'PDF'}
|
||||
readerSettings={readerSettings}
|
||||
articleActionHandler={actionHandler}
|
||||
/>
|
||||
}
|
||||
alwaysDisplayToolbar={article?.contentReader == 'PDF'}
|
||||
alwaysDisplayToolbar={libraryItem?.contentReader == 'PDF'}
|
||||
pageMetaDataProps={{
|
||||
title: article?.title ?? '',
|
||||
title: libraryItem?.title ?? '',
|
||||
path: router.pathname,
|
||||
description: article?.description ?? '',
|
||||
description: libraryItem?.description ?? '',
|
||||
}}
|
||||
>
|
||||
<Script async src="/static/mathjax/mathJaxConfiguration.js" />
|
||||
|
|
@ -497,17 +511,17 @@ export default function Home(): JSX.Element {
|
|||
showDisplaySettingsModal={
|
||||
readerSettings.setShowEditDisplaySettingsModal
|
||||
}
|
||||
alwaysDisplayToolbar={article?.contentReader == 'PDF'}
|
||||
alwaysDisplayToolbar={libraryItem?.contentReader == 'PDF'}
|
||||
>
|
||||
<VerticalArticleActionsMenu
|
||||
article={article}
|
||||
article={libraryItem}
|
||||
layout="top"
|
||||
showReaderDisplaySettings={article?.contentReader != 'PDF'}
|
||||
showReaderDisplaySettings={libraryItem?.contentReader != 'PDF'}
|
||||
articleActionHandler={actionHandler}
|
||||
/>
|
||||
</ReaderHeader>
|
||||
|
||||
{article?.contentReader == 'PDF' && <PdfHeaderSpacer />}
|
||||
{libraryItem?.contentReader == 'PDF' && <PdfHeaderSpacer />}
|
||||
|
||||
<VStack
|
||||
distribution="between"
|
||||
|
|
@ -523,9 +537,9 @@ export default function Home(): JSX.Element {
|
|||
},
|
||||
}}
|
||||
>
|
||||
{article?.contentReader !== 'PDF' ? (
|
||||
{libraryItem?.contentReader !== 'PDF' ? (
|
||||
<ArticleActionsMenu
|
||||
article={article}
|
||||
article={libraryItem}
|
||||
layout="side"
|
||||
readerSettings={readerSettings}
|
||||
showReaderDisplaySettings={true}
|
||||
|
|
@ -533,15 +547,15 @@ export default function Home(): JSX.Element {
|
|||
/>
|
||||
) : null}
|
||||
</VStack>
|
||||
{article && viewerData?.me && article.contentReader == 'PDF' && (
|
||||
{libraryItem && viewerData?.me && libraryItem.contentReader == 'PDF' && (
|
||||
<PdfArticleContainerNoSSR
|
||||
article={article}
|
||||
article={libraryItem}
|
||||
showHighlightsModal={showHighlightsModal}
|
||||
setShowHighlightsModal={setShowHighlightsModal}
|
||||
viewer={viewerData.me}
|
||||
/>
|
||||
)}
|
||||
{article && viewerData?.me && article.contentReader == 'WEB' && (
|
||||
{libraryItem && viewerData?.me && libraryItem.contentReader == 'WEB' && (
|
||||
<VStack
|
||||
id="article-wrapper"
|
||||
alignment="center"
|
||||
|
|
@ -558,10 +572,10 @@ export default function Home(): JSX.Element {
|
|||
},
|
||||
}}
|
||||
>
|
||||
{article && viewerData?.me ? (
|
||||
{libraryItem && viewerData?.me ? (
|
||||
<ArticleContainer
|
||||
viewer={viewerData.me}
|
||||
article={article}
|
||||
article={libraryItem}
|
||||
isAppleAppEmbed={false}
|
||||
highlightBarDisabled={false}
|
||||
fontSize={readerSettings.fontSize}
|
||||
|
|
@ -577,14 +591,80 @@ export default function Home(): JSX.Element {
|
|||
readerSettings.highlightOnRelease ?? undefined
|
||||
}
|
||||
textDirection={
|
||||
article.directionality ?? readerSettings.textDirection
|
||||
libraryItem.directionality ?? readerSettings.textDirection
|
||||
}
|
||||
articleMutations={{
|
||||
createHighlightMutation,
|
||||
deleteHighlightMutation,
|
||||
mergeHighlightMutation,
|
||||
updateHighlightMutation,
|
||||
articleReadingProgressMutation,
|
||||
createHighlightMutation: async (
|
||||
input: CreateHighlightInput
|
||||
) => {
|
||||
try {
|
||||
const result = await createHighlight.mutateAsync({
|
||||
itemId: libraryItem.id,
|
||||
slug: libraryItem.slug,
|
||||
input,
|
||||
})
|
||||
return result
|
||||
} catch (err) {
|
||||
console.log('error creating highlight', err)
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
deleteHighlightMutation: async (
|
||||
libraryItemId,
|
||||
highlightId: string
|
||||
) => {
|
||||
try {
|
||||
await deleteHighlight.mutateAsync({
|
||||
itemId: libraryItem.id,
|
||||
slug: libraryItem.slug,
|
||||
highlightId,
|
||||
})
|
||||
return true
|
||||
} catch (err) {
|
||||
console.log('error deleting highlight', err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
mergeHighlightMutation: async (input) => {
|
||||
try {
|
||||
const result = await mergeHighlight.mutateAsync({
|
||||
itemId: libraryItem.id,
|
||||
slug: libraryItem.slug,
|
||||
input,
|
||||
})
|
||||
return result?.highlight
|
||||
} catch (err) {
|
||||
console.log('error merging highlight', err)
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
updateHighlightMutation: async (input) => {
|
||||
try {
|
||||
const result = await updateHighlight.mutateAsync({
|
||||
itemId: libraryItem.id,
|
||||
slug: libraryItem.slug,
|
||||
input,
|
||||
})
|
||||
return result?.id
|
||||
} catch (err) {
|
||||
console.log('error updating highlight', err)
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
articleReadingProgressMutation: async (
|
||||
input: ArticleReadingProgressMutationInput
|
||||
) => {
|
||||
try {
|
||||
await updateItemReadStatus.mutateAsync({
|
||||
itemId: libraryItem.id,
|
||||
slug: libraryItem.slug,
|
||||
input,
|
||||
})
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -597,7 +677,7 @@ export default function Home(): JSX.Element {
|
|||
</VStack>
|
||||
)}
|
||||
|
||||
{article && viewerData?.me && article.contentReader == 'EPUB' && (
|
||||
{libraryItem && viewerData?.me && libraryItem.contentReader == 'EPUB' && (
|
||||
<VStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
|
|
@ -610,9 +690,9 @@ export default function Home(): JSX.Element {
|
|||
paddingTop: '80px',
|
||||
}}
|
||||
>
|
||||
{article && viewerData?.me ? (
|
||||
{libraryItem && viewerData?.me ? (
|
||||
<EpubContainerNoSSR
|
||||
article={article}
|
||||
article={libraryItem}
|
||||
showHighlightsModal={showHighlightsModal}
|
||||
setShowHighlightsModal={setShowHighlightsModal}
|
||||
viewer={viewerData.me}
|
||||
|
|
@ -627,15 +707,15 @@ export default function Home(): JSX.Element {
|
|||
</VStack>
|
||||
)}
|
||||
|
||||
{article && readerSettings.showSetLabelsModal && (
|
||||
{libraryItem && readerSettings.showSetLabelsModal && (
|
||||
<SetLabelsModal
|
||||
provider={article}
|
||||
provider={libraryItem}
|
||||
selectedLabels={labels.labels}
|
||||
dispatchLabels={dispatchLabels}
|
||||
onOpenChange={() => readerSettings.setShowSetLabelsModal(false)}
|
||||
/>
|
||||
)}
|
||||
{article?.contentReader === 'PDF' &&
|
||||
{libraryItem?.contentReader === 'PDF' &&
|
||||
readerSettings.showEditDisplaySettingsModal && (
|
||||
<PDFDisplaySettingsModal
|
||||
centerX={true}
|
||||
|
|
@ -645,7 +725,7 @@ export default function Home(): JSX.Element {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{article?.contentReader !== 'PDF' &&
|
||||
{libraryItem?.contentReader !== 'PDF' &&
|
||||
readerSettings.showEditDisplaySettingsModal && (
|
||||
<DisplaySettingsModal
|
||||
centerX={true}
|
||||
|
|
@ -655,20 +735,19 @@ export default function Home(): JSX.Element {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{article && showEditModal && (
|
||||
{libraryItem && showEditModal && (
|
||||
<EditArticleModal
|
||||
article={article}
|
||||
article={libraryItem}
|
||||
onOpenChange={() => setShowEditModal(false)}
|
||||
updateArticle={(title, author, description, savedAt, publishedAt) => {
|
||||
article.title = title
|
||||
article.author = author
|
||||
article.description = description
|
||||
article.savedAt = savedAt
|
||||
article.publishedAt = publishedAt
|
||||
|
||||
const titleEvent = new Event('updateTitle') as UpdateTitleEvent
|
||||
titleEvent.title = title
|
||||
document.dispatchEvent(titleEvent)
|
||||
// libraryItem.title = title
|
||||
// libraryItem.author = author
|
||||
// libraryItem.description = description
|
||||
// libraryItem.savedAt = savedAt
|
||||
// libraryItem.publishedAt = publishedAt
|
||||
// const titleEvent = new Event('updateTitle') as UpdateTitleEvent
|
||||
// titleEvent.title = title
|
||||
// document.dispatchEvent(titleEvent)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { AppProps } from 'next/app'
|
|||
import { IdProvider } from '@radix-ui/react-id'
|
||||
import { NextRouter, useRouter } from 'next/router'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { HydrationBoundary } from '@tanstack/react-query'
|
||||
import TopBarProgress from 'react-topbar-progress-indicator'
|
||||
import {
|
||||
KBarProvider,
|
||||
|
|
@ -23,8 +24,10 @@ import { updateTheme } from '../lib/themeUpdater'
|
|||
import { ThemeId } from '../components/tokens/stitches.config'
|
||||
import { posthog } from 'posthog-js'
|
||||
import { GoogleReCaptchaProvider } from '@google-recaptcha/react'
|
||||
import { SWRConfig } from 'swr'
|
||||
import { DEFAULT_HOME_PATH } from '../lib/navigations'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import React from 'react'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
TopBarProgress.config({
|
||||
barColors: {
|
||||
|
|
@ -76,6 +79,14 @@ const ConditionalCaptchaProvider = (props: {
|
|||
return <>{props.children}</>
|
||||
}
|
||||
|
||||
// const queryClient = new QueryClient({
|
||||
// defaultOptions: {
|
||||
// queries: {
|
||||
// gcTime: 1000 * 60 * 60 * 24,
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
export function OmnivoreApp({ Component, pageProps }: AppProps): JSX.Element {
|
||||
const router = useRouter()
|
||||
|
||||
|
|
@ -91,19 +102,21 @@ export function OmnivoreApp({ Component, pageProps }: AppProps): JSX.Element {
|
|||
|
||||
return (
|
||||
<ConditionalCaptchaProvider>
|
||||
<KBarProvider actions={generateActions(router)}>
|
||||
<KBarPortal>
|
||||
<KBarPositioner style={{ zIndex: 100 }}>
|
||||
<KBarAnimator style={animatorStyle}>
|
||||
<KBarSearch style={searchStyle} />
|
||||
<KBarResultsComponents />
|
||||
</KBarAnimator>
|
||||
</KBarPositioner>
|
||||
</KBarPortal>
|
||||
<IdProvider>
|
||||
<Component {...pageProps} />
|
||||
</IdProvider>
|
||||
</KBarProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<KBarProvider actions={generateActions(router)}>
|
||||
<KBarPortal>
|
||||
<KBarPositioner style={{ zIndex: 100 }}>
|
||||
<KBarAnimator style={animatorStyle}>
|
||||
<KBarSearch style={searchStyle} />
|
||||
<KBarResultsComponents />
|
||||
</KBarAnimator>
|
||||
</KBarPositioner>
|
||||
</KBarPortal>
|
||||
<IdProvider>
|
||||
<Component {...pageProps} />
|
||||
</IdProvider>
|
||||
</KBarProvider>
|
||||
</QueryClientProvider>
|
||||
</ConditionalCaptchaProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue