diff --git a/native/calendar/thread-picker-modal.react.js b/native/calendar/thread-picker-modal.react.js index f20a7f7e6..b25fc7881 100644 --- a/native/calendar/thread-picker-modal.react.js +++ b/native/calendar/thread-picker-modal.react.js @@ -1,99 +1,99 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { StyleSheet } from 'react-native'; import { useDispatch } from 'react-redux'; import { createLocalEntry, createLocalEntryActionType, } from 'lib/actions/entry-actions'; import { threadSearchIndex } from 'lib/selectors/nav-selectors'; import { onScreenEntryEditableThreadInfos } from 'lib/selectors/thread-selectors'; import Modal from '../components/modal.react'; import ThreadList from '../components/thread-list.react'; import { RootNavigatorContext } from '../navigation/root-navigator-context'; import type { RootNavigationProp } from '../navigation/root-navigator.react'; import type { NavigationRoute } from '../navigation/route-names'; import { useSelector } from '../redux/redux-utils'; import { waitForInteractions } from '../utils/timers'; export type ThreadPickerModalParams = {| presentedFrom: string, dateString: string, |}; type Props = {| navigation: RootNavigationProp<'ThreadPickerModal'>, route: NavigationRoute<'ThreadPickerModal'>, |}; function ThreadPickerModal(props: Props) { const { navigation, route: { params: { dateString }, }, } = props; const viewerID = useSelector( (state) => state.currentUserInfo && state.currentUserInfo.id, ); const nextLocalID = useSelector((state) => state.nextLocalID); const dispatch = useDispatch(); const rootNavigatorContext = React.useContext(RootNavigatorContext); const threadPicked = React.useCallback( (threadID: string) => { invariant( dateString && viewerID && rootNavigatorContext, 'inputs to threadPicked should be set', ); rootNavigatorContext.setKeyboardHandlingEnabled(false); dispatch({ type: createLocalEntryActionType, payload: createLocalEntry(threadID, nextLocalID, dateString, viewerID), }); }, [rootNavigatorContext, dispatch, viewerID, nextLocalID, dateString], ); React.useEffect( () => navigation.addListener('blur', async () => { await waitForInteractions(); invariant( rootNavigatorContext, 'RootNavigatorContext should be set in onScreenBlur', ); rootNavigatorContext.setKeyboardHandlingEnabled(true); }), [navigation, rootNavigatorContext], ); const index = useSelector((state) => threadSearchIndex(state)); const onScreenThreadInfos = useSelector((state) => onScreenEntryEditableThreadInfos(state), ); return ( - + ); } const styles = StyleSheet.create({ threadListItem: { paddingLeft: 10, paddingRight: 10, paddingVertical: 2, }, }); export default ThreadPickerModal; diff --git a/native/chat/image-paste-modal.react.js b/native/chat/image-paste-modal.react.js index 20aafc52b..c81b08683 100644 --- a/native/chat/image-paste-modal.react.js +++ b/native/chat/image-paste-modal.react.js @@ -1,101 +1,97 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Button, View, Image } from 'react-native'; import filesystem from 'react-native-fs'; import type { PhotoPaste } from 'lib/types/media-types'; import sleep from 'lib/utils/sleep'; import Modal from '../components/modal.react'; import { InputStateContext } from '../input/input-state'; import type { RootNavigationProp } from '../navigation/root-navigator.react'; import type { NavigationRoute } from '../navigation/route-names'; import { useStyles } from '../themes/colors'; export type ImagePasteModalParams = {| +imagePasteStagingInfo: PhotoPaste, +threadID: string, |}; type Props = {| +navigation: RootNavigationProp<'ImagePasteModal'>, +route: NavigationRoute<'ImagePasteModal'>, |}; function ImagePasteModal(props: Props) { const styles = useStyles(unboundStyles); const inputState = React.useContext(InputStateContext); const { navigation, route: { params: { imagePasteStagingInfo, threadID }, }, } = props; const sendImage = React.useCallback(async () => { navigation.goBackOnce(); const selection: $ReadOnlyArray = [imagePasteStagingInfo]; invariant(inputState, 'inputState should be set in ImagePasteModal'); await inputState.sendMultimediaMessage(threadID, selection); invariant( imagePasteStagingInfo, 'imagePasteStagingInfo should be set in ImagePasteModal', ); }, [imagePasteStagingInfo, inputState, navigation, threadID]); const cancel = React.useCallback(async () => { navigation.goBackOnce(); await sleep(5000); filesystem.unlink(imagePasteStagingInfo.uri); }, [imagePasteStagingInfo.uri, navigation]); return ( - + ); } let cancelButton; if (this.props.changeThreadSettingsLoadingStatus !== 'loading') { cancelButton = ( ); } else { cancelButton = ; } const inputProps = { ...tagInputProps, onSubmitEditing: this.onPressAdd, }; return ( - + {cancelButton} {addButton} ); } close = () => { this.props.navigation.goBackOnce(); }; tagInputRef = (tagInput: ?TagInput) => { this.tagInput = tagInput; }; onChangeTagInput = (userInfoInputArray: $ReadOnlyArray) => { if (this.props.changeThreadSettingsLoadingStatus === 'loading') { return; } this.setState({ userInfoInputArray }); }; tagDataLabelExtractor = (userInfo: AccountUserInfo) => userInfo.username; setUsernameInputText = (text: string) => { if (this.props.changeThreadSettingsLoadingStatus === 'loading') { return; } this.setState({ usernameInputText: text }); }; onUserSelect = (userID: string) => { if (this.props.changeThreadSettingsLoadingStatus === 'loading') { return; } for (let existingUserInfo of this.state.userInfoInputArray) { if (userID === existingUserInfo.id) { return; } } const userInfoInputArray = [ ...this.state.userInfoInputArray, this.props.otherUserInfos[userID], ]; this.setState({ userInfoInputArray, usernameInputText: '', }); }; onPressAdd = () => { if (this.state.userInfoInputArray.length === 0) { return; } this.props.dispatchActionPromise( changeThreadSettingsActionTypes, this.addUsersToThread(), ); }; async addUsersToThread() { try { const newMemberIDs = this.state.userInfoInputArray.map( (userInfo) => userInfo.id, ); const result = await this.props.changeThreadSettings({ threadID: this.props.route.params.threadInfo.id, changes: { newMemberIDs }, }); this.close(); return result; } catch (e) { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); throw e; } } onErrorAcknowledged = () => { invariant(this.tagInput, 'nameInput should be set'); this.tagInput.focus(); }; onUnknownErrorAlertAcknowledged = () => { this.setState( { userInfoInputArray: [], usernameInputText: '', }, this.onErrorAcknowledged, ); }; } const unboundStyles = { activityIndicator: { paddingRight: 6, }, addButton: { backgroundColor: 'greenButton', 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, }, }; export default React.memo(function ConnectedAddUsersModal( props: BaseProps, ) { const { parentThreadID } = props.route.params.threadInfo; const parentThreadInfo = useSelector((state) => parentThreadID ? threadInfoSelector(state)[parentThreadID] : null, ); const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); const userSearchIndex = useSelector(userSearchIndexForPotentialMembers); const changeThreadSettingsLoadingStatus = useSelector( createLoadingStatusSelector(changeThreadSettingsActionTypes), ); const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadSettings = useServerCall(changeThreadSettings); return ( ); }); diff --git a/native/chat/settings/color-picker-modal.react.js b/native/chat/settings/color-picker-modal.react.js index 407c23314..18357ed67 100644 --- a/native/chat/settings/color-picker-modal.react.js +++ b/native/chat/settings/color-picker-modal.react.js @@ -1,174 +1,171 @@ // @flow import * as React from 'react'; import { TouchableHighlight, Alert } from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; import { useSelector } from 'react-redux'; import { changeThreadSettingsActionTypes, changeThreadSettings, } from 'lib/actions/thread-actions'; import { type ThreadInfo, type ChangeThreadSettingsPayload, type UpdateThreadRequest, } from 'lib/types/thread-types'; import type { DispatchActionPromise } from 'lib/utils/action-utils'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import ColorPicker from '../../components/color-picker.react'; import Modal from '../../components/modal.react'; import type { RootNavigationProp } from '../../navigation/root-navigator.react'; import type { NavigationRoute } from '../../navigation/route-names'; import { type Colors, useStyles, useColors } from '../../themes/colors'; export type ColorPickerModalParams = {| presentedFrom: string, color: string, threadInfo: ThreadInfo, setColor: (color: string) => void, |}; type BaseProps = {| +navigation: RootNavigationProp<'ColorPickerModal'>, +route: NavigationRoute<'ColorPickerModal'>, |}; type Props = {| ...BaseProps, // Redux state +colors: Colors, +styles: typeof unboundStyles, +windowWidth: number, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +changeThreadSettings: ( request: UpdateThreadRequest, ) => Promise, |}; class ColorPickerModal extends React.PureComponent { render() { const { color, threadInfo } = this.props.route.params; // Based on the assumption we are always in portrait, // and consequently width is the lowest dimensions const modalStyle = { height: this.props.windowWidth - 5 }; return ( - + ); } close = () => { this.props.navigation.goBackOnce(); }; onColorSelected = (color: string) => { const colorEditValue = color.substr(1); this.props.route.params.setColor(colorEditValue); this.close(); this.props.dispatchActionPromise( changeThreadSettingsActionTypes, this.editColor(colorEditValue), { customKeyName: `${changeThreadSettingsActionTypes.started}:color` }, ); }; async editColor(newColor: string) { const threadID = this.props.route.params.threadInfo.id; try { return await this.props.changeThreadSettings({ threadID, changes: { color: newColor }, }); } catch (e) { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: this.onErrorAcknowledged }], { cancelable: false }, ); throw e; } } onErrorAcknowledged = () => { const { threadInfo, setColor } = this.props.route.params; setColor(threadInfo.color); }; } const unboundStyles = { closeButton: { borderRadius: 3, height: 18, position: 'absolute', right: 5, top: 5, width: 18, }, closeButtonIcon: { color: 'modalBackgroundSecondaryLabel', left: 3, position: 'absolute', }, colorPicker: { bottom: 10, left: 10, position: 'absolute', right: 10, top: 10, }, colorPickerContainer: { backgroundColor: 'modalBackground', borderRadius: 5, flex: 0, marginHorizontal: 15, marginVertical: 20, }, }; export default React.memo(function ConnectedColorPickerModal( props: BaseProps, ) { const styles = useStyles(unboundStyles); const colors = useColors(); const windowWidth = useSelector((state) => state.dimensions.width); const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadSettings = useServerCall(changeThreadSettings); return ( ); }); diff --git a/native/chat/settings/compose-subthread-modal.react.js b/native/chat/settings/compose-subthread-modal.react.js index 92728c90f..d44c72b58 100644 --- a/native/chat/settings/compose-subthread-modal.react.js +++ b/native/chat/settings/compose-subthread-modal.react.js @@ -1,166 +1,163 @@ // @flow import PropTypes from 'prop-types'; import * as React from 'react'; import { Text } from 'react-native'; import IonIcon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/MaterialIcons'; import { threadTypeDescriptions } from 'lib/shared/thread-utils'; import { type ThreadInfo, threadInfoPropType, threadTypes, } from 'lib/types/thread-types'; import { connect } from 'lib/utils/redux-utils'; import Button from '../../components/button.react'; import Modal from '../../components/modal.react'; import type { RootNavigationProp } from '../../navigation/root-navigator.react'; import type { NavigationRoute } from '../../navigation/route-names'; import { ComposeThreadRouteName } from '../../navigation/route-names'; import type { AppState } from '../../redux/redux-setup'; import { type Colors, colorsPropType, colorsSelector, styleSelector, } from '../../themes/colors'; export type ComposeSubthreadModalParams = {| presentedFrom: string, threadInfo: ThreadInfo, |}; type Props = {| navigation: RootNavigationProp<'ComposeSubthreadModal'>, route: NavigationRoute<'ComposeSubthreadModal'>, // Redux state colors: Colors, styles: typeof styles, |}; class ComposeSubthreadModal extends React.PureComponent { static propTypes = { navigation: PropTypes.shape({ navigate: PropTypes.func.isRequired, }).isRequired, route: PropTypes.shape({ params: PropTypes.shape({ presentedFrom: PropTypes.string.isRequired, threadInfo: threadInfoPropType.isRequired, }).isRequired, }).isRequired, colors: colorsPropType.isRequired, styles: PropTypes.objectOf(PropTypes.object).isRequired, }; render() { return ( - + Thread type ); } onPressOpen = () => { const threadInfo = this.props.route.params.threadInfo; this.props.navigation.navigate({ name: ComposeThreadRouteName, params: { threadType: threadTypes.CHAT_NESTED_OPEN, parentThreadInfo: threadInfo, }, key: `${ComposeThreadRouteName}|${threadInfo.id}|${threadTypes.CHAT_NESTED_OPEN}`, }); }; onPressSecret = () => { const threadInfo = this.props.route.params.threadInfo; this.props.navigation.navigate({ name: ComposeThreadRouteName, params: { threadType: threadTypes.CHAT_SECRET, parentThreadInfo: threadInfo, }, key: `${ComposeThreadRouteName}|${threadInfo.id}|${threadTypes.CHAT_SECRET}`, }); }; } const styles = { forwardIcon: { color: 'link', paddingLeft: 10, }, modal: { flex: 0, }, option: { alignItems: 'center', flexDirection: 'row', paddingHorizontal: 10, paddingVertical: 20, }, optionExplanation: { color: 'modalBackgroundLabel', flex: 1, fontSize: 14, paddingLeft: 20, textAlign: 'center', }, optionText: { color: 'modalBackgroundLabel', fontSize: 20, paddingLeft: 5, }, visibility: { color: 'modalBackgroundLabel', fontSize: 24, textAlign: 'center', }, visibilityIcon: { color: 'modalBackgroundLabel', paddingRight: 3, }, }; const stylesSelector = styleSelector(styles); export default connect((state: AppState) => ({ colors: colorsSelector(state), styles: stylesSelector(state), }))(ComposeSubthreadModal); diff --git a/native/chat/sidebar-list-modal.react.js b/native/chat/sidebar-list-modal.react.js index 84aee5a68..45acf6f52 100644 --- a/native/chat/sidebar-list-modal.react.js +++ b/native/chat/sidebar-list-modal.react.js @@ -1,162 +1,162 @@ // @flow import * as React from 'react'; import { TextInput, FlatList, StyleSheet } from 'react-native'; import { sidebarInfoSelector } from 'lib/selectors/thread-selectors'; import SearchIndex from 'lib/shared/search-index'; import { threadSearchText } from 'lib/shared/thread-utils'; import type { ThreadInfo, SidebarInfo } from 'lib/types/thread-types'; import Modal from '../components/modal.react'; import Search from '../components/search.react'; import type { RootNavigationProp } from '../navigation/root-navigator.react'; import type { NavigationRoute } from '../navigation/route-names'; import { MessageListRouteName } from '../navigation/route-names'; import { useSelector } from '../redux/redux-utils'; import { useIndicatorStyle } from '../themes/colors'; import { waitForModalInputFocus } from '../utils/timers'; import SidebarItem from './sidebar-item.react'; export type SidebarListModalParams = {| +threadInfo: ThreadInfo, |}; function keyExtractor(sidebarInfo: SidebarInfo) { return sidebarInfo.threadInfo.id; } function getItemLayout(data: ?$ReadOnlyArray, index: number) { return { length: 24, offset: 24 * index, index }; } type Props = {| +navigation: RootNavigationProp<'SidebarListModal'>, +route: NavigationRoute<'SidebarListModal'>, |}; function SidebarListModal(props: Props) { const threadID = props.route.params.threadInfo.id; const sidebarInfos = useSelector( (state) => sidebarInfoSelector(state)[threadID] ?? [], ); const [searchState, setSearchState] = React.useState({ text: '', results: new Set(), }); const listData = React.useMemo(() => { if (!searchState.text) { return sidebarInfos; } return sidebarInfos.filter(({ threadInfo }) => searchState.results.has(threadInfo.id), ); }, [sidebarInfos, searchState]); const userInfos = useSelector((state) => state.userStore.userInfos); const searchIndex = React.useMemo(() => { const index = new SearchIndex(); for (const sidebarInfo of sidebarInfos) { const { threadInfo } = sidebarInfo; index.addEntry(threadInfo.id, threadSearchText(threadInfo, userInfos)); } return index; }, [sidebarInfos, userInfos]); React.useEffect(() => { setSearchState((curState) => ({ ...curState, results: new Set(searchIndex.getSearchResults(curState.text)), })); }, [searchIndex]); const onChangeSearchText = React.useCallback( (searchText: string) => setSearchState({ text: searchText, results: new Set(searchIndex.getSearchResults(searchText)), }), [searchIndex], ); 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 { navigation } = props; const { navigate } = navigation; const onPressItem = React.useCallback( (threadInfo: ThreadInfo) => { setSearchState({ text: '', results: new Set(), }); if (searchTextInputRef.current) { searchTextInputRef.current.blur(); } navigate({ name: MessageListRouteName, params: { threadInfo }, key: `${MessageListRouteName}${threadInfo.id}`, }); }, [navigate], ); const renderItem = React.useCallback( (row: { item: SidebarInfo, ... }) => { return ( ); }, [onPressItem], ); const indicatorStyle = useIndicatorStyle(); return ( - + ); } const styles = StyleSheet.create({ search: { marginBottom: 8, }, sidebar: { marginLeft: 0, marginRight: 5, }, }); export default SidebarListModal; diff --git a/native/components/modal.react.js b/native/components/modal.react.js index e83dca55b..39a151440 100644 --- a/native/components/modal.react.js +++ b/native/components/modal.react.js @@ -1,71 +1,62 @@ // @flow +import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { View, TouchableWithoutFeedback, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import type { Edge } from 'react-native-safe-area-context'; -import { connect } from 'lib/utils/redux-utils'; - -import type { RootNavigationProp } from '../navigation/root-navigator.react'; -import type { AppState } from '../redux/redux-setup'; -import { styleSelector } from '../themes/colors'; +import { useStyles } from '../themes/colors'; import type { ViewStyle } from '../types/styles'; import KeyboardAvoidingView from './keyboard-avoiding-view.react'; type Props = $ReadOnly<{| - navigation: RootNavigationProp<>, - children: React.Node, - containerStyle?: ViewStyle, - modalStyle?: ViewStyle, - safeAreaEdges?: $ReadOnlyArray, - // Redux state - styles: typeof styles, + +children: React.Node, + +containerStyle?: ViewStyle, + +modalStyle?: ViewStyle, + +safeAreaEdges?: $ReadOnlyArray, |}>; -class Modal extends React.PureComponent { - close = () => { - if (this.props.navigation.isFocused()) { - this.props.navigation.goBackOnce(); +function Modal(props: Props) { + const navigation = useNavigation(); + const close = React.useCallback(() => { + if (navigation.isFocused()) { + navigation.goBack(); } - }; + }, [navigation]); - render() { - const { containerStyle, modalStyle, children, safeAreaEdges } = this.props; - return ( - - - - - - {children} - - - ); - } + const styles = useStyles(unboundStyles); + const { containerStyle, modalStyle, children, safeAreaEdges } = props; + return ( + + + + + + {children} + + + ); } -const styles = { +const unboundStyles = { container: { flex: 1, justifyContent: 'center', overflow: 'visible', }, modal: { backgroundColor: 'modalBackground', borderRadius: 5, flex: 1, justifyContent: 'center', marginBottom: 30, marginHorizontal: 15, marginTop: 100, padding: 12, }, }; -const stylesSelector = styleSelector(styles); -export default connect((state: AppState) => ({ - styles: stylesSelector(state), -}))(Modal); +export default Modal; diff --git a/native/more/custom-server-modal.react.js b/native/more/custom-server-modal.react.js index 34f507c4c..cda103996 100644 --- a/native/more/custom-server-modal.react.js +++ b/native/more/custom-server-modal.react.js @@ -1,129 +1,128 @@ // @flow import PropTypes from 'prop-types'; import * as React from 'react'; import { Text, TextInput } from 'react-native'; import type { DispatchActionPayload } from 'lib/utils/action-utils'; import { connect } from 'lib/utils/redux-utils'; import { setURLPrefix } from 'lib/utils/url-utils'; import Button from '../components/button.react'; import Modal from '../components/modal.react'; import type { RootNavigationProp } from '../navigation/root-navigator.react'; import type { AppState } from '../redux/redux-setup'; import { styleSelector } from '../themes/colors'; import { setCustomServer } from '../utils/url-utils'; export type CustomServerModalParams = {| presentedFrom: string, |}; type Props = {| navigation: RootNavigationProp<'CustomServerModal'>, // Redux state urlPrefix: string, customServer: ?string, styles: typeof styles, // Redux dispatch functions dispatchActionPayload: DispatchActionPayload, |}; type State = {| customServer: string, |}; class CustomServerModal extends React.PureComponent { static propTypes = { navigation: PropTypes.shape({ goBackOnce: PropTypes.func.isRequired, }).isRequired, urlPrefix: PropTypes.string.isRequired, customServer: PropTypes.string, styles: PropTypes.objectOf(PropTypes.object).isRequired, dispatchActionPayload: PropTypes.func.isRequired, }; 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.dispatchActionPayload(setURLPrefix, customServer); } if (customServer && customServer !== this.props.customServer) { this.props.dispatchActionPayload(setCustomServer, customServer); } this.props.navigation.goBackOnce(); }; } const styles = { button: { backgroundColor: 'greenButton', 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 stylesSelector = styleSelector(styles); export default connect( (state: AppState) => ({ urlPrefix: state.urlPrefix, customServer: state.customServer, styles: stylesSelector(state), }), null, true, )(CustomServerModal);