diff --git a/.eslintrc.js b/.eslintrc.js index 085f3a89d..952621fbf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,14 +17,15 @@ module.exports = { legacyDecorators: true } }, - plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel'], + plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel', 'jest'], env: { browser: true, commonjs: true, es6: true, node: true, jquery: true, - mocha: true + mocha: true, + 'jest/globals': true }, rules: { 'import/extensions': [ diff --git a/android/app/build.gradle b/android/app/build.gradle index 524eebd82..c95a2f79b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -144,7 +144,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode VERSIONCODE as Integer - versionName "4.23.0" + versionName "4.24.0" vectorDrawables.useSupportLibrary = true if (!isFoss) { manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] diff --git a/android/app/src/main/java/chat/rocket/reactnative/networking/SSLPinningModule.java b/android/app/src/main/java/chat/rocket/reactnative/networking/SSLPinningModule.java index 6a690a180..aad807859 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/networking/SSLPinningModule.java +++ b/android/app/src/main/java/chat/rocket/reactnative/networking/SSLPinningModule.java @@ -11,9 +11,12 @@ import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.Promise; import java.net.Socket; +import java.security.KeyStore; import java.security.Principal; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; + +import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509ExtendedKeyManager; import java.security.PrivateKey; import javax.net.ssl.SSLContext; @@ -21,11 +24,12 @@ import javax.net.ssl.X509TrustManager; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import okhttp3.OkHttpClient; -import java.lang.InterruptedException; import android.app.Activity; import javax.net.ssl.KeyManager; import android.security.KeyChain; import android.security.KeyChainAliasCallback; + +import java.util.Arrays; import java.util.concurrent.TimeUnit; import com.RNFetchBlob.RNFetchBlob; @@ -52,8 +56,9 @@ public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyC public void apply(OkHttpClient.Builder builder) { if (alias != null) { SSLSocketFactory sslSocketFactory = getSSLFactory(alias); + X509TrustManager trustManager = getTrustManagerFactory(); if (sslSocketFactory != null) { - builder.sslSocketFactory(sslSocketFactory); + builder.sslSocketFactory(sslSocketFactory, trustManager); } } } @@ -68,8 +73,9 @@ public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyC if (alias != null) { SSLSocketFactory sslSocketFactory = getSSLFactory(alias); + X509TrustManager trustManager = getTrustManagerFactory(); if (sslSocketFactory != null) { - builder.sslSocketFactory(sslSocketFactory); + builder.sslSocketFactory(sslSocketFactory, trustManager); } } @@ -162,25 +168,9 @@ public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyC } }; - final TrustManager[] trustAllCerts = new TrustManager[] { - new X509TrustManager() { - @Override - public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { - } - - @Override - public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { - } - - @Override - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return certChain; - } - } - }; - + final X509TrustManager trustManager = getTrustManagerFactory(); final SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(new KeyManager[]{keyManager}, trustAllCerts, new java.security.SecureRandom()); + sslContext.init(new KeyManager[]{keyManager}, new TrustManager[]{trustManager}, new java.security.SecureRandom()); SSLContext.setDefault(sslContext); final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); @@ -190,4 +180,19 @@ public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyC return null; } } + + public static X509TrustManager getTrustManagerFactory() { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers)); + } + final X509TrustManager trustManager = (X509TrustManager) trustManagers[0]; + return trustManager; + } catch (Exception e) { + return null; + } + } } diff --git a/app/AppContainer.tsx b/app/AppContainer.tsx index b73aaf8e3..441220fec 100644 --- a/app/AppContainer.tsx +++ b/app/AppContainer.tsx @@ -3,7 +3,7 @@ import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import { connect } from 'react-redux'; -import { SetUsernameStackParamList, StackParamList } from './navigationTypes'; +import { SetUsernameStackParamList, StackParamList } from './definitions/navigationTypes'; import Navigation from './lib/Navigation'; import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation'; import { ROOT_INSIDE, ROOT_LOADING, ROOT_OUTSIDE, ROOT_SET_USERNAME } from './actions/app'; diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.ts similarity index 96% rename from app/actions/actionsTypes.js rename to app/actions/actionsTypes.ts index 852ce83ea..ad2d1718d 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.ts @@ -2,8 +2,8 @@ const REQUEST = 'REQUEST'; const SUCCESS = 'SUCCESS'; const FAILURE = 'FAILURE'; const defaultTypes = [REQUEST, SUCCESS, FAILURE]; -function createRequestTypes(base, types = defaultTypes) { - const res = {}; +function createRequestTypes(base = {}, types = defaultTypes): Record { + const res: Record = {}; types.forEach(type => (res[type] = `${base}_${type}`)); return res; } diff --git a/app/actions/activeUsers.js b/app/actions/activeUsers.js deleted file mode 100644 index fc359602c..000000000 --- a/app/actions/activeUsers.js +++ /dev/null @@ -1,8 +0,0 @@ -import { SET_ACTIVE_USERS } from './actionsTypes'; - -export function setActiveUsers(activeUsers) { - return { - type: SET_ACTIVE_USERS, - activeUsers - }; -} diff --git a/app/actions/activeUsers.ts b/app/actions/activeUsers.ts new file mode 100644 index 000000000..737ae86b3 --- /dev/null +++ b/app/actions/activeUsers.ts @@ -0,0 +1,15 @@ +import { Action } from 'redux'; + +import { IActiveUsers } from '../reducers/activeUsers'; +import { SET_ACTIVE_USERS } from './actionsTypes'; + +export interface ISetActiveUsers extends Action { + activeUsers: IActiveUsers; +} + +export type TActionActiveUsers = ISetActiveUsers; + +export const setActiveUsers = (activeUsers: IActiveUsers): ISetActiveUsers => ({ + type: SET_ACTIVE_USERS, + activeUsers +}); diff --git a/app/actions/selectedUsers.js b/app/actions/selectedUsers.js deleted file mode 100644 index 65fbb0015..000000000 --- a/app/actions/selectedUsers.js +++ /dev/null @@ -1,28 +0,0 @@ -import * as types from './actionsTypes'; - -export function addUser(user) { - return { - type: types.SELECTED_USERS.ADD_USER, - user - }; -} - -export function removeUser(user) { - return { - type: types.SELECTED_USERS.REMOVE_USER, - user - }; -} - -export function reset() { - return { - type: types.SELECTED_USERS.RESET - }; -} - -export function setLoading(loading) { - return { - type: types.SELECTED_USERS.SET_LOADING, - loading - }; -} diff --git a/app/actions/selectedUsers.ts b/app/actions/selectedUsers.ts new file mode 100644 index 000000000..6924a5696 --- /dev/null +++ b/app/actions/selectedUsers.ts @@ -0,0 +1,43 @@ +import { Action } from 'redux'; + +import { ISelectedUser } from '../reducers/selectedUsers'; +import * as types from './actionsTypes'; + +type TUser = { + user: ISelectedUser; +}; + +type TAction = Action & TUser; + +interface ISetLoading extends Action { + loading: boolean; +} + +export type TActionSelectedUsers = TAction & ISetLoading; + +export function addUser(user: ISelectedUser): TAction { + return { + type: types.SELECTED_USERS.ADD_USER, + user + }; +} + +export function removeUser(user: ISelectedUser): TAction { + return { + type: types.SELECTED_USERS.REMOVE_USER, + user + }; +} + +export function reset(): Action { + return { + type: types.SELECTED_USERS.RESET + }; +} + +export function setLoading(loading: boolean): ISetLoading { + return { + type: types.SELECTED_USERS.SET_LOADING, + loading + }; +} diff --git a/app/constants/constantDisplayMode.js b/app/constants/constantDisplayMode.js deleted file mode 100644 index d7d7e1d53..000000000 --- a/app/constants/constantDisplayMode.js +++ /dev/null @@ -1,2 +0,0 @@ -export const DISPLAY_MODE_CONDENSED = 'condensed'; -export const DISPLAY_MODE_EXPANDED = 'expanded'; diff --git a/app/constants/constantDisplayMode.ts b/app/constants/constantDisplayMode.ts new file mode 100644 index 000000000..ecb6bd4b7 --- /dev/null +++ b/app/constants/constantDisplayMode.ts @@ -0,0 +1,9 @@ +export enum DisplayMode { + Condensed = 'condensed', + Expanded = 'expanded' +} + +export enum SortBy { + Alphabetical = 'alphabetical', + Activity = 'activity' +} diff --git a/app/containers/ActionSheet/Button.ts b/app/containers/ActionSheet/Button.ts index 5deb0f692..186cc6851 100644 --- a/app/containers/ActionSheet/Button.ts +++ b/app/containers/ActionSheet/Button.ts @@ -1,7 +1,8 @@ +import React from 'react'; import { TouchableOpacity } from 'react-native'; import { isAndroid } from '../../utils/deviceInfo'; import Touch from '../../utils/touch'; // Taken from https://github.com/rgommezz/react-native-scroll-bottom-sheet#touchables -export const Button = isAndroid ? Touch : TouchableOpacity; +export const Button: typeof React.Component = isAndroid ? Touch : TouchableOpacity; diff --git a/app/containers/Avatar/Avatar.tsx b/app/containers/Avatar/Avatar.tsx index 286bcc060..0ad2634f2 100644 --- a/app/containers/Avatar/Avatar.tsx +++ b/app/containers/Avatar/Avatar.tsx @@ -5,6 +5,7 @@ import Touchable from 'react-native-platform-touchable'; import { settings as RocketChatSettings } from '@rocket.chat/sdk'; import { avatarURL } from '../../utils/avatar'; +import { SubscriptionType } from '../../definitions/ISubscription'; import Emoji from '../markdown/Emoji'; import { IAvatar } from './interfaces'; @@ -27,8 +28,8 @@ const Avatar = React.memo( text, size = 25, borderRadius = 4, - type = 'd' - }: Partial) => { + type = SubscriptionType.DIRECT + }: IAvatar) => { if ((!text && !avatar && !emoji && !rid) || !server) { return null; } diff --git a/app/containers/Avatar/index.tsx b/app/containers/Avatar/index.tsx index 9c95db839..2ce066479 100644 --- a/app/containers/Avatar/index.tsx +++ b/app/containers/Avatar/index.tsx @@ -7,17 +7,17 @@ import { getUserSelector } from '../../selectors/login'; import Avatar from './Avatar'; import { IAvatar } from './interfaces'; -class AvatarContainer extends React.Component, any> { +class AvatarContainer extends React.Component { private mounted: boolean; - private subscription!: any; + private subscription: any; static defaultProps = { text: '', type: 'd' }; - constructor(props: Partial) { + constructor(props: IAvatar) { super(props); this.mounted = false; this.state = { avatarETag: '' }; @@ -55,7 +55,7 @@ class AvatarContainer extends React.Component, any> { try { if (this.isDirect) { const { text } = this.props; - const [user] = await usersCollection.query(Q.where('username', text!)).fetch(); + const [user] = await usersCollection.query(Q.where('username', text)).fetch(); record = user; } else { const { rid } = this.props; @@ -82,7 +82,7 @@ class AvatarContainer extends React.Component, any> { render() { const { avatarETag } = this.state; const { serverVersion } = this.props; - return ; + return ; } } diff --git a/app/containers/Avatar/interfaces.ts b/app/containers/Avatar/interfaces.ts index ed7fd3b9e..78152e522 100644 --- a/app/containers/Avatar/interfaces.ts +++ b/app/containers/Avatar/interfaces.ts @@ -1,23 +1,23 @@ export interface IAvatar { - server: string; - style: any; + server?: string; + style?: any; text: string; - avatar: string; - emoji: string; - size: number; - borderRadius: number; - type: string; - children: JSX.Element; - user: { - id: string; - token: string; + avatar?: string; + emoji?: string; + size?: number; + borderRadius?: number; + type?: string; + children?: JSX.Element; + user?: { + id?: string; + token?: string; }; - theme: string; - onPress(): void; - getCustomEmoji(): any; - avatarETag: string; - isStatic: boolean | string; - rid: string; - blockUnauthenticatedAccess: boolean; - serverVersion: string; + theme?: string; + onPress?: () => void; + getCustomEmoji?: () => any; + avatarETag?: string; + isStatic?: boolean | string; + rid?: string; + blockUnauthenticatedAccess?: boolean; + serverVersion?: string; } diff --git a/app/containers/BackgroundContainer/index.tsx b/app/containers/BackgroundContainer/index.tsx index fbe5d3077..fc26fe0ab 100644 --- a/app/containers/BackgroundContainer/index.tsx +++ b/app/containers/BackgroundContainer/index.tsx @@ -6,9 +6,9 @@ import sharedStyles from '../../views/Styles'; import { themes } from '../../constants/colors'; interface IBackgroundContainer { - text: string; - theme: string; - loading: boolean; + text?: string; + theme?: string; + loading?: boolean; } const styles = StyleSheet.create({ @@ -35,8 +35,8 @@ const styles = StyleSheet.create({ const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => ( - {text ? {text} : null} - {loading ? : null} + {text ? {text} : null} + {loading ? : null} ); diff --git a/app/containers/HeaderButton/Common.tsx b/app/containers/HeaderButton/Common.tsx index b40543ddb..0877fb4c6 100644 --- a/app/containers/HeaderButton/Common.tsx +++ b/app/containers/HeaderButton/Common.tsx @@ -29,9 +29,9 @@ export const CloseModal = React.memo( export const CancelModal = React.memo(({ onPress, testID }: Partial) => ( {isIOS ? ( - + ) : ( - + )} )); @@ -39,19 +39,19 @@ export const CancelModal = React.memo(({ onPress, testID }: Partial) => ( - + )); export const Download = React.memo(({ onPress, testID, ...props }: Partial) => ( - + )); export const Preferences = React.memo(({ onPress, testID, ...props }: Partial) => ( - + )); diff --git a/app/containers/HeaderButton/HeaderButtonItem.tsx b/app/containers/HeaderButton/HeaderButtonItem.tsx index b350232d0..08f6b5a3a 100644 --- a/app/containers/HeaderButton/HeaderButtonItem.tsx +++ b/app/containers/HeaderButton/HeaderButtonItem.tsx @@ -8,12 +8,12 @@ import { themes } from '../../constants/colors'; import sharedStyles from '../../views/Styles'; interface IHeaderButtonItem { - title: string; - iconName: string; - onPress(): void; - testID: string; - theme: string; - badge(): void; + title?: string; + iconName?: string; + onPress: (arg: T) => void; + testID?: string; + theme?: string; + badge?(): void; } export const BUTTON_HIT_SLOP = { @@ -44,9 +44,9 @@ const Item = ({ title, iconName, onPress, testID, theme, badge }: IHeaderButtonI <> {iconName ? ( - + ) : ( - {title} + {title} )} {badge ? badge() : null} diff --git a/app/containers/List/ListContainer.tsx b/app/containers/List/ListContainer.tsx index 408310d9f..deb9c8a71 100644 --- a/app/containers/List/ListContainer.tsx +++ b/app/containers/List/ListContainer.tsx @@ -11,10 +11,10 @@ const styles = StyleSheet.create({ }); interface IListContainer { - children: JSX.Element; + children: React.ReactNode; + testID?: string; } const ListContainer = React.memo(({ children, ...props }: IListContainer) => ( - // @ts-ignore ( - + {translateTitle ? I18n.t(title) : title} diff --git a/app/containers/List/ListIcon.tsx b/app/containers/List/ListIcon.tsx index 7d569dce7..71e4fbdf2 100644 --- a/app/containers/List/ListIcon.tsx +++ b/app/containers/List/ListIcon.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { StyleSheet, View } from 'react-native'; +import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; import { themes } from '../../constants/colors'; import { CustomIcon } from '../../lib/Icons'; @@ -7,11 +7,11 @@ import { withTheme } from '../../theme'; import { ICON_SIZE } from './constants'; interface IListIcon { - theme: string; + theme?: string; name: string; - color: string; - style: object; - testID: string; + color?: string; + style?: StyleProp; + testID?: string; } const styles = StyleSheet.create({ @@ -23,7 +23,7 @@ const styles = StyleSheet.create({ const ListIcon = React.memo(({ theme, name, color, style, testID }: IListIcon) => ( - + )); diff --git a/app/containers/List/ListInfo.tsx b/app/containers/List/ListInfo.tsx index bc4f02e33..2bfe68e32 100644 --- a/app/containers/List/ListInfo.tsx +++ b/app/containers/List/ListInfo.tsx @@ -20,13 +20,13 @@ const styles = StyleSheet.create({ interface IListHeader { info: string; - theme: string; - translateInfo: boolean; + theme?: string; + translateInfo?: boolean; } const ListInfo = React.memo(({ info, theme, translateInfo = true }: IListHeader) => ( - {translateInfo ? I18n.t(info) : info} + {translateInfo ? I18n.t(info) : info} )); diff --git a/app/containers/List/ListItem.tsx b/app/containers/List/ListItem.tsx index a05eaf9ad..87abfd2dd 100644 --- a/app/containers/List/ListItem.tsx +++ b/app/containers/List/ListItem.tsx @@ -56,11 +56,11 @@ const styles = StyleSheet.create({ interface IListItemContent { title?: string; subtitle?: string; - left?: Function; - right?: Function; + left?: () => JSX.Element | null; + right?: () => JSX.Element | null; disabled?: boolean; testID?: string; - theme: string; + theme?: string; color?: string; translateTitle?: boolean; translateSubtitle?: boolean; @@ -89,15 +89,15 @@ const Content = React.memo( {left ? {left()} : null} - + {translateTitle ? I18n.t(title) : title} {alert ? ( - + ) : null} {subtitle ? ( - + {translateSubtitle ? I18n.t(subtitle) : subtitle} ) : null} @@ -112,38 +112,39 @@ const Content = React.memo( ) ); -interface IListItemButton { +interface IListButtonPress { + onPress?: Function; +} + +interface IListItemButton extends IListButtonPress { title?: string; - onPress: Function; disabled?: boolean; - theme: string; - backgroundColor: string; + theme?: string; + backgroundColor?: string; underlayColor?: string; } -const Button = React.memo(({ onPress, backgroundColor, underlayColor, ...props }: IListItemButton) => ( +const Button = React.memo(({ onPress, backgroundColor, underlayColor, ...props }: IListItemButton) => ( onPress(props.title)} - style={{ backgroundColor: backgroundColor || themes[props.theme].backgroundColor }} + onPress={() => onPress!(props.title)} + style={{ backgroundColor: backgroundColor || themes[props.theme!].backgroundColor }} underlayColor={underlayColor} enabled={!props.disabled} - theme={props.theme}> + theme={props.theme!}> )); -interface IListItem { - onPress: Function; - theme: string; - backgroundColor: string; +interface IListItem extends IListItemContent, IListButtonPress { + backgroundColor?: string; } -const ListItem = React.memo(({ ...props }: IListItem) => { +const ListItem = React.memo(({ ...props }: IListItem) => { if (props.onPress) { return