Merge pull request #2623 from omnivore-app/web/feat-export-highlights-and-notes

Add ability to export highlights and notes from notebook
This commit is contained in:
Jackson Harper 2023-08-08 20:18:20 +08:00 committed by GitHub
commit 8e2e36aa62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 152 additions and 73 deletions

View file

@ -0,0 +1,46 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { IconProps } from './IconProps'
import React from 'react'
export class ExportIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
const color = (this.props.color || '#2A2A2A').toString()
return (
<svg
width={size}
height={size}
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M9.19727 9.54199H8.1556C7.60306 9.54199 7.07316 9.76149 6.68246 10.1522C6.29176 10.5429 6.07227 11.0728 6.07227 11.6253V19.9587C6.07227 20.5112 6.29176 21.0411 6.68246 21.4318C7.07316 21.8225 7.60306 22.042 8.1556 22.042H18.5723C19.1248 22.042 19.6547 21.8225 20.0454 21.4318C20.4361 21.0411 20.6556 20.5112 20.6556 19.9587V11.6253C20.6556 11.0728 20.4361 10.5429 20.0454 10.1522C19.6547 9.76149 19.1248 9.54199 18.5723 9.54199H17.5306"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13.3643 14.7503V3.29199"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.2393 6.41699L13.3643 3.29199L16.4893 6.41699"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
)
}
}

View file

