diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index f9763ce43..a45da551e 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -7,7 +7,9 @@ import { GlobalActions } from '../../../global/types'; import { LeftColumnContent, ISettings } from '../../../types'; import { ApiChat } from '../../../api/types'; -import { APP_NAME, APP_VERSION, FEEDBACK_URL } from '../../../config'; +import { + ANIMATION_LEVEL_MAX, APP_NAME, APP_VERSION, FEEDBACK_URL, +} from '../../../config'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; import buildClassName from '../../../util/buildClassName'; import { pick } from '../../../util/iteratees'; @@ -137,7 +139,7 @@ const LeftMainHeader: FC = ({ setSettingOption({ theme: newTheme }); setSettingOption({ shouldUseSystemTheme: false }); - switchTheme(newTheme, animationLevel > 0); + switchTheme(newTheme, animationLevel === ANIMATION_LEVEL_MAX); }, [animationLevel, setSettingOption, theme]); const handleAnimationLevelChange = useCallback((e: React.SyntheticEvent) => { diff --git a/src/modules/actions/ui/initial.ts b/src/modules/actions/ui/initial.ts index b7f7122ab..c034d712e 100644 --- a/src/modules/actions/ui/initial.ts +++ b/src/modules/actions/ui/initial.ts @@ -1,5 +1,6 @@ -import { addReducer } from '../../../lib/teact/teactn'; +import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn'; +import { ANIMATION_LEVEL_MAX } from '../../../config'; import { IS_ANDROID, IS_IOS, IS_SAFARI, IS_TOUCH_ENV, } from '../../../util/environment'; @@ -7,6 +8,8 @@ import { setLanguage } from '../../../util/langProvider'; import switchTheme from '../../../util/switchTheme'; import { selectTheme } from '../../selectors'; +subscribeToSystemThemeChange(); + addReducer('init', (global) => { const { animationLevel, messageTextSize, language } = global.settings.byKey; const theme = selectTheme(global); @@ -17,7 +20,7 @@ addReducer('init', (global) => { document.body.classList.add('initial'); document.body.classList.add(`animation-level-${animationLevel}`); document.body.classList.add(IS_TOUCH_ENV ? 'is-touch-env' : 'is-pointer-env'); - switchTheme(theme, animationLevel > 0); + switchTheme(theme, animationLevel === ANIMATION_LEVEL_MAX); if (IS_SAFARI) { document.body.classList.add('is-safari'); @@ -64,3 +67,26 @@ addReducer('clearAuthError', (global) => { authError: undefined, }; }); + +function subscribeToSystemThemeChange() { + function handleSystemThemeChange() { + const currentThemeMatch = document.documentElement.className.match(/theme-(\w+)/); + const currentTheme = currentThemeMatch ? currentThemeMatch[1] : 'light'; + const global = getGlobal(); + const nextTheme = selectTheme(global); + const { animationLevel } = global.settings.byKey; + + if (nextTheme !== currentTheme) { + switchTheme(nextTheme, animationLevel === ANIMATION_LEVEL_MAX); + // Force-update component containers + setGlobal({ ...global }); + } + } + + const mql = window.matchMedia('(prefers-color-scheme: dark)'); + if (typeof mql.addEventListener === 'function') { + mql.addEventListener('change', handleSystemThemeChange); + } else if (typeof mql.addListener === 'function') { + mql.addListener(handleSystemThemeChange); + } +} diff --git a/src/modules/selectors/ui.ts b/src/modules/selectors/ui.ts index 3f72dbb2d..7e0e4f69e 100644 --- a/src/modules/selectors/ui.ts +++ b/src/modules/selectors/ui.ts @@ -1,7 +1,7 @@ import { GlobalState } from '../../global/types'; import { RightColumnContent } from '../../types'; -import { IS_SINGLE_COLUMN_LAYOUT, SYSTEM_THEME } from '../../util/environment'; +import { getSystemTheme, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; import { selectCurrentMessageList, selectIsPollResultsOpen } from './messages'; import { selectCurrentTextSearch } from './localSearch'; import { selectCurrentStickerSearch, selectCurrentGifSearch } from './symbols'; @@ -57,5 +57,5 @@ export function selectIsRightColumnShown(global: GlobalState) { export function selectTheme(global: GlobalState) { const { theme, shouldUseSystemTheme } = global.settings.byKey; - return shouldUseSystemTheme ? SYSTEM_THEME : theme; + return shouldUseSystemTheme ? getSystemTheme() : theme; } diff --git a/src/types/index.ts b/src/types/index.ts index 6d4bc96b8..b24af66c0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -21,6 +21,7 @@ export interface IAlbum { } export type ThemeKey = 'light' | 'dark'; + export interface IThemeSettings { background?: string; backgroundColor?: string; diff --git a/src/util/environment.ts b/src/util/environment.ts index aff0ab634..8dd96130f 100644 --- a/src/util/environment.ts +++ b/src/util/environment.ts @@ -5,6 +5,10 @@ import { IS_TEST, } from '../config'; +export * from './environmentWebp'; + +export * from './environmentSystemTheme'; + export function getPlatform() { const { userAgent, platform } = window.navigator; const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']; @@ -55,30 +59,3 @@ export const IS_CANVAS_FILTER_SUPPORTED = ( export const DPR = window.devicePixelRatio || 1; export const MASK_IMAGE_DISABLED = true; - -export const SYSTEM_THEME = ( - window && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches -) ? 'dark' : 'light'; - -let isWebpSupportedCache: boolean | undefined; - -export function isWebpSupported() { - return Boolean(isWebpSupportedCache); -} - -function testWebp(): Promise { - return new Promise((resolve) => { - const webp = new Image(); - // eslint-disable-next-line max-len - webp.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA'; - const handleLoadOrError = () => { - resolve(webp.height === 2); - }; - webp.onload = handleLoadOrError; - webp.onerror = handleLoadOrError; - }); -} - -testWebp().then((hasWebp) => { - isWebpSupportedCache = hasWebp; -}); diff --git a/src/util/environmentSystemTheme.ts b/src/util/environmentSystemTheme.ts new file mode 100644 index 000000000..3430a7a64 --- /dev/null +++ b/src/util/environmentSystemTheme.ts @@ -0,0 +1,20 @@ +import { ThemeKey } from '../types'; + +let systemThemeCache: ThemeKey = ( + window && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches +) ? 'dark' : 'light'; + +export function getSystemTheme() { + return systemThemeCache; +} + +function handleSystemThemeChange(e: MediaQueryListEventMap['change']) { + systemThemeCache = e.matches ? 'dark' : 'light'; +} + +const mql = window.matchMedia('(prefers-color-scheme: dark)'); +if (typeof mql.addEventListener === 'function') { + mql.addEventListener('change', handleSystemThemeChange); +} else if (typeof mql.addListener === 'function') { + mql.addListener(handleSystemThemeChange); +} diff --git a/src/util/environmentWebp.ts b/src/util/environmentWebp.ts new file mode 100644 index 000000000..4bf20dbb3 --- /dev/null +++ b/src/util/environmentWebp.ts @@ -0,0 +1,22 @@ +let isWebpSupportedCache: boolean | undefined; + +export function isWebpSupported() { + return Boolean(isWebpSupportedCache); +} + +function testWebp(): Promise { + return new Promise((resolve) => { + const webp = new Image(); + // eslint-disable-next-line max-len + webp.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA'; + const handleLoadOrError = () => { + resolve(webp.height === 2); + }; + webp.onload = handleLoadOrError; + webp.onerror = handleLoadOrError; + }); +} + +testWebp().then((hasWebp) => { + isWebpSupportedCache = hasWebp; +});