diff --git a/packages/web/components/elements/icons/ConfusedSlothIcon.tsx b/packages/web/components/elements/icons/ConfusedSlothIcon.tsx index 47f300a3e..7b99e3799 100644 --- a/packages/web/components/elements/icons/ConfusedSlothIcon.tsx +++ b/packages/web/components/elements/icons/ConfusedSlothIcon.tsx @@ -8,7 +8,6 @@ import React from 'react' export function ConfusedSlothIcon(): JSX.Element { const { currentThemeIsDark } = useCurrentTheme() - console.log('is dark mdoe: ', currentThemeIsDark) return currentThemeIsDark ? ( ) : ( diff --git a/packages/web/components/patterns/ArticleNotes.tsx b/packages/web/components/patterns/ArticleNotes.tsx index abdceb74c..d0e10559c 100644 --- a/packages/web/components/patterns/ArticleNotes.tsx +++ b/packages/web/components/patterns/ArticleNotes.tsx @@ -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, diff --git a/packages/web/components/patterns/HighlightNotes.tsx b/packages/web/components/patterns/HighlightNotes.tsx index 887296470..ab007581d 100644 --- a/packages/web/components/patterns/HighlightNotes.tsx +++ b/packages/web/components/patterns/HighlightNotes.tsx @@ -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, diff --git a/packages/web/components/patterns/ReaderDropdownMenu.tsx b/packages/web/components/patterns/ReaderDropdownMenu.tsx index a9b32e798..6f3daee05 100644 --- a/packages/web/components/patterns/ReaderDropdownMenu.tsx +++ b/packages/web/components/patterns/ReaderDropdownMenu.tsx @@ -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 = { diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 582eeea6e..85c36053c 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -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' diff --git a/packages/web/components/templates/article/ArticleContainer.tsx b/packages/web/components/templates/article/ArticleContainer.tsx index f0c61bfd5..15ab8bdf6 100644 --- a/packages/web/components/templates/article/ArticleContainer.tsx +++ b/packages/web/components/templates/article/ArticleContainer.tsx @@ -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 diff --git a/packages/web/components/templates/article/EpubContainer.tsx b/packages/web/components/templates/article/EpubContainer.tsx index ea9ae366e..ebf2558d8 100644 --- a/packages/web/components/templates/article/EpubContainer.tsx +++ b/packages/web/components/templates/article/EpubContainer.tsx @@ -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(null) const renditionRef = useRef(undefined) - const [shareTarget, setShareTarget] = - useState(undefined) + const [shareTarget, setShareTarget] = useState( + undefined + ) const [touchStart, setTouchStart] = useState(0) const [notebookKey, setNotebookKey] = useState(uuidv4()) const [noteTarget, setNoteTarget] = useState(undefined) - const [noteTargetPageIndex, setNoteTargetPageIndex] = - useState(undefined) + const [noteTargetPageIndex, setNoteTargetPageIndex] = useState< + number | undefined + >(undefined) const highlightsRef = useRef([]) const book = useMemo(() => { diff --git a/packages/web/components/templates/article/HighlightNoteModal.tsx b/packages/web/components/templates/article/HighlightNoteModal.tsx index c505f96b3..42b416e04 100644 --- a/packages/web/components/templates/article/HighlightNoteModal.tsx +++ b/packages/web/components/templates/article/HighlightNoteModal.tsx @@ -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) diff --git a/packages/web/components/templates/article/Notebook.tsx b/packages/web/components/templates/article/Notebook.tsx index 29dbecf30..ce09af5e4 100644 --- a/packages/web/components/templates/article/Notebook.tsx +++ b/packages/web/components/templates/article/Notebook.tsx @@ -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('') const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] = useState(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(undefined) const [lastSaved, setLastSaved] = useState(undefined) - useEffect(() => { - const highlightsUpdated = () => { - mutate() - } - document.addEventListener('highlightsUpdated', highlightsUpdated) - return () => { - document.removeEventListener('highlightsUpdated', highlightsUpdated) - } - }, [mutate]) - return ( { - 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) }} /> diff --git a/packages/web/components/templates/article/PdfArticleContainer.tsx b/packages/web/components/templates/article/PdfArticleContainer.tsx index 1a684720c..cbe7bf84b 100644 --- a/packages/web/components/templates/article/PdfArticleContainer.tsx +++ b/packages/web/components/templates/article/PdfArticleContainer.tsx @@ -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(null) const [notebookKey, setNotebookKey] = useState(uuidv4()) const [noteTarget, setNoteTarget] = useState(undefined) - const [noteTargetPageIndex, setNoteTargetPageIndex] = - useState(undefined) + const [noteTargetPageIndex, setNoteTargetPageIndex] = useState< + number | undefined + >(undefined) const highlightsRef = useRef([]) const annotationOmnivoreId = (annotation: Annotation): string | undefined => { diff --git a/packages/web/components/templates/article/VerticalArticleActions.tsx b/packages/web/components/templates/article/VerticalArticleActions.tsx index 63507b074..689faefab 100644 --- a/packages/web/components/templates/article/VerticalArticleActions.tsx +++ b/packages/web/components/templates/article/VerticalArticleActions.tsx @@ -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' diff --git a/packages/web/components/templates/homeFeed/EditItemModals.tsx b/packages/web/components/templates/homeFeed/EditItemModals.tsx index fca5f99e0..6bbff5685 100644 --- a/packages/web/components/templates/homeFeed/EditItemModals.tsx +++ b/packages/web/components/templates/homeFeed/EditItemModals.tsx @@ -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' diff --git a/packages/web/components/templates/library/LibraryContainer.tsx b/packages/web/components/templates/library/LibraryContainer.tsx index ee8ae35fc..c89c67b9b 100644 --- a/packages/web/components/templates/library/LibraryContainer.tsx +++ b/packages/web/components/templates/library/LibraryContainer.tsx @@ -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() const [linkToUnsubscribe, setLinkToUnsubscribe] = useState() + const archiveItem = useArchiveItem() + const deleteItem = useDeleteItem() + const updateItemReadStatus = useUpdateItemReadStatus() + const [queryInputs, setQueryInputs] = useState(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( {props.showEditTitleModal && ( - props.actionHandler('update-item', item) - } onOpenChange={() => { props.setShowEditTitleModal(false) props.setLinkToEdit(undefined) diff --git a/packages/web/lib/articleActions.ts b/packages/web/lib/articleActions.ts index 3218a39df..cb6f8da8b 100644 --- a/packages/web/lib/articleActions.ts +++ b/packages/web/lib/articleActions.ts @@ -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: ( diff --git a/packages/web/lib/highlights/createHighlight.ts b/packages/web/lib/highlights/createHighlight.ts index 9b165483b..6c5d45e75 100644 --- a/packages/web/lib/highlights/createHighlight.ts +++ b/packages/web/lib/highlights/createHighlight.ts @@ -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 diff --git a/packages/web/lib/highlights/useSelection.tsx b/packages/web/lib/highlights/useSelection.tsx index c32db86d2..6d12c74ea 100644 --- a/packages/web/lib/highlights/useSelection.tsx +++ b/packages/web/lib/highlights/useSelection.tsx @@ -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(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 diff --git a/packages/web/lib/hooks/useLibraryItemActions.tsx b/packages/web/lib/hooks/useLibraryItemActions.tsx index 18474f702..fce259c47 100644 --- a/packages/web/lib/hooks/useLibraryItemActions.tsx +++ b/packages/web/lib/hooks/useLibraryItemActions.tsx @@ -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, + } } diff --git a/packages/web/lib/networking/highlights/gql.tsx b/packages/web/lib/networking/highlights/gql.tsx new file mode 100644 index 000000000..9225f8795 --- /dev/null +++ b/packages/web/lib/networking/highlights/gql.tsx @@ -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 + } + } + } +` diff --git a/packages/web/lib/networking/highlights/useItemHighlights.tsx b/packages/web/lib/networking/highlights/useItemHighlights.tsx new file mode 100644 index 000000000..da62088ef --- /dev/null +++ b/packages/web/lib/networking/highlights/useItemHighlights.tsx @@ -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 +} diff --git a/packages/web/lib/networking/library_items/gql.tsx b/packages/web/lib/networking/library_items/gql.tsx index cc8ce12f2..f9215c9b2 100644 --- a/packages/web/lib/networking/library_items/gql.tsx +++ b/packages/web/lib/networking/library_items/gql.tsx @@ -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) { diff --git a/packages/web/lib/networking/library_items/useLibraryItems.tsx b/packages/web/lib/networking/library_items/useLibraryItems.tsx index 7b92719da..174e8a917 100644 --- a/packages/web/lib/networking/library_items/useLibraryItems.tsx +++ b/packages/web/lib/networking/library_items/useLibraryItems.tsx @@ -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[] diff --git a/packages/web/lib/networking/mutations/createHighlightMutation.ts b/packages/web/lib/networking/mutations/createHighlightMutation.ts index f21268b96..703f2bfd2 100644 --- a/packages/web/lib/networking/mutations/createHighlightMutation.ts +++ b/packages/web/lib/networking/mutations/createHighlightMutation.ts @@ -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 diff --git a/packages/web/lib/networking/mutations/deleteLinkMutation.ts b/packages/web/lib/networking/mutations/deleteLinkMutation.ts deleted file mode 100644 index c76d35e47..000000000 --- a/packages/web/lib/networking/mutations/deleteLinkMutation.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { gql } from 'graphql-request' -import { gqlFetcher } from '../networkHelpers' - -export async function deleteLinkMutation( - linkId: string -): Promise { - 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 - } -} diff --git a/packages/web/lib/networking/mutations/moveToLibraryMutation.ts b/packages/web/lib/networking/mutations/moveToLibraryMutation.ts deleted file mode 100644 index 205312dd6..000000000 --- a/packages/web/lib/networking/mutations/moveToLibraryMutation.ts +++ /dev/null @@ -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 { - 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 - } -} diff --git a/packages/web/lib/networking/mutations/setLinkArchivedMutation.ts b/packages/web/lib/networking/mutations/setLinkArchivedMutation.ts deleted file mode 100644 index cf25ec90c..000000000 --- a/packages/web/lib/networking/mutations/setLinkArchivedMutation.ts +++ /dev/null @@ -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 | 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 | undefined - } catch (error) { - console.log('SetLinkArchivedInput error', error) - return undefined - } -} diff --git a/packages/web/lib/networking/mutations/updateShareHighlightCommentMutation.ts b/packages/web/lib/networking/mutations/updateShareHighlightCommentMutation.ts deleted file mode 100644 index d563862d0..000000000 --- a/packages/web/lib/networking/mutations/updateShareHighlightCommentMutation.ts +++ /dev/null @@ -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 { - 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 - } -} diff --git a/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx b/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx index 94870fd94..02ace5c84 100644 --- a/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx @@ -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) { diff --git a/packages/web/lib/networking/queries/useGetArticleQuery.tsx b/packages/web/lib/networking/queries/useGetArticleQuery.tsx index 48139d510..63974022b 100644 --- a/packages/web/lib/networking/queries/useGetArticleQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleQuery.tsx @@ -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! diff --git a/packages/web/lib/networking/queries/useGetArticleSavingStatus.tsx b/packages/web/lib/networking/queries/useGetArticleSavingStatus.tsx index 8a87f10ac..a99cdef22 100644 --- a/packages/web/lib/networking/queries/useGetArticleSavingStatus.tsx +++ b/packages/web/lib/networking/queries/useGetArticleSavingStatus.tsx @@ -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 diff --git a/packages/web/pages/[username]/[slug]/debug.tsx b/packages/web/pages/[username]/[slug]/debug.tsx index f933e2abc..2e74926fa 100644 --- a/packages/web/pages/[username]/[slug]/debug.tsx +++ b/packages/web/pages/[username]/[slug]/debug.tsx @@ -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 ( <> diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index c48ed2859..2d36747f8 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -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( () => 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 ) => { diff --git a/packages/web/pages/app/[username]/link-request/[id].tsx b/packages/web/pages/app/[username]/link-request/[id].tsx index a1d96f8ff..abd41ee02 100644 --- a/packages/web/pages/app/[username]/link-request/[id].tsx +++ b/packages/web/pages/app/[username]/link-request/[id].tsx @@ -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` diff --git a/packages/web/pages/highlights-old.tsx b/packages/web/pages/highlights-old.tsx deleted file mode 100644 index abdc5a183..000000000 --- a/packages/web/pages/highlights-old.tsx +++ /dev/null @@ -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 ( - - - - ) - } - - return ( - - - - { - router?.push(`/home?q=${searchQuery}`) - }} - /> - - {highlights.map((highlight) => { - return ( - viewer.viewerData?.me && ( - - ) - ) - })} - - - ) -} - -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 ( - { - setAnnotation(highlight.annotation) - }} - /> - ) -} - -function HighlightCard(props: HighlightCardProps): JSX.Element { - const [isOpen, setIsOpen] = useState(false) - const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] = - useState(undefined) - const [labelsTarget, setLabelsTarget] = useState( - 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 ( - - - - - - - {timeAgo(props.highlight.updatedAt)} - - {props.highlight.quote && ( - - {props.highlight.quote} - - )} - - {props.highlight.labels && ( - - {props.highlight.labels.map((label) => { - return ( - - ) - })} - - )} - - {props.highlight.libraryItem?.title} - - - {props.highlight.libraryItem?.author} - - {showConfirmDeleteHighlightId && ( - { - ;(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={ - - } - /> - )} - {labelsTarget && ( - { - // Don't actually need to do something here - console.log('update highlight: ', highlight) - }} - onOpenChange={() => { - props.mutate() - setLabelsTarget(undefined) - }} - /> - )} - - ) -}