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,
};