Theme cleanup, expose extra reader themes

This also removes synced reader preferences so everything is
per device now.
This commit is contained in:
Jackson Harper 2023-03-09 16:44:45 +08:00
parent bf0a7d2865
commit 23efda667d
16 changed files with 172 additions and 313 deletions

View file

@ -32,7 +32,7 @@ export function LogoBox(): JSX.Element {
},
}}
>
<OmnivoreNameLogo />
<OmnivoreNameLogo color={theme.colors.thHighContrast.toString()} />
</SpanBox>
</>
)

View file

@ -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())
}}
>

View file

@ -234,7 +234,7 @@ function ThemeSection(props: PrimaryDropdownProps): JSX.Element {
}}
>
<StyledToggleButton
data-state={currentTheme() != ThemeId.Darker ? 'on' : 'off'}
data-state={currentTheme() != ThemeId.Dark ? 'on' : 'off'}
onClick={() => {
updateTheme(ThemeId.Light)
}}
@ -243,9 +243,9 @@ function ThemeSection(props: PrimaryDropdownProps): JSX.Element {
<Sun size={15} color={theme.colors.thTextContrast2.toString()} />
</StyledToggleButton>
<StyledToggleButton
data-state={currentTheme() == ThemeId.Darker ? 'on' : 'off'}
data-state={currentTheme() == ThemeId.Dark ? 'on' : 'off'}
onClick={() => {
updateTheme(ThemeId.Darker)
updateTheme(ThemeId.Dark)
}}
>
Dark

View file

@ -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)

View file

@ -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)
@ -279,7 +279,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
<Box
id="article-container"
css={{
padding: '16px',
padding: '30px',
paddingTop: '80px',
maxWidth: `${styles.maxWidthPercentage ?? 100}%`,
background: props.isAppleAppEmbed
@ -310,6 +310,9 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
? `${styles.maxWidthPercentage}%`
: 1024 - styles.margin,
},
'@mdDown': {
padding: '15px',
},
}}
>
<VStack alignment="start" distribution="start">
@ -325,6 +328,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
fontFamily: styles.fontFamily,
width: '100%',
wordWrap: 'break-word',
color: styles.readerFontColor,
}}
>
{props.article.title}

View file

@ -520,10 +520,7 @@ function LayoutControls(props: LayoutControlsProps): JSX.Element {
function ThemeSelector(props: ReaderSettingsProps): JSX.Element {
const [currentTheme, setCurrentTheme] = useState(currentThemeName())
const isDark = useMemo(() => {
return currentTheme === 'Dark' || currentTheme === 'Darker'
}, [currentTheme])
console.log('currentTheme: ', currentTheme)
return (
<VStack
@ -563,13 +560,15 @@ function ThemeSelector(props: ReaderSettingsProps): JSX.Element {
border: '2px solid #6A6968',
},
}}
data-state={isDark ? 'unselected' : 'selected'}
data-state={currentTheme == ThemeId.Light ? 'unselected' : 'selected'}
onClick={() => {
updateTheme(ThemeId.Light)
setCurrentTheme(currentThemeName())
}}
>
{!isDark && <Check color="#6A6968" size={15} weight="bold" />}
{currentTheme == ThemeId.Light && (
<Check color="#6A6968" size={15} weight="bold" />
)}
</Button>
<Button
style="themeSwitch"
@ -591,13 +590,73 @@ function ThemeSelector(props: ReaderSettingsProps): JSX.Element {
border: '2px solid #6A6968',
},
}}
data-state={isDark ? 'selected' : 'unselected'}
data-state={currentTheme == ThemeId.Dark ? 'selected' : 'unselected'}
onClick={() => {
updateTheme(ThemeId.Dark)
setCurrentTheme(currentThemeName())
}}
>
{isDark && <Check color="#F9D354" size={20} />}
{currentTheme == ThemeId.Dark && <Check color="#F9D354" size={20} />}
</Button>
<Button
style="themeSwitch"
css={{
display: 'flex',
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center',
width: '30px',
height: '30px',
background: '#FBF0D9',
borderRadius: '50%',
border: 'unset',
'&:hover': {
transform: 'scale(1.1)',
border: '2px solid #6A6968',
},
'&[data-state="selected"]': {
border: '2px solid #6A6968',
},
}}
data-state={currentTheme == ThemeId.Sepia ? 'selected' : 'unselected'}
onClick={() => {
updateTheme(ThemeId.Sepia)
setCurrentTheme(currentThemeName())
}}
>
{currentTheme == ThemeId.Sepia && <Check color="#F9D354" size={20} />}
</Button>
<Button
style="themeSwitch"
css={{
display: 'flex',
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center',
width: '30px',
height: '30px',
background: '#F3F3F3',
borderRadius: '50%',
border: 'unset',
'&:hover': {
transform: 'scale(1.1)',
border: '2px solid #6A6968',
},
'&[data-state="selected"]': {
border: '2px solid #6A6968',
},
}}
data-state={
currentTheme == ThemeId.Apollo ? 'selected' : 'unselected'
}
onClick={() => {
updateTheme(ThemeId.Apollo)
setCurrentTheme(currentThemeName())
}}
>
{currentTheme == ThemeId.Apollo && (
<Check color="#F9D354" size={20} />
)}
</Button>
</HStack>
</VStack>

