Files
omnivore/packages/web/components/templates/article/ArticleContainer.tsx
Jackson Harper da6103b444 Fix issues with merging highlights
highlightLocations can't be a prop or they wont be updated
properly when highlights are created and merged.

This fixes issues with merging highlights. It changes how we
scroll to an initial highlight in the case of deep links too.
2022-06-23 16:09:28 -07:00

323 lines
11 KiB
TypeScript

import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
import { Article } from './../../../components/templates/article/Article'
import { Box, SpanBox, VStack } from './../../elements/LayoutPrimitives'
import { StyledText } from './../../elements/StyledText'
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, useRef } from 'react'
import { ReportIssuesModal } from './ReportIssuesModal'
import { reportIssueMutation } from '../../../lib/networking/mutations/reportIssueMutation'
import { ArticleHeaderToolbar } from './ArticleHeaderToolbar'
import { userPersonalizationMutation } from '../../../lib/networking/mutations/userPersonalizationMutation'
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
labels: Label[]
articleMutations: ArticleMutations
isAppleAppEmbed: boolean
highlightBarDisabled: boolean
highlightsBaseURL: string
margin?: number
fontSize?: number
fontFamily?: string
lineHeight?: number
maxWidthPercentage?: number
highContrastFont?: boolean
showHighlightsModal: boolean
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
}
export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
const [showReportIssuesModal, setShowReportIssuesModal] = useState(false)
const [fontSize, setFontSize] = useState(props.fontSize ?? 20)
// iOS app embed can overide the original margin and line height
const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] = useState<
number | null
>(null)
const [lineHeightOverride, setLineHeightOverride] = useState<number | null>(
null
)
const [fontFamilyOverride, setFontFamilyOverride] = useState<string | null>(
null
)
const [highContrastFont, setHighContrastFont] = useState(
props.highContrastFont ?? false
)
const highlightHref = useRef(
window.location.hash ? window.location.hash.split('#')[1] : null
)
const updateFontSize = async (newFontSize: number) => {
if (fontSize !== newFontSize) {
setFontSize(newFontSize)
await userPersonalizationMutation({ fontSize: newFontSize })
}
}
useEffect(() => {
updateFontSize(props.fontSize ?? 20)
}, [props.fontSize])
// Listen for preference change events sent from host apps (ios, macos...)
useEffect(() => {
interface UpdateLineHeightEvent extends Event {
lineHeight?: number
}
const updateLineHeight = (event: UpdateLineHeightEvent) => {
const newLineHeight = event.lineHeight ?? lineHeightOverride ?? 150
if (newLineHeight >= 100 && newLineHeight <= 300) {
setLineHeightOverride(newLineHeight)
}
}
interface UpdateMaxWidthPercentageEvent extends Event {
maxWidthPercentage?: number
}
const updateMaxWidthPercentage = (event: UpdateMaxWidthPercentageEvent) => {
const newMaxWidthPercentage =
event.maxWidthPercentage ?? maxWidthPercentageOverride ?? 100
if (newMaxWidthPercentage >= 40 && newMaxWidthPercentage <= 100) {
setMaxWidthPercentageOverride(newMaxWidthPercentage)
}
}
interface UpdateFontFamilyEvent extends Event {
fontFamily?: string
}
const updateFontFamily = (event: UpdateFontFamilyEvent) => {
const newFontFamily =
event.fontFamily ?? fontFamilyOverride ?? props.fontFamily ?? 'inter'
console.log('setting font fam to', event.fontFamily)
setFontFamilyOverride(newFontFamily)
}
interface UpdateFontContrastEvent extends Event {
fontContrast?: 'high' | 'normal'
}
const handleFontContrastChange = async (event: UpdateFontContrastEvent) => {
const highContrast = event.fontContrast == 'high'
setHighContrastFont(highContrast)
}
interface UpdateFontSizeEvent extends Event {
fontSize?: number
}
const handleFontSizeChange = async (event: UpdateFontSizeEvent) => {
const newFontSize = event.fontSize ?? 18
if (newFontSize >= 10 && newFontSize <= 28) {
updateFontSize(newFontSize)
}
}
interface UpdateColorModeEvent extends Event {
isDark?: boolean
}
const updateColorMode = (event: UpdateColorModeEvent) => {
const isDark = event.isDark ?? false
updateThemeLocally(isDark ? ThemeId.Dark : ThemeId.Light)
}
const share = () => {
if (navigator.share) {
navigator.share({
title: props.article.title,
url: props.article.originalArticleUrl,
})
}
}
document.addEventListener('updateFontFamily', updateFontFamily)
document.addEventListener('updateLineHeight', updateLineHeight)
document.addEventListener(
'updateMaxWidthPercentage',
updateMaxWidthPercentage
)
document.addEventListener('updateFontSize', handleFontSizeChange)
document.addEventListener('updateColorMode', updateColorMode)
document.addEventListener(
'handleFontContrastChange',
handleFontContrastChange
)
document.addEventListener('share', share)
return () => {
document.removeEventListener('updateFontFamily', updateFontFamily)
document.removeEventListener('updateLineHeight', updateLineHeight)
document.removeEventListener(
'updateMaxWidthPercentage',
updateMaxWidthPercentage
)
document.removeEventListener('updateFontSize', handleFontSizeChange)
document.removeEventListener('updateColorMode', updateColorMode)
document.removeEventListener(
'handleFontContrastChange',
handleFontContrastChange
)
document.removeEventListener('share', share)
}
})
const styles = {
fontSize,
margin: props.margin ?? 360,
maxWidthPercentage: maxWidthPercentageOverride ?? props.maxWidthPercentage,
lineHeight: lineHeightOverride ?? props.lineHeight ?? 150,
fontFamily: fontFamilyOverride ?? props.fontFamily ?? 'inter',
readerFontColor: highContrastFont
? theme.colors.readerFontHighContrast.toString()
: theme.colors.readerFont.toString(),
readerFontColorTransparent: theme.colors.readerFontTransparent.toString(),
readerTableHeaderColor: theme.colors.readerTableHeader.toString(),
readerHeadersColor: theme.colors.readerHeader.toString(),
}
return (
<>
<Box
id="article-container"
css={{
padding: '16px',
maxWidth: `${styles.maxWidthPercentage ?? 100}%`,
background: props.isAppleAppEmbed
? 'unset'
: theme.colors.grayBg.toString(),
'--text-font-family': styles.fontFamily,
'--text-font-size': `${styles.fontSize}px`,
'--line-height': `${styles.lineHeight}%`,
'--blockquote-padding': '0.5em 1em',
'--blockquote-icon-font-size': '1.3rem',
'--figure-margin': '1.6rem auto',
'--hr-margin': '1em',
'--font-color': styles.readerFontColor,
'--font-color-transparent': styles.readerFontColorTransparent,
'--table-header-color': styles.readerTableHeaderColor,
'--headers-color': styles.readerHeadersColor,
'@sm': {
'--blockquote-padding': '1em 2em',
'--blockquote-icon-font-size': '1.7rem',
'--figure-margin': '2.6875rem auto',
'--hr-margin': '2em',
margin: `30px 0px`,
},
'@md': {
maxWidth: styles.maxWidthPercentage
? `${styles.maxWidthPercentage}%`
: 1024 - styles.margin,
},
}}
>
<VStack alignment="start" distribution="start">
<StyledText
style="boldHeadline"
data-testid="article-headline"
css={{ fontFamily: styles.fontFamily }}
>
{props.article.title}
</StyledText>
<ArticleSubtitle
rawDisplayDate={
props.article.publishedAt ?? props.article.createdAt
}
author={props.article.author}
href={props.article.url}
/>
{props.labels ? (
<SpanBox
css={{
pb: '16px',
width: '100%',
'&:empty': { display: 'none' },
}}
>
{props.labels?.map((label) => (
<LabelChip
key={label.id}
text={label.name}
color={label.color}
/>
))}
</SpanBox>
) : null}
</VStack>
<Article
articleId={props.article.id}
content={props.article.content}
highlightHref={highlightHref}
initialAnchorIndex={props.article.readingProgressAnchorIndex}
articleMutations={props.articleMutations}
/>
<Button
style="ghost"
css={{
p: 0,
my: '$4',
color: '$error',
fontSize: '$1',
'&:hover': {
opacity: 0.8,
},
}}
onClick={() => setShowReportIssuesModal(true)}
>
Report issues with this page -{'>'}
</Button>
<Box css={{ height: '100px' }} />
</Box>
<HighlightsLayer
scrollToHighlight={highlightHref}
highlights={props.article.highlights}
articleTitle={props.article.title}
articleAuthor={props.article.author ?? ''}
articleId={props.article.id}
isAppleAppEmbed={props.isAppleAppEmbed}
highlightsBaseURL={props.highlightsBaseURL}
highlightBarDisabled={props.highlightBarDisabled}
showHighlightsModal={props.showHighlightsModal}
setShowHighlightsModal={props.setShowHighlightsModal}
articleMutations={props.articleMutations}
/>
{showReportIssuesModal ? (
<ReportIssuesModal
onCommit={(comment: string) => {
reportIssueMutation({
pageId: props.article.id,
itemUrl: props.article.url,
reportTypes: ['CONTENT_DISPLAY'],
reportComment: comment,
})
}}
onOpenChange={(open: boolean) => setShowReportIssuesModal(open)}
/>
) : null}
{/* {showShareModal && (
<ShareArticleModal
url={`${webBaseURL}/${props.viewerUsername}/${props.article.slug}/highlights?r=true`}
title={props.article.title}
imageURL={props.article.image}
author={props.article.author}
publishedAt={props.article.publishedAt ?? props.article.createdAt}
description={props.article.description}
originalArticleUrl={props.article.originalArticleUrl}
onOpenChange={(open: boolean) => setShowShareModal(open)}
/>
)} */}
</>
)
}