Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509145
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
31 KB
Referenced Files
None
Subscribers
None
View Options
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 </Text>
+ <Text style={styles.link} onPress={onTermsOfUsePressed}>
+ Terms of Service
+ </Text>
+ <Text> & </Text>
+ <Text style={styles.link} onPress={onPrivacyPolicyPressed}>
+ Privacy Policy
+ </Text>
+ <Text>
+ . In order to continue using Comm, we'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
Details
Attached
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)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment