Chore: add eslint-plugin-react-hooks lib (#4021)

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Alex Junior 2022-04-11 15:01:43 -03:00 committed by GitHub
parent 53eb251476
commit b0d408ebc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 113 additions and 72 deletions

View File

@ -17,7 +17,7 @@ module.exports = {
legacyDecorators: true
}
},
plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel', 'jest'],
plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel', 'jest', 'react-hooks'],
env: {
browser: true,
commonjs: true,
@ -148,7 +148,9 @@ module.exports = {
'no-async-promise-executor': [0],
'max-classes-per-file': [0],
'no-multiple-empty-lines': [0],
'no-sequences': 'off'
'no-sequences': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
},
globals: {
__DEV__: true
@ -237,7 +239,9 @@ module.exports = {
}
],
'new-cap': 'off',
'lines-between-class-members': 'off'
'lines-between-class-members': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
},
globals: {
JSX: true

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useContext, memo, useEffect } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { connect } from 'react-redux';
@ -27,21 +27,23 @@ const SetUsernameStack = () => (
// App
const Stack = createStackNavigator<StackParamList>();
const App = React.memo(({ root, isMasterDetail }: { root: string; isMasterDetail: boolean }) => {
const App = memo(({ root, isMasterDetail }: { root: string; isMasterDetail: boolean }) => {
const { theme } = useContext(ThemeContext);
useEffect(() => {
if (root) {
const state = Navigation.navigationRef.current?.getRootState();
const currentRouteName = getActiveRouteName(state);
Navigation.routeNameRef.current = currentRouteName;
setCurrentScreen(currentRouteName);
}
}, [root]);
if (!root) {
return null;
}
const { theme } = React.useContext(ThemeContext);
const navTheme = navigationTheme(theme);
React.useEffect(() => {
const state = Navigation.navigationRef.current?.getRootState();
const currentRouteName = getActiveRouteName(state);
Navigation.routeNameRef.current = currentRouteName;
setCurrentScreen(currentRouteName);
}, []);
return (
<NavigationContainer
theme={navTheme}

View File

@ -30,12 +30,12 @@ const Avatar = React.memo(
borderRadius = 4,
type = SubscriptionType.DIRECT
}: IAvatar) => {
const { theme } = useTheme();
if ((!text && !avatar && !emoji && !rid) || !server) {
return null;
}
const { theme } = useTheme();
const avatarStyle = {
width: size,
height: size,

View File

@ -15,10 +15,12 @@ interface IMessageBoxCommandsPreview {
const CommandsPreview = React.memo(
({ commandPreview, showCommandPreview }: IMessageBoxCommandsPreview) => {
const { theme } = useTheme();
if (!showCommandPreview) {
return null;
}
const { theme } = useTheme();
return (
<FlatList
testID='commandbox-container'

View File

@ -16,10 +16,12 @@ interface IMessageBoxMentions {
const Mentions = React.memo(
({ mentions, trackingType, loading }: IMessageBoxMentions) => {
const { theme } = useTheme();
if (!trackingType) {
return null;
}
const { theme } = useTheme();
return (
<View testID='messagebox-container'>
<FlatList

View File

@ -56,10 +56,12 @@ interface IMessageBoxReplyPreview {
const ReplyPreview = React.memo(
({ message, Message_TimeFormat, replying, close, useRealName }: IMessageBoxReplyPreview) => {
const { theme } = useTheme();
if (!replying) {
return null;
}
const { theme } = useTheme();
const time = moment(message.ts).format(Message_TimeFormat);
return (
<View style={[styles.container, { backgroundColor: themes[theme].messageboxBackground }]}>

View File

@ -1,4 +1,4 @@
/* eslint-disable class-methods-use-this */
/* eslint-disable react-hooks/rules-of-hooks */
import React, { useContext } from 'react';
import { StyleSheet, Text } from 'react-native';
import { BLOCK_CONTEXT, UiKitParserMessage, UiKitParserModal, uiKitMessage, uiKitModal } from '@rocket.chat/ui-kit';

View File

@ -17,12 +17,12 @@ interface IMarkdownPreview {
}
const MarkdownPreview = ({ msg, numberOfLines = 1, testID, style = [] }: IMarkdownPreview) => {
const { theme } = useTheme();
if (!msg) {
return null;
}
const { theme } = useTheme();
let m = formatText(msg);
m = formatHyperlink(m);
m = shortnameToUnicode(m);

View File

@ -24,11 +24,12 @@ export type TElement = {
};
const AttachedActions = ({ attachment }: { attachment: IAttachment }) => {
const { onAnswerButtonPress } = useContext(MessageContext);
const { theme } = useTheme();
if (!attachment.actions) {
return null;
}
const { onAnswerButtonPress } = useContext(MessageContext);
const { theme } = useTheme();
const attachedButtons = attachment.actions.map((element: TElement) => {
const onPress = () => {
@ -57,12 +58,12 @@ const AttachedActions = ({ attachment }: { attachment: IAttachment }) => {
const Attachments: React.FC<IMessageAttachments> = React.memo(
({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply }: IMessageAttachments) => {
const { theme } = useTheme();
if (!attachments || attachments.length === 0) {
return null;
}
const { theme } = useTheme();
const attachmentsElements = attachments.map((file: IAttachment, index: number) => {
if (file && file.image_url) {
return (

View File

@ -81,11 +81,13 @@ interface IMessageReply {
const Fields = React.memo(
({ attachment, getCustomEmoji }: IMessageFields) => {
const { theme } = useTheme();
const { baseUrl, user } = useContext(MessageContext);
if (!attachment.fields) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
const { theme } = useTheme();
return (
<>
{attachment.fields.map(field => (
@ -111,11 +113,12 @@ const Fields = React.memo(
const CollapsibleQuote = React.memo(
({ attachment, index, getCustomEmoji }: IMessageReply) => {
const { theme } = useTheme();
const [collapsed, setCollapsed] = useState(attachment?.collapsed);
if (!attachment) {
return null;
}
const [collapsed, setCollapsed] = useState(attachment.collapsed);
const { theme } = useTheme();
const onPress = () => {
setCollapsed(!collapsed);

View File

@ -16,6 +16,8 @@ import { E2E_MESSAGE_TYPE, themes } from '../../lib/constants';
const Content = React.memo(
(props: IMessageContent) => {
const { theme } = useTheme();
const { baseUrl, user, onLinkPress } = useContext(MessageContext);
if (props.isInfo) {
// @ts-ignore
const infoMessage = getInfoMessage({ ...props });
@ -49,7 +51,6 @@ const Content = React.memo(
} else if (isPreview) {
content = <MarkdownPreview msg={props.msg} />;
} else {
const { baseUrl, user, onLinkPress } = useContext(MessageContext);
content = (
<Markdown
msg={props.msg}

View File

@ -10,11 +10,12 @@ import { E2E_MESSAGE_TYPE, themes } from '../../lib/constants';
const Encrypted = React.memo(({ type }: { type: string }) => {
const { theme } = useTheme();
const { onEncryptedPress } = useContext(MessageContext);
if (type !== E2E_MESSAGE_TYPE) {
return null;
}
const { onEncryptedPress } = useContext(MessageContext);
return (
<Touchable onPress={onEncryptedPress} style={styles.encrypted} hitSlop={BUTTON_HIT_SLOP}>
<CustomIcon name='encrypted' size={16} color={themes[theme].auxiliaryText} />

View File

@ -110,6 +110,9 @@ const Message = React.memo((props: IMessage) => {
Message.displayName = 'Message';
const MessageTouchable = React.memo((props: IMessageTouchable & IMessage) => {
const { onPress, onLongPress } = useContext(MessageContext);
const { theme } = useTheme();
if (props.hasError) {
return (
<View>
@ -117,8 +120,6 @@ const MessageTouchable = React.memo((props: IMessageTouchable & IMessage) => {
</View>
);
}
const { onPress, onLongPress } = useContext(MessageContext);
const { theme } = useTheme();
return (
<Touchable

View File

@ -11,11 +11,12 @@ import { useTheme } from '../../theme';
const MessageError = React.memo(
({ hasError }: { hasError: boolean }) => {
const { theme } = useTheme();
const { onErrorPress } = useContext(MessageContext);
if (!hasError) {
return null;
}
const { onErrorPress } = useContext(MessageContext);
return (
<Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}>
<CustomIcon name='warning' color={themes[theme].dangerColor} size={18} />

View File

@ -11,16 +11,7 @@ import { useTheme } from '../../theme';
const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted }: IMessageRepliedThread) => {
const { theme } = useTheme();
if (!tmid || !isHeader) {
return null;
}
const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg);
const fetch = async () => {
const threadName = fetchThreadName ? await fetchThreadName(tmid, id) : '';
setMsg(threadName);
};
useEffect(() => {
if (!msg) {
@ -28,6 +19,15 @@ const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncry
}
}, []);
if (!tmid || !isHeader) {
return null;
}
const fetch = async () => {
const threadName = fetchThreadName ? await fetchThreadName(tmid, id) : '';
setMsg(threadName);
};
if (!msg) {
return null;
}

View File

@ -113,11 +113,13 @@ const Title = React.memo(({ attachment, timeFormat, theme }: { attachment: IAtta
const Description = React.memo(
({ attachment, getCustomEmoji, theme }: { attachment: IAttachment; getCustomEmoji: TGetCustomEmoji; theme: string }) => {
const { baseUrl, user } = useContext(MessageContext);
const text = attachment.text || attachment.title;
if (!text) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
return (
<Markdown
msg={text}
@ -145,10 +147,12 @@ const Description = React.memo(
const UrlImage = React.memo(
({ image }: { image?: string }) => {
const { baseUrl, user } = useContext(MessageContext);
if (!image) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
image = image.includes('http') ? image : `${baseUrl}/${image}?rc_uid=${user.id}&rc_token=${user.token}`;
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
},
@ -157,11 +161,12 @@ const UrlImage = React.memo(
const Fields = React.memo(
({ attachment, theme, getCustomEmoji }: { attachment: IAttachment; theme: string; getCustomEmoji: TGetCustomEmoji }) => {
const { baseUrl, user } = useContext(MessageContext);
if (!attachment.fields) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
return (
<View style={styles.fieldsContainer}>
{attachment.fields.map(field => (
@ -187,13 +192,12 @@ const Reply = React.memo(
({ attachment, timeFormat, index, getCustomEmoji }: IMessageReply) => {
const [loading, setLoading] = useState(false);
const { theme } = useTheme();
const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
if (!attachment) {
return null;
}
const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
const onPress = async () => {
let url = attachment.title_link || attachment.author_link;
if (attachment.message_link) {

View File

@ -12,12 +12,12 @@ import { useTheme } from '../../theme';
const Thread = React.memo(
({ msg, tcount, tlm, isThreadRoom, id }: IMessageThread) => {
const { theme } = useTheme();
const { threadBadgeColor, toggleFollowThread, user, replies } = useContext(MessageContext);
if (!tlm || isThreadRoom || tcount === 0) {
return null;
}
const { threadBadgeColor, toggleFollowThread, user, replies } = useContext(MessageContext);
return (
<View style={styles.buttonContainer}>
<View style={[styles.button, { backgroundColor: themes[theme].tintColor }]} testID={`message-thread-button-${msg}`}>

View File

@ -53,10 +53,12 @@ const styles = StyleSheet.create({
const UrlImage = React.memo(
({ image }: { image: string }) => {
const { baseUrl, user } = useContext(MessageContext);
if (!image) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
image = image.includes('http') ? image : `${baseUrl}/${image}?rc_uid=${user.id}&rc_token=${user.token}`;
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
},

View File

@ -57,9 +57,10 @@ interface IMessageUser {
const User = React.memo(
({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, navToRoomInfo, type, ...props }: IMessageUser) => {
const { user } = useContext(MessageContext);
const { theme } = useTheme();
if (isHeader || hasError) {
const { user } = useContext(MessageContext);
const { theme } = useTheme();
const username = (useRealName && author?.name) || author?.username;
const aliasUsername = alias ? (
<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>

View File

@ -6,7 +6,7 @@ import Message from './Message';
import MessageContext from './Context';
import debounce from '../../utils/debounce';
import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
import { useTheme, withTheme } from '../../theme';
import { withTheme } from '../../theme';
import openLink from '../../utils/openLink';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IAttachment, TAnyMessageModel } from '../../definitions';
@ -57,6 +57,7 @@ interface IMessageContainerProps {
toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>;
jumpToMessage?: (link: string) => void;
onPress?: () => void;
theme: string;
}
interface IMessageContainerState {
@ -292,8 +293,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
};
onLinkPress = (link: string): void => {
const { theme } = useTheme();
const { item, jumpToMessage } = this.props;
const { item, jumpToMessage, theme } = this.props;
const isMessageLink = item?.attachments?.findIndex((att: IAttachment) => att?.message_link === link) !== -1;
if (isMessageLink && jumpToMessage) {
return jumpToMessage(link);

View File

@ -20,16 +20,20 @@ interface IOmnichannelStatus {
}
const OmnichannelStatus = memo(({ searching, goQueue, queueSize, inquiryEnabled, user }: IOmnichannelStatus) => {
if (searching || !(RocketChat.isOmnichannelModuleAvailable() && user?.roles?.includes('livechat-agent'))) {
return null;
}
const { theme } = useTheme();
const [status, setStatus] = useState(isOmnichannelStatusAvailable(user));
const [status, setStatus] = useState<boolean>(false);
const canUseOmnichannel = RocketChat.isOmnichannelModuleAvailable() && user?.roles?.includes('livechat-agent');
useEffect(() => {
setStatus(isOmnichannelStatusAvailable(user));
if (canUseOmnichannel) {
setStatus(isOmnichannelStatusAvailable(user));
}
}, [user.statusLivechat]);
if (searching || !canUseOmnichannel) {
return null;
}
const toggleLivechat = async () => {
try {
setStatus(v => !v);

View File

@ -6,7 +6,7 @@ import { getDeviceToken } from '../../notifications/push';
import { extractHostname } from '../../utils/server';
import { BASIC_AUTH_KEY } from '../../utils/fetch';
import database, { getDatabase } from '../database';
import { useSsl } from '../../utils/url';
import { isSsl } from '../../utils/url';
import log from '../../utils/log';
import { ICertificate, IRocketChat } from '../../definitions';
import sdk from '../services/sdk';
@ -80,7 +80,7 @@ export async function removeServer({ server }: { server: string }): Promise<void
if (userId) {
const resume = UserPreferences.getString(`${RocketChat.TOKEN_KEY}-${userId}`);
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: isSsl(server) });
await sdk.login({ resume });
const token = getDeviceToken();

View File

@ -760,7 +760,7 @@ export const validateInviteToken = (token: string): any =>
// @ts-ignore
sdk.post('validateInviteToken', { token });
export const useInviteToken = (token: string): any =>
export const inviteToken = (token: string): any =>
// RC 2.4.0
// TODO: missing definitions from server
// @ts-ignore

View File

@ -3,7 +3,7 @@ import EJSON from 'ejson';
import isEmpty from 'lodash/isEmpty';
import { twoFactor } from '../../utils/twoFactor';
import { useSsl } from '../../utils/url';
import { isSsl } from '../../utils/url';
import { store as reduxStore } from '../store/auxStore';
import { Serialized, MatchPathPattern, OperationParams, PathFor, ResultFor } from '../../definitions/rest/helpers';
@ -14,7 +14,7 @@ class Sdk {
private initializeSdk(server: string): typeof Rocketchat {
// The app can't reconnect if reopen interval is 5s while in development
return new Rocketchat({ host: server, protocol: 'ddp', useSsl: useSsl(server), reopen: __DEV__ ? 20000 : 5000 });
return new Rocketchat({ host: server, protocol: 'ddp', useSsl: isSsl(server), reopen: __DEV__ ? 20000 : 5000 });
}
// TODO: We need to stop returning the SDK after all methods are dehydrated

View File

@ -16,7 +16,7 @@ const handleRequest = function* handleRequest({ token }) {
return;
}
const result = yield RocketChat.useInviteToken(token);
const result = yield RocketChat.inviteToken(token);
if (!result.success) {
yield put(inviteLinksFailure());
return;

View File

@ -11,5 +11,5 @@ export const isValidURL = (url: string): boolean => {
return !!pattern.test(url);
};
// Use useSsl: false only if server url starts with http://
export const useSsl = (url: string): boolean => !/http:\/\//.test(url);
// Use checkUseSsl: false only if server url starts with http://
export const isSsl = (url: string): boolean => !/http:\/\//.test(url);

View File

@ -201,6 +201,7 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
renderItem: (item: any) => (
<Message
{...renderItemCommonProps(item)}
theme={theme}
item={{
...item,
u: item.user,

View File

@ -40,6 +40,8 @@ const LeftButtons = ({
goRoomActionsView,
isMasterDetail
}: ILeftButtonsProps): React.ReactElement | null => {
const onPress = useCallback(() => goRoomActionsView(), []);
if (!isMasterDetail || tmid) {
const onPress = () => navigation.goBack();
let label = ' ';
@ -61,7 +63,6 @@ const LeftButtons = ({
/>
);
}
const onPress = useCallback(() => goRoomActionsView(), []);
if (baseUrl && userId && token) {
return <Avatar text={title} size={30} type={t} style={styles.avatar} onPress={onPress} />;

View File

@ -1144,7 +1144,8 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
renderItem = (item: TAnyMessageModel, previousItem: TAnyMessageModel, highlightedMessage?: string) => {
const { room, lastOpen, canAutoTranslate } = this.state;
const { user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled } = this.props;
const { user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme } =
this.props;
let dateSeparator = null;
let showUnreadSeparator = false;
@ -1207,6 +1208,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
toggleFollowThread={this.toggleFollowThread}
jumpToMessage={this.jumpToMessageByUrl}
highlighted={highlightedMessage === item.id}
theme={theme}
/>
);
}

View File

@ -20,12 +20,12 @@ interface IRoomListHeader {
const ListHeader = React.memo(
({ searching, goEncryption, goQueue, queueSize, inquiryEnabled, encryptionBanner, user }: IRoomListHeader) => {
const { theme } = useTheme();
if (searching) {
return null;
}
const { theme } = useTheme();
return (
<>
{encryptionBanner ? (

View File

@ -174,6 +174,7 @@
"eslint-plugin-jest": "24.7.0",
"eslint-plugin-jsx-a11y": "6.3.1",
"eslint-plugin-react": "7.20.3",
"eslint-plugin-react-hooks": "^4.4.0",
"eslint-plugin-react-native": "3.8.1",
"husky": "^6.0.0",
"identity-obj-proxy": "^3.0.0",

View File

@ -8570,6 +8570,11 @@ eslint-plugin-jsx-a11y@6.3.1:
jsx-ast-utils "^2.4.1"
language-tags "^1.0.5"
eslint-plugin-react-hooks@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.4.0.tgz#71c39e528764c848d8253e1aa2c7024ed505f6c4"
integrity sha512-U3RVIfdzJaeKDQKEJbz5p3NW8/L80PCATJAfuojwbaEL+gBjfGdhUcGde+WGUW46Q5sr/NgxevsIiDtNXrvZaQ==
eslint-plugin-react-native-globals@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz#ee1348bc2ceb912303ce6bdbd22e2f045ea86ea2"