From 8e4d47cf7b16c1c04d52e69d2da12b5ce0f265b8 Mon Sep 17 00:00:00 2001 From: Gerzon Z Date: Wed, 10 Nov 2021 11:10:34 -0400 Subject: [PATCH] Chore: Migrate ProfileView to TypeScript * Initial commit * Fix module import * Improve TextInput and KeyboardView interfaces and migrate scrollPersistTaps to TS * update interfaces * add new interfaces and extract them to their own file * chore: migrate style.js to ts Co-authored-by: AlexAlexandre --- .eslintrc.js | 2 +- app/containers/Avatar/interfaces.ts | 2 +- app/containers/TextInput.tsx | 2 +- app/externalModules.d.ts | 1 + app/presentation/KeyboardView.tsx | 13 +-- app/utils/scrollPersistTaps.js | 4 - app/utils/scrollPersistTaps.ts | 8 ++ app/views/ProfileView/{index.js => index.tsx} | 109 +++++++++--------- app/views/ProfileView/interfaces.ts | 79 +++++++++++++ .../ProfileView/{styles.js => styles.ts} | 0 10 files changed, 152 insertions(+), 68 deletions(-) delete mode 100644 app/utils/scrollPersistTaps.js create mode 100644 app/utils/scrollPersistTaps.ts rename app/views/ProfileView/{index.js => index.tsx} (87%) create mode 100644 app/views/ProfileView/interfaces.ts rename app/views/ProfileView/{styles.js => styles.ts} (100%) diff --git a/.eslintrc.js b/.eslintrc.js index f5b6d39c8..085f3a89d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,7 +2,7 @@ module.exports = { settings: { 'import/resolver': { node: { - extensions: ['.js', '.ios.js', '.android.js', '.native.js', '.ts', '.tsx'] + extensions: ['.ts', '.tsx', '.js', '.ios.js', '.android.js', '.native.js'] } } }, diff --git a/app/containers/Avatar/interfaces.ts b/app/containers/Avatar/interfaces.ts index 692c4d0a3..ed7fd3b9e 100644 --- a/app/containers/Avatar/interfaces.ts +++ b/app/containers/Avatar/interfaces.ts @@ -16,7 +16,7 @@ export interface IAvatar { onPress(): void; getCustomEmoji(): any; avatarETag: string; - isStatic: boolean; + isStatic: boolean | string; rid: string; blockUnauthenticatedAccess: boolean; serverVersion: string; diff --git a/app/containers/TextInput.tsx b/app/containers/TextInput.tsx index 5f9aaecf8..28ae1c2f1 100644 --- a/app/containers/TextInput.tsx +++ b/app/containers/TextInput.tsx @@ -58,7 +58,7 @@ interface IRCTextInputProps extends TextInputProps { }; loading?: boolean; containerStyle?: StyleProp; - inputStyle?: TextStyle; + inputStyle?: StyleProp; inputRef?: React.Ref; testID?: string; iconLeft?: string; diff --git a/app/externalModules.d.ts b/app/externalModules.d.ts index fb054b8f6..2634708cb 100644 --- a/app/externalModules.d.ts +++ b/app/externalModules.d.ts @@ -10,3 +10,4 @@ declare module '@rocket.chat/sdk'; declare module 'react-native-config-reader'; declare module 'react-native-keycommands'; declare module 'react-native-restart'; +declare module 'react-native-prompt-android'; diff --git a/app/presentation/KeyboardView.tsx b/app/presentation/KeyboardView.tsx index e5a4d3910..572b9eaf9 100644 --- a/app/presentation/KeyboardView.tsx +++ b/app/presentation/KeyboardView.tsx @@ -1,14 +1,11 @@ import React from 'react'; -import { KeyboardAwareScrollView } from '@codler/react-native-keyboard-aware-scroll-view'; +import { KeyboardAwareScrollView, KeyboardAwareScrollViewProps } from '@codler/react-native-keyboard-aware-scroll-view'; import scrollPersistTaps from '../utils/scrollPersistTaps'; -interface IKeyboardViewProps { - style: any; - contentContainerStyle: any; +interface IKeyboardViewProps extends KeyboardAwareScrollViewProps { keyboardVerticalOffset: number; - scrollEnabled: boolean; - children: JSX.Element; + children: React.ReactNode; } export default class KeyboardView extends React.PureComponent { @@ -22,9 +19,7 @@ export default class KeyboardView extends React.PureComponent + extraHeight={keyboardVerticalOffset}> {children} ); diff --git a/app/utils/scrollPersistTaps.js b/app/utils/scrollPersistTaps.js deleted file mode 100644 index a08e17af8..000000000 --- a/app/utils/scrollPersistTaps.js +++ /dev/null @@ -1,4 +0,0 @@ -export default { - keyboardShouldPersistTaps: 'always', - keyboardDismissMode: 'interactive' -}; diff --git a/app/utils/scrollPersistTaps.ts b/app/utils/scrollPersistTaps.ts new file mode 100644 index 000000000..c625ac2a9 --- /dev/null +++ b/app/utils/scrollPersistTaps.ts @@ -0,0 +1,8 @@ +import { KeyboardAwareScrollViewProps } from '@codler/react-native-keyboard-aware-scroll-view'; + +const scrollPersistTaps: Partial = { + keyboardShouldPersistTaps: 'always', + keyboardDismissMode: 'interactive' +}; + +export default scrollPersistTaps; diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.tsx similarity index 87% rename from app/views/ProfileView/index.js rename to app/views/ProfileView/index.tsx index 630d8c7aa..d0ca64eec 100644 --- a/app/views/ProfileView/index.js +++ b/app/views/ProfileView/index.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Keyboard, ScrollView, View } from 'react-native'; import { connect } from 'react-redux'; import prompt from 'react-native-prompt-android'; -import SHA256 from 'js-sha256'; -import ImagePicker from 'react-native-image-crop-picker'; +import { sha256 } from 'js-sha256'; +import ImagePicker, { Image } from 'react-native-image-crop-picker'; import RNPickerSelect from 'react-native-picker-select'; import { dequal } from 'dequal'; import omit from 'lodash/omit'; +import { StackNavigationOptions } from '@react-navigation/stack'; import Touch from '../../utils/touch'; import KeyboardView from '../../presentation/KeyboardView'; @@ -31,43 +31,40 @@ import { withTheme } from '../../theme'; import { getUserSelector } from '../../selectors/login'; import SafeAreaView from '../../containers/SafeAreaView'; import styles from './styles'; +import { IAvatar, IAvatarButton, INavigationOptions, IParams, IProfileViewProps, IProfileViewState, IUser } from './interfaces'; -class ProfileView extends React.Component { - static navigationOptions = ({ navigation, isMasterDetail }) => { - const options = { +class ProfileView extends React.Component { + private name: any; + private username: any; + private email: any; + private avatarUrl: any; + private newPassword: any; + + static navigationOptions = ({ navigation, isMasterDetail }: INavigationOptions) => { + const options: StackNavigationOptions = { title: I18n.t('Profile') }; if (!isMasterDetail) { options.headerLeft = () => ; } options.headerRight = () => ( - navigation.navigate('UserPreferencesView')} testID='preferences-view-open' /> + navigation?.navigate('UserPreferencesView')} testID='preferences-view-open' /> ); return options; }; - static propTypes = { - baseUrl: PropTypes.string, - user: PropTypes.object, - Accounts_AllowEmailChange: PropTypes.bool, - Accounts_AllowPasswordChange: PropTypes.bool, - Accounts_AllowRealNameChange: PropTypes.bool, - Accounts_AllowUserAvatarChange: PropTypes.bool, - Accounts_AllowUsernameChange: PropTypes.bool, - Accounts_CustomFields: PropTypes.string, - setUser: PropTypes.func, - theme: PropTypes.string - }; - - state = { + state: IProfileViewState = { saving: false, - name: null, - username: null, - email: null, - newPassword: null, - currentPassword: null, - avatarUrl: null, - avatar: {}, + name: '', + username: '', + email: '', + newPassword: '', + currentPassword: '', + avatarUrl: '', + avatar: { + data: {}, + url: '' + }, avatarSuggestions: {}, customFields: {} }; @@ -83,7 +80,7 @@ class ProfileView extends React.Component { } } - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: IProfileViewProps) { const { user } = this.props; /* * We need to ignore status because on Android ImagePicker @@ -96,7 +93,7 @@ class ProfileView extends React.Component { } } - setAvatar = avatar => { + setAvatar = (avatar: IAvatar) => { const { Accounts_AllowUserAvatarChange } = this.props; if (!Accounts_AllowUserAvatarChange) { @@ -106,7 +103,7 @@ class ProfileView extends React.Component { this.setState({ avatar }); }; - init = user => { + init = (user?: IUser) => { const { user: userProps } = this.props; const { name, username, emails, customFields } = user || userProps; @@ -117,7 +114,10 @@ class ProfileView extends React.Component { newPassword: null, currentPassword: null, avatarUrl: null, - avatar: {}, + avatar: { + data: {}, + url: '' + }, customFields: customFields || {} }); }; @@ -142,12 +142,12 @@ class ProfileView extends React.Component { !newPassword && user.emails && user.emails[0].address === email && - !avatar.data && + !avatar!.data && !customFieldsChanged ); }; - handleError = (e, func, action) => { + handleError = (e: any, func: string, action: string) => { if (e.data && e.data.error.includes('[error-too-many-requests]')) { return showErrorAlert(e.data.error); } @@ -165,7 +165,7 @@ class ProfileView extends React.Component { const { name, username, email, newPassword, currentPassword, avatar, customFields } = this.state; const { user, setUser } = this.props; - const params = {}; + const params = {} as IParams; // Name if (user.name !== name) { @@ -189,7 +189,7 @@ class ProfileView extends React.Component { // currentPassword if (currentPassword) { - params.currentPassword = SHA256(currentPassword); + params.currentPassword = sha256(currentPassword); } const requirePassword = !!params.email || newPassword; @@ -202,7 +202,7 @@ class ProfileView extends React.Component { { text: I18n.t('Cancel'), onPress: () => {}, style: 'cancel' }, { text: I18n.t('Save'), - onPress: p => { + onPress: (p: string) => { this.setState({ currentPassword: p }); this.submit(); } @@ -217,7 +217,7 @@ class ProfileView extends React.Component { } try { - if (avatar.url) { + if (avatar!.url) { try { logEvent(events.PROFILE_SAVE_AVATAR); await RocketChat.setAvatarFromService(avatar); @@ -283,7 +283,7 @@ class ProfileView extends React.Component { }; try { logEvent(events.PROFILE_PICK_AVATAR); - const response = await ImagePicker.openPicker(options); + const response: Image = await ImagePicker.openPicker(options); this.setAvatar({ url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' }); } catch (error) { logEvent(events.PROFILE_PICK_AVATAR_F); @@ -291,12 +291,12 @@ class ProfileView extends React.Component { } }; - pickImageWithURL = avatarUrl => { + pickImageWithURL = (avatarUrl: string) => { logEvent(events.PROFILE_PICK_AVATAR_WITH_URL); this.setAvatar({ url: avatarUrl, data: avatarUrl, service: 'url' }); }; - renderAvatarButton = ({ key, child, onPress, disabled = false }) => { + renderAvatarButton = ({ key, child, onPress, disabled = false }: IAvatarButton) => { const { theme } = this.props; return ( , - onPress: () => this.pickImageWithURL(avatarUrl), + onPress: () => this.pickImageWithURL(avatarUrl!), disabled: !avatarUrl, key: 'profile-view-avatar-url-button' })} @@ -365,19 +365,20 @@ class ProfileView extends React.Component { const parsedCustomFields = JSON.parse(Accounts_CustomFields); return Object.keys(parsedCustomFields).map((key, index, array) => { if (parsedCustomFields[key].type === 'select') { - const options = parsedCustomFields[key].options.map(option => ({ label: option, value: option })); + const options = parsedCustomFields[key].options.map((option: string) => ({ label: option, value: option })); return ( { - const newValue = {}; + const newValue: { [key: string]: string } = {}; newValue[key] = value; this.setState({ customFields: { ...customFields, ...newValue } }); }} value={customFields[key]}> { + // @ts-ignore this[key] = e; }} label={key} @@ -393,6 +394,7 @@ class ProfileView extends React.Component { return ( { + // @ts-ignore this[key] = e; }} key={key} @@ -400,12 +402,13 @@ class ProfileView extends React.Component { placeholder={key} value={customFields[key]} onChangeText={value => { - const newValue = {}; + const newValue: { [key: string]: string } = {}; newValue[key] = value; this.setState({ customFields: { ...customFields, ...newValue } }); }} onSubmitEditing={() => { if (array.length - 1 > index) { + // @ts-ignore return this[array[index + 1]].focus(); } this.avatarUrl.focus(); @@ -421,6 +424,7 @@ class ProfileView extends React.Component { logoutOtherLocations = () => { logEvent(events.PL_OTHER_LOCATIONS); + // @ts-ignore showConfirmationAlert({ message: I18n.t('You_will_be_logged_out_from_other_locations'), confirmationText: I18n.t('Logout'), @@ -469,7 +473,7 @@ class ProfileView extends React.Component { label={I18n.t('Name')} placeholder={I18n.t('Name')} value={name} - onChangeText={value => this.setState({ name: value })} + onChangeText={(value: string) => this.setState({ name: value })} onSubmitEditing={() => { this.username.focus(); }} @@ -500,7 +504,7 @@ class ProfileView extends React.Component { }} label={I18n.t('Email')} placeholder={I18n.t('Email')} - value={email} + value={email!} onChangeText={value => this.setState({ email: value })} onSubmitEditing={() => { this.newPassword.focus(); @@ -516,10 +520,11 @@ class ProfileView extends React.Component { }} label={I18n.t('New_Password')} placeholder={I18n.t('New_Password')} - value={newPassword} + value={newPassword!} onChangeText={value => this.setState({ newPassword: value })} onSubmitEditing={() => { if (Accounts_CustomFields && Object.keys(customFields).length) { + // @ts-ignore return this[Object.keys(customFields)[0]].focus(); } this.avatarUrl.focus(); @@ -537,7 +542,7 @@ class ProfileView extends React.Component { }} label={I18n.t('Avatar_Url')} placeholder={I18n.t('Avatar_Url')} - value={avatarUrl} + value={avatarUrl!} onChangeText={value => this.setState({ avatarUrl: value })} onSubmitEditing={this.submit} testID='profile-view-avatar-url' @@ -568,7 +573,7 @@ class ProfileView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ user: getUserSelector(state), Accounts_AllowEmailChange: state.settings.Accounts_AllowEmailChange, Accounts_AllowPasswordChange: state.settings.Accounts_AllowPasswordChange, @@ -579,8 +584,8 @@ const mapStateToProps = state => ({ baseUrl: state.server.server }); -const mapDispatchToProps = dispatch => ({ - setUser: params => dispatch(setUserAction(params)) +const mapDispatchToProps = (dispatch: any) => ({ + setUser: (params: any) => dispatch(setUserAction(params)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(ProfileView)); diff --git a/app/views/ProfileView/interfaces.ts b/app/views/ProfileView/interfaces.ts new file mode 100644 index 000000000..00117203e --- /dev/null +++ b/app/views/ProfileView/interfaces.ts @@ -0,0 +1,79 @@ +import { StackNavigationProp } from '@react-navigation/stack'; +import React from 'react'; + +export interface IUser { + id: string; + name: string; + username: string; + emails: { + [index: number]: { + address: string; + }; + }; + customFields: { + [index: string | number]: string; + }; +} + +export interface IParams { + name: string; + username: string; + email: string | null; + newPassword: string; + currentPassword: string; +} + +export interface IAvatarButton { + key: React.Key; + child: React.ReactNode; + onPress: Function; + disabled: boolean; +} + +export interface INavigationOptions { + navigation: StackNavigationProp; + isMasterDetail?: boolean; +} + +export interface IProfileViewProps { + user: IUser; + navigation: StackNavigationProp; + isMasterDetail?: boolean; + baseUrl: string; + Accounts_AllowEmailChange: boolean; + Accounts_AllowPasswordChange: boolean; + Accounts_AllowRealNameChange: boolean; + Accounts_AllowUserAvatarChange: boolean; + Accounts_AllowUsernameChange: boolean; + Accounts_CustomFields: string; + setUser: Function; + theme: string; +} + +export interface IAvatar { + data: {} | string | null; + url?: string; + contentType?: string; + service?: any; +} + +export interface IProfileViewState { + saving: boolean; + name: string; + username: string; + email: string | null; + newPassword: string | null; + currentPassword: string | null; + avatarUrl: string | null; + avatar: IAvatar; + avatarSuggestions: { + [service: string]: { + url: string; + blob: string; + contentType: string; + }; + }; + customFields: { + [key: string | number]: string; + }; +} diff --git a/app/views/ProfileView/styles.js b/app/views/ProfileView/styles.ts similarity index 100% rename from app/views/ProfileView/styles.js rename to app/views/ProfileView/styles.ts