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: {
'import/resolver': {
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;
getCustomEmoji(): any;
avatarETag: string;
isStatic: boolean;
isStatic: boolean | string;
rid: string;
blockUnauthenticatedAccess: boolean;
serverVersion: string;

View File

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

View File

@ -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';

View File

@ -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<IKeyboardViewProps, any> {
@ -22,9 +19,7 @@ export default class KeyboardView extends React.PureComponent<IKeyboardViewProps
contentContainerStyle={contentContainerStyle}
scrollEnabled={scrollEnabled}
alwaysBounceVertical={false}
extraHeight={keyboardVerticalOffset}
// @ts-ignore
behavior='position'>
extraHeight={keyboardVerticalOffset}>
{children}
</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 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<IProfileViewProps, IProfileViewState> {
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 = () => <HeaderButton.Drawer navigation={navigation} />;
}
options.headerRight = () => (
<HeaderButton.Preferences onPress={() => navigation.navigate('UserPreferencesView')} testID='preferences-view-open' />
<HeaderButton.Preferences onPress={() => 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 (
<Touch
@ -331,7 +331,7 @@ class ProfileView extends React.Component {
})}
{this.renderAvatarButton({
child: <CustomIcon name='link' size={30} color={themes[theme].bodyText} />,
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 (
<RNPickerSelect
key={key}
items={options}
onValueChange={value => {
const newValue = {};
const newValue: { [key: string]: string } = {};
newValue[key] = value;
this.setState({ customFields: { ...customFields, ...newValue } });
}}
value={customFields[key]}>
<RCTextInput
inputRef={e => {
// @ts-ignore
this[key] = e;
}}
label={key}
@ -393,6 +394,7 @@ class ProfileView extends React.Component {
return (
<RCTextInput
inputRef={e => {
// @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));

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