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 <alexalexandrejr@gmail.com>
This commit is contained in:
Gerzon Z 2021-11-10 11:10:34 -04:00 committed by GitHub
parent 7d15e2d309
commit 8e4d47cf7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 152 additions and 68 deletions

View File

@ -2,7 +2,7 @@ module.exports = {
settings: { settings: {
'import/resolver': { 'import/resolver': {
node: { node: {
extensions: ['.js', '.ios.js', '.android.js', '.native.js', '.ts', '.tsx'] extensions: ['.ts', '.tsx', '.js', '.ios.js', '.android.js', '.native.js']
} }
} }
}, },

View File

@ -16,7 +16,7 @@ export interface IAvatar {
onPress(): void; onPress(): void;
getCustomEmoji(): any; getCustomEmoji(): any;
avatarETag: string; avatarETag: string;
isStatic: boolean; isStatic: boolean | string;
rid: string; rid: string;
blockUnauthenticatedAccess: boolean; blockUnauthenticatedAccess: boolean;
serverVersion: string; serverVersion: string;

View File

@ -58,7 +58,7 @@ interface IRCTextInputProps extends TextInputProps {
}; };
loading?: boolean; loading?: boolean;
containerStyle?: StyleProp<ViewStyle>; containerStyle?: StyleProp<ViewStyle>;
inputStyle?: TextStyle; inputStyle?: StyleProp<TextStyle>;
inputRef?: React.Ref<unknown>; inputRef?: React.Ref<unknown>;
testID?: string; testID?: string;
iconLeft?: string; iconLeft?: string;

View File

@ -10,3 +10,4 @@ declare module '@rocket.chat/sdk';
declare module 'react-native-config-reader'; declare module 'react-native-config-reader';
declare module 'react-native-keycommands'; declare module 'react-native-keycommands';
declare module 'react-native-restart'; declare module 'react-native-restart';
declare module 'react-native-prompt-android';

View File

@ -1,14 +1,11 @@
import React from 'react'; 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'; import scrollPersistTaps from '../utils/scrollPersistTaps';
interface IKeyboardViewProps { interface IKeyboardViewProps extends KeyboardAwareScrollViewProps {
style: any;
contentContainerStyle: any;
keyboardVerticalOffset: number; keyboardVerticalOffset: number;
scrollEnabled: boolean; children: React.ReactNode;
children: JSX.Element;
} }
export default class KeyboardView extends React.PureComponent<IKeyboardViewProps, any> { export default class KeyboardView extends React.PureComponent<IKeyboardViewProps, any> {
@ -22,9 +19,7 @@ export default class KeyboardView extends React.PureComponent<IKeyboardViewProps
contentContainerStyle={contentContainerStyle} contentContainerStyle={contentContainerStyle}
scrollEnabled={scrollEnabled} scrollEnabled={scrollEnabled}
alwaysBounceVertical={false} alwaysBounceVertical={false}
extraHeight={keyboardVerticalOffset} extraHeight={keyboardVerticalOffset}>
// @ts-ignore
behavior='position'>
{children} {children}
</KeyboardAwareScrollView> </KeyboardAwareScrollView>
); );

View File

@ -1,4 +0,0 @@
export default {
keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'interactive'
};

View File

@ -0,0 +1,8 @@
import { KeyboardAwareScrollViewProps } from '@codler/react-native-keyboard-aware-scroll-view';
const scrollPersistTaps: Partial<KeyboardAwareScrollViewProps> = {
keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'interactive'
};
export default scrollPersistTaps;

View File

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Keyboard, ScrollView, View } from 'react-native'; import { Keyboard, ScrollView, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import prompt from 'react-native-prompt-android'; import prompt from 'react-native-prompt-android';
import SHA256 from 'js-sha256'; import { sha256 } from 'js-sha256';
import ImagePicker from 'react-native-image-crop-picker'; import ImagePicker, { Image } from 'react-native-image-crop-picker';
import RNPickerSelect from 'react-native-picker-select'; import RNPickerSelect from 'react-native-picker-select';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { StackNavigationOptions } from '@react-navigation/stack';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import KeyboardView from '../../presentation/KeyboardView'; import KeyboardView from '../../presentation/KeyboardView';
@ -31,43 +31,40 @@ import { withTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
import styles from './styles'; import styles from './styles';
import { IAvatar, IAvatarButton, INavigationOptions, IParams, IProfileViewProps, IProfileViewState, IUser } from './interfaces';
class ProfileView extends React.Component { class ProfileView extends React.Component<IProfileViewProps, IProfileViewState> {
static navigationOptions = ({ navigation, isMasterDetail }) => { private name: any;
const options = { private username: any;
private email: any;
private avatarUrl: any;
private newPassword: any;
static navigationOptions = ({ navigation, isMasterDetail }: INavigationOptions) => {
const options: StackNavigationOptions = {
title: I18n.t('Profile') title: I18n.t('Profile')
}; };
if (!isMasterDetail) { if (!isMasterDetail) {
options.headerLeft = () => <HeaderButton.Drawer navigation={navigation} />; options.headerLeft = () => <HeaderButton.Drawer navigation={navigation} />;
} }
options.headerRight = () => ( options.headerRight = () => (
<HeaderButton.Preferences onPress={() => navigation.navigate('UserPreferencesView')} testID='preferences-view-open' /> <HeaderButton.Preferences onPress={() => navigation?.navigate('UserPreferencesView')} testID='preferences-view-open' />
); );
return options; return options;
}; };
static propTypes = { state: IProfileViewState = {
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 = {
saving: false, saving: false,
name: null, name: '',
username: null, username: '',
email: null, email: '',
newPassword: null, newPassword: '',
currentPassword: null, currentPassword: '',
avatarUrl: null, avatarUrl: '',
avatar: {}, avatar: {
data: {},
url: ''
},
avatarSuggestions: {}, avatarSuggestions: {},
customFields: {} customFields: {}
}; };
@ -83,7 +80,7 @@ class ProfileView extends React.Component {
} }
} }
UNSAFE_componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps: IProfileViewProps) {
const { user } = this.props; const { user } = this.props;
/* /*
* We need to ignore status because on Android ImagePicker * 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; const { Accounts_AllowUserAvatarChange } = this.props;
if (!Accounts_AllowUserAvatarChange) { if (!Accounts_AllowUserAvatarChange) {
@ -106,7 +103,7 @@ class ProfileView extends React.Component {
this.setState({ avatar }); this.setState({ avatar });
}; };
init = user => { init = (user?: IUser) => {
const { user: userProps } = this.props; const { user: userProps } = this.props;
const { name, username, emails, customFields } = user || userProps; const { name, username, emails, customFields } = user || userProps;
@ -117,7 +114,10 @@ class ProfileView extends React.Component {
newPassword: null, newPassword: null,
currentPassword: null, currentPassword: null,
avatarUrl: null, avatarUrl: null,
avatar: {}, avatar: {
data: {},
url: ''
},
customFields: customFields || {} customFields: customFields || {}
}); });
}; };
@ -142,12 +142,12 @@ class ProfileView extends React.Component {
!newPassword && !newPassword &&
user.emails && user.emails &&
user.emails[0].address === email && user.emails[0].address === email &&
!avatar.data && !avatar!.data &&
!customFieldsChanged !customFieldsChanged
); );
}; };
handleError = (e, func, action) => { handleError = (e: any, func: string, action: string) => {
if (e.data && e.data.error.includes('[error-too-many-requests]')) { if (e.data && e.data.error.includes('[error-too-many-requests]')) {
return showErrorAlert(e.data.error); 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 { name, username, email, newPassword, currentPassword, avatar, customFields } = this.state;
const { user, setUser } = this.props; const { user, setUser } = this.props;
const params = {}; const params = {} as IParams;
// Name // Name
if (user.name !== name) { if (user.name !== name) {
@ -189,7 +189,7 @@ class ProfileView extends React.Component {
// currentPassword // currentPassword
if (currentPassword) { if (currentPassword) {
params.currentPassword = SHA256(currentPassword); params.currentPassword = sha256(currentPassword);
} }
const requirePassword = !!params.email || newPassword; const requirePassword = !!params.email || newPassword;
@ -202,7 +202,7 @@ class ProfileView extends React.Component {
{ text: I18n.t('Cancel'), onPress: () => {}, style: 'cancel' }, { text: I18n.t('Cancel'), onPress: () => {}, style: 'cancel' },
{ {
text: I18n.t('Save'), text: I18n.t('Save'),
onPress: p => { onPress: (p: string) => {
this.setState({ currentPassword: p }); this.setState({ currentPassword: p });
this.submit(); this.submit();
} }
@ -217,7 +217,7 @@ class ProfileView extends React.Component {
} }
try { try {
if (avatar.url) { if (avatar!.url) {
try { try {
logEvent(events.PROFILE_SAVE_AVATAR); logEvent(events.PROFILE_SAVE_AVATAR);
await RocketChat.setAvatarFromService(avatar); await RocketChat.setAvatarFromService(avatar);
@ -283,7 +283,7 @@ class ProfileView extends React.Component {
}; };
try { try {
logEvent(events.PROFILE_PICK_AVATAR); 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' }); this.setAvatar({ url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' });
} catch (error) { } catch (error) {
logEvent(events.PROFILE_PICK_AVATAR_F); 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); logEvent(events.PROFILE_PICK_AVATAR_WITH_URL);
this.setAvatar({ url: avatarUrl, data: avatarUrl, service: '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; const { theme } = this.props;
return ( return (
<Touch <Touch
@ -331,7 +331,7 @@ class ProfileView extends React.Component {
})} })}
{this.renderAvatarButton({ {this.renderAvatarButton({
child: <CustomIcon name='link' size={30} color={themes[theme].bodyText} />, child: <CustomIcon name='link' size={30} color={themes[theme].bodyText} />,
onPress: () => this.pickImageWithURL(avatarUrl), onPress: () => this.pickImageWithURL(avatarUrl!),
disabled: !avatarUrl, disabled: !avatarUrl,
key: 'profile-view-avatar-url-button' key: 'profile-view-avatar-url-button'
})} })}
@ -365,19 +365,20 @@ class ProfileView extends React.Component {
const parsedCustomFields = JSON.parse(Accounts_CustomFields); const parsedCustomFields = JSON.parse(Accounts_CustomFields);
return Object.keys(parsedCustomFields).map((key, index, array) => { return Object.keys(parsedCustomFields).map((key, index, array) => {
if (parsedCustomFields[key].type === 'select') { 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 ( return (
<RNPickerSelect <RNPickerSelect
key={key} key={key}
items={options} items={options}
onValueChange={value => { onValueChange={value => {
const newValue = {}; const newValue: { [key: string]: string } = {};
newValue[key] = value; newValue[key] = value;
this.setState({ customFields: { ...customFields, ...newValue } }); this.setState({ customFields: { ...customFields, ...newValue } });
}} }}
value={customFields[key]}> value={customFields[key]}>
<RCTextInput <RCTextInput
inputRef={e => { inputRef={e => {
// @ts-ignore
this[key] = e; this[key] = e;
}} }}
label={key} label={key}
@ -393,6 +394,7 @@ class ProfileView extends React.Component {
return ( return (
<RCTextInput <RCTextInput
inputRef={e => { inputRef={e => {
// @ts-ignore
this[key] = e; this[key] = e;
}} }}
key={key} key={key}
@ -400,12 +402,13 @@ class ProfileView extends React.Component {
placeholder={key} placeholder={key}
value={customFields[key]} value={customFields[key]}
onChangeText={value => { onChangeText={value => {
const newValue = {}; const newValue: { [key: string]: string } = {};
newValue[key] = value; newValue[key] = value;
this.setState({ customFields: { ...customFields, ...newValue } }); this.setState({ customFields: { ...customFields, ...newValue } });
}} }}
onSubmitEditing={() => { onSubmitEditing={() => {
if (array.length - 1 > index) { if (array.length - 1 > index) {
// @ts-ignore
return this[array[index + 1]].focus(); return this[array[index + 1]].focus();
} }
this.avatarUrl.focus(); this.avatarUrl.focus();
@ -421,6 +424,7 @@ class ProfileView extends React.Component {
logoutOtherLocations = () => { logoutOtherLocations = () => {
logEvent(events.PL_OTHER_LOCATIONS); logEvent(events.PL_OTHER_LOCATIONS);
// @ts-ignore
showConfirmationAlert({ showConfirmationAlert({
message: I18n.t('You_will_be_logged_out_from_other_locations'), message: I18n.t('You_will_be_logged_out_from_other_locations'),
confirmationText: I18n.t('Logout'), confirmationText: I18n.t('Logout'),
@ -469,7 +473,7 @@ class ProfileView extends React.Component {
label={I18n.t('Name')} label={I18n.t('Name')}
placeholder={I18n.t('Name')} placeholder={I18n.t('Name')}
value={name} value={name}
onChangeText={value => this.setState({ name: value })} onChangeText={(value: string) => this.setState({ name: value })}
onSubmitEditing={() => { onSubmitEditing={() => {
this.username.focus(); this.username.focus();
}} }}
@ -500,7 +504,7 @@ class ProfileView extends React.Component {
}} }}
label={I18n.t('Email')} label={I18n.t('Email')}
placeholder={I18n.t('Email')} placeholder={I18n.t('Email')}
value={email} value={email!}
onChangeText={value => this.setState({ email: value })} onChangeText={value => this.setState({ email: value })}
onSubmitEditing={() => { onSubmitEditing={() => {
this.newPassword.focus(); this.newPassword.focus();
@ -516,10 +520,11 @@ class ProfileView extends React.Component {
}} }}
label={I18n.t('New_Password')} label={I18n.t('New_Password')}
placeholder={I18n.t('New_Password')} placeholder={I18n.t('New_Password')}
value={newPassword} value={newPassword!}
onChangeText={value => this.setState({ newPassword: value })} onChangeText={value => this.setState({ newPassword: value })}
onSubmitEditing={() => { onSubmitEditing={() => {
if (Accounts_CustomFields && Object.keys(customFields).length) { if (Accounts_CustomFields && Object.keys(customFields).length) {
// @ts-ignore
return this[Object.keys(customFields)[0]].focus(); return this[Object.keys(customFields)[0]].focus();
} }
this.avatarUrl.focus(); this.avatarUrl.focus();
@ -537,7 +542,7 @@ class ProfileView extends React.Component {
}} }}
label={I18n.t('Avatar_Url')} label={I18n.t('Avatar_Url')}
placeholder={I18n.t('Avatar_Url')} placeholder={I18n.t('Avatar_Url')}
value={avatarUrl} value={avatarUrl!}
onChangeText={value => this.setState({ avatarUrl: value })} onChangeText={value => this.setState({ avatarUrl: value })}
onSubmitEditing={this.submit} onSubmitEditing={this.submit}
testID='profile-view-avatar-url' testID='profile-view-avatar-url'
@ -568,7 +573,7 @@ class ProfileView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
user: getUserSelector(state), user: getUserSelector(state),
Accounts_AllowEmailChange: state.settings.Accounts_AllowEmailChange, Accounts_AllowEmailChange: state.settings.Accounts_AllowEmailChange,
Accounts_AllowPasswordChange: state.settings.Accounts_AllowPasswordChange, Accounts_AllowPasswordChange: state.settings.Accounts_AllowPasswordChange,
@ -579,8 +584,8 @@ const mapStateToProps = state => ({
baseUrl: state.server.server baseUrl: state.server.server
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: any) => ({
setUser: params => dispatch(setUserAction(params)) setUser: (params: any) => dispatch(setUserAction(params))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(ProfileView)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(ProfileView));

View File

@ -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<any, 'ProfileView'>;
isMasterDetail?: boolean;
}
export interface IProfileViewProps {
user: IUser;
navigation: StackNavigationProp<any, 'ProfileView'>;
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;
};
}