/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @flow strict-local */ import type {EventSubscription} from '../vendor/emitter/EventEmitter'; import type {AppearancePreferences, ColorSchemeName} from './NativeAppearance'; import typeof INativeAppearance from './NativeAppearance'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import EventEmitter from '../vendor/emitter/EventEmitter'; import {isAsyncDebugging} from './DebugEnvironment'; import invariant from 'invariant'; type Appearance = { colorScheme: ?ColorSchemeName, }; let lazyState: ?{ +NativeAppearance: INativeAppearance, // Cache the color scheme to reduce the cost of reading it between changes. // NOTE: If `NativeAppearance` is null, this will always be null. appearance: ?Appearance, // NOTE: This is non-nullable to make it easier for `onChangedListener` to // return a non-nullable `EventSubscription` value. This is not the common // path, so we do not have to over-optimize it. +eventEmitter: EventEmitter<{change: [Appearance]}>, }; /** * Ensures that all state and listeners are lazily initialized correctly. */ function getState(): $NonMaybeType { if (lazyState != null) { return lazyState; } const eventEmitter = new EventEmitter<{change: [Appearance]}>(); // NOTE: Avoid initializing `NativeAppearance` until it is actually used. const NativeAppearance = require('./NativeAppearance').default; if (NativeAppearance == null) { // Assign `null` to avoid re-initializing on subsequent invocations. lazyState = { NativeAppearance: null, appearance: null, eventEmitter, }; } else { const state: $NonMaybeType = { NativeAppearance, appearance: null, eventEmitter, }; new NativeEventEmitter<{ appearanceChanged: [AppearancePreferences], }>(NativeAppearance).addListener('appearanceChanged', newAppearance => { state.appearance = { colorScheme: toColorScheme(newAppearance.colorScheme), }; eventEmitter.emit('change', state.appearance); }); lazyState = state; } return lazyState; } /** * Returns the current color scheme preference. This value may change, so the * value should not be cached without either listening to changes or using * the `useColorScheme` hook. */ export function getColorScheme(): ?ColorSchemeName { if (__DEV__) { if (isAsyncDebugging) { // Hard code light theme when using the async debugger as // sync calls aren't supported return 'light'; } } let colorScheme = null; const state = getState(); const {NativeAppearance} = state; if (NativeAppearance != null) { if (state.appearance == null) { // Lazily initialize `state.appearance`. This should only // happen once because we never reassign a null value to it. state.appearance = { colorScheme: toColorScheme(NativeAppearance.getColorScheme()), }; } colorScheme = state.appearance.colorScheme; } return colorScheme; } /** * Updates the current color scheme to the supplied value. */ export function setColorScheme(colorScheme: ?ColorSchemeName): void { const state = getState(); const {NativeAppearance} = state; if (NativeAppearance != null) { NativeAppearance.setColorScheme(colorScheme ?? 'unspecified'); state.appearance = { colorScheme: toColorScheme(NativeAppearance.getColorScheme()), }; } } /** * Add an event handler that is fired when appearance preferences change. */ export function addChangeListener( listener: ({colorScheme: ?ColorSchemeName}) => void, ): EventSubscription { const {eventEmitter} = getState(); return eventEmitter.addListener('change', listener); } /** * TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union */ function toColorScheme(colorScheme: ?string): ?ColorSchemeName { invariant( colorScheme === 'dark' || colorScheme === 'light' || colorScheme == null, "Unrecognized color scheme. Did you mean 'dark', 'light' or null?", ); return colorScheme; }