diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index bef12f4397..7a9a371c14 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1091,21 +1091,6 @@ "count": 1 } }, - "src/state/preferences/alt-text-required.tsx": { - "typescript/no-floating-promises": { - "count": 1 - } - }, - "src/state/preferences/autoplay.tsx": { - "typescript/no-floating-promises": { - "count": 1 - } - }, - "src/state/preferences/disable-haptics.tsx": { - "typescript/no-floating-promises": { - "count": 1 - } - }, "src/state/preferences/external-embeds-prefs.tsx": { "typescript/no-floating-promises": { "count": 1 @@ -1116,11 +1101,6 @@ "count": 1 } }, - "src/state/preferences/in-app-browser.tsx": { - "typescript/no-floating-promises": { - "count": 1 - } - }, "src/state/preferences/kawaii.tsx": { "typescript/no-floating-promises": { "count": 1 @@ -1131,26 +1111,11 @@ "count": 1 } }, - "src/state/preferences/large-alt-badge.tsx": { - "typescript/no-floating-promises": { - "count": 1 - } - }, - "src/state/preferences/subtitles.tsx": { - "typescript/no-floating-promises": { - "count": 1 - } - }, "src/state/preferences/trending.tsx": { "typescript/no-floating-promises": { "count": 1 } }, - "src/state/preferences/used-starter-packs.tsx": { - "typescript/no-floating-promises": { - "count": 1 - } - }, "src/state/queries/activity-subscriptions.ts": { "typescript/no-floating-promises": { "count": 1 diff --git a/src/state/preferences/alt-text-required.tsx b/src/state/preferences/alt-text-required.tsx index 91cdacb908..e1f138cfe4 100644 --- a/src/state/preferences/alt-text-required.tsx +++ b/src/state/preferences/alt-text-required.tsx @@ -1,58 +1,10 @@ -import { - createContext, - useCallback, - useContext, - useEffect, - useState, -} from 'react' - -import * as persisted from '#/state/persisted' - -type StateContext = persisted.Schema['requireAltTextEnabled'] -type SetContext = (v: persisted.Schema['requireAltTextEnabled']) => void - -const stateContext = createContext( - persisted.defaults.requireAltTextEnabled, -) -stateContext.displayName = 'AltTextRequiredStateContext' -const setContext = createContext( - (_: persisted.Schema['requireAltTextEnabled']) => {}, -) -setContext.displayName = 'AltTextRequiredSetContext' - -export function Provider({children}: React.PropsWithChildren<{}>) { - const [state, setState] = useState(persisted.get('requireAltTextEnabled')) - - const setStateWrapped = useCallback( - (requireAltTextEnabled: persisted.Schema['requireAltTextEnabled']) => { - setState(requireAltTextEnabled) - persisted.write('requireAltTextEnabled', requireAltTextEnabled) - }, - [setState], - ) - - useEffect(() => { - return persisted.onUpdate( - 'requireAltTextEnabled', - nextRequireAltTextEnabled => { - setState(nextRequireAltTextEnabled) - }, - ) - }, [setStateWrapped]) - - return ( - - - {children} - - - ) -} +import {usePref, useSetPref} from './simple-prefs' +/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */ export function useRequireAltTextEnabled() { - return useContext(stateContext) + return usePref('requireAltTextEnabled') } export function useSetRequireAltTextEnabled() { - return useContext(setContext) + return useSetPref('requireAltTextEnabled') } diff --git a/src/state/preferences/autoplay.tsx b/src/state/preferences/autoplay.tsx index ec5e8153f6..1d34586848 100644 --- a/src/state/preferences/autoplay.tsx +++ b/src/state/preferences/autoplay.tsx @@ -1,48 +1,10 @@ -import { - createContext, - useCallback, - useContext, - useEffect, - useState, -} from 'react' +import {usePref, useSetPref} from './simple-prefs' -import * as persisted from '#/state/persisted' - -type StateContext = boolean -type SetContext = (v: boolean) => void - -const stateContext = createContext( - Boolean(persisted.defaults.disableAutoplay), -) -stateContext.displayName = 'AutoplayStateContext' -const setContext = createContext((_: boolean) => {}) -setContext.displayName = 'AutoplaySetContext' - -export function Provider({children}: {children: React.ReactNode}) { - const [state, setState] = useState(Boolean(persisted.get('disableAutoplay'))) - - const setStateWrapped = useCallback( - (autoplayDisabled: persisted.Schema['disableAutoplay']) => { - setState(Boolean(autoplayDisabled)) - persisted.write('disableAutoplay', autoplayDisabled) - }, - [setState], - ) - - useEffect(() => { - return persisted.onUpdate('disableAutoplay', nextDisableAutoplay => { - setState(Boolean(nextDisableAutoplay)) - }) - }, [setStateWrapped]) - - return ( - - - {children} - - - ) +/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */ +export function useAutoplayDisabled() { + return Boolean(usePref('disableAutoplay')) } -export const useAutoplayDisabled = () => useContext(stateContext) -export const useSetAutoplayDisabled = () => useContext(setContext) +export function useSetAutoplayDisabled() { + return useSetPref('disableAutoplay') +} diff --git a/src/state/preferences/disable-haptics.tsx b/src/state/preferences/disable-haptics.tsx index bc67c95a63..6a3c5a42ea 100644 --- a/src/state/preferences/disable-haptics.tsx +++ b/src/state/preferences/disable-haptics.tsx @@ -1,48 +1,10 @@ -import { - createContext, - useCallback, - useContext, - useEffect, - useState, -} from 'react' +import {usePref, useSetPref} from './simple-prefs' -import * as persisted from '#/state/persisted' - -type StateContext = boolean -type SetContext = (v: boolean) => void - -const stateContext = createContext( - Boolean(persisted.defaults.disableHaptics), -) -stateContext.displayName = 'DisableHapticsStateContext' -const setContext = createContext((_: boolean) => {}) -setContext.displayName = 'DisableHapticsSetContext' - -export function Provider({children}: {children: React.ReactNode}) { - const [state, setState] = useState(Boolean(persisted.get('disableHaptics'))) - - const setStateWrapped = useCallback( - (hapticsEnabled: persisted.Schema['disableHaptics']) => { - setState(Boolean(hapticsEnabled)) - persisted.write('disableHaptics', hapticsEnabled) - }, - [setState], - ) - - useEffect(() => { - return persisted.onUpdate('disableHaptics', nextDisableHaptics => { - setState(Boolean(nextDisableHaptics)) - }) - }, [setStateWrapped]) - - return ( - - - {children} - - - ) +/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */ +export function useHapticsDisabled() { + return Boolean(usePref('disableHaptics')) } -export const useHapticsDisabled = () => useContext(stateContext) -export const useSetHapticsDisabled = () => useContext(setContext) +export function useSetHapticsDisabled() { + return useSetPref('disableHaptics') +} diff --git a/src/state/preferences/in-app-browser.tsx b/src/state/preferences/in-app-browser.tsx index 1ebb089642..9dffc08e39 100644 --- a/src/state/preferences/in-app-browser.tsx +++ b/src/state/preferences/in-app-browser.tsx @@ -1,55 +1,10 @@ -import { - createContext, - useCallback, - useContext, - useEffect, - useState, -} from 'react' - -import * as persisted from '#/state/persisted' - -type StateContext = persisted.Schema['useInAppBrowser'] -type SetContext = (v: persisted.Schema['useInAppBrowser']) => void - -const stateContext = createContext( - persisted.defaults.useInAppBrowser, -) -stateContext.displayName = 'InAppBrowserStateContext' -const setContext = createContext( - (_: persisted.Schema['useInAppBrowser']) => {}, -) -setContext.displayName = 'InAppBrowserSetContext' - -export function Provider({children}: React.PropsWithChildren<{}>) { - const [state, setState] = useState(persisted.get('useInAppBrowser')) - - const setStateWrapped = useCallback( - (inAppBrowser: persisted.Schema['useInAppBrowser']) => { - setState(inAppBrowser) - persisted.write('useInAppBrowser', inAppBrowser) - }, - [setState], - ) - - useEffect(() => { - return persisted.onUpdate('useInAppBrowser', nextUseInAppBrowser => { - setState(nextUseInAppBrowser) - }) - }, [setStateWrapped]) - - return ( - - - {children} - - - ) -} +import {usePref, useSetPref} from './simple-prefs' +/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */ export function useInAppBrowser() { - return useContext(stateContext) + return usePref('useInAppBrowser') } export function useSetInAppBrowser() { - return useContext(setContext) + return useSetPref('useInAppBrowser') } diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index 0f20953b7c..678350848e 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -1,15 +1,9 @@ -import {Provider as AltTextRequiredProvider} from './alt-text-required' -import {Provider as AutoplayProvider} from './autoplay' -import {Provider as DisableHapticsProvider} from './disable-haptics' import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs' import {Provider as HiddenPostsProvider} from './hidden-posts' -import {Provider as InAppBrowserProvider} from './in-app-browser' import {Provider as KawaiiProvider} from './kawaii' import {Provider as LanguagesProvider} from './languages' -import {Provider as LargeAltBadgeProvider} from './large-alt-badge' -import {Provider as SubtitlesProvider} from './subtitles' +import {Provider as SimplePrefsProvider} from './simple-prefs' import {Provider as TrendingSettingsProvider} from './trending' -import {Provider as UsedStarterPacksProvider} from './used-starter-packs' export { useRequireAltTextEnabled, @@ -29,27 +23,15 @@ export {useSetSubtitlesEnabled, useSubtitlesEnabled} from './subtitles' export function Provider({children}: React.PropsWithChildren<{}>) { return ( - - - - - - - - - - - {children} - - - - - - - - - - + + + + + {children} + + + + ) } diff --git a/src/state/preferences/large-alt-badge.tsx b/src/state/preferences/large-alt-badge.tsx index fd4856b04e..f611576f1a 100644 --- a/src/state/preferences/large-alt-badge.tsx +++ b/src/state/preferences/large-alt-badge.tsx @@ -1,58 +1,10 @@ -import { - createContext, - useCallback, - useContext, - useEffect, - useState, -} from 'react' - -import * as persisted from '#/state/persisted' - -type StateContext = persisted.Schema['largeAltBadgeEnabled'] -type SetContext = (v: persisted.Schema['largeAltBadgeEnabled']) => void - -const stateContext = createContext( - persisted.defaults.largeAltBadgeEnabled, -) -stateContext.displayName = 'LargeAltBadgeStateContext' -const setContext = createContext( - (_: persisted.Schema['largeAltBadgeEnabled']) => {}, -) -setContext.displayName = 'LargeAltBadgeSetContext' - -export function Provider({children}: React.PropsWithChildren<{}>) { - const [state, setState] = useState(persisted.get('largeAltBadgeEnabled')) - - const setStateWrapped = useCallback( - (largeAltBadgeEnabled: persisted.Schema['largeAltBadgeEnabled']) => { - setState(largeAltBadgeEnabled) - persisted.write('largeAltBadgeEnabled', largeAltBadgeEnabled) - }, - [setState], - ) - - useEffect(() => { - return persisted.onUpdate( - 'largeAltBadgeEnabled', - nextLargeAltBadgeEnabled => { - setState(nextLargeAltBadgeEnabled) - }, - ) - }, [setStateWrapped]) - - return ( - - - {children} - - - ) -} +import {usePref, useSetPref} from './simple-prefs' +/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */ export function useLargeAltBadgeEnabled() { - return useContext(stateContext) + return usePref('largeAltBadgeEnabled') } export function useSetLargeAltBadgeEnabled() { - return useContext(setContext) + return useSetPref('largeAltBadgeEnabled') } diff --git a/src/state/preferences/simple-prefs.tsx b/src/state/preferences/simple-prefs.tsx new file mode 100644 index 0000000000..ad26496117 --- /dev/null +++ b/src/state/preferences/simple-prefs.tsx @@ -0,0 +1,85 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react' + +import * as persisted from '#/state/persisted' + +/* + * Several preferences were each their own Provider, and each of those rendered a + * state context plus a setter context - three fibers apiece, all of them sitting + * between the app root and every screen. + * + * That depth is not free. React walks the full return path to the root on every + * bailout, and a scrolling list bails out on most of its cells on every update, + * so the provider stack is multiplied by (renders per second x mounted cells). + * Collapsing these into one state context and one setter context trades a wider + * re-render on preference change - which only happens from a settings screen - + * for a permanently shallower tree. + */ +const KEYS = [ + 'requireAltTextEnabled', + 'disableAutoplay', + 'disableHaptics', + 'useInAppBrowser', + 'largeAltBadgeEnabled', + 'subtitlesEnabled', + 'hasCheckedForStarterPack', +] as const + +type Key = (typeof KEYS)[number] +type Values = {[K in Key]: persisted.Schema[K]} +type SetFn = (key: K, value: Values[K]) => void + +function readAll(): Values { + const out = {} as Values + for (const key of KEYS) { + // @ts-expect-error indexed write across the key union + out[key] = persisted.get(key) + } + return out +} + +const stateContext = createContext( + Object.fromEntries(KEYS.map(k => [k, persisted.defaults[k]])) as Values, +) +stateContext.displayName = 'SimplePrefsStateContext' +const setContext = createContext(() => {}) +setContext.displayName = 'SimplePrefsSetContext' + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = useState(readAll) + + useEffect(() => { + const unsubs = KEYS.map(key => + persisted.onUpdate(key, next => { + setState(prev => ({...prev, [key]: next})) + }), + ) + return () => unsubs.forEach(unsub => unsub()) + }, []) + + const set = useCallback((key, value) => { + setState(prev => ({...prev, [key]: value})) + // @ts-expect-error indexed write across the key union + void persisted.write(key, value) + }, []) + + return ( + + {children} + + ) +} + +export function usePref(key: K): Values[K] { + return useContext(stateContext)[key] +} + +export function useSetPref(key: K): (value: Values[K]) => void { + const set = useContext(setContext) + return useCallback((value: Values[K]) => set(key, value), [set, key]) +} diff --git a/src/state/preferences/subtitles.tsx b/src/state/preferences/subtitles.tsx index 16428c2361..fb948bef65 100644 --- a/src/state/preferences/subtitles.tsx +++ b/src/state/preferences/subtitles.tsx @@ -1,48 +1,10 @@ -import { - createContext, - useCallback, - useContext, - useEffect, - useState, -} from 'react' +import {usePref, useSetPref} from './simple-prefs' -import * as persisted from '#/state/persisted' - -type StateContext = boolean -type SetContext = (v: boolean) => void - -const stateContext = createContext( - Boolean(persisted.defaults.subtitlesEnabled), -) -stateContext.displayName = 'SubtitlesStateContext' -const setContext = createContext((_: boolean) => {}) -setContext.displayName = 'SubtitlesSetContext' - -export function Provider({children}: {children: React.ReactNode}) { - const [state, setState] = useState(Boolean(persisted.get('subtitlesEnabled'))) - - const setStateWrapped = useCallback( - (subtitlesEnabled: persisted.Schema['subtitlesEnabled']) => { - setState(Boolean(subtitlesEnabled)) - persisted.write('subtitlesEnabled', subtitlesEnabled) - }, - [setState], - ) - - useEffect(() => { - return persisted.onUpdate('subtitlesEnabled', nextSubtitlesEnabled => { - setState(Boolean(nextSubtitlesEnabled)) - }) - }, [setStateWrapped]) - - return ( - - - {children} - - - ) +/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */ +export function useSubtitlesEnabled() { + return Boolean(usePref('subtitlesEnabled')) } -export const useSubtitlesEnabled = () => useContext(stateContext) -export const useSetSubtitlesEnabled = () => useContext(setContext) +export function useSetSubtitlesEnabled() { + return useSetPref('subtitlesEnabled') +} diff --git a/src/state/preferences/used-starter-packs.tsx b/src/state/preferences/used-starter-packs.tsx index 7b584db94f..ee51d804a9 100644 --- a/src/state/preferences/used-starter-packs.tsx +++ b/src/state/preferences/used-starter-packs.tsx @@ -1,42 +1,10 @@ -import {createContext, useContext, useEffect, useState} from 'react' +import {usePref, useSetPref} from './simple-prefs' -import * as persisted from '#/state/persisted' - -type StateContext = boolean | undefined -type SetContext = (v: boolean) => void - -const stateContext = createContext(false) -stateContext.displayName = 'UsedStarterPacksStateContext' -const setContext = createContext((_: boolean) => {}) -setContext.displayName = 'UsedStarterPacksSetContext' - -export function Provider({children}: {children: React.ReactNode}) { - const [state, setState] = useState(() => - persisted.get('hasCheckedForStarterPack'), - ) - - const setStateWrapped = (v: boolean) => { - setState(v) - persisted.write('hasCheckedForStarterPack', v) - } - - useEffect(() => { - return persisted.onUpdate( - 'hasCheckedForStarterPack', - nextHasCheckedForStarterPack => { - setState(nextHasCheckedForStarterPack) - }, - ) - }, []) - - return ( - - - {children} - - - ) +/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */ +export function useHasCheckedForStarterPack() { + return usePref('hasCheckedForStarterPack') } -export const useHasCheckedForStarterPack = () => useContext(stateContext) -export const useSetHasCheckedForStarterPack = () => useContext(setContext) +export function useSetHasCheckedForStarterPack() { + return useSetPref('hasCheckedForStarterPack') +}