diff --git a/native/chat/message-preview.react.js b/native/chat/message-preview.react.js index 7116f0162..6a031ba9d 100644 --- a/native/chat/message-preview.react.js +++ b/native/chat/message-preview.react.js @@ -1,90 +1,90 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text } from 'react-native'; import { useMessagePreview } from 'lib/shared/message-utils'; import { type MessageInfo } from 'lib/types/message-types'; import { type ThreadInfo } from 'lib/types/thread-types'; import { SingleLine } from '../components/single-line.react'; import { getDefaultTextMessageRules } from '../markdown/rules.react'; import { useStyles } from '../themes/colors'; type Props = { +messageInfo: MessageInfo, +threadInfo: ThreadInfo, }; function MessagePreview(props: Props): React.Node { const { messageInfo, threadInfo } = props; const messagePreviewResult = useMessagePreview( messageInfo, threadInfo, getDefaultTextMessageRules().simpleMarkdownRules, ); invariant( messagePreviewResult, 'useMessagePreview should only return falsey if pass null or undefined', ); const { message, username } = messagePreviewResult; let messageStyle; const styles = useStyles(unboundStyles); if (message.style === 'unread') { messageStyle = styles.unread; } else if (message.style === 'primary') { messageStyle = styles.primary; } else if (message.style === 'secondary') { messageStyle = styles.secondary; } invariant( messageStyle, `MessagePreview doesn't support ${message.style} style for message, ` + 'only unread, primary, and secondary', ); if (!username) { return ( {message.text} ); } let usernameStyle; if (username.style === 'unread') { usernameStyle = styles.unread; } else if (username.style === 'secondary') { usernameStyle = styles.secondary; } invariant( usernameStyle, `MessagePreview doesn't support ${username.style} style for username, ` + 'only unread and secondary', ); return ( {`${username.text}: `} {message.text} ); } const unboundStyles = { lastMessage: { flex: 1, fontSize: 14, }, primary: { color: 'listForegroundTertiaryLabel', }, secondary: { - color: 'listForegroundQuaternaryLabel', + color: 'listForegroundTertiaryLabel', }, unread: { color: 'listForegroundLabel', }, }; export default MessagePreview; diff --git a/native/chat/settings/add-users-modal.react.js b/native/chat/settings/add-users-modal.react.js index c32406720..85225f54b 100644 --- a/native/chat/settings/add-users-modal.react.js +++ b/native/chat/settings/add-users-modal.react.js @@ -1,301 +1,301 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text, ActivityIndicator, Alert } from 'react-native'; import { changeThreadSettingsActionTypes, changeThreadSettings, } from 'lib/actions/thread-actions'; import { useENSNames } from 'lib/hooks/ens-cache'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { threadInfoSelector } from 'lib/selectors/thread-selectors'; import { userInfoSelectorForPotentialMembers, userSearchIndexForPotentialMembers, } from 'lib/selectors/user-selectors'; import { getPotentialMemberItems } from 'lib/shared/search-utils'; import { threadActualMembers } from 'lib/shared/thread-utils'; import { type ThreadInfo } from 'lib/types/thread-types'; import { type AccountUserInfo } from 'lib/types/user-types'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import Button from '../../components/button.react'; import Modal from '../../components/modal.react'; import { createTagInput } from '../../components/tag-input.react'; import UserList from '../../components/user-list.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'; const TagInput = createTagInput(); const tagInputProps = { placeholder: 'Select users to add', autoFocus: true, returnKeyType: 'go', }; const tagDataLabelExtractor = (userInfo: AccountUserInfo) => userInfo.username; export type AddUsersModalParams = { +presentedFrom: string, +threadInfo: ThreadInfo, }; type Props = { +navigation: RootNavigationProp<'AddUsersModal'>, +route: NavigationRoute<'AddUsersModal'>, }; function AddUsersModal(props: Props): React.Node { const [usernameInputText, setUsernameInputText] = React.useState(''); const [userInfoInputArray, setUserInfoInputArray] = React.useState< $ReadOnlyArray, >([]); const tagInputRef = React.useRef(); const onUnknownErrorAlertAcknowledged = React.useCallback(() => { setUsernameInputText(''); setUserInfoInputArray([]); invariant(tagInputRef.current, 'tagInput should be set'); tagInputRef.current.focus(); }, []); const { navigation } = props; const { goBackOnce } = navigation; const close = React.useCallback(() => { goBackOnce(); }, [goBackOnce]); const callChangeThreadSettings = useServerCall(changeThreadSettings); const userInfoInputIDs = userInfoInputArray.map(userInfo => userInfo.id); const { route } = props; const { threadInfo } = route.params; const threadID = threadInfo.id; const addUsersToThread = React.useCallback(async () => { try { const result = await callChangeThreadSettings({ threadID: threadID, changes: { newMemberIDs: userInfoInputIDs }, }); close(); return result; } catch (e) { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); throw e; } }, [ callChangeThreadSettings, threadID, userInfoInputIDs, close, onUnknownErrorAlertAcknowledged, ]); const inputLength = userInfoInputArray.length; const dispatchActionPromise = useDispatchActionPromise(); const userInfoInputArrayEmpty = inputLength === 0; const onPressAdd = React.useCallback(() => { if (userInfoInputArrayEmpty) { return; } dispatchActionPromise(changeThreadSettingsActionTypes, addUsersToThread()); }, [userInfoInputArrayEmpty, dispatchActionPromise, addUsersToThread]); const changeThreadSettingsLoadingStatus = useSelector( createLoadingStatusSelector(changeThreadSettingsActionTypes), ); const isLoading = changeThreadSettingsLoadingStatus === 'loading'; const styles = useStyles(unboundStyles); let addButton = null; if (inputLength > 0) { let activityIndicator = null; if (isLoading) { activityIndicator = ( ); } const addButtonText = `Add (${inputLength})`; addButton = ( ); } let cancelButton; if (!isLoading) { cancelButton = ( ); } else { cancelButton = ; } const threadMemberIDs = React.useMemo( () => threadActualMembers(threadInfo.members), [threadInfo.members], ); const excludeUserIDs = React.useMemo( () => userInfoInputIDs.concat(threadMemberIDs), [userInfoInputIDs, threadMemberIDs], ); const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); const userSearchIndex = useSelector(userSearchIndexForPotentialMembers); const { parentThreadID, community } = props.route.params.threadInfo; const parentThreadInfo = useSelector(state => parentThreadID ? threadInfoSelector(state)[parentThreadID] : null, ); const communityThreadInfo = useSelector(state => community ? threadInfoSelector(state)[community] : null, ); const userSearchResults = React.useMemo( () => getPotentialMemberItems( usernameInputText, otherUserInfos, userSearchIndex, excludeUserIDs, parentThreadInfo, communityThreadInfo, threadInfo.type, ), [ usernameInputText, otherUserInfos, userSearchIndex, excludeUserIDs, parentThreadInfo, communityThreadInfo, threadInfo.type, ], ); const onChangeTagInput = React.useCallback( (newUserInfoInputArray: $ReadOnlyArray) => { if (!isLoading) { setUserInfoInputArray(newUserInfoInputArray); } }, [isLoading], ); const onChangeTagInputText = React.useCallback( (text: string) => { if (!isLoading) { setUsernameInputText(text); } }, [isLoading], ); const onUserSelect = React.useCallback( (userID: string) => { if (isLoading) { return; } if (userInfoInputIDs.some(existingUserID => userID === existingUserID)) { return; } setUserInfoInputArray(oldUserInfoInputArray => [ ...oldUserInfoInputArray, otherUserInfos[userID], ]); setUsernameInputText(''); }, [isLoading, userInfoInputIDs, otherUserInfos], ); const inputProps = React.useMemo( () => ({ ...tagInputProps, onSubmitEditing: onPressAdd, }), [onPressAdd], ); const userSearchResultWithENSNames = useENSNames(userSearchResults); const userInfoInputArrayWithENSNames = useENSNames(userInfoInputArray); return ( {cancelButton} {addButton} ); } const unboundStyles = { activityIndicator: { paddingRight: 6, }, addButton: { - backgroundColor: 'greenButton', + backgroundColor: 'vibrantGreenButton', borderRadius: 3, flexDirection: 'row', paddingHorizontal: 10, paddingVertical: 4, }, addText: { color: 'white', fontSize: 18, }, buttons: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 12, }, cancelButton: { backgroundColor: 'modalButton', borderRadius: 3, paddingHorizontal: 10, paddingVertical: 4, }, cancelText: { color: 'modalButtonLabel', fontSize: 18, }, }; const MemoizedAddUsersModal: React.ComponentType = React.memo( AddUsersModal, ); export default MemoizedAddUsersModal; diff --git a/native/chat/settings/delete-thread.react.js b/native/chat/settings/delete-thread.react.js index 374fd4c87..418f90c60 100644 --- a/native/chat/settings/delete-thread.react.js +++ b/native/chat/settings/delete-thread.react.js @@ -1,274 +1,274 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View, TextInput as BaseTextInput, Alert, ActivityIndicator, } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { deleteThreadActionTypes, deleteThread, } from 'lib/actions/thread-actions'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { threadInfoSelector } from 'lib/selectors/thread-selectors'; import { identifyInvalidatedThreads } from 'lib/shared/thread-utils'; import type { LoadingStatus } from 'lib/types/loading-types'; import type { ThreadInfo, LeaveThreadPayload } from 'lib/types/thread-types'; import { useServerCall, useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/action-utils'; import Button from '../../components/button.react'; import { clearThreadsActionType } from '../../navigation/action-types'; import { NavContext, type NavAction, } from '../../navigation/navigation-context'; import type { NavigationRoute } from '../../navigation/route-names'; import { useSelector } from '../../redux/redux-utils'; import { type Colors, useColors, useStyles } from '../../themes/colors'; import type { GlobalTheme } from '../../types/themes'; import type { ChatNavigationProp } from '../chat.react'; export type DeleteThreadParams = { +threadInfo: ThreadInfo, }; type BaseProps = { +navigation: ChatNavigationProp<'DeleteThread'>, +route: NavigationRoute<'DeleteThread'>, }; type Props = { ...BaseProps, // Redux state +threadInfo: ?ThreadInfo, +loadingStatus: LoadingStatus, +activeTheme: ?GlobalTheme, +colors: Colors, +styles: typeof unboundStyles, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +deleteThread: (threadID: string) => Promise, // withNavContext +navDispatch: (action: NavAction) => void, }; class DeleteThread extends React.PureComponent { mounted = false; passwordInput: ?React.ElementRef; static getThreadInfo(props: Props): ThreadInfo { const { threadInfo } = props; if (threadInfo) { return threadInfo; } return props.route.params.threadInfo; } componentDidMount() { this.mounted = true; } componentWillUnmount() { this.mounted = false; } guardedSetState(change, callback) { if (this.mounted) { this.setState(change, callback); } } componentDidUpdate(prevProps: Props) { const oldReduxThreadInfo = prevProps.threadInfo; const newReduxThreadInfo = this.props.threadInfo; if (newReduxThreadInfo && newReduxThreadInfo !== oldReduxThreadInfo) { this.props.navigation.setParams({ threadInfo: newReduxThreadInfo }); } } render() { const buttonContent = this.props.loadingStatus === 'loading' ? ( ) : ( Delete chat ); const threadInfo = DeleteThread.getThreadInfo(this.props); return ( {`The chat "${threadInfo.uiName}" will be permanently deleted. `} There is no way to reverse this. ); } passwordInputRef = ( passwordInput: ?React.ElementRef, ) => { this.passwordInput = passwordInput; }; focusPasswordInput = () => { invariant(this.passwordInput, 'passwordInput should be set'); this.passwordInput.focus(); }; submitDeletion = () => { this.props.dispatchActionPromise( deleteThreadActionTypes, this.deleteThread(), ); }; async deleteThread() { const threadInfo = DeleteThread.getThreadInfo(this.props); const { navDispatch } = this.props; navDispatch({ type: clearThreadsActionType, payload: { threadIDs: [threadInfo.id] }, }); try { const result = await this.props.deleteThread(threadInfo.id); const invalidated = identifyInvalidatedThreads( result.updatesResult.newUpdates, ); navDispatch({ type: clearThreadsActionType, payload: { threadIDs: [...invalidated] }, }); return result; } catch (e) { if (e.message === 'invalid_credentials') { Alert.alert( 'Permission not granted', 'You do not have permission to delete this thread', [{ text: 'OK' }], { cancelable: false }, ); } else { Alert.alert('Unknown error', 'Uhh... try again?', [{ text: 'OK' }], { cancelable: false, }); } } } } const unboundStyles = { deleteButton: { - backgroundColor: 'redButton', + backgroundColor: 'vibrantRedButton', borderRadius: 5, flex: 1, marginHorizontal: 24, marginVertical: 12, padding: 12, }, deleteText: { color: 'white', fontSize: 18, textAlign: 'center', }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, input: { color: 'panelForegroundLabel', flex: 1, fontFamily: 'Arial', fontSize: 16, paddingVertical: 0, borderBottomColor: 'transparent', }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, flexDirection: 'row', justifyContent: 'space-between', marginBottom: 24, paddingHorizontal: 24, paddingVertical: 12, }, warningText: { color: 'panelForegroundLabel', fontSize: 16, marginBottom: 24, marginHorizontal: 24, textAlign: 'center', }, }; const loadingStatusSelector = createLoadingStatusSelector( deleteThreadActionTypes, ); const ConnectedDeleteThread: React.ComponentType = React.memo( function ConnectedDeleteThread(props: BaseProps) { const threadID = props.route.params.threadInfo.id; const threadInfo = useSelector( state => threadInfoSelector(state)[threadID], ); const loadingStatus = useSelector(loadingStatusSelector); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const colors = useColors(); const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); const callDeleteThread = useServerCall(deleteThread); const navContext = React.useContext(NavContext); invariant(navContext, 'NavContext should be set in DeleteThread'); const navDispatch = navContext.dispatch; return ( ); }, ); export default ConnectedDeleteThread; diff --git a/native/chat/thread-list-modal.react.js b/native/chat/thread-list-modal.react.js index 495ae5feb..b089df70d 100644 --- a/native/chat/thread-list-modal.react.js +++ b/native/chat/thread-list-modal.react.js @@ -1,199 +1,199 @@ // @flow import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { Text, TextInput, FlatList, View, TouchableOpacity, } from 'react-native'; import type { ThreadSearchState } from 'lib/hooks/search-threads'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors'; import type { SetState } from 'lib/types/hook-types'; import type { ThreadInfo, SidebarInfo } from 'lib/types/thread-types'; import Modal from '../components/modal.react'; import Search from '../components/search.react'; import SWMansionIcon from '../components/swmansion-icon.react'; import ThreadPill from '../components/thread-pill.react'; import { useIndicatorStyle, useStyles } from '../themes/colors'; import { waitForModalInputFocus } from '../utils/timers'; import { useNavigateToThread } from './message-list-types'; function keyExtractor(sidebarInfo: SidebarInfo | ChatThreadItem) { return sidebarInfo.threadInfo.id; } function getItemLayout( data: ?$ReadOnlyArray, index: number, ) { return { length: 24, offset: 24 * index, index }; } type Props = { +threadInfo: ThreadInfo, +createRenderItem: ( onPressItem: (threadInfo: ThreadInfo) => void, ) => (row: { +item: U, +index: number, ... }) => React.Node, +listData: $ReadOnlyArray, +searchState: ThreadSearchState, +setSearchState: SetState, +onChangeSearchInputText: (text: string) => mixed, +searchPlaceholder?: string, +modalTitle: string, }; function ThreadListModal( props: Props, ): React.Node { const { threadInfo: parentThreadInfo, searchState, setSearchState, onChangeSearchInputText, listData, createRenderItem, searchPlaceholder, modalTitle, } = props; const searchTextInputRef = React.useRef(); const setSearchTextInputRef = React.useCallback( async (textInput: ?React.ElementRef) => { searchTextInputRef.current = textInput; if (!textInput) { return; } await waitForModalInputFocus(); if (searchTextInputRef.current) { searchTextInputRef.current.focus(); } }, [], ); const navigateToThread = useNavigateToThread(); const onPressItem = React.useCallback( (threadInfo: ThreadInfo) => { setSearchState({ text: '', results: new Set(), }); if (searchTextInputRef.current) { searchTextInputRef.current.blur(); } navigateToThread({ threadInfo }); }, [navigateToThread, setSearchState], ); const renderItem = React.useMemo(() => createRenderItem(onPressItem), [ createRenderItem, onPressItem, ]); const styles = useStyles(unboundStyles); const indicatorStyle = useIndicatorStyle(); const navigation = useNavigation(); return ( {modalTitle} ); } const unboundStyles = { parentNameWrapper: { alignItems: 'flex-start', }, body: { paddingHorizontal: 16, flex: 1, }, headerTopRow: { flexDirection: 'row', justifyContent: 'space-between', height: 32, alignItems: 'center', }, header: { borderBottomColor: 'subthreadsModalSearch', borderBottomWidth: 1, height: 94, padding: 16, justifyContent: 'space-between', }, modal: { borderRadius: 8, paddingHorizontal: 0, - backgroundColor: 'subthreadsModalBackgroud', + backgroundColor: 'subthreadsModalBackground', paddingTop: 0, justifyContent: 'flex-start', }, search: { height: 40, marginVertical: 16, backgroundColor: 'subthreadsModalSearch', }, title: { color: 'listForegroundLabel', fontSize: 20, fontWeight: '500', lineHeight: 26, alignSelf: 'center', marginLeft: 2, }, closeIcon: { color: 'subthreadsModalClose', }, closeButton: { marginRight: 2, height: 40, alignItems: 'center', justifyContent: 'center', }, }; export default ThreadListModal; diff --git a/native/navigation/community-drawer-content.react.js b/native/navigation/community-drawer-content.react.js index 3fe5f56cd..824f5ea31 100644 --- a/native/navigation/community-drawer-content.react.js +++ b/native/navigation/community-drawer-content.react.js @@ -1,190 +1,190 @@ // @flow import * as React from 'react'; import { FlatList, Platform } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useSelector } from 'react-redux'; import { childThreadInfos, communityThreadSelector, } from 'lib/selectors/thread-selectors'; import { threadIsChannel } from 'lib/shared/thread-utils'; import { type ThreadInfo, communitySubthreads } from 'lib/types/thread-types'; import { useNavigateToThread } from '../chat/message-list-types'; import { useStyles } from '../themes/colors'; import type { TextStyle } from '../types/styles'; import CommunityDrawerItemCommunity from './community-drawer-item-community.react'; const maxDepth = 2; const safeAreaEdges = Platform.select({ ios: ['top'], default: ['top', 'bottom'], }); function CommunityDrawerContent(): React.Node { const communities = useSelector(communityThreadSelector); const communitiesSuffixed = React.useMemo(() => appendSuffix(communities), [ communities, ]); const styles = useStyles(unboundStyles); const [openCommunity, setOpenCommunity] = React.useState( communitiesSuffixed.length === 1 ? communitiesSuffixed[0].id : null, ); const navigateToThread = useNavigateToThread(); const childThreadInfosMap = useSelector(childThreadInfos); const setOpenCommunityOrClose = React.useCallback((index: string) => { setOpenCommunity(open => (open === index ? null : index)); }, []); const renderItem = React.useCallback( ({ item }) => { const itemData = { threadInfo: item.threadInfo, itemChildren: item.itemChildren, labelStyle: item.labelStyle, hasSubchannelsButton: item.subchannelsButton, }; return ( ); }, [navigateToThread, openCommunity, setOpenCommunityOrClose], ); const labelStylesObj = useStyles(labelUnboundStyles); const labelStyles = React.useMemo( () => [ labelStylesObj.level0Label, labelStylesObj.level1Label, labelStylesObj.level2Label, ], [labelStylesObj], ); const drawerItemsData = React.useMemo( () => createRecursiveDrawerItemsData( childThreadInfosMap, communitiesSuffixed, labelStyles, ), [childThreadInfosMap, communitiesSuffixed, labelStyles], ); return ( ); } function createRecursiveDrawerItemsData( childThreadInfosMap: { +[id: string]: $ReadOnlyArray }, communities: $ReadOnlyArray, labelStyles: $ReadOnlyArray, ) { const result = communities.map(community => ({ key: community.id, threadInfo: community, itemChildren: [], labelStyle: labelStyles[0], subchannelsButton: false, })); let queue = result.map(item => [item, 0]); for (let i = 0; i < queue.length; i++) { const [item, lvl] = queue[i]; const itemChildThreadInfos = childThreadInfosMap[item.threadInfo.id] ?? []; if (lvl < maxDepth) { item.itemChildren = itemChildThreadInfos .filter(childItem => communitySubthreads.includes(childItem.type)) .map(childItem => ({ threadInfo: childItem, itemChildren: [], labelStyle: labelStyles[Math.min(lvl + 1, labelStyles.length - 1)], hasSubchannelsButton: lvl + 1 === maxDepth && threadHasSubchannels(childItem, childThreadInfosMap), })); queue = queue.concat( item.itemChildren.map(childItem => [childItem, lvl + 1]), ); } } return result; } function threadHasSubchannels( threadInfo: ThreadInfo, childThreadInfosMap: { +[id: string]: $ReadOnlyArray }, ) { if (!childThreadInfosMap[threadInfo.id]?.length) { return false; } return childThreadInfosMap[threadInfo.id].some(thread => threadIsChannel(thread), ); } function appendSuffix(chats: $ReadOnlyArray): ThreadInfo[] { const result = []; const names = new Map(); for (const chat of chats) { let name = chat.uiName; const numberOfOccurrences = names.get(name); names.set(name, (numberOfOccurrences ?? 0) + 1); if (numberOfOccurrences) { name = `${name} (${numberOfOccurrences.toString()})`; } result.push({ ...chat, uiName: name }); } return result; } const unboundStyles = { drawerContent: { flex: 1, paddingRight: 8, paddingTop: 8, - backgroundColor: 'drawerBackgroud', + backgroundColor: 'drawerBackground', }, }; const labelUnboundStyles = { level0Label: { color: 'drawerItemLabelLevel0', fontSize: 16, lineHeight: 24, fontWeight: '500', }, level1Label: { color: 'drawerItemLabelLevel1', fontSize: 14, lineHeight: 22, fontWeight: '500', }, level2Label: { color: 'drawerItemLabelLevel2', fontSize: 14, lineHeight: 22, fontWeight: '400', }, }; const MemoizedCommunityDrawerContent: React.ComponentType<{}> = React.memo( CommunityDrawerContent, ); export default MemoizedCommunityDrawerContent; diff --git a/native/profile/custom-server-modal.react.js b/native/profile/custom-server-modal.react.js index d561d1ec3..e82539135 100644 --- a/native/profile/custom-server-modal.react.js +++ b/native/profile/custom-server-modal.react.js @@ -1,137 +1,137 @@ // @flow import * as React from 'react'; import { Text } from 'react-native'; import { useDispatch } from 'react-redux'; import type { Dispatch } from 'lib/types/redux-types'; import { setURLPrefix } from 'lib/utils/url-utils'; import Button from '../components/button.react'; import Modal from '../components/modal.react'; import TextInput from '../components/text-input.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'; import { setCustomServer } from '../utils/url-utils'; export type CustomServerModalParams = { +presentedFrom: string, }; type BaseProps = { +navigation: RootNavigationProp<'CustomServerModal'>, +route: NavigationRoute<'CustomServerModal'>, }; type Props = { ...BaseProps, +urlPrefix: string, +customServer: ?string, +styles: typeof unboundStyles, +dispatch: Dispatch, }; type State = { +customServer: string, }; class CustomServerModal extends React.PureComponent { constructor(props: Props) { super(props); const { customServer } = props; this.state = { customServer: customServer ? customServer : '', }; } render() { return ( ); } onChangeCustomServer = (newCustomServer: string) => { this.setState({ customServer: newCustomServer }); }; onPressGo = () => { const { customServer } = this.state; if (customServer !== this.props.urlPrefix) { this.props.dispatch({ type: setURLPrefix, payload: customServer, }); } if (customServer && customServer !== this.props.customServer) { this.props.dispatch({ type: setCustomServer, payload: customServer, }); } this.props.navigation.goBackOnce(); }; } const unboundStyles = { button: { - backgroundColor: 'greenButton', + backgroundColor: 'vibrantGreenButton', borderRadius: 5, marginHorizontal: 2, marginVertical: 2, paddingHorizontal: 12, paddingVertical: 4, }, buttonText: { color: 'white', fontSize: 18, textAlign: 'center', }, container: { justifyContent: 'flex-end', }, modal: { flex: 0, flexDirection: 'row', }, textInput: { color: 'modalBackgroundLabel', flex: 1, fontSize: 16, margin: 0, padding: 0, borderBottomColor: 'transparent', }, }; const ConnectedCustomServerModal: React.ComponentType = React.memo( function ConnectedCustomServerModal(props: BaseProps) { const urlPrefix = useSelector(state => state.urlPrefix); const customServer = useSelector(state => state.customServer); const styles = useStyles(unboundStyles); const dispatch = useDispatch(); return ( ); }, ); export default ConnectedCustomServerModal; diff --git a/native/profile/delete-account.react.js b/native/profile/delete-account.react.js index 53cca58cf..47f9d02ac 100644 --- a/native/profile/delete-account.react.js +++ b/native/profile/delete-account.react.js @@ -1,279 +1,279 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View, TextInput as BaseTextInput, Alert, ActivityIndicator, } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { deleteAccountActionTypes, deleteAccount, } from 'lib/actions/user-actions'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { accountHasPassword } from 'lib/shared/account-utils'; import type { LogOutResult } from 'lib/types/account-types'; import type { LoadingStatus } from 'lib/types/loading-types'; import type { PreRequestUserState } from 'lib/types/session-types'; import type { DispatchActionPromise } from 'lib/utils/action-utils'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import { deleteNativeCredentialsFor } from '../account/native-credentials'; import Button from '../components/button.react'; import TextInput from '../components/text-input.react'; import { useSelector } from '../redux/redux-utils'; import { type Colors, useColors, useStyles } from '../themes/colors'; import type { GlobalTheme } from '../types/themes'; type Props = { // Redux state +isAccountWithPassword: boolean, +loadingStatus: LoadingStatus, +preRequestUserState: PreRequestUserState, +activeTheme: ?GlobalTheme, +colors: Colors, +styles: typeof unboundStyles, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +deleteAccount: ( password: ?string, preRequestUserState: PreRequestUserState, ) => Promise, }; type State = { +password: ?string, }; class DeleteAccount extends React.PureComponent { state: State = { password: null, }; mounted = false; passwordInput: ?React.ElementRef; componentDidMount() { this.mounted = true; } componentWillUnmount() { this.mounted = false; } render() { const buttonContent = this.props.loadingStatus === 'loading' ? ( ) : ( Delete account ); const { panelForegroundTertiaryLabel } = this.props.colors; let inputPasswordPrompt; if (this.props.isAccountWithPassword) { inputPasswordPrompt = ( <> PASSWORD ); } return ( Your account will be permanently deleted. There is no way to reverse this. {inputPasswordPrompt} ); } onChangePasswordText = (newPassword: string) => { this.setState({ password: newPassword }); }; passwordInputRef = ( passwordInput: ?React.ElementRef, ) => { this.passwordInput = passwordInput; }; focusPasswordInput = () => { invariant(this.passwordInput, 'passwordInput should be set'); this.passwordInput.focus(); }; submitDeletion = () => { this.props.dispatchActionPromise( deleteAccountActionTypes, this.deleteAccount(), ); }; async deleteAccount() { try { await deleteNativeCredentialsFor(); const result = await this.props.deleteAccount( this.state.password, this.props.preRequestUserState, ); return result; } catch (e) { if (e.message === 'invalid_credentials') { Alert.alert( 'Incorrect password', 'The password you entered is incorrect', [{ text: 'OK', onPress: this.onErrorAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: this.onErrorAlertAcknowledged }], { cancelable: false }, ); } } } onErrorAlertAcknowledged = () => { this.setState({ password: '' }, this.focusPasswordInput); }; } const unboundStyles = { deleteButton: { - backgroundColor: 'redButton', + backgroundColor: 'vibrantRedButton', borderRadius: 5, flex: 1, marginHorizontal: 24, marginVertical: 12, padding: 12, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, input: { color: 'panelForegroundLabel', flex: 1, fontFamily: 'Arial', fontSize: 16, paddingVertical: 0, borderBottomColor: 'transparent', }, lastWarningText: { marginBottom: 24, }, saveText: { color: 'white', fontSize: 18, textAlign: 'center', }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, flexDirection: 'row', justifyContent: 'space-between', marginBottom: 24, paddingHorizontal: 24, paddingVertical: 12, }, warningText: { color: 'panelForegroundLabel', fontSize: 16, marginHorizontal: 24, textAlign: 'center', }, }; const loadingStatusSelector = createLoadingStatusSelector( deleteAccountActionTypes, ); const ConnectedDeleteAccount: React.ComponentType<{ ... }> = React.memo<{ ... }>(function ConnectedDeleteAccount() { const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); const loadingStatus = useSelector(loadingStatusSelector); const preRequestUserState = useSelector(preRequestUserStateSelector); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const colors = useColors(); const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); const callDeleteAccount = useServerCall(deleteAccount); return ( ); }); export default ConnectedDeleteAccount; diff --git a/native/profile/edit-password.react.js b/native/profile/edit-password.react.js index 1c3cf849b..4d569d1a5 100644 --- a/native/profile/edit-password.react.js +++ b/native/profile/edit-password.react.js @@ -1,376 +1,376 @@ // @flow import { CommonActions } from '@react-navigation/native'; import invariant from 'invariant'; import * as React from 'react'; import { Text, View, TextInput as BaseTextInput, Alert, ActivityIndicator, } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { changeUserPasswordActionTypes, changeUserPassword, } from 'lib/actions/user-actions'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import type { LoadingStatus } from 'lib/types/loading-types'; import type { PasswordUpdate } from 'lib/types/user-types'; import { useServerCall, useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/action-utils'; import { setNativeCredentials } from '../account/native-credentials'; import Button from '../components/button.react'; import TextInput from '../components/text-input.react'; import type { NavigationRoute } from '../navigation/route-names'; import { useSelector } from '../redux/redux-utils'; import { type Colors, useColors, useStyles } from '../themes/colors'; import type { GlobalTheme } from '../types/themes'; import type { ProfileNavigationProp } from './profile.react'; type BaseProps = { +navigation: ProfileNavigationProp<'EditPassword'>, +route: NavigationRoute<'EditPassword'>, }; type Props = { ...BaseProps, // Redux state +loadingStatus: LoadingStatus, +username: ?string, +activeTheme: ?GlobalTheme, +colors: Colors, +styles: typeof unboundStyles, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +changeUserPassword: (passwordUpdate: PasswordUpdate) => Promise, }; type State = { +currentPassword: string, +newPassword: string, +confirmPassword: string, }; class EditPassword extends React.PureComponent { state: State = { currentPassword: '', newPassword: '', confirmPassword: '', }; mounted = false; currentPasswordInput: ?React.ElementRef; newPasswordInput: ?React.ElementRef; confirmPasswordInput: ?React.ElementRef; componentDidMount() { this.mounted = true; } componentWillUnmount() { this.mounted = false; } render() { const buttonContent = this.props.loadingStatus === 'loading' ? ( ) : ( Save ); const { panelForegroundTertiaryLabel } = this.props.colors; return ( CURRENT PASSWORD NEW PASSWORD ); } onChangeCurrentPassword = (currentPassword: string) => { this.setState({ currentPassword }); }; currentPasswordRef = ( currentPasswordInput: ?React.ElementRef, ) => { this.currentPasswordInput = currentPasswordInput; }; focusCurrentPassword = () => { invariant(this.currentPasswordInput, 'currentPasswordInput should be set'); this.currentPasswordInput.focus(); }; onChangeNewPassword = (newPassword: string) => { this.setState({ newPassword }); }; newPasswordRef = ( newPasswordInput: ?React.ElementRef, ) => { this.newPasswordInput = newPasswordInput; }; focusNewPassword = () => { invariant(this.newPasswordInput, 'newPasswordInput should be set'); this.newPasswordInput.focus(); }; onChangeConfirmPassword = (confirmPassword: string) => { this.setState({ confirmPassword }); }; confirmPasswordRef = ( confirmPasswordInput: ?React.ElementRef, ) => { this.confirmPasswordInput = confirmPasswordInput; }; focusConfirmPassword = () => { invariant(this.confirmPasswordInput, 'confirmPasswordInput should be set'); this.confirmPasswordInput.focus(); }; goBackOnce() { this.props.navigation.dispatch(state => ({ ...CommonActions.goBack(), target: state.key, })); } submitPassword = () => { if (this.state.newPassword === '') { Alert.alert( 'Empty password', 'New password cannot be empty', [{ text: 'OK', onPress: this.onNewPasswordAlertAcknowledged }], { cancelable: false }, ); } else if (this.state.newPassword !== this.state.confirmPassword) { Alert.alert( 'Passwords don’t match', 'New password fields must contain the same password', [{ text: 'OK', onPress: this.onNewPasswordAlertAcknowledged }], { cancelable: false }, ); } else if (this.state.newPassword === this.state.currentPassword) { this.goBackOnce(); } else { this.props.dispatchActionPromise( changeUserPasswordActionTypes, this.savePassword(), ); } }; async savePassword() { const { username } = this.props; if (!username) { return; } try { await this.props.changeUserPassword({ updatedFields: { password: this.state.newPassword, }, currentPassword: this.state.currentPassword, }); await setNativeCredentials({ username, password: this.state.newPassword, }); this.goBackOnce(); } catch (e) { if (e.message === 'invalid_credentials') { Alert.alert( 'Incorrect password', 'The current password you entered is incorrect', [{ text: 'OK', onPress: this.onCurrentPasswordAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } } } onNewPasswordAlertAcknowledged = () => { this.setState( { newPassword: '', confirmPassword: '' }, this.focusNewPassword, ); }; onCurrentPasswordAlertAcknowledged = () => { this.setState({ currentPassword: '' }, this.focusCurrentPassword); }; onUnknownErrorAlertAcknowledged = () => { this.setState( { currentPassword: '', newPassword: '', confirmPassword: '' }, this.focusCurrentPassword, ); }; } const unboundStyles = { header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, hr: { backgroundColor: 'panelForegroundBorder', height: 1, marginHorizontal: 15, }, input: { color: 'panelForegroundLabel', flex: 1, fontFamily: 'Arial', fontSize: 16, paddingVertical: 0, borderBottomColor: 'transparent', }, row: { flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 9, }, saveButton: { - backgroundColor: 'greenButton', + backgroundColor: 'vibrantGreenButton', borderRadius: 5, flex: 1, marginHorizontal: 24, marginVertical: 12, padding: 12, }, saveText: { color: 'white', fontSize: 18, textAlign: 'center', }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, paddingVertical: 3, }, }; const loadingStatusSelector = createLoadingStatusSelector( changeUserPasswordActionTypes, ); const ConnectedEditPassword: React.ComponentType = React.memo( function ConnectedEditPassword(props: BaseProps) { const loadingStatus = useSelector(loadingStatusSelector); const username = useSelector(state => { if (state.currentUserInfo && !state.currentUserInfo.anonymous) { return state.currentUserInfo.username; } return undefined; }); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const colors = useColors(); const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); const callChangeUserPassword = useServerCall(changeUserPassword); return ( ); }, ); export default ConnectedEditPassword; diff --git a/native/themes/colors.js b/native/themes/colors.js index 5b5563b69..c9ed7613b 100644 --- a/native/themes/colors.js +++ b/native/themes/colors.js @@ -1,326 +1,312 @@ // @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', + blockQuoteBackground: '#E0E0E0', + blockQuoteBorder: '#CCCCCC', + codeBackground: '#E0E0E0', + disabledButton: '#E0E0E0', disconnectedBarBackground: '#F5F5F5', editButton: '#A4A4A2', floatingButtonBackground: '#999999', - floatingButtonLabel: '#EEEEEE', - greenButton: '#6EC472', - greenText: 'green', + floatingButtonLabel: '#EBEBEB', headerChevron: '#0A0A0A', inlineEngagementBackground: '#E0E0E0', - inlineEngagementLabel: '#000000', - link: '#036AFF', - listBackground: 'white', - listBackgroundLabel: 'black', + inlineEngagementLabel: '#0A0A0A', + link: '#7E57C2', + listBackground: '#FFFFFF', + listBackgroundLabel: '#0A0A0A', listBackgroundSecondaryLabel: '#444444', listBackgroundTernaryLabel: '#999999', listChatBubble: '#F1F0F5', - listForegroundLabel: 'black', - listForegroundQuaternaryLabel: '#AAAAAA', + listForegroundLabel: '#0A0A0A', 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', + listSeparatorLabel: '#666666', + modalBackground: '#EBEBEB', modalBackgroundLabel: '#333333', modalBackgroundSecondaryLabel: '#AAAAAA', modalButton: '#BBBBBB', - modalButtonLabel: 'black', - modalContrastBackground: 'black', - modalContrastForegroundLabel: 'white', + modalButtonLabel: '#0A0A0A', + modalContrastBackground: '#0A0A0A', + modalContrastForegroundLabel: '#FFFFFF', modalContrastOpacity: 0.7, - modalForeground: 'white', + modalForeground: '#FFFFFF', modalForegroundBorder: '#CCCCCC', - modalForegroundLabel: 'black', + modalForegroundLabel: '#0A0A0A', modalForegroundSecondaryLabel: '#888888', modalForegroundTertiaryLabel: '#AAAAAA', modalIosHighlightUnderlay: '#CCCCCCDD', modalSubtext: '#CCCCCC', - modalSubtextLabel: '#555555', + modalSubtextLabel: '#666666', navigationCard: '#FFFFFF', - navigationChevron: '#BAB9BE', + navigationChevron: '#CCCCCC', panelBackground: '#F5F5F5', panelBackgroundLabel: '#888888', - panelForeground: 'white', + panelForeground: '#FFFFFF', panelForegroundBorder: '#CCCCCC', - panelForegroundLabel: 'black', + panelForegroundLabel: '#0A0A0A', panelForegroundSecondaryLabel: '#333333', panelForegroundTertiaryLabel: '#888888', - panelIosHighlightUnderlay: '#EEEEEEDD', + panelIosHighlightUnderlay: '#EBEBEBDD', panelSecondaryForeground: '#F5F5F5', - panelSecondaryForegroundBorder: '#D1D1D6', + panelSecondaryForegroundBorder: '#CCCCCC', purpleLink: '#7E57C2', purpleButton: '#7E57C2', - redButton: '#BB8888', - redText: '#FF4444', + redText: '#F53100', spoiler: '#33332C', tabBarAccent: '#7E57C2', tabBarBackground: '#F5F5F5', tabBarActiveTintColor: '#7E57C2', vibrantGreenButton: '#00C853', vibrantRedButton: '#F53100', tooltipBackground: '#E0E0E0', logInSpacer: '#FFFFFF33', - logInText: 'white', - siweButton: 'white', + logInText: '#FFFFFF', + siweButton: '#FFFFFF', siweButtonText: '#1F1F1F', drawerExpandButton: '#808080', drawerExpandButtonDisabled: '#CCCCCC', drawerItemLabelLevel0: '#0A0A0A', drawerItemLabelLevel1: '#0A0A0A', drawerItemLabelLevel2: '#1F1F1F', drawerOpenCommunityBackground: '#F5F5F5', - drawerBackgroud: '#FFFFFF', + drawerBackground: '#FFFFFF', subthreadsModalClose: '#808080', - subthreadsModalBackgroud: '#EEEEEE', - subthreadsModalSearch: 'rgba(0, 0, 0, 0.08)', + subthreadsModalBackground: '#EBEBEB', + subthreadsModalSearch: '#00000008', }); export type Colors = $Exact; const dark: Colors = Object.freeze({ blockQuoteBackground: '#A9A9A9', blockQuoteBorder: '#808080', codeBackground: '#0A0A0A', - disabledButton: '#444444', - disconnectedBarBackground: '#1D1D1D', - editButton: '#5B5B5D', + disabledButton: '#404040', + disconnectedBarBackground: '#1F1F1F', + editButton: '#666666', floatingButtonBackground: '#666666', - floatingButtonLabel: 'white', - greenButton: '#43A047', - greenText: '#44FF44', + floatingButtonLabel: '#FFFFFF', headerChevron: '#FFFFFF', inlineEngagementBackground: '#666666', inlineEngagementLabel: '#FFFFFF', - link: '#129AFF', + link: '#AE94DB', listBackground: '#0A0A0A', - listBackgroundLabel: '#C7C7CC', + listBackgroundLabel: '#CCCCCC', listBackgroundSecondaryLabel: '#BBBBBB', - listBackgroundTernaryLabel: '#888888', + listBackgroundTernaryLabel: '#808080', listChatBubble: '#26252A', - listForegroundLabel: 'white', - listForegroundQuaternaryLabel: '#555555', + listForegroundLabel: '#FFFFFF', listForegroundSecondaryLabel: '#CCCCCC', - listForegroundTertiaryLabel: '#999999', - listInputBackground: '#1D1D1D', - listInputBar: '#555555', - listInputBorder: '#333333', - listInputButton: '#AAAAAA', + listForegroundTertiaryLabel: '#808080', + listInputBackground: '#1F1F1F', + listInputBar: '#666666', + listInputButton: '#CCCCCC', listIosHighlightUnderlay: '#BBBBBB88', - listSearchBackground: '#1D1D1D', - listSearchIcon: '#AAAAAA', - listSeparator: '#3A3A3C', - listSeparatorLabel: '#EEEEEE', - mintButton: '#44CC99', + listSearchBackground: '#1F1F1F', + listSearchIcon: '#CCCCCC', + listSeparatorLabel: '#EBEBEB', modalBackground: '#0A0A0A', modalBackgroundLabel: '#CCCCCC', - modalBackgroundSecondaryLabel: '#555555', + modalBackgroundSecondaryLabel: '#666666', modalButton: '#666666', - modalButtonLabel: 'white', - modalContrastBackground: 'white', - modalContrastForegroundLabel: 'black', + modalButtonLabel: '#FFFFFF', + modalContrastBackground: '#FFFFFF', + modalContrastForegroundLabel: '#0A0A0A', modalContrastOpacity: 0.85, - modalForeground: '#1C1C1E', - modalForegroundBorder: '#1C1C1E', - modalForegroundLabel: 'white', + modalForeground: '#1F1F1F', + modalForegroundBorder: '#1F1F1F', + modalForegroundLabel: '#FFFFFF', modalForegroundSecondaryLabel: '#AAAAAA', modalForegroundTertiaryLabel: '#666666', modalIosHighlightUnderlay: '#AAAAAA88', - modalSubtext: '#444444', + modalSubtext: '#404040', modalSubtextLabel: '#AAAAAA', navigationCard: '#2A2A2A', - navigationChevron: '#5B5B5D', + navigationChevron: '#666666', panelBackground: '#0A0A0A', - panelBackgroundLabel: '#C7C7CC', - panelForeground: '#1D1D1D', + panelBackgroundLabel: '#CCCCCC', + panelForeground: '#1F1F1F', panelForegroundBorder: '#2C2C2E', - panelForegroundLabel: 'white', + panelForegroundLabel: '#FFFFFF', panelForegroundSecondaryLabel: '#CCCCCC', panelForegroundTertiaryLabel: '#AAAAAA', panelIosHighlightUnderlay: '#313035', panelSecondaryForeground: '#333333', panelSecondaryForegroundBorder: '#666666', purpleLink: '#AE94DB', purpleButton: '#7E57C2', - redButton: '#FF4444', - redText: '#FF4444', + redText: '#F53100', spoiler: '#33332C', tabBarAccent: '#AE94DB', tabBarBackground: '#0A0A0A', tabBarActiveTintColor: '#AE94DB', vibrantGreenButton: '#00C853', vibrantRedButton: '#F53100', tooltipBackground: '#1F1F1F', logInSpacer: '#FFFFFF33', - logInText: 'white', - siweButton: 'white', + logInText: '#FFFFFF', + siweButton: '#FFFFFF', siweButtonText: '#1F1F1F', drawerExpandButton: '#808080', drawerExpandButtonDisabled: '#404040', drawerItemLabelLevel0: '#CCCCCC', drawerItemLabelLevel1: '#CCCCCC', drawerItemLabelLevel2: '#F5F5F5', drawerOpenCommunityBackground: '#191919', - drawerBackgroud: '#1F1F1F', + drawerBackground: '#1F1F1F', subthreadsModalClose: '#808080', - subthreadsModalBackgroud: '#1F1F1F', - subthreadsModalSearch: 'rgba(255, 255, 255, 0.04)', + subthreadsModalBackground: '#1F1F1F', + subthreadsModalSearch: '#FFFFFF04', }); 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, };