More moves to react-query for key library functions
This commit is contained in:
@ -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 />
|
||||
) : (
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
DropdownOption,
|
||||
DropdownSeparator,
|
||||
} from '../elements/DropdownElements'
|
||||
import { ArticleAttributes } from '../../lib/networking/queries/useGetArticleQuery'
|
||||
import { ArticleAttributes } from '../../lib/networking/library_items/useLibraryItems'
|
||||
import { State } from '../../lib/networking/fragments/articleFragment'
|
||||
|
||||
type DropdownMenuProps = {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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,13 @@ 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/library_items/useLibraryItems'
|
||||
import {
|
||||
ArticleAttributes,
|
||||
Recommendation,
|
||||
TextDirection,
|
||||
} 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
|
||||
|
||||
@ -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'
|
||||
@ -42,13 +42,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(() => {
|
||||
|
||||
@ -9,8 +9,8 @@ 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
|
||||
@ -25,6 +25,7 @@ type HighlightNoteModalProps = {
|
||||
export function HighlightNoteModal(
|
||||
props: HighlightNoteModalProps
|
||||
): JSX.Element {
|
||||
const updateHighlight = useUpdateHighlight()
|
||||
const [noteContent, setNoteContent] = useState(
|
||||
props.highlight?.annotation ?? ''
|
||||
)
|
||||
@ -38,20 +39,24 @@ 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,
|
||||
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)
|
||||
|
||||
@ -16,10 +16,10 @@ import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery
|
||||
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'
|
||||
|
||||
type NotebookContentProps = {
|
||||
viewer: UserBasicData
|
||||
@ -43,11 +43,10 @@ type NoteState = {
|
||||
export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
||||
const isDark = isDarkTheme()
|
||||
|
||||
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)
|
||||
@ -112,7 +111,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 +121,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
||||
setNoteText('')
|
||||
}
|
||||
return result
|
||||
}, [articleData])
|
||||
}, [article])
|
||||
|
||||
useEffect(() => {
|
||||
if (highlights && props.onAnnotationsChanged) {
|
||||
@ -179,16 +178,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}
|
||||
@ -256,9 +245,6 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
||||
viewInReader={props.viewInReader}
|
||||
setSetLabelsTarget={setLabelsTarget}
|
||||
setShowConfirmDeleteHighlightId={setShowConfirmDeleteHighlightId}
|
||||
updateHighlight={() => {
|
||||
mutate()
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{sortedHighlights.length === 0 && (
|
||||
@ -298,7 +284,6 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
||||
props.item.id,
|
||||
showConfirmDeleteHighlightId
|
||||
)
|
||||
mutate()
|
||||
if (success) {
|
||||
showSuccessToast('Highlight deleted.', {
|
||||
position: 'bottom-right',
|
||||
@ -333,7 +318,6 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
||||
console.log('update highlight: ', highlight)
|
||||
}}
|
||||
onOpenChange={() => {
|
||||
mutate()
|
||||
setLabelsTarget(undefined)
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { ArticleAttributes } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { nanoid } from 'nanoid'
|
||||
@ -36,8 +36,9 @@ 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 annotationOmnivoreId = (annotation: Annotation): string | undefined => {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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 { ArticleAttributes } 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'
|
||||
|
||||
@ -18,7 +18,10 @@ import {
|
||||
LibraryItemNode,
|
||||
LibraryItems,
|
||||
LibraryItemsQueryInput,
|
||||
useArchiveItem,
|
||||
useDeleteItem,
|
||||
useGetLibraryItems,
|
||||
useUpdateItemReadStatus,
|
||||
} from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import {
|
||||
useGetViewerQuery,
|
||||
@ -110,6 +113,10 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
||||
const [linkToEdit, setLinkToEdit] = useState<LibraryItem>()
|
||||
const [linkToUnsubscribe, setLinkToUnsubscribe] = useState<LibraryItem>()
|
||||
|
||||
const archiveItem = useArchiveItem()
|
||||
const deleteItem = useDeleteItem()
|
||||
const updateItemReadStatus = useUpdateItemReadStatus()
|
||||
|
||||
const [queryInputs, setQueryInputs] =
|
||||
useState<LibraryItemsQueryInput>(defaultQuery)
|
||||
|
||||
@ -121,19 +128,6 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
||||
error: fetchItemsError,
|
||||
} = useGetLibraryItems(props.folder, queryInputs)
|
||||
|
||||
// useEffect(() => {
|
||||
// const handleRevalidate = () => {
|
||||
// ;(async () => {
|
||||
// console.log('revalidating library')
|
||||
// await mutate()
|
||||
// })()
|
||||
// }
|
||||
// document.addEventListener('revalidateLibrary', handleRevalidate)
|
||||
// return () => {
|
||||
// document.removeEventListener('revalidateLibrary', handleRevalidate)
|
||||
// }
|
||||
// }, [mutate])
|
||||
|
||||
useEffect(() => {
|
||||
if (queryValue.startsWith('#')) {
|
||||
debouncedFetchSearchResults(
|
||||
@ -167,13 +161,6 @@ 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?.pages
|
||||
@ -387,64 +374,98 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
|
||||
return
|
||||
}
|
||||
|
||||
// switch (action) {
|
||||
// case 'showDetail':
|
||||
// const username = viewerData?.me?.profile.username
|
||||
// if (username) {
|
||||
// setActiveCardId(item.node.id)
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
// break
|
||||
// case 'showOriginal':
|
||||
// const url = item.node.originalArticleUrl
|
||||
// if (url) {
|
||||
// window.open(url, '_blank')
|
||||
// }
|
||||
// break
|
||||
// case 'archive':
|
||||
// performActionOnItem('archive', item)
|
||||
// break
|
||||
// case 'unarchive':
|
||||
// performActionOnItem('unarchive', item)
|
||||
// break
|
||||
// case 'delete':
|
||||
// performActionOnItem('delete', item)
|
||||
// break
|
||||
// case 'restore':
|
||||
// performActionOnItem('restore', item)
|
||||
// break
|
||||
// case 'mark-read':
|
||||
// performActionOnItem('mark-read', item)
|
||||
// break
|
||||
// case 'mark-unread':
|
||||
// performActionOnItem('mark-unread', item)
|
||||
// break
|
||||
// case 'set-labels':
|
||||
// setLabelsTarget(item)
|
||||
// break
|
||||
// case 'open-notebook':
|
||||
// if (!notebookTarget) {
|
||||
// setNotebookTarget(item)
|
||||
// } else {
|
||||
// setNotebookTarget(undefined)
|
||||
// }
|
||||
// break
|
||||
// case 'unsubscribe':
|
||||
// performActionOnItem('unsubscribe', item)
|
||||
// case 'update-item':
|
||||
// performActionOnItem('update-item', item)
|
||||
// break
|
||||
// default:
|
||||
// console.warn('unknown action: ', action)
|
||||
// }
|
||||
switch (action) {
|
||||
case 'showDetail':
|
||||
const username = viewerData?.me?.profile.username
|
||||
if (username) {
|
||||
setActiveCardId(item.node.id)
|
||||
if (item.node.state === State.PROCESSING) {
|
||||
router.push(`/article?url=${encodeURIComponent(item.node.url)}`)
|
||||
} else {
|
||||
router.push(`/${username}/${item.node.slug}`)
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'showOriginal':
|
||||
const url = item.node.originalArticleUrl
|
||||
if (url) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
break
|
||||
case 'archive':
|
||||
case 'unarchive':
|
||||
try {
|
||||
await archiveItem.mutateAsync({
|
||||
linkId: item.node.id,
|
||||
archived: action == 'archive',
|
||||
})
|
||||
} catch {
|
||||
showErrorToast(`Error ${action}ing item`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
return
|
||||
}
|
||||
showSuccessToast(`Item ${action}d`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
break
|
||||
case 'delete':
|
||||
try {
|
||||
await deleteItem.mutateAsync(item.node.id)
|
||||
} catch {
|
||||
showErrorToast(`Error deleting item`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
return
|
||||
}
|
||||
showSuccessToast(`Item deleted`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
break
|
||||
case 'mark-read':
|
||||
case 'mark-unread':
|
||||
const desc = action == 'mark-read' ? 'read' : 'unread'
|
||||
const values =
|
||||
action == 'mark-read'
|
||||
? {
|
||||
readingProgressPercent: 100,
|
||||
readingProgressTopPercent: 100,
|
||||
readingProgressAnchorIndex: 0,
|
||||
}
|
||||
: {
|
||||
readingProgressPercent: 0,
|
||||
readingProgressTopPercent: 0,
|
||||
readingProgressAnchorIndex: 0,
|
||||
}
|
||||
try {
|
||||
await updateItemReadStatus.mutateAsync({
|
||||
id: item.node.id,
|
||||
force: true,
|
||||
...values,
|
||||
})
|
||||
} catch {
|
||||
showErrorToast(`Error marking as ${desc}`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
return
|
||||
}
|
||||
break
|
||||
case 'set-labels':
|
||||
setLabelsTarget(item)
|
||||
break
|
||||
case 'open-notebook':
|
||||
if (!notebookTarget) {
|
||||
setNotebookTarget(item)
|
||||
} else {
|
||||
setNotebookTarget(undefined)
|
||||
}
|
||||
break
|
||||
case 'unsubscribe':
|
||||
setLinkToUnsubscribe(item.node)
|
||||
break
|
||||
default:
|
||||
console.warn('unknown action: ', action)
|
||||
}
|
||||
}
|
||||
|
||||
const modalTargetItem = useMemo(() => {
|
||||
@ -1187,9 +1208,6 @@ export function LibraryItemsLayout(
|
||||
</VStack>
|
||||
{props.showEditTitleModal && (
|
||||
<EditLibraryItemModal
|
||||
updateItem={(item: LibraryItem) =>
|
||||
props.actionHandler('update-item', item)
|
||||
}
|
||||
onOpenChange={() => {
|
||||
props.setShowEditTitleModal(false)
|
||||
props.setLinkToEdit(undefined)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,18 +1,24 @@
|
||||
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,
|
||||
} from '../networking/library_items/useLibraryItems'
|
||||
|
||||
export default function useLibraryItemActions() {
|
||||
const archiveItem = useCallback(async (itemId: string) => {
|
||||
const result = await setLinkArchivedMutation({
|
||||
const archiveItem = useArchiveItem()
|
||||
const deleteItem = useDeleteItem()
|
||||
const moveItem = useMoveItemToFolder()
|
||||
|
||||
const doArchiveItem = useCallback(async (itemId: string) => {
|
||||
const result = await archiveItem.mutateAsync({
|
||||
linkId: itemId,
|
||||
archived: true,
|
||||
})
|
||||
@ -27,8 +33,8 @@ 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, undo: () => void) => {
|
||||
const result = await deleteItem.mutateAsync(itemId)
|
||||
|
||||
if (result) {
|
||||
showSuccessToastWithUndo('Item removed', async () => {
|
||||
@ -52,8 +58,8 @@ export default function useLibraryItemActions() {
|
||||
return !!result
|
||||
}, [])
|
||||
|
||||
const moveItem = useCallback(async (itemId: string) => {
|
||||
const result = await moveToFolderMutation(itemId, 'inbox')
|
||||
const doMoveItem = useCallback(async (itemId: string) => {
|
||||
const result = await moveItem.mutateAsync({ itemId, folder: 'inbox' })
|
||||
if (result) {
|
||||
showSuccessToast('Moved to library', { position: 'bottom-right' })
|
||||
} else {
|
||||
@ -85,5 +91,10 @@ export default function useLibraryItemActions() {
|
||||
[]
|
||||
)
|
||||
|
||||
return { archiveItem, deleteItem, moveItem, shareItem }
|
||||
return {
|
||||
archiveItem: doArchiveItem,
|
||||
deleteItem: doDeleteItem,
|
||||
moveItem: doMoveItem,
|
||||
shareItem,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
209
packages/web/lib/networking/highlights/useItemHighlights.tsx
Normal file
209
packages/web/lib/networking/highlights/useItemHighlights.tsx
Normal file
@ -0,0 +1,209 @@
|
||||
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
|
||||
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, (item) => {
|
||||
return {
|
||||
...item,
|
||||
highlights: [...item.highlights, newHighlight],
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteHighlight = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const deleteHighlight = async (variables: {
|
||||
itemId: 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, (item) => {
|
||||
return {
|
||||
...item,
|
||||
highlights: item.highlights.filter(
|
||||
(h) => h.id != deletedHighlight.id
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateHighlight = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const updateHighlight = async (variables: {
|
||||
itemId: string
|
||||
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, (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
|
||||
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, (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
|
||||
}
|
||||
@ -132,6 +132,19 @@ export const GQL_DELETE_LIBRARY_ITEM = gql`
|
||||
}
|
||||
`
|
||||
|
||||
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_UPDATE_LIBRARY_ITEM = gql`
|
||||
mutation UpdatePage($input: UpdatePageInput!) {
|
||||
updatePage(input: $input) {
|
||||
|
||||
@ -11,18 +11,15 @@ import { ContentReader, PageType, State } from '../fragments/articleFragment'
|
||||
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
|
||||
import { makeGqlFetcher, requestHeaders } from '../networkHelpers'
|
||||
import { Label } from '../fragments/labelFragment'
|
||||
import { moveToFolderMutation } from '../mutations/moveToLibraryMutation'
|
||||
import {
|
||||
GQL_DELETE_LIBRARY_ITEM,
|
||||
GQL_GET_LIBRARY_ITEM_CONTENT,
|
||||
GQL_MOVE_ITEM_TO_FOLDER,
|
||||
GQL_SEARCH_QUERY,
|
||||
GQL_SET_LINK_ARCHIVED,
|
||||
GQL_UPDATE_LIBRARY_ITEM,
|
||||
} from './gql'
|
||||
import { parseGraphQLResponse } from '../queries/gql-errors'
|
||||
import { gqlEndpoint } from '../../appConfig'
|
||||
import { GraphQLResponse } from 'graphql-request/dist/types'
|
||||
import { ArticleAttributes } from '../queries/useGetArticleQuery'
|
||||
|
||||
function gqlFetcher(
|
||||
query: string,
|
||||
@ -63,25 +60,55 @@ const updateItemPropertyInCache = (
|
||||
propertyName: string,
|
||||
propertyValue: any
|
||||
) => {
|
||||
const setter = createDictionary(propertyName, propertyValue)
|
||||
updateItemProperty(queryClient, itemId, (oldItem) => {
|
||||
const setter = createDictionary(propertyName, propertyValue)
|
||||
return {
|
||||
...oldItem,
|
||||
...setter,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const updateItemProperty = (
|
||||
queryClient: QueryClient,
|
||||
itemId: string,
|
||||
updateFunc: (input: ArticleAttributes) => ArticleAttributes
|
||||
) => {
|
||||
let foundItemSlug: string | undefined
|
||||
const keys = queryClient
|
||||
.getQueryCache()
|
||||
.findAll({ queryKey: ['libraryItems'] })
|
||||
keys.forEach((query) => {
|
||||
queryClient.setQueryData(query.queryKey, (data: any) => {
|
||||
if (!data) return data
|
||||
return {
|
||||
const updatedData = {
|
||||
...data,
|
||||
pages: data.pages.map((page: any) => ({
|
||||
...page,
|
||||
edges: page.edges.map((edge: any) =>
|
||||
edge.node.id === itemId
|
||||
? { ...edge, node: { ...edge.node, ...setter } }
|
||||
: edge
|
||||
),
|
||||
edges: page.edges.map((edge: any) => {
|
||||
if (edge.node.id === itemId) {
|
||||
foundItemSlug = edge.node.slug
|
||||
return {
|
||||
...edge,
|
||||
node: { ...edge.node, ...updateFunc(edge.node) },
|
||||
}
|
||||
}
|
||||
return edge
|
||||
}),
|
||||
})),
|
||||
}
|
||||
return updatedData
|
||||
})
|
||||
if (foundItemSlug)
|
||||
queryClient.setQueryData(
|
||||
['libraryItem', foundItemSlug],
|
||||
(oldData: ArticleAttributes) => {
|
||||
return {
|
||||
...oldData,
|
||||
...updateFunc(oldData),
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@ -93,7 +120,6 @@ const updateItemPropertiesInCache = (
|
||||
const keys = queryClient
|
||||
.getQueryCache()
|
||||
.findAll({ queryKey: ['libraryItems'] })
|
||||
console.log('updateItemPropertiesInCache::libraryItems: ', keys)
|
||||
keys.forEach((query) => {
|
||||
queryClient.setQueryData(query.queryKey, (data: any) => {
|
||||
if (!data) return data
|
||||
@ -243,6 +269,43 @@ export const useRestoreItem = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useMoveItemToFolder = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const restoreItem = async (variables: { itemId: string; folder: string }) => {
|
||||
const result = (await gqlFetcher(GQL_MOVE_ITEM_TO_FOLDER, {
|
||||
id: variables.itemId,
|
||||
folder: variables.folder,
|
||||
})) as UpdateLibraryItemData
|
||||
if (result.updatePage.errorCodes?.length) {
|
||||
throw new Error(result.updatePage.errorCodes[0])
|
||||
}
|
||||
return result.updatePage
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: restoreItem,
|
||||
onMutate: async (variables: { itemId: string; folder: string }) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['libraryItems'] })
|
||||
updateItemPropertyInCache(
|
||||
queryClient,
|
||||
variables.itemId,
|
||||
'folder',
|
||||
variables.folder
|
||||
)
|
||||
return { previousItems: queryClient.getQueryData(['libraryItems']) }
|
||||
},
|
||||
onError: (error, itemId, context) => {
|
||||
if (context?.previousItems) {
|
||||
queryClient.setQueryData(['libraryItems'], context.previousItems)
|
||||
}
|
||||
},
|
||||
onSettled: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['libraryItems'],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateItemReadStatus = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const updateItemReadStatus = async (
|
||||
@ -291,11 +354,6 @@ export const useGetLibraryItemContent = (username: string, slug: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['libraryItem', slug],
|
||||
queryFn: async () => {
|
||||
console.log('input: ', {
|
||||
slug,
|
||||
username,
|
||||
includeFriendsHighlights: false,
|
||||
})
|
||||
const response = (await gqlFetcher(GQL_GET_LIBRARY_ITEM_CONTENT, {
|
||||
slug,
|
||||
username,
|
||||
@ -313,6 +371,36 @@ export const useGetLibraryItemContent = (username: string, slug: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
export type TextDirection = 'RTL' | 'LTR'
|
||||
|
||||
export type ArticleAttributes = {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
originalArticleUrl: string
|
||||
author?: string
|
||||
image?: string
|
||||
savedAt: string
|
||||
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[]
|
||||
}
|
||||
|
||||
type ArticleResult = {
|
||||
article?: ArticleAttributes
|
||||
errorCodes?: string[]
|
||||
|
||||
@ -5,26 +5,7 @@ import {
|
||||
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
|
||||
}
|
||||
import { CreateHighlightInput } from '../highlights/useItemHighlights'
|
||||
|
||||
type CreateHighlightOutput = {
|
||||
createHighlight: InnerCreateHighlightOutput
|
||||
|
||||
@ -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,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,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,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) {
|
||||
|
||||
@ -9,7 +9,11 @@ import {
|
||||
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
|
||||
import { ScopedMutator } from 'swr/dist/_internal'
|
||||
import { Label, labelFragment } from '../fragments/labelFragment'
|
||||
import { LibraryItems, Recommendation } from '../library_items/useLibraryItems'
|
||||
import {
|
||||
ArticleAttributes,
|
||||
LibraryItems,
|
||||
Recommendation,
|
||||
} from '../library_items/useLibraryItems'
|
||||
import useSWR from 'swr'
|
||||
import { recommendationFragment } from '../library_items/gql'
|
||||
|
||||
@ -36,36 +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
|
||||
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
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -171,7 +168,7 @@ export default function Debug(): JSX.Element {
|
||||
// recommendations?: Recommendation[]
|
||||
|
||||
return result
|
||||
}, [articleData])
|
||||
}, [article])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -39,6 +39,14 @@ import {
|
||||
useGetLibraryItemContent,
|
||||
useUpdateItemReadStatus,
|
||||
} from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import {
|
||||
CreateHighlightInput,
|
||||
useCreateHighlight,
|
||||
useDeleteHighlight,
|
||||
useMergeHighlight,
|
||||
useMergeHighlights,
|
||||
useUpdateHighlight,
|
||||
} from '../../../lib/networking/highlights/useItemHighlights'
|
||||
|
||||
const PdfArticleContainerNoSSR = dynamic<PdfArticleContainerProps>(
|
||||
() => import('./../../../components/templates/article/PdfArticleContainer'),
|
||||
@ -59,13 +67,16 @@ export default function Reader(): JSX.Element {
|
||||
const archiveItem = useArchiveItem()
|
||||
const deleteItem = useDeleteItem()
|
||||
const updateItemReadStatus = useUpdateItemReadStatus()
|
||||
const createHighlight = useCreateHighlight()
|
||||
const deleteHighlight = useDeleteHighlight()
|
||||
const updateHighlight = useUpdateHighlight()
|
||||
const mergeHighlight = useMergeHighlight()
|
||||
|
||||
const { data: libraryItem, error: articleFetchError } =
|
||||
useGetLibraryItemContent(
|
||||
router.query.username as string,
|
||||
router.query.slug as string
|
||||
)
|
||||
console.log('articleFetchError: ', articleFetchError)
|
||||
useEffect(() => {
|
||||
dispatchLabels({
|
||||
type: 'RESET',
|
||||
@ -574,10 +585,59 @@ export default function Reader(): JSX.Element {
|
||||
libraryItem.directionality ?? readerSettings.textDirection
|
||||
}
|
||||
articleMutations={{
|
||||
createHighlightMutation,
|
||||
deleteHighlightMutation,
|
||||
mergeHighlightMutation,
|
||||
updateHighlightMutation,
|
||||
createHighlightMutation: async (
|
||||
input: CreateHighlightInput
|
||||
) => {
|
||||
try {
|
||||
const result = await createHighlight.mutateAsync({
|
||||
itemId: libraryItem.id,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
input,
|
||||
})
|
||||
return result?.id
|
||||
} catch (err) {
|
||||
console.log('error updating highlight', err)
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
articleReadingProgressMutation: async (
|
||||
input: ArticleReadingProgressMutationInput
|
||||
) => {
|
||||
|
||||
@ -4,7 +4,6 @@ import { Box } from '../../../../components/elements/LayoutPrimitives'
|
||||
import { useGetArticleSavingStatus } from '../../../../lib/networking/queries/useGetArticleSavingStatus'
|
||||
import { ErrorComponent } from '../../../../components/templates/SavingRequest'
|
||||
import { useSWRConfig } from 'swr'
|
||||
import { cacheArticle } from '../../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { PrimaryLayout } from '../../../../components/templates/PrimaryLayout'
|
||||
import { applyStoredTheme } from '../../../../lib/themeUpdater'
|
||||
|
||||
@ -81,10 +80,6 @@ function PrimaryContent(props: PrimaryContentProps): JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
if (article) {
|
||||
cacheArticle(mutate, props.username, article)
|
||||
}
|
||||
|
||||
if (successRedirectPath) {
|
||||
router.replace(
|
||||
`/app${successRedirectPath}?isAppEmbedView=true&highlightBarDisabled=true`
|
||||
|
||||
@ -1,352 +0,0 @@
|
||||
import {
|
||||
autoUpdate,
|
||||
offset,
|
||||
size,
|
||||
useFloating,
|
||||
useHover,
|
||||
useInteractions,
|
||||
} from '@floating-ui/react'
|
||||
import { NextRouter, useRouter } from 'next/router'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { TrashIcon } from '../components/elements/icons/TrashIcon'
|
||||
import { LabelChip } from '../components/elements/LabelChip'
|
||||
import { Box, HStack, VStack } from '../components/elements/LayoutPrimitives'
|
||||
import { ConfirmationModal } from '../components/patterns/ConfirmationModal'
|
||||
import { HighlightHoverActions } from '../components/patterns/HighlightHoverActions'
|
||||
import { HighlightViewNote } from '../components/patterns/HighlightNotes'
|
||||
import { timeAgo } from '../components/patterns/LibraryCards/LibraryCardStyles'
|
||||
import { SetHighlightLabelsModalPresenter } from '../components/templates/article/SetLabelsModalPresenter'
|
||||
import { EmptyHighlights } from '../components/templates/homeFeed/EmptyHighlights'
|
||||
import { LibraryFilterMenu } from '../components/templates/navMenu/LibraryMenu'
|
||||
import { theme } from '../components/tokens/stitches.config'
|
||||
import { useApplyLocalTheme } from '../lib/hooks/useApplyLocalTheme'
|
||||
import { useFetchMore } from '../lib/hooks/useFetchMoreScroll'
|
||||
import { Highlight } from '../lib/networking/fragments/highlightFragment'
|
||||
import { deleteHighlightMutation } from '../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { useGetHighlights } from '../lib/networking/queries/useGetHighlights'
|
||||
import {
|
||||
useGetViewerQuery,
|
||||
UserBasicData,
|
||||
} from '../lib/networking/queries/useGetViewerQuery'
|
||||
import { highlightColor } from '../lib/themeUpdater'
|
||||
import { showErrorToast, showSuccessToast } from '../lib/toastHelpers'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
export default function HighlightsPage(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const viewer = useGetViewerQuery()
|
||||
const [showFilterMenu, setShowFilterMenu] = useState(false)
|
||||
const [_, setShowAddLinkModal] = useState(false)
|
||||
|
||||
useApplyLocalTheme()
|
||||
|
||||
const { isLoading, setSize, size, data, mutate } = useGetHighlights({
|
||||
first: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const hasMore = useMemo(() => {
|
||||
if (!data) {
|
||||
return false
|
||||
}
|
||||
return data[data.length - 1].highlights.pageInfo.hasNextPage
|
||||
}, [data])
|
||||
|
||||
const handleFetchMore = useCallback(() => {
|
||||
if (isLoading || !hasMore) {
|
||||
return
|
||||
}
|
||||
setSize(size + 1)
|
||||
}, [isLoading, hasMore, setSize, size])
|
||||
|
||||
useFetchMore(handleFetchMore)
|
||||
|
||||
const highlights = useMemo(() => {
|
||||
if (!data) {
|
||||
return []
|
||||
}
|
||||
return data.flatMap((res) => res.highlights.edges.map((edge) => edge.node))
|
||||
}, [data])
|
||||
|
||||
if (!highlights.length) {
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
height: `100vh`,
|
||||
}}
|
||||
>
|
||||
<EmptyHighlights />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<HStack>
|
||||
<Toaster />
|
||||
|
||||
<LibraryFilterMenu
|
||||
setShowAddLinkModal={setShowAddLinkModal}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
searchTerm={undefined}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
router?.push(`/home?q=${searchQuery}`)
|
||||
}}
|
||||
/>
|
||||
<VStack
|
||||
css={{
|
||||
maxWidth: '70%',
|
||||
padding: '20px',
|
||||
margin: '30px 50px 0 0',
|
||||
}}
|
||||
>
|
||||
{highlights.map((highlight) => {
|
||||
return (
|
||||
viewer.viewerData?.me && (
|
||||
<HighlightCard
|
||||
key={highlight.id}
|
||||
highlight={highlight}
|
||||
viewer={viewer.viewerData.me}
|
||||
router={router}
|
||||
mutate={mutate}
|
||||
/>
|
||||
)
|
||||
)
|
||||
})}
|
||||
</VStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type HighlightCardProps = {
|
||||
highlight: Highlight
|
||||
viewer: UserBasicData
|
||||
router: NextRouter
|
||||
mutate: () => void
|
||||
}
|
||||
|
||||
type HighlightAnnotationProps = {
|
||||
highlight: Highlight
|
||||
}
|
||||
|
||||
function HighlightAnnotation({
|
||||
highlight,
|
||||
}: HighlightAnnotationProps): JSX.Element {
|
||||
const [noteMode, setNoteMode] = useState<'edit' | 'preview'>('preview')
|
||||
const [annotation, setAnnotation] = useState(highlight.annotation)
|
||||
|
||||
return (
|
||||
<HighlightViewNote
|
||||
targetId={highlight.id}
|
||||
text={annotation}
|
||||
placeHolder="Add notes to this highlight..."
|
||||
highlight={highlight}
|
||||
mode={noteMode}
|
||||
setEditMode={setNoteMode}
|
||||
updateHighlight={(highlight) => {
|
||||
setAnnotation(highlight.annotation)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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 viewInReader = useCallback(
|
||||
(highlightId: string) => {
|
||||
const router = props.router
|
||||
const viewer = props.viewer
|
||||
const item = props.highlight.libraryItem
|
||||
|
||||
if (!router || !router.isReady || !viewer || !item) {
|
||||
showErrorToast('Error navigating to highlight')
|
||||
return
|
||||
}
|
||||
|
||||
router.push(
|
||||
{
|
||||
pathname: '/[username]/[slug]',
|
||||
query: {
|
||||
username: viewer.profile.username,
|
||||
slug: item.slug,
|
||||
},
|
||||
hash: highlightId,
|
||||
},
|
||||
`${viewer.profile.username}/${item.slug}#${highlightId}`,
|
||||
{
|
||||
scroll: false,
|
||||
}
|
||||
)
|
||||
},
|
||||
[props.highlight.libraryItem, props.viewer, props.router]
|
||||
)
|
||||
|
||||
const { refs, floatingStyles, context } = useFloating({
|
||||
open: isOpen,
|
||||
onOpenChange: setIsOpen,
|
||||
middleware: [
|
||||
offset({
|
||||
mainAxis: -25,
|
||||
}),
|
||||
size(),
|
||||
],
|
||||
placement: 'top-end',
|
||||
whileElementsMounted: autoUpdate,
|
||||
})
|
||||
|
||||
const hover = useHover(context)
|
||||
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([hover])
|
||||
|
||||
return (
|
||||
<VStack
|
||||
ref={refs.setReference}
|
||||
{...getReferenceProps()}
|
||||
css={{
|
||||
width: '100%',
|
||||
fontFamily: '$inter',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
bg: '$thBackground2',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: '$thBackground3',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<HighlightHoverActions
|
||||
viewer={props.viewer}
|
||||
highlight={props.highlight}
|
||||
isHovered={isOpen ?? false}
|
||||
viewInReader={viewInReader}
|
||||
setLabelsTarget={setLabelsTarget}
|
||||
setShowConfirmDeleteHighlightId={setShowConfirmDeleteHighlightId}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
css={{
|
||||
width: '30px',
|
||||
height: '5px',
|
||||
backgroundColor: highlightColor(props.highlight.color),
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
css={{
|
||||
color: '$thText',
|
||||
fontSize: '11px',
|
||||
marginTop: '10px',
|
||||
fontWeight: 300,
|
||||
}}
|
||||
>
|
||||
{timeAgo(props.highlight.updatedAt)}
|
||||
</Box>
|
||||
{props.highlight.quote && (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{props.highlight.quote}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
<HighlightAnnotation highlight={props.highlight} />
|
||||
{props.highlight.labels && (
|
||||
<HStack
|
||||
css={{
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
{props.highlight.labels.map((label) => {
|
||||
return (
|
||||
<LabelChip key={label.id} color={label.color} text={label.name} />
|
||||
)
|
||||
})}
|
||||
</HStack>
|
||||
)}
|
||||
<Box
|
||||
css={{
|
||||
color: '$thText',
|
||||
fontSize: '12px',
|
||||
lineHeight: '20px',
|
||||
fontWeight: 300,
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
{props.highlight.libraryItem?.title}
|
||||
</Box>
|
||||
<Box
|
||||
css={{
|
||||
color: '$grayText',
|
||||
fontSize: '12px',
|
||||
lineHeight: '20px',
|
||||
fontWeight: 300,
|
||||
}}
|
||||
>
|
||||
{props.highlight.libraryItem?.author}
|
||||
</Box>
|
||||
{showConfirmDeleteHighlightId && (
|
||||
<ConfirmationModal
|
||||
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',
|
||||
})
|
||||
}
|
||||
})()
|
||||
setShowConfirmDeleteHighlightId(undefined)
|
||||
}}
|
||||
onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)}
|
||||
icon={
|
||||
<TrashIcon
|
||||
size={40}
|
||||
color={theme.colors.grayTextContrast.toString()}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{labelsTarget && (
|
||||
<SetHighlightLabelsModalPresenter
|
||||
highlight={labelsTarget}
|
||||
highlightId={labelsTarget.id}
|
||||
onUpdate={(highlight) => {
|
||||
// Don't actually need to do something here
|
||||
console.log('update highlight: ', highlight)
|
||||
}}
|
||||
onOpenChange={() => {
|
||||
props.mutate()
|
||||
setLabelsTarget(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user