Page MenuHomePhabricator

No OneTemporary

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 (
+ <View style={styles.loading}>
+ <ActivityIndicator size="small" color="#D3D3D3" />
+ </View>
+ );
+ }
+ return <Text style={styles.buttonText}>I accept</Text>;
+ }, [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 (
+ <Modal
+ disableClosing={true}
+ modalStyle={styles.modal}
+ safeAreaEdges={safeAreaEdges}
+ >
+ <Text style={styles.header}>Terms of Service and Privacy Policy</Text>
+ <Text style={styles.label}>
+ <Text>We recently updated our&nbsp;</Text>
+ <Text style={styles.link} onPress={onTermsOfUsePressed}>
+ Terms of Service
+ </Text>
+ <Text>&nbsp;&amp;&nbsp;</Text>
+ <Text style={styles.link} onPress={onPrivacyPolicyPressed}>
+ Privacy Policy
+ </Text>
+ <Text>
+ . In order to continue using Comm, we&apos;re asking you to read
+ through, acknowledge, and accept the updated policies.
+ </Text>
+ </Text>
+
+ <View style={styles.buttonsContainer}>
+ <Button style={styles.button} onPress={onAccept}>
+ <Text style={styles.buttonText}>{buttonContent}</Text>
+ </Button>
+ <Text style={styles.error}>{acknowledgmentError}</Text>
+ </View>
+ </Modal>
+ );
+}
+
+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 (
<SafeAreaView style={styles.container} edges={safeAreaEdges}>
<KeyboardAvoidingView
behavior="padding"
style={[styles.container, containerStyle]}
>
<TouchableWithoutFeedback onPress={close}>
<View style={StyleSheet.absoluteFill} />
</TouchableWithoutFeedback>
<View style={[styles.modal, modalStyle]}>{children}</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
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<ParamList: ParamListBase = ParamListBase> = {
...$Exact<StackNavigationHelpers<ParamList>>,
...RootRouterExtraNavigationHelpers,
...
};
type RootNavigatorProps = StackNavigatorProps<RootNavigationHelpers<>>;
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 (
<RootNavigatorContext.Provider value={rootNavigationContext}>
<StackView
{...rest}
state={state}
descriptors={descriptors}
navigation={navigation}
detachInactiveScreens={Platform.OS !== 'ios'}
/>
</RootNavigatorContext.Provider>
);
}
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<ParamList> = $Keys<ParamList>,
> = {
...StackNavigationProp<ParamList, RouteName>,
...RootRouterExtraNavigationHelpers,
};
export type RootNavigationProp<
RouteName: $Keys<ScreenParamList> = $Keys<ScreenParamList>,
> = {
...StackNavigationProp<ScreenParamList, RouteName>,
...RootRouterExtraNavigationHelpers,
};
const Root = createRootNavigator<
ScreenParamList,
RootParamList,
RootNavigationHelpers<ScreenParamList>,
>();
function RootComponent(): React.Node {
return (
<Root.Navigator screenOptions={defaultScreenOptions}>
<Root.Screen
name={LoggedOutModalRouteName}
component={LoggedOutModal}
options={disableGesturesScreenOptions}
/>
<Root.Screen name={AppRouteName} component={AppNavigator} />
+ <Root.Screen
+ name={TermsAndPrivacyRouteName}
+ component={TermsAndPrivacyModal}
+ options={termsAndPrivacyModalScreenOptions}
+ />
<Root.Screen
name={ThreadPickerModalRouteName}
component={ThreadPickerModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={ImagePasteModalRouteName}
component={ImagePasteModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={AddUsersModalRouteName}
component={AddUsersModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={CustomServerModalRouteName}
component={CustomServerModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={ColorSelectorModalRouteName}
component={ColorSelectorModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={ComposeSubchannelModalRouteName}
component={ComposeSubchannelModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={SidebarListModalRouteName}
component={SidebarListModal}
options={modalOverlayScreenOptions}
/>
</Root.Navigator>
);
}
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<ScreenParamList>,
> = RouteProp<ScreenParamList, RouteName>;
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<typeof light>;
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<S: Styles> = $ObjMap<S, ReplaceField>;
function stylesFromColors<IS: Styles>(
obj: IS,
themeColors: Colors,
): StyleSheetOf<IS> {
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<IS: Styles>(
obj: IS,
): (state: AppState) => StyleSheetOf<IS> {
return createSelector(colorsSelector, (themeColors: Colors) =>
stylesFromColors(obj, themeColors),
);
}
function useStyles<IS: Styles>(obj: IS): StyleSheetOf<IS> {
const ourColors = useColors();
return React.useMemo(() => stylesFromColors(obj, ourColors), [
obj,
ourColors,
]);
}
function useOverlayStyles<IS: Styles>(obj: IS): StyleSheetOf<IS> {
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<IS: Styles>(
obj: IS,
theme: GlobalTheme,
): StyleSheetOf<IS> {
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,
};

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 1:38 AM (9 h, 8 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690167
Default Alt Text
(31 KB)

Event Timeline