diff --git a/.eslintrc.js b/.eslintrc.js index 952621fbf..6451c266f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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 diff --git a/app/AppContainer.tsx b/app/AppContainer.tsx index af3d34fe0..f67b38c7f 100644 --- a/app/AppContainer.tsx +++ b/app/AppContainer.tsx @@ -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(); -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 ( { + const { theme } = useTheme(); + if ((!text && !avatar && !emoji && !rid) || !server) { return null; } - const { theme } = useTheme(); - const avatarStyle = { width: size, height: size, diff --git a/app/containers/MessageBox/CommandsPreview/index.tsx b/app/containers/MessageBox/CommandsPreview/index.tsx index 924fc41e7..5f810b889 100644 --- a/app/containers/MessageBox/CommandsPreview/index.tsx +++ b/app/containers/MessageBox/CommandsPreview/index.tsx @@ -15,10 +15,12 @@ interface IMessageBoxCommandsPreview { const CommandsPreview = React.memo( ({ commandPreview, showCommandPreview }: IMessageBoxCommandsPreview) => { + const { theme } = useTheme(); + if (!showCommandPreview) { return null; } - const { theme } = useTheme(); + return ( { + const { theme } = useTheme(); + if (!trackingType) { return null; } - const { theme } = useTheme(); + return ( { + const { theme } = useTheme(); + if (!replying) { return null; } - const { theme } = useTheme(); + const time = moment(message.ts).format(Message_TimeFormat); return ( diff --git a/app/containers/UIKit/index.tsx b/app/containers/UIKit/index.tsx index 14ee93c25..31806d7cf 100644 --- a/app/containers/UIKit/index.tsx +++ b/app/containers/UIKit/index.tsx @@ -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'; diff --git a/app/containers/markdown/Preview.tsx b/app/containers/markdown/Preview.tsx index 65262da72..8cffc15ed 100644 --- a/app/containers/markdown/Preview.tsx +++ b/app/containers/markdown/Preview.tsx @@ -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); diff --git a/app/containers/message/Attachments.tsx b/app/containers/message/Attachments.tsx index 397620e1d..1f99e2336 100644 --- a/app/containers/message/Attachments.tsx +++ b/app/containers/message/Attachments.tsx @@ -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 = 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 ( diff --git a/app/containers/message/Components/CollapsibleQuote/index.tsx b/app/containers/message/Components/CollapsibleQuote/index.tsx index 2def3340c..03abc25cd 100644 --- a/app/containers/message/Components/CollapsibleQuote/index.tsx +++ b/app/containers/message/Components/CollapsibleQuote/index.tsx @@ -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); diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index 243e80948..c742cd503 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -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 = ; } else { - const { baseUrl, user, onLinkPress } = useContext(MessageContext); content = ( { const { theme } = useTheme(); + const { onEncryptedPress } = useContext(MessageContext); + if (type !== E2E_MESSAGE_TYPE) { return null; } - const { onEncryptedPress } = useContext(MessageContext); return ( diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index f99b11a41..aa572aad1 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -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 ( @@ -117,8 +120,6 @@ const MessageTouchable = React.memo((props: IMessageTouchable & IMessage) => { ); } - const { onPress, onLongPress } = useContext(MessageContext); - const { theme } = useTheme(); return ( { const { theme } = useTheme(); + const { onErrorPress } = useContext(MessageContext); if (!hasError) { return null; } - const { onErrorPress } = useContext(MessageContext); + return ( diff --git a/app/containers/message/RepliedThread.tsx b/app/containers/message/RepliedThread.tsx index b681264dc..c81b096e8 100644 --- a/app/containers/message/RepliedThread.tsx +++ b/app/containers/message/RepliedThread.tsx @@ -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; } diff --git a/app/containers/message/Reply.tsx b/app/containers/message/Reply.tsx index 757f0e80b..27a197c51 100644 --- a/app/containers/message/Reply.tsx +++ b/app/containers/message/Reply.tsx @@ -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 ( { + 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 ; }, @@ -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 ( {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) { diff --git a/app/containers/message/Thread.tsx b/app/containers/message/Thread.tsx index 00d9e5ab5..6e5a25d2b 100644 --- a/app/containers/message/Thread.tsx +++ b/app/containers/message/Thread.tsx @@ -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 ( diff --git a/app/containers/message/Urls.tsx b/app/containers/message/Urls.tsx index 91846e1bb..b1c5b5c06 100644 --- a/app/containers/message/Urls.tsx +++ b/app/containers/message/Urls.tsx @@ -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 ; }, diff --git a/app/containers/message/User.tsx b/app/containers/message/User.tsx index e8a7ee7de..b6c974d0b 100644 --- a/app/containers/message/User.tsx +++ b/app/containers/message/User.tsx @@ -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 ? ( @{username} diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 37ed92cac..288b7049f 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -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; jumpToMessage?: (link: string) => void; onPress?: () => void; + theme: string; } interface IMessageContainerState { @@ -292,8 +293,7 @@ class MessageContainer extends React.Component { - 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); diff --git a/app/ee/omnichannel/containers/OmnichannelStatus.tsx b/app/ee/omnichannel/containers/OmnichannelStatus.tsx index 3df4e544d..95ef0a0b2 100644 --- a/app/ee/omnichannel/containers/OmnichannelStatus.tsx +++ b/app/ee/omnichannel/containers/OmnichannelStatus.tsx @@ -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(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); diff --git a/app/lib/methods/logout.ts b/app/lib/methods/logout.ts index eac5b8ac9..5a8247ab5 100644 --- a/app/lib/methods/logout.ts +++ b/app/lib/methods/logout.ts @@ -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 // @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 diff --git a/app/lib/services/sdk.ts b/app/lib/services/sdk.ts index eb2c56b25..78aab00d7 100644 --- a/app/lib/services/sdk.ts +++ b/app/lib/services/sdk.ts @@ -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 diff --git a/app/sagas/inviteLinks.js b/app/sagas/inviteLinks.js index de67c52a8..f37048074 100644 --- a/app/sagas/inviteLinks.js +++ b/app/sagas/inviteLinks.js @@ -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; diff --git a/app/utils/url.ts b/app/utils/url.ts index 501795973..efe675b7e 100644 --- a/app/utils/url.ts +++ b/app/utils/url.ts @@ -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); diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index a3f1ce658..e5a5a5e78 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -201,6 +201,7 @@ class MessagesView extends React.Component { renderItem: (item: any) => ( { + 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 ; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 773f09b88..994538e42 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1144,7 +1144,8 @@ class RoomView extends React.Component { 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 { toggleFollowThread={this.toggleFollowThread} jumpToMessage={this.jumpToMessageByUrl} highlighted={highlightedMessage === item.id} + theme={theme} /> ); } diff --git a/app/views/RoomsListView/ListHeader/index.tsx b/app/views/RoomsListView/ListHeader/index.tsx index 57ee9a7bc..d5fd96e3f 100644 --- a/app/views/RoomsListView/ListHeader/index.tsx +++ b/app/views/RoomsListView/ListHeader/index.tsx @@ -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 ? ( diff --git a/package.json b/package.json index a19a544b3..ef18e865a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 02080610a..3f462d8c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"