diff --git a/native/account/terms-and-privacy-modal.react.js b/native/account/terms-and-privacy-modal.react.js new file mode 100644 index 000000000..a66cd6f3b --- /dev/null +++ b/native/account/terms-and-privacy-modal.react.js @@ -0,0 +1,190 @@ +// @flow + +import { useIsFocused } from '@react-navigation/native'; +import * as React from 'react'; +import { + ActivityIndicator, + BackHandler, + Linking, + Platform, + Text, + View, +} from 'react-native'; + +import { + policyAcknowledgment, + policyAcknowledgmentActionTypes, +} from 'lib/actions/user-actions'; +import { type PolicyType, policyTypes } from 'lib/facts/policies'; +import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; +import { + useDispatchActionPromise, + useServerCall, +} from 'lib/utils/action-utils'; +import { acknowledgePolicy } from 'lib/utils/policy-acknowledge-utlis'; + +import Button from '../components/button.react'; +import Modal from '../components/modal.react'; +import type { RootNavigationProp } from '../navigation/root-navigator.react'; +import type { NavigationRoute } from '../navigation/route-names'; +import { useSelector } from '../redux/redux-utils'; +import { useStyles } from '../themes/colors'; + +export type TermsAndPrivacyModalParams = { + +policyType: PolicyType, +}; + +type Props = { + +navigation: RootNavigationProp<'TermsAndPrivacyModal'>, + +route: NavigationRoute<'TermsAndPrivacyModal'>, +}; + +const loadingStatusSelector = createLoadingStatusSelector( + policyAcknowledgmentActionTypes, +); + +function TermsAndPrivacyModal(props: Props): React.Node { + const loadingStatus = useSelector(loadingStatusSelector); + const [acknowledgmentError, setAcknowledgmentError] = React.useState(''); + const sendAcknowledgmentRequest = useServerCall(policyAcknowledgment); + const dispatchActionPromise = useDispatchActionPromise(); + + const policyType = props.route.params.policyType; + const policyState = useSelector(store => store.userPolicies[policyType]); + const isAcknowledged = policyState?.isAcknowledged; + const isFocused = useIsFocused(); + + React.useEffect(() => { + if (isAcknowledged && isFocused) { + props.navigation.goBack(); + } + }, [isAcknowledged, props.navigation, isFocused]); + + const onAccept = React.useCallback(() => { + acknowledgePolicy( + policyTypes.tosAndPrivacyPolicy, + dispatchActionPromise, + sendAcknowledgmentRequest, + setAcknowledgmentError, + ); + }, [dispatchActionPromise, sendAcknowledgmentRequest]); + + const styles = useStyles(unboundStyles); + + const buttonContent = React.useMemo(() => { + if (loadingStatus === 'loading') { + return ( + + + + ); + } + return I accept; + }, [loadingStatus, styles.buttonText, styles.loading]); + + const onBackPress = props.navigation.isFocused; + React.useEffect(() => { + BackHandler.addEventListener('hardwareBackPress', onBackPress); + return () => { + BackHandler.removeEventListener('hardwareBackPress', onBackPress); + }; + }, [onBackPress]); + + const safeAreaEdges = ['top', 'bottom']; + return ( + + Terms of Service and Privacy Policy + + We recently updated our  + + Terms of Service + +  &  + + Privacy Policy + + + . In order to continue using Comm, we're asking you to read + through, acknowledge, and accept the updated policies. + + + + + + {acknowledgmentError} + + + ); +} + +const unboundStyles = { + modal: { + backgroundColor: 'modalForeground', + paddingBottom: 10, + paddingTop: 32, + paddingHorizontal: 32, + flex: 0, + borderColor: 'modalForegroundBorder', + }, + header: { + color: 'modalForegroundLabel', + fontSize: 20, + fontWeight: '600', + textAlign: 'center', + paddingBottom: 16, + }, + label: { + color: 'modalForegroundSecondaryLabel', + fontSize: 16, + lineHeight: 20, + textAlign: 'center', + }, + link: { + color: 'purpleLink', + fontWeight: 'bold', + }, + buttonsContainer: { + flexDirection: 'column', + marginTop: 24, + height: 72, + paddingHorizontal: 16, + }, + button: { + borderRadius: 5, + height: 48, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'purpleButton', + }, + buttonText: { + color: 'white', + fontSize: 16, + fontWeight: '600', + textAlign: 'center', + }, + error: { + marginTop: 6, + fontStyle: 'italic', + color: 'redText', + textAlign: 'center', + }, + loading: { + paddingTop: Platform.OS === 'android' ? 0 : 6, + }, +}; + +const onTermsOfUsePressed = () => { + Linking.openURL('https://comm.app/terms'); +}; + +const onPrivacyPolicyPressed = () => { + Linking.openURL('https://comm.app/privacy'); +}; + +export default TermsAndPrivacyModal; diff --git a/native/components/modal.react.js b/native/components/modal.react.js index 94af3f67c..06d9df892 100644 --- a/native/components/modal.react.js +++ b/native/components/modal.react.js @@ -1,63 +1,67 @@ // @flow import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { View, TouchableWithoutFeedback, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useStyles } from '../themes/colors'; import type { ViewStyle } from '../types/styles'; import KeyboardAvoidingView from './keyboard-avoiding-view.react'; type Props = $ReadOnly<{ +children: React.Node, +containerStyle?: ViewStyle, +modalStyle?: ViewStyle, +safeAreaEdges?: $ReadOnlyArray<'top' | 'right' | 'bottom' | 'left'>, + +disableClosing?: boolean, }>; function Modal(props: Props): React.Node { const navigation = useNavigation(); const close = React.useCallback(() => { + if (props.disableClosing) { + return; + } if (navigation.isFocused()) { navigation.goBack(); } - }, [navigation]); + }, [navigation, props.disableClosing]); const styles = useStyles(unboundStyles); const { containerStyle, modalStyle, children, safeAreaEdges } = props; return ( {children} ); } const unboundStyles = { container: { flex: 1, justifyContent: 'center', overflow: 'visible', }, modal: { backgroundColor: 'modalBackground', borderColor: 'modalForegroundBorder', borderWidth: 2, borderRadius: 5, flex: 1, justifyContent: 'center', marginBottom: 30, marginHorizontal: 15, marginTop: 100, padding: 12, }, }; export default Modal; diff --git a/native/navigation/root-navigator.react.js b/native/navigation/root-navigator.react.js index cb49e73a0..1b859061e 100644 --- a/native/navigation/root-navigator.react.js +++ b/native/navigation/root-navigator.react.js @@ -1,222 +1,233 @@ // @flow import { createNavigatorFactory, useNavigationBuilder, type StackNavigationState, type StackOptions, type StackNavigationEventMap, type StackNavigatorProps, type ExtraStackNavigatorProps, type ParamListBase, type StackNavigationHelpers, type StackNavigationProp, } from '@react-navigation/native'; import { StackView, TransitionPresets } from '@react-navigation/stack'; import * as React from 'react'; import { Platform } from 'react-native'; import { enableScreens } from 'react-native-screens'; import LoggedOutModal from '../account/logged-out-modal.react'; +import TermsAndPrivacyModal from '../account/terms-and-privacy-modal.react'; import ThreadPickerModal from '../calendar/thread-picker-modal.react'; import ImagePasteModal from '../chat/image-paste-modal.react'; import AddUsersModal from '../chat/settings/add-users-modal.react'; import ColorSelectorModal from '../chat/settings/color-selector-modal.react'; import ComposeSubchannelModal from '../chat/settings/compose-subchannel-modal.react'; import SidebarListModal from '../chat/sidebar-list-modal.react'; import CustomServerModal from '../profile/custom-server-modal.react'; import AppNavigator from './app-navigator.react'; import { defaultStackScreenOptions } from './options'; import { RootNavigatorContext } from './root-navigator-context'; import RootRouter, { type RootRouterExtraNavigationHelpers, } from './root-router'; import { LoggedOutModalRouteName, AppRouteName, ThreadPickerModalRouteName, ImagePasteModalRouteName, AddUsersModalRouteName, CustomServerModalRouteName, ColorSelectorModalRouteName, ComposeSubchannelModalRouteName, SidebarListModalRouteName, type ScreenParamList, type RootParamList, + TermsAndPrivacyRouteName, } from './route-names'; enableScreens(); export type RootNavigationHelpers = { ...$Exact>, ...RootRouterExtraNavigationHelpers, ... }; type RootNavigatorProps = StackNavigatorProps>; function RootNavigator({ initialRouteName, children, screenOptions, defaultScreenOptions, screenListeners, id, ...rest }: RootNavigatorProps) { const [keyboardHandlingEnabled, setKeyboardHandlingEnabled] = React.useState( true, ); const mergedScreenOptions = React.useMemo(() => { if (typeof screenOptions === 'function') { return input => ({ ...screenOptions(input), keyboardHandlingEnabled, }); } return { ...screenOptions, keyboardHandlingEnabled, }; }, [screenOptions, keyboardHandlingEnabled]); const { state, descriptors, navigation } = useNavigationBuilder(RootRouter, { id, initialRouteName, children, screenOptions: mergedScreenOptions, defaultScreenOptions, screenListeners, }); const rootNavigationContext = React.useMemo( () => ({ setKeyboardHandlingEnabled }), [setKeyboardHandlingEnabled], ); return ( ); } const createRootNavigator = createNavigatorFactory< StackNavigationState, StackOptions, StackNavigationEventMap, RootNavigationHelpers<>, ExtraStackNavigatorProps, >(RootNavigator); const baseTransitionPreset = Platform.select({ ios: TransitionPresets.ModalSlideFromBottomIOS, default: TransitionPresets.FadeFromBottomAndroid, }); const transitionPreset = { ...baseTransitionPreset, cardStyleInterpolator: interpolatorProps => { const baseCardStyleInterpolator = baseTransitionPreset.cardStyleInterpolator( interpolatorProps, ); const overlayOpacity = interpolatorProps.current.progress.interpolate({ inputRange: [0, 1], outputRange: ([0, 0.7]: number[]), // Flow... extrapolate: 'clamp', }); return { ...baseCardStyleInterpolator, overlayStyle: [ baseCardStyleInterpolator.overlayStyle, { opacity: overlayOpacity }, ], }; }, }; const defaultScreenOptions = { ...defaultStackScreenOptions, ...transitionPreset, cardStyle: { backgroundColor: 'transparent' }, presentation: 'modal', headerShown: false, }; const disableGesturesScreenOptions = { gestureEnabled: false, }; const modalOverlayScreenOptions = { cardOverlayEnabled: true, }; +const termsAndPrivacyModalScreenOptions = { + gestureEnabled: false, + cardOverlayEnabled: true, +}; export type RootRouterNavigationProp< ParamList: ParamListBase = ParamListBase, RouteName: $Keys = $Keys, > = { ...StackNavigationProp, ...RootRouterExtraNavigationHelpers, }; export type RootNavigationProp< RouteName: $Keys = $Keys, > = { ...StackNavigationProp, ...RootRouterExtraNavigationHelpers, }; const Root = createRootNavigator< ScreenParamList, RootParamList, RootNavigationHelpers, >(); function RootComponent(): React.Node { return ( + ); } export default RootComponent; diff --git a/native/navigation/route-names.js b/native/navigation/route-names.js index e04a91a9c..c5e8114af 100644 --- a/native/navigation/route-names.js +++ b/native/navigation/route-names.js @@ -1,174 +1,177 @@ // @flow import type { RouteProp } from '@react-navigation/native'; +import type { TermsAndPrivacyModalParams } from '../account/terms-and-privacy-modal.react'; import type { ThreadPickerModalParams } from '../calendar/thread-picker-modal.react'; import type { ComposeSubchannelParams } from '../chat/compose-subchannel.react'; import type { ImagePasteModalParams } from '../chat/image-paste-modal.react'; import type { MessageListParams } from '../chat/message-list-types'; import type { MultimediaMessageTooltipModalParams } from '../chat/multimedia-message-tooltip-modal.react'; import type { RobotextMessageTooltipModalParams } from '../chat/robotext-message-tooltip-modal.react'; import type { AddUsersModalParams } from '../chat/settings/add-users-modal.react'; import type { ColorSelectorModalParams } from '../chat/settings/color-selector-modal.react'; import type { ComposeSubchannelModalParams } from '../chat/settings/compose-subchannel-modal.react'; import type { DeleteThreadParams } from '../chat/settings/delete-thread.react'; import type { ThreadSettingsMemberTooltipModalParams } from '../chat/settings/thread-settings-member-tooltip-modal.react'; import type { ThreadSettingsParams } from '../chat/settings/thread-settings.react'; import type { SidebarListModalParams } from '../chat/sidebar-list-modal.react'; import type { TextMessageTooltipModalParams } from '../chat/text-message-tooltip-modal.react'; import type { CameraModalParams } from '../media/camera-modal.react'; import type { ImageModalParams } from '../media/image-modal.react'; import type { VideoPlaybackModalParams } from '../media/video-playback-modal.react'; import type { CustomServerModalParams } from '../profile/custom-server-modal.react'; import type { RelationshipListItemTooltipModalParams } from '../profile/relationship-list-item-tooltip-modal.react'; import type { ActionResultModalParams } from './action-result-modal.react'; export const ActionResultModalRouteName = 'ActionResultModal'; export const AddUsersModalRouteName = 'AddUsersModal'; export const AppearancePreferencesRouteName = 'AppearancePreferences'; export const AppRouteName = 'App'; export const AppsRouteName = 'Apps'; export const BackgroundChatThreadListRouteName = 'BackgroundChatThreadList'; export const BlockListRouteName = 'BlockList'; export const BuildInfoRouteName = 'BuildInfo'; export const CalendarRouteName = 'Calendar'; export const CameraModalRouteName = 'CameraModal'; export const ChatRouteName = 'Chat'; export const ChatThreadListRouteName = 'ChatThreadList'; export const ColorSelectorModalRouteName = 'ColorSelectorModal'; export const ComposeSubchannelModalRouteName = 'ComposeSubchannelModal'; export const ComposeSubchannelRouteName = 'ComposeSubchannel'; export const CustomServerModalRouteName = 'CustomServerModal'; export const DefaultNotificationsPreferencesRouteName = 'DefaultNotifications'; export const DeleteAccountRouteName = 'DeleteAccount'; export const DeleteThreadRouteName = 'DeleteThread'; export const DevToolsRouteName = 'DevTools'; export const EditPasswordRouteName = 'EditPassword'; export const FriendListRouteName = 'FriendList'; export const HomeChatThreadListRouteName = 'HomeChatThreadList'; export const ImageModalRouteName = 'ImageModal'; export const ImagePasteModalRouteName = 'ImagePasteModal'; export const LoggedOutModalRouteName = 'LoggedOutModal'; export const MessageListRouteName = 'MessageList'; export const MultimediaMessageTooltipModalRouteName = 'MultimediaMessageTooltipModal'; export const PrivacyPreferencesRouteName = 'PrivacyPreferences'; export const ProfileRouteName = 'Profile'; export const ProfileScreenRouteName = 'ProfileScreen'; export const RelationshipListItemTooltipModalRouteName = 'RelationshipListItemTooltipModal'; export const RobotextMessageTooltipModalRouteName = 'RobotextMessageTooltipModal'; export const SidebarListModalRouteName = 'SidebarListModal'; export const TabNavigatorRouteName = 'TabNavigator'; export const TextMessageTooltipModalRouteName = 'TextMessageTooltipModal'; export const ThreadPickerModalRouteName = 'ThreadPickerModal'; export const ThreadSettingsMemberTooltipModalRouteName = 'ThreadSettingsMemberTooltipModal'; export const ThreadSettingsRouteName = 'ThreadSettings'; export const VideoPlaybackModalRouteName = 'VideoPlaybackModal'; +export const TermsAndPrivacyRouteName = 'TermsAndPrivacyModal'; export type RootParamList = { +LoggedOutModal: void, +App: void, +ThreadPickerModal: ThreadPickerModalParams, +AddUsersModal: AddUsersModalParams, +CustomServerModal: CustomServerModalParams, +ColorSelectorModal: ColorSelectorModalParams, +ComposeSubchannelModal: ComposeSubchannelModalParams, +SidebarListModal: SidebarListModalParams, +ImagePasteModal: ImagePasteModalParams, + +TermsAndPrivacyModal: TermsAndPrivacyModalParams, }; export type MessageTooltipRouteNames = | typeof RobotextMessageTooltipModalRouteName | typeof MultimediaMessageTooltipModalRouteName | typeof TextMessageTooltipModalRouteName; export type TooltipModalParamList = { +MultimediaMessageTooltipModal: MultimediaMessageTooltipModalParams, +TextMessageTooltipModal: TextMessageTooltipModalParams, +ThreadSettingsMemberTooltipModal: ThreadSettingsMemberTooltipModalParams, +RelationshipListItemTooltipModal: RelationshipListItemTooltipModalParams, +RobotextMessageTooltipModal: RobotextMessageTooltipModalParams, }; export type OverlayParamList = { +TabNavigator: void, +ImageModal: ImageModalParams, +ActionResultModal: ActionResultModalParams, +CameraModal: CameraModalParams, +VideoPlaybackModal: VideoPlaybackModalParams, ...TooltipModalParamList, }; export type TabParamList = { +Calendar: void, +Chat: void, +Profile: void, +Apps: void, }; export type ChatParamList = { +ChatThreadList: void, +MessageList: MessageListParams, +ComposeSubchannel: ComposeSubchannelParams, +ThreadSettings: ThreadSettingsParams, +DeleteThread: DeleteThreadParams, }; export type ChatTopTabsParamList = { +HomeChatThreadList: void, +BackgroundChatThreadList: void, }; export type ProfileParamList = { +ProfileScreen: void, +EditPassword: void, +DeleteAccount: void, +BuildInfo: void, +DevTools: void, +AppearancePreferences: void, +PrivacyPreferences: void, +DefaultNotifications: void, +FriendList: void, +BlockList: void, }; export type ScreenParamList = { ...RootParamList, ...OverlayParamList, ...TabParamList, ...ChatParamList, ...ChatTopTabsParamList, ...ProfileParamList, }; export type NavigationRoute< RouteName: string = $Keys, > = RouteProp; export const accountModals = [LoggedOutModalRouteName]; export const scrollBlockingModals = [ ImageModalRouteName, MultimediaMessageTooltipModalRouteName, TextMessageTooltipModalRouteName, ThreadSettingsMemberTooltipModalRouteName, RelationshipListItemTooltipModalRouteName, RobotextMessageTooltipModalRouteName, VideoPlaybackModalRouteName, ]; export const chatRootModals = [ AddUsersModalRouteName, ColorSelectorModalRouteName, ComposeSubchannelModalRouteName, ]; export const threadRoutes = [ MessageListRouteName, ThreadSettingsRouteName, DeleteThreadRouteName, ComposeSubchannelRouteName, ]; diff --git a/native/themes/colors.js b/native/themes/colors.js index 4dee4e54a..6c2bd0c9a 100644 --- a/native/themes/colors.js +++ b/native/themes/colors.js @@ -1,294 +1,298 @@ // @flow import * as React from 'react'; import { StyleSheet } from 'react-native'; import { createSelector } from 'reselect'; import { selectBackgroundIsDark } from '../navigation/nav-selectors'; import { NavContext } from '../navigation/navigation-context'; import { useSelector } from '../redux/redux-utils'; import type { AppState } from '../redux/state-types'; import type { GlobalTheme } from '../types/themes'; const light = Object.freeze({ blockQuoteBackground: '#D3D3D3', blockQuoteBorder: '#C0C0C0', codeBackground: '#DCDCDC', disabledButton: '#D3D3D3', disconnectedBarBackground: '#F5F5F5', editButton: '#A4A4A2', floatingButtonBackground: '#999999', floatingButtonLabel: '#EEEEEE', greenButton: '#6EC472', greenText: 'green', headerChevron: '#0A0A0A', inlineSidebarBackground: '#E0E0E0', inlineSidebarLabel: '#000000', link: '#036AFF', listBackground: 'white', listBackgroundLabel: 'black', listBackgroundSecondaryLabel: '#444444', listBackgroundTernaryLabel: '#999999', listChatBubble: '#F1F0F5', listForegroundLabel: 'black', listForegroundQuaternaryLabel: '#AAAAAA', listForegroundSecondaryLabel: '#333333', listForegroundTertiaryLabel: '#666666', listInputBackground: '#F5F5F5', listInputBar: '#E2E2E2', listInputBorder: '#AAAAAAAA', listInputButton: '#8E8D92', listIosHighlightUnderlay: '#DDDDDDDD', listSearchBackground: '#F5F5F5', listSearchIcon: '#8E8D92', listSeparator: '#EEEEEE', listSeparatorLabel: '#555555', mintButton: '#44CC99', modalBackground: '#EEEEEE', modalBackgroundLabel: '#333333', modalBackgroundSecondaryLabel: '#AAAAAA', modalButton: '#BBBBBB', modalButtonLabel: 'black', modalContrastBackground: 'black', modalContrastForegroundLabel: 'white', modalContrastOpacity: 0.7, modalForeground: 'white', modalForegroundBorder: '#CCCCCC', modalForegroundLabel: 'black', modalForegroundSecondaryLabel: '#888888', modalForegroundTertiaryLabel: '#AAAAAA', modalIosHighlightUnderlay: '#CCCCCCDD', modalSubtext: '#CCCCCC', modalSubtextLabel: '#555555', navigationCard: '#FFFFFF', navigationChevron: '#BAB9BE', panelBackground: '#F5F5F5', panelBackgroundLabel: '#888888', panelForeground: 'white', panelForegroundBorder: '#CCCCCC', panelForegroundLabel: 'black', panelForegroundSecondaryLabel: '#333333', panelForegroundTertiaryLabel: '#888888', panelIosHighlightUnderlay: '#EEEEEEDD', panelSecondaryForeground: '#F5F5F5', panelSecondaryForegroundBorder: '#D1D1D6', + purpleLink: '#7E57C2', + purpleButton: '#7E57C2', redButton: '#BB8888', redText: '#FF4444', spoiler: '#33332C', tabBarAccent: '#7E57C2', tabBarBackground: '#F5F5F5', tabBarActiveTintColor: '#7E57C2', vibrantGreenButton: '#00C853', vibrantRedButton: '#F53100', tooltipBackground: '#E0E0E0', }); export type Colors = $Exact; const dark: Colors = Object.freeze({ blockQuoteBackground: '#A9A9A9', blockQuoteBorder: '#808080', codeBackground: '#0A0A0A', disabledButton: '#444444', disconnectedBarBackground: '#1D1D1D', editButton: '#5B5B5D', floatingButtonBackground: '#666666', floatingButtonLabel: 'white', greenButton: '#43A047', greenText: '#44FF44', headerChevron: '#FFFFFF', inlineSidebarBackground: '#666666', inlineSidebarLabel: '#FFFFFF', link: '#129AFF', listBackground: '#0A0A0A', listBackgroundLabel: '#C7C7CC', listBackgroundSecondaryLabel: '#BBBBBB', listBackgroundTernaryLabel: '#888888', listChatBubble: '#26252A', listForegroundLabel: 'white', listForegroundQuaternaryLabel: '#555555', listForegroundSecondaryLabel: '#CCCCCC', listForegroundTertiaryLabel: '#999999', listInputBackground: '#1D1D1D', listInputBar: '#555555', listInputBorder: '#333333', listInputButton: '#AAAAAA', listIosHighlightUnderlay: '#BBBBBB88', listSearchBackground: '#1D1D1D', listSearchIcon: '#AAAAAA', listSeparator: '#3A3A3C', listSeparatorLabel: '#EEEEEE', mintButton: '#44CC99', modalBackground: '#0A0A0A', modalBackgroundLabel: '#CCCCCC', modalBackgroundSecondaryLabel: '#555555', modalButton: '#666666', modalButtonLabel: 'white', modalContrastBackground: 'white', modalContrastForegroundLabel: 'black', modalContrastOpacity: 0.85, modalForeground: '#1C1C1E', modalForegroundBorder: '#1C1C1E', modalForegroundLabel: 'white', modalForegroundSecondaryLabel: '#AAAAAA', modalForegroundTertiaryLabel: '#666666', modalIosHighlightUnderlay: '#AAAAAA88', modalSubtext: '#444444', modalSubtextLabel: '#AAAAAA', navigationCard: '#2A2A2A', navigationChevron: '#5B5B5D', panelBackground: '#0A0A0A', panelBackgroundLabel: '#C7C7CC', panelForeground: '#1D1D1D', panelForegroundBorder: '#2C2C2E', panelForegroundLabel: 'white', panelForegroundSecondaryLabel: '#CCCCCC', panelForegroundTertiaryLabel: '#AAAAAA', panelIosHighlightUnderlay: '#313035', panelSecondaryForeground: '#333333', panelSecondaryForegroundBorder: '#666666', + purpleLink: '#AE94DB', + purpleButton: '#7E57C2', redButton: '#FF4444', redText: '#FF4444', spoiler: '#33332C', tabBarAccent: '#AE94DB', tabBarBackground: '#0A0A0A', tabBarActiveTintColor: '#AE94DB', vibrantGreenButton: '#00C853', vibrantRedButton: '#F53100', tooltipBackground: '#1F1F1F', }); const colors = { light, dark }; const colorsSelector: (state: AppState) => Colors = createSelector( (state: AppState) => state.globalThemeInfo.activeTheme, (theme: ?GlobalTheme) => { const explicitTheme = theme ? theme : 'light'; return colors[explicitTheme]; }, ); const magicStrings = new Set(); for (const theme in colors) { for (const magicString in colors[theme]) { magicStrings.add(magicString); } } type Styles = { [name: string]: { [field: string]: mixed } }; type ReplaceField = (input: any) => any; export type StyleSheetOf = $ObjMap; function stylesFromColors( obj: IS, themeColors: Colors, ): StyleSheetOf { const result = {}; for (const key in obj) { const style = obj[key]; const filledInStyle = { ...style }; for (const styleKey in style) { const styleValue = style[styleKey]; if (typeof styleValue !== 'string') { continue; } if (magicStrings.has(styleValue)) { const mapped = themeColors[styleValue]; if (mapped) { filledInStyle[styleKey] = mapped; } } } result[key] = filledInStyle; } return StyleSheet.create(result); } function styleSelector( obj: IS, ): (state: AppState) => StyleSheetOf { return createSelector(colorsSelector, (themeColors: Colors) => stylesFromColors(obj, themeColors), ); } function useStyles(obj: IS): StyleSheetOf { const ourColors = useColors(); return React.useMemo(() => stylesFromColors(obj, ourColors), [ obj, ourColors, ]); } function useOverlayStyles(obj: IS): StyleSheetOf { const navContext = React.useContext(NavContext); const navigationState = navContext && navContext.state; const theme = useSelector( (state: AppState) => state.globalThemeInfo.activeTheme, ); const backgroundIsDark = React.useMemo( () => selectBackgroundIsDark(navigationState, theme), [navigationState, theme], ); const syntheticTheme = backgroundIsDark ? 'dark' : 'light'; return React.useMemo(() => stylesFromColors(obj, colors[syntheticTheme]), [ obj, syntheticTheme, ]); } function useColors(): Colors { return useSelector(colorsSelector); } function getStylesForTheme( obj: IS, theme: GlobalTheme, ): StyleSheetOf { return stylesFromColors(obj, colors[theme]); } export type IndicatorStyle = 'white' | 'black'; function useIndicatorStyle(): IndicatorStyle { const theme = useSelector( (state: AppState) => state.globalThemeInfo.activeTheme, ); return theme && theme === 'dark' ? 'white' : 'black'; } const indicatorStyleSelector: ( state: AppState, ) => IndicatorStyle = createSelector( (state: AppState) => state.globalThemeInfo.activeTheme, (theme: ?GlobalTheme) => { return theme && theme === 'dark' ? 'white' : 'black'; }, ); export type KeyboardAppearance = 'default' | 'light' | 'dark'; const keyboardAppearanceSelector: ( state: AppState, ) => KeyboardAppearance = createSelector( (state: AppState) => state.globalThemeInfo.activeTheme, (theme: ?GlobalTheme) => { return theme && theme === 'dark' ? 'dark' : 'light'; }, ); function useKeyboardAppearance(): KeyboardAppearance { return useSelector(keyboardAppearanceSelector); } export { colors, colorsSelector, styleSelector, useStyles, useOverlayStyles, useColors, getStylesForTheme, useIndicatorStyle, indicatorStyleSelector, useKeyboardAppearance, };