@ -1,5 +1,5 @@
/* eslint-disable react/no-children-prop */
import { useMemo, useState } from 'react'
import { useState } from 'react'
import type { Highlight } from '../../lib/networking/fragments/highlightFragment'
import { LabelChip } from '../elements/LabelChip'
import {
@ -14,7 +14,6 @@ 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 {
@ -25,7 +24,6 @@ import {
useHover,
useInteractions,
} from '@floating-ui/react'
import { LibraryHoverActions } from './LibraryCards/LibraryHoverActions'
import { HighlightHoverActions } from './HighlightHoverActions'
type HighlightViewProps = {
@ -54,7 +52,6 @@ const StyledQuote = styled(Blockquote, {
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({
@ -73,7 +70,7 @@ export function HighlightView(props: HighlightViewProps): JSX.Element {
const hover = useHover(context)
const { getReferenceProps, getFloatingProps } = useInteractions([hover])
const highlightAlpha = isDark ? 1.0 : 0.35
const highlightAlpha = isDark ? 0.5 : 0.35
return (
<VStack
@ -112,13 +109,19 @@ export function HighlightView(props: HighlightViewProps): JSX.Element {
<StyledQuote>
<SpanBox
css={{
'*': {
m: '0px',
'> *': {
display: 'inline',
padding: '2px',
backgroundColor: `rgba(var(--colors-highlightBackground), ${highlightAlpha})`,
boxShadow: `3px 0 0 rgba(var(--colors-highlightBackground), ${highlightAlpha}), -3px 0 0 rgba(var(--colors-highlightBackground), ${highlightAlpha})`,
boxDecorationBreak: 'clone',
borderRadius: '2px',
},
'> ul': {
display: 'block',
boxShadow: 'unset',
backgroundColor: 'unset',
},
fontSize: '15px',
lineHeight: 1.5,
color: '$thTextSubtle2',

View file

@ -776,7 +776,11 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
}}
>
<>
<NotebookHeader setShowNotebook={props.setShowHighlightsModal} />
<NotebookHeader
viewer={props.viewer}
item={props.item}
setShowNotebook={props.setShowHighlightsModal}
/>
<NotebookContent
viewer={props.viewer}
item={props.item}

View file

@ -3,7 +3,6 @@ import { StyledText } from '../../elements/StyledText'
import { theme } from '../../tokens/stitches.config'
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
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'
@ -382,47 +381,3 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
</VStack>
)
}
type SectionTitleProps = {
title: string
selected: boolean
setSelected: (set: boolean) => void
}
function SectionTitle(props: SectionTitleProps): JSX.Element {
return (
<>
<Button
style="plainIcon"
css={{
display: 'flex',
alignItems: 'center',
gap: '5px',
color: props.selected ? '$thTextContrast' : '$thTextSubtle',
borderBottom: props.selected
? '1px solid $thTextContrast'
: '1px solid transparent',
}}
onClick={(event) => {
props.setSelected(true)
event.stopPropagation()
}}
>
<StyledText
css={{
m: '0px',
pt: '2px',
pb: '2px',
px: '5px',
fontFamily: '$inter',
fontWeight: '500',
fontSize: '13px',
color: '$thNotebookSubtle',
}}
>
{props.title}
</StyledText>
</Button>
</>
)
}

View file

@ -1,28 +1,47 @@
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'
import { ExportIcon } from '../../elements/icons/ExportIcon'
import { useCallback, useMemo } from 'react'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
import { highlightsAsMarkdown } from '../homeFeed/HighlightItem'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
type NotebookHeaderProps = {
viewer: UserBasicData
item: ReadableItem
setShowNotebook: (set: boolean) => void
}
export const NotebookHeader = (props: NotebookHeaderProps) => {
const handleClose = useCallback(() => {
props.setShowNotebook(false)
}, [props])
const { articleData } = useGetArticleQuery({
slug: props.item.slug,
username: props.viewer.profile.username,
includeFriendsHighlights: false,
})
const exportHighlights = useCallback(() => {
if (articleData?.article.article.highlights) {
const markdown = highlightsAsMarkdown(
articleData?.article.article.highlights
)
if (markdown.length > 1) {
;(async () => {
await navigator.clipboard.writeText(markdown)
showSuccessToast('Highlights and notes copied')
})()
} else {
showSuccessToast('Nothing to export')
}
} else {
showErrorToast('Could not copy highlights')
}
}, [articleData])
return (
<HStack
@ -68,6 +87,19 @@ export const NotebookHeader = (props: NotebookHeaderProps) => {
title="Delete Article Note"
/>
</Dropdown> */}
<Button
style="plainIcon"
onClick={(event) => {
exportHighlights()
event.preventDefault()
}}
>
<ExportIcon
size={25}
color={theme.colors.thNotebookSubtle.toString()}
/>
</Button>
<Button style="plainIcon" onClick={() => props.setShowNotebook(false)}>
<Sidebar size={25} color={theme.colors.thNotebookSubtle.toString()} />
</Button>

View file

@ -34,7 +34,11 @@ export const NotebookPresenter = (props: NotebookPresenterProps) => {
}}
>
<>
<NotebookHeader setShowNotebook={props.setOpen} />
<NotebookHeader
viewer={props.viewer}
item={props.item}
setShowNotebook={props.setOpen}
/>
<NotebookContent
viewer={props.viewer}
item={props.item}

View file

@ -508,7 +508,11 @@ export default function PdfArticleContainer(
}}
>
<>
<NotebookHeader setShowNotebook={props.setShowHighlightsModal} />
<NotebookHeader
viewer={props.viewer}
item={props.article}
setShowNotebook={props.setShowHighlightsModal}
/>
<NotebookContent
viewer={props.viewer}
item={props.article}

View file

@ -13,6 +13,7 @@ import {
import { Box, VStack } from '../../elements/LayoutPrimitives'
import { styled, theme } from '../../tokens/stitches.config'
import { getHighlightLocation } from '../article/Notebook'
type HighlightsMenuProps = {
viewer: UserBasicData
@ -131,6 +132,36 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
)
}
const sortHighlights = (highlights: Highlight[]) => {
const sorted = (a: number, b: number) => {
if (a < b) {
return -1
}
if (a > b) {
return 1
}
return 0
}
return (highlights ?? [])
.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)
})
}
export function highlightAsMarkdown(highlight: Highlight) {
let buffer = `> ${highlight.quote}`
if (highlight.annotation) {
@ -143,14 +174,14 @@ export function highlightAsMarkdown(highlight: Highlight) {
export function highlightsAsMarkdown(highlights: Highlight[]) {
const noteMD = highlights.find((h) => h.type == 'NOTE')
const highlightMD = highlights
const highlightMD = sortHighlights(highlights)
.filter((h) => h.type == 'HIGHLIGHT')
.map((highlight) => {
return highlightAsMarkdown(highlight)
})
.join('\n\n')
if (noteMD) {
if (noteMD?.annotation) {
return `${noteMD.annotation}\n\n${highlightMD}`
}
return highlightMD

View file

@ -280,8 +280,8 @@ const darkThemeSpec = {
thLibrarySelectionColor: '#3D3D3D',
thNotebookSubtle: '#898989',
thNotebookBorder: '#898989',
thNotebookBackground: '#3B3938',
thNotebookBorder: '#3D3D3D',
thNotebookBackground: '#2F2F2F',
thNotebookTextBackground: '#3D3D3D',
thNotebookHighContrast: '#2A2A2A',