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 {
-