Merge pull request #3530 from omnivore-app/fix/web-header-fixes

This commit is contained in:
Jackson Harper
2024-02-14 18:14:09 +08:00
committed by GitHub
46 changed files with 562 additions and 370 deletions

View File

@ -1,4 +1,4 @@
import { LIBRARY_LEFT_MENU_WIDTH } from '../templates/homeFeed/LibraryFilterMenu'
import { LIBRARY_LEFT_MENU_WIDTH } from '../templates/navMenu/LibraryMenu'
import { theme } from '../tokens/stitches.config'
import { OmnivoreFullLogo } from './images/OmnivoreFullLogo'
import { OmnivoreNameLogo } from './images/OmnivoreNameLogo'

View File

@ -192,6 +192,8 @@ const textVariants = {
fontSize: '12px',
lineHeight: '20px',
color: '$thTextSubtle2',
marginBlockStart: '0',
marginTop: '10px',
},
error: {
color: '$error',

View File

@ -14,7 +14,7 @@ import {
FLAIR_ICON_NAMES,
} from './LibraryCardStyles'
import { sortedLabels } from '../../../lib/labelsSort'
import { LIBRARY_LEFT_MENU_WIDTH } from '../../templates/homeFeed/LibraryFilterMenu'
import { LIBRARY_LEFT_MENU_WIDTH } from '../../templates/navMenu/LibraryMenu'
import { LibraryHoverActions } from './LibraryHoverActions'
import {
useHover,

View File

@ -6,7 +6,7 @@ type LoadingViewProps = {
}
export function LoadingView(props: LoadingViewProps): JSX.Element {
applyStoredTheme(false)
applyStoredTheme()
return (
<VStack

View File

@ -31,9 +31,6 @@ export function SettingsHeader(props: HeaderProps): JSX.Element {
}}
>
<LogoBox />
<HStack css={{ ml: 'auto' }}>
<PrimaryDropdown showThemeSection={true} />
</HStack>
</HStack>
</nav>
)

View File

@ -2,7 +2,7 @@ import { useRouter } from 'next/router'
import { Moon, Sun } from 'phosphor-react'
import { ReactNode, useCallback, useState } from 'react'
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
import { currentTheme, updateTheme } from '../../lib/themeUpdater'
import { getCurrentLocalTheme, updateTheme } from '../../lib/themeUpdater'
import { Avatar } from '../elements/Avatar'
import { AvatarDropdown } from '../elements/AvatarDropdown'
import {
@ -271,7 +271,7 @@ export const StyledToggleButton = styled('button', {
})
function ThemeSection(props: PrimaryDropdownProps): JSX.Element {
const [displayTheme, setDisplayTheme] = useState(currentTheme())
const [displayTheme, setDisplayTheme] = useState(getCurrentLocalTheme())
const doUpdateTheme = useCallback(
(newTheme: ThemeId) => {
@ -315,7 +315,7 @@ function ThemeSection(props: PrimaryDropdownProps): JSX.Element {
}}
>
<StyledToggleButton
data-state={currentTheme() != ThemeId.Dark ? 'on' : 'off'}
data-state={getCurrentLocalTheme() != ThemeId.Dark ? 'on' : 'off'}
onClick={() => {
doUpdateTheme(ThemeId.Light)
}}
@ -324,7 +324,7 @@ function ThemeSection(props: PrimaryDropdownProps): JSX.Element {
<Sun size={15} color={theme.colors.thTextContrast2.toString()} />
</StyledToggleButton>
<StyledToggleButton
data-state={currentTheme() == ThemeId.Dark ? 'on' : 'off'}
data-state={getCurrentLocalTheme() == ThemeId.Dark ? 'on' : 'off'}
onClick={() => {
doUpdateTheme(ThemeId.Dark)
}}

View File

@ -12,6 +12,7 @@ import { deinitAnalytics, setupAnalytics } from '../../lib/analytics'
import { primaryCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
import { applyStoredTheme } from '../../lib/themeUpdater'
import { logout } from '../../lib/logout'
import { useApplyLocalTheme } from '../../lib/hooks/useApplyLocalTheme'
type PrimaryLayoutProps = {
children: ReactNode
@ -23,7 +24,7 @@ type PrimaryLayoutProps = {
}
export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
applyStoredTheme(false)
useApplyLocalTheme()
const { viewerData } = useGetViewerQuery()
const router = useRouter()

View File

@ -11,7 +11,7 @@ import { KeyboardShortcutListModal } from './KeyboardShortcutListModal'
import { PageMetaData } from '../patterns/PageMetaData'
import { DEFAULT_HEADER_HEIGHT } from './homeFeed/HeaderSpacer'
import { logout } from '../../lib/logout'
import { SettingsMenu } from './SettingsMenu'
import { SettingsMenu } from './navMenu/SettingsMenu'
type SettingsLayoutProps = {
title?: string
@ -26,7 +26,7 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element {
useState(false)
useKeyboardShortcuts(navigationCommands(router))
applyStoredTheme(false)
applyStoredTheme()
const showLogout = useCallback(() => {
setShowLogoutConfirmation(true)

View File

@ -3,7 +3,11 @@ import { Box, VStack } from '../../elements/LayoutPrimitives'
import { v4 as uuidv4 } from 'uuid'
import { nanoid } from 'nanoid'
import { useState, useEffect, useRef, useMemo } from 'react'
import { currentTheme, getTheme, isDarkTheme } from '../../../lib/themeUpdater'
import {
getCurrentLocalTheme,
getTheme,
isDarkTheme,
} from '../../../lib/themeUpdater'
import PSPDFKit from 'pspdfkit'
import { Instance, HighlightAnnotation, List, Annotation, Rect } from 'pspdfkit'
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
@ -38,15 +42,13 @@ type EpubPatch = {
export default function EpubContainer(props: EpubContainerProps): JSX.Element {
const epubRef = useRef<HTMLDivElement | null>(null)
const renditionRef = useRef<Rendition | undefined>(undefined)
const [shareTarget, setShareTarget] = useState<Highlight | undefined>(
undefined
)
const [shareTarget, setShareTarget] =
useState<Highlight | undefined>(undefined)
const [touchStart, setTouchStart] = useState(0)
const [notebookKey, setNotebookKey] = useState<string>(uuidv4())
const [noteTarget, setNoteTarget] = useState<Highlight | undefined>(undefined)
const [noteTargetPageIndex, setNoteTargetPageIndex] = useState<
number | undefined
>(undefined)
const [noteTargetPageIndex, setNoteTargetPageIndex] =
useState<number | undefined>(undefined)
const highlightsRef = useRef<Highlight[]>([])
const book = useMemo(() => {
@ -102,7 +104,7 @@ export default function EpubContainer(props: EpubContainerProps): JSX.Element {
}
})
const themeId = currentTheme()
const themeId = getCurrentLocalTheme()
if (themeId) {
const readerTheme = getTheme(themeId)
renditionRef.current.themes.override(

View File

@ -13,10 +13,17 @@ import { TickedRangeSlider } from '../../elements/TickedRangeSlider'
import { showSuccessToast } from '../../../lib/toastHelpers'
import { ReaderSettings } from '../../../lib/hooks/useReaderSettings'
import { useCallback, useState } from 'react'
import { currentThemeName, updateTheme } from '../../../lib/themeUpdater'
import {
getCurrentLocalTheme,
currentThemeName,
updateTheme,
} from '../../../lib/themeUpdater'
import { LineHeightIncreaseIcon } from '../../elements/images/LineHeightIncreaseIconProps'
import { LineHeightDecreaseIcon } from '../../elements/images/LineHeightDecreaseIcon'
import * as Switch from '@radix-ui/react-switch'
import { Checkbox } from '@radix-ui/react-checkbox'
import { useCurrentTheme } from '../../../lib/hooks/useCurrentTheme'
import { useDarkModeListener } from '../../../lib/hooks/useDarkModeListener'
type ReaderSettingsProps = {
readerSettings: ReaderSettings
@ -564,7 +571,9 @@ function LayoutControls(props: LayoutControlsProps): JSX.Element {
}
function ThemeSelector(): JSX.Element {
const [currentTheme, setCurrentTheme] = useState(currentThemeName())
const isDarkMode = useDarkModeListener()
const { currentTheme, setCurrentTheme, resetSystemTheme } = useCurrentTheme()
return (
<VStack
css={{
@ -575,7 +584,50 @@ function ThemeSelector(): JSX.Element {
height: '100%',
}}
>
<StyledText style="displaySettingsLabel">Themes</StyledText>
<HStack
distribution="start"
css={{
width: '100%',
}}
>
<StyledText style="displaySettingsLabel">Themes</StyledText>
<HStack
alignment="center"
distribution="center"
css={{ ml: 'auto', gap: '5px', mt: '10px', cursor: 'pointer' }}
onClick={() => {
console.log('clicked use system')
updateTheme(ThemeId.System)
}}
>
<Label
htmlFor="auto-checkbox"
css={{
fontFamily: '$display',
fontWeight: '500',
fontSize: '12px',
lineHeight: '20px',
color: '$thTextSubtle2',
}}
>
Auto
</Label>
<input
type="checkbox"
id="auto-checkbox"
checked={currentTheme == ThemeId.System}
onChange={(event) => {
if (event.target.checked) {
setCurrentTheme(ThemeId.System)
} else {
resetSystemTheme()
}
event.stopPropagation()
}}
></input>
</HStack>
</HStack>
<HStack
distribution="start"
css={{
@ -605,8 +657,7 @@ function ThemeSelector(): JSX.Element {
}}
data-state={currentTheme == ThemeId.Light ? 'selected' : 'unselected'}
onClick={() => {
updateTheme(ThemeId.Light)
setCurrentTheme(currentThemeName())
setCurrentTheme(ThemeId.Light)
}}
>
{currentTheme == ThemeId.Light && (
@ -635,8 +686,7 @@ function ThemeSelector(): JSX.Element {
}}
data-state={currentTheme == ThemeId.Dark ? 'selected' : 'unselected'}
onClick={() => {
updateTheme(ThemeId.Dark)
setCurrentTheme(currentThemeName())
setCurrentTheme(ThemeId.Dark)
}}
>
{currentTheme == ThemeId.Dark && <Check color="#F9D354" size={20} />}
@ -663,8 +713,7 @@ function ThemeSelector(): JSX.Element {
}}
data-state={currentTheme == ThemeId.Sepia ? 'selected' : 'unselected'}
onClick={() => {
updateTheme(ThemeId.Sepia)
setCurrentTheme(currentThemeName())
setCurrentTheme(ThemeId.Sepia)
}}
>
{currentTheme == ThemeId.Sepia && <Check color="#6A6968" size={20} />}
@ -693,8 +742,7 @@ function ThemeSelector(): JSX.Element {
currentTheme == ThemeId.Apollo ? 'selected' : 'unselected'
}
onClick={() => {
updateTheme(ThemeId.Apollo)
setCurrentTheme(currentThemeName())
setCurrentTheme(ThemeId.Apollo)
}}
>
{currentTheme == ThemeId.Apollo && (

View File

@ -1,6 +1,6 @@
import { Box } from '../../elements/LayoutPrimitives'
import { useMemo } from 'react'
import { LIBRARY_LEFT_MENU_WIDTH } from './LibraryFilterMenu'
import { LIBRARY_LEFT_MENU_WIDTH } from '../navMenu/LibraryMenu'
import { LayoutType } from './HomeFeedContainer'
import { SuggestionBox, SuggestionAction } from '../../elements/SuggestionBox'

View File

@ -36,7 +36,7 @@ import { AddLinkModal } from './AddLinkModal'
import { EditLibraryItemModal } from './EditItemModals'
import { EmptyLibrary } from './EmptyLibrary'
import { HighlightItemsLayout } from './HighlightsLayout'
import { LibraryFilterMenu } from './LibraryFilterMenu'
import { LibraryFilterMenu } from '../navMenu/LibraryMenu'
import {
LibraryHeader,
MultiSelectMode,
@ -96,13 +96,11 @@ export function HomeFeedContainer(): JSX.Element {
const gridContainerRef = useRef<HTMLDivElement>(null)
const [labelsTarget, setLabelsTarget] = useState<LibraryItem | undefined>(
undefined
)
const [labelsTarget, setLabelsTarget] =
useState<LibraryItem | undefined>(undefined)
const [notebookTarget, setNotebookTarget] = useState<LibraryItem | undefined>(
undefined
)
const [notebookTarget, setNotebookTarget] =
useState<LibraryItem | undefined>(undefined)
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
const [showEditTitleModal, setShowEditTitleModal] = useState(false)

View File

@ -16,7 +16,7 @@ import {
import { LayoutType } from './HomeFeedContainer'
import { OmnivoreSmallLogo } from '../../elements/images/OmnivoreNameLogo'
import { DEFAULT_HEADER_HEIGHT, HeaderSpacer } from './HeaderSpacer'
import { LIBRARY_LEFT_MENU_WIDTH } from '../../templates/homeFeed/LibraryFilterMenu'
import { LIBRARY_LEFT_MENU_WIDTH } from '../navMenu/LibraryMenu'
import { CardCheckbox } from '../../patterns/LibraryCards/LibraryCardStyles'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
@ -36,7 +36,6 @@ import { HeaderCheckboxIcon } from '../../elements/icons/HeaderCheckboxIcon'
import { HeaderSearchIcon } from '../../elements/icons/HeaderSearchIcon'
import { HeaderToggleGridIcon } from '../../elements/icons/HeaderToggleGridIcon'
import { HeaderToggleListIcon } from '../../elements/icons/HeaderToggleListIcon'
import useWindowDimensions from '../../../lib/hooks/useGetWindowDimensions'
export type MultiSelectMode = 'off' | 'none' | 'some' | 'visible' | 'search'
@ -124,20 +123,6 @@ export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
}
function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
const dimensions = useWindowDimensions()
const [showSearchBar, setShowSearchBar] = useState(false)
const [pinnedSearches, setPinnedSearches] = usePersistedState<
PinnedSearch[] | null
>({
key: `--library-pinned-searches`,
initialValue: [],
isSessionStorage: false,
})
const isWideWindow = useMemo(() => {
return dimensions.width >= 480
}, [dimensions])
return (
<HStack
alignment="center"
@ -153,78 +138,81 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
<MultiSelectControls {...props} />
</HStack>
) : (
<>
{(!showSearchBar || isWideWindow) && (
<>
<SpanBox
css={{
display: 'none',
'@mdDown': { display: 'flex' },
}}
>
<MenuHeaderButton {...props} />
</SpanBox>
<Button
title="Select multiple"
style="plainIcon"
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
onClick={(e) => {
props.setMultiSelectMode('visible')
e.preventDefault()
}}
>
<HeaderCheckboxIcon />
</Button>
</>
)}
{showSearchBar ? (
<SearchBox {...props} setShowSearchBar={setShowSearchBar} />
) : (
<Button
title="search"
style="plainIcon"
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
onClick={(e) => {
setShowSearchBar(true)
e.preventDefault()
}}
>
<HeaderSearchIcon />
</Button>
)}
<Button
title={
props.layout == 'GRID_LAYOUT'
? 'Switch to list layout'
: 'Switch to grid layout'
}
style="plainIcon"
css={{
display: 'flex',
marginLeft: 'auto',
'&:hover': { opacity: '1.0' },
}}
onClick={(e) => {
props.updateLayout(
props.layout == 'GRID_LAYOUT' ? 'LIST_LAYOUT' : 'GRID_LAYOUT'
)
e.preventDefault()
}}
>
{props.layout == 'LIST_LAYOUT' ? (
<HeaderToggleGridIcon />
) : (
<HeaderToggleListIcon />
)}
</Button>
</>
<HeaderControls {...props} />
)}
</HStack>
)
}
const HeaderControls = (props: LibraryHeaderProps): JSX.Element => {
const [showSearchBar, setShowSearchBar] = useState(false)
return (
<>
<SpanBox
css={{
display: 'none',
'@mdDown': { display: 'flex' },
}}
>
<MenuHeaderButton {...props} />
</SpanBox>
<Button
title="Select multiple"
style="plainIcon"
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
onClick={(e) => {
props.setMultiSelectMode('visible')
e.preventDefault()
}}
>
<HeaderCheckboxIcon />
</Button>
{showSearchBar ? (
<SearchBox {...props} setShowSearchBar={setShowSearchBar} />
) : (
<Button
title="search"
style="plainIcon"
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
onClick={(e) => {
setShowSearchBar(true)
e.preventDefault()
}}
>
<HeaderSearchIcon />
</Button>
)}
<Button
title={
props.layout == 'GRID_LAYOUT'
? 'Switch to list layout'
: 'Switch to grid layout'
}
style="plainIcon"
css={{
display: 'flex',
marginLeft: 'auto',
'&:hover': { opacity: '1.0' },
}}
onClick={(e) => {
props.updateLayout(
props.layout == 'GRID_LAYOUT' ? 'LIST_LAYOUT' : 'GRID_LAYOUT'
)
e.preventDefault()
}}
>
{props.layout == 'LIST_LAYOUT' ? (
<HeaderToggleGridIcon />
) : (
<HeaderToggleListIcon />
)}
</Button>
</>
)
}
type MenuHeaderButtonProps = {
showFilterMenu: boolean
setShowFilterMenu: (show: boolean) => void

View File

@ -0,0 +1,50 @@
import { HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { SplitButton } from '../../elements/SplitButton'
import { PrimaryDropdown } from '../PrimaryDropdown'
import { LIBRARY_LEFT_MENU_WIDTH } from './LibraryMenu'
type NavMenuFooterProps = {
setShowAddLinkModal?: (show: true) => void
}
export const NavMenuFooter = (props: NavMenuFooterProps): JSX.Element => {
return (
<HStack
css={{
gap: '10px',
height: '65px',
position: 'fixed',
bottom: '0%',
alignItems: 'center',
backgroundColor: '$thBackground2',
width: LIBRARY_LEFT_MENU_WIDTH,
overflowY: 'auto',
overflowX: 'hidden',
'&::-webkit-scrollbar': {
display: 'none',
},
'@mdDown': {
width: '100%',
},
}}
>
<PrimaryDropdown showThemeSection={true} />
<SpanBox
css={{
marginLeft: 'auto',
marginRight: '15px',
}}
>
{props.setShowAddLinkModal && (
<SplitButton
title="Add"
setShowLinkMode={() => {
props.setShowAddLinkModal && props.setShowAddLinkModal(true)
}}
/>
)}
</SpanBox>
</HStack>
)
}

View File

@ -22,6 +22,7 @@ import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon'
import { SplitButton } from '../../elements/SplitButton'
import { AvatarDropdown } from '../../elements/AvatarDropdown'
import { PrimaryDropdown } from '../PrimaryDropdown'
import { NavMenuFooter } from './Footer'
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
@ -122,7 +123,7 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
<SavedSearches {...props} savedSearches={savedSearches} />
<Subscriptions {...props} subscriptions={subscriptions} />
<Labels {...props} labels={labels} />
<Footer {...props} />
<NavMenuFooter {...props} />
<Box css={{ height: '250px ' }} />
</Box>
{/* This spacer pushes library content to the right of
@ -610,41 +611,3 @@ function EditButton(props: EditButtonProps): JSX.Element {
</Link>
)
}
const Footer = (props: LibraryFilterMenuProps): JSX.Element => {
return (
<HStack
css={{
gap: '10px',
height: '65px',
position: 'fixed',
bottom: '0%',
alignItems: 'center',
backgroundColor: '$thBackground2',
width: LIBRARY_LEFT_MENU_WIDTH,
overflowY: 'auto',
overflowX: 'hidden',
'&::-webkit-scrollbar': {
display: 'none',
},
'@mdDown': {
width: '100%',
},
}}
>
<PrimaryDropdown showThemeSection={true} />
<SpanBox
css={{
marginLeft: 'auto',
marginRight: '15px',
}}
>
<SplitButton
title="Add"
setShowLinkMode={() => props.setShowAddLinkModal(true)}
/>
</SpanBox>
</HStack>
)
}

View File

@ -1,12 +1,13 @@
import { useMemo } from 'react'
import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
import { LIBRARY_LEFT_MENU_WIDTH } from './homeFeed/LibraryFilterMenu'
import { LogoBox } from '../elements/LogoBox'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { LIBRARY_LEFT_MENU_WIDTH } from './LibraryMenu'
import { LogoBox } from '../../elements/LogoBox'
import Link from 'next/link'
import { styled, theme } from '../tokens/stitches.config'
import { Button } from '../elements/Button'
import { styled, theme } from '../../tokens/stitches.config'
import { Button } from '../../elements/Button'
import { ArrowSquareUpRight } from 'phosphor-react'
import { useRouter } from 'next/router'
import { NavMenuFooter } from './Footer'
const HorizontalDivider = styled(SpanBox, {
width: '100%',
@ -173,6 +174,7 @@ export function SettingsMenu(): JSX.Element {
destination="https://docs.omnivore.app"
title="Documentation"
/>
<NavMenuFooter />
</VStack>
</Box>
{/* This spacer pushes library content to the right of

View File

@ -7,6 +7,7 @@ export enum ThemeId {
Sepia = 'Sepia',
Apollo = 'Apollo',
Black = 'Black',
System = 'System',
}
export const { styled, css, theme, getCssText, globalCss, keyframes, config } =

View File

@ -0,0 +1,11 @@
import { useEffect } from 'react'
import { applyStoredTheme } from '../themeUpdater'
import { useDarkModeListener } from './useDarkModeListener'
export function useApplyLocalTheme() {
const isDark = useDarkModeListener()
useEffect(() => {
applyStoredTheme()
}, [isDark])
}

View File

@ -0,0 +1,97 @@
import { useCallback, useMemo } from 'react'
import { usePersistedState } from './usePersistedState'
import { getCurrentLocalTheme, updateThemeLocally } from '../themeUpdater'
import { useDarkModeListener } from './useDarkModeListener'
const themeKey = 'currentTheme'
const preferredDarkThemeKey = 'preferredDarkThemeKey'
const preferredLightThemeKey = 'preferredLightThemeKey'
export function useCurrentTheme() {
const isDarkMode = useDarkModeListener()
const [currentThemeInternal, setCurrentThemeInternal] = usePersistedState<
string | undefined
>({
key: themeKey,
initialValue: getCurrentLocalTheme(),
})
const [preferredLightTheme, setPreferredLightTheme] =
usePersistedState<string>({
key: preferredLightThemeKey,
initialValue: 'Light',
})
const [preferredDarkTheme, setPreferredDarkTheme] = usePersistedState<string>(
{
key: preferredDarkThemeKey,
initialValue: 'Dark',
}
)
const isDarkTheme = (themeId: string): boolean => {
if (
themeId === 'Dark' ||
themeId === 'Darker' ||
themeId === 'Apollo' ||
themeId == 'Black'
) {
return true
}
return false
}
const isLightTheme = (themeId: string): boolean => {
if (themeId === 'Sepia' || themeId == 'Light') {
return true
}
return false
}
const currentTheme = useMemo(() => {
return currentThemeInternal
}, [currentThemeInternal])
const setCurrentTheme = useCallback(
(themeId: string) => {
if (isDarkTheme(themeId)) {
setPreferredDarkTheme(themeId)
}
if (isLightTheme(themeId)) {
setPreferredLightTheme(themeId)
}
if (themeId == 'System') {
const current = currentThemeInternal
if (current && isDarkTheme(current)) {
setPreferredDarkTheme(current)
}
if (current && isLightTheme(current)) {
setPreferredLightTheme(current)
}
}
setCurrentThemeInternal(themeId)
updateThemeLocally(themeId)
},
[setCurrentThemeInternal]
)
// This is used when the user disables "System" theme
const resetSystemTheme = useCallback(() => {
if (isDarkMode) {
setCurrentThemeInternal(preferredDarkTheme ?? 'Dark')
} else {
setCurrentThemeInternal(preferredLightTheme ?? 'Light')
}
}, [
isDarkMode,
preferredDarkTheme,
preferredLightTheme,
setCurrentThemeInternal,
])
return {
currentTheme,
setCurrentTheme,
resetSystemTheme,
}
}

View File

@ -14,6 +14,8 @@ export default function useWindowDimensions() {
})
useEffect(() => {
setWindowDimensions(getWindowDimensions())
function handleResize() {
setWindowDimensions(getWindowDimensions())
}

View File

@ -37,7 +37,7 @@ export type ReaderSettings = {
}
export const useReaderSettings = (): ReaderSettings => {
applyStoredTheme(false)
applyStoredTheme()
const [, updateState] = useState({})

View File

@ -8,6 +8,8 @@ import {
} from '../components/tokens/stitches.config'
const themeKey = 'theme'
const preferredDarkThemeKey = 'preferredDarkThemeKey'
const preferredLightThemeKey = 'preferredLightThemeKey'
// Map legacy theme names to their new equivelents
const LEGACY_THEMES: { [string: string]: string } = {
@ -25,8 +27,36 @@ export function updateTheme(themeId: string): void {
updateThemeLocally(themeId)
}
const visibleThemeId = (themeId: string) => {
if (themeId == ThemeId.System) {
if (typeof window !== 'undefined' && window.matchMedia) {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
try {
const preferred = window.localStorage.getItem(preferredDarkThemeKey)
if (preferred) {
return JSON.parse(preferred)
}
} catch {}
return ThemeId.Dark
} else {
try {
const preferred = window.localStorage.getItem(preferredLightThemeKey)
if (preferred) {
return JSON.parse(preferred)
}
} catch {}
return ThemeId.Light
}
}
return ThemeId.Light
} else {
return themeId
}
}
export function getTheme(themeId: string) {
switch (themeId) {
const themeToUse = visibleThemeId(themeId)
switch (themeToUse) {
case ThemeId.Dark:
return darkTheme
case ThemeId.Sepia:
@ -56,7 +86,7 @@ export function updateThemeLocally(themeId: string): void {
}
export function currentThemeName(): string {
switch (currentTheme()) {
switch (getCurrentLocalTheme()) {
case ThemeId.Light:
return 'Light'
case ThemeId.Dark:
@ -71,7 +101,7 @@ export function currentThemeName(): string {
return 'Light'
}
export function currentTheme(): ThemeId | undefined {
export function getCurrentLocalTheme(): ThemeId | undefined {
if (typeof window === 'undefined') {
return undefined
}
@ -88,7 +118,7 @@ export function currentTheme(): ThemeId | undefined {
return ThemeId.Light
}
export function applyStoredTheme(syncWithServer = true): ThemeId | undefined {
export function applyStoredTheme(): ThemeId | undefined {
if (typeof window === 'undefined') {
return undefined
}

View File

@ -30,7 +30,7 @@ export default function Debug(): JSX.Element {
includeFriendsHighlights: false,
})
applyStoredTheme(false)
applyStoredTheme()
const sortedAttributes = useMemo(() => {
// if (!sortedAttributes) {

View File

@ -20,7 +20,7 @@ export default function ArticleSavingRequestPage(): JSX.Element {
const readerSettings = useReaderSettings()
const [articleId, setArticleId] = useState<string | undefined>(undefined)
applyStoredTheme(false)
applyStoredTheme()
useEffect(() => {
if (!router.isReady) return

View File

@ -25,13 +25,12 @@ type AppArticleEmbedContentProps = {
}
export default function AppArticleEmbed(): JSX.Element {
applyStoredTheme(false) // false to skip server sync
applyStoredTheme()
const router = useRouter()
const [contentProps, setContentProps] = useState<
AppArticleEmbedContentProps | undefined
>(undefined)
const [contentProps, setContentProps] =
useState<AppArticleEmbedContentProps | undefined>(undefined)
useEffect(() => {
if (!router.isReady) return

View File

@ -9,7 +9,7 @@ import { PrimaryLayout } from '../../../../components/templates/PrimaryLayout'
import { applyStoredTheme } from '../../../../lib/themeUpdater'
export default function LinkRequestPage(): JSX.Element {
applyStoredTheme(false) // false to skip server sync
applyStoredTheme()
const router = useRouter()
const [requestID, setRequestID] = useState<string | undefined>(undefined)

View File

@ -2,6 +2,6 @@ import { PrivacyPolicy } from '../../components/templates/PrivacyPolicy'
import { applyStoredTheme } from '../../lib/themeUpdater'
export default function Privacy(): JSX.Element {
applyStoredTheme(false) // false to skip server sync
applyStoredTheme()
return <PrivacyPolicy isAppEmbed />
}

View File

@ -2,6 +2,6 @@ import { TermsAndConditions } from '../../components/templates/TermsAndCondition
import { applyStoredTheme } from '../../lib/themeUpdater'
export default function Terms(): JSX.Element {
applyStoredTheme(false) // false to skip server sync
applyStoredTheme()
return <TermsAndConditions isAppEmbed />
}

View File

@ -16,7 +16,7 @@ export default function ArticleSavingRequestPage(): JSX.Element {
const readerSettings = useReaderSettings()
const [url, setUrl] = useState<string | undefined>(undefined)
applyStoredTheme(false)
applyStoredTheme()
useEffect(() => {
if (!router.isReady) return

View File

@ -20,7 +20,7 @@ export default function ArticleSavingRequestPage(): JSX.Element {
const readerSettings = useReaderSettings()
const [articleId, setArticleId] = useState<string | undefined>(undefined)
applyStoredTheme(false)
applyStoredTheme()
useEffect(() => {
if (!router.isReady) return

View File

@ -198,7 +198,7 @@ export default function Account(): JSX.Element {
})()
}, [email])
applyStoredTheme(false)
applyStoredTheme()
return (
<SettingsLayout>

View File

@ -28,7 +28,7 @@ export default function Api(): JSX.Element {
const neverExpiresDate = new Date(8640000000000000)
const defaultExpiresAt = 'Never'
applyStoredTheme(false)
applyStoredTheme()
async function onDelete(id: string): Promise<void> {
const result = await revokeApiKeyMutation(id)

View File

@ -20,7 +20,7 @@ export default function DeleteMyAccount(): JSX.Element {
const viewer = useGetViewerQuery()
const [showConfirm, setShowConfirm] = useState(false)
applyStoredTheme(false)
applyStoredTheme()
async function deleteAccount(): Promise<void> {
const viewerId = viewer.viewerData?.me?.id

View File

@ -81,11 +81,10 @@ function CopyTextButton(props: CopyTextButtonProps): JSX.Element {
export default function EmailsPage(): JSX.Element {
const { emailAddresses, revalidate, isValidating } =
useGetNewsletterEmailsQuery()
const [confirmDeleteEmailId, setConfirmDeleteEmailId] = useState<
undefined | string
>(undefined)
const [confirmDeleteEmailId, setConfirmDeleteEmailId] =
useState<undefined | string>(undefined)
applyStoredTheme(false)
applyStoredTheme()
async function createEmail(): Promise<void> {
const email = await createNewsletterEmailMutation()
@ -114,143 +113,148 @@ export default function EmailsPage(): JSX.Element {
return emailAddresses.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
}, [emailAddresses])
return <>
<SettingsTable
pageId="settings-emails-tag"
pageInfoLink="https://docs.omnivore.app/using/inbox.html"
headerTitle="Address"
createTitle="Create a new email address"
createAction={createEmail}
suggestionInfo={{
title: 'Subscribe to newsletters with an Omnivore Email Address',
message:
'Create an Omnivore email address and use it to subscribe to newsletters or send yourself documents. Newsletters and documents will be categorized and added to your library when we receive a message. View all received emails with the "Recently Received Emails" link at the bottom of this page.',
docs: 'https://docs.omnivore.app/using/inbox.html',
key: '--settings-emails-show-help',
CTAText: 'Create an email address',
onClickCTA: () => {
createEmail()
},
}}
>
{sortedEmailAddresses.length > 0 ? (
sortedEmailAddresses.map((email, i) => {
return (
<SettingsTableRow
key={email.address}
title={email.address}
isLast={i === sortedEmailAddresses.length - 1}
onDelete={() => setConfirmDeleteEmailId(email.id)}
deleteTitle="Delete"
sublineElement={
<StyledText
css={{
my: '5px',
fontSize: '11px',
a: {
color: '$omnivoreCtaYellow',
},
}}
>
{`created ${formattedShortDate(email.createdAt)}, `}
<Link href="/settings/subscriptions" legacyBehavior>{`${email.subscriptionCount} subscriptions`}</Link>
</StyledText>
}
titleElement={
<CopyTextBtnWrapper
css={{
marginLeft: '20px',
'@mdDown': {
marginRight: '10px',
},
}}
>
<CopyTextButton
text={email.address}
type={TextType.EmailAddress}
/>
</CopyTextBtnWrapper>
}
extraElement={
email.confirmationCode ? (
<HStack
alignment="start"
distribution="center"
css={{
width: '100%',
backgroundColor: '$grayBgActive',
borderRadius: '6px',
padding: '4px 4px 4px 0px',
'@md': {
width: '30%',
backgroundColor: 'transparent',
},
}}
>
<>
<StyledText
css={{
fontSize: '11px',
'@md': {
marginTop: '5px',
},
'@mdDown': {
marginLeft: 'auto',
},
marginRight: '10px',
}}
>
{`Gmail: ${email.confirmationCode}`}
</StyledText>
<Box>
<CopyTextBtnWrapper>
<CopyTextButton
text={email.confirmationCode || ''}
type={TextType.ConfirmationCode}
/>
</CopyTextBtnWrapper>
</Box>
</>
</HStack>
) : (
<></>
)
}
/>
);
})
) : (
<EmptySettingsRow
text={isValidating ? '-' : 'No Email Addresses Found'}
/>
)}
<SpanBox
css={{
pt: '15px',
fontSize: '12px',
marginLeft: 'auto',
a: {
color: '$omnivoreCtaYellow',
return (
<>
<SettingsTable
pageId="settings-emails-tag"
pageInfoLink="https://docs.omnivore.app/using/inbox.html"
headerTitle="Address"
createTitle="Create a new email address"
createAction={createEmail}
suggestionInfo={{
title: 'Subscribe to newsletters with an Omnivore Email Address',
message:
'Create an Omnivore email address and use it to subscribe to newsletters or send yourself documents. Newsletters and documents will be categorized and added to your library when we receive a message. View all received emails with the "Recently Received Emails" link at the bottom of this page.',
docs: 'https://docs.omnivore.app/using/inbox.html',
key: '--settings-emails-show-help',
CTAText: 'Create an email address',
onClickCTA: () => {
createEmail()
},
}}
>
<Link href="/settings/emails/recent">
View recently received emails
</Link>
</SpanBox>
</SettingsTable>
{sortedEmailAddresses.length > 0 ? (
sortedEmailAddresses.map((email, i) => {
return (
<SettingsTableRow
key={email.address}
title={email.address}
isLast={i === sortedEmailAddresses.length - 1}
onDelete={() => setConfirmDeleteEmailId(email.id)}
deleteTitle="Delete"
sublineElement={
<StyledText
css={{
my: '5px',
fontSize: '11px',
a: {
color: '$omnivoreCtaYellow',
},
}}
>
{`created ${formattedShortDate(email.createdAt)}, `}
<Link
href="/settings/subscriptions"
legacyBehavior
>{`${email.subscriptionCount} subscriptions`}</Link>
</StyledText>
}
titleElement={
<CopyTextBtnWrapper
css={{
marginLeft: '20px',
'@mdDown': {
marginRight: '10px',
},
}}
>
<CopyTextButton
text={email.address}
type={TextType.EmailAddress}
/>
</CopyTextBtnWrapper>
}
extraElement={
email.confirmationCode ? (
<HStack
alignment="start"
distribution="center"
css={{
width: '100%',
backgroundColor: '$grayBgActive',
borderRadius: '6px',
padding: '4px 4px 4px 0px',
'@md': {
width: '30%',
backgroundColor: 'transparent',
},
}}
>
<>
<StyledText
css={{
fontSize: '11px',
'@md': {
marginTop: '5px',
},
'@mdDown': {
marginLeft: 'auto',
},
marginRight: '10px',
}}
>
{`Gmail: ${email.confirmationCode}`}
</StyledText>
<Box>
<CopyTextBtnWrapper>
<CopyTextButton
text={email.confirmationCode || ''}
type={TextType.ConfirmationCode}
/>
</CopyTextBtnWrapper>
</Box>
</>
</HStack>
) : (
<></>
)
}
/>
)
})
) : (
<EmptySettingsRow
text={isValidating ? '-' : 'No Email Addresses Found'}
/>
)}
<SpanBox
css={{
pt: '15px',
fontSize: '12px',
marginLeft: 'auto',
a: {
color: '$omnivoreCtaYellow',
},
}}
>
<Link href="/settings/emails/recent">
View recently received emails
</Link>
</SpanBox>
</SettingsTable>
{confirmDeleteEmailId ? (
<ConfirmationModal
message={
'Are you sure? You will stop receiving emails sent to this address.'
}
onAccept={async () => {
await deleteEmail(confirmDeleteEmailId)
setConfirmDeleteEmailId(undefined)
}}
onOpenChange={() => setConfirmDeleteEmailId(undefined)}
/>
) : null}
</>;
{confirmDeleteEmailId ? (
<ConfirmationModal
message={
'Are you sure? You will stop receiving emails sent to this address.'
}
onAccept={async () => {
await deleteEmail(confirmDeleteEmailId)
setConfirmDeleteEmailId(undefined)
}}
onOpenChange={() => setConfirmDeleteEmailId(undefined)}
/>
) : null}
</>
)
}

View File

@ -152,15 +152,13 @@ const ViewRecentEmailModal = (
export default function RecentEmails(): JSX.Element {
const { recentEmails, revalidate, isValidating } = useGetRecentEmailsQuery()
const [viewingEmailText, setViewingEmailText] = useState<
RecentEmail | undefined
>(undefined)
const [viewingEmailText, setViewingEmailText] =
useState<RecentEmail | undefined>(undefined)
const [viewingEmailHtml, setViewingEmailHtml] = useState<
RecentEmail | undefined
>(undefined)
const [viewingEmailHtml, setViewingEmailHtml] =
useState<RecentEmail | undefined>(undefined)
applyStoredTheme(false)
applyStoredTheme()
const sortedRecentEmails = useMemo(() => {
return recentEmails.sort((a, b) => b.createdAt.localeCompare(a.createdAt))

View File

@ -99,7 +99,7 @@ export default function Rss(): JSX.Element {
revalidate()
}
applyStoredTheme(false)
applyStoredTheme()
return (
<SettingsTable

View File

@ -74,7 +74,7 @@ type integrationsCard = {
}
}
export default function Integrations(): JSX.Element {
applyStoredTheme(false)
applyStoredTheme()
const { integrations, revalidate } = useGetIntegrationsQuery()
const { webhooks } = useGetWebhooksQuery()

View File

@ -153,16 +153,15 @@ export default function LabelsPage(): JSX.Element {
const [descriptionInputText, setDescriptionInputText] = useState<string>('')
const [isCreateMode, setIsCreateMode] = useState<boolean>(false)
const [windowWidth, setWindowWidth] = useState<number>(0)
const [confirmRemoveLabelId, setConfirmRemoveLabelId] = useState<
string | null
>(null)
const [confirmRemoveLabelId, setConfirmRemoveLabelId] =
useState<string | null>(null)
const [showLabelPageHelp, setShowLabelPageHelp] = usePersistedState<boolean>({
key: `--settings-labels-show-help`,
initialValue: true,
})
const breakpoint = 768
applyStoredTheme(false)
applyStoredTheme()
const sortedLabels = useMemo(() => {
return labels.sort((left: Label, right: Label) =>

View File

@ -139,7 +139,7 @@ export default function PinnedSearches(): JSX.Element {
dispatchList({ type: 'RESET' })
}, [])
applyStoredTheme(false)
applyStoredTheme()
return (
<SettingsLayout>

View File

@ -149,9 +149,8 @@ const CreateActionModal = (props: CreateActionModalProps): JSX.Element => {
}
}
const [actionType, setActionType] = useState<RuleActionType | undefined>(
undefined
)
const [actionType, setActionType] =
useState<RuleActionType | undefined>(undefined)
return (
<Modal
@ -219,9 +218,8 @@ export default function Rules(): JSX.Element {
const { rules, revalidate } = useGetRulesQuery()
const { labels } = useGetLabelsQuery()
const [isCreateRuleModalOpen, setIsCreateRuleModalOpen] = useState(false)
const [createActionRule, setCreateActionRule] = useState<Rule | undefined>(
undefined
)
const [createActionRule, setCreateActionRule] =
useState<Rule | undefined>(undefined)
const dataSource = useMemo(() => {
return rules.map((rule: Rule) => {
@ -235,7 +233,7 @@ export default function Rules(): JSX.Element {
})
}, [rules])
applyStoredTheme(false)
applyStoredTheme()
const deleteRule = useCallback(
async (rule: Rule) => {

View File

@ -156,19 +156,19 @@ export default function SavedSearchesPage(): JSX.Element {
const [editingId, setEditingId] = useState<string | null>(null)
const [isCreateMode, setIsCreateMode] = useState<boolean>(false)
const [windowWidth, setWindowWidth] = useState<number>(0)
const [confirmRemoveSavedSearchId, setConfirmRemoveSavedSearchId] = useState<
string | null
>(null)
const [confirmRemoveSavedSearchId, setConfirmRemoveSavedSearchId] =
useState<string | null>(null)
const [draggedElementId, setDraggedElementId] = useState<string | null>(null)
const [draggedElementPosition, setDraggedElementPosition] = useState<{
x: number
y: number
} | null>(null)
const [draggedElementPosition, setDraggedElementPosition] =
useState<{
x: number
y: number
} | null>(null)
const [sortedSavedSearch, setSortedSavedSearch] = useState<SavedSearch[]>([])
// Some theming stuff here.
const breakpoint = 768
applyStoredTheme(false)
applyStoredTheme()
useEffect(() => {
setSortedSavedSearch(
@ -579,14 +579,15 @@ function GenericTableCard(
editingId === savedSearch?.id || (isCreateMode && !savedSearch)
const iconColor = isDarkTheme() ? '#D8D7D5' : '#5F5E58'
const DEFAULT_STYLE = { position: null }
const [style, setStyle] = useState<
Partial<{
position: string | null
top: string
left: string
maxWidth: string
}>
>(DEFAULT_STYLE)
const [style, setStyle] =
useState<
Partial<{
position: string | null
top: string
left: string
maxWidth: string
}>
>(DEFAULT_STYLE)
const handleEdit = () => {
editingId && updateSavedSearch(editingId)
setEditingId(null)

View File

@ -22,7 +22,7 @@ export default function SubscriptionsPage(): JSX.Element {
const [confirmUnsubscribeSubscription, setConfirmUnsubscribeSubscription] =
useState<Subscription | null>(null)
applyStoredTheme(false)
applyStoredTheme()
async function onUnsubscribe(subscription: Subscription): Promise<void> {
const result = await unsubscribeMutation(subscription.name, subscription.id)
@ -86,7 +86,8 @@ export default function SubscriptionsPage(): JSX.Element {
at{' '}
<Link
href={`/settings/emails?address=${subscription.newsletterEmail}`}
legacyBehavior>
legacyBehavior
>
{subscription.newsletterEmail}
</Link>
</>
@ -101,7 +102,7 @@ export default function SubscriptionsPage(): JSX.Element {
</StyledText>
}
/>
);
)
})
) : (
<EmptySettingsRow
@ -125,5 +126,5 @@ export default function SubscriptionsPage(): JSX.Element {
) : null}
</>
</SettingsTable>
);
)
}

View File

@ -65,7 +65,7 @@ export default function Webhooks(): JSX.Element {
return rows
}, [webhooks])
applyStoredTheme(false)
applyStoredTheme()
function validateEventTypes(eventTypes: WebhookEvent[]): boolean {
if (eventTypes.length > 0) return true

View File

@ -25,7 +25,7 @@ type RunningState = 'none' | 'confirming' | 'running' | 'completed'
export default function BulkPerformer(): JSX.Element {
const router = useRouter()
applyStoredTheme(false)
applyStoredTheme()
const [action, setAction] = useState<BulkAction | undefined>()
const [query, setQuery] = useState<string>('in:all')

View File

@ -17,7 +17,7 @@ import { validateCsvFile } from '../../../utils/csvValidator'
type UploadState = 'none' | 'uploading' | 'completed'
export default function ImportUploader(): JSX.Element {
applyStoredTheme(false)
applyStoredTheme()
const [errorMessage, setErrorMessage] = useState<string | undefined>()
const [file, setFile] = useState<File>()

View File

@ -20,7 +20,7 @@ import { Tray } from 'phosphor-react'
type UploadState = 'none' | 'uploading' | 'completed'
export default function ImportUploader(): JSX.Element {
applyStoredTheme(false)
applyStoredTheme()
const [errorMessage, setErrorMessage] = useState<string | undefined>()
const [uploadState, setUploadState] = useState<UploadState>('none')