diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index 89a7aa59d..e9085ad81 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -242,15 +242,31 @@ export const Button = styled('button', { border: 'none', cursor: 'pointer', '&:hover': { - opacity: 0.8, + opacity: 0.7, }, }, articleActionIcon: { bg: 'transparent', border: 'none', cursor: 'pointer', + padding: '4px', + borderRadius: '5px', + '&:hover': { - opacity: 0.8, + opacity: 0.7, + }, + }, + hoverActionIcon: { + bg: 'transparent', + border: 'none', + cursor: 'pointer', + padding: '4px', + height: '100%', + pt: '6px', + minWidth: '25px', + + '&:hover': { + bg: '$grayBgHover', }, }, ghost: { diff --git a/packages/web/components/elements/DropdownElements.tsx b/packages/web/components/elements/DropdownElements.tsx index d1fccf24e..b601b7f6b 100644 --- a/packages/web/components/elements/DropdownElements.tsx +++ b/packages/web/components/elements/DropdownElements.tsx @@ -172,7 +172,10 @@ export function Dropdown( } = props return ( - + {triggerElement} void } export function LabelChip(props: LabelChipProps): JSX.Element { - const router = useRouter() const isDark = isDarkTheme() - const luminance = getLuminance(props.color) - const textColor = luminance > 0.5 ? '#000000' : '#ffffff' const selectedBorder = isDark ? '#FFEA9F' : 'black' const unSelectedBorder = isDark ? '#6A6968' : '#D9D9D9' - if (props.useAppAppearance) { - return ( - - - - {props.text} - {props.xAction && ( - - )} - - - ) - } - return ( - + + + {props.text} + + ) + // } + + // return ( + // + // ) } diff --git a/packages/web/components/patterns/ArticleNotes.tsx b/packages/web/components/patterns/ArticleNotes.tsx new file mode 100644 index 000000000..438e834ff --- /dev/null +++ b/packages/web/components/patterns/ArticleNotes.tsx @@ -0,0 +1,217 @@ +/* eslint-disable react/no-children-prop */ +import { + ChangeEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import { VStack } from '../elements/LayoutPrimitives' + +import MarkdownIt from 'markdown-it' +import MdEditor, { Plugins } from 'react-markdown-editor-lite' +import 'react-markdown-editor-lite/lib/index.css' +import throttle from 'lodash/throttle' +import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation' +import { Highlight } from '../../lib/networking/fragments/highlightFragment' +import Counter from './MDEditorSavePlugin' +import { isDarkTheme } from '../../lib/themeUpdater' +import { RcEditorStyles } from './RcEditorStyles' + +const mdParser = new MarkdownIt() + +MdEditor.use(Plugins.TabInsert, { + tabMapValue: 1, // note that 1 means a '\t' instead of ' '. +}) + +console.log() +MdEditor.use(Counter) + +type NoteSectionProps = { + targetId: string + + placeHolder: string + + text: string + setText: (text: string) => void + + saveText: (text: string) => void +} + +export function ArticleNotes(props: NoteSectionProps): JSX.Element { + const saveText = useCallback( + (text) => { + props.saveText(text) + }, + [props] + ) + + return ( + + ) +} + +type HighlightViewNoteProps = { + targetId: string + + placeHolder: string + mode: 'edit' | 'preview' + + highlight: Highlight + + setEditMode: (set: 'edit' | 'preview') => void + + text: string + setText: (text: string) => void + + updateHighlight: (highlight: Highlight) => void +} + +export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element { + const [lastSaved, setLastSaved] = useState(undefined) + + const saveText = useCallback( + (text) => { + ;(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 = { + targetId: string + + placeHolder: string + + text: string | undefined + setText: (text: string) => void + fillBackground: boolean | undefined + + saveText: (text: string) => void +} + +export function MarkdownNote(props: MarkdownNote): JSX.Element { + const editorRef = useRef(null) + const isDark = isDarkTheme() + + const saveRef = useRef(props.saveText) + + useEffect(() => { + saveRef.current = props.saveText + }, [props]) + + const debouncedSave = useMemo<(text: string) => void>(() => { + const func = (text: string) => { + saveRef.current?.(text) + } + return throttle(func, 3000) + }, []) + + const handleEditorChange = useCallback( + ( + data: { text: string; html: string }, + event?: ChangeEvent | undefined + ) => { + props.setText(data.text) + if (event) { + event.preventDefault() + } + + debouncedSave(data.text) + }, + [] + ) + + useEffect(() => { + const saveMarkdownNote = () => { + const md = editorRef.current?.getMdValue() + if (md) { + props.saveText(md) + } + } + document.addEventListener('saveMarkdownNote', saveMarkdownNote) + return () => { + document.removeEventListener('saveMarkdownNote', saveMarkdownNote) + } + }, [props, editorRef]) + + return ( + ) => { + if (event.code.toLowerCase() === 'escape') { + event.preventDefault() + event.stopPropagation() + } + }} + > + mdParser.render(text)} + onChange={handleEditorChange} + /> + + ) +} diff --git a/packages/web/components/patterns/HighlightHoverActions.tsx b/packages/web/components/patterns/HighlightHoverActions.tsx new file mode 100644 index 000000000..4d3c9ae5e --- /dev/null +++ b/packages/web/components/patterns/HighlightHoverActions.tsx @@ -0,0 +1,113 @@ +import { useState } from 'react' +import { Box, SpanBox } from '../elements/LayoutPrimitives' +import { LibraryItemNode } from '../../lib/networking/queries/useGetLibraryItemsQuery' +import { Button } from '../elements/Button' +import { theme } from '../tokens/stitches.config' +import { + ArchiveBox, + Book, + BookOpen, + Copy, + DotsThree, + Notebook, + Tag, + Trash, + Tray, +} from 'phosphor-react' +//import { CardMenu } from '../CardMenu' +import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery' +import { Highlight } from '../../lib/networking/fragments/highlightFragment' +import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' + +type HighlightHoverActionsProps = { + viewer: UserBasicData + highlight: Highlight + + isHovered: boolean + + viewInReader: (highlightId: string) => void + + setLabelsTarget: (target: Highlight) => void + setShowConfirmDeleteHighlightId: (set: string) => void +} + +export const HighlightHoverActions = (props: HighlightHoverActionsProps) => { + const [menuOpen, setMenuOpen] = useState(false) + + return ( + + + + + + + ) +} diff --git a/packages/web/components/patterns/HighlightNotes.tsx b/packages/web/components/patterns/HighlightNotes.tsx index 97a395cd9..253caa3d3 100644 --- a/packages/web/components/patterns/HighlightNotes.tsx +++ b/packages/web/components/patterns/HighlightNotes.tsx @@ -8,7 +8,7 @@ import { useState, } from 'react' import { formattedShortTime } from '../../lib/dateFormatting' -import { HStack, SpanBox, VStack } from '../elements/LayoutPrimitives' +import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives' import MarkdownIt from 'markdown-it' import MdEditor, { Plugins } from 'react-markdown-editor-lite' @@ -18,14 +18,10 @@ 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' import remarkGfm from 'remark-gfm' +import { RcEditorStyles } from './RcEditorStyles' +import { isDarkTheme } from '../../lib/themeUpdater' +import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' const mdParser = new MarkdownIt() @@ -33,52 +29,14 @@ MdEditor.use(Plugins.TabInsert, { tabMapValue: 1, // note that 1 means a '\t' instead of ' '. }) -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 = { + targetId: string + placeHolder: string mode: 'edit' | 'preview' highlight: Highlight - sizeMode: 'normal' | 'maximized' setEditMode: (set: 'edit' | 'preview') => void text: string | undefined @@ -87,9 +45,10 @@ type HighlightViewNoteProps = { export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element { const [lastSaved, setLastSaved] = useState(undefined) + const [errorSaving, setErrorSaving] = useState(undefined) const saveText = useCallback( - (text, updateTime) => { + (text, updateTime, interactive) => { ;(async () => { const success = await updateHighlightMutation({ annotation: text, @@ -99,6 +58,13 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element { setLastSaved(updateTime) props.highlight.annotation = text props.updateHighlight(props.highlight) + if (interactive) { + showSuccessToast('Note saved', { + position: 'bottom-right', + }) + } + } else { + setErrorSaving('Error saving note.') } })() }, @@ -107,48 +73,52 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element { return ( ) } type MarkdownNote = { + targetId: string + 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 + errorSaving: string | undefined + + saveText: (text: string, updateTime: Date, interactive: boolean) => void } export function MarkdownNote(props: MarkdownNote): JSX.Element { const editorRef = useRef(null) + const isDark = isDarkTheme() const [lastChanged, setLastChanged] = useState(undefined) - const [errorSaving, setErrorSaving] = useState(undefined) const saveRef = useRef(props.saveText) useEffect(() => { saveRef.current = props.saveText - }, [props.lastSaved, lastChanged]) + }, [props]) const debouncedSave = useMemo< (text: string, updateTime: Date) => void >(() => { const func = (text: string, updateTime: Date) => { - saveRef.current?.(text, updateTime) + saveRef.current?.(text, updateTime, false) } return throttle(func, 3000) }, []) @@ -164,9 +134,10 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element { const updateTime = new Date() setLastChanged(updateTime) + debouncedSave(data.text, updateTime) }, - [props.lastSaved, lastChanged] + [] ) return ( @@ -174,29 +145,15 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element { {props.mode == 'edit' ? ( .section': { - borderRight: 'unset', - }, - '.rc-md-editor .editor-container .sec-md .input': { - padding: '10px', - borderRadius: '5px', - fontSize: '16px', - }, + ...RcEditorStyles(isDark, false), }} onKeyDown={(event: React.KeyboardEvent) => { if (event.code.toLowerCase() === 'escape') { props.setEditMode('preview') event.preventDefault() + event.stopPropagation() } }} > @@ -230,7 +187,7 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element { ]} style={{ width: '100%', - height: props.sizeMode == 'normal' ? '160px' : '320px', + height: '160px', }} renderHTML={(text: string) => mdParser.render(text)} onChange={handleEditorChange} @@ -246,7 +203,7 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element { alignment="start" distribution="start" > - {errorSaving && ( + {props.errorSaving && ( - {errorSaving} + {props.errorSaving} )} {props.lastSaved !== undefined ? ( @@ -267,65 +224,60 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element { )}`} ) : null} - {lastChanged !== props.lastSaved && ( - + - - )} + Cancel + + + ) : ( <> *': { m: '0px', }, @@ -342,111 +294,3 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element { ) } - -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 b9d55ea50..d12f3023e 100644 --- a/packages/web/components/patterns/HighlightView.tsx +++ b/packages/web/components/patterns/HighlightView.tsx @@ -1,6 +1,5 @@ /* eslint-disable react/no-children-prop */ -import { BookOpen, PencilLine } from 'phosphor-react' -import { useState } from 'react' +import { useMemo, useState } from 'react' import type { Highlight } from '../../lib/networking/fragments/highlightFragment' import { LabelChip } from '../elements/LabelChip' import { @@ -10,61 +9,105 @@ import { SpanBox, HStack, } from '../elements/LayoutPrimitives' -import { styled } from '../tokens/stitches.config' +import { styled, theme } from '../tokens/stitches.config' import { HighlightViewNote } from './HighlightNotes' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' +import { isDarkTheme } from '../../lib/themeUpdater' +import { HighlightsMenu } from '../templates/homeFeed/HighlightItem' +import { ReadableItem } from '../../lib/networking/queries/useGetLibraryItemsQuery' +import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery' +import { + autoUpdate, + offset, + size, + useFloating, + useHover, + useInteractions, +} from '@floating-ui/react' +import { LibraryHoverActions } from './LibraryCards/LibraryHoverActions' +import { HighlightHoverActions } from './HighlightHoverActions' type HighlightViewProps = { + item: ReadableItem + viewer: UserBasicData highlight: Highlight author?: string title?: string updateHighlight: (highlight: Highlight) => void + + viewInReader: (highlightId: string) => void + + setLabelsTarget: (target: Highlight) => void + setShowConfirmDeleteHighlightId: (set: string) => void } const StyledQuote = styled(Blockquote, { + p: '0px', margin: '0px 0px 0px 0px', fontSize: '18px', lineHeight: '27px', + borderRadius: '4px', + width: '100%', }) export function HighlightView(props: HighlightViewProps): JSX.Element { + const isDark = isDarkTheme() const [noteMode, setNoteMode] = useState<'preview' | 'edit'>('preview') + const [isHovered, setIsHovered] = useState(false) + const [isOpen, setIsOpen] = useState(false) + + 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]) + + const highlightAlpha = isDark ? 1.0 : 0.35 return ( - - - + - - + @@ -72,10 +115,16 @@ export function HighlightView(props: HighlightViewProps): JSX.Element { css={{ '> *': { m: '0px', + display: 'inline', + padding: '2px', + backgroundColor: `rgba(var(--colors-highlightBackground), ${highlightAlpha})`, + boxShadow: `1px 0 0 rgba(var(--colors-highlightBackground), ${highlightAlpha}), -1px 0 0 rgba(var(--colors-highlightBackground), ${highlightAlpha})`, + boxDecorationBreak: 'clone', + borderRadius: '2px', }, fontSize: '15px', lineHeight: 1.5, - color: '$grayText', + color: '$thTextSubtle2', img: { display: 'block', margin: '0.5em auto !important', @@ -96,43 +145,27 @@ export function HighlightView(props: HighlightViewProps): JSX.Element { ))} - { - setNoteMode(noteMode == 'preview' ? 'edit' : 'preview') - event.preventDefault() - }} - > - {noteMode === 'edit' ? ( - - ) : ( - - )} - - + ) } diff --git a/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx b/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx index 1775c6143..09ae38c45 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx @@ -6,14 +6,6 @@ import { Box, SpanBox } from '../../elements/LayoutPrimitives' dayjs.extend(relativeTime) -export const MetaStyle = { - width: '100%', - color: '$thTextSubtle3', - fontSize: '13px', - fontWeight: '400', - fontFamily: '$display', -} - export const MenuStyle = { display: 'flex', marginLeft: 'auto', @@ -30,6 +22,14 @@ export const MenuStyle = { }, } +export const MetaStyle = { + width: '100%', + color: '$thTextSubtle3', + fontSize: '13px', + fontWeight: '400', + fontFamily: '$display', +} + export const TitleStyle = { color: '$thTextContrast2', fontSize: '16px', @@ -119,7 +119,9 @@ export function LibraryItemMetadata( props: LibraryItemMetadataProps ): JSX.Element { const highlightCount = useMemo(() => { - return props.item.highlights?.length ?? 0 + return ( + props.item.highlights?.filter((h) => h.type == 'HIGHLIGHT').length ?? 0 + ) }, [props.item.highlights]) return ( diff --git a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx index b2fd95ac7..a269a006b 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx @@ -5,20 +5,30 @@ import { CoverImage } from '../../elements/CoverImage' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { useCallback, useState } from 'react' -import { DotsThreeVertical } from 'phosphor-react' import Link from 'next/link' -import { CardMenu } from '../CardMenu' import { AuthorInfoStyle, CardCheckbox, DescriptionStyle, LibraryItemMetadata, - MenuStyle, MetaStyle, siteName, TitleStyle, + MenuStyle, } from './LibraryCardStyles' import { sortedLabels } from '../../../lib/labelsSort' +import { LibraryHoverActions } from './LibraryHoverActions' +import { + useHover, + useFloating, + useInteractions, + size, + offset, + autoUpdate, +} from '@floating-ui/react' +import { CardMenu } from '../CardMenu' +import { DotsThree } from 'phosphor-react' +import { isTouchScreenDevice } from '../../../lib/deviceType' dayjs.extend(relativeTime) @@ -54,9 +64,29 @@ export function ProgressBar(props: ProgressBarProps): JSX.Element { export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element { const [isHovered, setIsHovered] = useState(false) + const [isOpen, setIsOpen] = useState(false) + + 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 ( ) : ( - - + {!isTouchScreenDevice() && ( + + + + )} + - - - + + + + + )} ) @@ -133,7 +179,7 @@ const LibraryGridCardContent = (props: LinkedItemCardProps): JSX.Element => { { onOpenChange={(open) => setMenuOpen(open)} actionHandler={props.handleAction} triggerElement={ - + } /> )} + { + if (!router || !router.isReady || !props.viewer) { + showErrorToast('Error navigating to highlight') + return + } + console.log('pushing user: ', props.viewer, 'slug: ', props.item.slug) + router.push( + { + pathname: '/[username]/[slug]', + query: { + username: props.viewer.profile.username, + slug: props.item.slug, + }, + hash: highlightId, + }, + `${props.viewer.profile.username}/${props.item.slug}#${highlightId}`, + { + scroll: false, + } + ) + }, + [router, props] + ) const sortedHighlights = useMemo(() => { const sorted = (a: number, b: number) => { @@ -123,14 +149,23 @@ export function LibraryHighlightGridCard( <> {sortedHighlights.map((highlight) => ( - + { + console.log('TODO: set labels') + }} + setShowConfirmDeleteHighlightId={() => { + console.log('TODO: confirm delete') + }} updateHighlight={(highlight) => { console.log('updated highlight: ', highlight) }} diff --git a/packages/web/components/patterns/LibraryCards/LibraryHoverActions.tsx b/packages/web/components/patterns/LibraryCards/LibraryHoverActions.tsx new file mode 100644 index 000000000..d17c39bca --- /dev/null +++ b/packages/web/components/patterns/LibraryCards/LibraryHoverActions.tsx @@ -0,0 +1,125 @@ +import { useState } from 'react' +import { Box, SpanBox } from '../../elements/LayoutPrimitives' +import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery' +import { LinkedItemCardAction } from './CardTypes' +import { Button } from '../../elements/Button' +import { theme } from '../../tokens/stitches.config' +import { + ArchiveBox, + DotsThree, + Notebook, + Tag, + Trash, + Tray, +} from 'phosphor-react' +import { CardMenu } from '../CardMenu' +import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' + +type LibraryHoverActionsProps = { + viewer: UserBasicData + + isHovered: boolean + + item: LibraryItemNode + handleAction: (action: LinkedItemCardAction) => void +} + +export const LibraryHoverActions = (props: LibraryHoverActionsProps) => { + const [menuOpen, setMenuOpen] = useState(false) + + return ( + + + + + + setMenuOpen(open)} + actionHandler={props.handleAction} + triggerElement={ + + + + } + /> + + ) +} diff --git a/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx index 205e65f93..6c8277c5a 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx @@ -2,34 +2,67 @@ import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives' import { LabelChip } from '../../elements/LabelChip' import type { LinkedItemCardProps } from './CardTypes' import { useCallback, useState } from 'react' -import { DotsThree } from 'phosphor-react' import Link from 'next/link' -import { CardMenu } from '../CardMenu' import { AuthorInfoStyle, CardCheckbox, LibraryItemMetadata, - MenuStyle, MetaStyle, siteName, TitleStyle, + MenuStyle, } from './LibraryCardStyles' import { sortedLabels } from '../../../lib/labelsSort' import { LIBRARY_LEFT_MENU_WIDTH } from '../../templates/homeFeed/LibraryFilterMenu' +import { LibraryHoverActions } from './LibraryHoverActions' +import { + useHover, + useFloating, + useInteractions, + size, + offset, + autoUpdate, +} from '@floating-ui/react' +import { CardMenu } from '../CardMenu' +import { DotsThree } from 'phosphor-react' +import { isTouchScreenDevice } from '../../../lib/deviceType' export function LibraryListCard(props: LinkedItemCardProps): JSX.Element { const [isHovered, setIsHovered] = useState(false) + const [isOpen, setIsOpen] = useState(false) + + 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 ( ) : ( - - + {!isTouchScreenDevice() && ( + + + + )} + - - - + + + + + )} ) @@ -76,8 +131,8 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element { export function LibraryListCardContent( props: LinkedItemCardProps ): JSX.Element { - const { isChecked, setIsChecked, item } = props const [menuOpen, setMenuOpen] = useState(false) + const { isChecked, setIsChecked, item } = props const originText = siteName(props.item.originalArticleUrl, props.item.url) const handleCheckChanged = useCallback(() => { @@ -99,7 +154,7 @@ export function LibraryListCardContent( - {props.item.title} + + {props.item.title} + {props.item.author} diff --git a/packages/web/components/patterns/MDEditorSavePlugin.tsx b/packages/web/components/patterns/MDEditorSavePlugin.tsx new file mode 100644 index 000000000..e25110376 --- /dev/null +++ b/packages/web/components/patterns/MDEditorSavePlugin.tsx @@ -0,0 +1,30 @@ +/* eslint-disable functional/no-class */ + +import { FloppyDisk } from 'phosphor-react' +import { PluginComponent } from 'react-markdown-editor-lite' +import { Button } from '../elements/Button' + +export default class MDEditorSavePlugin extends PluginComponent { + static pluginName = 'save' + + static align = 'right' + + constructor(props: any) { + super(props) + } + + render() { + return ( + + ) + } +} diff --git a/packages/web/components/patterns/RcEditorStyles.tsx b/packages/web/components/patterns/RcEditorStyles.tsx new file mode 100644 index 000000000..f0b65ad6f --- /dev/null +++ b/packages/web/components/patterns/RcEditorStyles.tsx @@ -0,0 +1,33 @@ +export const RcEditorStyles = (isDark: boolean, shadow: boolean) => { + return { + '.rc-md-editor .rc-md-navigation': { + background: '$grayBg', + borderBottom: '1px solid $thBorderSubtle', + }, + '.rc-md-editor': { + borderRadius: '5px', + backgroundColor: isDark ? '#2A2A2A' : 'white', + border: '1px solid $thBorderSubtle', + }, + '.rc-md-navigation': { + borderRadius: '5px', + borderBottomLeftRadius: '0px', + borderBottomRightRadius: '0px', + background: 'var(--colors-grayBg)', + }, + '.rc-md-editor .editor-container >.section': { + borderRight: 'unset', + }, + '.rc-md-editor .editor-container .sec-md .input': { + padding: '10px', + borderRadius: '5px', + fontSize: '16px', + color: isDark ? '#EBEBEB' : 'black', + backgroundColor: isDark ? '#2A2A2A' : 'white', + }, + '.rc-md-editor .drop-wrap': { + border: '1px solid $thBorderSubtle', + backgroundColor: isDark ? '#2A2A2A' : 'white', + }, + } +} diff --git a/packages/web/components/templates/KeyboardShortcutListModal.tsx b/packages/web/components/templates/KeyboardShortcutListModal.tsx index 1b0c1d685..3285446d8 100644 --- a/packages/web/components/templates/KeyboardShortcutListModal.tsx +++ b/packages/web/components/templates/KeyboardShortcutListModal.tsx @@ -125,7 +125,7 @@ const readerCommands = () => { callback: () => {}, }, { - actionDescription: 'Open Notebook', + actionDescription: 'Toggle Notebook open', shortcutKeys: ['t'], shortcutKeyDescription: 't', callback: () => {}, diff --git a/packages/web/components/templates/article/EpubContainer.tsx b/packages/web/components/templates/article/EpubContainer.tsx index e59e0a5aa..85a21ad84 100644 --- a/packages/web/components/templates/article/EpubContainer.tsx +++ b/packages/web/components/templates/article/EpubContainer.tsx @@ -333,7 +333,6 @@ export default function EpubContainer(props: EpubContainerProps): JSX.Element { key={notebookKey} viewer={props.viewer} item={props.article} - highlights={highlightsRef.current} onClose={(updatedHighlights, deletedAnnotations) => { console.log( 'closed PDF notebook: ', diff --git a/packages/web/components/templates/article/HighlightViewItem.tsx b/packages/web/components/templates/article/HighlightViewItem.tsx index 8a4186c43..df2440929 100644 --- a/packages/web/components/templates/article/HighlightViewItem.tsx +++ b/packages/web/components/templates/article/HighlightViewItem.tsx @@ -2,9 +2,8 @@ import { useState } from 'react' import { Highlight } from '../../../lib/networking/fragments/highlightFragment' import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery' import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' -import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' +import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { HighlightView } from '../../patterns/HighlightView' -import { HighlightsMenu } from '../homeFeed/HighlightItem' type HighlightViewItemProps = { viewer: UserBasicData @@ -25,38 +24,28 @@ export function HighlightViewItem(props: HighlightViewItemProps): JSX.Element { 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 12f4a7fc1..4aa3f7ee0 100644 --- a/packages/web/components/templates/article/HighlightsLayer.tsx +++ b/packages/web/components/templates/article/HighlightsLayer.tsx @@ -19,13 +19,17 @@ import { HighlightBar, HighlightAction } from '../../patterns/HighlightBar' import { removeHighlights } from '../../../lib/highlights/deleteHighlight' import { createHighlight } from '../../../lib/highlights/createHighlight' import { HighlightNoteModal } from './HighlightNoteModal' -import { NotebookModal } from './NotebookModal' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { ArticleMutations } from '../../../lib/articleActions' import { isTouchScreenDevice } from '../../../lib/deviceType' import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery' import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter' +import SlidingPane from 'react-sliding-pane' +import 'react-sliding-pane/dist/react-sliding-pane.css' +import { NotebookContent } from './Notebook' +import { NotebookHeader } from './NotebookHeader' +import useGetWindowDimensions from '../../../lib/hooks/useGetWindowDimensions' type HighlightsLayerProps = { viewer: UserBasicData @@ -74,15 +78,15 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { const focusedHighlightMousePos = useRef({ pageX: 0, pageY: 0 }) const [currentHighlightIdx, setCurrentHighlightIdx] = useState(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 windowDimensions = useGetWindowDimensions() const createHighlightFromSelection = useCallback( async ( @@ -183,6 +187,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { ) setHighlights(highlights.filter(($0) => $0.id !== highlightId)) setFocusedHighlight(undefined) + document.dispatchEvent(new Event('highlightsUpdated')) } else { console.error('Failed to delete highlight') } @@ -439,7 +444,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { await removeHighlightCallback() break case 'create': - await createHighlightCallback('none') + await createHighlightCallback() break case 'comment': if (props.highlightBarDisabled || focusedHighlight) { @@ -541,6 +546,21 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { } } + const deleteHighlightById = useCallback( + (event: Event) => { + const annotationId = (event as CustomEvent).detail as string + if (annotationId) { + removeHighlights( + highlights.map((h) => h.id), + highlightLocations + ) + const keptHighlights = highlights.filter(($0) => $0.id !== annotationId) + setHighlights([...keptHighlights]) + } + }, + [highlights, highlightLocations] + ) + useEffect(() => { const safeHandleAction = async (action: HighlightAction) => { try { @@ -652,7 +672,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { dispatchHighlightMessage('noteCreated') } else { try { - await createHighlightCallback('none') + await createHighlightCallback() dispatchHighlightMessage('noteCreated') } catch (error) { dispatchHighlightError('saveAnnotation', error) @@ -671,6 +691,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { document.addEventListener('setHighlightLabels', setHighlightLabels) document.addEventListener('scrollToNextHighlight', goToNextHighlight) document.addEventListener('scrollToPrevHighlight', goToPreviousHighlight) + document.addEventListener('deleteHighlightbyId', deleteHighlightById) return () => { document.removeEventListener('annotate', annotate) @@ -687,91 +708,95 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { 'scrollToPrevHighlight', goToPreviousHighlight ) + document.removeEventListener('deleteHighlightbyId', deleteHighlightById) } }) - if (highlightModalAction?.highlightModalAction == 'addComment') { - return ( - - setHighlightModalAction({ highlightModalAction: 'none' }) - } - createHighlightForNote={highlightModalAction?.createHighlightForNote} - /> - ) - } - - if (labelsTarget) { - return ( - setLabelsTarget(undefined)} - /> - ) - } - - // Display the button bar if we are not in the native app and there - // is a focused highlight or selection data - if (!props.highlightBarDisabled && (focusedHighlight || selectionData)) { - const anchorCoordinates = () => { - return { - pageX: - selectionData?.focusPosition.x ?? - focusedHighlightMousePos.current?.pageX ?? - 0, - pageY: - selectionData?.focusPosition.y ?? - focusedHighlightMousePos.current?.pageY ?? - 0, - } + const anchorCoordinates = () => { + return { + pageX: + selectionData?.focusPosition.x ?? + focusedHighlightMousePos.current?.pageX ?? + 0, + pageY: + selectionData?.focusPosition.y ?? + focusedHighlightMousePos.current?.pageY ?? + 0, } - - return ( - <> - - - ) } - if (props.showHighlightsModal) { - return ( - { - // 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}` - ) + return ( + <> + {highlightModalAction?.highlightModalAction == 'addComment' && ( + + setHighlightModalAction({ highlightModalAction: 'none' }) + } + createHighlightForNote={highlightModalAction?.createHighlightForNote} + /> + )} + {labelsTarget && ( + setLabelsTarget(undefined)} + /> + )} + {/* // Display the button bar if we are not in the native app and there // is + a focused highlight or selection data */} + {!props.highlightBarDisabled && (focusedHighlight || selectionData) && ( + <> + + + )} + { props.setShowHighlightsModal(false) }} - /> - ) - } - - return <> + > + <> + + { + // 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}` + ) + }} + /> + + + + ) } diff --git a/packages/web/components/templates/article/Notebook.tsx b/packages/web/components/templates/article/Notebook.tsx index 1d17f8d55..f511c6e8a 100644 --- a/packages/web/components/templates/article/Notebook.tsx +++ b/packages/web/components/templates/article/Notebook.tsx @@ -2,8 +2,8 @@ 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { CaretDown, CaretRight } from 'phosphor-react' import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { diff_match_patch } from 'diff-match-patch' @@ -12,28 +12,26 @@ import { createHighlightMutation } from '../../../lib/networking/mutations/creat import { v4 as uuidv4 } from 'uuid' import { nanoid } from 'nanoid' import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation' -import { HighlightNoteBox } 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 { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery' import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter' +import { Button } from '../../elements/Button' +import { ArticleNotes } from '../../patterns/ArticleNotes' +import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery' +import { formattedShortTime } from '../../../lib/dateFormatting' +import { isDarkTheme } from '../../../lib/themeUpdater' -type NotebookProps = { +type NotebookContentProps = { viewer: UserBasicData item: ReadableItem - highlights: Highlight[] - - sizeMode: 'normal' | 'maximized' viewInReader: (highlightId: string) => void - onAnnotationsChanged?: ( - highlights: Highlight[], - deletedAnnotations: Highlight[] - ) => void + onAnnotationsChanged?: (highlights: Highlight[]) => void showConfirmDeleteNote?: boolean setShowConfirmDeleteNote?: (show: boolean) => void @@ -45,161 +43,95 @@ export const getHighlightLocation = (patch: string): number | undefined => { return patches[0].start1 || undefined } -type AnnotationInfo = { - loaded: boolean - +type NoteState = { + isCreating: boolean note: Highlight | undefined - noteId: string - - allAnnotations: Highlight[] - deletedAnnotations: Highlight[] + createStarted: Date | undefined } -export function Notebook(props: NotebookProps): JSX.Element { +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 [noteText, setNoteText] = useState('') const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] = useState(undefined) - const [labelsTarget, setLabelsTarget] = useState( - undefined - ) - 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 - 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, + const [labelsTarget, setLabelsTarget] = + useState(undefined) + const noteState = useRef({ + isCreating: false, note: undefined, - noteId: uuidv4(), - allAnnotations: [], - deletedAnnotations: [], + createStarted: undefined, }) - useEffect(() => { - dispatchAnnotations({ - type: 'RESET', - allHighlights: props.highlights, - }) - }, [props.highlights]) + const newNoteId = useMemo(() => { + return uuidv4() + }, []) - useEffect(() => { - if (props.onAnnotationsChanged) { - props.onAnnotationsChanged( - annotations.allAnnotations, - annotations.deletedAnnotations - ) - } - }, [annotations]) + const updateNote = useCallback( + (note: Highlight, text: string, startTime: Date) => { + ;(async () => { + const result = await updateHighlightMutation({ + highlightId: note.id, + annotation: text, + }) + if (result) { + setLastSaved(startTime) + } else { + setErrorSaving('Error saving') + } + })() + }, + [] + ) - const deleteDocumentNote = useCallback(() => { - const note = annotations.note - if (!note) { - showErrorToast('No note found') - return - } + const createNote = useCallback((text: string) => { + console.log('creating note: ', newNoteId, noteState.current.isCreating) + noteState.current.isCreating = true + noteState.current.createStarted = new Date() ;(async () => { try { - const result = await deleteHighlightMutation(note.id) - if (!result) { - throw new Error() - } - showSuccessToast('Note deleted') - dispatchAnnotations({ - note, - type: 'DELETE_NOTE', + const success = await createHighlightMutation({ + id: newNoteId, + shortId: nanoid(8), + type: 'NOTE', + articleId: props.item.id, + annotation: text, }) - } catch (err) { - console.log('error deleting note', err) - showErrorToast('Error deleting note') + if (success) { + noteState.current.note = success + noteState.current.isCreating = false + } else { + setErrorSaving('Error creating note') + } + } catch (error) { + console.error('error creating note: ', error) + noteState.current.isCreating = false + setErrorSaving('Error creating note') } })() - }, [annotations]) + }, []) + + const highlights = useMemo(() => { + const result = articleData?.article.article.highlights + const note = result?.find((h) => h.type === 'NOTE') + if (note) { + noteState.current.note = note + noteState.current.isCreating = false + setNoteText(note.annotation || '') + } + return result + }, [articleData]) + + useEffect(() => { + if (highlights && props.onAnnotationsChanged) { + props.onAnnotationsChanged(highlights) + } + }, [highlights]) const sortedHighlights = useMemo(() => { const sorted = (a: number, b: number) => { @@ -212,7 +144,7 @@ export function Notebook(props: NotebookProps): JSX.Element { return 0 } - return annotations.allAnnotations + return (highlights ?? []) .filter((h) => h.type === 'HIGHLIGHT') .sort((a: Highlight, b: Highlight) => { if (a.highlightPositionPercent && b.highlightPositionPercent) { @@ -229,83 +161,123 @@ export function Notebook(props: NotebookProps): JSX.Element { } catch {} return a.createdAt.localeCompare(b.createdAt) }) - }, [annotations]) + }, [highlights]) 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 - } + (text) => { + const changeTime = new Date() - 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) - })() + setLastChanged(changeTime) + if (noteState.current.note) { + updateNote(noteState.current.note, text, changeTime) return } + if (noteState.current.isCreating) { + if (noteState.current.createStarted) { + const timeSinceStart = + new Date().getTime() - noteState.current.createStarted.getTime() - 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, - }) + if (timeSinceStart > 4000) { + createNote(text) + return } - cb(!!success) - })() + } return } + createNote(text) }, - [annotations, props.item] + [noteText, noteState, createNote, updateNote, highlights] ) + const deleteDocumentNote = useCallback(() => { + ;(async () => { + highlights + ?.filter((h) => h.type === 'NOTE') + .forEach(async (h) => { + const result = await deleteHighlightMutation(h.id) + if (!result) { + showErrorToast('Error deleting note') + } + }) + noteState.current.note = undefined + })() + setNoteText('') + }, [noteState, highlights]) + + const [errorSaving, setErrorSaving] = useState(undefined) + 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 ( - setNotesEditMode(edit ? 'edit' : 'preview')} - /> - - - - + css={{ + height: '100%', + width: '100%', + px: '20px', + bg: '$thLibrarySearchbox', + '@mdDown': { p: '15px' }, + }} + > + <> + + + + + {errorSaving && ( + + {errorSaving} + + )} + {lastSaved !== undefined ? ( + <> + {lastChanged === lastSaved + ? 'Saved' + : `Last saved ${formattedShortTime(lastSaved.toISOString())}`} + + ) : null} + + + + {sortedHighlights.map((highlight) => ( { - dispatchAnnotations({ - type: 'UPDATE_HIGHLIGHT', - updateHighlight: highlight, - }) + mutate() }} /> ))} {sortedHighlights.length === 0 && ( You have not added any highlights to this document. @@ -340,32 +313,34 @@ export function Notebook(props: NotebookProps): JSX.Element { )} - + > + {showConfirmDeleteHighlightId && ( { ;(async () => { + const highlightId = showConfirmDeleteHighlightId const success = await deleteHighlightMutation( showConfirmDeleteHighlightId ) - console.log(' ConfirmationModal::DeleteHighlight', success) + mutate() if (success) { - dispatchAnnotations({ - type: 'DELETE_HIGHLIGHT', - deleteHighlightId: showConfirmDeleteHighlightId, + showSuccessToast('Highlight deleted.', { + position: 'bottom-right', }) - showSuccessToast('Highlight deleted.') + const event = new CustomEvent('deleteHighlightbyId', { + detail: highlightId, + }) + document.dispatchEvent(event) } else { - showErrorToast('Error deleting highlight') + showErrorToast('Error deleting highlight', { + position: 'bottom-right', + }) } })() setShowConfirmDeleteHighlightId(undefined) @@ -383,7 +358,10 @@ export function Notebook(props: NotebookProps): JSX.Element { setLabelsTarget(undefined)} + onOpenChange={() => { + mutate() + setLabelsTarget(undefined) + }} /> )} {props.showConfirmDeleteNote && ( @@ -407,62 +385,46 @@ export function Notebook(props: NotebookProps): JSX.Element { ) } -type TitledSectionProps = { +type SectionTitleProps = { title: string - editMode?: boolean - setEditMode?: (set: boolean) => void + selected: boolean + setSelected: (set: boolean) => void } -function TitledSection(props: TitledSectionProps): JSX.Element { +function SectionTitle(props: SectionTitleProps): JSX.Element { return ( <> - { + props.setSelected(true) + event.stopPropagation() + }} > {props.title} - {props.setEditMode && ( - { - if (props.setEditMode) { - props.setEditMode(!props.editMode) - } - event.preventDefault() - }} - > - {props.editMode ? ( - - ) : ( - - )} - - )} - + ) } diff --git a/packages/web/components/templates/article/NotebookHeader.tsx b/packages/web/components/templates/article/NotebookHeader.tsx new file mode 100644 index 000000000..5edeccd91 --- /dev/null +++ b/packages/web/components/templates/article/NotebookHeader.tsx @@ -0,0 +1,77 @@ +import { useCallback } from 'react' +import { Highlight } from '../../../lib/networking/fragments/highlightFragment' +import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery' +import { + UserBasicData, + useGetViewerQuery, +} from '../../../lib/networking/queries/useGetViewerQuery' +import { CloseButton } from '../../elements/CloseButton' +import { Dropdown, DropdownOption } from '../../elements/DropdownElements' +import { HStack } from '../../elements/LayoutPrimitives' +import { MenuTrigger } from '../../elements/MenuTrigger' +import { StyledText } from '../../elements/StyledText' +import { NotebookModal } from './NotebookModal' +import { Sidebar } from 'phosphor-react' +import { theme } from '../../tokens/stitches.config' +import { Button } from '../../elements/Button' + +type NotebookHeaderProps = { + setShowNotebook: (set: boolean) => void +} + +export const NotebookHeader = (props: NotebookHeaderProps) => { + const handleClose = useCallback(() => { + props.setShowNotebook(false) + }, [props]) + + return ( + + + Notebook + + + {/* }> + { + // exportHighlights() + }} + title="Export Notebook" + /> + { + // setShowConfirmDeleteNote(true) + }} + title="Delete Article Note" + /> + */} + + + + ) +} diff --git a/packages/web/components/templates/article/NotebookModal.tsx b/packages/web/components/templates/article/NotebookModal.tsx index 25281f854..55205f6be 100644 --- a/packages/web/components/templates/article/NotebookModal.tsx +++ b/packages/web/components/templates/article/NotebookModal.tsx @@ -16,7 +16,7 @@ import { diff_match_patch } from 'diff-match-patch' import { MenuTrigger } from '../../elements/MenuTrigger' import { highlightsAsMarkdown } from '../homeFeed/HighlightItem' import 'react-markdown-editor-lite/lib/index.css' -import { Notebook } from './Notebook' +import { NotebookContent } from './Notebook' import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery' @@ -24,10 +24,9 @@ type NotebookModalProps = { viewer: UserBasicData item: ReadableItem - highlights: Highlight[] viewHighlightInReader: (arg: string) => void - onClose: (highlights: Highlight[], deletedAnnotations: Highlight[]) => void + onClose: (highlights: Highlight[], deletedHighlights: Highlight[]) => void } export const getHighlightLocation = (patch: string): number | undefined => { @@ -37,26 +36,22 @@ export const getHighlightLocation = (patch: string): number | undefined => { } export function NotebookModal(props: NotebookModalProps): JSX.Element { - const [sizeMode, setSizeMode] = useState<'normal' | 'maximized'>('normal') const [showConfirmDeleteNote, setShowConfirmDeleteNote] = useState(false) const [allAnnotations, setAllAnnotations] = useState( undefined ) - const [deletedAnnotations, setDeletedAnnotations] = useState< + + const [deletedHighlights, setDeletedAnnotations] = useState< Highlight[] | undefined >(undefined) const handleClose = useCallback(() => { - props.onClose(allAnnotations ?? [], deletedAnnotations ?? []) - }, [props, allAnnotations, deletedAnnotations]) + props.onClose(allAnnotations ?? [], deletedHighlights ?? []) + }, [props, allAnnotations]) - const handleAnnotationsChange = useCallback( - (allAnnotations, deletedAnnotations) => { - setAllAnnotations(allAnnotations) - setDeletedAnnotations(deletedAnnotations) - }, - [] - ) + const handleAnnotationsChange = useCallback((allAnnotations) => { + setAllAnnotations(allAnnotations) + }, []) const exportHighlights = useCallback(() => { ;(async () => { @@ -88,9 +83,11 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element { }} css={{ overflow: 'auto', - height: sizeMode === 'normal' ? 'unset' : '100%', - maxWidth: sizeMode === 'normal' ? '640px' : '100%', - minHeight: sizeMode === 'normal' ? '525px' : 'unset', + bg: '$thLibraryBackground', + width: '100%', + height: 'unset', + maxWidth: '748px', + minHeight: '525px', '@mdDown': { top: '20px', width: '100%', @@ -99,9 +96,18 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element { transform: 'translate(-50%)', }, }} + onKeyUp={(event) => { + switch (event.key) { + case 'Escape': + handleClose() + event.preventDefault() + event.stopPropagation() + break + } + }} > - }> { @@ -144,9 +149,8 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element { - void }): JSX.Element { ) } - -function SizeToggle(props: SizeToggleProps): JSX.Element { - return ( - - ) -} diff --git a/packages/web/components/templates/article/NotebookPresenter.tsx b/packages/web/components/templates/article/NotebookPresenter.tsx index c76aad2e5..985e62ab3 100644 --- a/packages/web/components/templates/article/NotebookPresenter.tsx +++ b/packages/web/components/templates/article/NotebookPresenter.tsx @@ -1,33 +1,59 @@ -import { Highlight } from '../../../lib/networking/fragments/highlightFragment' import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery' -import { - UserBasicData, - useGetViewerQuery, -} from '../../../lib/networking/queries/useGetViewerQuery' -import { NotebookModal } from './NotebookModal' +import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' +import SlidingPane from 'react-sliding-pane' +import 'react-sliding-pane/dist/react-sliding-pane.css' +import { NotebookContent } from './Notebook' +import { NotebookHeader } from './NotebookHeader' +import useGetWindowDimensions from '../../../lib/hooks/useGetWindowDimensions' type NotebookPresenterProps = { viewer: UserBasicData item: ReadableItem - highlights: Highlight[] - onClose: (highlights: Highlight[]) => void + open: boolean + setOpen: (open: boolean) => void } export const NotebookPresenter = (props: NotebookPresenterProps) => { + const windowDimensions = useGetWindowDimensions() + return ( - { - console.log('NotebookModal: ', highlights, deletedAnnotations) - props.onClose(highlights) + { + props.setOpen(false) }} - viewHighlightInReader={(highlightId) => { - window.location.href = `/${props.viewer.profile.username}/${props.item.slug}#${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}` + ) + }} + /> + + ) } diff --git a/packages/web/components/templates/article/PdfArticleContainer.tsx b/packages/web/components/templates/article/PdfArticleContainer.tsx index cb4809a32..9beff0387 100644 --- a/packages/web/components/templates/article/PdfArticleContainer.tsx +++ b/packages/web/components/templates/article/PdfArticleContainer.tsx @@ -13,11 +13,15 @@ import { articleReadingProgressMutation } from '../../../lib/networking/mutation import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation' import { useCanShareNative } from '../../../lib/hooks/useCanShareNative' import { pspdfKitKey } from '../../../lib/appConfig' -import { NotebookModal } from './NotebookModal' import { HighlightNoteModal } from './HighlightNoteModal' import { showErrorToast } from '../../../lib/toastHelpers' import { HEADER_HEIGHT } from '../homeFeed/HeaderSpacer' import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' +import SlidingPane from 'react-sliding-pane' +import 'react-sliding-pane/dist/react-sliding-pane.css' +import { NotebookContent } from './Notebook' +import { NotebookHeader } from './NotebookHeader' +import useWindowDimensions from '../../../lib/hooks/useGetWindowDimensions' export type PdfArticleContainerProps = { viewer: UserBasicData @@ -30,14 +34,12 @@ 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() @@ -475,6 +477,8 @@ export default function PdfArticleContainer( // the PSPDFKit instance if the theme, article URL, or page URL changes. Everything else // should be handled by the PSPDFKit instance callbacks. + const windowDimensions = useWindowDimensions() + return ( )} - {props.showHighlightsModal && ( - { - console.log( - 'closed PDF notebook: ', - updatedHighlights, - deletedAnnotations - ) - deletedAnnotations.forEach((highlight) => { - const event = new CustomEvent('deleteHighlightbyId', { - detail: highlight.id, + { + props.setShowHighlightsModal(false) + }} + > + <> + + { + const event = new CustomEvent('scrollToHighlightId', { + detail: highlightId, }) document.dispatchEvent(event) - }) - props.setShowHighlightsModal(false) - }} - viewHighlightInReader={(highlightId) => { - const event = new CustomEvent('scrollToHighlightId', { - detail: highlightId, - }) - document.dispatchEvent(event) - props.setShowHighlightsModal(false) - }} - /> - )} + }} + /> + + ) } diff --git a/packages/web/components/templates/article/SetLabelsModal.tsx b/packages/web/components/templates/article/SetLabelsModal.tsx index 9b484e015..06f021591 100644 --- a/packages/web/components/templates/article/SetLabelsModal.tsx +++ b/packages/web/components/templates/article/SetLabelsModal.tsx @@ -14,6 +14,7 @@ import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQ import { v4 as uuidv4 } from 'uuid' import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects' import { LabelsDispatcher } from '../../../lib/hooks/useSetPageLabels' +import * as Dialog from '@radix-ui/react-dialog' type SetLabelsModalProps = { provider: LabelsProvider @@ -30,9 +31,8 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element { const availableLabels = useGetLabelsQuery() const [tabCount, setTabCount] = useState(-1) const [tabStartValue, setTabStartValue] = useState('') - const [errorMessage, setErrorMessage] = useState( - undefined - ) + const [errorMessage, setErrorMessage] = + useState(undefined) const errorTimeoutRef = useRef() const [highlightLastLabel, setHighlightLastLabel] = useState(false) @@ -171,44 +171,46 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element { return ( - - { - event.preventDefault() - props.onOpenChange(false) - }} - onEscapeKeyDown={(event) => { - props.onOpenChange(false) - event.preventDefault() - }} - > - - - - - - - + + + { + event.preventDefault() + props.onOpenChange(false) + }} + onEscapeKeyDown={(event) => { + props.onOpenChange(false) + event.preventDefault() + }} + > + + + + + + + + ) } diff --git a/packages/web/components/templates/homeFeed/HighlightItem.tsx b/packages/web/components/templates/homeFeed/HighlightItem.tsx index eea578052..69af42634 100644 --- a/packages/web/components/templates/homeFeed/HighlightItem.tsx +++ b/packages/web/components/templates/homeFeed/HighlightItem.tsx @@ -10,7 +10,7 @@ import { DropdownOption, DropdownSeparator, } from '../../elements/DropdownElements' -import { Box } from '../../elements/LayoutPrimitives' +import { Box, VStack } from '../../elements/LayoutPrimitives' import { styled, theme } from '../../tokens/stitches.config' @@ -57,70 +57,77 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element { }, [props.highlight]) return ( - - - - } + - { - copyHighlight() - }} - title="Copy" - /> - { - props.setLabelsTarget(props.highlight) - }} - title="Labels" - /> - { - props.setShowConfirmDeleteHighlightId(props.highlight.id) - }} - 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() + { + copyHighlight() }} + title="Copy" + /> + { + props.setLabelsTarget(props.highlight) + }} + title="Labels" + /> + { + props.setShowConfirmDeleteHighlightId(props.highlight.id) + }} + title="Delete" + /> + + - View In Reader - - - + { + 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 + + + + ) } diff --git a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx index 6df02906b..d1c0dbd17 100644 --- a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx +++ b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx @@ -19,7 +19,7 @@ import { timeAgo, } from '../../patterns/LibraryCards/LibraryCardStyles' import { LibraryHighlightGridCard } from '../../patterns/LibraryCards/LibraryHighlightGridCard' -import { Notebook } from '../article/Notebook' +import { NotebookContent } from '../article/Notebook' import { EmptyHighlights } from './EmptyHighlights' import { HEADER_HEIGHT } from './HeaderSpacer' import { highlightsAsMarkdown } from './HighlightItem' @@ -34,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[], @@ -183,6 +182,7 @@ export function HighlightItemsLayout( flexGrow: '1', justifyContent: 'center', overflowY: 'scroll', + bg: '$thLibrarySearchbox', '@lgDown': { display: 'none', flexGrow: 'unset', @@ -415,24 +415,11 @@ function HighlightList(props: HighlightListProps): JSX.Element { - - NOTEBOOK - }> { @@ -442,13 +429,13 @@ function HighlightList(props: HighlightListProps): JSX.Element { /> - + {props.viewer && ( - )} diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 831510bc9..8c886fc91 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -84,13 +84,11 @@ export function HomeFeedContainer(): JSX.Element { const gridContainerRef = useRef(null) - const [labelsTarget, setLabelsTarget] = useState( - undefined - ) + const [labelsTarget, setLabelsTarget] = + useState(undefined) - const [notebookTarget, setNotebookTarget] = useState( - undefined - ) + const [notebookTarget, setNotebookTarget] = + useState(undefined) const [showAddLinkModal, setShowAddLinkModal] = useState(false) const [showEditTitleModal, setShowEditTitleModal] = useState(false) @@ -207,6 +205,11 @@ export function HomeFeedContainer(): JSX.Element { } setActiveCardId(id) scrollToActiveCard(id, true) + + const newItem = getItem(id) + if (notebookTarget && newItem) { + setNotebookTarget(newItem) + } }, [libraryItems] ) @@ -261,6 +264,13 @@ export function HomeFeedContainer(): JSX.Element { return libraryItems.find((item) => item.node.id === activeCardId) }, [libraryItems, activeCardId]) + const getItem = useCallback( + (itemId) => { + return libraryItems.find((item) => item.node.id === itemId) + }, + [libraryItems] + ) + const activeItemIndex = useMemo(() => { if (!activeCardId) { return undefined @@ -278,8 +288,6 @@ export function HomeFeedContainer(): JSX.Element { alreadyScrolled.current = true if (activeItem) { - console.log('refreshing') - // refresh items on home feed performActionOnItem('refresh', activeItem) } } @@ -342,7 +350,11 @@ export function HomeFeedContainer(): JSX.Element { setLabelsTarget(item) break case 'open-notebook': - setNotebookTarget(item) + if (!notebookTarget) { + setNotebookTarget(item) + } else { + setNotebookTarget(undefined) + } break case 'unsubscribe': performActionOnItem('unsubscribe', item) @@ -481,6 +493,7 @@ export function HomeFeedContainer(): JSX.Element { handleCardAction('set-labels', activeItem) break case 'openNotebook': + console.log('openNotebook: ', notebookTarget) handleCardAction('open-notebook', activeItem) break case 'sortDescending': @@ -1051,12 +1064,13 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element { { - if (props.notebookTarget?.node.highlights) { - props.notebookTarget.node.highlights = highlights - } - props.setNotebookTarget(undefined) + open={props.notebookTarget?.node !== undefined} + setOpen={(open: boolean) => { + // onClose={(highlights: Highlight[]) => { + // if (props.notebookTarget?.node.highlights) { + // props.notebookTarget.node.highlights = highlights + // } + props.setNotebookTarget(open ? props.notebookTarget : undefined) }} /> )} @@ -1101,18 +1115,16 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element { width: '100%', gridAutoRows: 'auto', borderRadius: '6px', - gridGap: props.layout == 'LIST_LAYOUT' ? '0' : '20px', + gridGap: props.layout == 'LIST_LAYOUT' ? '10px' : '20px', marginTop: '10px', marginBottom: '0px', paddingTop: '0', paddingBottom: '0px', overflow: 'hidden', - boxShadow: - props.layout == 'LIST_LAYOUT' - ? '0 1px 3px 0 rgba(0, 0, 0, 0.1),0 1px 2px 0 rgba(0, 0, 0, 0.06);' - : 'unset', + '@media (max-width: 930px)': { + gridGap: props.layout == 'LIST_LAYOUT' ? '0px' : '20px', + }, '@xlgDown': { - border: 'unset', borderRadius: props.layout == 'LIST_LAYOUT' ? 0 : undefined, }, '@smDown': { diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index d37381370..6e57310c7 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -180,6 +180,11 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = thLibraryMenuUnselected: '#898989', thLibrarySelectionColor: '#FFEA9F', + thNotebookSubtle: '#6A6968', + thNotebookBorder: '#D9D9D9', + thNotebookBackground: '#FCFCFC', + thNotebookTextBackground: '#EBEBEB', + thTextContrast: '#1E1E1E', thTextContrast2: '#3D3D3D', @@ -274,6 +279,12 @@ const darkThemeSpec = { thLibraryMenuUnselected: '#898989', thLibrarySelectionColor: '#3D3D3D', + thNotebookSubtle: '#898989', + thNotebookBorder: '#898989', + thNotebookBackground: '#3B3938', + thNotebookTextBackground: '#3D3D3D', + thNotebookHighContrast: '#2A2A2A', + thTextContrast: '#FFFFFF', thTextContrast2: '#EBEBEB', diff --git a/packages/web/lib/highlights/createHighlight.ts b/packages/web/lib/highlights/createHighlight.ts index 2a7b8715c..bbe5b8653 100644 --- a/packages/web/lib/highlights/createHighlight.ts +++ b/packages/web/lib/highlights/createHighlight.ts @@ -131,6 +131,8 @@ export async function createHighlight( ) } + document.dispatchEvent(new Event('highlightsUpdated')) + if (highlight) { const highlights = [...keptHighlights, highlight] return { diff --git a/packages/web/lib/hooks/useGetWindowDimensions.tsx b/packages/web/lib/hooks/useGetWindowDimensions.tsx new file mode 100644 index 000000000..d05c42e61 --- /dev/null +++ b/packages/web/lib/hooks/useGetWindowDimensions.tsx @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react' + +function getWindowDimensions() { + const { innerWidth: width, innerHeight: height } = window + return { + width, + height, + } +} +export default function useWindowDimensions() { + const [windowDimensions, setWindowDimensions] = useState( + getWindowDimensions() + ) + + useEffect(() => { + function handleResize() { + setWindowDimensions(getWindowDimensions()) + } + + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, []) + + return windowDimensions +} diff --git a/packages/web/lib/networking/queries/useGetArticleQuery.tsx b/packages/web/lib/networking/queries/useGetArticleQuery.tsx index 0924f6e74..f9f9df7cf 100644 --- a/packages/web/lib/networking/queries/useGetArticleQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleQuery.tsx @@ -14,6 +14,7 @@ import { Recommendation, recommendationFragment, } from './useGetLibraryItemsQuery' +import useSWR from 'swr' type ArticleQueryInput = { username?: string @@ -25,6 +26,8 @@ type ArticleQueryOutput = { articleData?: ArticleData isLoading: boolean articleFetchError: string[] | null + + mutate: () => void } type ArticleData = { @@ -107,7 +110,7 @@ export function useGetArticleQuery({ includeFriendsHighlights, } - const { data, error } = useSWRImmutable( + const { data, error, mutate } = useSWR( slug ? [query, username, slug, includeFriendsHighlights] : null, makeGqlFetcher(variables) ) @@ -124,6 +127,7 @@ export function useGetArticleQuery({ } return { + mutate: mutate, articleData: resultData, isLoading: !error && !data, articleFetchError: resultError ? (resultError as string[]) : null, diff --git a/packages/web/lib/themeUpdater.tsx b/packages/web/lib/themeUpdater.tsx index 07b860c5c..6c57109a6 100644 --- a/packages/web/lib/themeUpdater.tsx +++ b/packages/web/lib/themeUpdater.tsx @@ -107,6 +107,7 @@ export function isDarkTheme(): boolean { return ( currentTheme === 'Dark' || currentTheme === 'Darker' || + currentTheme === 'Apollo' || currentTheme == 'Black' ) } diff --git a/packages/web/package.json b/packages/web/package.json index 5e7657691..92050b88b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -18,6 +18,7 @@ "build-storybook": "build-storybook -s public" }, "dependencies": { + "@floating-ui/react": "^0.24.3", "@radix-ui/react-avatar": "^0.1.1", "@radix-ui/react-checkbox": "^0.1.5", "@radix-ui/react-dialog": "^0.1.1", @@ -61,6 +62,7 @@ "react-markdown-editor-lite": "^1.3.4", "react-masonry-css": "^1.0.16", "react-pro-sidebar": "^0.7.1", + "react-sliding-pane": "^7.3.0", "react-spinners": "^0.13.7", "react-super-responsive-table": "^5.2.1", "react-topbar-progress-indicator": "^4.1.1", diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index 4ba683215..415f161f5 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -259,6 +259,10 @@ export default function Home(): JSX.Element { ) { return } + if (showHighlightsModal) { + setShowHighlightsModal(false) + return + } const query = window.sessionStorage.getItem('q') if (query) { router.push(`/home?${query}`) @@ -350,7 +354,7 @@ export default function Home(): JSX.Element { name: 'Notebook', shortcut: ['t'], perform: () => { - setShowHighlightsModal(true) + setShowHighlightsModal(!showHighlightsModal) }, }, { @@ -361,7 +365,7 @@ export default function Home(): JSX.Element { perform: () => setShowEditModal(true), }, ], - [readerSettings] + [readerSettings, showHighlightsModal] ) const [labels, dispatchLabels] = useSetPageLabels(article?.id) diff --git a/packages/web/styles/globals.css b/packages/web/styles/globals.css index 569bddf90..0cd082d5d 100644 --- a/packages/web/styles/globals.css +++ b/packages/web/styles/globals.css @@ -419,20 +419,22 @@ button { margin: 0px; } -.omnivore-masonry-grid { - display: -webkit-box; /* Not needed if autoprefixing */ - display: -ms-flexbox; /* Not needed if autoprefixing */ - display: flex; - margin-left: -16px; /* gutter size offset */ - margin-right: 14px; - width: auto; +.slide-panel-overlay { + z-index: 100 !important; + background: transparent !important; + pointer-events: none; } -.omnivore-masonry-grid_column { - padding-left: 16px; /* gutter size */ - background-clip: padding-box; + +.slide-pane__content { + padding: 0px !important; + pointer-events: all; + border-top-left-radius: 10px; + border-bottom-right-radius: 10px; + border-left: 1px solid var(--colors-thNotebookBorder); + background: var(--colors-thNotebookBackground); } -/* .omnivore-masonry-grid_column > div { - background: grey; - margin-bottom: 16px; -} */ +.slide-pane { + background: transparent !important; + box-shadow: 0px 4px 4px rgba(33, 33, 33, 0.1) !important; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 025cd3b97..eb21f53dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2504,6 +2504,34 @@ dependencies: tslib "^2.1.0" +"@floating-ui/core@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.3.1.tgz#4d795b649cc3b1cbb760d191c80dcb4353c9a366" + integrity sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g== + +"@floating-ui/dom@^1.3.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.4.2.tgz#eb3a37f7506c4f95ef735967dc3496b5012e11cb" + integrity sha512-VKmvHVatWnewmGGy+7Mdy4cTJX71Pli6v/Wjb5RQBuq5wjUYx+Ef+kRThi8qggZqDgD8CogCpqhRoVp3+yQk+g== + dependencies: + "@floating-ui/core" "^1.3.1" + +"@floating-ui/react-dom@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.1.tgz#7972a4fc488a8c746cded3cfe603b6057c308a91" + integrity sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA== + dependencies: + "@floating-ui/dom" "^1.3.0" + +"@floating-ui/react@^0.24.3": + version "0.24.3" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.24.3.tgz#4f11f09c7245555724f5167dd6925133457db89c" + integrity sha512-wWC9duiog4HmbgKSKObDRuXqMjZR/6m75MIG+slm5CVWbridAjK9STcnCsGYmdpK78H/GmzYj4ADVP8paZVLYQ== + dependencies: + "@floating-ui/react-dom" "^2.0.1" + aria-hidden "^1.1.3" + tabbable "^6.0.1" + "@google-cloud/common@^3.8.1": version "3.9.0" resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-3.9.0.tgz#d93e62d13e66edacfad1cd25b20fdbbc11d9f6dd" @@ -10070,6 +10098,13 @@ aria-hidden@^1.1.1: dependencies: tslib "^1.0.0" +aria-hidden@^1.1.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" + integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ== + dependencies: + tslib "^2.0.0" + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -14432,6 +14467,11 @@ executable@^4.1.1: dependencies: pify "^2.2.0" +exenv@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" + integrity sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw== + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -23806,6 +23846,11 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-lifecycles-compat@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + react-markdown-editor-lite@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/react-markdown-editor-lite/-/react-markdown-editor-lite-1.3.4.tgz#77992d2389b9427a06595c63d95f52be66e5fea9" @@ -23842,6 +23887,16 @@ react-masonry-css@^1.0.16: resolved "https://registry.yarnpkg.com/react-masonry-css/-/react-masonry-css-1.0.16.tgz#72b28b4ae3484e250534700860597553a10f1a2c" integrity sha512-KSW0hR2VQmltt/qAa3eXOctQDyOu7+ZBevtKgpNDSzT7k5LA/0XntNa9z9HKCdz3QlxmJHglTZ18e4sX4V8zZQ== +react-modal@^3.14.3: + version "3.16.1" + resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.16.1.tgz#34018528fc206561b1a5467fc3beeaddafb39b2b" + integrity sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg== + dependencies: + exenv "^1.2.0" + prop-types "^15.7.2" + react-lifecycles-compat "^3.0.0" + warning "^4.0.3" + react-popper-tooltip@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/react-popper-tooltip/-/react-popper-tooltip-3.1.1.tgz#329569eb7b287008f04fcbddb6370452ad3f9eac" @@ -23925,6 +23980,14 @@ react-slidedown@^2.4.5: dependencies: tslib "^2.0.0" +react-sliding-pane@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/react-sliding-pane/-/react-sliding-pane-7.3.0.tgz#a6a03b90db216e7ec6f746c7e649d19ba03ff4e0" + integrity sha512-KCyxw2BBvXjwYm1UX83Vk67D4kxec2icJxrSPidNus8voh1yB1K6bluwShAe3OvN5zk8H9InL22jGomTUOOudw== + dependencies: + prop-types "^15.7.2" + react-modal "^3.14.3" + react-spinners@^0.13.7: version "0.13.7" resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.13.7.tgz#0f423c415bfa56765ce9fb36ff604e52a92b37a9" @@ -26195,6 +26258,11 @@ synchronous-promise@^2.0.15: resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.15.tgz#07ca1822b9de0001f5ff73595f3d08c4f720eb8e" integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg== +tabbable@^6.0.1: + version "6.1.2" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.1.2.tgz#b0d3ca81d582d48a80f71b267d1434b1469a3703" + integrity sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ== + tapable@^1.0.0, tapable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" @@ -27739,7 +27807,7 @@ walker@~1.0.5: dependencies: makeerror "1.0.12" -warning@^4.0.2: +warning@^4.0.2, warning@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==