diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index 43f7bc34c..a219854f7 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -130,7 +130,7 @@ export const Button = styled('button', { ctaModal: { height: '32px', verticalAlign: 'middle', - color: '$textDefault', + color: '$thTextContrast', backgroundColor: '$grayBase', fontWeight: '600', padding: '0px 12px', diff --git a/packages/web/components/elements/LogoBox.tsx b/packages/web/components/elements/LogoBox.tsx index a0afa173d..94d45e895 100644 --- a/packages/web/components/elements/LogoBox.tsx +++ b/packages/web/components/elements/LogoBox.tsx @@ -32,7 +32,7 @@ export function LogoBox(): JSX.Element { }, }} > - + ) diff --git a/packages/web/components/elements/MobileInstallHelp.tsx b/packages/web/components/elements/MobileInstallHelp.tsx index 926e6c287..b26a9bc74 100644 --- a/packages/web/components/elements/MobileInstallHelp.tsx +++ b/packages/web/components/elements/MobileInstallHelp.tsx @@ -257,7 +257,7 @@ export default function MobileInstallHelp({ height: 35, width: 35, cursor: 'pointer', - backgroundColor: '$tooltipIcons', + backgroundColor: '$labelButtonsBg', ...(selectedTooltip !== item.label && { filter: 'grayscale(1)', }), diff --git a/packages/web/components/elements/StyledText.tsx b/packages/web/components/elements/StyledText.tsx index 69858fe21..be03ade2f 100644 --- a/packages/web/components/elements/StyledText.tsx +++ b/packages/web/components/elements/StyledText.tsx @@ -85,7 +85,7 @@ const textVariants = { fontWeight: '600', fontSize: '16px', lineHeight: '1', - color: '$textDefault', + color: '$thTextContrast', }, shareHighlightModalAnnotation: { fontSize: '18px', diff --git a/packages/web/components/patterns/DropdownMenu.tsx b/packages/web/components/patterns/DropdownMenu.tsx index efa46dfcc..7f6ffe6eb 100644 --- a/packages/web/components/patterns/DropdownMenu.tsx +++ b/packages/web/components/patterns/DropdownMenu.tsx @@ -11,10 +11,8 @@ import { currentThemeName } from '../../lib/themeUpdater' import { Check } from 'phosphor-react' export type HeaderDropdownAction = - | 'apply-darker-theme' | 'apply-dark-theme' | 'apply-light-theme' - | 'apply-lighter-theme' | 'navigate-to-install' | 'navigate-to-emails' | 'navigate-to-labels' @@ -49,7 +47,7 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { css={{ background: '#FFFFFF' }} data-state={isDark ? 'unselected' : 'selected'} onClick={() => { - props.actionHandler('apply-lighter-theme') + props.actionHandler('apply-light-theme') setCurrentTheme(currentThemeName()) }} > diff --git a/packages/web/components/templates/PrimaryDropdown.tsx b/packages/web/components/templates/PrimaryDropdown.tsx index 3cfa6e774..b4cda53c8 100644 --- a/packages/web/components/templates/PrimaryDropdown.tsx +++ b/packages/web/components/templates/PrimaryDropdown.tsx @@ -234,7 +234,7 @@ function ThemeSection(props: PrimaryDropdownProps): JSX.Element { }} > { updateTheme(ThemeId.Light) }} @@ -243,9 +243,9 @@ function ThemeSection(props: PrimaryDropdownProps): JSX.Element { { - updateTheme(ThemeId.Darker) + updateTheme(ThemeId.Dark) }} > Dark diff --git a/packages/web/components/templates/PrimaryLayout.tsx b/packages/web/components/templates/PrimaryLayout.tsx index 790dd7b2c..74bd10dbd 100644 --- a/packages/web/components/templates/PrimaryLayout.tsx +++ b/packages/web/components/templates/PrimaryLayout.tsx @@ -10,6 +10,7 @@ import { KeyboardShortcutListModal } from './KeyboardShortcutListModal' import { logoutMutation } from '../../lib/networking/mutations/logoutMutation' import { setupAnalytics } from '../../lib/analytics' import { primaryCommands } from '../../lib/keyboardShortcuts/navigationShortcuts' +import { applyStoredTheme } from '../../lib/themeUpdater' type PrimaryLayoutProps = { children: ReactNode @@ -21,6 +22,8 @@ type PrimaryLayoutProps = { } export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element { + applyStoredTheme(false) + const { viewerData } = useGetViewerQuery() const router = useRouter() const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false) @@ -82,32 +85,24 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element { ) : null} - - {props.children} - {showLogoutConfirmation ? ( - setShowLogoutConfirmation(false)} - /> - ) : null} - {showKeyboardCommandsModal ? ( - setShowKeyboardCommandsModal(false)} - /> - ) : null} - + {props.children} + {showLogoutConfirmation ? ( + setShowLogoutConfirmation(false)} + /> + ) : null} + {showKeyboardCommandsModal ? ( + setShowKeyboardCommandsModal(false)} + /> + ) : null}
diff --git a/packages/web/components/templates/article/Article.tsx b/packages/web/components/templates/article/Article.tsx index c2fed68ca..8204bc7d1 100644 --- a/packages/web/components/templates/article/Article.tsx +++ b/packages/web/components/templates/article/Article.tsx @@ -1,14 +1,16 @@ import { Box } from '../../elements/LayoutPrimitives' -import { useReadingProgressAnchor } from '../../../lib/hooks/useReadingProgressAnchor' +import { + getTopOmnivoreAnchorElement, + parseDomTree, +} from '../../../lib/anchorElements' import { ScrollOffsetChangeset, useScrollWatcher, } from '../../../lib/hooks/useScrollWatcher' -import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react' +import { MutableRefObject, useEffect, useRef, useState } from 'react' import { Tweet } from 'react-twitter-widgets' import { render } from 'react-dom' import { isDarkTheme } from '../../../lib/themeUpdater' -import debounce from 'lodash/debounce' import { ArticleMutations } from '../../../lib/articleActions' export type ArticleProps = { @@ -16,6 +18,7 @@ export type ArticleProps = { content: string initialAnchorIndex: number initialReadingProgress?: number + initialReadingProgressTop?: number highlightHref: MutableRefObject articleMutations: ArticleMutations } @@ -27,41 +30,31 @@ export function Article(props: ArticleProps): JSX.Element { props.initialReadingProgress ) - const [readingAnchorIndex, setReadingAnchorIndex] = useState( - props.initialAnchorIndex - ) - const [shouldScrollToInitialPosition, setShouldScrollToInitialPosition] = useState(true) const articleContentRef = useRef(null) - useReadingProgressAnchor(articleContentRef, setReadingAnchorIndex) - - const debouncedSetReadingProgress = useMemo( - () => - debounce((readingProgress: number) => { - setReadingProgress(readingProgress) - }, 2000), - [] - ) - - // Stop the invocation of the debounced function - // after unmounting - useEffect(() => { - return () => { - debouncedSetReadingProgress.cancel() - } - }, []) + const clampToPercent = (float: number) => { + return Math.floor(Math.max(0, Math.min(100, float))) + } useEffect(() => { ;(async () => { if (!readingProgress) return + if (!articleContentRef.current) return + if (!window.document.scrollingElement) return + const anchor = getTopOmnivoreAnchorElement(articleContentRef.current) + const topPositionPercent = + window.scrollY / window.document.scrollingElement.scrollHeight + const anchorIndex = Number(anchor) + await props.articleMutations.articleReadingProgressMutation({ id: props.articleId, - // round reading progress to 100% if more than that - readingProgressPercent: readingProgress > 100 ? 100 : readingProgress, - readingProgressAnchorIndex: readingAnchorIndex, + readingProgressPercent: clampToPercent(readingProgress), + readingProgressTopPercent: clampToPercent(topPositionPercent * 100), + readingProgressAnchorIndex: + anchorIndex == Number.NaN ? undefined : anchorIndex, }) })() @@ -82,13 +75,13 @@ export function Article(props: ArticleProps): JSX.Element { useScrollWatcher((changeset: ScrollOffsetChangeset) => { if (window && window.document.scrollingElement) { - const newReadingProgress = - window.scrollY / window.document.scrollingElement.scrollHeight - const adjustedReadingProgress = - newReadingProgress > 0.92 ? 1 : newReadingProgress - debouncedSetReadingProgress(adjustedReadingProgress * 100) + const bottomProgress = + (window.scrollY + window.document.scrollingElement.clientHeight) / + window.document.scrollingElement.scrollHeight + + setReadingProgress(bottomProgress * 100) } - }, 1000) + }, 2500) // Scroll to initial anchor position useEffect(() => { @@ -101,6 +94,8 @@ export function Article(props: ArticleProps): JSX.Element { setShouldScrollToInitialPosition(false) + parseDomTree(articleContentRef.current) + // If we are scrolling to a highlight, dont scroll to read position if (props.highlightHref.current) { return diff --git a/packages/web/components/templates/article/ArticleContainer.tsx b/packages/web/components/templates/article/ArticleContainer.tsx index 93f427d7e..d4e425542 100644 --- a/packages/web/components/templates/article/ArticleContainer.tsx +++ b/packages/web/components/templates/article/ArticleContainer.tsx @@ -9,17 +9,15 @@ import { import { theme, ThemeId } from './../../tokens/stitches.config' import { HighlightsLayer } from '../../templates/article/HighlightsLayer' import { Button } from '../../elements/Button' -import { useEffect, useState, useRef, useMemo } from 'react' +import { useEffect, useState, useRef, useMemo, useCallback } from 'react' import { ReportIssuesModal } from './ReportIssuesModal' import { reportIssueMutation } from '../../../lib/networking/mutations/reportIssueMutation' -import { userPersonalizationMutation } from '../../../lib/networking/mutations/userPersonalizationMutation' import { updateTheme, updateThemeLocally } from '../../../lib/themeUpdater' import { ArticleMutations } from '../../../lib/articleActions' import { LabelChip } from '../../elements/LabelChip' import { Label } from '../../../lib/networking/fragments/labelFragment' import { Recommendation } from '../../../lib/networking/queries/useGetLibraryItemsQuery' import { Avatar } from '../../elements/Avatar' -import { usePersistedState } from '../../../lib/hooks/usePersistedState' type ArticleContainerProps = { article: ArticleAttributes @@ -125,12 +123,14 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { window.location.hash ? window.location.hash.split('#')[1] : null ) - const updateFontSize = async (newFontSize: number) => { - if (fontSize !== newFontSize) { - setFontSize(newFontSize) - await userPersonalizationMutation({ fontSize: newFontSize }) - } - } + const updateFontSize = useCallback( + (newFontSize: number) => { + if (fontSize !== newFontSize) { + setFontSize(newFontSize) + } + }, + [setFontSize] + ) useEffect(() => { updateFontSize(props.fontSize ?? 20) @@ -263,7 +263,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { ? theme.colors.readerFontHighContrast.toString() : theme.colors.readerFont.toString(), readerTableHeaderColor: theme.colors.readerTableHeader.toString(), - readerHeadersColor: theme.colors.readerHeader.toString(), + readerHeadersColor: theme.colors.readerFont.toString(), } const recommendationsWithNotes = useMemo(() => { @@ -279,7 +279,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { @@ -325,6 +329,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { fontFamily: styles.fontFamily, width: '100%', wordWrap: 'break-word', + color: styles.readerFontColor, }} > {props.article.title} @@ -361,6 +366,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { content={props.article.content} highlightHref={highlightHref} initialAnchorIndex={props.article.readingProgressAnchorIndex} + initialReadingProgressTop={props.article.readingProgressTopPercent} articleMutations={props.articleMutations} /> + + diff --git a/packages/web/components/templates/article/ShareModal.tsx b/packages/web/components/templates/article/ShareModal.tsx index 4a1bfed14..4e3dea330 100644 --- a/packages/web/components/templates/article/ShareModal.tsx +++ b/packages/web/components/templates/article/ShareModal.tsx @@ -3,13 +3,18 @@ import { ModalContent, ModalOverlay, } from '../../elements/ModalPrimitives' -import { Box, HStack, StyledLink, VStack } from '../../elements/LayoutPrimitives' +import { + Box, + HStack, + StyledLink, + VStack, +} from '../../elements/LayoutPrimitives' import { Button } from '../../elements/Button' import { StyledText } from '../../elements/StyledText' import { theme } from '../../tokens/stitches.config' import { useCopyLink } from '../../../lib/hooks/useCopyLink' import { CloseIcon } from '../../elements/images/CloseIcon' -import {OmnivoreLogoIcon} from '../../elements/images/OmnivoreNameLogo' +import { OmnivoreLogoIcon } from '../../elements/images/OmnivoreNameLogo' import { useState } from 'react' import { TooltipWrapped } from '../../elements/Tooltip' import { TwitterLogo, FacebookLogo } from 'phosphor-react' @@ -26,16 +31,14 @@ type ShareModalLayoutProps = { children: React.ReactNode } -export function ShareModalLayout( - props: ShareModalLayoutProps -): JSX.Element { +export function ShareModalLayout(props: ShareModalLayoutProps): JSX.Element { const { copyLink, isLinkCopied } = useCopyLink(props.url, props.type) - const [switchOn, setSwitchOn] = useState(false); + const [switchOn, setSwitchOn] = useState(false) const toggleSwitch = () => { - setSwitchOn(!switchOn); + setSwitchOn(!switchOn) } const iconColor = theme.colors.grayText.toString() - + return ( @@ -43,15 +46,29 @@ export function ShareModalLayout( onPointerDownOutside={(event) => { event.preventDefault() }} - css={{ overflow: 'auto', p: '0px', border: '1px solid $grayBorder', boxShadow: 'none'}} + css={{ + overflow: 'auto', + p: '0px', + border: '1px solid $grayBorder', + boxShadow: 'none', + }} > - {props.modalTitle} + + {props.modalTitle} + {props.children} Secret URL @@ -78,45 +106,83 @@ export function ShareModalLayout( tooltipContent="Link copied!" tooltipSide="top" active={isLinkCopied} - style={{background: "linear-gradient(0deg, rgba(10, 8, 6, 0.8), rgba(10, 8, 6, 0.8)), #FFFFFF;"}} - arrowStyles={{fill: "linear-gradient(0deg, rgba(10, 8, 6, 0.8), rgba(10, 8, 6, 0.8)), #FFFFFF;"}} + style={{ + background: + 'linear-gradient(0deg, rgba(10, 8, 6, 0.8), rgba(10, 8, 6, 0.8)), #FFFFFF;', + }} + arrowStyles={{ + fill: 'linear-gradient(0deg, rgba(10, 8, 6, 0.8), rgba(10, 8, 6, 0.8)), #FFFFFF;', + }} > - + - {switchOn && } + {switchOn && ( + + )} - + - + diff --git a/packages/web/components/templates/article/SkeletonArticleContainer.tsx b/packages/web/components/templates/article/SkeletonArticleContainer.tsx index 73ea03830..6873306e5 100644 --- a/packages/web/components/templates/article/SkeletonArticleContainer.tsx +++ b/packages/web/components/templates/article/SkeletonArticleContainer.tsx @@ -19,7 +19,7 @@ export function SkeletonArticleContainer( fontFamily: props.fontFamily ?? 'inter', readerFontColor: theme.colors.readerFont.toString(), readerTableHeaderColor: theme.colors.readerTableHeader.toString(), - readerHeadersColor: theme.colors.readerHeader.toString(), + readerHeadersColor: theme.colors.readerFont.toString(), } return ( diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 480c6fcdd..60bc3863b 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -34,7 +34,6 @@ import { } from '../../../lib/networking/fragments/articleFragment' import { Action, createAction, useKBar, useRegisterActions } from 'kbar' import { EditLibraryItemModal } from './EditItemModals' -import { useGetUserPreferences } from '../../../lib/networking/queries/useGetUserPreferences' import debounce from 'lodash/debounce' import { SearchItem, @@ -65,8 +64,6 @@ const debouncedFetchSearchResults = debounce((query, cb) => { }, 300) export function HomeFeedContainer(): JSX.Element { - useGetUserPreferences() - const { viewerData } = useGetViewerQuery() const router = useRouter() const { queryValue } = useKBar((state) => ({ queryValue: state.searchQuery })) diff --git a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx index 786efb148..8d341233a 100644 --- a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx +++ b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx @@ -56,6 +56,7 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element { props.setShowAddLinkModal(true)} /> + {/* This spacer pushes library content to the right of the fixed left side menu. */} @@ -186,6 +187,7 @@ function Labels(props: LibraryFilterMenuProps): JSX.Element { { window.location.href = '/settings/labels' }} @@ -203,6 +205,7 @@ type MenuPanelProps = { children: ReactNode editFunc?: () => void editTitle?: string + hideBottomBorder?: boolean } function MenuPanel(props: MenuPanelProps): JSX.Element { @@ -211,7 +214,9 @@ function MenuPanel(props: MenuPanelProps): JSX.Element { css={{ m: '0px', width: '100%', - borderBottom: '1px solid $thBorderColor', + borderBottom: props.hideBottomBorder + ? '1px solid transparent' + : '1px solid $thBorderColor', px: '15px', }} alignment="start" @@ -422,10 +427,19 @@ function AddLinkButton(props: AddLinkButtonProps): JSX.Element { <> @@ -436,6 +450,7 @@ function AddLinkButton(props: AddLinkButtonProps): JSX.Element { pr: '20px', fontSize: '14px', verticalAlign: 'center', + color: isDark ? theme.colors.thHighContrast.toString() : theme.colors.thTextContrast2.toString(), @@ -456,7 +471,6 @@ function AddLinkButton(props: AddLinkButtonProps): JSX.Element { Add Link - ) } diff --git a/packages/web/components/templates/reader/ReaderHeader.tsx b/packages/web/components/templates/reader/ReaderHeader.tsx index e3345535f..c4884c1d3 100644 --- a/packages/web/components/templates/reader/ReaderHeader.tsx +++ b/packages/web/components/templates/reader/ReaderHeader.tsx @@ -35,9 +35,12 @@ export function ReaderHeader(props: ReaderHeaderProps): JSX.Element { '@xlgDown': { height: MOBILE_HEADER_HEIGHT, pt: '0px', - bg: '$thBackground3', + bg: '$readerMargin', borderBottom: '1px solid $thBorderColor', }, + '@mdDown': { + bg: '$readerBg', + }, }} > { + let topVisibleRect: Element | undefined = undefined + const anchors = Array.from( + document.querySelectorAll(`[data-omnivore-anchor-idx]`) + ).reverse() + + for (const anchor of anchors) { + const rect = anchor.getBoundingClientRect() + if (rect.top >= 0 && rect.bottom <= articleContentElement.clientHeight) { + if ( + topVisibleRect && + topVisibleRect.getBoundingClientRect().top < rect.top + ) { + continue + } + topVisibleRect = anchor + } + } + + return topVisibleRect?.getAttribute(`data-omnivore-anchor-idx`) ?? undefined +} + +export function parseDomTree( + pageNode: HTMLDivElement | null +): HTMLDivElement[] { + if (!pageNode || pageNode.childNodes.length == 0) { + return [] + } + + const nodesToVisitStack: [HTMLDivElement] = [pageNode] + const visitedNodeList = [] + + while (nodesToVisitStack.length > 0) { + const currentNode = nodesToVisitStack.pop() + if ( + currentNode?.nodeType !== Node.ELEMENT_NODE || + // Avoiding dynamic elements from being counted as anchor-allowed elements + ANCHOR_ELEMENTS_BLOCKED_ATTRIBUTES.some((attrib) => + currentNode.hasAttribute(attrib) + ) + ) { + continue + } + visitedNodeList.push(currentNode) + ;[].slice + .call(currentNode.childNodes) + .reverse() + .forEach(function (node) { + nodesToVisitStack.push(node) + }) + } + + visitedNodeList.shift() + visitedNodeList.forEach((node, index) => { + // start from index 1, index 0 reserved for anchor unknown. + node.setAttribute('data-omnivore-anchor-idx', (index + 1).toString()) + }) + return visitedNodeList +} diff --git a/packages/web/lib/hooks/useReaderSettings.tsx b/packages/web/lib/hooks/useReaderSettings.tsx index af2293bd3..98d0430fe 100644 --- a/packages/web/lib/hooks/useReaderSettings.tsx +++ b/packages/web/lib/hooks/useReaderSettings.tsx @@ -1,16 +1,11 @@ import { useRegisterActions } from 'kbar' import { useCallback, useState } from 'react' -import { userPersonalizationMutation } from '../networking/mutations/userPersonalizationMutation' -import { - useGetUserPreferences, - UserPreferences, -} from '../networking/queries/useGetUserPreferences' +import { applyStoredTheme } from '../themeUpdater' import { usePersistedState } from './usePersistedState' const DEFAULT_FONT = 'Inter' export type ReaderSettings = { - preferencesData: UserPreferences | undefined fontSize: number lineHeight: number marginWidth: number @@ -41,12 +36,13 @@ export type ReaderSettings = { } export const useReaderSettings = (): ReaderSettings => { - const { preferencesData } = useGetUserPreferences() + applyStoredTheme(false) + const [, updateState] = useState({}) const [fontSize, setFontSize] = usePersistedState({ key: 'fontSize', - initialValue: preferencesData?.fontSize ?? 20, + initialValue: 20, }) const [lineHeight, setLineHeight] = usePersistedState({ key: 'lineHeight', @@ -76,12 +72,12 @@ export const useReaderSettings = (): ReaderSettings => { useState(false) const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false) - const updateFontSize = async (newFontSize: number) => { - setFontSize(newFontSize) - ;(async () => { - await userPersonalizationMutation({ fontSize: newFontSize }) - })() - } + const updateFontSize = useCallback( + (newFontSize: number) => { + setFontSize(newFontSize) + }, + [setFontSize] + ) // const [hideMargins, setHideMargins] = usePersistedState({ // key: `--display-hide-margins`, @@ -207,7 +203,6 @@ export const useReaderSettings = (): ReaderSettings => { ) return { - preferencesData, fontSize, lineHeight, marginWidth, diff --git a/packages/web/lib/hooks/useReadingProgressAnchor.tsx b/packages/web/lib/hooks/useReadingProgressAnchor.tsx deleted file mode 100644 index cdac3c552..000000000 --- a/packages/web/lib/hooks/useReadingProgressAnchor.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useEffect } from 'react' - -const ANCHOR_ELEMENTS_BLOCKED_ATTRIBUTES = [ - 'omnivore-highlight-id', - 'data-twitter-tweet-id', - 'data-instagram-id', -] - -export const useReadingProgressAnchor = ( - articleContentRef: React.MutableRefObject, - setReadingAnchorIndex: React.Dispatch> -): void => { - useEffect(() => { - const visitedNodeList = parseDomTree(articleContentRef.current) - const observerOptions = { - root: null, - rootMargin: '0px', - // we only track elements on becoming completely visible. - threshold: [1], - } - - function intersectionCallback(entries: IntersectionObserverEntry[]): void { - let topIntersectingElemId = 0 - let minTopElem = 100000 - entries.forEach(function (entry: IntersectionObserverEntry) { - const elem = entry.target - const elemId = elem.getAttribute('data-omnivore-anchor-idx') || '0' - - if (entry.isIntersecting && entry.intersectionRatio === 1) { - // Among all intersecting elements, find the topmost element. - if (entry.boundingClientRect.top < minTopElem) { - minTopElem = entry.boundingClientRect.top - topIntersectingElemId = parseInt(elemId) - } - } - }) - - if (topIntersectingElemId > 0) { - /* - * Intersection observer is great in finding us the last element on the - * page that becomes visible on scroll. But for better user experience - * we are interested in the topmost element visible on the page that we - * can scroll to at the top on the next article page reader view. We - * iterate in reverse over anchor elements here us to find the topmost - * visible element. - */ - let topVisibleElemId = topIntersectingElemId - while (topVisibleElemId - 1 > 0) { - const elem = document.querySelector( - `[data-omnivore-anchor-idx='${(topVisibleElemId - 1).toString()}']` - ) - if (elem) { - const rect = elem.getBoundingClientRect() - if ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= - (window.innerHeight || document.documentElement.clientHeight) && - rect.right <= - (window.innerWidth || document.documentElement.clientWidth) - ) { - /* Is Visible */ - topVisibleElemId = topVisibleElemId - 1 - } else { - break - } - } else { - // Prevents the Event loop from the eternal blocking - throw new Error('Unable to find previous intersection element!') - } - } - setReadingAnchorIndex(topVisibleElemId) - } - } - - const nodeObserver = new IntersectionObserver( - intersectionCallback, - observerOptions - ) - visitedNodeList?.forEach((elem) => { - nodeObserver.observe(elem) - }) - - return () => { - nodeObserver.disconnect() - } - }, [articleContentRef, setReadingAnchorIndex]) -} - -function parseDomTree(pageNode: HTMLDivElement | null): HTMLDivElement[] { - if (!pageNode || pageNode.childNodes.length == 0) { - return [] - } - - const nodesToVisitStack: [HTMLDivElement] = [pageNode] - const visitedNodeList = [] - - while (nodesToVisitStack.length > 0) { - const currentNode = nodesToVisitStack.pop() - if ( - currentNode?.nodeType !== Node.ELEMENT_NODE || - // Avoiding dynamic elements from being counted as anchor-allowed elements - ANCHOR_ELEMENTS_BLOCKED_ATTRIBUTES.some((attrib) => - currentNode.hasAttribute(attrib) - ) - ) { - continue - } - visitedNodeList.push(currentNode) - ;[].slice - .call(currentNode.childNodes) - .reverse() - .forEach(function (node) { - nodesToVisitStack.push(node) - }) - } - - visitedNodeList.shift() - visitedNodeList.forEach((node, index) => { - // start from index 1, index 0 reserved for anchor unknown. - node.setAttribute('data-omnivore-anchor-idx', (index + 1).toString()) - }) - return visitedNodeList -} diff --git a/packages/web/lib/hooks/useScrollWatcher.tsx b/packages/web/lib/hooks/useScrollWatcher.tsx index 19580ed43..af47493f3 100644 --- a/packages/web/lib/hooks/useScrollWatcher.tsx +++ b/packages/web/lib/hooks/useScrollWatcher.tsx @@ -37,6 +37,11 @@ export function useScrollWatcher(effect: Effect, delay: number): void { } window.addEventListener('scroll', handleScroll) - return () => window.removeEventListener('scroll', handleScroll) + return () => { + if (throttleTimeout.current) { + clearTimeout(throttleTimeout.current) + } + window.removeEventListener('scroll', handleScroll) + } }, [currentOffset, delay, effect]) } diff --git a/packages/web/lib/networking/fragments/articleFragment.ts b/packages/web/lib/networking/fragments/articleFragment.ts index a8cc4636a..3511cfbaf 100644 --- a/packages/web/lib/networking/fragments/articleFragment.ts +++ b/packages/web/lib/networking/fragments/articleFragment.ts @@ -13,6 +13,7 @@ export const articleFragment = gql` contentReader originalArticleUrl readingProgressPercent + readingProgressTopPercent readingProgressAnchorIndex slug isArchived @@ -52,6 +53,7 @@ export type ArticleFragmentData = { contentReader?: ContentReader originalArticleUrl: string readingProgressPercent: number + readingProgressTopPercent?: number readingProgressAnchorIndex: number slug: string isArchived: boolean diff --git a/packages/web/lib/networking/mutations/articleReadingProgressMutation.ts b/packages/web/lib/networking/mutations/articleReadingProgressMutation.ts index e8a81eebf..6c41950d0 100644 --- a/packages/web/lib/networking/mutations/articleReadingProgressMutation.ts +++ b/packages/web/lib/networking/mutations/articleReadingProgressMutation.ts @@ -3,8 +3,9 @@ import { gqlFetcher } from '../networkHelpers' export type ArticleReadingProgressMutationInput = { id: string - readingProgressPercent: number - readingProgressAnchorIndex: number + readingProgressPercent?: number + readingProgressTopPercent?: number + readingProgressAnchorIndex?: number } export async function articleReadingProgressMutation( diff --git a/packages/web/lib/networking/mutations/userPersonalizationMutation.ts b/packages/web/lib/networking/mutations/userPersonalizationMutation.ts deleted file mode 100644 index 5a07ade0b..000000000 --- a/packages/web/lib/networking/mutations/userPersonalizationMutation.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { gql } from 'graphql-request' -import { - UserPreferences, - SortOrder, - updateUserPreferencesCache, -} from '../queries/useGetUserPreferences' -import { gqlFetcher } from '../networkHelpers' - -type UserPersonalizationInput = { - theme?: string - fontSize?: number - fontFamily?: string - margin?: number - libraryLayoutType?: string - librarySortOrder?: SortOrder -} - -type SetUserPersonalizationResult = { - setUserPersonalization: InnerSetUserPersonalization -} - -type InnerSetUserPersonalization = { - updatedUserPersonalization?: UserPreferences -} - -export async function userPersonalizationMutation( - input: UserPersonalizationInput -): Promise { - const mutation = gql` - mutation SetUserPersonalization($input: SetUserPersonalizationInput!) { - setUserPersonalization(input: $input) { - ... on SetUserPersonalizationSuccess { - updatedUserPersonalization { - id - theme - fontSize - fontFamily - margin - libraryLayoutType - librarySortOrder - } - } - ... on SetUserPersonalizationError { - errorCodes - } - } - } - ` - - try { - const data = await gqlFetcher(mutation, { input }) - const result = data as SetUserPersonalizationResult | undefined - if (result?.setUserPersonalization?.updatedUserPersonalization) { - updateUserPreferencesCache(result.setUserPersonalization.updatedUserPersonalization) - return result.setUserPersonalization?.updatedUserPersonalization - } - return undefined - } catch { - return undefined - } -} diff --git a/packages/web/lib/networking/queries/search.tsx b/packages/web/lib/networking/queries/search.tsx index 7d3a780e3..fa6ec85d5 100644 --- a/packages/web/lib/networking/queries/search.tsx +++ b/packages/web/lib/networking/queries/search.tsx @@ -11,7 +11,7 @@ export async function searchQuery({ limit = 10, searchQuery, }: LibraryItemsQueryInput): Promise { - const query = gql` + const query = gql` query Search($after: String, $first: Int, $query: String) { search(first: $first, after: $after, query: $query) { ... on SearchSuccess { @@ -27,6 +27,7 @@ export async function searchQuery({ createdAt isArchived readingProgressPercent + readingProgressTopPercent readingProgressAnchorIndex author image @@ -71,8 +72,8 @@ export async function searchQuery({ } try { - const data = (await gqlFetcher(query, {...variables})) - return data as LibraryItemsData || undefined; + const data = await gqlFetcher(query, { ...variables }) + return (data as LibraryItemsData) || undefined } catch (error) { console.log('search error', error) return undefined diff --git a/packages/web/lib/networking/queries/useGetArticleQuery.tsx b/packages/web/lib/networking/queries/useGetArticleQuery.tsx index 6f76b05c6..df8bc08cc 100644 --- a/packages/web/lib/networking/queries/useGetArticleQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleQuery.tsx @@ -50,6 +50,7 @@ export type ArticleAttributes = { description?: string contentReader: ContentReader readingProgressPercent: number + readingProgressTopPercent?: number readingProgressAnchorIndex: number slug: string savedByViewer?: boolean diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index 0d4d7d686..c0f459b38 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -67,6 +67,7 @@ export type LibraryItemNode = { contentReader?: ContentReader originalArticleUrl: string readingProgressPercent: number + readingProgressTopPercent?: number readingProgressAnchorIndex: number slug: string isArchived: boolean @@ -149,6 +150,7 @@ export function useGetLibraryItemsQuery({ createdAt isArchived readingProgressPercent + readingProgressTopPercent readingProgressAnchorIndex author image @@ -349,11 +351,13 @@ export function useGetLibraryItemsQuery({ node: { ...item.node, readingProgressPercent: 100, + readingProgressTopPercent: 100, }, }) articleReadingProgressMutation({ id: item.node.id, readingProgressPercent: 100, + readingProgressTopPercent: 100, readingProgressAnchorIndex: 0, }) break @@ -363,11 +367,14 @@ export function useGetLibraryItemsQuery({ node: { ...item.node, readingProgressPercent: 0, + readingProgressTopPercent: 0, + readingProgressAnchorIndex: 0, }, }) articleReadingProgressMutation({ id: item.node.id, readingProgressPercent: 0, + readingProgressTopPercent: 0, readingProgressAnchorIndex: 0, }) break diff --git a/packages/web/lib/networking/queries/useGetUserPreferences.tsx b/packages/web/lib/networking/queries/useGetUserPreferences.tsx deleted file mode 100644 index 9b74a795b..000000000 --- a/packages/web/lib/networking/queries/useGetUserPreferences.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { gql } from 'graphql-request' -import useSWR, { mutate } from 'swr' -import { gqlFetcher } from '../networkHelpers' -import { applyStoredTheme, updateThemeLocally } from '../../themeUpdater' -import { ThemeId } from '../../../components/tokens/stitches.config' - -type UserPreferencesResponse = { - preferencesData?: UserPreferences - preferencesDataError?: unknown - isLoading: boolean - isValidating: boolean -} - -type QueryResponse = { - getUserPersonalization: InnerQueryReponse -} - -type InnerQueryReponse = { - userPersonalization: UserPreferences -} - -export type UserPreferences = { - id: string - theme: string - fontSize: number - fontFamily: string - margin: number - lineHeight?: number - libraryLayoutType: string - librarySortOrder?: SortOrder -} - -export type SortOrder = 'ASCENDING' | 'DESCENDING' - -const QUERY = gql` - query GetUserPersonalization { - getUserPersonalization { - ... on GetUserPersonalizationSuccess { - userPersonalization { - id - theme - margin - fontSize - fontFamily - libraryLayoutType - librarySortOrder - } - } - ... on GetUserPersonalizationError { - errorCodes - } - } - } -` - -export function updateUserPreferencesCache( - userPersonalization: UserPreferences -): void { - mutate( - QUERY, - { - getUserPersonalization: { userPersonalization }, - }, - false - ) -} - -export function useGetUserPreferences(): UserPreferencesResponse { - const currentTheme = applyStoredTheme(false) - const { data, error, isValidating } = useSWR(QUERY, gqlFetcher, { - dedupingInterval: 200000, - }) - - const preferencesData = (data as QueryResponse | undefined) - ?.getUserPersonalization.userPersonalization - - const serverThemeKey = preferencesData?.theme as ThemeId | undefined - if (!isValidating && serverThemeKey && currentTheme !== serverThemeKey) { - updateThemeLocally(serverThemeKey) - } - - return { - preferencesData, - isValidating, - preferencesDataError: error, // TODO: figure out error possibilities - isLoading: !error && !data, - } -} diff --git a/packages/web/lib/themeUpdater.tsx b/packages/web/lib/themeUpdater.tsx index 24668b03b..6a99fbe72 100644 --- a/packages/web/lib/themeUpdater.tsx +++ b/packages/web/lib/themeUpdater.tsx @@ -1,20 +1,38 @@ import { ThemeId, - lighterTheme, darkTheme, - darkerTheme, + sepiaTheme, + apolloTheme, } from '../components/tokens/stitches.config' -import { userPersonalizationMutation } from './networking/mutations/userPersonalizationMutation' const themeKey = 'theme' +// Map legacy theme names to their new equivelents +const LEGACY_THEMES: { [string: string]: string } = { + White: ThemeId.Light, + LightGray: ThemeId.Light, + Gray: ThemeId.Dark, + Darker: ThemeId.Dark, +} + export function updateTheme(themeId: string): void { if (typeof window === 'undefined') { return } updateThemeLocally(themeId) - userPersonalizationMutation({ theme: themeId }) +} + +function getTheme(themeId: string) { + switch (currentTheme()) { + case ThemeId.Dark: + return darkTheme + case ThemeId.Sepia: + return sepiaTheme + case ThemeId.Apollo: + return apolloTheme + } + return ThemeId.Light } export function updateThemeLocally(themeId: string): void { @@ -23,17 +41,13 @@ export function updateThemeLocally(themeId: string): void { } document.body.classList.remove( - lighterTheme, + ...Object.keys(LEGACY_THEMES), + sepiaTheme, darkTheme, - darkerTheme, - ThemeId.Light, - ThemeId.Dark, - ThemeId.Darker, - ThemeId.Lighter, - ThemeId.Sepia, - ThemeId.Charcoal + apolloTheme, + ...Object.keys(ThemeId) ) - document.body.classList.add(themeId) + document.body.classList.add(getTheme(themeId)) } export function currentThemeName(): string { @@ -42,17 +56,12 @@ export function currentThemeName(): string { return 'Light' case ThemeId.Dark: return 'Dark' - case ThemeId.Darker: - return 'Darker' - case ThemeId.Lighter: - return 'Lighter' case ThemeId.Sepia: return 'Sepia' - case ThemeId.Charcoal: - return 'Charcoal' - default: - return '' + case ThemeId.Apollo: + return 'Apollo' } + return 'Light' } export function currentTheme(): ThemeId | undefined { @@ -60,7 +69,16 @@ export function currentTheme(): ThemeId | undefined { return undefined } - return window.localStorage.getItem(themeKey) as ThemeId | undefined + const str = window.localStorage.getItem(themeKey) + if (str && Object.values(ThemeId).includes(str as ThemeId)) { + return str as ThemeId + } + + if (str && Object.keys(LEGACY_THEMES).includes(str)) { + return LEGACY_THEMES[str] as ThemeId + } + + return ThemeId.Light } export function applyStoredTheme(syncWithServer = true): ThemeId | undefined { @@ -72,7 +90,7 @@ export function applyStoredTheme(syncWithServer = true): ThemeId | undefined { | ThemeId | undefined if (theme && Object.values(ThemeId).includes(theme)) { - syncWithServer ? updateTheme(theme) : updateThemeLocally(theme) + updateThemeLocally(theme) } return theme } @@ -81,35 +99,3 @@ export function isDarkTheme(): boolean { const currentTheme = currentThemeName() return currentTheme === 'Dark' || currentTheme === 'Darker' } - -export function darkenTheme(): void { - switch (currentTheme()) { - case ThemeId.Dark: - updateTheme(ThemeId.Darker) - break - case ThemeId.Light: - updateTheme(ThemeId.Dark) - break - case ThemeId.Lighter: - updateTheme(ThemeId.Light) - break - default: - break - } -} - -export function lightenTheme(): void { - switch (currentTheme()) { - case ThemeId.Dark: - updateTheme(ThemeId.Light) - break - case ThemeId.Darker: - updateTheme(ThemeId.Dark) - break - case ThemeId.Light: - updateTheme(ThemeId.Lighter) - break - default: - break - } -} diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index af9c9b364..0b61ef31c 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -47,7 +47,6 @@ const PdfArticleContainerNoSSR = dynamic( export default function Home(): JSX.Element { const router = useRouter() const { cache, mutate } = useSWRConfig() - const scrollRef = useRef(null) const { slug } = router.query const [showEditModal, setShowEditModal] = useState(false) @@ -355,12 +354,12 @@ export default function Home(): JSX.Element { {article && viewerData?.me ? ( diff --git a/packages/web/pages/_app.tsx b/packages/web/pages/_app.tsx index ad224caca..ea31fe24a 100644 --- a/packages/web/pages/_app.tsx +++ b/packages/web/pages/_app.tsx @@ -24,7 +24,8 @@ import { KBarResultsComponents, searchStyle, } from '../components/elements/KBar' -import { darkenTheme, lightenTheme } from '../lib/themeUpdater' +import { updateTheme } from '../lib/themeUpdater' +import { ThemeId } from '../components/tokens/stitches.config' TopBarProgress.config({ barColors: { @@ -52,7 +53,7 @@ const generateActions = (router: NextRouter) => { shortcut: ['v', 'l'], keywords: 'light theme', priority: Priority.LOW, - perform: () => lightenTheme(), + perform: () => updateTheme(ThemeId.Light), }, { id: 'darkTheme', @@ -61,7 +62,7 @@ const generateActions = (router: NextRouter) => { shortcut: ['v', 'd'], keywords: 'dark theme', priority: Priority.LOW, - perform: () => darkenTheme(), + perform: () => updateTheme(ThemeId.Dark), }, ] diff --git a/packages/web/pages/_document.tsx b/packages/web/pages/_document.tsx index e5c96fe8b..f63ebc224 100644 --- a/packages/web/pages/_document.tsx +++ b/packages/web/pages/_document.tsx @@ -5,40 +5,6 @@ import { getCssText, globalStyles } from '../components/tokens/stitches.config' export default class Document extends NextDocument { render() { - const setUserPreferences = ` - function getCookie(cname) { - let name = cname + "="; - let ca = document.cookie.split(';'); - for(let i = 0; i < ca.length; i++) { - let c = ca[i]; - while (c.charAt(0) == ' ') { - c = c.substring(1); - } - if (c.indexOf(name) == 0) { - return c.substring(name.length, c.length); - } - } - return ""; - } - - function storeCookieInLocalStorage(key) { - let value = getCookie(key); - if (value != "") { - window.localStorage.setItem(key, value) - } - } - - storeCookieInLocalStorage("authToken") - storeCookieInLocalStorage("theme") - - var themeId = window.localStorage.getItem('theme') - - if (themeId) { - document.body.classList.remove('theme-default', 'White', 'Gray', 'LightGray', 'Dark', 'Sepia', 'Charcoal') - document.body.classList.add(themeId) - } - ` - globalStyles() return ( @@ -112,7 +78,6 @@ export default class Document extends NextDocument {
-