mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Pull in link to highlight changes
This commit is contained in:
parent
5d42b65611
commit
c5549d72a6
5 changed files with 112 additions and 83 deletions
|
|
@ -6,6 +6,7 @@ import { styled } from '../tokens/stitches.config'
|
|||
|
||||
type HighlightViewProps = {
|
||||
highlight: Highlight
|
||||
scrollToHighlight?: (arg: string) => void;
|
||||
author?: string
|
||||
title?: string
|
||||
}
|
||||
|
|
@ -15,25 +16,23 @@ export function HighlightView(props: HighlightViewProps): JSX.Element {
|
|||
() => props.highlight.quote.split('\n'),
|
||||
[props.highlight.quote]
|
||||
)
|
||||
const annotation = props.highlight.annotation ?? '';
|
||||
|
||||
const StyledQuote = styled(Blockquote, {
|
||||
margin: '0px 24px 16px 24px',
|
||||
fontSize: '18px',
|
||||
lineHeight: '27px',
|
||||
color: '$textDefault',
|
||||
margin: '0px 0px 24px 0px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
fontFamily: 'Inter',
|
||||
color: '$omnivoreGray',
|
||||
cursor: 'pointer',
|
||||
})
|
||||
|
||||
const scrollToHighlight = () => props.scrollToHighlight ? props.scrollToHighlight(props.highlight.id) : null
|
||||
|
||||
return (
|
||||
<VStack css={{ width: '100%', boxSizing: 'border-box' }}>
|
||||
{annotation && (
|
||||
<Box css={{p:'16px', m:'16px', ml:'24px', mr:'24px', borderRadius: '6px', bg: '$grayBase'}}>
|
||||
<StyledText style='shareHighlightModalAnnotation'>{annotation}</StyledText>
|
||||
</Box>)
|
||||
}
|
||||
<StyledQuote>
|
||||
<VStack css={{ width: '100%', bg: '$omnivoreYellow', p: '20px' }}>
|
||||
<StyledQuote onClick={scrollToHighlight}>
|
||||
{props.highlight.prefix}
|
||||
<SpanBox css={{ bg: '$highlightBackground', p: '1px', borderRadius: '2px', }}>
|
||||
<SpanBox css={{ bg: '$highlight', p: '1px', borderRadius: '2px' }}>
|
||||
{lines.map((line: string, index: number) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
|
|
@ -48,10 +47,11 @@ export function HighlightView(props: HighlightViewProps): JSX.Element {
|
|||
</SpanBox>
|
||||
{props.highlight.suffix}
|
||||
</StyledQuote>
|
||||
<Box css={{p: '24px', pt: '0', width: '100%', boxSizing: 'border-box'}}>
|
||||
{props.author && props.title &&(
|
||||
<StyledText style="highlightTitleAndAuthor">{props.title + props.author}</StyledText>
|
||||
<Box css={{}}>
|
||||
{props.author && (
|
||||
<StyledText style="highlightAuthor">{props.author}</StyledText>
|
||||
)}
|
||||
<StyledText style="highlightTitle">{props.title}</StyledText>
|
||||
</Box>
|
||||
</VStack>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ import { ArticleMutations } from '../../../lib/articleActions'
|
|||
export type ArticleProps = {
|
||||
articleId: string
|
||||
content: string
|
||||
highlightReady: boolean
|
||||
initialAnchorIndex: number
|
||||
initialReadingProgress?: number
|
||||
highlightHref: MutableRefObject<string | null>
|
||||
scrollElementRef: MutableRefObject<HTMLDivElement | null>
|
||||
articleMutations: ArticleMutations
|
||||
}
|
||||
|
|
@ -139,50 +141,53 @@ export function Article(props: ArticleProps): JSX.Element {
|
|||
return
|
||||
}
|
||||
|
||||
if (!shouldScrollToInitialPosition) {
|
||||
return
|
||||
}
|
||||
|
||||
setShouldScrollToInitialPosition(false)
|
||||
|
||||
if (props.initialReadingProgress && props.initialReadingProgress >= 98) {
|
||||
return
|
||||
}
|
||||
|
||||
const anchorElement = document.querySelector(
|
||||
`[data-omnivore-anchor-idx='${props.initialAnchorIndex.toString()}']`
|
||||
)
|
||||
|
||||
if (anchorElement) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const calculateOffset = (obj: any): number => {
|
||||
let offset = 0
|
||||
if (obj.offsetParent) {
|
||||
do {
|
||||
offset += obj.offsetTop
|
||||
} while ((obj = obj.offsetParent))
|
||||
return offset
|
||||
}
|
||||
|
||||
return 0
|
||||
if (props.highlightReady) {
|
||||
if (!shouldScrollToInitialPosition) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.scrollElementRef.current) {
|
||||
props.scrollElementRef.current?.scroll(
|
||||
0,
|
||||
calculateOffset(anchorElement)
|
||||
)
|
||||
} else {
|
||||
window.document.documentElement.scroll(
|
||||
0,
|
||||
calculateOffset(anchorElement)
|
||||
)
|
||||
setShouldScrollToInitialPosition(false)
|
||||
|
||||
if (props.initialReadingProgress && props.initialReadingProgress >= 98) {
|
||||
return
|
||||
}
|
||||
|
||||
const anchorElement = props.highlightHref.current
|
||||
? document.querySelector(
|
||||
`[omnivore-highlight-id="${props.highlightHref.current}"]`
|
||||
)
|
||||
: document.querySelector(
|
||||
`[data-omnivore-anchor-idx='${props.initialAnchorIndex.toString()}']`
|
||||
)
|
||||
|
||||
if (anchorElement) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const calculateOffset = (obj: any): number => {
|
||||
let offset = 0
|
||||
if (obj.offsetParent) {
|
||||
do {
|
||||
offset += obj.offsetTop
|
||||
} while ((obj = obj.offsetParent))
|
||||
return offset
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
const calculatedOffset = calculateOffset(anchorElement)
|
||||
|
||||
if (props.scrollElementRef.current) {
|
||||
props.scrollElementRef.current?.scroll(0, calculatedOffset - 100)
|
||||
} else {
|
||||
window.document.documentElement.scroll(0, calculatedOffset - 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
props.highlightReady,
|
||||
props.scrollElementRef,
|
||||
props.initialAnchorIndex,
|
||||
props.initialReadingProgress,
|
||||
props.scrollElementRef,
|
||||
shouldScrollToInitialPosition,
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { ArticleSubtitle } from './../../patterns/ArticleSubtitle'
|
|||
import { theme, ThemeId } from './../../tokens/stitches.config'
|
||||
import { HighlightsLayer } from '../../templates/article/HighlightsLayer'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { MutableRefObject, useEffect, useState } from 'react'
|
||||
import { MutableRefObject, useEffect, useState, useRef } from 'react'
|
||||
import { ReportIssuesModal } from './ReportIssuesModal'
|
||||
import { reportIssueMutation } from '../../../lib/networking/mutations/reportIssueMutation'
|
||||
import { ArticleHeaderToolbar } from './ArticleHeaderToolbar'
|
||||
|
|
@ -15,6 +15,7 @@ import { updateThemeLocally } from '../../../lib/themeUpdater'
|
|||
import { ArticleMutations } from '../../../lib/articleActions'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { HighlightLocation, makeHighlightStartEndOffset } from '../../../lib/highlights/highlightGenerator'
|
||||
|
||||
type ArticleContainerProps = {
|
||||
article: ArticleAttributes
|
||||
|
|
@ -36,6 +37,11 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
const [showShareModal, setShowShareModal] = useState(false)
|
||||
const [showReportIssuesModal, setShowReportIssuesModal] = useState(false)
|
||||
const [fontSize, setFontSize] = useState(props.fontSize ?? 20)
|
||||
const highlightHref = useRef(window.location.hash ? window.location.hash.split('#')[1] : null)
|
||||
const [highlightReady, setHighlightReady] = useState(false)
|
||||
const [highlightLocations, setHighlightLocations] = useState<
|
||||
HighlightLocation[]
|
||||
>([])
|
||||
|
||||
const updateFontSize = async (newFontSize: number) => {
|
||||
if (fontSize !== newFontSize) {
|
||||
|
|
@ -48,6 +54,21 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
updateFontSize(props.fontSize ?? 20)
|
||||
}, [props.fontSize])
|
||||
|
||||
// Load the highlights
|
||||
useEffect(() => {
|
||||
const res: HighlightLocation[] = []
|
||||
props.article.highlights.forEach((highlight) => {
|
||||
try {
|
||||
const offset = makeHighlightStartEndOffset(highlight)
|
||||
res.push(offset)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
setHighlightLocations(res)
|
||||
setHighlightReady(true)
|
||||
}, [props.article.highlights, setHighlightLocations])
|
||||
|
||||
// Listen for font size and color mode change events sent from host apps (ios, macos...)
|
||||
useEffect(() => {
|
||||
const increaseFontSize = async () => {
|
||||
|
|
@ -158,6 +179,8 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
)}
|
||||
</VStack>
|
||||
<Article
|
||||
highlightReady={highlightReady}
|
||||
highlightHref={highlightHref}
|
||||
articleId={props.article.id}
|
||||
content={props.article.content}
|
||||
initialAnchorIndex={props.article.readingProgressAnchorIndex}
|
||||
|
|
@ -182,6 +205,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
<Box css={{ height: '100px' }} />
|
||||
</Box>
|
||||
<HighlightsLayer
|
||||
highlightLocations={highlightLocations}
|
||||
highlights={props.article.highlights}
|
||||
articleTitle={props.article.title}
|
||||
articleAuthor={props.article.author ?? ''}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ type HighlightsLayerProps = {
|
|||
highlightsBaseURL: string
|
||||
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||
articleMutations: ArticleMutations
|
||||
highlightLocations: HighlightLocation[]
|
||||
}
|
||||
|
||||
type HighlightModalAction = 'none' | 'addComment' | 'share'
|
||||
|
|
@ -49,9 +50,6 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
const [highlightModalAction, setHighlightModalAction] =
|
||||
useState<HighlightActionProps>({ highlightModalAction: 'none' })
|
||||
|
||||
const [highlightLocations, setHighlightLocations] = useState<
|
||||
HighlightLocation[]
|
||||
>([])
|
||||
const focusedHighlightMousePos = useRef({ pageX: 0, pageY: 0 })
|
||||
|
||||
const [focusedHighlight, setFocusedHighlight] = useState<
|
||||
|
|
@ -59,26 +57,12 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
>(undefined)
|
||||
|
||||
const [selectionData, setSelectionData] = useSelection(
|
||||
highlightLocations,
|
||||
props.highlightLocations,
|
||||
false //noteModal.open,
|
||||
)
|
||||
|
||||
const canShareNative = useCanShareNative()
|
||||
|
||||
// Load the highlights
|
||||
useEffect(() => {
|
||||
const res: HighlightLocation[] = []
|
||||
highlights.forEach((highlight) => {
|
||||
try {
|
||||
const offset = makeHighlightStartEndOffset(highlight)
|
||||
res.push(offset)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
setHighlightLocations(res)
|
||||
}, [highlights, setHighlightLocations])
|
||||
|
||||
const removeHighlightCallback = useCallback(
|
||||
async (id?: string) => {
|
||||
const highlightId = id || focusedHighlight?.id
|
||||
|
|
@ -89,7 +73,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
if (didDeleteHighlight) {
|
||||
removeHighlights(
|
||||
highlights.map(($0) => $0.id),
|
||||
highlightLocations
|
||||
props.highlightLocations
|
||||
)
|
||||
setHighlights(highlights.filter(($0) => $0.id !== highlightId))
|
||||
setFocusedHighlight(undefined)
|
||||
|
|
@ -97,16 +81,16 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
console.error('Failed to delete highlight')
|
||||
}
|
||||
},
|
||||
[focusedHighlight, highlights, highlightLocations]
|
||||
[focusedHighlight, highlights, props.highlightLocations]
|
||||
)
|
||||
|
||||
const updateHighlightsCallback = useCallback(
|
||||
(highlight: Highlight) => {
|
||||
removeHighlights([highlight.id], highlightLocations)
|
||||
removeHighlights([highlight.id], props.highlightLocations)
|
||||
const keptHighlights = highlights.filter(($0) => $0.id !== highlight.id)
|
||||
setHighlights([...keptHighlights, highlight])
|
||||
},
|
||||
[highlights, highlightLocations]
|
||||
[highlights, props.highlightLocations]
|
||||
)
|
||||
|
||||
const handleNativeShare = useCallback(
|
||||
|
|
@ -159,7 +143,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
selection: selection,
|
||||
articleId: props.articleId,
|
||||
existingHighlights: highlights,
|
||||
highlightStartEndOffsets: highlightLocations,
|
||||
highlightStartEndOffsets: props.highlightLocations,
|
||||
annotation: note,
|
||||
}, props.articleMutations)
|
||||
|
||||
|
|
@ -214,10 +198,22 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
selectionData,
|
||||
setSelectionData,
|
||||
canShareNative,
|
||||
highlightLocations,
|
||||
props.highlightLocations,
|
||||
]
|
||||
)
|
||||
|
||||
const scrollToHighlight = (id: string) => {
|
||||
const foundElement = document.querySelector(`[omnivore-highlight-id="${id}"]`)
|
||||
if(foundElement){
|
||||
foundElement.scrollIntoView({
|
||||
block: 'center',
|
||||
behavior: 'smooth'
|
||||
})
|
||||
window.location.hash = `#${id}`
|
||||
props.setShowHighlightsModal(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect mouseclick on a highlight -- call `setFocusedHighlight` when highlight detected
|
||||
const handleClickHighlight = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
|
|
@ -261,7 +257,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
})
|
||||
} else setFocusedHighlight(undefined)
|
||||
},
|
||||
[highlights, highlightLocations]
|
||||
[highlights, props.highlightLocations]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -469,9 +465,10 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
if (props.showHighlightsModal) {
|
||||
return (
|
||||
<HighlightsModal
|
||||
highlights={highlights}
|
||||
onOpenChange={() => props.setShowHighlightsModal(false)}
|
||||
deleteHighlightAction={(highlightId: string) => {
|
||||
highlights={highlights}
|
||||
onOpenChange={() => props.setShowHighlightsModal(false)}
|
||||
scrollToHighlight={scrollToHighlight}
|
||||
deleteHighlightAction={(highlightId: string) => {
|
||||
removeHighlightCallback(highlightId)
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { Pen, Trash } from 'phosphor-react'
|
|||
|
||||
type HighlightsModalProps = {
|
||||
highlights: Highlight[]
|
||||
scrollToHighlight?: (arg: string) => void;
|
||||
deleteHighlightAction?: (highlightId: string) => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
|
@ -60,6 +61,7 @@ export function HighlightsModal(props: HighlightsModalProps): JSX.Element {
|
|||
key={highlight.id}
|
||||
highlight={highlight}
|
||||
showDelete={!!props.deleteHighlightAction}
|
||||
scrollToHighlight={props.scrollToHighlight}
|
||||
deleteHighlightAction={() => {
|
||||
if (props.deleteHighlightAction) {
|
||||
props.deleteHighlightAction(highlight.id)
|
||||
|
|
@ -82,6 +84,7 @@ export function HighlightsModal(props: HighlightsModalProps): JSX.Element {
|
|||
type ModalHighlightViewProps = {
|
||||
highlight: Highlight
|
||||
showDelete: boolean
|
||||
scrollToHighlight?: (arg: string) => void;
|
||||
deleteHighlightAction: () => void
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +159,7 @@ function ModalHighlightView(props: ModalHighlightViewProps): JSX.Element {
|
|||
return (
|
||||
<>
|
||||
<VStack>
|
||||
<HighlightView highlight={props.highlight} />
|
||||
<HighlightView scrollToHighlight={props.scrollToHighlight} highlight={props.highlight} />
|
||||
{props.highlight.annotation && !isEditing ? (
|
||||
<StyledText css={{ px: '24px' }}>{props.highlight.annotation}</StyledText>
|
||||
) : null}
|
||||
|
|
|
|||
Loading…
Reference in a new issue