diff --git a/jest/jestSetup.js b/jest/jestSetup.js index a6b7c24f..ac175900 100644 --- a/jest/jestSetup.js +++ b/jest/jestSetup.js @@ -95,3 +95,13 @@ jest.mock('expo-application', () => ({ nativeApplicationVersion: '1.0.0', nativeBuildVersion: '1', })) + +jest.mock('expo-modules-core', () => ({ + requireNativeModule: jest.fn().mockImplementation(moduleName => { + if (moduleName === 'ExpoPlatformInfo') { + return { + getIsReducedMotionEnabled: () => false, + } + } + }), +})) diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/platforminfo/ExpoPlatformInfoModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/platforminfo/ExpoPlatformInfoModule.kt new file mode 100644 index 00000000..189796f8 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/platforminfo/ExpoPlatformInfoModule.kt @@ -0,0 +1,24 @@ +package expo.modules.blueskyswissarmy.platforminfo + +import android.provider.Settings +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class ExpoPlatformInfoModule : Module() { + override fun definition() = + ModuleDefinition { + Name("ExpoPlatformInfo") + + // See https://github.com/software-mansion/react-native-reanimated/blob/7df5fd57d608fe25724608835461cd925ff5151d/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeProxy/NativeProxyCommon.java#L242 + Function("getIsReducedMotionEnabled") { + val resolver = appContext.reactContext?.contentResolver ?: return@Function false + val scale = Settings.Global.getString(resolver, Settings.Global.TRANSITION_ANIMATION_SCALE) ?: return@Function false + + try { + return@Function scale.toFloat() == 0f + } catch (_: Error) { + return@Function false + } + } + } +} diff --git a/modules/expo-bluesky-swiss-army/expo-module.config.json b/modules/expo-bluesky-swiss-army/expo-module.config.json index 1111f8a0..adb535e7 100644 --- a/modules/expo-bluesky-swiss-army/expo-module.config.json +++ b/modules/expo-bluesky-swiss-army/expo-module.config.json @@ -1,12 +1,13 @@ { "platforms": ["ios", "tvos", "android", "web"], "ios": { - "modules": ["ExpoBlueskySharedPrefsModule", "ExpoBlueskyReferrerModule"] + "modules": ["ExpoBlueskySharedPrefsModule", "ExpoBlueskyReferrerModule", "ExpoPlatformInfoModule"] }, "android": { "modules": [ "expo.modules.blueskyswissarmy.sharedprefs.ExpoBlueskySharedPrefsModule", - "expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule" + "expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule", + "expo.modules.blueskyswissarmy.platforminfo.ExpoPlatformInfoModule" ] } } diff --git a/modules/expo-bluesky-swiss-army/index.ts b/modules/expo-bluesky-swiss-army/index.ts index 89cea00a..f62596cb 100644 --- a/modules/expo-bluesky-swiss-army/index.ts +++ b/modules/expo-bluesky-swiss-army/index.ts @@ -1,4 +1,5 @@ +import * as PlatformInfo from './src/PlatformInfo' import * as Referrer from './src/Referrer' import * as SharedPrefs from './src/SharedPrefs' -export {Referrer, SharedPrefs} +export {PlatformInfo, Referrer, SharedPrefs} diff --git a/modules/expo-bluesky-swiss-army/ios/PlatformInfo/ExpoPlatformInfoModule.swift b/modules/expo-bluesky-swiss-army/ios/PlatformInfo/ExpoPlatformInfoModule.swift new file mode 100644 index 00000000..4a1e6d7e --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/PlatformInfo/ExpoPlatformInfoModule.swift @@ -0,0 +1,11 @@ +import ExpoModulesCore + +public class ExpoPlatformInfoModule: Module { + public func definition() -> ModuleDefinition { + Name("ExpoPlatformInfo") + + Function("getIsReducedMotionEnabled") { + return UIAccessibility.isReduceMotionEnabled + } + } +} diff --git a/modules/expo-bluesky-swiss-army/src/PlatformInfo/index.native.ts b/modules/expo-bluesky-swiss-army/src/PlatformInfo/index.native.ts new file mode 100644 index 00000000..e05f173d --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/PlatformInfo/index.native.ts @@ -0,0 +1,7 @@ +import {requireNativeModule} from 'expo-modules-core' + +const NativeModule = requireNativeModule('ExpoPlatformInfo') + +export function getIsReducedMotionEnabled(): boolean { + return NativeModule.getIsReducedMotionEnabled() +} diff --git a/modules/expo-bluesky-swiss-army/src/PlatformInfo/index.ts b/modules/expo-bluesky-swiss-army/src/PlatformInfo/index.ts new file mode 100644 index 00000000..9b9b7fc0 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/PlatformInfo/index.ts @@ -0,0 +1,5 @@ +import {NotImplementedError} from '../NotImplemented' + +export function getIsReducedMotionEnabled(): boolean { + throw new NotImplementedError() +} diff --git a/modules/expo-bluesky-swiss-army/src/PlatformInfo/index.web.ts b/modules/expo-bluesky-swiss-army/src/PlatformInfo/index.web.ts new file mode 100644 index 00000000..c7ae6b7c --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/PlatformInfo/index.web.ts @@ -0,0 +1,6 @@ +export function getIsReducedMotionEnabled(): boolean { + if (typeof window === 'undefined') { + return false + } + return window.matchMedia('(prefers-reduced-motion: reduce)').matches +} diff --git a/patches/react-native-reanimated+3.11.0.patch b/patches/react-native-reanimated+3.11.0.patch index 9147cf08..a79a0ac0 100644 --- a/patches/react-native-reanimated+3.11.0.patch +++ b/patches/react-native-reanimated+3.11.0.patch @@ -207,31 +207,3 @@ index 88b3fdf..2488ebc 100644 const { layout, entering, exiting, sharedTransitionTag } = this.props; if ( -diff --git a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js -index ac9be5d..86d4605 100644 ---- a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js -+++ b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js -@@ -47,4 +47,5 @@ export { LayoutAnimationConfig } from './component/LayoutAnimationConfig'; - export { PerformanceMonitor } from './component/PerformanceMonitor'; - export { startMapper, stopMapper } from './mappers'; - export { startScreenTransition, finishScreenTransition, ScreenTransition } from './screenTransition'; -+export { isReducedMotion } from './PlatformChecker'; - //# sourceMappingURL=index.js.map -diff --git a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts -index f01dc57..161ef22 100644 ---- a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts -+++ b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts -@@ -36,3 +36,4 @@ export type { FlatListPropsWithLayout } from './component/FlatList'; - export { startMapper, stopMapper } from './mappers'; - export { startScreenTransition, finishScreenTransition, ScreenTransition, } from './screenTransition'; - export type { AnimatedScreenTransition, GoBackGesture, ScreenTransitionConfig, } from './screenTransition'; -+export { isReducedMotion } from './PlatformChecker'; -diff --git a/node_modules/react-native-reanimated/src/reanimated2/index.ts b/node_modules/react-native-reanimated/src/reanimated2/index.ts -index 5885fa1..a3c693f 100644 ---- a/node_modules/react-native-reanimated/src/reanimated2/index.ts -+++ b/node_modules/react-native-reanimated/src/reanimated2/index.ts -@@ -284,3 +284,4 @@ export type { - GoBackGesture, - ScreenTransitionConfig, - } from './screenTransition'; -+export { isReducedMotion } from './PlatformChecker'; diff --git a/src/platform/detection.ts b/src/platform/detection.ts index 0c0360a8..f00df0ee 100644 --- a/src/platform/detection.ts +++ b/src/platform/detection.ts @@ -1,5 +1,4 @@ import {Platform} from 'react-native' -import {isReducedMotion} from 'react-native-reanimated' import {getLocales} from 'expo-localization' import {fixLegacyLanguageCode} from '#/locale/helpers' @@ -21,5 +20,3 @@ export const deviceLocales = dedupArray( .map?.(locale => fixLegacyLanguageCode(locale.languageCode)) .filter(code => typeof code === 'string'), ) as string[] - -export const prefersReducedMotion = isReducedMotion() diff --git a/src/state/a11y.tsx b/src/state/a11y.tsx index aefcfd1e..08948267 100644 --- a/src/state/a11y.tsx +++ b/src/state/a11y.tsx @@ -1,8 +1,8 @@ import React from 'react' import {AccessibilityInfo} from 'react-native' -import {isReducedMotion} from 'react-native-reanimated' import {isWeb} from '#/platform/detection' +import {PlatformInfo} from '../../modules/expo-bluesky-swiss-army' const Context = React.createContext({ reduceMotionEnabled: false, @@ -15,7 +15,7 @@ export function useA11y() { export function Provider({children}: React.PropsWithChildren<{}>) { const [reduceMotionEnabled, setReduceMotionEnabled] = React.useState(() => - isReducedMotion(), + PlatformInfo.getIsReducedMotionEnabled(), ) const [screenReaderEnabled, setScreenReaderEnabled] = React.useState(false) diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 88fc370a..399a7e79 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -1,6 +1,7 @@ import {z} from 'zod' -import {deviceLocales, prefersReducedMotion} from '#/platform/detection' +import {deviceLocales} from '#/platform/detection' +import {PlatformInfo} from '../../../modules/expo-bluesky-swiss-army' const externalEmbedOptions = ['show', 'hide'] as const @@ -128,7 +129,7 @@ export const defaults: Schema = { lastSelectedHomeFeed: undefined, pdsAddressHistory: [], disableHaptics: false, - disableAutoplay: prefersReducedMotion, + disableAutoplay: PlatformInfo.getIsReducedMotionEnabled(), kawaii: false, hasCheckedForStarterPack: false, } diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index ca2420fe..3a9f67de 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -9,6 +9,7 @@ import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as Prompt from '#/components/Prompt' import {H3, P, Text} from '#/components/Typography' +import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army' export function Dialogs() { const scrollable = Dialog.useDialogControl() @@ -17,6 +18,8 @@ export function Dialogs() { const testDialog = Dialog.useDialogControl() const {closeAllDialogs} = useDialogStateControlContext() const unmountTestDialog = Dialog.useDialogControl() + const [reducedMotionEnabled, setReducedMotionEnabled] = + React.useState() const [shouldRenderUnmountTest, setShouldRenderUnmountTest] = React.useState(false) const unmountTestInterval = React.useRef() @@ -147,6 +150,22 @@ export function Dialogs() { Open Shared Prefs Tester + + This is a prompt