View file

@ -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 }))

View file

@ -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',
},
}}
>
<HStack

View file

@ -2,12 +2,10 @@ import type * as Stitches from '@stitches/react'
import { createStitches, createTheme } from '@stitches/react'
export enum ThemeId {
Lighter = 'White',
Light = 'LightGray',
Dark = 'Gray',
Darker = 'Dark',
Light = 'Light',
Dark = 'Dark',
Sepia = 'Sepia',
Charcoal = 'Charcoal',
Apollo = 'Apollo',
}
export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
@ -150,6 +148,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
readerFont: '#3D3D3D',
readerFontHighContrast: 'black',
readerTableHeader: '#FFFFFF',
readerMargin: 'white',
// Avatar Fallback color
avatarBg: '#FFEA9F',
@ -232,6 +231,8 @@ const darkThemeSpec = {
readerFont: '#b9b9b9',
readerFontHighContrast: 'white',
readerTableHeader: '#FFFFFF',
readerMargin: '#2A2A2A',
avatarBg: '#7B5C3E',
avatarFont: '#D9D9D9',
@ -277,43 +278,38 @@ const darkThemeSpec = {
const sepiaThemeSpec = {
colors: {
// Reader Colors
readerBg: '#F9F1DC',
readerFont: '#554A34',
readerFontHighContrast: 'black',
readerHeader: '554A34',
BACKING: 'red',
readerBg: '#FBF0D9',
readerFont: '#5F4B32',
readerMargin: '#F3F3F3',
readerFontHighContrast: '#0A0806',
readerTableHeader: '#FFFFFF',
},
}
const charcoalThemeSpec = {
const apolloThemeSpec = {
colors: {
// Reader Colors
readerBg: '#303030',
readerFont: '#b9b9b9',
readerBg: '#6A6968',
readerFont: '#F3F3F3',
readerMargin: '#474747',
readerFontHighContrast: 'white',
readerHeader: '#b9b9b9',
readerTableHeader: '#FFFFFF',
},
}
// Dark and Darker theme now match each other.
// Use the darkThemeSpec object to make updates.
export const darkTheme = createTheme(ThemeId.Dark, darkThemeSpec)
export const darkerTheme = createTheme(ThemeId.Darker, darkThemeSpec)
export const sepiaTheme = createTheme(ThemeId.Sepia, {
...darkThemeSpec,
...sepiaThemeSpec,
})
export const charcoalTheme = createTheme(ThemeId.Charcoal, {
export const apolloTheme = createTheme(ThemeId.Apollo, {
...darkThemeSpec,
...charcoalThemeSpec,
colors: {
...darkThemeSpec.colors,
...apolloThemeSpec.colors,
},
})
// Lighter theme now matches the default theme.
// This only exists for users that might still have a lighter theme set
export const lighterTheme = createTheme(ThemeId.Lighter, {})
// Apply global styles in here
export const globalStyles = globalCss({
body: {

View file

@ -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<boolean | undefined>({
// key: `--display-hide-margins`,
@ -207,7 +203,6 @@ export const useReaderSettings = (): ReaderSettings => {
)
return {
preferencesData,
fontSize,
lineHeight,
marginWidth,

View file

@ -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<UserPreferences | undefined> {
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
}
}

View file

@ -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,
}
}

View file

@ -1,39 +1,54 @@
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 {
if (typeof window !== 'undefined') {
console.trace('storing theme: ', themeId)
window.localStorage.setItem(themeKey, themeId)
}
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 +57,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 +70,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 +91,8 @@ export function applyStoredTheme(syncWithServer = true): ThemeId | undefined {
| ThemeId
| undefined
if (theme && Object.values(ThemeId).includes(theme)) {
syncWithServer ? updateTheme(theme) : updateThemeLocally(theme)
console.log('applying stored theme: ', theme)
updateThemeLocally(theme)
}
return theme
}
@ -81,35 +101,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
}
}

View file

@ -47,7 +47,6 @@ const PdfArticleContainerNoSSR = dynamic<PdfArticleContainerProps>(
export default function Home(): JSX.Element {
const router = useRouter()
const { cache, mutate } = useSWRConfig()
const scrollRef = useRef<HTMLDivElement | null>(null)
const { slug } = router.query
const [showEditModal, setShowEditModal] = useState(false)
@ -355,12 +354,12 @@ export default function Home(): JSX.Element {
<VStack
alignment="center"
distribution="start"
ref={scrollRef}
className="disable-webkit-callout"
css={{
width: '100%',
height: '100%',
background: '$thBackground',
background: '$readerMargin',
overflow: 'scroll',
}}
>
{article && viewerData?.me ? (

View file

@ -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),
},
]

View file

@ -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 {
<body>
<Main />
<NextScript />
<script dangerouslySetInnerHTML={{ __html: setUserPreferences }} />
</body>
</Html>
)