From 4e7c5b5819ee2467d69f4ab3fdc9afd88eba9795 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 4 May 2022 09:08:40 -0400 Subject: [PATCH 1/4] Support System theme --- app/soapbox/containers/soapbox.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index e6cd733d8..ffc19ad21 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -86,8 +86,17 @@ const SoapboxMount = () => { const [localeLoading, setLocaleLoading] = useState(true); const [isLoaded, setIsLoaded] = useState(false); + const colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: dark)'); + const [isSystemDarkMode, setSystemDarkMode] = useState(colorSchemeQueryList.matches); + const userTheme = settings.get('themeMode'); + const darkMode = userTheme === 'dark' || (userTheme === 'system' && isSystemDarkMode); + const themeCss = generateThemeCss(soapboxConfig); + const handleSystemModeChange = (event: MediaQueryListEvent) => { + setSystemDarkMode(event.matches); + }; + // Load the user's locale useEffect(() => { MESSAGES[locale]().then(messages => { @@ -105,6 +114,12 @@ const SoapboxMount = () => { }); }, []); + useEffect(() => { + colorSchemeQueryList.addEventListener('change', handleSystemModeChange); + + return () => colorSchemeQueryList.removeEventListener('change', handleSystemModeChange); + }, []); + // @ts-ignore: I don't actually know what these should be, lol const shouldUpdateScroll = (prevRouterProps, { location }) => { return !(location.state?.soapboxModalKey && location.state?.soapboxModalKey !== prevRouterProps?.location?.state?.soapboxModalKey); @@ -128,7 +143,7 @@ const SoapboxMount = () => { return ( - + {themeCss && } @@ -147,7 +162,7 @@ const SoapboxMount = () => { return ( - + {themeCss && } From 210cee2b7a6c48597bdd957d2346733d4743ea9e Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 4 May 2022 09:09:10 -0400 Subject: [PATCH 2/4] Move theme toggle to profile dropdown --- app/soapbox/features/soapbox_config/index.js | 2 +- app/soapbox/features/ui/components/navbar.tsx | 9 +-- .../ui/components/profile-dropdown.tsx | 31 +++++++-- .../features/ui/components/theme-toggle.tsx | 67 +++++++++++++++++++ .../features/ui/components/theme_toggle.tsx | 51 -------------- 5 files changed, 94 insertions(+), 66 deletions(-) create mode 100644 app/soapbox/features/ui/components/theme-toggle.tsx delete mode 100644 app/soapbox/features/ui/components/theme_toggle.tsx diff --git a/app/soapbox/features/soapbox_config/index.js b/app/soapbox/features/soapbox_config/index.js index 8c5a8816c..44317a1b7 100644 --- a/app/soapbox/features/soapbox_config/index.js +++ b/app/soapbox/features/soapbox_config/index.js @@ -23,7 +23,7 @@ import { FileChooserLogo, Checkbox, } from 'soapbox/features/forms'; -import ThemeToggle from 'soapbox/features/ui/components/theme_toggle'; +import ThemeToggle from 'soapbox/features/ui/components/theme-toggle'; import { isMobile } from 'soapbox/is_mobile'; import { normalizeSoapboxConfig } from 'soapbox/normalizers'; diff --git a/app/soapbox/features/ui/components/navbar.tsx b/app/soapbox/features/ui/components/navbar.tsx index 5a075eead..c6d810f0c 100644 --- a/app/soapbox/features/ui/components/navbar.tsx +++ b/app/soapbox/features/ui/components/navbar.tsx @@ -6,8 +6,7 @@ import { Link } from 'react-router-dom'; import { Avatar, Button, Icon } from 'soapbox/components/ui'; import Search from 'soapbox/features/compose/components/search'; -import ThemeToggle from 'soapbox/features/ui/components/theme_toggle'; -import { useOwnAccount, useSoapboxConfig, useSettings, useFeatures } from 'soapbox/hooks'; +import { useOwnAccount, useSoapboxConfig, useSettings } from 'soapbox/hooks'; import { openSidebar } from '../../../actions/sidebar'; @@ -19,7 +18,6 @@ const Navbar = () => { const account = useOwnAccount(); const settings = useSettings(); - const features = useFeatures(); const soapboxConfig = useSoapboxConfig(); const singleUserMode = soapboxConfig.get('singleUserMode'); @@ -69,11 +67,6 @@ const Navbar = () => {
- {/* TODO: make this available for everyone when it's ready (possibly in a different place) */} - {(features.darkMode || settings.get('isDeveloper')) && ( - - )} - {account ? (
diff --git a/app/soapbox/features/ui/components/profile-dropdown.tsx b/app/soapbox/features/ui/components/profile-dropdown.tsx index 034a6d099..7253b7ba6 100644 --- a/app/soapbox/features/ui/components/profile-dropdown.tsx +++ b/app/soapbox/features/ui/components/profile-dropdown.tsx @@ -7,11 +7,13 @@ import { Link } from 'react-router-dom'; import { logOut, switchAccount } from 'soapbox/actions/auth'; import { fetchOwnAccounts } from 'soapbox/actions/auth'; import { Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'soapbox/components/ui'; -import { useAppSelector } from 'soapbox/hooks'; +import { useAppSelector, useFeatures, useSettings } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; import Account from '../../../components/account'; +import ThemeToggle from './theme-toggle'; + import type { Account as AccountEntity } from 'soapbox/types/entities'; const messages = defineMessages({ @@ -24,9 +26,10 @@ interface IProfileDropdown { } type IMenuItem = { - text: string | React.ReactElement | null, - to?: string, - icon?: string, + text: string | React.ReactElement | null + to?: string + toggle?: JSX.Element + icon?: string action?: (event: React.MouseEvent) => void } @@ -34,6 +37,8 @@ const getAccount = makeGetAccount(); const ProfileDropdown: React.FC = ({ account, children }) => { const dispatch = useDispatch(); + const features = useFeatures(); + const settings = useSettings(); const intl = useIntl(); const authUsers = useAppSelector((state) => state.auth.get('users')); @@ -73,6 +78,12 @@ const ProfileDropdown: React.FC = ({ account, children }) => { } }); + if (features.darkMode || settings.get('isDeveloper')) { + menu.push({ text: null }); + + menu.push({ text: 'Theme', toggle: }); + } + menu.push({ text: null }); menu.push({ @@ -89,7 +100,7 @@ const ProfileDropdown: React.FC = ({ account, children }) => { }); return menu; - }, [account, authUsers]); + }, [account, authUsers, features]); React.useEffect(() => { fetchOwnAccountThrottled(); @@ -103,7 +114,15 @@ const ProfileDropdown: React.FC = ({ account, children }) => { {menu.map((menuItem, idx) => { - if (!menuItem.text) { + if (menuItem.toggle) { + return ( +
+ {menuItem.text} + + {menuItem.toggle} +
+ ); + } else if (!menuItem.text) { return ; } else { const Comp: any = menuItem.action ? MenuItem : MenuLink; diff --git a/app/soapbox/features/ui/components/theme-toggle.tsx b/app/soapbox/features/ui/components/theme-toggle.tsx new file mode 100644 index 000000000..60fc2bbf3 --- /dev/null +++ b/app/soapbox/features/ui/components/theme-toggle.tsx @@ -0,0 +1,67 @@ +import React, { useMemo } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useDispatch } from 'react-redux'; + +import { changeSetting } from 'soapbox/actions/settings'; +import { Icon } from 'soapbox/components/ui'; +import { useSettings } from 'soapbox/hooks'; + +const messages = defineMessages({ + switchToLight: { id: 'tabs_bar.theme_toggle_light', defaultMessage: 'Switch to light theme' }, + switchToDark: { id: 'tabs_bar.theme_toggle_dark', defaultMessage: 'Switch to dark theme' }, +}); + +interface IThemeToggle { + showLabel?: boolean, +} + +const ThemeToggle = ({ showLabel }: IThemeToggle) => { + const intl = useIntl(); + const dispatch = useDispatch(); + const themeMode = useSettings().get('themeMode'); + + const label = intl.formatMessage(themeMode === 'light' ? messages.switchToDark : messages.switchToLight); + + const onToggle = (event: React.ChangeEvent) => { + dispatch(changeSetting(['themeMode'], event.target.value)); + }; + + const themeIconSrc = useMemo(() => { + switch (themeMode) { + case 'system': + return require('@tabler/icons/icons/device-desktop.svg'); + case 'light': + return require('@tabler/icons/icons/sun.svg'); + case 'dark': + return require('@tabler/icons/icons/moon.svg'); + default: + return null; + } + }, [themeMode]); + + return ( + + ); +}; + +export default ThemeToggle; diff --git a/app/soapbox/features/ui/components/theme_toggle.tsx b/app/soapbox/features/ui/components/theme_toggle.tsx deleted file mode 100644 index e8c63c60f..000000000 --- a/app/soapbox/features/ui/components/theme_toggle.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import { defineMessages, useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; -import Toggle from 'react-toggle'; -import { v4 as uuidv4 } from 'uuid'; - -import { changeSetting } from 'soapbox/actions/settings'; -import { Icon } from 'soapbox/components/ui'; -import { useSettings } from 'soapbox/hooks'; - -const messages = defineMessages({ - switchToLight: { id: 'tabs_bar.theme_toggle_light', defaultMessage: 'Switch to light theme' }, - switchToDark: { id: 'tabs_bar.theme_toggle_dark', defaultMessage: 'Switch to dark theme' }, -}); - -interface IThemeToggle { - showLabel?: boolean, -} - -function ThemeToggle({ showLabel }: IThemeToggle) { - const intl = useIntl(); - const dispatch = useDispatch(); - const themeMode = useSettings().get('themeMode'); - - const id = uuidv4(); - const label = intl.formatMessage(themeMode === 'light' ? messages.switchToDark : messages.switchToLight); - - const onToggle = () => { - const setting = themeMode === 'light' ? 'dark' : 'light'; - dispatch(changeSetting(['themeMode'], setting)); - }; - - return ( -
-
- , - unchecked: , - }} - onChange={onToggle} - /> - {showLabel && ()} -
-
- ); -} - -export default ThemeToggle; From 528906fe3119b1d1353258a7a4b0e6138e0f199d Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 4 May 2022 09:35:45 -0400 Subject: [PATCH 3/4] Set 'system' as default theme --- app/soapbox/actions/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js index 098cdfdb1..05f8f310b 100644 --- a/app/soapbox/actions/settings.js +++ b/app/soapbox/actions/settings.js @@ -33,7 +33,7 @@ export const defaultSettings = ImmutableMap({ missingDescriptionModal: false, defaultPrivacy: 'public', defaultContentType: 'text/plain', - themeMode: 'light', + themeMode: 'system', locale: navigator.language.split(/[-_]/)[0] || 'en', showExplanationBox: true, explanationBox: true, From bf86d70a93d1f6964cf96485d9194c17a3cc1e32 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 4 May 2022 09:40:04 -0400 Subject: [PATCH 4/4] Use i18n --- .../features/ui/components/profile-dropdown.tsx | 3 ++- app/soapbox/features/ui/components/theme-toggle.tsx | 13 ++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/soapbox/features/ui/components/profile-dropdown.tsx b/app/soapbox/features/ui/components/profile-dropdown.tsx index 7253b7ba6..d21e491a2 100644 --- a/app/soapbox/features/ui/components/profile-dropdown.tsx +++ b/app/soapbox/features/ui/components/profile-dropdown.tsx @@ -18,6 +18,7 @@ import type { Account as AccountEntity } from 'soapbox/types/entities'; const messages = defineMessages({ add: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' }, + theme: { id: 'profile_dropdown.theme', defaultMessage: 'Theme' }, logout: { id: 'profile_dropdown.logout', defaultMessage: 'Log out @{acct}' }, }); @@ -81,7 +82,7 @@ const ProfileDropdown: React.FC = ({ account, children }) => { if (features.darkMode || settings.get('isDeveloper')) { menu.push({ text: null }); - menu.push({ text: 'Theme', toggle: }); + menu.push({ text: intl.formatMessage(messages.theme), toggle: }); } menu.push({ text: null }); diff --git a/app/soapbox/features/ui/components/theme-toggle.tsx b/app/soapbox/features/ui/components/theme-toggle.tsx index 60fc2bbf3..3d69a4afa 100644 --- a/app/soapbox/features/ui/components/theme-toggle.tsx +++ b/app/soapbox/features/ui/components/theme-toggle.tsx @@ -7,8 +7,9 @@ import { Icon } from 'soapbox/components/ui'; import { useSettings } from 'soapbox/hooks'; const messages = defineMessages({ - switchToLight: { id: 'tabs_bar.theme_toggle_light', defaultMessage: 'Switch to light theme' }, - switchToDark: { id: 'tabs_bar.theme_toggle_dark', defaultMessage: 'Switch to dark theme' }, + light: { id: 'theme_toggle.light', defaultMessage: 'Light' }, + dark: { id: 'theme_toggle.dark', defaultMessage: 'Dark' }, + system: { id: 'theme_toggle.system', defaultMessage: 'System' }, }); interface IThemeToggle { @@ -20,8 +21,6 @@ const ThemeToggle = ({ showLabel }: IThemeToggle) => { const dispatch = useDispatch(); const themeMode = useSettings().get('themeMode'); - const label = intl.formatMessage(themeMode === 'light' ? messages.switchToDark : messages.switchToLight); - const onToggle = (event: React.ChangeEvent) => { dispatch(changeSetting(['themeMode'], event.target.value)); }; @@ -51,9 +50,9 @@ const ThemeToggle = ({ showLabel }: IThemeToggle) => { defaultValue={themeMode} className='focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-800 dark:border-gray-600 block w-full pl-8 pr-12 sm:text-sm border-gray-300 rounded-md' > - - - + + +