omnivore/packages/web/components/templates/article/ArticleContainer.tsx
Jackson Harper eb8cb3854c Do all scroll watching on the main window
This moves all scrolling from child divs to the main window. This
improves our keyboard handling, as focus will be given to the
body element, not the child div when navigating by keyboard
commands, so arrow keys and space bar will work after navigating
to the reader with the keyboard commands.
2022-06-17 15:59:51 -07:00

342 lines
12 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 [highlightReady, setHighlightReady] = useState(false)
const [highlightLocations, setHighlightLocations] = useState<
HighlightLocation[]
>([])
const updateFontSize = async (newFontSize: number) => {
if (fontSize !== newFontSize) {
setFontSize(newFontSize)
await userPersonalizationMutation({ fontSize: newFontSize })
}
}
useEffect(() => {
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 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
highlightReady={highlightReady}
highlightHref={highlightHref}
articleId={props.article.id}
content={props.article.content}
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
highlightLocations={highlightLocations}
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)}
/>
)} */}
</>
)
}