import { Article } from './../../../components/templates/article/Article' import { Box, HStack, SpanBox, VStack } from './../../elements/LayoutPrimitives' import { StyledText } from './../../elements/StyledText' import { ArticleSubtitle, ReaderSavedInfo, } from './../../patterns/ArticleSubtitle' import { theme, ThemeId } from './../../tokens/stitches.config' import { HighlightsLayer } from '../../templates/article/HighlightsLayer' import { Button } from '../../elements/Button' import { useEffect, useState, useRef, useMemo, useCallback } from 'react' import { ReportIssuesModal } from './ReportIssuesModal' import { reportIssueMutation } from '../../../lib/networking/mutations/reportIssueMutation' import { updateTheme, updateThemeLocally } from '../../../lib/themeUpdater' import { ArticleMutations } from '../../../lib/articleActions' import { LabelChip } from '../../elements/LabelChip' import { Label } from '../../../lib/networking/fragments/labelFragment' import { ArticleAttributes, Recommendation, TextDirection, useRestoreItem, useUpdateItemReadStatus, } from '../../../lib/networking/library_items/useLibraryItems' import { Avatar } from '../../elements/Avatar' import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' import { State } from '../../../lib/networking/fragments/articleFragment' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' type ArticleContainerProps = { viewer: UserBasicData article: ArticleAttributes labels: Label[] articleMutations: ArticleMutations isAppleAppEmbed: boolean highlightBarDisabled: boolean margin?: number fontSize?: number fontFamily?: string lineHeight?: number maxWidthPercentage?: number highContrastText?: boolean showHighlightsModal: boolean highlightOnRelease?: boolean justifyText?: boolean textDirection?: TextDirection setShowHighlightsModal: React.Dispatch> } type RecommendationCommentsProps = { recommendationsWithNotes: Recommendation[] } export interface UpdateTitleEvent extends Event { title?: string } const RecommendationComments = ( props: RecommendationCommentsProps ): JSX.Element => { return ( Comments{' '}  {` ${props.recommendationsWithNotes.length}`} {props.recommendationsWithNotes.map((item) => ( {item.note} ))} ) } export function ArticleContainer(props: ArticleContainerProps): JSX.Element { const [labels, setLabels] = useState(props.labels) const [title, setTitle] = useState(undefined) const [showReportIssuesModal, setShowReportIssuesModal] = useState(false) const [fontSize, setFontSize] = useState(props.fontSize ?? 20) const [highlightOnRelease, setHighlightOnRelease] = useState( props.highlightOnRelease ) // iOS app embed can overide the original margin and line height const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] = useState< number | null >(null) const [lineHeightOverride, setLineHeightOverride] = useState( null ) const [fontFamilyOverride, setFontFamilyOverride] = useState( null ) const [highContrastTextOverride, setHighContrastTextOverride] = useState< boolean | undefined >(undefined) const [justifyTextOverride, setJustifyTextOverride] = useState< boolean | undefined >(undefined) const highlightHref = useRef( window.location.hash ? window.location.hash.split('#')[1] : null ) const [textDirection, setTextDirection] = useState( props.textDirection ?? 'LTR' ) const restoreItem = useRestoreItem() const updateFontSize = useCallback( (newFontSize: number) => { setFontSize(newFontSize) }, [setFontSize] ) useEffect(() => { setLabels(props.labels) updateFontSize(props.fontSize ?? 20) }, [props.labels, props.fontSize, updateFontSize]) // 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 UpdateHighlightModeEvent extends Event { enableHighlightOnRelease?: string } const updateHighlightMode = (event: UpdateHighlightModeEvent) => { const isEnabled = event.enableHighlightOnRelease === 'on' setHighlightOnRelease(isEnabled) } interface UpdateTextDirectionEvent extends Event { textDirection: TextDirection } const handleUpdateTextDirection = (event: UpdateTextDirectionEvent) => { setTextDirection(event.textDirection) } 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' setFontFamilyOverride(newFontFamily) } interface UpdateFontContrastEvent extends Event { fontContrast?: 'high' | 'normal' } const handleFontContrastChange = async (event: UpdateFontContrastEvent) => { const highContrast = event.fontContrast == 'high' setHighContrastTextOverride(highContrast) } interface UpdateFontSizeEvent extends Event { fontSize?: number } const handleFontSizeChange = async (event: UpdateFontSizeEvent) => { const newFontSize = event.fontSize ?? 18 if (newFontSize >= 10 && newFontSize <= 48) { updateFontSize(newFontSize) } } interface UpdateThemeEvent extends Event { themeName?: string } const handleThemeChange = async (event: UpdateThemeEvent) => { const newTheme = event.themeName if (newTheme) { updateTheme(newTheme) } } interface UpdateColorModeEvent extends Event { isDark?: string } const updateColorMode = (event: UpdateColorModeEvent) => { const isDark = event.isDark ?? 'false' updateThemeLocally(isDark === 'true' ? ThemeId.Dark : ThemeId.Light) } interface UpdateJustifyText extends Event { justifyText?: boolean } const updateJustifyText = (event: UpdateJustifyText) => { setJustifyTextOverride(event.justifyText ?? false) } interface UpdateLabelsEvent extends Event { labels?: Label[] } const handleUpdateLabels = (event: UpdateLabelsEvent) => { setLabels(event.labels ?? []) } const handleUpdateTitle = (event: UpdateTitleEvent) => { if (event.title) { setTitle(event.title) } } const share = () => { if (navigator.share) { navigator.share({ title: (title ?? props.article.title) + '\n', text: (title ?? props.article.title) + '\n', url: props.article.originalArticleUrl, }) } } const saveReadPosition = () => { console.log('saving read position') } document.addEventListener('saveReadPosition', saveReadPosition) document.addEventListener('updateFontFamily', updateFontFamily) document.addEventListener('updateLineHeight', updateLineHeight) document.addEventListener( 'updateMaxWidthPercentage', updateMaxWidthPercentage ) document.addEventListener('updateTheme', handleThemeChange) document.addEventListener('updateFontSize', handleFontSizeChange) document.addEventListener('updateColorMode', updateColorMode) document.addEventListener( 'handleFontContrastChange', handleFontContrastChange ) document.addEventListener('updateJustifyText', updateJustifyText) document.addEventListener('updateTitle', handleUpdateTitle) document.addEventListener('updateLabels', handleUpdateLabels) document.addEventListener('share', share) document.addEventListener( 'handleAutoHighlightModeChange', updateHighlightMode ) return () => { document.removeEventListener('updateFontFamily', updateFontFamily) document.removeEventListener('updateLineHeight', updateLineHeight) document.removeEventListener( 'updateMaxWidthPercentage', updateMaxWidthPercentage ) document.removeEventListener('updateTheme', handleThemeChange) document.removeEventListener('updateFontSize', handleFontSizeChange) document.removeEventListener('updateColorMode', updateColorMode) document.removeEventListener( 'handleFontContrastChange', handleFontContrastChange ) document.removeEventListener('updateJustifyText', updateJustifyText) document.removeEventListener('updateTitle', handleUpdateTitle) document.removeEventListener('updateLabels', handleUpdateLabels) document.removeEventListener('share', share) document.removeEventListener( 'handleAutoHighlightModeChange', updateHighlightMode ) document.removeEventListener('saveReadPosition', saveReadPosition) } }) const textColorValue = (isHighContrast: boolean) => { return isHighContrast ? theme.colors.readerFontHighContrast.toString() : theme.colors.readerFont.toString() } const justifyTextValue = (isJustified: boolean) => { return isJustified ? 'justify' : 'start' } const appliedFont = (name: string | undefined | null) => { if (name === 'System Default') { return 'unset' } return name } const styles = { fontSize, margin: props.margin ?? 360, maxWidthPercentage: maxWidthPercentageOverride ?? props.maxWidthPercentage, lineHeight: lineHeightOverride ?? props.lineHeight ?? 150, fontFamily: appliedFont(fontFamilyOverride) ?? appliedFont(props.fontFamily) ?? 'inter', readerFontColor: highContrastTextOverride != undefined ? textColorValue(highContrastTextOverride) : textColorValue(props.highContrastText ?? false), readerTableHeaderColor: theme.colors.readerTableHeader.toString(), readerHeadersColor: theme.colors.readerFont.toString(), } const maxWidthStyles = { default: styles.maxWidthPercentage ? `${styles.maxWidthPercentage}%` : 1024 - styles.margin, small: styles.maxWidthPercentage ? `${styles.maxWidthPercentage}%` : `${120 - Math.round((styles.margin * 10) / 100)}%`, } const recommendationsWithNotes = useMemo(() => { return ( props.article.recommendations?.filter((recommendation) => { return recommendation.note }) ?? [] ) }, [props.article.recommendations]) console.log('props.article', props.article) return ( <> {title ?? props.article.title} {labels ? ( {labels?.map((label) => ( ))} ) : null} {recommendationsWithNotes.length > 0 && ( )} {!props.isAppleAppEmbed && props.article && props.article.state == State.DELETED && ( This item has been deleted. To access all the highlights and content you can restore it. If you do not restore this item it will be removed from your trash after two weeks or when you manually empty your trash. )} {/* {userHasFeature(props.viewer, 'ai-summaries') && ( )} */}
{showReportIssuesModal ? ( { reportIssueMutation({ pageId: props.article.id, itemUrl: props.article.url, reportTypes: ['CONTENT_DISPLAY'], reportComment: comment, }) }} onOpenChange={(open: boolean) => setShowReportIssuesModal(open)} /> ) : null} ) }