diff --git a/packages/web/components/elements/HighlightNoteTextEditArea.tsx b/packages/web/components/elements/HighlightNoteTextEditArea.tsx deleted file mode 100644 index 7de604a19..000000000 --- a/packages/web/components/elements/HighlightNoteTextEditArea.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useCallback, useState } from 'react' -import { Highlight } from '../../lib/networking/fragments/highlightFragment' -import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation' -import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' -import { Button } from './Button' -import { HStack, VStack } from './LayoutPrimitives' -import { StyledTextArea } from './StyledTextArea' - -type HighlightNoteTextEditAreaProps = { - setIsEditing: (editing: boolean) => void - highlight: Highlight - updateHighlight: (highlight: Highlight) => void -} - -export const HighlightNoteTextEditArea = ( - props: HighlightNoteTextEditAreaProps -): JSX.Element => { - const [noteContent, setNoteContent] = useState( - props.highlight.annotation ?? '' - ) - - const handleNoteContentChange = useCallback( - (event: React.ChangeEvent): void => { - setNoteContent(event.target.value) - }, - [setNoteContent] - ) - - return ( - - - - - - - - ) -} diff --git a/packages/web/components/patterns/HighlightNotes.tsx b/packages/web/components/patterns/HighlightNotes.tsx new file mode 100644 index 000000000..3ccf0f647 --- /dev/null +++ b/packages/web/components/patterns/HighlightNotes.tsx @@ -0,0 +1,443 @@ +/* eslint-disable react/no-children-prop */ +import { + ChangeEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import { formattedShortTime } from '../../lib/dateFormatting' +import { HStack, SpanBox, VStack } from '../elements/LayoutPrimitives' + +import MarkdownIt from 'markdown-it' +import MdEditor from 'react-markdown-editor-lite' +import 'react-markdown-editor-lite/lib/index.css' +import ReactMarkdown from 'react-markdown' +import throttle from 'lodash/throttle' +import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation' +import { Highlight } from '../../lib/networking/fragments/highlightFragment' +import { Button } from '../elements/Button' +import { + ModalContent, + ModalOverlay, + ModalRoot, +} from '../elements/ModalPrimitives' +import { CloseButton } from '../elements/CloseButton' +import { StyledText } from '../elements/StyledText' + +const mdParser = new MarkdownIt() + +type NoteSectionProps = { + placeHolder: string + mode: 'edit' | 'preview' + + sizeMode: 'normal' | 'maximized' + setEditMode: (set: 'edit' | 'preview') => void + + text: string | undefined + saveText: (text: string, completed: (success: boolean) => void) => void +} + +export function HighlightNoteBox(props: NoteSectionProps): JSX.Element { + const [lastSaved, setLastSaved] = useState(undefined) + + const saveText = useCallback( + (text, updateTime) => { + props.saveText(text, (success) => { + if (success) { + setLastSaved(updateTime) + } + }) + }, + [props] + ) + + return ( + + ) +} + +type HighlightViewNoteProps = { + placeHolder: string + mode: 'edit' | 'preview' + + highlight: Highlight + + sizeMode: 'normal' | 'maximized' + setEditMode: (set: 'edit' | 'preview') => void + + text: string | undefined + updateHighlight: (highlight: Highlight) => void +} + +export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element { + const [lastSaved, setLastSaved] = useState(undefined) + + const saveText = useCallback( + (text, updateTime) => { + ;(async () => { + const success = await updateHighlightMutation({ + annotation: text, + highlightId: props.highlight?.id, + }) + if (success) { + setLastSaved(updateTime) + props.highlight.annotation = text + props.updateHighlight(props.highlight) + } + })() + }, + [props] + ) + + return ( + + ) +} + +type MarkdownNote = { + placeHolder: string + mode: 'edit' | 'preview' + + sizeMode: 'normal' | 'maximized' + setEditMode: (set: 'edit' | 'preview') => void + + text: string | undefined + fillBackground: boolean | undefined + + lastSaved: Date | undefined + saveText: (text: string, updateTime: Date) => void +} + +export function MarkdownNote(props: MarkdownNote): JSX.Element { + const editorRef = useRef(null) + const [lastChanged, setLastChanged] = useState(undefined) + const [errorSaving, setErrorSaving] = useState(undefined) + + const saveRef = useRef(props.saveText) + + useEffect(() => { + saveRef.current = props.saveText + }, [props.lastSaved, lastChanged]) + + const debouncedSave = useMemo< + (text: string, updateTime: Date) => void + >(() => { + const func = (text: string, updateTime: Date) => { + saveRef.current?.(text, updateTime) + } + return throttle(func, 3000) + }, []) + + const handleEditorChange = useCallback( + ( + data: { text: string; html: string }, + event?: ChangeEvent | undefined + ) => { + if (event) { + event.preventDefault() + } + + const updateTime = new Date() + setLastChanged(updateTime) + debouncedSave(data.text, updateTime) + }, + [props.lastSaved, lastChanged] + ) + + return ( + <> + {props.mode == 'edit' ? ( + .section': { + borderRight: 'unset', + }, + '.rc-md-editor .editor-container .sec-md .input': { + padding: '10px', + borderRadius: '5px', + fontSize: '16px', + }, + }} + onKeyDown={(event: React.KeyboardEvent) => { + if (event.code.toLowerCase() === 'escape') { + props.setEditMode('preview') + event.preventDefault() + } + }} + > + mdParser.render(text)} + onChange={handleEditorChange} + /> + + {errorSaving && ( + + {errorSaving} + + )} + {props.lastSaved !== undefined ? ( + <> + {lastChanged === props.lastSaved + ? 'Saved' + : `Last saved ${formattedShortTime( + props.lastSaved.toISOString() + )}`} + + ) : null} + {lastChanged !== props.lastSaved && ( + + + + )} + + + ) : ( + <> + *': { + m: '0px', + }, + }} + onClick={() => props.setEditMode('edit')} + > + + + + )} + + ) +} + +type MarkdownModalProps = { + placeHolder: string + mode: 'edit' | 'preview' + + sizeMode: 'normal' | 'maximized' + setEditMode: (set: 'edit' | 'preview') => void + + text: string | undefined + saveText: (text: string, completed: (success: boolean) => void) => void +} + +export function MarkdownModal(props: MarkdownModalProps): JSX.Element { + const [lastSaved, setLastSaved] = useState(undefined) + + const saveText = useCallback( + (text, updateTime) => { + props.saveText(text, (success) => { + if (success) { + setLastSaved(updateTime) + } + }) + }, + [props] + ) + + const handleClose = useCallback(() => { + console.log('onOpenChange') + }, []) + + return ( + + + + + + + Edit Note + + + {/* }> + { + exportHighlights() + }} + title="Export Notebook" + /> + { + setShowConfirmDeleteNote(true) + }} + title="Delete Document Note" + /> + */} + + + + + + + + + + ) +} diff --git a/packages/web/components/patterns/HighlightView.tsx b/packages/web/components/patterns/HighlightView.tsx index 6141e6962..c85f47e4d 100644 --- a/packages/web/components/patterns/HighlightView.tsx +++ b/packages/web/components/patterns/HighlightView.tsx @@ -1,60 +1,134 @@ -import { Fragment, useMemo } from 'react' +/* eslint-disable react/no-children-prop */ +import { BookOpen, PencilLine } from 'phosphor-react' +import { useState } from 'react' import type { Highlight } from '../../lib/networking/fragments/highlightFragment' import { LabelChip } from '../elements/LabelChip' -import { Box, VStack, Blockquote, SpanBox } from '../elements/LayoutPrimitives' -import { StyledText } from '../elements/StyledText' +import { + Box, + VStack, + Blockquote, + SpanBox, + HStack, +} from '../elements/LayoutPrimitives' import { styled } from '../tokens/stitches.config' +import { HighlightViewNote, MarkdownModal } from './HighlightNotes' +import ReactMarkdown from 'react-markdown' type HighlightViewProps = { highlight: Highlight author?: string title?: string - scrollToHighlight?: (arg: string) => void + updateHighlight: (highlight: Highlight) => void } const StyledQuote = styled(Blockquote, { margin: '0px 0px 0px 0px', fontSize: '18px', lineHeight: '27px', - color: '$grayText', - padding: '0px 16px', - borderLeft: '2px solid $omnivoreCtaYellow', }) export function HighlightView(props: HighlightViewProps): JSX.Element { - const lines = useMemo( - () => props.highlight.quote.split('\n'), - [props.highlight.quote] - ) + const [noteMode, setNoteMode] = useState<'preview' | 'edit'>('preview') return ( - - { - if (props.scrollToHighlight) { - props.scrollToHighlight(props.highlight.id) - } + + + + + + - - {lines.map((line: string, index: number) => ( - - {line} - {index !== lines.length - 1 && ( - <> -
-
- - )} -
+ + *': { + m: '0px', + }, + fontSize: '15px', + lineHeight: 1.5, + color: '$grayText', + img: { + display: 'block', + margin: '0.5em auto !important', + maxWidth: '100% !important', + height: 'auto', + }, + }} + > + + + + + {props.highlight.labels?.map(({ name, color }, index) => ( + ))} -
-
- - {props.highlight.labels?.map(({ name, color }, index) => ( - - ))} - -
+ + + + { + setNoteMode(noteMode == 'preview' ? 'edit' : 'preview') + event.preventDefault() + }} + > + {noteMode === 'edit' ? ( + + ) : ( + + )} + + + + ) } diff --git a/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx index fb5b4cd1b..39613cbcb 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx @@ -1,4 +1,4 @@ -import { Box, VStack, HStack } from '../../elements/LayoutPrimitives' +import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives' import { useCallback, useMemo, useState } from 'react' import { CaretDown, CaretUp } from 'phosphor-react' import { MetaStyle, timeAgo, TitleStyle } from './LibraryCardStyles' @@ -7,9 +7,9 @@ import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery' import { Button } from '../../elements/Button' import { theme } from '../../tokens/stitches.config' -import { HighlightItem } from '../../templates/homeFeed/HighlightItem' import { getHighlightLocation } from '../../templates/article/NotebookModal' import { Highlight } from '../../../lib/networking/fragments/highlightFragment' +import { HighlightView } from '../HighlightView' export const GridSeparator = styled(Box, { height: '1px', @@ -46,21 +46,23 @@ export function LibraryHighlightGridCard( return [] } - return props.item.highlights.sort((a: Highlight, b: Highlight) => { - if (a.highlightPositionPercent && b.highlightPositionPercent) { - return sorted(a.highlightPositionPercent, b.highlightPositionPercent) - } - // We do this in a try/catch because it might be an invalid diff - // With PDF it will definitely be an invalid diff. - try { - const aPos = getHighlightLocation(a.patch) - const bPos = getHighlightLocation(b.patch) - if (aPos && bPos) { - return sorted(aPos, bPos) + return props.item.highlights + .filter((h) => h.type === 'HIGHLIGHT') + .sort((a: Highlight, b: Highlight) => { + if (a.highlightPositionPercent && b.highlightPositionPercent) { + return sorted(a.highlightPositionPercent, b.highlightPositionPercent) } - } catch {} - return a.createdAt.localeCompare(b.createdAt) - }) + // We do this in a try/catch because it might be an invalid diff + // With PDF it will definitely be an invalid diff. + try { + const aPos = getHighlightLocation(a.patch) + const bPos = getHighlightLocation(b.patch) + if (aPos && bPos) { + return sorted(aPos, bPos) + } + } catch {} + return a.createdAt.localeCompare(b.createdAt) + }) }, [props.item.highlights]) return ( @@ -121,17 +123,20 @@ export function LibraryHighlightGridCard( <> {sortedHighlights.map((highlight) => ( - + + { + console.log('updated highlight: ', highlight) + }} + /> + + ))} diff --git a/packages/web/components/templates/article/ArticleContainer.tsx b/packages/web/components/templates/article/ArticleContainer.tsx index 174f74f4e..5f80488e9 100644 --- a/packages/web/components/templates/article/ArticleContainer.tsx +++ b/packages/web/components/templates/article/ArticleContainer.tsx @@ -18,14 +18,15 @@ import { LabelChip } from '../../elements/LabelChip' import { Label } from '../../../lib/networking/fragments/labelFragment' import { Recommendation } from '../../../lib/networking/queries/useGetLibraryItemsQuery' import { Avatar } from '../../elements/Avatar' +import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' type ArticleContainerProps = { + viewer: UserBasicData article: ArticleAttributes labels: Label[] articleMutations: ArticleMutations isAppleAppEmbed: boolean highlightBarDisabled: boolean - highlightsBaseURL: string margin?: number fontSize?: number fontFamily?: string @@ -107,15 +108,12 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { const [showReportIssuesModal, setShowReportIssuesModal] = useState(false) const [fontSize, setFontSize] = useState(props.fontSize ?? 20) // iOS app embed can overide the original margin and line height - const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] = useState< - number | null - >(null) - const [lineHeightOverride, setLineHeightOverride] = useState( - null - ) - const [fontFamilyOverride, setFontFamilyOverride] = useState( - null - ) + const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] = + useState(null) + const [lineHeightOverride, setLineHeightOverride] = + useState(null) + const [fontFamilyOverride, setFontFamilyOverride] = + useState(null) const [highContrastText, setHighContrastText] = useState( props.highContrastText ?? false ) @@ -388,13 +386,14 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { - - - ) -} diff --git a/packages/web/components/templates/article/HighlightPostToFeedModal.tsx b/packages/web/components/templates/article/HighlightPostToFeedModal.tsx deleted file mode 100644 index 73812a544..000000000 --- a/packages/web/components/templates/article/HighlightPostToFeedModal.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { - ModalRoot, - ModalContent, - ModalOverlay, -} from './../../elements/ModalPrimitives' -import { Box, HStack } from '../../elements/LayoutPrimitives' -import { Button } from '../../elements/Button' -import { StyledText } from '../../elements/StyledText' -import { Highlight } from '../../../lib/networking/fragments/highlightFragment' -import { HighlightView } from '../../patterns/HighlightView' -import { useCallback, useState } from 'react' -import { StyledTextArea } from '../../elements/StyledTextArea' - -type HighlightPostToFeedModalProps = { - highlight: Highlight - author: string - title: string - onCommit: (highlight: Highlight, comment: string) => void - onOpenChange: (open: boolean) => void -} - -export function HighlightPostToFeedModal( - props: HighlightPostToFeedModalProps -): JSX.Element { - const [comment, setComment] = useState('') - - const handleCommentChange = useCallback( - (event: React.ChangeEvent): void => { - setComment(event.target.value) - }, - [setComment] - ) - - const postHighlight = useCallback(async () => { - props.onCommit(props.highlight, comment) - props.onOpenChange(false) - }, [comment, props]) - - return ( - - - { - event.preventDefault() - }} - css={{ overflow: 'auto' }} - > - - - - Post Highlight - - - - - - - - ) -} diff --git a/packages/web/components/templates/article/HighlightViewItem.tsx b/packages/web/components/templates/article/HighlightViewItem.tsx new file mode 100644 index 000000000..5b9effade --- /dev/null +++ b/packages/web/components/templates/article/HighlightViewItem.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react' +import { Highlight } from '../../../lib/networking/fragments/highlightFragment' +import { + LibraryItem, + ReadableItem, +} from '../../../lib/networking/queries/useGetLibraryItemsQuery' +import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' +import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' +import { HighlightView } from '../../patterns/HighlightView' +import { HighlightsMenu } from '../homeFeed/HighlightItem' + +type HighlightViewItemProps = { + viewer: UserBasicData + + item: ReadableItem + highlight: Highlight + + viewInReader: (highlightId: string) => void + + deleteHighlightAction: () => void + updateHighlight: (highlight: Highlight) => void + + setSetLabelsTarget: (highlight: Highlight) => void + setShowConfirmDeleteHighlightId: (id: string | undefined) => void +} + +export function HighlightViewItem(props: HighlightViewItemProps): JSX.Element { + const [hover, setHover] = useState(false) + + return ( + setHover(true)} + onMouseLeave={() => setHover(false)} + > + + + + + + + + + ) +} diff --git a/packages/web/components/templates/article/HighlightsLayer.tsx b/packages/web/components/templates/article/HighlightsLayer.tsx index 675393b70..9ede4f3c3 100644 --- a/packages/web/components/templates/article/HighlightsLayer.tsx +++ b/packages/web/components/templates/article/HighlightsLayer.tsx @@ -26,17 +26,25 @@ import { isTouchScreenDevice } from '../../../lib/deviceType' import { SetLabelsModal } from './SetLabelsModal' import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabelsForHighlight' import { Label } from '../../../lib/networking/fragments/labelFragment' +import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' +import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery' +import { useRouter } from 'next/router' +import { MarkdownModal } from '../../patterns/HighlightNotes' type HighlightsLayerProps = { + viewer: UserBasicData + + item: ReadableItem highlights: Highlight[] + articleId: string articleTitle: string articleAuthor: string isAppleAppEmbed: boolean highlightBarDisabled: boolean showHighlightsModal: boolean - highlightsBaseURL: string scrollToHighlight: MutableRefObject + setShowHighlightsModal: React.Dispatch> articleMutations: ArticleMutations } @@ -59,6 +67,7 @@ interface SpeakingSectionEvent extends Event { } export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { + const router = useRouter() const [highlights, setHighlights] = useState(props.highlights) const [highlightModalAction, setHighlightModalAction] = useState({ highlightModalAction: 'none' }) @@ -68,15 +77,13 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { >([]) const focusedHighlightMousePos = useRef({ pageX: 0, pageY: 0 }) - const [focusedHighlight, setFocusedHighlight] = useState< - Highlight | undefined - >(undefined) + const [focusedHighlight, setFocusedHighlight] = + useState(undefined) const [selectionData, setSelectionData] = useSelection(highlightLocations) - const [labelsTarget, setLabelsTarget] = useState( - undefined - ) + const [labelsTarget, setLabelsTarget] = + useState(undefined) const canShareNative = useCanShareNative() @@ -121,14 +128,16 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { // Load the highlights useEffect(() => { const res: HighlightLocation[] = [] - highlights.forEach((highlight) => { - try { - const offset = makeHighlightStartEndOffset(highlight) - res.push(offset) - } catch (err) { - console.error(err) - } - }) + highlights + .filter((h) => h.type == 'HIGHLIGHT') + .forEach((highlight) => { + try { + const offset = makeHighlightStartEndOffset(highlight) + res.push(offset) + } catch (err) { + console.error(err) + } + }) setHighlightLocations(res) // If we were given an initial highlight to scroll to we do @@ -139,7 +148,10 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { `[omnivore-highlight-id="${props.scrollToHighlight.current}"]` ) if (anchorElement) { - anchorElement.scrollIntoView({ behavior: 'auto' }) + anchorElement.scrollIntoView({ + block: 'center', + behavior: 'auto', + }) } } }, [highlights, setHighlightLocations, props.scrollToHighlight]) @@ -179,23 +191,20 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { [highlights, highlightLocations] ) - const handleNativeShare = useCallback( - (highlightID: string) => { - navigator - ?.share({ - title: props.articleTitle, - url: `${props.highlightsBaseURL}/${highlightID}`, - }) - .then(() => { - setFocusedHighlight(undefined) - }) - .catch((error) => { - console.log(error) - setFocusedHighlight(undefined) - }) - }, - [props.articleTitle, props.highlightsBaseURL] - ) + // const handleNativeShare = useCallback((highlightID: string) => { + // // navigator + // // ?.share({ + // // title: props.articleTitle, + // // url: `${props.highlightsBaseURL}/${highlightID}`, + // // }) + // // .then(() => { + // // setFocusedHighlight(undefined) + // // }) + // // .catch((error) => { + // // console.log(error) + // // setFocusedHighlight(undefined) + // // }) + // }, []) const openNoteModal = useCallback( (inputs: HighlightActionProps) => { @@ -280,7 +289,6 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { } }, [ - handleNativeShare, highlights, openNoteModal, props.articleId, @@ -356,6 +364,24 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { [highlights, highlightLocations, openNoteModal] ) + const handleCloseNotebook = useCallback( + (updatedHighlights: Highlight[], deletedHighlights: Highlight[]) => { + props.setShowHighlightsModal(false) + + setHighlights(updatedHighlights) + + removeHighlights( + deletedHighlights.map((h) => h.id), + highlightLocations + ) + + updatedHighlights.forEach((h) => { + updateHighlightsCallback(h) + }) + }, + [highlights, highlightLocations] + ) + useEffect(() => { if (typeof window === 'undefined') { return @@ -388,37 +414,37 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { }) } break - case 'share': - if (props.isAppleAppEmbed) { - window?.webkit?.messageHandlers.highlightAction?.postMessage({ - actionID: 'share', - highlightID: focusedHighlight?.id, - }) - } + // case 'share': + // if (props.isAppleAppEmbed) { + // window?.webkit?.messageHandlers.highlightAction?.postMessage({ + // actionID: 'share', + // highlightID: focusedHighlight?.id, + // }) + // } - window?.AndroidWebKitMessenger?.handleIdentifiableMessage( - 'shareHighlight', - JSON.stringify({ - highlightID: focusedHighlight?.id, - }) - ) + // window?.AndroidWebKitMessenger?.handleIdentifiableMessage( + // 'shareHighlight', + // JSON.stringify({ + // highlightID: focusedHighlight?.id, + // }) + // ) - if (focusedHighlight) { - if (canShareNative) { - handleNativeShare(focusedHighlight.shortId) - } else { - setHighlightModalAction({ - highlight: focusedHighlight, - highlightModalAction: 'share', - }) - } - } else { - await createHighlightCallback('share') - } - break - case 'unshare': - console.log('unshare') - break // TODO: implement -- need to show confirmation dialog + // if (focusedHighlight) { + // if (canShareNative) { + // handleNativeShare(focusedHighlight.shortId) + // } else { + // setHighlightModalAction({ + // highlight: focusedHighlight, + // highlightModalAction: 'share', + // }) + // } + // } else { + // await createHighlightCallback('share') + // } + // break + // case 'unshare': + // console.log('unshare') + // break // TODO: implement -- need to show confirmation dialog case 'setHighlightLabels': if (props.isAppleAppEmbed) { window?.webkit?.messageHandlers.highlightAction?.postMessage({ @@ -434,7 +460,6 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { [ createHighlightCallback, focusedHighlight, - handleNativeShare, openNoteModal, props.highlightBarDisabled, props.isAppleAppEmbed, @@ -498,7 +523,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { } const copy = async () => { - if (focusedHighlight) { + if (focusedHighlight && focusedHighlight.quote) { if (window.AndroidWebKitMessenger) { window.AndroidWebKitMessenger.handleIdentifiableMessage( 'writeToClipboard', @@ -647,12 +672,28 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { if (props.showHighlightsModal) { return ( props.setShowHighlightsModal(false)} - deleteHighlightAction={(highlightId: string) => { - removeHighlightCallback(highlightId) + onClose={handleCloseNotebook} + viewHighlightInReader={(highlightId) => { + // The timeout here is a bit of a hack to work around rerendering + setTimeout(() => { + const target = document.querySelector( + `[omnivore-highlight-id="${highlightId}"]` + ) + target?.scrollIntoView({ + block: 'center', + behavior: 'auto', + }) + }, 1) + history.replaceState( + undefined, + window.location.href, + `#${highlightId}` + ) + props.setShowHighlightsModal(false) }} - updateHighlight={updateHighlightsCallback} /> ) } diff --git a/packages/web/components/templates/article/Notebook.tsx b/packages/web/components/templates/article/Notebook.tsx new file mode 100644 index 000000000..37ee8a531 --- /dev/null +++ b/packages/web/components/templates/article/Notebook.tsx @@ -0,0 +1,482 @@ +import { Box, HStack, VStack, SpanBox } from '../../elements/LayoutPrimitives' +import { StyledText } from '../../elements/StyledText' +import { theme } from '../../tokens/stitches.config' +import type { Highlight } from '../../../lib/networking/fragments/highlightFragment' +import { useCallback, useEffect, useMemo, useReducer, useState } from 'react' +import { BookOpen, PencilLine, X } from 'phosphor-react' +import { SetLabelsModal } from './SetLabelsModal' +import { Label } from '../../../lib/networking/fragments/labelFragment' +import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabelsForHighlight' +import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation' +import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' +import { diff_match_patch } from 'diff-match-patch' +import { highlightsAsMarkdown } from '../homeFeed/HighlightItem' +import 'react-markdown-editor-lite/lib/index.css' +import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation' +import { v4 as uuidv4 } from 'uuid' +import { nanoid } from 'nanoid' +import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation' +import { HighlightNoteBox, MarkdownNote } from '../../patterns/HighlightNotes' +import { HighlightViewItem } from './HighlightViewItem' +import { ConfirmationModal } from '../../patterns/ConfirmationModal' +import { TrashIcon } from '../../elements/images/TrashIcon' +import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' +import { + LibraryItem, + ReadableItem, +} from '../../../lib/networking/queries/useGetLibraryItemsQuery' + +type NotebookProps = { + viewer: UserBasicData + + item: ReadableItem + highlights: Highlight[] + + sizeMode: 'normal' | 'maximized' + + viewInReader: (highlightId: string) => void + + onAnnotationsChanged?: ( + highlights: Highlight[], + deletedAnnotations: Highlight[] + ) => void +} + +export const getHighlightLocation = (patch: string): number | undefined => { + const dmp = new diff_match_patch() + const patches = dmp.patch_fromText(patch) + return patches[0].start1 || undefined +} + +type AnnotationInfo = { + loaded: boolean + + note: Highlight | undefined + noteId: string + + allAnnotations: Highlight[] + deletedAnnotations: Highlight[] +} + +export function Notebook(props: NotebookProps): JSX.Element { + const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] = + useState(undefined) + const [labelsTarget, setLabelsTarget] = + useState(undefined) + const [showConfirmDeleteNote, setShowConfirmDeleteNote] = useState(false) + const [notesEditMode, setNotesEditMode] = + useState<'edit' | 'preview'>('preview') + const [, updateState] = useState({}) + + const annotationsReducer = ( + state: AnnotationInfo, + action: { + type: string + allHighlights?: Highlight[] + note?: Highlight | undefined + + updateHighlight?: Highlight | undefined + deleteHighlightId?: string | undefined + } + ) => { + switch (action.type) { + case 'RESET': { + const note = action.allHighlights?.find((h) => h.type == 'NOTE') + return { + ...state, + loaded: true, + note: note, + noteId: note?.id ?? state.noteId, + allAnnotations: [...(action.allHighlights ?? [])], + } + } + case 'CREATE_NOTE': { + if (!action.note) { + throw new Error('No note on CREATE_NOTE action') + } + return { + ...state, + note: action.note, + noteId: action.note.id, + allAnnotations: [...state.allAnnotations, action.note], + } + } + case 'DELETE_NOTE': { + // If there is no note to delete, just make sure we have cleared out the note + const noteId = action.note?.id + if (!action.note?.id) { + return { + ...state, + node: undefined, + noteId: uuidv4(), + } + } + const idx = state.allAnnotations.findIndex((h) => h.id === noteId) + return { + ...state, + note: undefined, + noteId: uuidv4(), + allAnnotations: state.allAnnotations.splice(idx, 1), + } + } + case 'DELETE_HIGHLIGHT': { + const highlightId = action.deleteHighlightId + console.log(' DELETE_HIGHLIGHT: ', highlightId) + + if (!highlightId) { + throw new Error('No highlightId for delete action.') + } + const idx = state.allAnnotations.findIndex((h) => h.id === highlightId) + if (idx < 0) { + return { ...state } + } + const deleted = state.deletedAnnotations + deleted.push(state.allAnnotations[idx]) + + return { + ...state, + deletedAnnotations: deleted, + allAnnotations: state.allAnnotations.splice(idx, 1), + } + } + case 'UPDATE_HIGHLIGHT': { + const highlight = action.updateHighlight + if (!highlight) { + throw new Error('No highlightId for delete action.') + } + const idx = state.allAnnotations.findIndex((h) => h.id === highlight.id) + if (idx !== -1) { + state.allAnnotations[idx] = highlight + } + return { + ...state, + } + } + default: + return state + } + } + + const [annotations, dispatchAnnotations] = useReducer(annotationsReducer, { + loaded: false, + note: undefined, + noteId: uuidv4(), + allAnnotations: [], + deletedAnnotations: [], + }) + + useEffect(() => { + dispatchAnnotations({ + type: 'RESET', + allHighlights: props.highlights, + }) + }, [props.highlights]) + + useEffect(() => { + if (props.onAnnotationsChanged) { + props.onAnnotationsChanged( + annotations.allAnnotations, + annotations.deletedAnnotations + ) + } + }, [annotations]) + + const deleteDocumentNote = useCallback(() => { + const note = annotations.note + if (!note) { + showErrorToast('No note found') + return + } + ;(async () => { + try { + const result = await deleteHighlightMutation(note.id) + if (!result) { + throw new Error() + } + showSuccessToast('Note deleted') + dispatchAnnotations({ + note, + type: 'DELETE_NOTE', + }) + } catch (err) { + console.log('error deleting note', err) + showErrorToast('Error deleting note') + } + })() + }, [annotations]) + + const sortedHighlights = useMemo(() => { + const sorted = (a: number, b: number) => { + if (a < b) { + return -1 + } + if (a > b) { + return 1 + } + return 0 + } + + return annotations.allAnnotations + .filter((h) => h.type === 'HIGHLIGHT') + .sort((a: Highlight, b: Highlight) => { + if (a.highlightPositionPercent && b.highlightPositionPercent) { + return sorted(a.highlightPositionPercent, b.highlightPositionPercent) + } + // We do this in a try/catch because it might be an invalid diff + // With PDF it will definitely be an invalid diff. + try { + const aPos = getHighlightLocation(a.patch) + const bPos = getHighlightLocation(b.patch) + if (aPos && bPos) { + return sorted(aPos, bPos) + } + } catch {} + return a.createdAt.localeCompare(b.createdAt) + }) + }, [annotations]) + + const handleSaveNoteText = useCallback( + (text, cb: (success: boolean) => void) => { + if (!annotations.loaded) { + // We haven't loaded the user's annotations yet, so we can't + // find or create their highlight note. + return + } + + if (!annotations.note) { + const noteId = annotations.noteId + ;(async () => { + const success = await createHighlightMutation({ + id: noteId, + shortId: nanoid(8), + type: 'NOTE', + articleId: props.item.id, + annotation: text, + }) + console.log('success creating annotation note: ', success) + if (success) { + dispatchAnnotations({ + type: 'CREATE_NOTE', + note: success, + }) + } + cb(!!success) + })() + return + } + + if (annotations.note) { + const note = annotations.note + ;(async () => { + const success = await updateHighlightMutation({ + highlightId: note.id, + annotation: text, + }) + console.log('success updating annotation note: ', success) + if (success) { + note.annotation = text + dispatchAnnotations({ + type: 'UPDATE_NOTE', + note: note, + }) + } + cb(!!success) + })() + return + } + }, + [annotations, props.item] + ) + return ( + + setNotesEditMode(edit ? 'edit' : 'preview')} + /> + + + + + + {sortedHighlights.map((highlight) => ( + { + dispatchAnnotations({ + type: 'DELETE_HIGHLIGHT', + deleteHighlightId: highlight.id, + }) + }} + updateHighlight={() => { + dispatchAnnotations({ + type: 'UPDATE_HIGHLIGHT', + updateHighlight: highlight, + }) + }} + /> + ))} + {sortedHighlights.length === 0 && ( + + You have not added any highlights to this document. + + )} + + + + {showConfirmDeleteHighlightId && ( + { + ;(async () => { + const success = await deleteHighlightMutation( + showConfirmDeleteHighlightId + ) + console.log(' ConfirmationModal::DeleteHighlight', success) + if (success) { + dispatchAnnotations({ + type: 'DELETE_HIGHLIGHT', + deleteHighlightId: showConfirmDeleteHighlightId, + }) + showSuccessToast('Highlight deleted.') + } else { + showErrorToast('Error deleting highlight') + } + })() + setShowConfirmDeleteHighlightId(undefined) + }} + onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)} + icon={ + + } + /> + )} + {labelsTarget && ( + { + const result = setLabelsForHighlight( + labelsTarget.id, + labels.map((label) => label.id) + ) + return result + }} + /> + )} + {showConfirmDeleteNote && ( + { + deleteDocumentNote() + setShowConfirmDeleteNote(false) + }} + onOpenChange={() => setShowConfirmDeleteNote(false)} + /> + )} + + ) +} + +type TitledSectionProps = { + title: string + editMode?: boolean + setEditMode?: (set: boolean) => void +} + +function TitledSection(props: TitledSectionProps): JSX.Element { + return ( + <> + + + {props.title} + + {props.setEditMode && ( + { + if (props.setEditMode) { + props.setEditMode(!props.editMode) + } + event.preventDefault() + }} + > + {props.editMode ? ( + + ) : ( + + )} + + )} + + + ) +} diff --git a/packages/web/components/templates/article/NotebookModal.tsx b/packages/web/components/templates/article/NotebookModal.tsx index 1aca83f46..f7a272689 100644 --- a/packages/web/components/templates/article/NotebookModal.tsx +++ b/packages/web/components/templates/article/NotebookModal.tsx @@ -2,37 +2,33 @@ import { ModalRoot, ModalOverlay, ModalContent, - ModalTitleBar, } from '../../elements/ModalPrimitives' -import { Box, HStack, VStack, SpanBox } from '../../elements/LayoutPrimitives' +import { HStack, SpanBox } from '../../elements/LayoutPrimitives' import { Button } from '../../elements/Button' import { StyledText } from '../../elements/StyledText' -import { TrashIcon } from '../../elements/images/TrashIcon' import { theme } from '../../tokens/stitches.config' import type { Highlight } from '../../../lib/networking/fragments/highlightFragment' -import { HighlightView } from '../../patterns/HighlightView' -import { useCallback, useMemo, useState } from 'react' -import { StyledTextArea } from '../../elements/StyledTextArea' -import { ConfirmationModal } from '../../patterns/ConfirmationModal' -import { DotsThree } from 'phosphor-react' +import { useCallback, useState } from 'react' +import { ArrowsIn, ArrowsOut, X } from 'phosphor-react' import { Dropdown, DropdownOption } from '../../elements/DropdownElements' -import { SetLabelsModal } from './SetLabelsModal' -import { Label } from '../../../lib/networking/fragments/labelFragment' -import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabelsForHighlight' -import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { diff_match_patch } from 'diff-match-patch' -import { HighlightNoteTextEditArea } from '../../elements/HighlightNoteTextEditArea' -import { CloseButton } from '../../elements/CloseButton' import { MenuTrigger } from '../../elements/MenuTrigger' -import { highlightsAsMarkdown, HighlightsMenu } from '../homeFeed/HighlightItem' +import { highlightsAsMarkdown } from '../homeFeed/HighlightItem' +import 'react-markdown-editor-lite/lib/index.css' +import { Notebook } from './Notebook' +import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' +import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery' +import { MarkdownNote } from '../../patterns/HighlightNotes' type NotebookModalProps = { + viewer: UserBasicData + + item: ReadableItem highlights: Highlight[] - scrollToHighlight?: (arg: string) => void - updateHighlight: (highlight: Highlight) => void - deleteHighlightAction?: (highlightId: string) => void - onOpenChange: (open: boolean) => void + + viewHighlightInReader: (arg: string) => void + onClose: (highlights: Highlight[], deletedAnnotations: Highlight[]) => void } export const getHighlightLocation = (patch: string): number | undefined => { @@ -42,252 +38,179 @@ export const getHighlightLocation = (patch: string): number | undefined => { } export function NotebookModal(props: NotebookModalProps): JSX.Element { - const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] = - useState(undefined) - const [labelsTarget, setLabelsTarget] = useState( - undefined + const [sizeMode, setSizeMode] = useState<'normal' | 'maximized'>('normal') + const [showConfirmDeleteNote, setShowConfirmDeleteNote] = useState(false) + const [allAnnotations, setAllAnnotations] = + useState(undefined) + const [deletedAnnotations, setDeletedAnnotations] = + useState(undefined) + + const handleClose = useCallback(() => { + props.onClose(allAnnotations ?? [], deletedAnnotations ?? []) + }, [allAnnotations, deletedAnnotations]) + + const handleAnnotationsChange = useCallback( + (allAnnotations, deletedAnnotations) => { + setAllAnnotations(allAnnotations) + setDeletedAnnotations(deletedAnnotations) + }, + [] ) - const [, updateState] = useState({}) const exportHighlights = useCallback(() => { ;(async () => { - if (!props.highlights) { + if (!allAnnotations) { showErrorToast('No highlights to export') return } - const markdown = highlightsAsMarkdown(props.highlights) + const markdown = highlightsAsMarkdown(allAnnotations) await navigator.clipboard.writeText(markdown) showSuccessToast('Highlight copied') })() - }, [props.highlights]) + }, [allAnnotations]) - const sortedHighlights = useMemo(() => { - const sorted = (a: number, b: number) => { - if (a < b) { - return -1 - } - if (a > b) { - return 1 - } - return 0 - } - - return props.highlights.sort((a: Highlight, b: Highlight) => { - if (a.highlightPositionPercent && b.highlightPositionPercent) { - return sorted(a.highlightPositionPercent, b.highlightPositionPercent) - } - // We do this in a try/catch because it might be an invalid diff - // With PDF it will definitely be an invalid diff. - try { - const aPos = getHighlightLocation(a.patch) - const bPos = getHighlightLocation(b.patch) - if (aPos && bPos) { - return sorted(aPos, bPos) - } - } catch {} - return a.createdAt.localeCompare(b.createdAt) - }) - }, [props.highlights]) + const viewInReader = useCallback( + (highlightId) => { + props.viewHighlightInReader(highlightId) + handleClose() + }, + [props, handleClose] + ) return ( - + { + onInteractOutside={(event) => { event.preventDefault() - props.onOpenChange(false) }} - css={{ overflow: 'auto', px: '24px' }} + css={{ + overflow: 'auto', + height: sizeMode === 'normal' ? 'unset' : '100%', + maxWidth: sizeMode === 'normal' ? '640px' : '100%', + minHeight: sizeMode === 'normal' ? '525px' : 'unset', + '@mdDown': { + top: '20px', + width: '100%', + height: '100%', + maxHeight: 'unset', + transform: 'translate(-50%)', + }, + }} > - + + + Notebook + - Notebook - - }> - { - exportHighlights() - }} - title="Export" - /> - - props.onOpenChange(false)} /> - - - - {sortedHighlights.map((highlight) => ( - { - if (props.deleteHighlightAction) { - props.deleteHighlightAction(highlight.id) - } + + }> + { + exportHighlights() }} - updateHighlight={props.updateHighlight} + title="Export Notebook" /> - ))} - {sortedHighlights.length === 0 && ( - - - You have not added any highlights or notes to this document - - - )} - - + { + setShowConfirmDeleteNote(true) + }} + title="Delete Document Note" + /> + + + + + - {showConfirmDeleteHighlightId && ( - { - if (props.deleteHighlightAction) { - props.deleteHighlightAction(showConfirmDeleteHighlightId) - } - setShowConfirmDeleteHighlightId(undefined) - }} - onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)} - icon={ - - } - /> - )} - {labelsTarget && ( - { - const result = setLabelsForHighlight( - labelsTarget.id, - labels.map((label) => label.id) - ) - return result - }} - /> - )} ) } -type ModalHighlightViewProps = { - highlight: Highlight - showDelete: boolean - scrollToHighlight?: (arg: string) => void - deleteHighlightAction: () => void - updateHighlight: (highlight: Highlight) => void - - setSetLabelsTarget: (highlight: Highlight) => void - setShowConfirmDeleteHighlightId: (id: string | undefined) => void +type SizeToggleProps = { + mode: 'normal' | 'maximized' + setMode: (mode: 'normal' | 'maximized') => void } -function ModalHighlightView(props: ModalHighlightViewProps): JSX.Element { - const [hover, setHover] = useState(false) - const [isEditing, setIsEditing] = useState(false) - - const copyHighlight = useCallback(async () => { - await navigator.clipboard.writeText(props.highlight.quote) - }, [props.highlight]) - +function CloseButton(props: { close: () => void }): JSX.Element { return ( - setHover(true)} - onMouseLeave={() => setHover(false)} + + ) +} + +function SizeToggle(props: SizeToggleProps): JSX.Element { + return ( + ) } diff --git a/packages/web/components/templates/article/PdfArticleContainer.tsx b/packages/web/components/templates/article/PdfArticleContainer.tsx index 80e0c3aec..81c859fc1 100644 --- a/packages/web/components/templates/article/PdfArticleContainer.tsx +++ b/packages/web/components/templates/article/PdfArticleContainer.tsx @@ -2,7 +2,7 @@ import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticle import { Box } from '../../elements/LayoutPrimitives' import { v4 as uuidv4 } from 'uuid' import { nanoid } from 'nanoid' -import { useState, useEffect, useCallback, useRef } from 'react' +import { useState, useEffect, useRef } from 'react' import { isDarkTheme } from '../../../lib/themeUpdater' import PSPDFKit from 'pspdfkit' import { Instance, HighlightAnnotation, List, Annotation, Rect } from 'pspdfkit' @@ -12,15 +12,15 @@ import { deleteHighlightMutation } from '../../../lib/networking/mutations/delet import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation' import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation' import { useCanShareNative } from '../../../lib/hooks/useCanShareNative' -import { webBaseURL } from '../../../lib/appConfig' import { pspdfKitKey } from '../../../lib/appConfig' import { NotebookModal } from './NotebookModal' import { HighlightNoteModal } from './HighlightNoteModal' import { showErrorToast } from '../../../lib/toastHelpers' import { HEADER_HEIGHT, MOBILE_HEADER_HEIGHT } from '../homeFeed/HeaderSpacer' +import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' export type PdfArticleContainerProps = { - viewerUsername: string + viewer: UserBasicData article: ArticleAttributes showHighlightsModal: boolean setShowHighlightsModal: React.Dispatch> @@ -30,43 +30,41 @@ export default function PdfArticleContainer( props: PdfArticleContainerProps ): JSX.Element { const containerRef = useRef(null) - const [shareTarget, setShareTarget] = useState( - undefined - ) + const [shareTarget, setShareTarget] = + useState(undefined) const [notebookKey, setNotebookKey] = useState(uuidv4()) const [noteTarget, setNoteTarget] = useState(undefined) - const [noteTargetPageIndex, setNoteTargetPageIndex] = useState< - number | undefined - >(undefined) + const [noteTargetPageIndex, setNoteTargetPageIndex] = + useState(undefined) const highlightsRef = useRef([]) const canShareNative = useCanShareNative() - const getHighlightURL = useCallback( - (highlightID: string): string => - `${webBaseURL}/${props.viewerUsername}/${props.article.slug}/highlights/${highlightID}`, - [props.article.slug, props.viewerUsername] - ) + // const getHighlightURL = useCallback( + // (highlightID: string): string => + // `${webBaseURL}/${props.viewerUsername}/${props.article.slug}/highlights/${highlightID}`, + // [props.article.slug, props.viewerUsername] + // ) - const nativeShare = useCallback( - async (highlightID: string, title: string) => { - await navigator?.share({ - title: title, - url: getHighlightURL(highlightID), - }) - }, - [getHighlightURL] - ) + // const nativeShare = useCallback( + // async (highlightID: string, title: string) => { + // await navigator?.share({ + // title: title, + // url: getHighlightURL(highlightID), + // }) + // }, + // [getHighlightURL] + // ) - const handleOpenShare = useCallback( - (highlight: Highlight) => { - if (canShareNative) { - nativeShare(highlight.shortId, props.article.title) - } else { - setShareTarget(highlight) - } - }, - [nativeShare, canShareNative, props.article.title] - ) + // const handleOpenShare = useCallback( + // (highlight: Highlight) => { + // if (canShareNative) { + // nativeShare(highlight.shortId, props.article.title) + // } else { + // setShareTarget(highlight) + // } + // }, + // [nativeShare, canShareNative, props.article.title] + // ) const annotationOmnivoreId = (annotation: Annotation): string | undefined => { if ( @@ -178,23 +176,23 @@ export default function PdfArticleContainer( instance.setSelectedAnnotation(null) }, } - const share = { - type: 'custom' as const, - title: 'Share', - id: 'tooltip-share-annotation', - className: 'TooltipItem-Share', - onPress: () => { - if ( - annotation.customData && - annotation.customData.omnivoreHighlight && - (annotation.customData.omnivoreHighlight as Highlight).shortId - ) { - const data = annotation.customData.omnivoreHighlight as Highlight - handleOpenShare(data) - } - instance.setSelectedAnnotation(null) - }, - } + // const share = { + // type: 'custom' as const, + // title: 'Share', + // id: 'tooltip-share-annotation', + // className: 'TooltipItem-Share', + // onPress: () => { + // if ( + // annotation.customData && + // annotation.customData.omnivoreHighlight && + // (annotation.customData.omnivoreHighlight as Highlight).shortId + // ) { + // const data = annotation.customData.omnivoreHighlight as Highlight + // handleOpenShare(data) + // } + // instance.setSelectedAnnotation(null) + // }, + // } return [copy, note, remove] } @@ -237,7 +235,9 @@ export default function PdfArticleContainer( // Store the highlights in the highlightsRef and apply them to the PDF highlightsRef.current = props.article.highlights - for (const highlight of props.article.highlights) { + for (const highlight of props.article.highlights.filter( + (h) => h.type == 'HIGHLIGHT' + )) { const patch = JSON.parse(highlight.patch) if (highlight.annotation && patch.customData.omnivoreHighight) { patch.customData.omnivoreHighight.annotation = highlight.annotation @@ -491,15 +491,26 @@ export default function PdfArticleContainer( {props.showHighlightsModal && ( props.setShowHighlightsModal(false)} - /* eslint-disable @typescript-eslint/no-empty-function */ - updateHighlight={() => {}} - deleteHighlightAction={(highlightId: string) => { - const event = new CustomEvent('deleteHighlightbyId', { - detail: highlightId, + onClose={(updatedHighlights, deletedAnnotations) => { + console.log( + 'closed PDF notebook: ', + updatedHighlights, + deletedAnnotations + ) + deletedAnnotations.forEach((highlight) => { + const event = new CustomEvent('deleteHighlightbyId', { + detail: highlight.id, + }) + document.dispatchEvent(event) }) - document.dispatchEvent(event) + props.setShowHighlightsModal(false) + }} + viewHighlightInReader={(highlightId) => { + // TODO: scroll to highlight in PDF + props.setShowHighlightsModal(false) }} /> )} diff --git a/packages/web/components/templates/homeFeed/HighlightItem.tsx b/packages/web/components/templates/homeFeed/HighlightItem.tsx index 43fdc60ed..acdc83db9 100644 --- a/packages/web/components/templates/homeFeed/HighlightItem.tsx +++ b/packages/web/components/templates/homeFeed/HighlightItem.tsx @@ -1,210 +1,61 @@ -import { styled } from '@stitches/react' +import { Item } from '@radix-ui/react-dropdown-menu' +import Link from 'next/link' import { useRouter } from 'next/router' import { DotsThreeVertical } from 'phosphor-react' -import { Fragment, useCallback, useMemo, useState } from 'react' +import { useCallback } from 'react' import { Highlight } from '../../../lib/networking/fragments/highlightFragment' -import { Label } from '../../../lib/networking/fragments/labelFragment' -import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation' -import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabelsForHighlight' -import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery' +import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery' import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' -import { Dropdown, DropdownOption } from '../../elements/DropdownElements' -import { HighlightNoteTextEditArea } from '../../elements/HighlightNoteTextEditArea' -import { LabelChip } from '../../elements/LabelChip' import { - Blockquote, - Box, - HStack, - SpanBox, - VStack, -} from '../../elements/LayoutPrimitives' -import { StyledText } from '../../elements/StyledText' -import { ConfirmationModal } from '../../patterns/ConfirmationModal' -import { theme } from '../../tokens/stitches.config' -import { SetLabelsModal } from '../article/SetLabelsModal' + Dropdown, + DropdownOption, + DropdownSeparator, +} from '../../elements/DropdownElements' +import { Box, SpanBox } from '../../elements/LayoutPrimitives' -type HighlightItemProps = { - highlight: Highlight - viewer: UserBasicData | undefined - item: LibraryItemNode - - deleteHighlight: (item: LibraryItemNode, highlight: Highlight) => void -} - -const StyledQuote = styled(Blockquote, { - margin: '0px', - fontSize: '16px', - fontFamily: '$inter', - fontWeight: '500', - lineHeight: '1.50', - color: '$thHighContrast', - paddingLeft: '15px', - borderLeft: '2px solid $omnivoreCtaYellow', -}) - -export function HighlightItem(props: HighlightItemProps): JSX.Element { - const router = useRouter() - const [hover, setHover] = useState(false) - const [isEditing, setIsEditing] = useState(false) - - const lines = useMemo( - () => props.highlight.quote.split('\n'), - [props.highlight.quote] - ) - - const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] = - useState(undefined) - const [labelsTarget, setLabelsTarget] = useState( - undefined - ) - const [, updateState] = useState({}) - - return ( - <> - setHover(true)} - onMouseLeave={() => setHover(false)} - > - - { - if (router && props.viewer) { - const dest = `/${props.viewer.profile.username}/${props.item.slug}#${props.highlight.id}` - router.push(dest) - } - event.preventDefault() - }} - > - - {lines.map((line: string, index: number) => ( - - {line} - {index !== lines.length - 1 && ( - <> -
-
- - )} -
- ))} -
-
- - - {props.highlight.labels?.map((label: Label, index: number) => ( - - ))} - - - {!isEditing && ( - setIsEditing(true)} - > - {props.highlight.annotation - ? props.highlight.annotation - : 'Add your notes...'} - - )} - {isEditing && ( - {}} - /> - )} -
- - - -
- {showConfirmDeleteHighlightId && ( - { - setShowConfirmDeleteHighlightId(undefined) - const result = await deleteHighlightMutation( - showConfirmDeleteHighlightId - ) - if (result) { - showSuccessToast('Highlight deleted') - props.deleteHighlight(props.item, props.highlight) - } else { - showErrorToast('Error deleting highlight') - } - }} - onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)} - /> - )} - {labelsTarget && ( - { - const result = setLabelsForHighlight( - labelsTarget.id, - labels.map((label) => label.id) - ) - return result - }} - /> - )} - - ) -} +import { styled, theme } from '../../tokens/stitches.config' type HighlightsMenuProps = { + viewer: UserBasicData + + item: ReadableItem highlight: Highlight + viewInReader: (highlightId: string) => void + setLabelsTarget: (target: Highlight) => void setShowConfirmDeleteHighlightId: (set: string) => void } +const StyledLinkItem = styled('a', { + display: 'flex', + fontSize: '14px', + fontWeight: '400', + py: '10px', + px: '15px', + borderRadius: 3, + cursor: 'pointer', + color: '$utilityTextDefault', + textDecoration: 'none', + + '&:hover': { + outline: 'none', + backgroundColor: '$grayBgHover', + }, +}) + export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element { const copyHighlight = useCallback(() => { - ;(async () => { - await navigator.clipboard.writeText(props.highlight.quote) - showSuccessToast('Highlight copied') - })() + const quote = props.highlight.quote + if (quote) { + ;(async () => { + await navigator.clipboard.writeText(quote) + showSuccessToast('Highlight copied') + })() + } else { + showErrorToast('No highlight text.') + } }, [props.highlight]) return ( @@ -249,6 +100,28 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element { }} title="Delete" /> + + + { + console.log('event.ctrlKey: ', event.ctrlKey, event.metaKey) + if (event.ctrlKey || event.metaKey) { + window.open( + `/${props.viewer.profile.username}/${props.item.slug}#${props.highlight.id}`, + '_blank' + ) + return + } + props.viewInReader(props.highlight.id) + event.preventDefault() + event.stopPropagation() + }} + > + View In Reader + + ) } @@ -263,9 +136,17 @@ export function highlightAsMarkdown(highlight: Highlight) { } export function highlightsAsMarkdown(highlights: Highlight[]) { - return highlights + const noteMD = highlights.find((h) => h.type == 'NOTE') + + const highlightMD = highlights + .filter((h) => h.type == 'HIGHLIGHT') .map((highlight) => { return highlightAsMarkdown(highlight) }) .join('\n\n') + + if (noteMD) { + return `${noteMD.annotation}\n\n${highlightMD}` + } + return highlightMD } diff --git a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx index 27f04fd28..f0519a710 100644 --- a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx +++ b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx @@ -1,3 +1,4 @@ +import { useRouter } from 'next/router' import { HighlighterCircle } from 'phosphor-react' import { useCallback, useEffect, useReducer, useState } from 'react' import { Toaster } from 'react-hot-toast' @@ -18,9 +19,10 @@ import { timeAgo, } from '../../patterns/LibraryCards/LibraryCardStyles' import { LibraryHighlightGridCard } from '../../patterns/LibraryCards/LibraryHighlightGridCard' +import { Notebook } from '../article/Notebook' import { EmptyHighlights } from './EmptyHighlights' import { HEADER_HEIGHT, MOBILE_HEADER_HEIGHT } from './HeaderSpacer' -import { HighlightItem, highlightsAsMarkdown } from './HighlightItem' +import { highlightsAsMarkdown } from './HighlightItem' type HighlightItemsLayoutProps = { items: LibraryItem[] @@ -32,9 +34,8 @@ type HighlightItemsLayoutProps = { export function HighlightItemsLayout( props: HighlightItemsLayoutProps ): JSX.Element { - const [currentItem, setCurrentItem] = useState( - undefined - ) + const [currentItem, setCurrentItem] = + useState(undefined) const listReducer = ( state: LibraryItem[], @@ -124,6 +125,9 @@ export function HighlightItemsLayout( '@xlgDown': { height: `calc(100vh - ${MOBILE_HEADER_HEIGHT})`, }, + '@lgDown': { + overflowY: 'scroll', + }, bg: '$thBackground2', overflow: 'hidden', }} @@ -165,7 +169,7 @@ export function HighlightItemsLayout( borderBottom: '1px solid $thBorderColor', }} alignment="center" - distribution="start" + distribution="center" >
- - - + )} @@ -256,6 +251,7 @@ function LibraryItemsList(props: LibraryItemsListProps): JSX.Element { )} ))} + ) } @@ -366,6 +362,8 @@ type HighlightListProps = { } function HighlightList(props: HighlightListProps): JSX.Element { + const router = useRouter() + const exportHighlights = useCallback(() => { ;(async () => { if (!props.item.node.highlights) { @@ -378,68 +376,231 @@ function HighlightList(props: HighlightListProps): JSX.Element { })() }, [props.item.node.highlights]) + const viewInReader = useCallback( + (highlightId) => { + if (!router || !router.isReady || !props.viewer) { + showErrorToast('Error navigating to highlight') + return + } + console.log( + 'pushing user: ', + props.viewer, + 'slug: ', + props.item.node.slug + ) + router.push( + { + pathname: '/[username]/[slug]', + query: { + username: props.viewer.profile.username, + slug: props.item.node.slug, + }, + hash: highlightId, + }, + `${props.viewer.profile.username}/${props.item.node.slug}#${highlightId}`, + { + scroll: false, + } + ) + }, + [router, props] + ) + return ( - - - - + }> + { + exportHighlights() }} - > - HIGHLIGHTS - - }> - { - exportHighlights() - }} - title="Export" - /> - - - - {(props.item.node.highlights ?? []).map((highlight) => ( - - ))} - - - - + title="Export" + /> + + + + {props.viewer && ( + + )} + + ) + + // return ( + // + // + // + // + // NOTEBOOK + // + // }> + // { + // exportHighlights() + // }} + // title="Export" + // /> + // + // + + // + // + // NOTE + // + // + // { + // console.log('saving text', highlight) + // }} + // /> + // + + // {sortedHighlights && ( + // <> + // + // + // HIGHLIGHTS + // + // + // + // {sortedHighlights.map((highlight) => ( + // <> + // { + // console.log('updated highlight: ', highlight) + // }} + + // deleteHighlightAction={(highlight) => { + // console.log('deleting: ', highlight) + // }} + + // setSetLabelsTarget: (highlight: Highlight) => void + // setShowConfirmDeleteHighlightId: (id: string | undefined) => void + + // /> + // + // + // ))} + // + // + // + // + // )} + // + // + // ) } type HighlightCountChipProps = { diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 70d39ba04..b1b8a2f4f 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -78,9 +78,8 @@ export function HomeFeedContainer(): JSX.Element { const gridContainerRef = useRef(null) - const [labelsTarget, setLabelsTarget] = useState( - undefined - ) + const [labelsTarget, setLabelsTarget] = + useState(undefined) const [showAddLinkModal, setShowAddLinkModal] = useState(false) const [showEditTitleModal, setShowEditTitleModal] = useState(false) @@ -701,6 +700,10 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { {...props} /> )} + + {props.showAddLinkModal && ( + props.setShowAddLinkModal(false)} /> + )} ) @@ -916,10 +919,6 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element { )} - - {props.showAddLinkModal && ( - props.setShowAddLinkModal(false)} /> - )} {props.showEditTitleModal && ( diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index 95c696e04..74c378191 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -168,6 +168,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = thBackground2: '#F3F3F3', thBackground3: '#FFFFFF', thBackground4: '#EBEBEB', + thBackground5: '#F5F5F5', thBackgroundActive: '#F9F9F9', thBackgroundContrast: '#FFFFFF', @@ -254,6 +255,7 @@ const darkThemeSpec = { thBackground2: '#3D3D3D', thBackground3: '#242424', thBackground4: '#3D3D3D', + thBackground5: '#3D3D3D', thBackgroundActive: '#2E2E2E', thBackgroundContrast: '#000000', diff --git a/packages/web/lib/appConfig.ts b/packages/web/lib/appConfig.ts index d28608bec..cf5c573a5 100644 --- a/packages/web/lib/appConfig.ts +++ b/packages/web/lib/appConfig.ts @@ -4,7 +4,6 @@ type AppEnvironment = 'prod' | 'dev' | 'demo' | 'local' type BaseURLs = { webBaseURL: string serverBaseURL: string - highlightsBaseURL: string } type BaseURLRecords = Record @@ -14,22 +13,18 @@ const baseURLRecords: BaseURLRecords = { prod: { webBaseURL: process.env.NEXT_PUBLIC_BASE_URL ?? '', serverBaseURL: process.env.NEXT_PUBLIC_SERVER_BASE_URL ?? '', - highlightsBaseURL: process.env.NEXT_PUBLIC_HIGHLIGHTS_BASE_URL ?? '', }, dev: { webBaseURL: process.env.NEXT_PUBLIC_DEV_BASE_URL ?? '', serverBaseURL: process.env.NEXT_PUBLIC_DEV_SERVER_BASE_URL ?? '', - highlightsBaseURL: process.env.NEXT_PUBLIC_DEV_HIGHLIGHTS_BASE_URL ?? '', }, demo: { webBaseURL: process.env.NEXT_PUBLIC_DEMO_BASE_URL ?? '', serverBaseURL: process.env.NEXT_PUBLIC_DEMO_SERVER_BASE_URL ?? '', - highlightsBaseURL: process.env.NEXT_PUBLIC_DEMO_HIGHLIGHTS_BASE_URL ?? '', }, local: { webBaseURL: process.env.NEXT_PUBLIC_LOCAL_BASE_URL ?? '', serverBaseURL: process.env.NEXT_PUBLIC_LOCAL_SERVER_BASE_URL ?? '', - highlightsBaseURL: process.env.NEXT_PUBLIC_LOCAL_HIGHLIGHTS_BASE_URL ?? '', }, } @@ -43,16 +38,6 @@ function serverBaseURL(env: AppEnvironment): string { return value } -function highlightsURL(env: AppEnvironment): string { - const value = baseURLRecords[appEnv].highlightsBaseURL - if (value.length == 0) { - throw new Error( - `Couldn't find environment variable for highlights base url in ${env} environment` - ) - } - return value -} - function webURL(env: AppEnvironment): string { const value = baseURLRecords[appEnv].webBaseURL if (value.length == 0) { @@ -96,6 +81,4 @@ export const gqlEndpoint = `${serverBaseURL(appEnv)}/api/graphql` export const fetchEndpoint = `${serverBaseURL(appEnv)}/api` -export const highlightsBaseURL = highlightsURL(appEnv) - export const webBaseURL = webURL(appEnv) diff --git a/packages/web/lib/dateFormatting.ts b/packages/web/lib/dateFormatting.ts index 0ac9bff52..64e91d917 100644 --- a/packages/web/lib/dateFormatting.ts +++ b/packages/web/lib/dateFormatting.ts @@ -17,3 +17,10 @@ export function formattedShortDate(rawDate: string): string { timeZone, }).format(new Date(rawDate)) } + +export function formattedShortTime(rawDate: string): string { + return new Intl.DateTimeFormat(locale, { + timeStyle: 'short', + timeZone, + }).format(new Date(rawDate)) +} diff --git a/packages/web/lib/highlights/createHighlight.ts b/packages/web/lib/highlights/createHighlight.ts index 045f28819..2a7b8715c 100644 --- a/packages/web/lib/highlights/createHighlight.ts +++ b/packages/web/lib/highlights/createHighlight.ts @@ -11,6 +11,7 @@ import { extendRangeToWordBoundaries } from './normalizeHighlightRange' import type { Highlight } from '../networking/fragments/highlightFragment' import { removeHighlights } from './deleteHighlight' import { ArticleMutations } from '../articleActions' +import { NodeHtmlMarkdown } from 'node-html-markdown' type CreateHighlightInput = { selection: SelectionAttributes @@ -28,6 +29,20 @@ type CreateHighlightOutput = { newHighlightIndex?: number } +/* ********************************************************* * + * Re-use + * If using it several times, creating an instance saves time + * ********************************************************* */ +const nhm = new NodeHtmlMarkdown( + /* options (optional) */ {}, + /* customTransformers (optional) */ undefined, + /* customCodeBlockTranslators (optional) */ undefined +) + +export const htmlToMarkdown = (html: string) => { + return nhm.translate(/* html */ html) +} + export async function createHighlight( input: CreateHighlightInput, articleMutations: ArticleMutations @@ -42,6 +57,10 @@ export async function createHighlight( extendRangeToWordBoundaries(range) + // Create a temp container for copying the range HTML + const container = document.createElement('div') + container.appendChild(range.cloneContents()) + const id = uuidv4() const patch = generateDiffPatch(range) @@ -79,12 +98,15 @@ export async function createHighlight( ) const newHighlightAttributes = { - prefix: highlightAttributes.prefix, - suffix: highlightAttributes.suffix, - quote: highlightAttributes.quote, id, shortId: nanoid(8), patch, + + prefix: highlightAttributes.prefix, + suffix: highlightAttributes.suffix, + quote: htmlToMarkdown(container.innerHTML), + html: container.innerHTML, + annotation: annotations.length > 0 ? annotations.join('\n') : undefined, articleId: input.articleId, highlightPositionPercent: input.highlightPositionPercent, diff --git a/packages/web/lib/highlights/deleteHighlight.ts b/packages/web/lib/highlights/deleteHighlight.ts index 2f07e36b1..b09bdbcaa 100644 --- a/packages/web/lib/highlights/deleteHighlight.ts +++ b/packages/web/lib/highlights/deleteHighlight.ts @@ -1,7 +1,13 @@ import { HighlightLocation } from './highlightGenerator' -import { getHighlightElements, getHighlightNoteButton } from './highlightHelpers' +import { + getHighlightElements, + getHighlightNoteButton, +} from './highlightHelpers' -export function removeHighlights(ids: string[], locations: HighlightLocation[]): void { +export function removeHighlights( + ids: string[], + locations: HighlightLocation[] +): void { ids.forEach((id) => { const elements = getHighlightElements(id) const noteButtons = getHighlightNoteButton(id) diff --git a/packages/web/lib/networking/fragments/highlightFragment.ts b/packages/web/lib/networking/fragments/highlightFragment.ts index 94fdfd341..a6cf2e4c6 100644 --- a/packages/web/lib/networking/fragments/highlightFragment.ts +++ b/packages/web/lib/networking/fragments/highlightFragment.ts @@ -4,6 +4,7 @@ import { Label } from './labelFragment' export const highlightFragment = gql` fragment HighlightFields on Highlight { id + type shortId quote prefix @@ -24,11 +25,13 @@ export const highlightFragment = gql` } } ` +export type HighlightType = 'HIGHLIGHT' | 'REDACTION' | 'NOTE' export type Highlight = { id: string + type: HighlightType shortId: string - quote: string + quote?: string prefix?: string suffix?: string patch: string diff --git a/packages/web/lib/networking/mutations/createHighlightMutation.ts b/packages/web/lib/networking/mutations/createHighlightMutation.ts index acfc18206..2193b46a1 100644 --- a/packages/web/lib/networking/mutations/createHighlightMutation.ts +++ b/packages/web/lib/networking/mutations/createHighlightMutation.ts @@ -1,17 +1,28 @@ import { gql } from 'graphql-request' import { gqlFetcher } from '../networkHelpers' -import { Highlight, highlightFragment } from './../fragments/highlightFragment' +import { + Highlight, + highlightFragment, + HighlightType, +} from './../fragments/highlightFragment' export type CreateHighlightInput = { - prefix: string - suffix: string - quote: string id: string shortId: string - patch: string articleId: string + + prefix?: string + suffix?: string + quote?: string + html?: string + annotation?: string + + patch?: string + highlightPositionPercent?: number highlightPositionAnchorIndex?: number + + type?: HighlightType } type CreateHighlightOutput = { diff --git a/packages/web/lib/networking/mutations/mergeHighlightMutation.ts b/packages/web/lib/networking/mutations/mergeHighlightMutation.ts index fe63a6fc1..11b05d434 100644 --- a/packages/web/lib/networking/mutations/mergeHighlightMutation.ts +++ b/packages/web/lib/networking/mutations/mergeHighlightMutation.ts @@ -10,6 +10,7 @@ export type MergeHighlightInput = { quote: string prefix?: string suffix?: string + html?: string annotation?: string overlapHighlightIdList: string[] highlightPositionPercent?: number diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index c0f459b38..9b175684d 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -11,6 +11,12 @@ import { Label } from './../fragments/labelFragment' import { showErrorToast, showSuccessToast } from '../../toastHelpers' import { Highlight, highlightFragment } from '../fragments/highlightFragment' +export interface ReadableItem { + id: string + title: string + slug: string +} + export type LibraryItemsQueryInput = { limit: number sortDescending: boolean diff --git a/packages/web/package.json b/packages/web/package.json index d56609176..119b1298e 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -41,8 +41,10 @@ "downshift": "^6.1.9", "graphql-request": "^3.6.1", "kbar": "^0.1.0-beta.35", + "markdown-it": "^13.0.1", "nanoid": "^3.1.29", "next": "^12.1.0", + "node-html-markdown": "^1.3.0", "phosphor-react": "^1.4.0", "pspdfkit": "^2022.2.3", "react": "^17.0.2", @@ -50,6 +52,8 @@ "react-dom": "^17.0.2", "react-dropzone": "^14.2.3", "react-hot-toast": "^2.1.1", + "react-markdown": "^8.0.6", + "react-markdown-editor-lite": "^1.3.4", "react-masonry-css": "^1.0.16", "react-pro-sidebar": "^0.7.1", "react-spinners": "^0.13.7", @@ -77,6 +81,7 @@ "@types/diff-match-patch": "^1.0.32", "@types/jest": "^27.0.2", "@types/lodash.debounce": "^4.0.6", + "@types/markdown-it": "^12.2.3", "@types/react": "17.0.2", "@types/react-dom": "^17.0.2", "@types/segment-analytics": "^0.0.34", diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index 73ccd48bd..e0388352b 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -71,8 +71,6 @@ export default function Home(): JSX.Element { const actionHandler = useCallback( async (action: string, arg?: unknown) => { - console.log('handling action: ', action, article) - switch (action) { case 'unarchive': if (article) { @@ -348,7 +346,7 @@ export default function Home(): JSX.Element { article={article} showHighlightsModal={showHighlightsModal} setShowHighlightsModal={setShowHighlightsModal} - viewerUsername={viewerData.me?.profile?.username} + viewer={viewerData.me} /> ) : ( {article && viewerData?.me ? ( (undefined) + const [contentProps, setContentProps] = + useState(undefined) useEffect(() => { if (!router.isReady) return @@ -62,7 +62,7 @@ export default function AppArticleEmbed(): JSX.Element { function AppArticleEmbedContent( props: AppArticleEmbedContentProps ): JSX.Element { - const scrollRef = useRef(null) + const { viewerData } = useGetViewerQuery() const [showHighlightsModal, setShowHighlightsModal] = useState(false) const { articleData } = useGetArticleQuery({ @@ -71,7 +71,7 @@ function AppArticleEmbedContent( includeFriendsHighlights: false, }) - if (articleData) { + if (articleData && viewerData?.me) { return (