diff --git a/.eslintrc.js b/.eslintrc.js index 085f3a89..952621fb 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/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 62fa05a8..f85a69dd 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -1419,6 +1419,7 @@ Array [ @@ -294,6 +310,8 @@ dependencies { implementation "com.tencent:mmkv-static:1.2.1" implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation "com.squareup.okhttp3:okhttp-urlconnection:4.9.0" + androidTestImplementation('com.wix:detox:+') { transitive = true } + androidTestImplementation 'junit:junit:4.12' } // Run this once to be able to run the application with BUCK diff --git a/android/app/src/androidTest/java/chat/rocket/reactnative/DetoxTest.java b/android/app/src/androidTest/java/chat/rocket/reactnative/DetoxTest.java new file mode 100644 index 00000000..1ab897cd --- /dev/null +++ b/android/app/src/androidTest/java/chat/rocket/reactnative/DetoxTest.java @@ -0,0 +1,32 @@ +// Replace "com.example" here and below with your app's package name from the top of MainActivity.java +package chat.rocket.reactnative; + +import com.wix.detox.Detox; +import com.wix.detox.config.DetoxConfig; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.rule.ActivityTestRule; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class DetoxTest { + @Rule + // Replace 'MainActivity' with the value of android:name entry in + // in AndroidManifest.xml + public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false); + + @Test + public void runDetoxTests() { + DetoxConfig detoxConfig = new DetoxConfig(); + detoxConfig.idlePolicyConfig.masterTimeoutSec = 90; + detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60; + detoxConfig.rnContextLoadTimeoutSec = (chat.rocket.reactnative.BuildConfig.DEBUG ? 180 : 60); + + Detox.runTests(mActivityRule, detoxConfig); + } +} diff --git a/android/app/src/e2e/res/xml/network_security_config.xml b/android/app/src/e2e/res/xml/network_security_config.xml new file mode 100644 index 00000000..95aaf3c1 --- /dev/null +++ b/android/app/src/e2e/res/xml/network_security_config.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file 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 6a690a18..aad80785 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/android/build.gradle b/android/build.gradle index 31650a07..626e7564 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -53,6 +53,10 @@ allprojects { url("$rootDir/../node_modules/jsc-android/dist") } + maven { + url "$rootDir/../node_modules/detox/Detox-android" + } + maven { url jitsi_url } diff --git a/app/AppContainer.tsx b/app/AppContainer.tsx index f7f08bb2..b73aaf8e 100644 --- a/app/AppContainer.tsx +++ b/app/AppContainer.tsx @@ -3,6 +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 Navigation from './lib/Navigation'; import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation'; import { ROOT_INSIDE, ROOT_LOADING, ROOT_OUTSIDE, ROOT_SET_USERNAME } from './actions/app'; @@ -17,7 +18,7 @@ import { ThemeContext } from './theme'; import { setCurrentScreen } from './utils/log'; // SetUsernameStack -const SetUsername = createStackNavigator(); +const SetUsername = createStackNavigator(); const SetUsernameStack = () => ( @@ -25,7 +26,7 @@ const SetUsernameStack = () => ( ); // App -const Stack = createStackNavigator(); +const Stack = createStackNavigator(); const App = React.memo(({ root, isMasterDetail }: { root: string; isMasterDetail: boolean }) => { if (!root) { return null; 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 852ce83e..ad2d1718 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 fc359602..00000000 --- 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 00000000..737ae86b --- /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 65fbb001..00000000 --- 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 00000000..6924a569 --- /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 d7d7e1d5..00000000 --- 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 00000000..ecb6bd4b --- /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/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index 35e69c70..11414150 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -124,7 +124,11 @@ const ActionSheet = React.memo( const renderFooter = () => data?.hasCancel ? ( - + {I18n.t('Cancel')} ) : null; diff --git a/app/containers/ActionSheet/Button.ts b/app/containers/ActionSheet/Button.ts index 5deb0f69..186cc685 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/ActionSheet/Provider.tsx b/app/containers/ActionSheet/Provider.tsx index 0e58ae57..8e786b05 100644 --- a/app/containers/ActionSheet/Provider.tsx +++ b/app/containers/ActionSheet/Provider.tsx @@ -17,7 +17,7 @@ export const useActionSheet = () => useContext(context); const { Provider, Consumer } = context; -export const withActionSheet = (Component: React.ComponentType) => +export const withActionSheet = (Component: any): any => forwardRef((props: any, ref: ForwardedRef) => ( {(contexts: any) => } )); diff --git a/app/containers/Avatar/Avatar.tsx b/app/containers/Avatar/Avatar.tsx index 286bcc06..0ad2634f 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 9c95db83..2ce06647 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 ed7fd3b9..78152e52 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/Button/index.tsx b/app/containers/Button/index.tsx index 9e475a67..8c99dcce 100644 --- a/app/containers/Button/index.tsx +++ b/app/containers/Button/index.tsx @@ -70,6 +70,7 @@ export default class Button extends React.PureComponent, a disabled && styles.disabled, style ]} + accessibilityLabel={title} {...otherProps}> {loading ? ( diff --git a/app/containers/EmojiPicker/index.tsx b/app/containers/EmojiPicker/index.tsx index 64f5dbfe..12217cf9 100644 --- a/app/containers/EmojiPicker/index.tsx +++ b/app/containers/EmojiPicker/index.tsx @@ -31,7 +31,7 @@ interface IEmojiPickerProps { customEmojis?: any; style: object; theme?: string; - onEmojiSelected?: Function; + onEmojiSelected?: ((emoji: any) => void) | ((keyboardId: any, params?: any) => void); tabEmojiStyle?: object; } @@ -201,4 +201,5 @@ const mapStateToProps = (state: any) => ({ customEmojis: state.customEmojis }); -export default connect(mapStateToProps)(withTheme(EmojiPicker)); +// TODO - remove this as any, at the new PR to fix the HOC erros +export default connect(mapStateToProps)(withTheme(EmojiPicker)) as any; diff --git a/app/containers/HeaderButton/HeaderButtonContainer.tsx b/app/containers/HeaderButton/HeaderButtonContainer.tsx index 2d4c45b6..f757d43d 100644 --- a/app/containers/HeaderButton/HeaderButtonContainer.tsx +++ b/app/containers/HeaderButton/HeaderButtonContainer.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; interface IHeaderButtonContainer { - children: JSX.Element; + children: React.ReactNode; left?: boolean; } diff --git a/app/containers/LoginServices.tsx b/app/containers/LoginServices.tsx index bf175dd7..aab5c889 100644 --- a/app/containers/LoginServices.tsx +++ b/app/containers/LoginServices.tsx @@ -423,4 +423,4 @@ const mapStateToProps = (state: any) => ({ services: state.login.services }); -export default connect(mapStateToProps)(withTheme(LoginServices)); +export default connect(mapStateToProps)(withTheme(LoginServices)) as any; diff --git a/app/containers/MessageActions/index.tsx b/app/containers/MessageActions/index.tsx index eb9be967..4d32abf3 100644 --- a/app/containers/MessageActions/index.tsx +++ b/app/containers/MessageActions/index.tsx @@ -305,8 +305,6 @@ const MessageActions = React.memo( }; const handleDelete = (message: any) => { - // TODO - migrate this function for ts when fix the lint erros - // @ts-ignore showConfirmationAlert({ message: I18n.t('You_will_not_be_able_to_recover_this_message'), confirmationText: I18n.t('Delete'), diff --git a/app/containers/MessageBox/EmojiKeyboard.tsx b/app/containers/MessageBox/EmojiKeyboard.tsx index bbb0e20a..91acc45d 100644 --- a/app/containers/MessageBox/EmojiKeyboard.tsx +++ b/app/containers/MessageBox/EmojiKeyboard.tsx @@ -13,7 +13,7 @@ interface IMessageBoxEmojiKeyboard { } export default class EmojiKeyboard extends React.PureComponent { - private readonly baseUrl: any; + private readonly baseUrl: string; constructor(props: IMessageBoxEmojiKeyboard) { super(props); diff --git a/app/containers/MessageBox/RecordAudio.tsx b/app/containers/MessageBox/RecordAudio.tsx index fa6c509e..e219e642 100644 --- a/app/containers/MessageBox/RecordAudio.tsx +++ b/app/containers/MessageBox/RecordAudio.tsx @@ -13,6 +13,7 @@ import { events, logEvent } from '../../utils/log'; interface IMessageBoxRecordAudioProps { theme: string; + permissionToUpload: boolean; recordingCallback: Function; onFinish: Function; } @@ -192,9 +193,11 @@ export default class RecordAudio extends React.PureComponent { @@ -179,41 +182,13 @@ class MessageBox extends Component { showCommandPreview: false, command: {}, tshow: false, - mentionLoading: false + mentionLoading: false, + permissionToUpload: true }; this.text = ''; this.selection = { start: 0, end: 0 }; this.focused = false; - // MessageBox Actions - this.options = [ - { - title: I18n.t('Take_a_photo'), - icon: 'camera-photo', - onPress: this.takePhoto - }, - { - title: I18n.t('Take_a_video'), - icon: 'camera', - onPress: this.takeVideo - }, - { - title: I18n.t('Choose_from_library'), - icon: 'image', - onPress: this.chooseFromLibrary - }, - { - title: I18n.t('Choose_file'), - icon: 'attach', - onPress: this.chooseFile - }, - { - title: I18n.t('Create_Discussion'), - icon: 'discussions', - onPress: this.createDiscussion - } - ]; - const libPickerLabels = { cropperChooseText: I18n.t('Choose'), cropperCancelText: I18n.t('Cancel'), @@ -277,6 +252,8 @@ class MessageBox extends Component { this.onChangeText(usedCannedResponse); } + this.setOptions(); + this.unsubscribeFocus = navigation.addListener('focus', () => { // didFocus // We should wait pushed views be dismissed @@ -321,10 +298,20 @@ class MessageBox extends Component { } } - shouldComponentUpdate(nextProps: any, nextState: any) { - const { showEmojiKeyboard, showSend, recording, mentions, commandPreview, tshow, mentionLoading, trackingType } = this.state; + shouldComponentUpdate(nextProps: IMessageBoxProps, nextState: IMessageBoxState) { + const { + showEmojiKeyboard, + showSend, + recording, + mentions, + commandPreview, + tshow, + mentionLoading, + trackingType, + permissionToUpload + } = this.state; - const { roomType, replying, editing, isFocused, message, theme, usedCannedResponse } = this.props; + const { roomType, replying, editing, isFocused, message, theme, usedCannedResponse, uploadFilePermission } = this.props; if (nextProps.theme !== theme) { return true; } @@ -358,6 +345,9 @@ class MessageBox extends Component { if (nextState.tshow !== tshow) { return true; } + if (nextState.permissionToUpload !== permissionToUpload) { + return true; + } if (!dequal(nextState.mentions, mentions)) { return true; } @@ -367,12 +357,22 @@ class MessageBox extends Component { if (!dequal(nextProps.message?.id, message?.id)) { return true; } + if (!dequal(nextProps.uploadFilePermission, uploadFilePermission)) { + return true; + } if (nextProps.usedCannedResponse !== usedCannedResponse) { return true; } return false; } + componentDidUpdate(prevProps: IMessageBoxProps) { + const { uploadFilePermission } = this.props; + if (!dequal(prevProps.uploadFilePermission, uploadFilePermission)) { + this.setOptions(); + } + } + componentWillUnmount() { console.countReset(`${this.constructor.name}.render calls`); if (this.onChangeText && this.onChangeText.stop) { @@ -404,6 +404,19 @@ class MessageBox extends Component { } } + setOptions = async () => { + const { uploadFilePermission, rid } = this.props; + + // Servers older than 4.2 + if (!uploadFilePermission) { + this.setState({ permissionToUpload: true }); + return; + } + + const permissionToUpload = await RocketChat.hasPermission([uploadFilePermission], rid); + this.setState({ permissionToUpload: permissionToUpload[0] }); + }; + onChangeText: any = (text: string): void => { const isTextEmpty = text.length === 0; this.setShowSend(!isTextEmpty); @@ -666,8 +679,9 @@ class MessageBox extends Component { }; canUploadFile = (file: any) => { + const { permissionToUpload } = this.state; const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props; - const result = canUploadFile(file, FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize); + const result = canUploadFile(file, FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize, permissionToUpload); if (result.success) { return true; } @@ -766,8 +780,41 @@ class MessageBox extends Component { showMessageBoxActions = () => { logEvent(events.ROOM_SHOW_BOX_ACTIONS); + const { permissionToUpload } = this.state; const { showActionSheet } = this.props; - showActionSheet({ options: this.options }); + + const options = []; + if (permissionToUpload) { + options.push( + { + title: I18n.t('Take_a_photo'), + icon: 'camera-photo', + onPress: this.takePhoto + }, + { + title: I18n.t('Take_a_video'), + icon: 'camera', + onPress: this.takeVideo + }, + { + title: I18n.t('Choose_from_library'), + icon: 'image', + onPress: this.chooseFromLibrary + }, + { + title: I18n.t('Choose_file'), + icon: 'attach', + onPress: this.chooseFile + } + ); + } + + options.push({ + title: I18n.t('Create_Discussion'), + icon: 'discussions', + onPress: this.createDiscussion + }); + showActionSheet({ options }); }; editCancel = () => { @@ -968,8 +1015,17 @@ class MessageBox extends Component { }; renderContent = () => { - const { recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview, mentionLoading } = - this.state; + const { + recording, + showEmojiKeyboard, + showSend, + mentions, + trackingType, + commandPreview, + showCommandPreview, + mentionLoading, + permissionToUpload + } = this.state; const { editing, message, @@ -995,7 +1051,12 @@ class MessageBox extends Component { const recordAudio = showSend || !Message_AudioRecorderEnabled ? null : ( - + ); const commandsPreviewAndMentions = !recording ? ( @@ -1117,11 +1178,12 @@ const mapStateToProps = (state: any) => ({ user: getUserSelector(state), FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize, - Message_AudioRecorderEnabled: state.settings.Message_AudioRecorderEnabled + Message_AudioRecorderEnabled: state.settings.Message_AudioRecorderEnabled, + uploadFilePermission: state.permissions['mobile-upload-file'] }); const dispatchToProps = { typing: (rid: any, status: any) => userTypingAction(rid, status) }; // @ts-ignore -export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withActionSheet(MessageBox)); +export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withActionSheet(MessageBox)) as any; diff --git a/app/containers/Passcode/Base/Button.tsx b/app/containers/Passcode/Base/Button.tsx index f7e6c1a9..50a1cf41 100644 --- a/app/containers/Passcode/Base/Button.tsx +++ b/app/containers/Passcode/Base/Button.tsx @@ -7,28 +7,28 @@ import Touch from '../../../utils/touch'; import { CustomIcon } from '../../../lib/Icons'; interface IPasscodeButton { - text: string; - icon: string; + text?: string; + icon?: string; theme: string; - disabled: boolean; - onPress: Function; + disabled?: boolean; + onPress?: Function; } -const Button = React.memo(({ text, disabled, theme, onPress, icon }: Partial) => { - const press = () => onPress && onPress(text!); +const Button = React.memo(({ text, disabled, theme, onPress, icon }: IPasscodeButton) => { + const press = () => onPress && onPress(text); return ( {icon ? ( - + ) : ( - {text} + {text} )} ); diff --git a/app/containers/Passcode/Base/index.tsx b/app/containers/Passcode/Base/index.tsx index dd1d90e8..c6591770 100644 --- a/app/containers/Passcode/Base/index.tsx +++ b/app/containers/Passcode/Base/index.tsx @@ -20,7 +20,7 @@ interface IPasscodeBase { previousPasscode?: string; title: string; subtitle?: string; - showBiometry?: string; + showBiometry?: boolean; onEndProcess: Function; onError?: Function; onBiometryPress?(): void; diff --git a/app/containers/Passcode/PasscodeEnter.tsx b/app/containers/Passcode/PasscodeEnter.tsx index cc284b24..0a9b6b1f 100644 --- a/app/containers/Passcode/PasscodeEnter.tsx +++ b/app/containers/Passcode/PasscodeEnter.tsx @@ -15,7 +15,7 @@ import I18n from '../../i18n'; interface IPasscodePasscodeEnter { theme: string; - hasBiometry: string; + hasBiometry: boolean; finishProcess: Function; } diff --git a/app/containers/SearchBox.tsx b/app/containers/SearchBox.tsx index 4a08c91c..6668e0f7 100644 --- a/app/containers/SearchBox.tsx +++ b/app/containers/SearchBox.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; +import { StyleSheet, Text, TextInputProps, View } from 'react-native'; import Touchable from 'react-native-platform-touchable'; import TextInput from '../presentation/TextInput'; @@ -45,7 +45,7 @@ const styles = StyleSheet.create({ }); interface ISearchBox { - onChangeText: () => void; + onChangeText: TextInputProps['onChangeText']; onSubmitEditing: () => void; hasCancel: boolean; onCancelPress: Function; diff --git a/app/containers/Status/Status.tsx b/app/containers/Status/Status.tsx index e62bc806..dd780bbd 100644 --- a/app/containers/Status/Status.tsx +++ b/app/containers/Status/Status.tsx @@ -8,6 +8,7 @@ interface IStatus { status: string; size: number; style?: StyleProp; + testID?: string; } const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: IStatus) => { diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index b9aaf962..9d4d005e 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -43,7 +43,11 @@ const Content = React.memo( content = {I18n.t('Sent_an_attachment')}; } else if (props.isEncrypted) { content = ( - {I18n.t('Encrypted_message')} + + {I18n.t('Encrypted_message')} + ); } else { const { baseUrl, user, onLinkPress } = useContext(MessageContext); diff --git a/app/containers/message/Reply.tsx b/app/containers/message/Reply.tsx index fbc8984f..8d812605 100644 --- a/app/containers/message/Reply.tsx +++ b/app/containers/message/Reply.tsx @@ -13,6 +13,7 @@ import { themes } from '../../constants/colors'; import MessageContext from './Context'; import { fileDownloadAndPreview } from '../../utils/fileDownload'; import { formatAttachmentUrl } from '../../lib/utils'; +import { IAttachment } from '../../definitions/IAttachment'; import RCActivityIndicator from '../ActivityIndicator'; const styles = StyleSheet.create({ @@ -90,43 +91,26 @@ const styles = StyleSheet.create({ } }); -interface IMessageReplyAttachment { - author_name: string; - message_link: string; - ts: string; - text: string; - title: string; - short: boolean; - value: string; - title_link: string; - author_link: string; - type: string; - color: string; - description: string; - fields: IMessageReplyAttachment[]; - thumb_url: string; -} - interface IMessageTitle { - attachment: Partial; + attachment: IAttachment; timeFormat: string; theme: string; } interface IMessageDescription { - attachment: Partial; + attachment: IAttachment; getCustomEmoji: Function; theme: string; } interface IMessageFields { - attachment: Partial; + attachment: IAttachment; theme: string; getCustomEmoji: Function; } interface IMessageReply { - attachment: IMessageReplyAttachment; + attachment: IAttachment; timeFormat: string; index: number; theme: string; @@ -198,7 +182,7 @@ const Fields = React.memo( {field.title} {/* @ts-ignore*/} void; + +export interface ITranslations { + _id: string; + language: string; + value: string; +} + +export interface ILastMessage { + _id: string; + rid: string; + tshow: boolean; + tmid: string; + msg: string; + ts: Date; + u: IUserMessage; + _updatedAt: Date; + urls: string[]; + mentions: IUserMention[]; + channels: IUserChannel[]; + md: MarkdownAST; + attachments: IAttachment[]; + reactions: IReaction[]; + unread: boolean; + status: boolean; +} + +export interface IMessage { + msg?: string; + t?: SubscriptionType; + ts: Date; + u: IUserMessage; + alias: string; + parseUrls: boolean; + groupable?: boolean; + avatar?: string; + emoji?: string; + attachments?: IAttachment[]; + urls?: string[]; + _updatedAt: Date; + status?: number; + pinned?: boolean; + starred?: boolean; + editedBy?: IEditedBy; + reactions?: IReaction[]; + role?: string; + drid?: string; + dcount?: number; + dlm?: Date; + tmid?: string; + tcount?: number; + tlm?: Date; + replies?: string[]; + mentions?: IUserMention[]; + channels?: IUserChannel[]; + unread?: boolean; + autoTranslate?: boolean; + translations?: ITranslations[]; + tmsg?: string; + blocks?: any; + e2e?: string; + tshow?: boolean; + md?: MarkdownAST; + subscription: { id: string }; +} + +export type TMessageModel = IMessage & Model; diff --git a/app/definitions/INotification.ts b/app/definitions/INotification.ts new file mode 100644 index 00000000..77467d9d --- /dev/null +++ b/app/definitions/INotification.ts @@ -0,0 +1,12 @@ +export interface INotification { + message: string; + style: string; + ejson: string; + collapse_key: string; + notId: string; + msgcnt: string; + title: string; + from: string; + image: string; + soundname: string; +} diff --git a/app/definitions/IPermission.ts b/app/definitions/IPermission.ts new file mode 100644 index 00000000..0ccc1346 --- /dev/null +++ b/app/definitions/IPermission.ts @@ -0,0 +1,9 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface IPermission { + id: string; + roles: string[]; + _updatedAt: Date; +} + +export type TPermissionModel = IPermission & Model; diff --git a/app/definitions/IReaction.ts b/app/definitions/IReaction.ts new file mode 100644 index 00000000..a28f5d06 --- /dev/null +++ b/app/definitions/IReaction.ts @@ -0,0 +1,5 @@ +export interface IReaction { + _id: string; + emoji: string; + usernames: string[]; +} diff --git a/app/definitions/IRole.ts b/app/definitions/IRole.ts new file mode 100644 index 00000000..1fec4215 --- /dev/null +++ b/app/definitions/IRole.ts @@ -0,0 +1,8 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface IRole { + id: string; + description?: string; +} + +export type TRoleModel = IRole & Model; diff --git a/app/definitions/IRoom.ts b/app/definitions/IRoom.ts new file mode 100644 index 00000000..d55cf34a --- /dev/null +++ b/app/definitions/IRoom.ts @@ -0,0 +1,20 @@ +import Model from '@nozbe/watermelondb/Model'; + +import { IServedBy } from './IServedBy'; + +export interface IRoom { + id: string; + customFields: string[]; + broadcast: boolean; + encrypted: boolean; + ro: boolean; + v?: string[]; + servedBy?: IServedBy; + departmentId?: string; + livechatData?: any; + tags?: string[]; + e2eKeyId?: string; + avatarETag?: string; +} + +export type TRoomModel = IRoom & Model; diff --git a/app/definitions/IServedBy.ts b/app/definitions/IServedBy.ts new file mode 100644 index 00000000..4bf31aad --- /dev/null +++ b/app/definitions/IServedBy.ts @@ -0,0 +1,5 @@ +export interface IServedBy { + _id: string; + username: string; + ts: Date; +} diff --git a/app/definitions/IServer.ts b/app/definitions/IServer.ts new file mode 100644 index 00000000..0c3bf57d --- /dev/null +++ b/app/definitions/IServer.ts @@ -0,0 +1,20 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface IServer { + name: string; + iconURL: string; + useRealName: boolean; + FileUpload_MediaTypeWhiteList: string; + FileUpload_MaxFileSize: number; + roomsUpdatedAt: Date; + version: string; + lastLocalAuthenticatedSession: Date; + autoLock: boolean; + autoLockTime?: number; + biometry?: boolean; + uniqueID: string; + enterpriseModules: string; + E2E_Enable: boolean; +} + +export type TServerModel = IServer & Model; diff --git a/app/definitions/IServerHistory.ts b/app/definitions/IServerHistory.ts new file mode 100644 index 00000000..296cba4e --- /dev/null +++ b/app/definitions/IServerHistory.ts @@ -0,0 +1,10 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface IServerHistory { + id: string; + url: string; + username: string; + updatedAt: Date; +} + +export type TServerHistory = IServerHistory & Model; diff --git a/app/definitions/ISettings.ts b/app/definitions/ISettings.ts new file mode 100644 index 00000000..1fbb63ac --- /dev/null +++ b/app/definitions/ISettings.ts @@ -0,0 +1,12 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface ISettings { + id: string; + valueAsString?: string; + valueAsBoolean?: boolean; + valueAsNumber?: number; + valueAsArray?: string[]; + _updatedAt?: Date; +} + +export type TSettingsModel = ISettings & Model; diff --git a/app/definitions/ISlashCommand.ts b/app/definitions/ISlashCommand.ts new file mode 100644 index 00000000..a859448d --- /dev/null +++ b/app/definitions/ISlashCommand.ts @@ -0,0 +1,12 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface ISlashCommand { + id: string; + params?: string; + description?: string; + clientOnly?: boolean; + providesPreview?: boolean; + appId?: string; +} + +export type TSlashCommandModel = ISlashCommand & Model; diff --git a/app/definitions/ISubscription.ts b/app/definitions/ISubscription.ts new file mode 100644 index 00000000..1f241599 --- /dev/null +++ b/app/definitions/ISubscription.ts @@ -0,0 +1,91 @@ +import Model from '@nozbe/watermelondb/Model'; +import Relation from '@nozbe/watermelondb/Relation'; + +import { ILastMessage, TMessageModel } from './IMessage'; +import { IServedBy } from './IServedBy'; +import { TThreadModel } from './IThread'; +import { TThreadMessageModel } from './IThreadMessage'; +import { TUploadModel } from './IUpload'; + +export enum SubscriptionType { + GROUP = 'p', + DIRECT = 'd', + CHANNEL = 'c', + OMNICHANNEL = 'l', + THREAD = 'thread' +} + +export interface IVisitor { + _id: string; + username: string; + token: string; + status: string; + lastMessageTs: Date; +} + +export interface ISubscription { + _id: string; // _id belongs watermelonDB + id: string; // id from server + f: boolean; + t: SubscriptionType; + ts: Date; + ls: Date; + name: string; + fname?: string; + rid: string; // the same as id + open: boolean; + alert: boolean; + roles?: string[]; + unread: number; + userMentions: number; + groupMentions: number; + tunread?: string[]; + tunreadUser?: string[]; + tunreadGroup?: string[]; + roomUpdatedAt: Date; + ro: boolean; + lastOpen?: Date; + description?: string; + announcement?: string; + bannerClosed?: boolean; + topic?: string; + blocked?: boolean; + blocker?: boolean; + reactWhenReadOnly?: boolean; + archived: boolean; + joinCodeRequired?: boolean; + muted?: string[]; + ignored?: string[]; + broadcast?: boolean; + prid?: string; + draftMessage?: string; + lastThreadSync?: Date; + jitsiTimeout?: number; + autoTranslate?: boolean; + autoTranslateLanguage: string; + lastMessage?: ILastMessage; + hideUnreadStatus?: boolean; + sysMes?: string[] | boolean; + uids?: string[]; + usernames?: string[]; + visitor?: IVisitor; + departmentId?: string; + servedBy?: IServedBy; + livechatData?: any; + tags?: string[]; + E2EKey?: string; + encrypted?: boolean; + e2eKeyId?: string; + avatarETag?: string; + teamId?: string; + teamMain?: boolean; + search?: boolean; + username?: string; + // https://nozbe.github.io/WatermelonDB/Relation.html#relation-api + messages: Relation; + threads: Relation; + threadMessages: Relation; + uploads: Relation; +} + +export type TSubscriptionModel = ISubscription & Model; diff --git a/app/definition/ITeam.js b/app/definitions/ITeam.ts similarity index 79% rename from app/definition/ITeam.js rename to app/definitions/ITeam.ts index 10919715..8cf8bddc 100644 --- a/app/definition/ITeam.js +++ b/app/definitions/ITeam.ts @@ -1,5 +1,5 @@ // https://github.com/RocketChat/Rocket.Chat/blob/develop/definition/ITeam.ts -export const TEAM_TYPE = { +exports.TEAM_TYPE = { PUBLIC: 0, PRIVATE: 1 }; diff --git a/app/definitions/ITheme.ts b/app/definitions/ITheme.ts new file mode 100644 index 00000000..208a0b2d --- /dev/null +++ b/app/definitions/ITheme.ts @@ -0,0 +1,8 @@ +export type TThemeMode = 'automatic' | 'light' | 'dark'; + +export type TDarkLevel = 'black' | 'dark'; + +export interface IThemePreference { + currentTheme: TThemeMode; + darkLevel: TDarkLevel; +} diff --git a/app/definitions/IThread.ts b/app/definitions/IThread.ts new file mode 100644 index 00000000..ad151283 --- /dev/null +++ b/app/definitions/IThread.ts @@ -0,0 +1,78 @@ +import Model from '@nozbe/watermelondb/Model'; +import { MarkdownAST } from '@rocket.chat/message-parser'; + +import { IAttachment } from './IAttachment'; +import { IEditedBy, IUserChannel, IUserMention, IUserMessage } from './IMessage'; +import { IReaction } from './IReaction'; +import { SubscriptionType } from './ISubscription'; + +export interface IUrl { + title: string; + description: string; + image: string; + url: string; +} + +interface IFileThread { + _id: string; + name: string; + type: string; +} + +export interface IThreadResult { + _id: string; + rid: string; + ts: string; + msg: string; + file?: IFileThread; + files?: IFileThread[]; + groupable?: boolean; + attachments?: IAttachment[]; + md?: MarkdownAST; + u: IUserMessage; + _updatedAt: string; + urls: IUrl[]; + mentions: IUserMention[]; + channels: IUserChannel[]; + replies: string[]; + tcount: number; + tlm: string; +} + +export interface IThread { + id: string; + msg?: string; + t?: SubscriptionType; + rid: string; + _updatedAt: Date; + ts: Date; + u: IUserMessage; + alias?: string; + parseUrls?: boolean; + groupable?: boolean; + avatar?: string; + emoji?: string; + attachments?: IAttachment[]; + urls?: IUrl[]; + status?: number; + pinned?: boolean; + starred?: boolean; + editedBy?: IEditedBy; + reactions?: IReaction[]; + role?: string; + drid?: string; + dcount?: number; + dlm?: number; + tmid?: string; + tcount?: number; + tlm?: Date; + replies?: string[]; + mentions?: IUserMention[]; + channels?: IUserChannel[]; + unread?: boolean; + autoTranslate?: boolean; + translations?: any; + e2e?: string; +} + +export type TThreadModel = IThread & Model; diff --git a/app/definitions/IThreadMessage.ts b/app/definitions/IThreadMessage.ts new file mode 100644 index 00000000..c773e4dc --- /dev/null +++ b/app/definitions/IThreadMessage.ts @@ -0,0 +1,44 @@ +import Model from '@nozbe/watermelondb/Model'; + +import { IAttachment } from './IAttachment'; +import { IEditedBy, ITranslations, IUserChannel, IUserMention, IUserMessage } from './IMessage'; +import { IReaction } from './IReaction'; +import { SubscriptionType } from './ISubscription'; + +export interface IThreadMessage { + msg?: string; + t?: SubscriptionType; + rid: string; + ts: Date; + u: IUserMessage; + alias?: string; + parseUrls?: boolean; + groupable?: boolean; + avatar?: string; + emoji?: string; + attachments?: IAttachment[]; + urls?: string[]; + _updatedAt?: Date; + status?: number; + pinned?: boolean; + starred?: boolean; + editedBy?: IEditedBy; + reactions?: IReaction[]; + role?: string; + drid?: string; + dcount?: number; + dlm?: Date; + tmid?: string; + tcount?: number; + tlm?: Date; + replies?: string[]; + mentions?: IUserMention[]; + channels?: IUserChannel[]; + unread?: boolean; + autoTranslate?: boolean; + translations?: ITranslations[]; + e2e?: string; + subscription?: { id: string }; +} + +export type TThreadMessageModel = IThreadMessage & Model; diff --git a/app/definitions/IUpload.ts b/app/definitions/IUpload.ts new file mode 100644 index 00000000..6ff03c51 --- /dev/null +++ b/app/definitions/IUpload.ts @@ -0,0 +1,16 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface IUpload { + id: string; + path?: string; + name?: string; + description?: string; + size: number; + type?: string; + store?: string; + progress: number; + error: boolean; + subscription: { id: string }; +} + +export type TUploadModel = IUpload & Model; diff --git a/app/definitions/IUrl.ts b/app/definitions/IUrl.ts new file mode 100644 index 00000000..9b72fda2 --- /dev/null +++ b/app/definitions/IUrl.ts @@ -0,0 +1,6 @@ +export interface IUrl { + title: string; + description: string; + image: string; + url: string; +} diff --git a/app/definitions/IUser.ts b/app/definitions/IUser.ts new file mode 100644 index 00000000..012ef808 --- /dev/null +++ b/app/definitions/IUser.ts @@ -0,0 +1,10 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface IUser { + _id: string; + name?: string; + username: string; + avatarETag?: string; +} + +export type TUserModel = IUser & Model; diff --git a/app/definitions/index.ts b/app/definitions/index.ts new file mode 100644 index 00000000..80eeb88c --- /dev/null +++ b/app/definitions/index.ts @@ -0,0 +1,19 @@ +import { RouteProp } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { Dispatch } from 'redux'; + +export * from './IAttachment'; +export * from './IMessage'; +export * from './INotification'; +export * from './IRoom'; +export * from './IServer'; +export * from './ISubscription'; + +export interface IBaseScreen, S extends string> { + navigation: StackNavigationProp; + route: RouteProp; + dispatch: Dispatch; + theme: string; +} + +export * from './redux'; diff --git a/app/definitions/redux/index.ts b/app/definitions/redux/index.ts new file mode 100644 index 00000000..e95763e2 --- /dev/null +++ b/app/definitions/redux/index.ts @@ -0,0 +1,31 @@ +import { TActionSelectedUsers } from '../../actions/selectedUsers'; +import { TActionActiveUsers } from '../../actions/activeUsers'; +// REDUCERS +import { IActiveUsers } from '../../reducers/activeUsers'; +import { ISelectedUsers } from '../../reducers/selectedUsers'; + +export interface IApplicationState { + settings: any; + login: any; + meteor: any; + server: any; + selectedUsers: ISelectedUsers; + createChannel: any; + app: any; + room: any; + rooms: any; + sortPreferences: any; + share: any; + customEmojis: any; + activeUsers: IActiveUsers; + usersTyping: any; + inviteLinks: any; + createDiscussion: any; + inquiry: any; + enterpriseModules: any; + encryption: any; + permissions: any; + roles: any; +} + +export type TApplicationActions = TActionActiveUsers & TActionSelectedUsers; diff --git a/app/dimensions.tsx b/app/dimensions.tsx index dc164362..67600968 100644 --- a/app/dimensions.tsx +++ b/app/dimensions.tsx @@ -22,7 +22,7 @@ export interface IDimensionsContextProps { export const DimensionsContext = React.createContext>(Dimensions.get('window')); -export function withDimensions(Component: any) { +export function withDimensions(Component: any): any { const DimensionsComponent = (props: any) => ( {contexts => } ); diff --git a/app/ee/omnichannel/lib/subscriptions/inquiry.js b/app/ee/omnichannel/lib/subscriptions/inquiry.js index 00d32082..d10d5c89 100644 --- a/app/ee/omnichannel/lib/subscriptions/inquiry.js +++ b/app/ee/omnichannel/lib/subscriptions/inquiry.js @@ -6,7 +6,6 @@ import { inquiryQueueAdd, inquiryQueueRemove, inquiryQueueUpdate, inquiryRequest const removeListener = listener => listener.stop(); let connectedListener; -let disconnectedListener; let queueListener; const streamTopic = 'stream-livechat-inquiry-queue-observer'; @@ -48,10 +47,6 @@ export default function subscribeInquiry() { connectedListener.then(removeListener); connectedListener = false; } - if (disconnectedListener) { - disconnectedListener.then(removeListener); - disconnectedListener = false; - } if (queueListener) { queueListener.then(removeListener); queueListener = false; @@ -59,7 +54,6 @@ export default function subscribeInquiry() { }; connectedListener = RocketChat.onStreamData('connected', handleConnection); - disconnectedListener = RocketChat.onStreamData('close', handleConnection); queueListener = RocketChat.onStreamData(streamTopic, handleQueueMessageReceived); try { diff --git a/app/ee/omnichannel/views/QueueListView.js b/app/ee/omnichannel/views/QueueListView.js index defe9233..5d537cee 100644 --- a/app/ee/omnichannel/views/QueueListView.js +++ b/app/ee/omnichannel/views/QueueListView.js @@ -161,4 +161,5 @@ const mapStateToProps = state => ({ showAvatar: state.sortPreferences.showAvatar, displayMode: state.sortPreferences.displayMode }); + export default connect(mapStateToProps)(withDimensions(withTheme(QueueListView))); diff --git a/app/externalModules.d.ts b/app/externalModules.d.ts index f68cb5e3..02c57204 100644 --- a/app/externalModules.d.ts +++ b/app/externalModules.d.ts @@ -13,3 +13,4 @@ declare module 'react-native-mime-types'; declare module 'react-native-restart'; declare module 'react-native-prompt-android'; declare module 'react-native-jitsi-meet'; +declare module 'rn-root-view'; diff --git a/app/i18n/locales/ar.json b/app/i18n/locales/ar.json index 896d4d0d..0f6eebaa 100644 --- a/app/i18n/locales/ar.json +++ b/app/i18n/locales/ar.json @@ -328,7 +328,6 @@ "N_users": "{{n}} مستخدمين", "N_channels": "{{n}} القنوات", "Name": "اسم", - "Navigation_history": "تاريخ التصفح", "Never": "أبداً", "New_Message": "رسالة جديدة", "New_Password": "كلمة مرور جديدة", diff --git a/app/i18n/locales/de.json b/app/i18n/locales/de.json index 5a6fe2bd..8bb4dee5 100644 --- a/app/i18n/locales/de.json +++ b/app/i18n/locales/de.json @@ -330,7 +330,6 @@ "N_users": "{{n}} Benutzer", "N_channels": "{{n}} Kanäle", "Name": "Name", - "Navigation_history": "Navigations-Verlauf", "Never": "Niemals", "New_Message": "Neue Nachricht", "New_Password": "Neues Kennwort", diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index e4ae9fe1..754a6304 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -21,6 +21,7 @@ "error-save-video": "Error while saving video", "error-field-unavailable": "{{field}} is already in use :(", "error-file-too-large": "File is too large", + "error-not-permission-to-upload-file": "You don't have permission to upload files", "error-importer-not-defined": "The importer was not defined correctly, it is missing the Import class.", "error-input-is-not-a-valid-field": "{{input}} is not a valid {{field}}", "error-invalid-actionlink": "Invalid action link", @@ -330,7 +331,6 @@ "N_users": "{{n}} users", "N_channels": "{{n}} channels", "Name": "Name", - "Navigation_history": "Navigation history", "Never": "Never", "New_Message": "New Message", "New_Password": "New Password", @@ -787,4 +787,4 @@ "Unsupported_format": "Unsupported format", "Downloaded_file": "Downloaded file", "Error_Download_file": "Error while downloading file" -} +} \ No newline at end of file diff --git a/app/i18n/locales/fr.json b/app/i18n/locales/fr.json index 811486b0..81e8e21b 100644 --- a/app/i18n/locales/fr.json +++ b/app/i18n/locales/fr.json @@ -330,7 +330,6 @@ "N_users": "{{n}} utilisateurs", "N_channels": "{{n}} canaux", "Name": "Nom", - "Navigation_history": "Historique de navigation", "Never": "Jamais", "New_Message": "Nouveau message", "New_Password": "Nouveau mot de passe", @@ -782,5 +781,8 @@ "No_canned_responses": "Pas de réponses standardisées", "Send_email_confirmation": "Envoyer un e-mail de confirmation", "sending_email_confirmation": "envoi d'e-mail de confirmation", - "Enable_Message_Parser": "Activer le parseur de messages" + "Enable_Message_Parser": "Activer le parseur de messages", + "Unsupported_format": "Format non supporté", + "Downloaded_file": "Fichier téléchargé", + "Error_Download_file": "Erreur lors du téléchargement du fichier" } \ No newline at end of file diff --git a/app/i18n/locales/it.json b/app/i18n/locales/it.json index 7b1f1f66..5e9e4f9f 100644 --- a/app/i18n/locales/it.json +++ b/app/i18n/locales/it.json @@ -322,7 +322,6 @@ "N_people_reacted": "{{n}} persone hanno reagito", "N_users": "{{n}} utenti", "Name": "Nome", - "Navigation_history": "Cronologia di navigazione", "Never": "Mai", "New_Message": "Nuovo messaggio", "New_Password": "Nuova password", diff --git a/app/i18n/locales/nl.json b/app/i18n/locales/nl.json index df4e3aaf..aa63fdb7 100644 --- a/app/i18n/locales/nl.json +++ b/app/i18n/locales/nl.json @@ -330,7 +330,6 @@ "N_users": "{{n}} gebruikers", "N_channels": "{{n}} kanalen", "Name": "Naam", - "Navigation_history": "Navigatie geschiedenis", "Never": "Nooit", "New_Message": "Nieuw bericht", "New_Password": "Nieuw wachtwoord", @@ -782,5 +781,8 @@ "No_canned_responses": "Geen standaardantwoorden", "Send_email_confirmation": "Stuur e-mailbevestiging", "sending_email_confirmation": "e-mailbevestiging aan het verzenden", - "Enable_Message_Parser": "Berichtparser inschakelen" + "Enable_Message_Parser": "Berichtparser inschakelen", + "Unsupported_format": "Niet ondersteund formaat", + "Downloaded_file": "Gedownload bestand", + "Error_Download_file": "Fout tijdens het downloaden van bestand" } \ No newline at end of file diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 24905e9f..8aa2f5ae 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -309,7 +309,6 @@ "N_users": "{{n}} usuários", "N_channels": "{{n}} canais", "Name": "Nome", - "Navigation_history": "Histórico de navegação", "Never": "Nunca", "New_Message": "Nova Mensagem", "New_Password": "Nova Senha", @@ -737,4 +736,4 @@ "Unsupported_format": "Formato não suportado", "Downloaded_file": "Arquivo baixado", "Error_Download_file": "Erro ao baixar o arquivo" -} +} \ No newline at end of file diff --git a/app/i18n/locales/pt-PT.json b/app/i18n/locales/pt-PT.json index f3d23c51..f2cd45e7 100644 --- a/app/i18n/locales/pt-PT.json +++ b/app/i18n/locales/pt-PT.json @@ -329,7 +329,6 @@ "N_users": "{{n}} utilizadores", "N_channels": "{{n}} canais", "Name": "Nome", - "Navigation_history": "Histórico de navegação", "Never": "Nunca", "New_Message": "Nova Mensagem", "New_Password": "Nova Palavra-passe", diff --git a/app/i18n/locales/ru.json b/app/i18n/locales/ru.json index 071e8f3e..bdf2c161 100644 --- a/app/i18n/locales/ru.json +++ b/app/i18n/locales/ru.json @@ -330,7 +330,6 @@ "N_users": "{{n}} пользователи", "N_channels": "{{n}} каналов", "Name": "Имя", - "Navigation_history": "История навигации", "Never": "Никогда", "New_Message": "Новое сообщение", "New_Password": "Новый пароль", @@ -782,5 +781,8 @@ "No_canned_responses": "Нет заготовленных ответов", "Send_email_confirmation": "Отправить электронное письмо с подтверждением", "sending_email_confirmation": "отправка подтверждения по электронной почте", - "Enable_Message_Parser": "Включить парсер сообщений" + "Enable_Message_Parser": "Включить парсер сообщений", + "Unsupported_format": "Неподдерживаемый формат", + "Downloaded_file": "Скачанный файл", + "Error_Download_file": "Ошибка при скачивании файла" } \ No newline at end of file diff --git a/app/i18n/locales/tr.json b/app/i18n/locales/tr.json index aeb4caba..cc565192 100644 --- a/app/i18n/locales/tr.json +++ b/app/i18n/locales/tr.json @@ -323,7 +323,6 @@ "N_people_reacted": "{{n}} kişi tepki verdi", "N_users": "{{n}} kullanıcı", "Name": "İsim", - "Navigation_history": "Gezinti geçmişi", "Never": "Asla", "New_Message": "Yeni İleti", "New_Password": "Yeni Şifre", diff --git a/app/i18n/locales/zh-CN.json b/app/i18n/locales/zh-CN.json index e0136890..24e166b9 100644 --- a/app/i18n/locales/zh-CN.json +++ b/app/i18n/locales/zh-CN.json @@ -320,7 +320,6 @@ "N_people_reacted": "{{n}} 人回复", "N_users": "{{n}} 位用户", "Name": "名称", - "Navigation_history": "浏览历史记录", "Never": "从不", "New_Message": "新信息", "New_Password": "新密码", diff --git a/app/i18n/locales/zh-TW.json b/app/i18n/locales/zh-TW.json index 85d2690c..258f8fdf 100644 --- a/app/i18n/locales/zh-TW.json +++ b/app/i18n/locales/zh-TW.json @@ -321,7 +321,6 @@ "N_people_reacted": "{{n}} 人回复", "N_users": "{{n}} 位使用者", "Name": "名稱", - "Navigation_history": "瀏覽歷史記錄", "Never": "從不", "New_Message": "新訊息", "New_Password": "新密碼", diff --git a/app/index.tsx b/app/index.tsx index e6457e23..cc0a06a2 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -30,6 +30,8 @@ import InAppNotification from './containers/InAppNotification'; import { ActionSheetProvider } from './containers/ActionSheet'; import debounce from './utils/debounce'; import { isFDroidBuild } from './constants/environment'; +import { IThemePreference } from './definitions/ITheme'; +import { ICommand } from './definitions/ICommand'; RNScreens.enableScreens(); @@ -42,10 +44,7 @@ interface IDimensions { interface IState { theme: string; - themePreferences: { - currentTheme: 'automatic' | 'light'; - darkLevel: string; - }; + themePreferences: IThemePreference; width: number; height: number; scale: number; @@ -175,7 +174,7 @@ export default class Root extends React.Component<{}, IState> { setTheme = (newTheme = {}) => { // change theme state this.setState( - prevState => newThemeState(prevState, newTheme), + prevState => newThemeState(prevState, newTheme as IThemePreference), () => { const { themePreferences } = this.state; // subscribe to Appearance changes @@ -191,7 +190,7 @@ export default class Root extends React.Component<{}, IState> { initTablet = () => { const { width } = this.state; this.setMasterDetail(width); - this.onKeyCommands = KeyCommandsEmitter.addListener('onKeyCommand', (command: unknown) => { + this.onKeyCommands = KeyCommandsEmitter.addListener('onKeyCommand', (command: ICommand) => { EventEmitter.emit(KEY_COMMAND, { event: command }); }); }; diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index 589aa6bd..b680a919 100644 --- a/app/lib/methods/getPermissions.js +++ b/app/lib/methods/getPermissions.js @@ -55,7 +55,8 @@ const PERMISSIONS = [ 'convert-team', 'edit-omnichannel-contact', 'edit-livechat-room-customfields', - 'view-canned-responses' + 'view-canned-responses', + 'mobile-upload-file' ]; export async function setPermissions() { diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.js index 4dd84adf..c2fc9fcd 100644 --- a/app/lib/methods/subscriptions/rooms.js +++ b/app/lib/methods/subscriptions/rooms.js @@ -8,7 +8,6 @@ import messagesStatus from '../../../constants/messagesStatus'; import log from '../../../utils/log'; import random from '../../../utils/random'; import store from '../../createStore'; -import { roomsRequest } from '../../../actions/rooms'; import { handlePayloadUserInteraction } from '../actions'; import buildMessage from '../helpers/buildMessage'; import RocketChat from '../../rocketchat'; @@ -21,8 +20,6 @@ import { E2E_MESSAGE_TYPE } from '../../encryption/constants'; const removeListener = listener => listener.stop(); -let connectedListener; -let disconnectedListener; let streamListener; let subServer; let queue = {}; @@ -255,10 +252,6 @@ const debouncedUpdate = subscription => { }; export default function subscribeRooms() { - const handleConnection = () => { - store.dispatch(roomsRequest()); - }; - const handleStreamMessageReceived = protectedFunction(async ddpMessage => { const db = database.active; @@ -388,14 +381,6 @@ export default function subscribeRooms() { }); const stop = () => { - if (connectedListener) { - connectedListener.then(removeListener); - connectedListener = false; - } - if (disconnectedListener) { - disconnectedListener.then(removeListener); - disconnectedListener = false; - } if (streamListener) { streamListener.then(removeListener); streamListener = false; @@ -407,8 +392,6 @@ export default function subscribeRooms() { } }; - connectedListener = this.sdk.onStreamData('connected', handleConnection); - // disconnectedListener = this.sdk.onStreamData('close', handleConnection); streamListener = this.sdk.onStreamData('stream-notify-user', handleStreamMessageReceived); try { diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 7fd058d8..1cb627a4 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -24,7 +24,7 @@ import { selectServerFailure } from '../actions/server'; import { useSsl } from '../utils/url'; import EventEmitter from '../utils/events'; import { updatePermission } from '../actions/permissions'; -import { TEAM_TYPE } from '../definition/ITeam'; +import { TEAM_TYPE } from '../definitions/ITeam'; import { updateSettings } from '../actions/settings'; import { compareServerVersion, methods } from './utils'; import reduxStore from './createStore'; @@ -239,37 +239,34 @@ const RocketChat = { this.code = null; } - this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) }); + // The app can't reconnect if reopen interval is 5s while in development + this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server), reopen: __DEV__ ? 20000 : 5000 }); this.getSettings(); - const sdkConnect = () => - this.sdk - .connect() - .then(() => { - const { server: currentServer } = reduxStore.getState().server; - if (user && user.token && server === currentServer) { - reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError)); - } - }) - .catch(err => { - console.log('connect error', err); - - // when `connect` raises an error, we try again in 10 seconds - this.connectTimeout = setTimeout(() => { - if (this.sdk?.client?.host === server) { - sdkConnect(); - } - }, 10000); - }); - - sdkConnect(); + this.sdk + .connect() + .then(() => { + console.log('connected'); + }) + .catch(err => { + console.log('connect error', err); + }); this.connectingListener = this.sdk.onStreamData('connecting', () => { reduxStore.dispatch(connectRequest()); }); this.connectedListener = this.sdk.onStreamData('connected', () => { + const { connected } = reduxStore.getState().meteor; + if (connected) { + return; + } reduxStore.dispatch(connectSuccess()); + const { server: currentServer } = reduxStore.getState().server; + const { user } = reduxStore.getState().login; + if (user?.token && server === currentServer) { + reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError)); + } }); this.closeListener = this.sdk.onStreamData('close', () => { @@ -848,17 +845,21 @@ const RocketChat = { // RC 3.13.0 return this.post('teams.removeRoom', { roomId, teamId }); }, - leaveTeam({ teamName, rooms }) { + leaveTeam({ teamId, rooms }) { // RC 3.13.0 - return this.post('teams.leave', { teamName, rooms }); + return this.post('teams.leave', { + teamId, + // RC 4.2.0 + ...(rooms?.length && { rooms }) + }); }, - removeTeamMember({ teamId, teamName, userId, rooms }) { + removeTeamMember({ teamId, userId, rooms }) { // RC 3.13.0 return this.post('teams.removeMember', { teamId, - teamName, userId, - rooms + // RC 4.2.0 + ...(rooms?.length && { rooms }) }); }, updateTeamRoom({ roomId, isDefault }) { @@ -1151,10 +1152,6 @@ const RocketChat = { // RC 0.36.0 return this.methodCallWrapper('livechat:transfer', transferData); }, - getPagesLivechat(rid, offset) { - // RC 2.3.0 - return this.sdk.get(`livechat/visitors.pagesVisited/${rid}?count=50&offset=${offset}`); - }, getDepartmentInfo(departmentId) { // RC 2.2.0 return this.sdk.get(`livechat/department/${departmentId}?includeAgents=false`); @@ -1533,16 +1530,7 @@ const RocketChat = { return this.sdk.get(`${this.roomTypeToApiType(type)}.files`, { roomId, offset, - sort: { uploadedAt: -1 }, - fields: { - name: 1, - description: 1, - size: 1, - type: 1, - uploadedAt: 1, - url: 1, - userId: 1 - } + sort: { uploadedAt: -1 } }); }, getMessages(roomId, type, query, offset) { diff --git a/app/lib/userPreferences.js b/app/lib/userPreferences.ts similarity index 75% rename from app/lib/userPreferences.js rename to app/lib/userPreferences.ts index 3377856d..6f4818da 100644 --- a/app/lib/userPreferences.js +++ b/app/lib/userPreferences.ts @@ -7,11 +7,12 @@ const MMKV = new MMKVStorage.Loader() .initialize(); class UserPreferences { + private mmkv: MMKVStorage.API; constructor() { this.mmkv = MMKV; } - async getStringAsync(key) { + async getStringAsync(key: string) { try { const value = await this.mmkv.getStringAsync(key); return value; @@ -20,11 +21,11 @@ class UserPreferences { } } - setStringAsync(key, value) { + setStringAsync(key: string, value: string) { return this.mmkv.setStringAsync(key, value); } - async getBoolAsync(key) { + async getBoolAsync(key: string) { try { const value = await this.mmkv.getBoolAsync(key); return value; @@ -33,11 +34,11 @@ class UserPreferences { } } - setBoolAsync(key, value) { + setBoolAsync(key: string, value: boolean) { return this.mmkv.setBoolAsync(key, value); } - async getMapAsync(key) { + async getMapAsync(key: string) { try { const value = await this.mmkv.getMapAsync(key); return value; @@ -46,11 +47,11 @@ class UserPreferences { } } - setMapAsync(key, value) { + setMapAsync(key: string, value: object) { return this.mmkv.setMapAsync(key, value); } - removeItem(key) { + removeItem(key: string) { return this.mmkv.removeItem(key); } } diff --git a/app/navigationTypes.ts b/app/navigationTypes.ts new file mode 100644 index 00000000..cbf17f42 --- /dev/null +++ b/app/navigationTypes.ts @@ -0,0 +1,45 @@ +import { NavigatorScreenParams } from '@react-navigation/core'; + +import { ISubscription } from './definitions/ISubscription'; +import { IServer } from './definitions/IServer'; +import { IAttachment } from './definitions/IAttachment'; +import { MasterDetailInsideStackParamList } from './stacks/MasterDetailStack/types'; +import { OutsideParamList, InsideStackParamList } from './stacks/types'; + +export type SetUsernameStackParamList = { + SetUsernameView: { + title: string; + }; +}; + +export type StackParamList = { + AuthLoading: undefined; + OutsideStack: NavigatorScreenParams; + InsideStack: NavigatorScreenParams; + MasterDetailStack: NavigatorScreenParams; + SetUsernameStack: NavigatorScreenParams; +}; + +export type ShareInsideStackParamList = { + ShareListView: undefined; + ShareView: { + attachments: IAttachment[]; + isShareView?: boolean; + isShareExtension: boolean; + serverInfo: IServer; + text: string; + room: ISubscription; + thread: any; // TODO: Change + }; + SelectServerView: undefined; +}; + +export type ShareOutsideStackParamList = { + WithoutServersView: undefined; +}; + +export type ShareAppStackParamList = { + AuthLoading?: undefined; + OutsideStack?: NavigatorScreenParams; + InsideStack?: NavigatorScreenParams; +}; diff --git a/app/notifications/push/index.js b/app/notifications/push/index.js deleted file mode 100644 index 074e22bf..00000000 --- a/app/notifications/push/index.js +++ /dev/null @@ -1,50 +0,0 @@ -import EJSON from 'ejson'; - -import store from '../../lib/createStore'; -import { deepLinkingOpen } from '../../actions/deepLinking'; -import { isFDroidBuild } from '../../constants/environment'; -import PushNotification from './push'; - -export const onNotification = notification => { - if (notification) { - const data = notification.getData(); - if (data) { - try { - const { rid, name, sender, type, host, messageType, messageId } = EJSON.parse(data.ejson); - - const types = { - c: 'channel', - d: 'direct', - p: 'group', - l: 'channels' - }; - let roomName = type === 'd' ? sender.username : name; - if (type === 'l') { - roomName = sender.name; - } - - const params = { - host, - rid, - messageId, - path: `${types[type]}/${roomName}`, - isCall: messageType === 'jitsi_call_started' - }; - store.dispatch(deepLinkingOpen(params)); - } catch (e) { - console.warn(e); - } - } - } -}; - -export const getDeviceToken = () => PushNotification.getDeviceToken(); -export const setBadgeCount = count => PushNotification.setBadgeCount(count); -export const initializePushNotifications = () => { - if (!isFDroidBuild) { - setBadgeCount(); - return PushNotification.configure({ - onNotification - }); - } -}; diff --git a/app/notifications/push/index.ts b/app/notifications/push/index.ts new file mode 100644 index 00000000..af25c9ee --- /dev/null +++ b/app/notifications/push/index.ts @@ -0,0 +1,57 @@ +import EJSON from 'ejson'; + +import store from '../../lib/createStore'; +import { deepLinkingOpen } from '../../actions/deepLinking'; +import { isFDroidBuild } from '../../constants/environment'; +import PushNotification from './push'; +import { INotification, SubscriptionType } from '../../definitions'; + +interface IEjson { + rid: string; + name: string; + sender: { username: string; name: string }; + type: string; + host: string; + messageType: string; + messageId: string; +} + +export const onNotification = (notification: INotification): void => { + if (notification) { + try { + const { rid, name, sender, type, host, messageType, messageId }: IEjson = EJSON.parse(notification.ejson); + + const types: Record = { + c: 'channel', + d: 'direct', + p: 'group', + l: 'channels' + }; + let roomName = type === SubscriptionType.DIRECT ? sender.username : name; + if (type === SubscriptionType.OMNICHANNEL) { + roomName = sender.name; + } + + const params = { + host, + rid, + messageId, + path: `${types[type]}/${roomName}`, + isCall: messageType === 'jitsi_call_started' + }; + // TODO REDUX MIGRATION TO TS + store.dispatch(deepLinkingOpen(params)); + } catch (e) { + console.warn(e); + } + } +}; + +export const getDeviceToken = (): string => PushNotification.getDeviceToken(); +export const setBadgeCount = (count?: number): void => PushNotification.setBadgeCount(count); +export const initializePushNotifications = (): Promise | undefined => { + if (!isFDroidBuild) { + setBadgeCount(); + return PushNotification.configure(onNotification); + } +}; diff --git a/app/notifications/push/push.android.js b/app/notifications/push/push.android.js deleted file mode 100644 index 51e767ad..00000000 --- a/app/notifications/push/push.android.js +++ /dev/null @@ -1,32 +0,0 @@ -import { NotificationsAndroid, PendingNotifications } from 'react-native-notifications'; - -class PushNotification { - constructor() { - this.onRegister = null; - this.onNotification = null; - this.deviceToken = null; - - NotificationsAndroid.setRegistrationTokenUpdateListener(deviceToken => { - this.deviceToken = deviceToken; - }); - - NotificationsAndroid.setNotificationOpenedListener(notification => { - this.onNotification(notification); - }); - } - - getDeviceToken() { - return this.deviceToken; - } - - setBadgeCount = () => {}; - - configure(params) { - this.onRegister = params.onRegister; - this.onNotification = params.onNotification; - NotificationsAndroid.refreshToken(); - return PendingNotifications.getInitialNotification(); - } -} - -export default new PushNotification(); diff --git a/app/notifications/push/push.ios.js b/app/notifications/push/push.ios.js deleted file mode 100644 index 068be4ee..00000000 --- a/app/notifications/push/push.ios.js +++ /dev/null @@ -1,61 +0,0 @@ -import NotificationsIOS, { NotificationAction, NotificationCategory } from 'react-native-notifications'; - -import reduxStore from '../../lib/createStore'; -import I18n from '../../i18n'; - -const replyAction = new NotificationAction({ - activationMode: 'background', - title: I18n.t('Reply'), - textInput: { - buttonTitle: I18n.t('Reply'), - placeholder: I18n.t('Type_message') - }, - identifier: 'REPLY_ACTION' -}); - -class PushNotification { - constructor() { - this.onRegister = null; - this.onNotification = null; - this.deviceToken = null; - - NotificationsIOS.addEventListener('remoteNotificationsRegistered', deviceToken => { - this.deviceToken = deviceToken; - }); - - NotificationsIOS.addEventListener('notificationOpened', (notification, completion) => { - const { background } = reduxStore.getState().app; - if (background) { - this.onNotification(notification); - } - completion(); - }); - - const actions = []; - actions.push( - new NotificationCategory({ - identifier: 'MESSAGE', - actions: [replyAction] - }) - ); - NotificationsIOS.requestPermissions(actions); - } - - getDeviceToken() { - return this.deviceToken; - } - - setBadgeCount = (count = 0) => { - NotificationsIOS.setBadgesCount(count); - }; - - async configure(params) { - this.onRegister = params.onRegister; - this.onNotification = params.onNotification; - - const initial = await NotificationsIOS.getInitialNotification(); - // NotificationsIOS.consumeBackgroundQueue(); - return Promise.resolve(initial); - } -} -export default new PushNotification(); diff --git a/app/notifications/push/push.ios.ts b/app/notifications/push/push.ios.ts new file mode 100644 index 00000000..92c2d3a6 --- /dev/null +++ b/app/notifications/push/push.ios.ts @@ -0,0 +1,63 @@ +// @ts-ignore +// TODO BUMP LIB VERSION +import NotificationsIOS, { NotificationAction, NotificationCategory, Notification } from 'react-native-notifications'; + +import reduxStore from '../../lib/createStore'; +import I18n from '../../i18n'; +import { INotification } from '../../definitions/INotification'; + +class PushNotification { + onNotification: (notification: Notification) => void; + deviceToken: string; + + constructor() { + this.onNotification = () => {}; + this.deviceToken = ''; + + NotificationsIOS.addEventListener('remoteNotificationsRegistered', (deviceToken: string) => { + this.deviceToken = deviceToken; + }); + + NotificationsIOS.addEventListener('notificationOpened', (notification: Notification, completion: () => void) => { + // TODO REDUX MIGRATION TO TS + const { background } = reduxStore.getState().app; + if (background) { + this.onNotification(notification?.getData()); + } + completion(); + }); + + const actions = [ + new NotificationCategory({ + identifier: 'MESSAGE', + actions: [ + new NotificationAction({ + activationMode: 'background', + title: I18n.t('Reply'), + textInput: { + buttonTitle: I18n.t('Reply'), + placeholder: I18n.t('Type_message') + }, + identifier: 'REPLY_ACTION' + }) + ] + }) + ]; + NotificationsIOS.requestPermissions(actions); + } + + getDeviceToken() { + return this.deviceToken; + } + + setBadgeCount = (count = 0) => { + NotificationsIOS.setBadgesCount(count); + }; + + async configure(onNotification: (notification: INotification) => void) { + this.onNotification = onNotification; + const initial = await NotificationsIOS.getInitialNotification(); + return Promise.resolve(initial); + } +} +export default new PushNotification(); diff --git a/app/notifications/push/push.ts b/app/notifications/push/push.ts new file mode 100644 index 00000000..16aa8cf5 --- /dev/null +++ b/app/notifications/push/push.ts @@ -0,0 +1,36 @@ +// @ts-ignore +// TODO BUMP LIB VERSION +import { NotificationsAndroid, PendingNotifications, Notification } from 'react-native-notifications'; + +import { INotification } from '../../definitions/INotification'; + +class PushNotification { + onNotification: (notification: Notification) => void; + deviceToken: string; + constructor() { + this.onNotification = () => {}; + this.deviceToken = ''; + + NotificationsAndroid.setRegistrationTokenUpdateListener((deviceToken: string) => { + this.deviceToken = deviceToken; + }); + + NotificationsAndroid.setNotificationOpenedListener((notification: Notification) => { + this.onNotification(notification?.getData()); + }); + } + + getDeviceToken() { + return this.deviceToken; + } + + setBadgeCount = (_?: number) => {}; + + configure(onNotification: (notification: INotification) => void) { + this.onNotification = onNotification; + NotificationsAndroid.refreshToken(); + return PendingNotifications.getInitialNotification(); + } +} + +export default new PushNotification(); diff --git a/app/presentation/DirectoryItem/index.tsx b/app/presentation/DirectoryItem/index.tsx index b8d9811a..234c1e31 100644 --- a/app/presentation/DirectoryItem/index.tsx +++ b/app/presentation/DirectoryItem/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Text, View } from 'react-native'; +import { Text, View, ViewStyle } from 'react-native'; import Touch from '../../utils/touch'; import Avatar from '../../containers/Avatar'; @@ -10,7 +10,7 @@ import { themes } from '../../constants/colors'; export { ROW_HEIGHT }; interface IDirectoryItemLabel { - text: string; + text?: string; theme: string; } @@ -21,9 +21,9 @@ interface IDirectoryItem { type: string; onPress(): void; testID: string; - style: any; - rightLabel: string; - rid: string; + style?: ViewStyle; + rightLabel?: string; + rid?: string; theme: string; teamMain?: boolean; } @@ -32,7 +32,7 @@ const DirectoryItemLabel = React.memo(({ text, theme }: IDirectoryItemLabel) => if (!text) { return null; } - return {text}; + return {text}; }); const DirectoryItem = ({ diff --git a/app/presentation/KeyboardView.tsx b/app/presentation/KeyboardView.tsx index 5319df82..aa4f1182 100644 --- a/app/presentation/KeyboardView.tsx +++ b/app/presentation/KeyboardView.tsx @@ -4,7 +4,7 @@ import { KeyboardAwareScrollView, KeyboardAwareScrollViewProps } from '@codler/r import scrollPersistTaps from '../utils/scrollPersistTaps'; interface IKeyboardViewProps extends KeyboardAwareScrollViewProps { - keyboardVerticalOffset: number; + keyboardVerticalOffset?: number; scrollEnabled?: boolean; children: React.ReactNode; } diff --git a/app/presentation/RoomItem/Actions.tsx b/app/presentation/RoomItem/Actions.tsx index 19c63baa..2b53955a 100644 --- a/app/presentation/RoomItem/Actions.tsx +++ b/app/presentation/RoomItem/Actions.tsx @@ -5,7 +5,7 @@ import { RectButton } from 'react-native-gesture-handler'; import { isRTL } from '../../i18n'; import { CustomIcon } from '../../lib/Icons'; import { themes } from '../../constants/colors'; -import { DISPLAY_MODE_CONDENSED } from '../../constants/constantDisplayMode'; +import { DisplayMode } from '../../constants/constantDisplayMode'; import styles, { ACTION_WIDTH, LONG_SWIPE, ROW_HEIGHT_CONDENSED } from './styles'; interface ILeftActions { @@ -40,7 +40,7 @@ export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleR reverse ); - const isCondensed = displayMode === DISPLAY_MODE_CONDENSED; + const isCondensed = displayMode === DisplayMode.Condensed; const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null; return ( @@ -87,7 +87,7 @@ export const RightActions = React.memo( reverse ); - const isCondensed = displayMode === DISPLAY_MODE_CONDENSED; + const isCondensed = displayMode === DisplayMode.Condensed; const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null; return ( diff --git a/app/presentation/RoomItem/IconOrAvatar.js b/app/presentation/RoomItem/IconOrAvatar.js index cedd3b0f..29343477 100644 --- a/app/presentation/RoomItem/IconOrAvatar.js +++ b/app/presentation/RoomItem/IconOrAvatar.js @@ -3,7 +3,7 @@ import { View } from 'react-native'; import PropTypes from 'prop-types'; import Avatar from '../../containers/Avatar'; -import { DISPLAY_MODE_CONDENSED, DISPLAY_MODE_EXPANDED } from '../../constants/constantDisplayMode'; +import { DisplayMode } from '../../constants/constantDisplayMode'; import TypeIcon from './TypeIcon'; import styles from './styles'; @@ -22,11 +22,11 @@ const IconOrAvatar = ({ }) => { if (showAvatar) { return ( - + ); } - if (displayMode === DISPLAY_MODE_EXPANDED && showLastMessage) { + if (displayMode === DisplayMode.Expanded && showLastMessage) { return ( - {showLastMessage && displayMode === DISPLAY_MODE_EXPANDED ? ( + {showLastMessage && displayMode === DisplayMode.Expanded ? ( <> {showAvatar ? ( diff --git a/app/presentation/RoomItem/Wrapper.tsx b/app/presentation/RoomItem/Wrapper.tsx index cb4d6e1b..30c3283d 100644 --- a/app/presentation/RoomItem/Wrapper.tsx +++ b/app/presentation/RoomItem/Wrapper.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { View } from 'react-native'; import { themes } from '../../constants/colors'; -import { DISPLAY_MODE_CONDENSED } from '../../constants/constantDisplayMode'; +import { DisplayMode } from '../../constants/constantDisplayMode'; import IconOrAvatar from './IconOrAvatar'; import styles from './styles'; @@ -25,7 +25,7 @@ interface IWrapper { const Wrapper = ({ accessibilityLabel, theme, children, displayMode, ...props }: IWrapper) => ( {children} diff --git a/app/presentation/UserItem.tsx b/app/presentation/UserItem.tsx index 3dadc2bf..b2e9d0b1 100644 --- a/app/presentation/UserItem.tsx +++ b/app/presentation/UserItem.tsx @@ -46,7 +46,7 @@ interface IUserItem { testID: string; onLongPress?: () => void; style?: StyleProp; - icon: string; + icon?: string | null; theme: string; } diff --git a/app/reducers/activeUsers.js b/app/reducers/activeUsers.js deleted file mode 100644 index 8f6c5b38..00000000 --- a/app/reducers/activeUsers.js +++ /dev/null @@ -1,15 +0,0 @@ -import { SET_ACTIVE_USERS } from '../actions/actionsTypes'; - -const initialState = {}; - -export default function activeUsers(state = initialState, action) { - switch (action.type) { - case SET_ACTIVE_USERS: - return { - ...state, - ...action.activeUsers - }; - default: - return state; - } -} diff --git a/app/reducers/activeUsers.test.ts b/app/reducers/activeUsers.test.ts new file mode 100644 index 00000000..fbe35207 --- /dev/null +++ b/app/reducers/activeUsers.test.ts @@ -0,0 +1,16 @@ +import { setActiveUsers } from '../actions/activeUsers'; +import { IActiveUsers, initialState } from './activeUsers'; +import { mockedStore } from './mockedStore'; + +describe('test reducer', () => { + it('should return initial state', () => { + const state = mockedStore.getState().activeUsers; + expect(state).toEqual(initialState); + }); + it('should return modified store after action', () => { + const activeUsers: IActiveUsers = { any: { status: 'online', statusText: 'any' } }; + mockedStore.dispatch(setActiveUsers(activeUsers)); + const state = mockedStore.getState().activeUsers; + expect(state).toEqual({ ...activeUsers }); + }); +}); diff --git a/app/reducers/activeUsers.ts b/app/reducers/activeUsers.ts new file mode 100644 index 00000000..9877a5ce --- /dev/null +++ b/app/reducers/activeUsers.ts @@ -0,0 +1,26 @@ +import { TApplicationActions } from '../definitions'; +import { SET_ACTIVE_USERS } from '../actions/actionsTypes'; + +type TUserStatus = 'online' | 'offline'; +export interface IActiveUser { + status: TUserStatus; + statusText?: string; +} + +export interface IActiveUsers { + [key: string]: IActiveUser; +} + +export const initialState: IActiveUsers = {}; + +export default function activeUsers(state = initialState, action: TApplicationActions): IActiveUsers { + switch (action.type) { + case SET_ACTIVE_USERS: + return { + ...state, + ...action.activeUsers + }; + default: + return state; + } +} diff --git a/app/reducers/mockedStore.ts b/app/reducers/mockedStore.ts new file mode 100644 index 00000000..5a03297f --- /dev/null +++ b/app/reducers/mockedStore.ts @@ -0,0 +1,7 @@ +import { applyMiddleware, compose, createStore } from 'redux'; +import createSagaMiddleware from 'redux-saga'; + +import reducers from '.'; + +const enhancers = compose(applyMiddleware(createSagaMiddleware())); +export const mockedStore = createStore(reducers, enhancers); diff --git a/app/reducers/selectedUsers.test.ts b/app/reducers/selectedUsers.test.ts new file mode 100644 index 00000000..329be4f9 --- /dev/null +++ b/app/reducers/selectedUsers.test.ts @@ -0,0 +1,36 @@ +import { addUser, reset, setLoading, removeUser } from '../actions/selectedUsers'; +import { mockedStore } from './mockedStore'; +import { initialState } from './selectedUsers'; + +describe('test selectedUsers reducer', () => { + it('should return initial state', () => { + const state = mockedStore.getState().selectedUsers; + expect(state).toEqual(initialState); + }); + + it('should return modified store after addUser', () => { + const user = { _id: 'xxx', name: 'xxx', fname: 'xxx' }; + mockedStore.dispatch(addUser(user)); + const state = mockedStore.getState().selectedUsers.users; + expect(state).toEqual([user]); + }); + + it('should return empty store after remove user', () => { + const user = { _id: 'xxx', name: 'xxx', fname: 'xxx' }; + mockedStore.dispatch(removeUser(user)); + const state = mockedStore.getState().selectedUsers.users; + expect(state).toEqual([]); + }); + + it('should return initial state after reset', () => { + mockedStore.dispatch(reset()); + const state = mockedStore.getState().selectedUsers; + expect(state).toEqual(initialState); + }); + + it('should return loading after call action', () => { + mockedStore.dispatch(setLoading(true)); + const state = mockedStore.getState().selectedUsers.loading; + expect(state).toEqual(true); + }); +}); diff --git a/app/reducers/selectedUsers.js b/app/reducers/selectedUsers.ts similarity index 55% rename from app/reducers/selectedUsers.js rename to app/reducers/selectedUsers.ts index 42d7982c..f6573ac9 100644 --- a/app/reducers/selectedUsers.js +++ b/app/reducers/selectedUsers.ts @@ -1,11 +1,26 @@ +import { TApplicationActions } from '../definitions'; import { SELECTED_USERS } from '../actions/actionsTypes'; -const initialState = { +export interface ISelectedUser { + _id: string; + name: string; + fname: string; + search?: boolean; + // username is used when is from searching + username?: string; +} + +export interface ISelectedUsers { + users: ISelectedUser[]; + loading: boolean; +} + +export const initialState: ISelectedUsers = { users: [], loading: false }; -export default function (state = initialState, action) { +export default function (state = initialState, action: TApplicationActions): ISelectedUsers { switch (action.type) { case SELECTED_USERS.ADD_USER: return { diff --git a/app/reducers/sortPreferences.js b/app/reducers/sortPreferences.js index 31b50185..4ad9e797 100644 --- a/app/reducers/sortPreferences.js +++ b/app/reducers/sortPreferences.js @@ -1,13 +1,13 @@ import { SORT_PREFERENCES } from '../actions/actionsTypes'; -import { DISPLAY_MODE_EXPANDED } from '../constants/constantDisplayMode'; +import { DisplayMode, SortBy } from '../constants/constantDisplayMode'; const initialState = { - sortBy: 'activity', + sortBy: SortBy.Activity, groupByType: false, showFavorites: false, showUnread: false, showAvatar: true, - displayMode: DISPLAY_MODE_EXPANDED + displayMode: DisplayMode.Expanded }; export default (state = initialState, action) => { diff --git a/app/sagas/room.js b/app/sagas/room.js index e3437a4a..f45bf123 100644 --- a/app/sagas/room.js +++ b/app/sagas/room.js @@ -67,7 +67,7 @@ const handleLeaveRoom = function* handleLeaveRoom({ room, roomType, selected }) if (roomType === 'channel') { result = yield RocketChat.leaveRoom(room.rid, room.t); } else if (roomType === 'team') { - result = yield RocketChat.leaveTeam({ teamName: room.name, ...(selected && { rooms: selected }) }); + result = yield RocketChat.leaveTeam({ teamId: room.teamId, ...(selected && { rooms: selected }) }); } if (result?.success) { diff --git a/app/share.tsx b/app/share.tsx index ceb85477..fbfcd0b5 100644 --- a/app/share.tsx +++ b/app/share.tsx @@ -14,6 +14,7 @@ import { defaultHeader, getActiveRouteName, navigationTheme, themedHeader } from import RocketChat, { THEME_PREFERENCES_KEY } from './lib/rocketchat'; import { ThemeContext } from './theme'; import { localAuthenticate } from './utils/localAuthentication'; +import { IThemePreference } from './definitions/ITheme'; import ScreenLockedView from './views/ScreenLockedView'; // Outside Stack import WithoutServersView from './views/WithoutServersView'; @@ -25,6 +26,7 @@ import { setCurrentScreen } from './utils/log'; import AuthLoadingView from './views/AuthLoadingView'; import { DimensionsContext } from './dimensions'; import debounce from './utils/debounce'; +import { ShareInsideStackParamList, ShareOutsideStackParamList, ShareAppStackParamList } from './navigationTypes'; interface IDimensions { width: number; @@ -35,10 +37,7 @@ interface IDimensions { interface IState { theme: string; - themePreferences: { - currentTheme: 'automatic' | 'light'; - darkLevel: string; - }; + themePreferences: IThemePreference; root: any; width: number; height: number; @@ -46,7 +45,7 @@ interface IState { fontScale: number; } -const Inside = createStackNavigator(); +const Inside = createStackNavigator(); const InsideStack = () => { const { theme } = useContext(ThemeContext); @@ -65,24 +64,19 @@ const InsideStack = () => { ); }; -const Outside = createStackNavigator(); +const Outside = createStackNavigator(); const OutsideStack = () => { const { theme } = useContext(ThemeContext); return ( - + ); }; // App -const Stack = createStackNavigator(); +const Stack = createStackNavigator(); export const App = ({ root }: any) => ( <> @@ -112,7 +106,7 @@ class Root extends React.Component<{}, IState> { this.init(); } - componentWillUnmount() { + componentWillUnmount(): void { RocketChat.closeShareExtension(); unsubscribeTheme(); } @@ -139,7 +133,7 @@ class Root extends React.Component<{}, IState> { setTheme = (newTheme = {}) => { // change theme state this.setState( - prevState => newThemeState(prevState, newTheme), + prevState => newThemeState(prevState, newTheme as IThemePreference), () => { const { themePreferences } = this.state; // subscribe to Appearance changes diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.tsx similarity index 82% rename from app/stacks/InsideStack.js rename to app/stacks/InsideStack.tsx index 800c44e5..ec3ae318 100644 --- a/app/stacks/InsideStack.js +++ b/app/stacks/InsideStack.tsx @@ -1,12 +1,11 @@ import React from 'react'; import { I18nManager } from 'react-native'; -import { createStackNavigator } from '@react-navigation/stack'; +import { createStackNavigator, StackNavigationOptions } from '@react-navigation/stack'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { ThemeContext } from '../theme'; import { ModalAnimation, StackAnimation, defaultHeader, themedHeader } from '../utils/navigation'; import Sidebar from '../views/SidebarView'; - // Chats Stack import RoomView from '../views/RoomView'; import RoomsListView from '../views/RoomsListView'; @@ -22,7 +21,6 @@ import MessagesView from '../views/MessagesView'; import AutoTranslateView from '../views/AutoTranslateView'; import DirectoryView from '../views/DirectoryView'; import NotificationPrefView from '../views/NotificationPreferencesView'; -import VisitorNavigationView from '../views/VisitorNavigationView'; import ForwardLivechatView from '../views/ForwardLivechatView'; import LivechatEditView from '../views/LivechatEditView'; import PickerView from '../views/PickerView'; @@ -37,10 +35,8 @@ import { themes } from '../constants/colors'; import ProfileView from '../views/ProfileView'; import UserPreferencesView from '../views/UserPreferencesView'; import UserNotificationPrefView from '../views/UserNotificationPreferencesView'; - // Display Preferences View import DisplayPrefsView from '../views/DisplayPrefsView'; - // Settings Stack import SettingsView from '../views/SettingsView'; import SecurityPrivacyView from '../views/SecurityPrivacyView'; @@ -49,21 +45,16 @@ import LanguageView from '../views/LanguageView'; import ThemeView from '../views/ThemeView'; import DefaultBrowserView from '../views/DefaultBrowserView'; import ScreenLockConfigView from '../views/ScreenLockConfigView'; - // Admin Stack import AdminPanelView from '../views/AdminPanelView'; - // NewMessage Stack import NewMessageView from '../views/NewMessageView'; import CreateChannelView from '../views/CreateChannelView'; - // E2ESaveYourPassword Stack import E2ESaveYourPasswordView from '../views/E2ESaveYourPasswordView'; import E2EHowItWorksView from '../views/E2EHowItWorksView'; - // E2EEnterYourPassword Stack import E2EEnterYourPasswordView from '../views/E2EEnterYourPasswordView'; - // InsideStackNavigator import AttachmentView from '../views/AttachmentView'; import ModalBlockView from '../views/ModalBlockView'; @@ -76,13 +67,26 @@ import AddChannelTeamView from '../views/AddChannelTeamView'; import AddExistingChannelView from '../views/AddExistingChannelView'; import SelectListView from '../views/SelectListView'; import DiscussionsView from '../views/DiscussionsView'; +import { + AdminPanelStackParamList, + ChatsStackParamList, + DisplayPrefStackParamList, + DrawerParamList, + E2EEnterYourPasswordStackParamList, + E2ESaveYourPasswordStackParamList, + InsideStackParamList, + NewMessageStackParamList, + ProfileStackParamList, + SettingsStackParamList +} from './types'; // ChatsStackNavigator -const ChatsStack = createStackNavigator(); +const ChatsStack = createStackNavigator(); const ChatsStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + @@ -111,11 +115,6 @@ const ChatsStackNavigator = () => { component={NotificationPrefView} options={NotificationPrefView.navigationOptions} /> - { component={ThreadMessagesView} options={ThreadMessagesView.navigationOptions} /> - + - + { - - + + ); }; // ProfileStackNavigator -const ProfileStack = createStackNavigator(); +const ProfileStack = createStackNavigator(); const ProfileStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + - + { }; // SettingsStackNavigator -const SettingsStack = createStackNavigator(); +const SettingsStack = createStackNavigator(); const SettingsStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + - + { }; // AdminPanelStackNavigator -const AdminPanelStack = createStackNavigator(); +const AdminPanelStack = createStackNavigator(); const AdminPanelStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + ); }; // DisplayPreferenceNavigator -const DisplayPrefStack = createStackNavigator(); +const DisplayPrefStack = createStackNavigator(); const DisplayPrefStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + ); }; // DrawerNavigator -const Drawer = createDrawerNavigator(); +const Drawer = createDrawerNavigator(); const DrawerNavigator = () => { const { theme } = React.useContext(ThemeContext); @@ -259,12 +242,13 @@ const DrawerNavigator = () => { }; // NewMessageStackNavigator -const NewMessageStack = createStackNavigator(); +const NewMessageStack = createStackNavigator(); const NewMessageStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + { }; // E2ESaveYourPasswordStackNavigator -const E2ESaveYourPasswordStack = createStackNavigator(); +const E2ESaveYourPasswordStack = createStackNavigator(); const E2ESaveYourPasswordStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + { }; // E2EEnterYourPasswordStackNavigator -const E2EEnterYourPasswordStack = createStackNavigator(); +const E2EEnterYourPasswordStack = createStackNavigator(); const E2EEnterYourPasswordStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + { }; // InsideStackNavigator -const InsideStack = createStackNavigator(); +const InsideStack = createStackNavigator(); const InsideStackNavigator = () => { const { theme } = React.useContext(ThemeContext); diff --git a/app/stacks/MasterDetailStack/ModalContainer.js b/app/stacks/MasterDetailStack/ModalContainer.js deleted file mode 100644 index 7be11f8c..00000000 --- a/app/stacks/MasterDetailStack/ModalContainer.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { StyleSheet, TouchableWithoutFeedback, View } from 'react-native'; -import PropTypes from 'prop-types'; - -import sharedStyles from '../../views/Styles'; -import { themes } from '../../constants/colors'; - -const styles = StyleSheet.create({ - root: { - flex: 1, - alignItems: 'center', - justifyContent: 'center' - }, - backdrop: { - ...StyleSheet.absoluteFill - } -}); - -export const ModalContainer = ({ navigation, children, theme }) => ( - - navigation.pop()}> - - - {children} - -); - -ModalContainer.propTypes = { - navigation: PropTypes.object, - children: PropTypes.element, - theme: PropTypes.string -}; diff --git a/app/stacks/MasterDetailStack/ModalContainer.tsx b/app/stacks/MasterDetailStack/ModalContainer.tsx new file mode 100644 index 00000000..376ff876 --- /dev/null +++ b/app/stacks/MasterDetailStack/ModalContainer.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { StyleSheet, TouchableWithoutFeedback, useWindowDimensions, View } from 'react-native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { NavigationContainerProps } from '@react-navigation/core'; + +import sharedStyles from '../../views/Styles'; +import { themes } from '../../constants/colors'; + +interface IModalContainer extends NavigationContainerProps { + navigation: StackNavigationProp; + children: React.ReactNode; + theme: string; +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + alignItems: 'center', + justifyContent: 'center' + }, + backdrop: { + ...StyleSheet.absoluteFillObject + } +}); + +export const ModalContainer = ({ navigation, children, theme }: IModalContainer): JSX.Element => { + const { height } = useWindowDimensions(); + const modalHeight = sharedStyles.modalFormSheet.height; + return ( + + navigation.pop()}> + + + height ? height : modalHeight + }}> + {children} + + + ); +}; diff --git a/app/stacks/MasterDetailStack/index.js b/app/stacks/MasterDetailStack/index.tsx similarity index 86% rename from app/stacks/MasterDetailStack/index.js rename to app/stacks/MasterDetailStack/index.tsx index 5537d14d..2cb94671 100644 --- a/app/stacks/MasterDetailStack/index.js +++ b/app/stacks/MasterDetailStack/index.tsx @@ -1,12 +1,10 @@ import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; import { useIsFocused } from '@react-navigation/native'; -import { createStackNavigator } from '@react-navigation/stack'; +import { createStackNavigator, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { ThemeContext } from '../../theme'; import { FadeFromCenterModal, StackAnimation, defaultHeader, themedHeader } from '../../utils/navigation'; - // Chats Stack import RoomView from '../../views/RoomView'; import RoomsListView from '../../views/RoomsListView'; @@ -22,7 +20,6 @@ import MessagesView from '../../views/MessagesView'; import AutoTranslateView from '../../views/AutoTranslateView'; import DirectoryView from '../../views/DirectoryView'; import NotificationPrefView from '../../views/NotificationPreferencesView'; -import VisitorNavigationView from '../../views/VisitorNavigationView'; import ForwardLivechatView from '../../views/ForwardLivechatView'; import CannedResponsesListView from '../../views/CannedResponsesListView'; import CannedResponseDetail from '../../views/CannedResponseDetail'; @@ -46,7 +43,6 @@ import UserPreferencesView from '../../views/UserPreferencesView'; import UserNotificationPrefView from '../../views/UserNotificationPreferencesView'; import SecurityPrivacyView from '../../views/SecurityPrivacyView'; import E2EEncryptionSecurityView from '../../views/E2EEncryptionSecurityView'; - // InsideStackNavigator import AttachmentView from '../../views/AttachmentView'; import ModalBlockView from '../../views/ModalBlockView'; @@ -64,9 +60,15 @@ import AddExistingChannelView from '../../views/AddExistingChannelView'; import SelectListView from '../../views/SelectListView'; import DiscussionsView from '../../views/DiscussionsView'; import { ModalContainer } from './ModalContainer'; +import { + MasterDetailChatsStackParamList, + MasterDetailDrawerParamList, + MasterDetailInsideStackParamList, + ModalStackParamList +} from './types'; // ChatsStackNavigator -const ChatsStack = createStackNavigator(); +const ChatsStack = createStackNavigator(); const ChatsStackNavigator = React.memo(() => { const { theme } = React.useContext(ThemeContext); @@ -83,28 +85,35 @@ const ChatsStackNavigator = React.memo(() => { }, [isFocused]); return ( - + ); }); // DrawerNavigator -const Drawer = createDrawerNavigator(); +const Drawer = createDrawerNavigator(); const DrawerNavigator = React.memo(() => ( } drawerType='permanent'> )); -const ModalStack = createStackNavigator(); -const ModalStackNavigator = React.memo(({ navigation }) => { +export interface INavigation { + navigation: StackNavigationProp; +} + +const ModalStack = createStackNavigator(); +const ModalStackNavigator = React.memo(({ navigation }: INavigation) => { const { theme } = React.useContext(ThemeContext); return ( - + { /> - + { component={NotificationPrefView} options={NotificationPrefView.navigationOptions} /> - - - + + @@ -228,21 +220,13 @@ const ModalStackNavigator = React.memo(({ navigation }) => { component={E2EEnterYourPasswordView} options={E2EEnterYourPasswordView.navigationOptions} /> - + - + { ); }); -ModalStackNavigator.propTypes = { - navigation: PropTypes.object -}; - // InsideStackNavigator -const InsideStack = createStackNavigator(); +const InsideStack = createStackNavigator(); const InsideStackNavigator = React.memo(() => { const { theme } = React.useContext(ThemeContext); return ( - + diff --git a/app/stacks/MasterDetailStack/types.ts b/app/stacks/MasterDetailStack/types.ts new file mode 100644 index 00000000..242c13ba --- /dev/null +++ b/app/stacks/MasterDetailStack/types.ts @@ -0,0 +1,200 @@ +import { TextInputProps } from 'react-native'; +import { NavigatorScreenParams } from '@react-navigation/core'; + +import { IAttachment } from '../../definitions/IAttachment'; +import { IMessage } from '../../definitions/IMessage'; +import { ISubscription, SubscriptionType } from '../../definitions/ISubscription'; + +export type MasterDetailChatsStackParamList = { + RoomView: { + rid: string; + t: SubscriptionType; + tmid?: string; + message?: string; + name?: string; + fname?: string; + prid?: string; + room: ISubscription; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; + }; +}; + +export type MasterDetailDrawerParamList = { + ChatsStackNavigator: NavigatorScreenParams; +}; + +export type ModalStackParamList = { + RoomActionsView: { + room: ISubscription; + member: any; + rid: string; + t: SubscriptionType; + joined: boolean; + }; + RoomInfoView: { + room: ISubscription; + member: any; + rid: string; + t: SubscriptionType; + }; + SelectListView: { + data: any; + title: string; + infoText: string; + nextAction: Function; + showAlert: boolean; + isSearch: boolean; + onSearch: Function; + isRadio?: boolean; + }; + RoomInfoEditView: { + rid: string; + }; + RoomMembersView: { + rid: string; + room: ISubscription; + }; + SearchMessagesView: { + rid: string; + t: SubscriptionType; + encrypted?: boolean; + showCloseModal?: boolean; + }; + SelectedUsersView: { + maxUsers: number; + showButton: boolean; + title: string; + buttonText: string; + nextAction: Function; + }; + InviteUsersView: { + rid: string; + }; + AddChannelTeamView: { + teamId?: string; + teamChannels: []; // TODO: Change + }; + AddExistingChannelView: { + teamId?: boolean; + }; + InviteUsersEditView: { + rid: string; + }; + MessagesView: { + rid: string; + t: SubscriptionType; + name: string; + }; + AutoTranslateView: { + rid: string; + room: ISubscription; + }; + DirectoryView: undefined; + QueueListView: undefined; + NotificationPrefView: { + rid: string; + room: ISubscription; + }; + ForwardLivechatView: { + rid: string; + }; + CannedResponsesListView: { + rid: string; + }; + CannedResponseDetail: { + cannedResponse: { + shortcut: string; + text: string; + scopeName: string; + tags: string[]; + }; + room: ISubscription; + }; + LivechatEditView: { + room: ISubscription; + roomUser: any; // TODO: Change + }; + PickerView: { + title: string; + data: []; // TODO: Change + value: any; // TODO: Change + onChangeText: TextInputProps['onChangeText']; + goBack: Function; + onChangeValue: Function; + }; + ThreadMessagesView: { + rid: string; + t: SubscriptionType; + }; + TeamChannelsView: { + teamId: string; + }; + MarkdownTableView: { + renderRows: Function; + tableWidth: number; + }; + ReadReceiptsView: { + messageId: string; + }; + SettingsView: undefined; + LanguageView: undefined; + ThemeView: undefined; + DefaultBrowserView: undefined; + ScreenLockConfigView: undefined; + StatusView: undefined; + ProfileView: undefined; + DisplayPrefsView: undefined; + AdminPanelView: undefined; + NewMessageView: undefined; + SelectedUsersViewCreateChannel: { + maxUsers: number; + showButton: boolean; + title: string; + buttonText: string; + nextAction: Function; + }; // TODO: Change + CreateChannelView: { + isTeam?: boolean; // TODO: To check + teamId?: string; + }; + CreateDiscussionView: { + channel: ISubscription; + message: IMessage; + showCloseModal: boolean; + }; + E2ESaveYourPasswordView: undefined; + E2EHowItWorksView: { + showCloseModal: boolean; + }; + E2EEnterYourPasswordView: undefined; + UserPreferencesView: undefined; + UserNotificationPrefView: undefined; + SecurityPrivacyView: undefined; + E2EEncryptionSecurityView: undefined; +}; + +export type MasterDetailInsideStackParamList = { + DrawerNavigator: NavigatorScreenParams>; // TODO: Change + ModalStackNavigator: NavigatorScreenParams; + AttachmentView: { + attachment: IAttachment; + }; + ModalBlockView: { + data: any; // TODO: Change + }; + JitsiMeetView: { + rid: string; + url: string; + onlyAudio?: boolean; + }; + ShareView: { + attachments: IAttachment[]; + isShareView?: boolean; + serverInfo: {}; + text: string; + room: ISubscription; + thread: any; // TODO: Change + }; +}; diff --git a/app/stacks/OutsideStack.js b/app/stacks/OutsideStack.tsx similarity index 81% rename from app/stacks/OutsideStack.js rename to app/stacks/OutsideStack.tsx index 392850c3..fb791330 100644 --- a/app/stacks/OutsideStack.js +++ b/app/stacks/OutsideStack.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import { createStackNavigator } from '@react-navigation/stack'; +import { createStackNavigator, StackNavigationOptions } from '@react-navigation/stack'; import { connect } from 'react-redux'; import { ThemeContext } from '../theme'; import { ModalAnimation, StackAnimation, defaultHeader, themedHeader } from '../utils/navigation'; - // Outside Stack import NewServerView from '../views/NewServerView'; import WorkspaceView from '../views/WorkspaceView'; @@ -14,37 +13,34 @@ import SendEmailConfirmationView from '../views/SendEmailConfirmationView'; import RegisterView from '../views/RegisterView'; import LegalView from '../views/LegalView'; import AuthenticationWebView from '../views/AuthenticationWebView'; +import { OutsideModalParamList, OutsideParamList } from './types'; // Outside -const Outside = createStackNavigator(); +const Outside = createStackNavigator(); const _OutsideStack = () => { const { theme } = React.useContext(ThemeContext); return ( - + - + ); }; -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ root: state.app.root }); const OutsideStack = connect(mapStateToProps)(_OutsideStack); // OutsideStackModal -const OutsideModal = createStackNavigator(); +const OutsideModal = createStackNavigator(); const OutsideStackModal = () => { const { theme } = React.useContext(ThemeContext); diff --git a/app/stacks/types.ts b/app/stacks/types.ts new file mode 100644 index 00000000..c6016f2e --- /dev/null +++ b/app/stacks/types.ts @@ -0,0 +1,272 @@ +import { NavigatorScreenParams } from '@react-navigation/core'; +import { TextInputProps } from 'react-native'; +import Model from '@nozbe/watermelondb/Model'; + +import { IOptionsField } from '../views/NotificationPreferencesView/options'; +import { IServer } from '../definitions/IServer'; +import { IAttachment } from '../definitions/IAttachment'; +import { IMessage } from '../definitions/IMessage'; +import { ISubscription, SubscriptionType, TSubscriptionModel } from '../definitions/ISubscription'; + +export type ChatsStackParamList = { + RoomsListView: undefined; + RoomView: { + rid: string; + t: SubscriptionType; + tmid?: string; + message?: string; + name?: string; + fname?: string; + prid?: string; + room?: ISubscription; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; + }; + RoomActionsView: { + room: ISubscription; + member: any; + rid: string; + t: SubscriptionType; + joined: boolean; + }; + SelectListView: { + data: any; + title: string; + infoText: string; + nextAction: Function; + showAlert: boolean; + isSearch: boolean; + onSearch: Function; + isRadio?: boolean; + }; + RoomInfoView: { + room: ISubscription; + member: any; + rid: string; + t: SubscriptionType; + }; + RoomInfoEditView: { + rid: string; + }; + RoomMembersView: { + rid: string; + room: ISubscription; + }; + SearchMessagesView: { + rid: string; + t: SubscriptionType; + encrypted?: boolean; + showCloseModal?: boolean; + }; + SelectedUsersView: { + maxUsers?: number; + showButton?: boolean; + title?: string; + buttonText?: string; + nextAction?: Function; + }; + InviteUsersView: { + rid: string; + }; + InviteUsersEditView: { + rid: string; + }; + MessagesView: { + rid: string; + t: SubscriptionType; + name: string; + }; + AutoTranslateView: { + rid: string; + room: TSubscriptionModel; + }; + DirectoryView: undefined; + NotificationPrefView: { + rid: string; + room: Model; + }; + ForwardLivechatView: { + rid: string; + }; + LivechatEditView: { + room: ISubscription; + roomUser: any; // TODO: Change + }; + PickerView: { + title: string; + data: IOptionsField[]; + value?: any; // TODO: Change + onChangeText?: ((text: string) => IOptionsField[]) | ((term?: string) => Promise); + goBack?: boolean; + onChangeValue: Function; + }; + ThreadMessagesView: { + rid: string; + t: SubscriptionType; + }; + TeamChannelsView: { + teamId: string; + }; + CreateChannelView: { + isTeam?: boolean; // TODO: To check + teamId?: string; + }; + AddChannelTeamView: { + teamId?: string; + teamChannels: []; // TODO: Change + }; + AddExistingChannelView: { + teamId?: string; + teamChannels: []; // TODO: Change + }; + MarkdownTableView: { + renderRows: (drawExtraBorders?: boolean) => JSX.Element; + tableWidth: number; + }; + ReadReceiptsView: { + messageId: string; + }; + QueueListView: undefined; + CannedResponsesListView: { + rid: string; + }; + CannedResponseDetail: { + cannedResponse: { + shortcut: string; + text: string; + scopeName: string; + tags: string[]; + }; + room: ISubscription; + }; +}; + +export type ProfileStackParamList = { + ProfileView: undefined; + UserPreferencesView: undefined; + UserNotificationPrefView: undefined; + PickerView: { + title: string; + data: IOptionsField[]; + value: any; // TODO: Change + onChangeText?: TextInputProps['onChangeText']; + goBack?: Function; + onChangeValue: Function; + }; +}; + +export type SettingsStackParamList = { + SettingsView: undefined; + SecurityPrivacyView: undefined; + E2EEncryptionSecurityView: undefined; + LanguageView: undefined; + ThemeView: undefined; + DefaultBrowserView: undefined; + ScreenLockConfigView: undefined; + ProfileView: undefined; + DisplayPrefsView: undefined; +}; + +export type AdminPanelStackParamList = { + AdminPanelView: undefined; +}; + +export type DisplayPrefStackParamList = { + DisplayPrefsView: undefined; +}; + +export type DrawerParamList = { + ChatsStackNavigator: NavigatorScreenParams; + ProfileStackNavigator: NavigatorScreenParams; + SettingsStackNavigator: NavigatorScreenParams; + AdminPanelStackNavigator: NavigatorScreenParams; + DisplayPrefStackNavigator: NavigatorScreenParams; +}; + +export type NewMessageStackParamList = { + NewMessageView: undefined; + SelectedUsersViewCreateChannel: { + maxUsers?: number; + showButton?: boolean; + title?: string; + buttonText?: string; + nextAction?: Function; + }; // TODO: Change + CreateChannelView: { + isTeam?: boolean; // TODO: To check + teamId?: string; + }; + CreateDiscussionView: { + channel: ISubscription; + message: IMessage; + showCloseModal: boolean; + }; +}; + +export type E2ESaveYourPasswordStackParamList = { + E2ESaveYourPasswordView: undefined; + E2EHowItWorksView?: { + showCloseModal?: boolean; + }; +}; + +export type E2EEnterYourPasswordStackParamList = { + E2EEnterYourPasswordView: undefined; +}; + +export type InsideStackParamList = { + DrawerNavigator: NavigatorScreenParams; + NewMessageStackNavigator: NavigatorScreenParams; + E2ESaveYourPasswordStackNavigator: NavigatorScreenParams; + E2EEnterYourPasswordStackNavigator: NavigatorScreenParams; + AttachmentView: { + attachment: IAttachment; + }; + StatusView: undefined; + ShareView: { + attachments: IAttachment[]; + isShareView?: boolean; + isShareExtension: boolean; + serverInfo: IServer; + text: string; + room: ISubscription; + thread: any; // TODO: Change + }; + ModalBlockView: { + data: any; // TODO: Change; + }; + JitsiMeetView: { + rid: string; + url: string; + onlyAudio?: boolean; + }; +}; + +export type OutsideParamList = { + NewServerView: undefined; + WorkspaceView: undefined; + LoginView: { + title: string; + username?: string; + }; + ForgotPasswordView: { + title: string; + }; + SendEmailConfirmationView: { + user?: string; + }; + RegisterView: { + title: string; + }; + LegalView: undefined; +}; + +export type OutsideModalParamList = { + OutsideStack: NavigatorScreenParams; + AuthenticationWebView: { + authType: string; + url: string; + ssoToken?: string; + }; +}; diff --git a/app/theme.tsx b/app/theme.tsx index 8618dbd9..6bfd248d 100644 --- a/app/theme.tsx +++ b/app/theme.tsx @@ -1,18 +1,17 @@ import React from 'react'; import hoistNonReactStatics from 'hoist-non-react-statics'; +import { IThemePreference } from './definitions/ITheme'; + interface IThemeContextProps { theme: string; - themePreferences?: { - currentTheme: 'automatic' | 'light'; - darkLevel: string; - }; + themePreferences?: IThemePreference; setTheme?: (newTheme?: {}) => void; } export const ThemeContext = React.createContext({ theme: 'light' }); -export function withTheme(Component: React.ComponentType): (props: any) => JSX.Element { +export function withTheme(Component: any): any { const ThemedComponent = (props: any) => ( {contexts => } ); diff --git a/app/utils/appGroup.js b/app/utils/appGroup.ts similarity index 83% rename from app/utils/appGroup.js rename to app/utils/appGroup.ts index 63fb428a..f92227c0 100644 --- a/app/utils/appGroup.js +++ b/app/utils/appGroup.ts @@ -4,7 +4,7 @@ import { isIOS } from './deviceInfo'; const { AppGroup } = NativeModules; -const appGroup = { +const appGroup: { path: string } = { path: isIOS ? AppGroup.path : '' }; diff --git a/app/utils/avatar.js b/app/utils/avatar.ts similarity index 72% rename from app/utils/avatar.js rename to app/utils/avatar.ts index 4cc15cda..7e4b2819 100644 --- a/app/utils/avatar.js +++ b/app/utils/avatar.ts @@ -1,6 +1,8 @@ import { compareServerVersion, methods } from '../lib/utils'; +import { SubscriptionType } from '../definitions/ISubscription'; +import { IAvatar } from '../containers/Avatar/interfaces'; -const formatUrl = (url, size, query) => `${url}?format=png&size=${size}${query}`; +const formatUrl = (url: string, size: number, query: string) => `${url}?format=png&size=${size}${query}`; export const avatarURL = ({ type, @@ -13,9 +15,9 @@ export const avatarURL = ({ rid, blockUnauthenticatedAccess, serverVersion -}) => { +}: IAvatar): string => { let room; - if (type === 'd') { + if (type === SubscriptionType.DIRECT) { room = text; } else if (rid && !compareServerVersion(serverVersion, '3.6.0', methods.lowerThan)) { room = `room/${rid}`; diff --git a/app/utils/base64-js/index.js b/app/utils/base64-js/index.ts similarity index 87% rename from app/utils/base64-js/index.js rename to app/utils/base64-js/index.ts index 5616f71d..71fac91c 100644 --- a/app/utils/base64-js/index.js +++ b/app/utils/base64-js/index.ts @@ -1,8 +1,8 @@ /* eslint-disable no-bitwise */ // https://github.com/beatgammit/base64-js/blob/master/index.js -const lookup = []; -const revLookup = []; +const lookup: string[] = []; +const revLookup: number[] = []; const Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array; const code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; @@ -16,7 +16,7 @@ for (let i = 0, len = code.length; i < len; i += 1) { revLookup['-'.charCodeAt(0)] = 62; revLookup['_'.charCodeAt(0)] = 63; -const getLens = b64 => { +const getLens = (b64: string) => { const len = b64.length; // We're encoding some strings not multiple of 4, so, disable this check @@ -37,16 +37,17 @@ const getLens = b64 => { }; // base64 is 4/3 + up to two characters of the original data -export const byteLength = b64 => { +export const byteLength = (b64: string) => { const lens = getLens(b64); const validLen = lens[0]; const placeHoldersLen = lens[1]; return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; }; -const _byteLength = (b64, validLen, placeHoldersLen) => ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; +const _byteLength = (b64: string, validLen: number, placeHoldersLen: number) => + ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; -export const toByteArray = b64 => { +export const toByteArray = (b64: string) => { let tmp; const lens = getLens(b64); const validLen = lens[0]; @@ -92,10 +93,10 @@ export const toByteArray = b64 => { return arr; }; -const tripletToBase64 = num => +const tripletToBase64 = (num: number) => lookup[(num >> 18) & 0x3f] + lookup[(num >> 12) & 0x3f] + lookup[(num >> 6) & 0x3f] + lookup[num & 0x3f]; -const encodeChunk = (uint8, start, end) => { +const encodeChunk = (uint8: number[] | Uint8Array, start: number, end: number) => { let tmp; const output = []; for (let i = start; i < end; i += 3) { @@ -105,7 +106,7 @@ const encodeChunk = (uint8, start, end) => { return output.join(''); }; -export const fromByteArray = uint8 => { +export const fromByteArray = (uint8: number[] | Uint8Array) => { let tmp; const len = uint8.length; const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes diff --git a/app/utils/debounce.js b/app/utils/debounce.js deleted file mode 100644 index 106c61d0..00000000 --- a/app/utils/debounce.js +++ /dev/null @@ -1,20 +0,0 @@ -export default function debounce(func, wait, immediate) { - let timeout; - function _debounce(...args) { - const context = this; - const later = function __debounce() { - timeout = null; - if (!immediate) { - func.apply(context, args); - } - }; - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - func.apply(context, args); - } - } - _debounce.stop = () => clearTimeout(timeout); - return _debounce; -} diff --git a/app/utils/debounce.ts b/app/utils/debounce.ts new file mode 100644 index 00000000..e0c28b23 --- /dev/null +++ b/app/utils/debounce.ts @@ -0,0 +1,22 @@ +export default function debounce(func: Function, wait?: number, immediate?: boolean) { + let timeout: number | null; + function _debounce(...args: any[]) { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-this-alias + const context = this; + const later = function __debounce() { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + const callNow = immediate && !timeout; + clearTimeout(timeout!); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + } + _debounce.stop = () => clearTimeout(timeout!); + return _debounce; +} diff --git a/app/utils/deviceInfo.js b/app/utils/deviceInfo.ts similarity index 92% rename from app/utils/deviceInfo.js rename to app/utils/deviceInfo.ts index 7961b440..9cb7ac72 100644 --- a/app/utils/deviceInfo.js +++ b/app/utils/deviceInfo.ts @@ -9,7 +9,7 @@ export const getBundleId = DeviceInfo.getBundleId(); export const getDeviceModel = DeviceInfo.getModel(); // Theme is supported by system on iOS 13+ or Android 10+ -export const supportSystemTheme = () => { +export const supportSystemTheme = (): boolean => { const systemVersion = parseInt(DeviceInfo.getSystemVersion(), 10); return systemVersion >= (isIOS ? 13 : 10); }; diff --git a/app/utils/events.js b/app/utils/events.ts similarity index 53% rename from app/utils/events.js rename to app/utils/events.ts index 8e67fc82..fc0b975a 100644 --- a/app/utils/events.js +++ b/app/utils/events.ts @@ -1,11 +1,25 @@ +import { ICommand } from '../definitions/ICommand'; import log from './log'; +type TEventEmitterEmmitArgs = + | { rid: string } + | { message: string } + | { method: string } + | { invalid: boolean } + | { force: boolean } + | { hasBiometry: boolean } + | { event: string | ICommand } + | { cancel: () => void } + | { submit: (param: string) => void }; + class EventEmitter { + private events: { [key: string]: any }; + constructor() { this.events = {}; } - addEventListener(event, listener) { + addEventListener(event: string, listener: Function) { if (typeof this.events[event] !== 'object') { this.events[event] = []; } @@ -13,7 +27,7 @@ class EventEmitter { return listener; } - removeListener(event, listener) { + removeListener(event: string, listener: Function) { if (typeof this.events[event] === 'object') { const idx = this.events[event].indexOf(listener); if (idx > -1) { @@ -25,9 +39,9 @@ class EventEmitter { } } - emit(event, ...args) { + emit(event: string, ...args: TEventEmitterEmmitArgs[]) { if (typeof this.events[event] === 'object') { - this.events[event].forEach(listener => { + this.events[event].forEach((listener: Function) => { try { listener.apply(this, args); } catch (e) { diff --git a/app/utils/fetch.js b/app/utils/fetch.ts similarity index 75% rename from app/utils/fetch.js rename to app/utils/fetch.ts index 84f5669a..c8758da8 100644 --- a/app/utils/fetch.js +++ b/app/utils/fetch.ts @@ -4,15 +4,20 @@ import { settings as RocketChatSettings } from '@rocket.chat/sdk'; import RocketChat from '../lib/rocketchat'; +interface CustomHeaders { + 'User-Agent': string; + Authorization?: string; +} + // this form is required by Rocket.Chat's parser in "app/statistics/server/lib/UAParserCustom.js" -export const headers = { +export const headers: CustomHeaders = { 'User-Agent': `RC Mobile; ${ Platform.OS } ${DeviceInfo.getSystemVersion()}; v${DeviceInfo.getVersion()} (${DeviceInfo.getBuildNumber()})` }; let _basicAuth; -export const setBasicAuth = basicAuth => { +export const setBasicAuth = (basicAuth: string): void => { _basicAuth = basicAuth; if (basicAuth) { RocketChatSettings.customHeaders = { ...headers, Authorization: `Basic ${_basicAuth}` }; @@ -24,12 +29,15 @@ export const BASIC_AUTH_KEY = 'BASIC_AUTH_KEY'; RocketChatSettings.customHeaders = headers; -export default (url, options = {}) => { +export default (url: string, options: { headers?: Headers; signal?: AbortSignal } = {}): Promise => { let customOptions = { ...options, headers: RocketChatSettings.customHeaders }; if (options && options.headers) { customOptions = { ...customOptions, headers: { ...options.headers, ...customOptions.headers } }; } + // TODO: Refactor when migrate rocketchat.js + // @ts-ignore if (RocketChat.controller) { + // @ts-ignore const { signal } = RocketChat.controller; customOptions = { ...customOptions, signal }; } diff --git a/app/utils/fileDownload/index.ts b/app/utils/fileDownload/index.ts index dda1a78f..279d3b3a 100644 --- a/app/utils/fileDownload/index.ts +++ b/app/utils/fileDownload/index.ts @@ -5,13 +5,7 @@ import EventEmitter from '../events'; import { LISTENER } from '../../containers/Toast'; import I18n from '../../i18n'; import { DOCUMENTS_PATH, DOWNLOAD_PATH } from '../../constants/localPath'; - -interface IAttachment { - title: string; - title_link: string; - type: string; - description: string; -} +import { IAttachment } from '../../definitions/IAttachment'; export const getLocalFilePathFromFile = (localPath: string, attachment: IAttachment): string => `${localPath}${attachment.title}`; diff --git a/app/utils/fileUpload/index.ios.js b/app/utils/fileUpload/index.ios.ts similarity index 65% rename from app/utils/fileUpload/index.ios.js rename to app/utils/fileUpload/index.ios.ts index a9764055..ae5cfabc 100644 --- a/app/utils/fileUpload/index.ios.js +++ b/app/utils/fileUpload/index.ios.ts @@ -1,19 +1,25 @@ +import { IFileUpload } from './interfaces'; + class Upload { + public xhr: XMLHttpRequest; + + public formData: FormData; + constructor() { this.xhr = new XMLHttpRequest(); this.formData = new FormData(); } - then = callback => { + then = (callback: (param: { respInfo: XMLHttpRequest }) => XMLHttpRequest) => { this.xhr.onload = () => callback({ respInfo: this.xhr }); this.xhr.send(this.formData); }; - catch = callback => { + catch = (callback: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null) => { this.xhr.onerror = callback; }; - uploadProgress = callback => { + uploadProgress = (callback: (param: number, arg1: number) => any) => { this.xhr.upload.onprogress = ({ total, loaded }) => callback(loaded, total); }; @@ -24,7 +30,7 @@ class Upload { } class FileUpload { - fetch = (method, url, headers, data) => { + fetch = (method: string, url: string, headers: { [x: string]: string }, data: IFileUpload[]) => { const upload = new Upload(); upload.xhr.open(method, url); @@ -35,6 +41,7 @@ class FileUpload { data.forEach(item => { if (item.uri) { upload.formData.append(item.name, { + // @ts-ignore uri: item.uri, type: item.type, name: item.filename diff --git a/app/utils/fileUpload/index.android.js b/app/utils/fileUpload/index.ts similarity index 64% rename from app/utils/fileUpload/index.android.js rename to app/utils/fileUpload/index.ts index 5c45c27b..1d2bdb31 100644 --- a/app/utils/fileUpload/index.android.js +++ b/app/utils/fileUpload/index.ts @@ -1,7 +1,11 @@ import RNFetchBlob from 'rn-fetch-blob'; +import { IFileUpload } from './interfaces'; + +type TMethods = 'POST' | 'GET' | 'DELETE' | 'PUT' | 'post' | 'get' | 'delete' | 'put'; + class FileUpload { - fetch = (method, url, headers, data) => { + fetch = (method: TMethods, url: string, headers: { [key: string]: string }, data: IFileUpload[]) => { const formData = data.map(item => { if (item.uri) { return { diff --git a/app/utils/fileUpload/interfaces.ts b/app/utils/fileUpload/interfaces.ts new file mode 100644 index 00000000..a3002f72 --- /dev/null +++ b/app/utils/fileUpload/interfaces.ts @@ -0,0 +1,7 @@ +export interface IFileUpload { + name: string; + uri?: string; + type: string; + filename: string; + data: any; +} diff --git a/app/utils/goRoom.js b/app/utils/goRoom.ts similarity index 58% rename from app/utils/goRoom.js rename to app/utils/goRoom.ts index 1025a17d..dc8a3188 100644 --- a/app/utils/goRoom.js +++ b/app/utils/goRoom.ts @@ -1,7 +1,17 @@ +import { ChatsStackParamList } from '../stacks/types'; import Navigation from '../lib/Navigation'; import RocketChat from '../lib/rocketchat'; +import { ISubscription, SubscriptionType } from '../definitions/ISubscription'; -const navigate = ({ item, isMasterDetail, ...props }) => { +const navigate = ({ + item, + isMasterDetail, + ...props +}: { + item: IItem; + isMasterDetail: boolean; + navigationMethod?: () => ChatsStackParamList; +}) => { let navigationMethod = props.navigationMethod ?? Navigation.navigate; if (isMasterDetail) { @@ -20,7 +30,22 @@ const navigate = ({ item, isMasterDetail, ...props }) => { }); }; -export const goRoom = async ({ item = {}, isMasterDetail = false, ...props }) => { +interface IItem extends Partial { + rid: string; + name: string; + t: SubscriptionType; +} + +export const goRoom = async ({ + item, + isMasterDetail = false, + ...props +}: { + item: IItem; + isMasterDetail: boolean; + navigationMethod?: any; + jumpToMessageId?: string; +}): Promise => { if (item.t === 'd' && item.search) { // if user is using the search we need first to join/create room try { @@ -30,8 +55,8 @@ export const goRoom = async ({ item = {}, isMasterDetail = false, ...props }) => return navigate({ item: { rid: result.room._id, - name: username, - t: 'd' + name: username!, + t: SubscriptionType.DIRECT }, isMasterDetail, ...props diff --git a/app/utils/info.js b/app/utils/info.js deleted file mode 100644 index 5d72f200..00000000 --- a/app/utils/info.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Alert } from 'react-native'; - -import I18n from '../i18n'; - -export const showErrorAlert = (message, title, onPress = () => {}) => - Alert.alert(title, message, [{ text: 'OK', onPress }], { cancelable: true }); - -export const showConfirmationAlert = ({ title, message, confirmationText, dismissText = I18n.t('Cancel'), onPress, onCancel }) => - Alert.alert( - title || I18n.t('Are_you_sure_question_mark'), - message, - [ - { - text: dismissText, - onPress: onCancel, - style: 'cancel' - }, - { - text: confirmationText, - style: 'destructive', - onPress - } - ], - { cancelable: false } - ); diff --git a/app/utils/info.ts b/app/utils/info.ts new file mode 100644 index 00000000..da882ee4 --- /dev/null +++ b/app/utils/info.ts @@ -0,0 +1,41 @@ +import { Alert } from 'react-native'; + +import I18n from '../i18n'; + +export const showErrorAlert = (message: string, title?: string, onPress = () => {}): void => + Alert.alert(title!, message, [{ text: 'OK', onPress }], { cancelable: true }); + +interface IShowConfirmationAlert { + title?: string; + message: string; + confirmationText: string; + dismissText?: string; + onPress: () => void; + onCancel?: () => void; +} + +export const showConfirmationAlert = ({ + title, + message, + confirmationText, + dismissText = I18n.t('Cancel'), + onPress, + onCancel +}: IShowConfirmationAlert): void => + Alert.alert( + title || I18n.t('Are_you_sure_question_mark'), + message, + [ + { + text: dismissText, + onPress: onCancel, + style: 'cancel' + }, + { + text: confirmationText, + style: 'destructive', + onPress + } + ], + { cancelable: false } + ); diff --git a/app/utils/isReadOnly.js b/app/utils/isReadOnly.ts similarity index 59% rename from app/utils/isReadOnly.js rename to app/utils/isReadOnly.ts index 62ae4fff..d94b73c4 100644 --- a/app/utils/isReadOnly.js +++ b/app/utils/isReadOnly.ts @@ -1,16 +1,21 @@ import RocketChat from '../lib/rocketchat'; import reduxStore from '../lib/createStore'; +import { ISubscription } from '../definitions/ISubscription'; -const canPostReadOnly = async ({ rid }) => { +const canPostReadOnly = async ({ rid }: { rid: string }) => { // TODO: this is not reactive. If this permission changes, the component won't be updated const postReadOnlyPermission = reduxStore.getState().permissions['post-readonly']; const permission = await RocketChat.hasPermission([postReadOnlyPermission], rid); return permission[0]; }; -const isMuted = (room, user) => room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username); +const isMuted = (room: ISubscription, user: { username: string }) => + room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username); -export const isReadOnly = async (room, user) => { +export const isReadOnly = async ( + room: ISubscription, + user: { id?: string; username: string; token?: string } +): Promise => { if (room.archived) { return true; } diff --git a/app/utils/isValidEmail.js b/app/utils/isValidEmail.ts similarity index 78% rename from app/utils/isValidEmail.js rename to app/utils/isValidEmail.ts index a8bd490f..e230fc32 100644 --- a/app/utils/isValidEmail.js +++ b/app/utils/isValidEmail.ts @@ -1,4 +1,4 @@ -export default function isValidEmail(email) { +export default function isValidEmail(email: string): boolean { /* eslint-disable no-useless-escape */ const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; diff --git a/app/utils/layoutAnimation.js b/app/utils/layoutAnimation.ts similarity index 100% rename from app/utils/layoutAnimation.js rename to app/utils/layoutAnimation.ts diff --git a/app/utils/localAuthentication.js b/app/utils/localAuthentication.ts similarity index 73% rename from app/utils/localAuthentication.js rename to app/utils/localAuthentication.ts index 29f25685..f4359963 100644 --- a/app/utils/localAuthentication.js +++ b/app/utils/localAuthentication.ts @@ -16,16 +16,17 @@ import { } from '../constants/localAuthentication'; import I18n from '../i18n'; import { setLocalAuthenticated } from '../actions/login'; +import { TServerModel } from '../definitions/IServer'; import EventEmitter from './events'; import { isIOS } from './deviceInfo'; -export const saveLastLocalAuthenticationSession = async (server, serverRecord) => { +export const saveLastLocalAuthenticationSession = async (server: string, serverRecord?: TServerModel): Promise => { const serversDB = database.servers; const serversCollection = serversDB.get('servers'); - await serversDB.action(async () => { + await serversDB.write(async () => { try { if (!serverRecord) { - serverRecord = await serversCollection.find(server); + serverRecord = (await serversCollection.find(server)) as TServerModel; } await serverRecord.update(record => { record.lastLocalAuthenticatedSession = new Date(); @@ -36,31 +37,31 @@ export const saveLastLocalAuthenticationSession = async (server, serverRecord) = }); }; -export const resetAttempts = () => AsyncStorage.multiRemove([LOCKED_OUT_TIMER_KEY, ATTEMPTS_KEY]); +export const resetAttempts = (): Promise => AsyncStorage.multiRemove([LOCKED_OUT_TIMER_KEY, ATTEMPTS_KEY]); -const openModal = hasBiometry => - new Promise(resolve => { +const openModal = (hasBiometry: boolean) => + new Promise(resolve => { EventEmitter.emit(LOCAL_AUTHENTICATE_EMITTER, { submit: () => resolve(), hasBiometry }); }); -const openChangePasscodeModal = ({ force }) => - new Promise((resolve, reject) => { +const openChangePasscodeModal = ({ force }: { force: boolean }) => + new Promise((resolve, reject) => { EventEmitter.emit(CHANGE_PASSCODE_EMITTER, { - submit: passcode => resolve(passcode), + submit: (passcode: string) => resolve(passcode), cancel: () => reject(), force }); }); -export const changePasscode = async ({ force = false }) => { +export const changePasscode = async ({ force = false }: { force: boolean }): Promise => { const passcode = await openChangePasscodeModal({ force }); await UserPreferences.setStringAsync(PASSCODE_KEY, sha256(passcode)); }; -export const biometryAuth = force => +export const biometryAuth = (force?: boolean): Promise => LocalAuthentication.authenticateAsync({ disableDeviceFallback: true, cancelLabel: force ? I18n.t('Dont_activate') : I18n.t('Local_authentication_biometry_fallback'), @@ -71,11 +72,11 @@ export const biometryAuth = force => * It'll help us to get the permission to use FaceID * and enable/disable the biometry when user put their first passcode */ -const checkBiometry = async serverRecord => { +const checkBiometry = async (serverRecord: TServerModel) => { const serversDB = database.servers; const result = await biometryAuth(true); - await serversDB.action(async () => { + await serversDB.write(async () => { try { await serverRecord.update(record => { record.biometry = !!result?.success; @@ -86,7 +87,13 @@ const checkBiometry = async serverRecord => { }); }; -export const checkHasPasscode = async ({ force = true, serverRecord }) => { +export const checkHasPasscode = async ({ + force = true, + serverRecord +}: { + force?: boolean; + serverRecord: TServerModel; +}): Promise<{ newPasscode?: boolean } | void> => { const storedPasscode = await UserPreferences.getStringAsync(PASSCODE_KEY); if (!storedPasscode) { await changePasscode({ force }); @@ -96,13 +103,13 @@ export const checkHasPasscode = async ({ force = true, serverRecord }) => { return Promise.resolve(); }; -export const localAuthenticate = async server => { +export const localAuthenticate = async (server: string): Promise => { const serversDB = database.servers; const serversCollection = serversDB.get('servers'); - let serverRecord; + let serverRecord: TServerModel; try { - serverRecord = await serversCollection.find(server); + serverRecord = (await serversCollection.find(server)) as TServerModel; } catch (error) { return Promise.reject(); } @@ -125,7 +132,7 @@ export const localAuthenticate = async server => { const diffToLastSession = moment().diff(serverRecord?.lastLocalAuthenticatedSession, 'seconds'); // if last authenticated session is older than configured auto lock time, authentication is required - if (diffToLastSession >= serverRecord?.autoLockTime) { + if (diffToLastSession >= serverRecord.autoLockTime!) { // set isLocalAuthenticated to false store.dispatch(setLocalAuthenticated(false)); @@ -150,7 +157,7 @@ export const localAuthenticate = async server => { } }; -export const supportedBiometryLabel = async () => { +export const supportedBiometryLabel = async (): Promise => { try { const enrolled = await LocalAuthentication.isEnrolledAsync(); diff --git a/app/utils/log/events.js b/app/utils/log/events.ts similarity index 99% rename from app/utils/log/events.js rename to app/utils/log/events.ts index fc7a3497..82dd3079 100644 --- a/app/utils/log/events.js +++ b/app/utils/log/events.ts @@ -253,7 +253,6 @@ export default { RA_GO_AUTOTRANSLATE: 'ra_go_autotranslate', RA_GO_NOTIFICATIONPREF: 'ra_go_notification_pref', RA_GO_FORWARDLIVECHAT: 'ra_go_forward_livechat', - RA_GO_VISITORNAVIGATION: 'ra_go_visitor_navigation', RA_SHARE: 'ra_share', RA_LEAVE: 'ra_leave', RA_LEAVE_F: 'ra_leave_f', diff --git a/app/utils/log/index.js b/app/utils/log/index.ts similarity index 66% rename from app/utils/log/index.js rename to app/utils/log/index.ts index 41074832..d52bd0e9 100644 --- a/app/utils/log/index.js +++ b/app/utils/log/index.ts @@ -4,13 +4,13 @@ import { isFDroidBuild } from '../../constants/environment'; import events from './events'; const analytics = firebaseAnalytics || ''; -let bugsnag = ''; -let crashlytics; +let bugsnag: any = ''; +let crashlytics: any; let reportCrashErrors = true; let reportAnalyticsEvents = true; -export const getReportCrashErrorsValue = () => reportCrashErrors; -export const getReportAnalyticsEventsValue = () => reportAnalyticsEvents; +export const getReportCrashErrorsValue = (): boolean => reportCrashErrors; +export const getReportAnalyticsEventsValue = (): boolean => reportAnalyticsEvents; if (!isFDroidBuild) { bugsnag = require('@bugsnag/react-native').default; @@ -18,7 +18,7 @@ if (!isFDroidBuild) { onBreadcrumb() { return reportAnalyticsEvents; }, - onError(error) { + onError(error: { breadcrumbs: string[] }) { if (!reportAnalyticsEvents) { error.breadcrumbs = []; } @@ -34,13 +34,13 @@ export { events }; let metadata = {}; -export const logServerVersion = serverVersion => { +export const logServerVersion = (serverVersion: string): void => { metadata = { serverVersion }; }; -export const logEvent = (eventName, payload) => { +export const logEvent = (eventName: string, payload?: { [key: string]: any }): void => { try { if (!isFDroidBuild) { analytics().logEvent(eventName, payload); @@ -51,26 +51,26 @@ export const logEvent = (eventName, payload) => { } }; -export const setCurrentScreen = currentScreen => { +export const setCurrentScreen = (currentScreen: string): void => { if (!isFDroidBuild) { analytics().setCurrentScreen(currentScreen); bugsnag.leaveBreadcrumb(currentScreen, { type: 'navigation' }); } }; -export const toggleCrashErrorsReport = value => { +export const toggleCrashErrorsReport = (value: boolean): boolean => { crashlytics().setCrashlyticsCollectionEnabled(value); return (reportCrashErrors = value); }; -export const toggleAnalyticsEventsReport = value => { +export const toggleAnalyticsEventsReport = (value: boolean): boolean => { analytics().setAnalyticsCollectionEnabled(value); return (reportAnalyticsEvents = value); }; -export default e => { +export default (e: any): void => { if (e instanceof Error && bugsnag && e.message !== 'Aborted' && !__DEV__) { - bugsnag.notify(e, event => { + bugsnag.notify(e, (event: { addMetadata: (arg0: string, arg1: {}) => void }) => { event.addMetadata('details', { ...metadata }); }); if (!isFDroidBuild) { diff --git a/app/utils/media.js b/app/utils/media.ts similarity index 55% rename from app/utils/media.js rename to app/utils/media.ts index b05f95a9..78b1c29f 100644 --- a/app/utils/media.js +++ b/app/utils/media.ts @@ -1,20 +1,30 @@ -export const canUploadFile = (file, allowList, maxFileSize) => { +import { IAttachment } from '../views/ShareView/interfaces'; + +export const canUploadFile = ( + file: IAttachment, + allowList: string, + maxFileSize: number, + permissionToUploadFile: boolean +): { success: boolean; error?: string } => { if (!(file && file.path)) { return { success: true }; } if (maxFileSize > -1 && file.size > maxFileSize) { return { success: false, error: 'error-file-too-large' }; } + if (!permissionToUploadFile) { + return { success: false, error: 'error-not-permission-to-upload-file' }; + } // if white list is empty, all media types are enabled if (!allowList || allowList === '*') { return { success: true }; } const allowedMime = allowList.split(','); - if (allowedMime.includes(file.mime)) { + if (allowedMime.includes(file.mime!)) { return { success: true }; } const wildCardGlob = '/*'; - const wildCards = allowedMime.filter(item => item.indexOf(wildCardGlob) > 0); + const wildCards = allowedMime.filter((item: string) => item.indexOf(wildCardGlob) > 0); if (file.mime && wildCards.includes(file.mime.replace(/(\/.*)$/, wildCardGlob))) { return { success: true }; } diff --git a/app/utils/messageTypes.js b/app/utils/messageTypes.ts similarity index 100% rename from app/utils/messageTypes.js rename to app/utils/messageTypes.ts diff --git a/app/utils/moment.js b/app/utils/moment.ts similarity index 57% rename from app/utils/moment.js rename to app/utils/moment.ts index 064b0f7f..3379429c 100644 --- a/app/utils/moment.js +++ b/app/utils/moment.ts @@ -1,4 +1,4 @@ -const localeKeys = { +const localeKeys: { [key: string]: string } = { en: 'en', ru: 'ru', 'pt-BR': 'pt-br', @@ -13,4 +13,4 @@ const localeKeys = { 'zh-TW': 'zh-tw' }; -export const toMomentLocale = locale => localeKeys[locale]; +export const toMomentLocale = (locale: string): string => localeKeys[locale]; diff --git a/app/utils/navigation/animations.js b/app/utils/navigation/animations.ts similarity index 71% rename from app/utils/navigation/animations.js rename to app/utils/navigation/animations.ts index 9f99764c..a9f18408 100644 --- a/app/utils/navigation/animations.js +++ b/app/utils/navigation/animations.ts @@ -1,12 +1,14 @@ import { Animated, Easing } from 'react-native'; -import { HeaderStyleInterpolators, TransitionPresets } from '@react-navigation/stack'; +import { HeaderStyleInterpolators, TransitionPreset, TransitionPresets } from '@react-navigation/stack'; +// eslint-disable-next-line import/no-unresolved +import { StackCardStyleInterpolator, TransitionSpec } from '@react-navigation/stack/lib/typescript/src/types'; import { isAndroid } from '../deviceInfo'; import conditional from './conditional'; const { multiply } = Animated; -const forFadeFromCenter = ({ current, closing }) => { +const forFadeFromCenter: StackCardStyleInterpolator = ({ current, closing }) => { const opacity = conditional( closing, current.progress, @@ -23,7 +25,7 @@ const forFadeFromCenter = ({ current, closing }) => { }; }; -const FadeIn = { +const FadeIn: TransitionSpec = { animation: 'timing', config: { duration: 250, @@ -31,7 +33,7 @@ const FadeIn = { } }; -const FadeOut = { +const FadeOut: TransitionSpec = { animation: 'timing', config: { duration: 150, @@ -48,7 +50,7 @@ export const FadeFromCenterModal = { cardStyleInterpolator: forFadeFromCenter }; -const forStackAndroid = ({ current, inverted, layouts: { screen } }) => { +const forStackAndroid: StackCardStyleInterpolator = ({ current, inverted, layouts: { screen } }) => { const translateX = multiply( current.progress.interpolate({ inputRange: [0, 1], @@ -70,7 +72,7 @@ const forStackAndroid = ({ current, inverted, layouts: { screen } }) => { }; }; -const StackAndroid = { +const StackAndroid: TransitionPreset = { gestureDirection: 'horizontal', transitionSpec: { open: FadeIn, diff --git a/app/utils/navigation/conditional.js b/app/utils/navigation/conditional.ts similarity index 87% rename from app/utils/navigation/conditional.js rename to app/utils/navigation/conditional.ts index 015c52ae..84c76d83 100644 --- a/app/utils/navigation/conditional.js +++ b/app/utils/navigation/conditional.ts @@ -10,7 +10,11 @@ const { add, multiply } = Animated; * @param main Animated Node to use if the condition is `true` * @param fallback Animated Node to use if the condition is `false` */ -export default function conditional(condition, main, fallback) { +export default function conditional( + condition: Animated.AnimatedInterpolation, + main: Animated.Animated, + fallback: Animated.Animated +): Animated.AnimatedAddition { // To implement this behavior, we multiply the main node with the condition. // So if condition is 0, result will be 0, and if condition is 1, result will be main node. // Then we multiple reverse of the condition (0 if condition is 1) with the fallback. diff --git a/app/utils/openLink.js b/app/utils/openLink.ts similarity index 83% rename from app/utils/openLink.js rename to app/utils/openLink.ts index 92df16a7..4048b3ad 100644 --- a/app/utils/openLink.js +++ b/app/utils/openLink.ts @@ -14,7 +14,7 @@ const scheme = { brave: 'brave:' }; -const appSchemeURL = (url, browser) => { +const appSchemeURL = (url: string, browser: string): string => { let schemeUrl = url; const parsedUrl = parse(url, true); const { protocol } = parsedUrl; @@ -35,7 +35,7 @@ const appSchemeURL = (url, browser) => { return schemeUrl; }; -const openLink = async (url, theme = 'light') => { +const openLink = async (url: string, theme = 'light'): Promise => { try { const browser = await UserPreferences.getStringAsync(DEFAULT_BROWSER_KEY); @@ -43,11 +43,12 @@ const openLink = async (url, theme = 'light') => { await WebBrowser.openBrowserAsync(url, { toolbarColor: themes[theme].headerBackground, controlsColor: themes[theme].headerTintColor, - collapseToolbar: true, + // https://github.com/expo/expo/pull/4923 + enableBarCollapsing: true, showTitle: true }); } else { - const schemeUrl = appSchemeURL(url, browser.replace(':', '')); + const schemeUrl = appSchemeURL(url, browser!.replace(':', '')); await Linking.openURL(schemeUrl); } } catch { diff --git a/app/utils/random.js b/app/utils/random.ts similarity index 80% rename from app/utils/random.js rename to app/utils/random.ts index 8f6adb88..2d2cd178 100644 --- a/app/utils/random.js +++ b/app/utils/random.ts @@ -1,4 +1,4 @@ -export default function random(length) { +export default function random(length: number): string { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < length; i += 1) { diff --git a/app/utils/review.js b/app/utils/review.ts similarity index 96% rename from app/utils/review.js rename to app/utils/review.ts index 15f1cf96..bbbb498b 100644 --- a/app/utils/review.js +++ b/app/utils/review.ts @@ -15,7 +15,7 @@ const reviewDelay = 2000; const numberOfDays = 7; const numberOfPositiveEvent = 5; -const daysBetween = (date1, date2) => { +const daysBetween = (date1: Date, date2: Date): number => { const one_day = 1000 * 60 * 60 * 24; const date1_ms = date1.getTime(); const date2_ms = date2.getTime(); @@ -32,7 +32,7 @@ const onCancelPress = () => { } }; -export const onReviewPress = async () => { +export const onReviewPress = async (): Promise => { logEvent(events.SE_REVIEW_THIS_APP); await onCancelPress(); try { diff --git a/app/utils/room.js b/app/utils/room.js deleted file mode 100644 index 67df65a9..00000000 --- a/app/utils/room.js +++ /dev/null @@ -1,53 +0,0 @@ -import moment from 'moment'; - -import { themes } from '../constants/colors'; -import I18n from '../i18n'; - -export const isBlocked = room => { - if (room) { - const { t, blocked, blocker } = room; - if (t === 'd' && (blocked || blocker)) { - return true; - } - } - return false; -}; - -export const capitalize = s => { - if (typeof s !== 'string') { - return ''; - } - return s.charAt(0).toUpperCase() + s.slice(1); -}; - -export const formatDate = date => - moment(date).calendar(null, { - lastDay: `[${I18n.t('Yesterday')}]`, - sameDay: 'LT', - lastWeek: 'dddd', - sameElse: 'L' - }); - -export const formatDateThreads = date => - moment(date).calendar(null, { - sameDay: 'LT', - lastDay: `[${I18n.t('Yesterday')}] LT`, - lastWeek: 'dddd LT', - sameElse: 'LL' - }); - -export const getBadgeColor = ({ subscription, messageId, theme }) => { - if (subscription?.tunreadUser?.includes(messageId)) { - return themes[theme].mentionMeColor; - } - if (subscription?.tunreadGroup?.includes(messageId)) { - return themes[theme].mentionGroupColor; - } - if (subscription?.tunread?.includes(messageId)) { - return themes[theme].tunreadColor; - } -}; - -export const makeThreadName = messageRecord => messageRecord.msg || messageRecord?.attachments[0]?.title; - -export const isTeamRoom = ({ teamId, joined }) => teamId && joined; diff --git a/app/utils/room.ts b/app/utils/room.ts new file mode 100644 index 00000000..3e4e0ef4 --- /dev/null +++ b/app/utils/room.ts @@ -0,0 +1,65 @@ +import moment from 'moment'; + +import { themes } from '../constants/colors'; +import I18n from '../i18n'; +import { IAttachment } from '../definitions/IAttachment'; +import { ISubscription, SubscriptionType } from '../definitions/ISubscription'; + +export const isBlocked = (room: ISubscription): boolean => { + if (room) { + const { t, blocked, blocker } = room; + if (t === SubscriptionType.DIRECT && (blocked || blocker)) { + return true; + } + } + return false; +}; + +export const capitalize = (s: string): string => { + if (typeof s !== 'string') { + return ''; + } + return s.charAt(0).toUpperCase() + s.slice(1); +}; + +export const formatDate = (date: Date): string => + moment(date).calendar(null, { + lastDay: `[${I18n.t('Yesterday')}]`, + sameDay: 'LT', + lastWeek: 'dddd', + sameElse: 'L' + }); + +export const formatDateThreads = (date: Date): string => + moment(date).calendar(null, { + sameDay: 'LT', + lastDay: `[${I18n.t('Yesterday')}] LT`, + lastWeek: 'dddd LT', + sameElse: 'LL' + }); + +export const getBadgeColor = ({ + subscription, + messageId, + theme +}: { + // TODO: Refactor when migrate model folder + subscription: any; + messageId: string; + theme: string; +}): string | undefined => { + if (subscription?.tunreadUser?.includes(messageId)) { + return themes[theme].mentionMeColor; + } + if (subscription?.tunreadGroup?.includes(messageId)) { + return themes[theme].mentionGroupColor; + } + if (subscription?.tunread?.includes(messageId)) { + return themes[theme].tunreadColor; + } +}; + +export const makeThreadName = (messageRecord: { id?: string; msg?: string; attachments?: IAttachment[] }): string | undefined => + messageRecord.msg || messageRecord.attachments![0].title; + +export const isTeamRoom = ({ teamId, joined }: { teamId: string; joined: boolean }): boolean => !!teamId && joined; diff --git a/app/utils/server.js b/app/utils/server.ts similarity index 84% rename from app/utils/server.js rename to app/utils/server.ts index e7be96b3..52064757 100644 --- a/app/utils/server.js +++ b/app/utils/server.ts @@ -3,7 +3,7 @@ url = 'https://open.rocket.chat/method' hostname = 'open.rocket.chat' */ -export const extractHostname = url => { +export const extractHostname = (url: string): string => { let hostname; if (url.indexOf('//') > -1) { diff --git a/app/utils/shortnameToUnicode/ascii.js b/app/utils/shortnameToUnicode/ascii.ts similarity index 98% rename from app/utils/shortnameToUnicode/ascii.js rename to app/utils/shortnameToUnicode/ascii.ts index 4d9d04cd..7eca5f7d 100644 --- a/app/utils/shortnameToUnicode/ascii.js +++ b/app/utils/shortnameToUnicode/ascii.ts @@ -3,7 +3,7 @@ /* eslint-disable object-curly-spacing */ /* eslint-disable comma-spacing */ /* eslint-disable key-spacing */ -const ascii = { +const ascii: { [key: string]: string } = { '*\\0/*': '🙆', '*\\O/*': '🙆', '-___-': '😑', diff --git a/app/utils/shortnameToUnicode/emojis.js b/app/utils/shortnameToUnicode/emojis.ts similarity index 99% rename from app/utils/shortnameToUnicode/emojis.js rename to app/utils/shortnameToUnicode/emojis.ts index 14cd6133..6a8a63c3 100644 --- a/app/utils/shortnameToUnicode/emojis.js +++ b/app/utils/shortnameToUnicode/emojis.ts @@ -3,7 +3,7 @@ /* eslint-disable object-curly-spacing */ /* eslint-disable comma-spacing */ /* eslint-disable key-spacing */ -const emojis = { +const emojis: { [key: string]: string } = { ':england:': '🏴', ':scotland:': '🏴', ':wales:': '🏴', diff --git a/app/utils/shortnameToUnicode/index.js b/app/utils/shortnameToUnicode/index.ts similarity index 80% rename from app/utils/shortnameToUnicode/index.js rename to app/utils/shortnameToUnicode/index.ts index 0a54aa3a..b533da8f 100644 --- a/app/utils/shortnameToUnicode/index.js +++ b/app/utils/shortnameToUnicode/index.ts @@ -2,11 +2,11 @@ import emojis from './emojis'; import ascii, { asciiRegexp } from './ascii'; const shortnamePattern = new RegExp(/:[-+_a-z0-9]+:/, 'gi'); -const replaceShortNameWithUnicode = shortname => emojis[shortname] || shortname; +const replaceShortNameWithUnicode = (shortname: string) => emojis[shortname] || shortname; const regAscii = new RegExp(`((\\s|^)${asciiRegexp}(?=\\s|$|[!,.?]))`, 'gi'); -const unescapeHTML = string => { - const unescaped = { +const unescapeHTML = (string: string) => { + const unescaped: { [key: string]: string } = { '&': '&', '&': '&', '&': '&', @@ -27,7 +27,7 @@ const unescapeHTML = string => { return string.replace(/&(?:amp|#38|#x26|lt|#60|#x3C|gt|#62|#x3E|apos|#39|#x27|quot|#34|#x22);/gi, match => unescaped[match]); }; -const shortnameToUnicode = str => { +const shortnameToUnicode = (str: string): string => { str = str.replace(shortnamePattern, replaceShortNameWithUnicode); str = str.replace(regAscii, (entire, m1, m2, m3) => { diff --git a/app/utils/sslPinning.js b/app/utils/sslPinning.ts similarity index 54% rename from app/utils/sslPinning.js rename to app/utils/sslPinning.ts index 50f944e6..42245c98 100644 --- a/app/utils/sslPinning.js +++ b/app/utils/sslPinning.ts @@ -7,6 +7,26 @@ import I18n from '../i18n'; import { extractHostname } from './server'; const { SSLPinning } = NativeModules; +const { documentDirectory } = FileSystem; + +const extractFileScheme = (path: string) => path.replace('file://', ''); // file:// isn't allowed by obj-C + +const getPath = (name: string) => `${documentDirectory}/${name}`; + +interface ICertificate { + path: string; + password: string; +} + +const persistCertificate = async (name: string, password: string) => { + const certificatePath = getPath(name); + const certificate: ICertificate = { + path: extractFileScheme(certificatePath), + password + }; + await UserPreferences.setMapAsync(name, certificate); + return certificate; +}; const RCSSLPinning = Platform.select({ ios: { @@ -14,6 +34,7 @@ const RCSSLPinning = Platform.select({ new Promise(async (resolve, reject) => { try { const res = await DocumentPicker.pick({ + // @ts-ignore type: ['com.rsa.pkcs-12'] }); const { uri, name } = res; @@ -25,17 +46,9 @@ const RCSSLPinning = Platform.select({ text: 'OK', onPress: async password => { try { - const certificatePath = `${FileSystem.documentDirectory}/${name}`; - + const certificatePath = getPath(name); await FileSystem.copyAsync({ from: uri, to: certificatePath }); - - const certificate = { - path: certificatePath.replace('file://', ''), // file:// isn't allowed by obj-C - password - }; - - await UserPreferences.setMapAsync(name, certificate); - + await persistCertificate(name, password!); resolve(name); } catch (e) { reject(e); @@ -49,16 +62,19 @@ const RCSSLPinning = Platform.select({ reject(e); } }), - setCertificate: async (alias, server) => { - if (alias) { - const certificate = await UserPreferences.getMapAsync(alias); + setCertificate: async (name: string, server: string) => { + if (name) { + let certificate = (await UserPreferences.getMapAsync(name)) as ICertificate; + if (!certificate.path.match(extractFileScheme(documentDirectory!))) { + certificate = await persistCertificate(name, certificate.password); + } await UserPreferences.setMapAsync(extractHostname(server), certificate); } } }, android: { pickCertificate: () => SSLPinning?.pickCertificate(), - setCertificate: alias => SSLPinning?.setCertificate(alias) + setCertificate: name => SSLPinning?.setCertificate(name) } }); diff --git a/app/utils/theme.js b/app/utils/theme.ts similarity index 71% rename from app/utils/theme.js rename to app/utils/theme.ts index c9038941..0e9d8e05 100644 --- a/app/utils/theme.js +++ b/app/utils/theme.ts @@ -2,12 +2,13 @@ import { Appearance } from 'react-native-appearance'; import changeNavigationBarColor from 'react-native-navigation-bar-color'; import setRootViewColor from 'rn-root-view'; +import { IThemePreference, TThemeMode } from '../definitions/ITheme'; import { themes } from '../constants/colors'; import { isAndroid } from './deviceInfo'; -let themeListener; +let themeListener: { remove: () => void } | null; -export const defaultTheme = () => { +export const defaultTheme = (): TThemeMode => { const systemTheme = Appearance.getColorScheme(); if (systemTheme && systemTheme !== 'no-preference') { return systemTheme; @@ -15,7 +16,7 @@ export const defaultTheme = () => { return 'light'; }; -export const getTheme = themePreferences => { +export const getTheme = (themePreferences: IThemePreference): string => { const { darkLevel, currentTheme } = themePreferences; let theme = currentTheme; if (currentTheme === 'automatic') { @@ -24,7 +25,7 @@ export const getTheme = themePreferences => { return theme === 'dark' ? darkLevel : 'light'; }; -export const newThemeState = (prevState, newTheme) => { +export const newThemeState = (prevState: { themePreferences: IThemePreference }, newTheme: IThemePreference) => { // new theme preferences const themePreferences = { ...prevState.themePreferences, @@ -35,12 +36,13 @@ export const newThemeState = (prevState, newTheme) => { return { themePreferences, theme: getTheme(themePreferences) }; }; -export const setNativeTheme = async themePreferences => { +export const setNativeTheme = async (themePreferences: IThemePreference): Promise => { const theme = getTheme(themePreferences); if (isAndroid) { const iconsLight = theme === 'light'; try { - await changeNavigationBarColor(themes[theme].navbarBackground, iconsLight); + // The late param as default is true @ react-native-navigation-bar-color/src/index.js line 8 + await changeNavigationBarColor(themes[theme].navbarBackground, iconsLight, true); } catch (error) { // Do nothing } @@ -55,7 +57,7 @@ export const unsubscribeTheme = () => { } }; -export const subscribeTheme = (themePreferences, setTheme) => { +export const subscribeTheme = (themePreferences: IThemePreference, setTheme: () => void): void => { const { currentTheme } = themePreferences; if (!themeListener && currentTheme === 'automatic') { // not use listener params because we use getTheme diff --git a/app/utils/throttle.js b/app/utils/throttle.js deleted file mode 100644 index 88751335..00000000 --- a/app/utils/throttle.js +++ /dev/null @@ -1,26 +0,0 @@ -export default function throttle(fn, threshhold = 250, scope) { - let last; - let deferTimer; - - const _throttle = (...args) => { - const context = scope || this; - - const now = +new Date(); - - if (last && now < last + threshhold) { - // hold on to it - clearTimeout(deferTimer); - deferTimer = setTimeout(() => { - last = now; - fn.apply(context, args); - }, threshhold); - } else { - last = now; - fn.apply(context, args); - } - }; - - _throttle.stop = () => clearTimeout(deferTimer); - - return _throttle; -} diff --git a/app/utils/touch.js b/app/utils/touch.tsx similarity index 55% rename from app/utils/touch.js rename to app/utils/touch.tsx index 0bfece04..3573c87c 100644 --- a/app/utils/touch.js +++ b/app/utils/touch.tsx @@ -1,19 +1,27 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { RectButton } from 'react-native-gesture-handler'; +import { RectButton, RectButtonProps } from 'react-native-gesture-handler'; import { themes } from '../constants/colors'; -class Touch extends React.Component { - setNativeProps(props) { +interface ITouchProps extends RectButtonProps { + children: React.ReactNode; + theme: string; + accessibilityLabel?: string; + testID?: string; +} + +class Touch extends React.Component { + private ref: any; + + setNativeProps(props: ITouchProps): void { this.ref.setNativeProps(props); } - getRef = ref => { + getRef = (ref: RectButton): void => { this.ref = ref; }; - render() { + render(): JSX.Element { const { children, onPress, theme, underlayColor, ...props } = this.props; return ( @@ -30,11 +38,4 @@ class Touch extends React.Component { } } -Touch.propTypes = { - children: PropTypes.node, - onPress: PropTypes.func, - theme: PropTypes.string, - underlayColor: PropTypes.string -}; - export default Touch; diff --git a/app/utils/twoFactor.js b/app/utils/twoFactor.ts similarity index 68% rename from app/utils/twoFactor.js rename to app/utils/twoFactor.ts index 6f2fa9c9..a52ff93f 100644 --- a/app/utils/twoFactor.js +++ b/app/utils/twoFactor.ts @@ -3,13 +3,18 @@ import { settings } from '@rocket.chat/sdk'; import { TWO_FACTOR } from '../containers/TwoFactor'; import EventEmitter from './events'; -export const twoFactor = ({ method, invalid }) => +interface ITwoFactor { + method: string; + invalid: boolean; +} + +export const twoFactor = ({ method, invalid }: ITwoFactor): Promise<{ twoFactorCode: string; twoFactorMethod: string }> => new Promise((resolve, reject) => { EventEmitter.emit(TWO_FACTOR, { method, invalid, cancel: () => reject(), - submit: code => { + submit: (code: string) => { settings.customHeaders = { ...settings.customHeaders, 'x-2fa-code': code, diff --git a/app/utils/url.js b/app/utils/url.ts similarity index 77% rename from app/utils/url.js rename to app/utils/url.ts index 623524d7..50179597 100644 --- a/app/utils/url.js +++ b/app/utils/url.ts @@ -1,4 +1,4 @@ -export const isValidURL = url => { +export const isValidURL = (url: string): boolean => { const pattern = new RegExp( '^(https?:\\/\\/)?' + // protocol '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name @@ -12,4 +12,4 @@ export const isValidURL = url => { }; // Use useSsl: false only if server url starts with http:// -export const useSsl = url => !/http:\/\//.test(url); +export const useSsl = (url: string): boolean => !/http:\/\//.test(url); diff --git a/app/views/AddChannelTeamView.tsx b/app/views/AddChannelTeamView.tsx index d477f9ba..8a72d3c9 100644 --- a/app/views/AddChannelTeamView.tsx +++ b/app/views/AddChannelTeamView.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { RouteProp } from '@react-navigation/native'; import { connect } from 'react-redux'; +import { CompositeNavigationProp } from '@react-navigation/core'; import * as List from '../containers/List'; import StatusBar from '../containers/StatusBar'; @@ -9,16 +10,24 @@ import { useTheme } from '../theme'; import * as HeaderButton from '../containers/HeaderButton'; import SafeAreaView from '../containers/SafeAreaView'; import I18n from '../i18n'; - -type TNavigation = StackNavigationProp; +import { ChatsStackParamList, DrawerParamList, NewMessageStackParamList } from '../stacks/types'; interface IAddChannelTeamView { - route: RouteProp<{ AddChannelTeamView: { teamId: string; teamChannels: object[] } }, 'AddChannelTeamView'>; - navigation: TNavigation; + navigation: CompositeNavigationProp< + StackNavigationProp, + CompositeNavigationProp, StackNavigationProp> + >; + route: RouteProp; isMasterDetail: boolean; } -const setHeader = (navigation: TNavigation, isMasterDetail: boolean) => { +const setHeader = ({ + navigation, + isMasterDetail +}: { + navigation: StackNavigationProp; + isMasterDetail: boolean; +}) => { const options: StackNavigationOptions = { headerTitle: I18n.t('Add_Channel_to_Team') }; @@ -35,7 +44,7 @@ const AddChannelTeamView = ({ navigation, route, isMasterDetail }: IAddChannelTe const { theme } = useTheme(); useEffect(() => { - setHeader(navigation, isMasterDetail); + setHeader({ navigation, isMasterDetail }); }, []); return ( diff --git a/app/views/AddExistingChannelView.tsx b/app/views/AddExistingChannelView.tsx index 5efdbf34..86ab9b9c 100644 --- a/app/views/AddExistingChannelView.tsx +++ b/app/views/AddExistingChannelView.tsx @@ -21,6 +21,7 @@ import { animateNextTransition } from '../utils/layoutAnimation'; import { goRoom } from '../utils/goRoom'; import { showErrorAlert } from '../utils/info'; import debounce from '../utils/debounce'; +import { ChatsStackParamList } from '../stacks/types'; interface IAddExistingChannelViewState { // TODO: refactor with Room Model @@ -31,8 +32,8 @@ interface IAddExistingChannelViewState { } interface IAddExistingChannelViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ AddExistingChannelView: { teamId: string } }, 'AddExistingChannelView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; isMasterDetail: boolean; addTeamChannelPermission: string[]; @@ -41,7 +42,7 @@ interface IAddExistingChannelViewProps { const QUERY_SIZE = 50; class AddExistingChannelView extends React.Component { - private teamId: string; + private teamId?: string; constructor(props: IAddExistingChannelViewProps) { super(props); this.query(); diff --git a/app/views/AdminPanelView/index.tsx b/app/views/AdminPanelView/index.tsx index 80f728e1..f0af5dfa 100644 --- a/app/views/AdminPanelView/index.tsx +++ b/app/views/AdminPanelView/index.tsx @@ -9,6 +9,7 @@ import * as HeaderButton from '../../containers/HeaderButton'; import { withTheme } from '../../theme'; import { getUserSelector } from '../../selectors/login'; import SafeAreaView from '../../containers/SafeAreaView'; +import { AdminPanelStackParamList } from '../../stacks/types'; interface IAdminPanelViewProps { baseUrl: string; @@ -16,7 +17,7 @@ interface IAdminPanelViewProps { } interface INavigationOptions { - navigation: DrawerScreenProps; + navigation: DrawerScreenProps; isMasterDetail: boolean; } diff --git a/app/views/AttachmentView.tsx b/app/views/AttachmentView.tsx index 90adf8b4..d0bd021c 100644 --- a/app/views/AttachmentView.tsx +++ b/app/views/AttachmentView.tsx @@ -24,6 +24,8 @@ import { getUserSelector } from '../selectors/login'; import { withDimensions } from '../dimensions'; import { getHeaderHeight } from '../containers/Header'; import StatusBar from '../containers/StatusBar'; +import { InsideStackParamList } from '../stacks/types'; +import { IAttachment } from '../definitions/IAttachment'; const styles = StyleSheet.create({ container: { @@ -31,24 +33,14 @@ const styles = StyleSheet.create({ } }); -// TODO: refactor when react-navigation is done -export interface IAttachment { - title: string; - title_link?: string; - image_url?: string; - image_type?: string; - video_url?: string; - video_type?: string; -} - interface IAttachmentViewState { attachment: IAttachment; loading: boolean; } interface IAttachmentViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ AttachmentView: { attachment: IAttachment } }, 'AttachmentView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; baseUrl: string; width: number; @@ -131,7 +123,11 @@ class AttachmentView extends React.Component; - route: IRoute; + navigation: StackNavigationProp; + route: RouteProp; } interface IAuthenticationWebView extends INavigationOption { diff --git a/app/views/AutoTranslateView/index.tsx b/app/views/AutoTranslateView/index.tsx index 92a77543..95442689 100644 --- a/app/views/AutoTranslateView/index.tsx +++ b/app/views/AutoTranslateView/index.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { FlatList, StyleSheet, Switch } from 'react-native'; +import { RouteProp } from '@react-navigation/core'; +import { ChatsStackParamList } from '../../stacks/types'; import RocketChat from '../../lib/rocketchat'; import I18n from '../../i18n'; import StatusBar from '../../containers/StatusBar'; @@ -9,6 +11,7 @@ import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; import { withTheme } from '../../theme'; import SafeAreaView from '../../containers/SafeAreaView'; import { events, logEvent } from '../../utils/log'; +import { ISubscription } from '../../definitions/ISubscription'; const styles = StyleSheet.create({ list: { @@ -16,19 +19,8 @@ const styles = StyleSheet.create({ } }); -interface IRoom { - observe: Function; - autoTranslateLanguage: boolean; - autoTranslate: boolean; -} - interface IAutoTranslateViewProps { - route: { - params: { - rid?: string; - room?: IRoom; - }; - }; + route: RouteProp; theme: string; } @@ -50,7 +42,7 @@ class AutoTranslateView extends React.Component { if (room && room.observe) { this.roomObservable = room.observe(); - this.subscription = this.roomObservable.subscribe((changes: IRoom) => { + this.subscription = this.roomObservable.subscribe((changes: ISubscription) => { if (this.mounted) { const { selectedLanguage, enableAutoTranslate } = this.state; if (selectedLanguage !== changes.autoTranslateLanguage) { diff --git a/app/views/CreateChannelView.tsx b/app/views/CreateChannelView.tsx index 45b2cc2f..e8d719ab 100644 --- a/app/views/CreateChannelView.tsx +++ b/app/views/CreateChannelView.tsx @@ -25,6 +25,7 @@ import { events, logEvent } from '../utils/log'; import SafeAreaView from '../containers/SafeAreaView'; import RocketChat from '../lib/rocketchat'; import sharedStyles from './Styles'; +import { ChatsStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -91,8 +92,8 @@ interface ICreateChannelViewState { } interface ICreateChannelViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ CreateChannelView: { isTeam: boolean; teamId: string } }, 'CreateChannelView'>; + navigation: StackNavigationProp; + route: RouteProp; baseUrl: string; create: (data: ICreateFunction) => void; removeUser: (user: IOtherUser) => void; @@ -118,7 +119,7 @@ interface ISwitch extends SwitchProps { } class CreateChannelView extends React.Component { - private teamId: string; + private teamId?: string; constructor(props: ICreateChannelViewProps) { super(props); @@ -240,7 +241,7 @@ class CreateChannelView extends React.Component - // TODO: remove this ts-ignore when migrate the file: app/utils/avatar.js - // @ts-ignore avatarURL({ text: RocketChat.getRoomAvatar(item), type: item.t, diff --git a/app/views/CreateDiscussionView/SelectUsers.tsx b/app/views/CreateDiscussionView/SelectUsers.tsx index 65a4e0a4..d63c5ae6 100644 --- a/app/views/CreateDiscussionView/SelectUsers.tsx +++ b/app/views/CreateDiscussionView/SelectUsers.tsx @@ -12,6 +12,7 @@ import { MultiSelect } from '../../containers/UIKit/MultiSelect'; import { themes } from '../../constants/colors'; import styles from './styles'; import { ICreateDiscussionViewSelectUsers } from './interfaces'; +import { SubscriptionType } from '../../definitions/ISubscription'; interface IUser { name: string; @@ -62,11 +63,9 @@ const SelectUsers = ({ }, 300); const getAvatar = (item: any) => - // TODO: remove this ts-ignore when migrate the file: app/utils/avatar.js - // @ts-ignore avatarURL({ text: RocketChat.getRoomAvatar(item), - type: 'd', + type: SubscriptionType.DIRECT, user: { id: userId, token }, server, avatarETag: item.avatarETag, diff --git a/app/views/CreateDiscussionView/index.tsx b/app/views/CreateDiscussionView/index.tsx index 2933b2c7..53d741d2 100644 --- a/app/views/CreateDiscussionView/index.tsx +++ b/app/views/CreateDiscussionView/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { ScrollView, Switch, Text } from 'react-native'; +import { StackNavigationOptions } from '@react-navigation/stack'; import Loading from '../../containers/Loading'; import KeyboardView from '../../presentation/KeyboardView'; @@ -89,7 +90,7 @@ class CreateChannelView extends React.Component { ) : null, headerLeft: showCloseModal ? () => : undefined - }); + } as StackNavigationOptions); }; submit = () => { diff --git a/app/views/CreateDiscussionView/interfaces.ts b/app/views/CreateDiscussionView/interfaces.ts index 46883311..6009881c 100644 --- a/app/views/CreateDiscussionView/interfaces.ts +++ b/app/views/CreateDiscussionView/interfaces.ts @@ -1,14 +1,12 @@ +import { RouteProp } from '@react-navigation/core'; +import { StackNavigationProp } from '@react-navigation/stack'; + +import { NewMessageStackParamList } from '../../stacks/types'; +import { SubscriptionType } from '../../definitions/ISubscription'; + export interface ICreateChannelViewProps { - navigation: any; - route: { - params?: { - channel: string; - message: { - msg: string; - }; - showCloseModal: boolean; - }; - }; + navigation: StackNavigationProp; + route: RouteProp; server: string; user: { id: string; @@ -18,7 +16,7 @@ export interface ICreateChannelViewProps { loading: boolean; result: { rid: string; - t: string; + t: SubscriptionType; prid: string; }; failure: boolean; diff --git a/app/views/DefaultBrowserView.tsx b/app/views/DefaultBrowserView.tsx index 0282e0df..cfe977d2 100644 --- a/app/views/DefaultBrowserView.tsx +++ b/app/views/DefaultBrowserView.tsx @@ -107,7 +107,7 @@ class DefaultBrowserView extends React.Component { logEvent(events.DB_CHANGE_DEFAULT_BROWSER, { browser: newBrowser }); try { - const browser = newBrowser !== 'systemDefault:' ? newBrowser : null; + const browser = newBrowser || 'systemDefault:'; await UserPreferences.setStringAsync(DEFAULT_BROWSER_KEY, browser); this.setState({ browser }); } catch { diff --git a/app/views/DirectoryView/Options.tsx b/app/views/DirectoryView/Options.tsx index fcc0f7bf..11206806 100644 --- a/app/views/DirectoryView/Options.tsx +++ b/app/views/DirectoryView/Options.tsx @@ -63,7 +63,11 @@ export default class DirectoryOptions extends PureComponent changeType(itemType)} style={styles.dropdownItemButton} theme={theme}> + changeType(itemType)} + style={styles.dropdownItemButton} + theme={theme} + accessibilityLabel={I18n.t(text)}> {I18n.t(text)} @@ -90,7 +94,7 @@ export default class DirectoryOptions extends PureComponent - + ; baseUrl: string; isFederationEnabled: boolean; user: { diff --git a/app/views/DisplayPrefsView.js b/app/views/DisplayPrefsView.tsx similarity index 76% rename from app/views/DisplayPrefsView.js rename to app/views/DisplayPrefsView.tsx index 09da4edc..959682c4 100644 --- a/app/views/DisplayPrefsView.js +++ b/app/views/DisplayPrefsView.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; import { Switch } from 'react-native'; import { RadioButton } from 'react-native-ui-lib'; +import { StackNavigationProp } from '@react-navigation/stack'; import { useDispatch, useSelector } from 'react-redux'; import { setPreference } from '../actions/sortPreferences'; @@ -15,13 +15,30 @@ import * as HeaderButton from '../containers/HeaderButton'; import SafeAreaView from '../containers/SafeAreaView'; import { ICON_SIZE } from '../containers/List/constants'; import log, { events, logEvent } from '../utils/log'; -import { DISPLAY_MODE_CONDENSED, DISPLAY_MODE_EXPANDED } from '../constants/constantDisplayMode'; +import { DisplayMode, SortBy } from '../constants/constantDisplayMode'; +import { SettingsStackParamList } from '../stacks/types'; -const DisplayPrefsView = props => { +interface IParam { + sortBy: SortBy; + groupByType: boolean; + showFavorites: boolean; + showUnread: boolean; + showAvatar: boolean; + displayMode: DisplayMode; +} + +interface IDisplayPrefsView { + navigation: StackNavigationProp; + isMasterDetail: boolean; +} + +const DisplayPrefsView = (props: IDisplayPrefsView): JSX.Element => { const { theme } = useTheme(); - const { sortBy, groupByType, showFavorites, showUnread, showAvatar, displayMode } = useSelector(state => state.sortPreferences); - const { isMasterDetail } = useSelector(state => state.app); + const { sortBy, groupByType, showFavorites, showUnread, showAvatar, displayMode } = useSelector( + (state: any) => state.sortPreferences + ); + const { isMasterDetail } = useSelector((state: any) => state.app); const dispatch = useDispatch(); useEffect(() => { @@ -36,7 +53,7 @@ const DisplayPrefsView = props => { } }, []); - const setSortPreference = async param => { + const setSortPreference = async (param: Partial) => { try { dispatch(setPreference(param)); await RocketChat.saveSortPreference(param); @@ -47,12 +64,12 @@ const DisplayPrefsView = props => { const sortByName = async () => { logEvent(events.DP_SORT_CHANNELS_BY_NAME); - await setSortPreference({ sortBy: 'alphabetical' }); + await setSortPreference({ sortBy: SortBy.Alphabetical }); }; const sortByActivity = async () => { logEvent(events.DP_SORT_CHANNELS_BY_ACTIVITY); - await setSortPreference({ sortBy: 'activity' }); + await setSortPreference({ sortBy: SortBy.Activity }); }; const toggleGroupByType = async () => { @@ -77,23 +94,23 @@ const DisplayPrefsView = props => { const displayExpanded = async () => { logEvent(events.DP_DISPLAY_EXPANDED); - await setSortPreference({ displayMode: DISPLAY_MODE_EXPANDED }); + await setSortPreference({ displayMode: DisplayMode.Expanded }); }; const displayCondensed = async () => { logEvent(events.DP_DISPLAY_CONDENSED); - await setSortPreference({ displayMode: DISPLAY_MODE_CONDENSED }); + await setSortPreference({ displayMode: DisplayMode.Condensed }); }; - const renderCheckBox = value => ( + const renderCheckBox = (value: boolean) => ( ); - const renderAvatarSwitch = value => ( + const renderAvatarSwitch = (value: boolean) => ( toggleAvatar()} testID='display-pref-view-avatar-switch' /> ); - const renderRadio = value => ( + const renderRadio = (value: boolean) => ( { left={() => } title='Expanded' testID='display-pref-view-expanded' - right={() => renderRadio(displayMode === DISPLAY_MODE_EXPANDED)} + right={() => renderRadio(displayMode === DisplayMode.Expanded)} onPress={displayExpanded} /> @@ -119,7 +136,7 @@ const DisplayPrefsView = props => { left={() => } title='Condensed' testID='display-pref-view-condensed' - right={() => renderRadio(displayMode === DISPLAY_MODE_CONDENSED)} + right={() => renderRadio(displayMode === DisplayMode.Condensed)} onPress={displayCondensed} /> @@ -139,7 +156,7 @@ const DisplayPrefsView = props => { testID='display-pref-view-activity' left={() => } onPress={sortByActivity} - right={() => renderRadio(sortBy === 'activity')} + right={() => renderRadio(sortBy === SortBy.Activity)} /> { testID='display-pref-view-name' left={() => } onPress={sortByName} - right={() => renderRadio(sortBy === 'alphabetical')} + right={() => renderRadio(sortBy === SortBy.Alphabetical)} /> @@ -184,9 +201,6 @@ const DisplayPrefsView = props => { ); }; -DisplayPrefsView.propTypes = { - navigation: PropTypes.object, - isMasterDetail: PropTypes.bool -}; +DisplayPrefsView.propTypes = {}; export default DisplayPrefsView; diff --git a/app/views/E2EEncryptionSecurityView.tsx b/app/views/E2EEncryptionSecurityView.tsx index d5b0b27a..34759043 100644 --- a/app/views/E2EEncryptionSecurityView.tsx +++ b/app/views/E2EEncryptionSecurityView.tsx @@ -75,8 +75,6 @@ class E2EEncryptionSecurityView extends React.Component { - // TODO: Remove ts-ignore when migrate the showConfirmationAlert - // @ts-ignore showConfirmationAlert({ title: I18n.t('Are_you_sure_question_mark'), message: I18n.t('E2E_encryption_reset_message'), diff --git a/app/views/E2EEnterYourPasswordView.tsx b/app/views/E2EEnterYourPasswordView.tsx index dd9cdfa8..6d63f90d 100644 --- a/app/views/E2EEnterYourPasswordView.tsx +++ b/app/views/E2EEnterYourPasswordView.tsx @@ -17,6 +17,7 @@ import KeyboardView from '../presentation/KeyboardView'; import StatusBar from '../containers/StatusBar'; import { events, logEvent } from '../utils/log'; import sharedStyles from './Styles'; +import { E2EEnterYourPasswordStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -36,7 +37,7 @@ interface IE2EEnterYourPasswordViewState { interface IE2EEnterYourPasswordViewProps { encryptionDecodeKey: (password: string) => void; theme: string; - navigation: StackNavigationProp; + navigation: StackNavigationProp; } class E2EEnterYourPasswordView extends React.Component { diff --git a/app/views/E2EHowItWorksView.tsx b/app/views/E2EHowItWorksView.tsx index 0fbdf77a..fce1a2d0 100644 --- a/app/views/E2EHowItWorksView.tsx +++ b/app/views/E2EHowItWorksView.tsx @@ -9,6 +9,7 @@ import * as HeaderButton from '../containers/HeaderButton'; import Markdown from '../containers/markdown'; import { withTheme } from '../theme'; import I18n from '../i18n'; +import { E2ESaveYourPasswordStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -23,8 +24,8 @@ const styles = StyleSheet.create({ }); interface INavigation { - navigation: StackNavigationProp; - route: RouteProp<{ E2EHowItWorksView: { showCloseModal: boolean } }, 'E2EHowItWorksView'>; + navigation: StackNavigationProp; + route: RouteProp; } interface IE2EHowItWorksViewProps extends INavigation { diff --git a/app/views/E2ESaveYourPasswordView.tsx b/app/views/E2ESaveYourPasswordView.tsx index 1c4e13a5..3d9a32ee 100644 --- a/app/views/E2ESaveYourPasswordView.tsx +++ b/app/views/E2ESaveYourPasswordView.tsx @@ -19,6 +19,7 @@ import Button from '../containers/Button'; import { withTheme } from '../theme'; import I18n from '../i18n'; import sharedStyles from './Styles'; +import { E2ESaveYourPasswordStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -60,7 +61,7 @@ interface IE2ESaveYourPasswordViewState { interface IE2ESaveYourPasswordViewProps { server: string; - navigation: StackNavigationProp; + navigation: StackNavigationProp; encryptionSetBanner(): void; theme: string; } diff --git a/app/views/ForgotPasswordView.tsx b/app/views/ForgotPasswordView.tsx index c08a1acd..375d089d 100644 --- a/app/views/ForgotPasswordView.tsx +++ b/app/views/ForgotPasswordView.tsx @@ -14,6 +14,7 @@ import { themes } from '../constants/colors'; import FormContainer, { FormContainerInner } from '../containers/FormContainer'; import { events, logEvent } from '../utils/log'; import sharedStyles from './Styles'; +import { OutsideParamList } from '../stacks/types'; interface IForgotPasswordViewState { email: string; @@ -22,8 +23,8 @@ interface IForgotPasswordViewState { } interface IForgotPasswordViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ ForgotPasswordView: { title: string } }, 'ForgotPasswordView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; } diff --git a/app/views/ForwardLivechatView.tsx b/app/views/ForwardLivechatView.tsx index 42a29782..ea17466d 100644 --- a/app/views/ForwardLivechatView.tsx +++ b/app/views/ForwardLivechatView.tsx @@ -14,6 +14,7 @@ import OrSeparator from '../containers/OrSeparator'; import Input from '../containers/UIKit/MultiSelect/Input'; import { forwardRoom as forwardRoomAction } from '../actions/room'; import { ILivechatDepartment } from './definition/ILivechatDepartment'; +import { ChatsStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -47,8 +48,8 @@ interface IParsedData { } interface IForwardLivechatViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ ForwardLivechatView: { rid: string } }, 'ForwardLivechatView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; forwardRoom: (rid: string, transferData: ITransferData) => void; } diff --git a/app/views/InviteUsersEditView/index.tsx b/app/views/InviteUsersEditView/index.tsx index 62ce5121..4ae1a67d 100644 --- a/app/views/InviteUsersEditView/index.tsx +++ b/app/views/InviteUsersEditView/index.tsx @@ -19,6 +19,7 @@ import { withTheme } from '../../theme'; import SafeAreaView from '../../containers/SafeAreaView'; import { events, logEvent } from '../../utils/log'; import styles from './styles'; +import { ChatsStackParamList } from '../../stacks/types'; const OPTIONS = { days: [ @@ -67,9 +68,9 @@ const OPTIONS = { ] }; -interface IInviteUsersEditView { - navigation: StackNavigationProp; - route: RouteProp<{ InviteUsersEditView: { rid: string } }, 'InviteUsersEditView'>; +interface IInviteUsersEditViewProps { + navigation: StackNavigationProp; + route: RouteProp; theme: string; createInviteLink(rid: string): void; inviteLinksSetParams(params: { [key: string]: number }): void; @@ -77,14 +78,14 @@ interface IInviteUsersEditView { maxUses: number; } -class InviteUsersView extends React.Component { +class InviteUsersEditView extends React.Component { static navigationOptions = (): StackNavigationOptions => ({ title: I18n.t('Invite_users') }); private rid: string; - constructor(props: IInviteUsersEditView) { + constructor(props: IInviteUsersEditViewProps) { super(props); this.rid = props.route.params?.rid; } @@ -160,4 +161,4 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ createInviteLink: (rid: string) => dispatch(inviteLinksCreateAction(rid)) }); -export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersView)); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersEditView)); diff --git a/app/views/InviteUsersView/index.tsx b/app/views/InviteUsersView/index.tsx index cfcd3fa1..b7bf3071 100644 --- a/app/views/InviteUsersView/index.tsx +++ b/app/views/InviteUsersView/index.tsx @@ -6,6 +6,7 @@ import { StackNavigationProp, StackNavigationOptions } from '@react-navigation/s import { RouteProp } from '@react-navigation/core'; import { Dispatch } from 'redux'; +import { ChatsStackParamList } from '../../stacks/types'; import { inviteLinksClear as inviteLinksClearAction, inviteLinksCreate as inviteLinksCreateAction @@ -22,9 +23,9 @@ import SafeAreaView from '../../containers/SafeAreaView'; import { events, logEvent } from '../../utils/log'; import styles from './styles'; -interface IInviteUsersView { - navigation: StackNavigationProp; - route: RouteProp; +interface IInviteUsersViewProps { + navigation: StackNavigationProp; + route: RouteProp; theme: string; timeDateFormat: string; invite: { @@ -36,14 +37,14 @@ interface IInviteUsersView { createInviteLink(rid: string): void; clearInviteLink(): void; } -class InviteUsersView extends React.Component { +class InviteUsersView extends React.Component { private rid: string; static navigationOptions: StackNavigationOptions = { title: I18n.t('Invite_users') }; - constructor(props: IInviteUsersView) { + constructor(props: IInviteUsersViewProps) { super(props); this.rid = props.route.params?.rid; } diff --git a/app/views/JitsiMeetView.tsx b/app/views/JitsiMeetView.tsx index 44034cda..aa6658d2 100644 --- a/app/views/JitsiMeetView.tsx +++ b/app/views/JitsiMeetView.tsx @@ -12,6 +12,7 @@ import ActivityIndicator from '../containers/ActivityIndicator'; import { events, logEvent } from '../utils/log'; import { isAndroid, isIOS } from '../utils/deviceInfo'; import { withTheme } from '../theme'; +import { InsideStackParamList } from '../stacks/types'; const formatUrl = (url: string, baseUrl: string, uriSize: number, avatarAuthURLFragment: string) => `${baseUrl}/avatar/${url}?format=png&width=${uriSize}&height=${uriSize}${avatarAuthURLFragment}`; @@ -25,8 +26,8 @@ interface IJitsiMeetViewState { } interface IJitsiMeetViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ JitsiMeetView: { rid: string; url: string; onlyAudio?: boolean } }, 'JitsiMeetView'>; + navigation: StackNavigationProp; + route: RouteProp; baseUrl: string; theme: string; user: { diff --git a/app/views/LoginView.tsx b/app/views/LoginView.tsx index 4643687e..e43505f3 100644 --- a/app/views/LoginView.tsx +++ b/app/views/LoginView.tsx @@ -15,6 +15,7 @@ import TextInput from '../containers/TextInput'; import { loginRequest as loginRequestAction } from '../actions/login'; import LoginServices from '../containers/LoginServices'; import sharedStyles from './Styles'; +import { OutsideParamList } from '../stacks/types'; const styles = StyleSheet.create({ registerDisabled: { @@ -47,9 +48,9 @@ const styles = StyleSheet.create({ } }); -interface IProps { - navigation: StackNavigationProp; - route: RouteProp; +interface ILoginViewProps { + navigation: StackNavigationProp; + route: RouteProp; Site_Name: string; Accounts_RegistrationForm: string; Accounts_RegistrationForm_LinkReplacementText: string; @@ -67,15 +68,15 @@ interface IProps { inviteLinkToken: string; } -class LoginView extends React.Component { +class LoginView extends React.Component { private passwordInput: any; - static navigationOptions = ({ route, navigation }: Partial) => ({ + static navigationOptions = ({ route, navigation }: ILoginViewProps) => ({ title: route?.params?.title ?? 'Rocket.Chat', headerRight: () => }); - constructor(props: IProps) { + constructor(props: ILoginViewProps) { super(props); this.state = { user: props.route.params?.username ?? '', @@ -83,7 +84,7 @@ class LoginView extends React.Component { }; } - UNSAFE_componentWillReceiveProps(nextProps: IProps) { + UNSAFE_componentWillReceiveProps(nextProps: ILoginViewProps) { const { error } = this.props; if (nextProps.failure && !dequal(error, nextProps.error)) { if (nextProps.error?.error === 'error-invalid-email') { diff --git a/app/views/MarkdownTableView.tsx b/app/views/MarkdownTableView.tsx index a65994ee..e260199e 100644 --- a/app/views/MarkdownTableView.tsx +++ b/app/views/MarkdownTableView.tsx @@ -7,12 +7,10 @@ import I18n from '../i18n'; import { isIOS } from '../utils/deviceInfo'; import { themes } from '../constants/colors'; import { withTheme } from '../theme'; +import { ChatsStackParamList } from '../stacks/types'; interface IMarkdownTableViewProps { - route: RouteProp< - { MarkdownTableView: { renderRows: (drawExtraBorders?: boolean) => JSX.Element; tableWidth: number } }, - 'MarkdownTableView' - >; + route: RouteProp; theme: string; } diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index a948edcd..c9e3d601 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -3,8 +3,9 @@ import { FlatList, Text, View } from 'react-native'; import { connect } from 'react-redux'; import { dequal } from 'dequal'; import { StackNavigationProp } from '@react-navigation/stack'; -import { RouteProp } from '@react-navigation/core'; +import { CompositeNavigationProp, RouteProp } from '@react-navigation/core'; +import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import Message from '../../containers/message'; import ActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; @@ -18,22 +19,19 @@ import { withActionSheet } from '../../containers/ActionSheet'; import SafeAreaView from '../../containers/SafeAreaView'; import getThreadName from '../../lib/methods/getThreadName'; import styles from './styles'; - -type TMessagesViewRouteParams = { - MessagesView: { - rid: string; - t: string; - name: string; - }; -}; +import { ChatsStackParamList } from '../../stacks/types'; +import { ISubscription, SubscriptionType } from '../../definitions/ISubscription'; interface IMessagesViewProps { user: { id: string; }; baseUrl: string; - navigation: StackNavigationProp; - route: RouteProp; + navigation: CompositeNavigationProp< + StackNavigationProp, + StackNavigationProp + >; + route: RouteProp; customEmojis: { [key: string]: string }; theme: string; showActionSheet: Function; @@ -41,6 +39,14 @@ interface IMessagesViewProps { isMasterDetail: boolean; } +interface IRoomInfoParam { + room: ISubscription; + member: any; + rid: string; + t: SubscriptionType; + joined: boolean; +} + interface IMessagesViewState { loading: boolean; messages: []; @@ -65,17 +71,22 @@ interface IMessageItem { } interface IParams { - rid?: string; - jumpToMessageId: string; - t?: string; - room: any; + rid: string; + t: SubscriptionType; tmid?: string; + message?: string; name?: string; + fname?: string; + prid?: string; + room: ISubscription; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; } class MessagesView extends React.Component { - private rid?: string; - private t?: string; + private rid: string; + private t: SubscriptionType; private content: any; private room: any; @@ -121,7 +132,7 @@ class MessagesView extends React.Component { }); }; - navToRoomInfo = (navParam: { rid: string }) => { + navToRoomInfo = (navParam: IRoomInfoParam) => { const { navigation, user } = this.props; if (navParam.rid === user.id) { return; @@ -147,7 +158,7 @@ class MessagesView extends React.Component { ...params, tmid: item.tmid, name: await getThreadName(this.rid, item.tmid, item._id), - t: 'thread' + t: SubscriptionType.THREAD }; navigation.push('RoomView', params); } else { diff --git a/app/views/ModalBlockView.js b/app/views/ModalBlockView.tsx similarity index 70% rename from app/views/ModalBlockView.js rename to app/views/ModalBlockView.tsx index c87bf331..1a517745 100644 --- a/app/views/ModalBlockView.js +++ b/app/views/ModalBlockView.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import { connect } from 'react-redux'; import { KeyboardAwareScrollView } from '@codler/react-native-keyboard-aware-scroll-view'; @@ -15,6 +16,7 @@ import { CONTAINER_TYPES, MODAL_ACTIONS } from '../lib/methods/actions'; import { textParser } from '../containers/UIKit/utils'; import Navigation from '../lib/Navigation'; import sharedStyles from './Styles'; +import { MasterDetailInsideStackParamList } from '../stacks/MasterDetailStack/types'; const styles = StyleSheet.create({ container: { @@ -30,14 +32,49 @@ const styles = StyleSheet.create({ } }); -Object.fromEntries = Object.fromEntries || (arr => arr.reduce((acc, [k, v]) => ((acc[k] = v), acc), {})); -const groupStateByBlockIdMap = (obj, [key, { blockId, value }]) => { +interface IValueBlockId { + value: string; + blockId: string; +} + +type TElementToState = [string, IValueBlockId]; +interface IActions { + actionId: string; + value: any; + blockId?: string; +} + +interface IValues { + [key: string]: { + [key: string]: string; + }; +} +interface IModalBlockViewState { + data: any; + loading: boolean; + errors?: any; +} + +interface IModalBlockViewProps { + navigation: StackNavigationProp; + route: RouteProp; + theme: string; + language: string; + user: { + id: string; + token: string; + }; +} + +// eslint-disable-next-line no-sequences +Object.fromEntries = Object.fromEntries || ((arr: any[]) => arr.reduce((acc, [k, v]) => ((acc[k] = v), acc), {})); +const groupStateByBlockIdMap = (obj: any, [key, { blockId, value }]: TElementToState) => { obj[blockId] = obj[blockId] || {}; obj[blockId][key] = value; return obj; }; -const groupStateByBlockId = obj => Object.entries(obj).reduce(groupStateByBlockIdMap, {}); -const filterInputFields = ({ element, elements = [] }) => { +const groupStateByBlockId = (obj: { [key: string]: any }) => Object.entries(obj).reduce(groupStateByBlockIdMap, {}); +const filterInputFields = ({ element, elements = [] }: { element: any; elements?: any[] }) => { if (element && element.initialValue) { return true; } @@ -45,7 +82,8 @@ const filterInputFields = ({ element, elements = [] }) => { return true; } }; -const mapElementToState = ({ element, blockId, elements = [] }) => { + +const mapElementToState = ({ element, blockId, elements = [] }: { element: any; blockId: string; elements?: any[] }): any => { if (elements.length) { return elements .map(e => ({ element: e, blockId })) @@ -54,10 +92,15 @@ const mapElementToState = ({ element, blockId, elements = [] }) => { } return [element.actionId, { value: element.initialValue, blockId }]; }; -const reduceState = (obj, el) => (Array.isArray(el[0]) ? { ...obj, ...Object.fromEntries(el) } : { ...obj, [el[0]]: el[1] }); +const reduceState = (obj: any, el: any) => + Array.isArray(el[0]) ? { ...obj, ...Object.fromEntries(el) } : { ...obj, [el[0]]: el[1] }; -class ModalBlockView extends React.Component { - static navigationOptions = ({ route }) => { +class ModalBlockView extends React.Component { + private submitting: boolean; + + private values: IValues; + + static navigationOptions = ({ route }: Pick): StackNavigationOptions => { const data = route.params?.data; const { view } = data; const { title } = view; @@ -66,18 +109,7 @@ class ModalBlockView extends React.Component { }; }; - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string, - language: PropTypes.string, - user: PropTypes.shape({ - id: PropTypes.string, - token: PropTypes.string - }) - }; - - constructor(props) { + constructor(props: IModalBlockViewProps) { super(props); this.submitting = false; const data = props.route.params?.data; @@ -95,7 +127,7 @@ class ModalBlockView extends React.Component { EventEmitter.addEventListener(viewId, this.handleUpdate); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: IModalBlockViewProps) { const { navigation, route } = this.props; const oldData = prevProps.route.params?.data ?? {}; const newData = route.params?.data ?? {}; @@ -128,7 +160,7 @@ class ModalBlockView extends React.Component { /> ) - : null, + : undefined, headerRight: submit ? () => ( @@ -140,13 +172,13 @@ class ModalBlockView extends React.Component { /> ) - : null + : undefined }); }; - handleUpdate = ({ type, ...data }) => { + handleUpdate = ({ type, ...data }: { type: string }) => { if ([MODAL_ACTIONS.ERRORS].includes(type)) { - const { errors } = data; + const { errors }: any = data; this.setState({ errors }); } else { this.setState({ data }); @@ -154,7 +186,7 @@ class ModalBlockView extends React.Component { } }; - cancel = async ({ closeModal }) => { + cancel = async ({ closeModal }: { closeModal?: () => void }) => { const { data } = this.state; const { appId, viewId, view } = data; @@ -210,7 +242,7 @@ class ModalBlockView extends React.Component { this.setState({ loading: false }); }; - action = async ({ actionId, value, blockId }) => { + action = async ({ actionId, value, blockId }: IActions) => { const { data } = this.state; const { mid, appId, viewId } = data; await RocketChat.triggerBlockAction({ @@ -227,7 +259,7 @@ class ModalBlockView extends React.Component { this.changeState({ actionId, value, blockId }); }; - changeState = ({ actionId, value, blockId = 'default' }) => { + changeState = ({ actionId, value, blockId = 'default' }: IActions) => { this.values[actionId] = { blockId, value @@ -266,7 +298,7 @@ class ModalBlockView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ language: state.login.user && state.login.user.language }); diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.tsx similarity index 81% rename from app/views/NewMessageView.js rename to app/views/NewMessageView.tsx index 020588ff..cd182251 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { FlatList, StyleSheet, Text, View } from 'react-native'; +import { Dispatch } from 'redux'; import { connect } from 'react-redux'; import { Q } from '@nozbe/watermelondb'; import { dequal } from 'dequal'; -import * as List from '../containers/List'; +import * as List from '../containers/List'; import Touch from '../utils/touch'; import database from '../lib/database'; import RocketChat from '../lib/rocketchat'; @@ -18,7 +19,6 @@ import * as HeaderButton from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; import { themes } from '../constants/colors'; import { withTheme } from '../theme'; -import { getUserSelector } from '../selectors/login'; import Navigation from '../lib/Navigation'; import { createChannelRequest } from '../actions/createChannel'; import { goRoom } from '../utils/goRoom'; @@ -47,33 +47,54 @@ const styles = StyleSheet.create({ } }); -class NewMessageView extends React.Component { - static navigationOptions = ({ navigation }) => ({ +interface IButton { + onPress: () => void; + testID: string; + title: string; + icon: string; + first?: boolean; +} + +interface ISearch { + _id: string; + status: string; + username: string; + avatarETag: string; + outside: boolean; + rid: string; + name: string; + t: string; + search: boolean; +} + +interface INewMessageViewState { + search: ISearch[]; + // TODO: Refactor when migrate room + chats: any[]; + permissions: boolean[]; +} + +interface INewMessageViewProps { + navigation: StackNavigationProp; + create: (params: { group: boolean }) => void; + maxUsers: number; + theme: string; + isMasterDetail: boolean; + serverVersion: string; + createTeamPermission: string[]; + createDirectMessagePermission: string[]; + createPublicChannelPermission: string[]; + createPrivateChannelPermission: string[]; + createDiscussionPermission: string[]; +} + +class NewMessageView extends React.Component { + static navigationOptions = ({ navigation }: INewMessageViewProps): StackNavigationOptions => ({ headerLeft: () => , title: I18n.t('New_Message') }); - static propTypes = { - navigation: PropTypes.object, - baseUrl: PropTypes.string, - user: PropTypes.shape({ - id: PropTypes.string, - token: PropTypes.string, - roles: PropTypes.array - }), - create: PropTypes.func, - maxUsers: PropTypes.number, - theme: PropTypes.string, - isMasterDetail: PropTypes.bool, - serverVersion: PropTypes.string, - createTeamPermission: PropTypes.array, - createDirectMessagePermission: PropTypes.array, - createPublicChannelPermission: PropTypes.array, - createPrivateChannelPermission: PropTypes.array, - createDiscussionPermission: PropTypes.array - }; - - constructor(props) { + constructor(props: INewMessageViewProps) { super(props); this.init(); this.state = { @@ -102,7 +123,7 @@ class NewMessageView extends React.Component { this.handleHasPermission(); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: INewMessageViewProps) { const { createTeamPermission, createPublicChannelPermission, @@ -122,7 +143,7 @@ class NewMessageView extends React.Component { } } - onSearchChangeText(text) { + onSearchChangeText(text: string) { this.search(text); } @@ -131,7 +152,7 @@ class NewMessageView extends React.Component { return navigation.pop(); }; - search = async text => { + search = async (text: string) => { const result = await RocketChat.search({ text, filterRooms: false }); this.setState({ search: result @@ -162,7 +183,8 @@ class NewMessageView extends React.Component { }); }; - goRoom = item => { + // TODO: Refactor when migrate room + goRoom = (item: any) => { logEvent(events.NEW_MSG_CHAT_WITH_USER); const { isMasterDetail, navigation } = this.props; if (isMasterDetail) { @@ -171,7 +193,7 @@ class NewMessageView extends React.Component { goRoom({ item, isMasterDetail }); }; - renderButton = ({ onPress, testID, title, icon, first }) => { + renderButton = ({ onPress, testID, title, icon, first }: IButton) => { const { theme } = this.props; return ( @@ -218,7 +240,7 @@ class NewMessageView extends React.Component { return ( - this.onSearchChangeText(text)} testID='new-message-view-search' /> + this.onSearchChangeText(text)} testID='new-message-view-search' /> {permissions[0] || permissions[1] ? this.renderButton({ @@ -258,9 +280,10 @@ class NewMessageView extends React.Component { ); }; - renderItem = ({ item, index }) => { + // TODO: Refactor when migrate room + renderItem = ({ item, index }: { item: ISearch | any; index: number }) => { const { search, chats } = this.state; - const { baseUrl, user, theme } = this.props; + const { theme } = this.props; let style = { borderColor: themes[theme].separatorColor }; if (index === 0) { @@ -277,10 +300,8 @@ class NewMessageView extends React.Component { name={item.search ? item.name : item.fname} username={item.search ? item.username : item.name} onPress={() => this.goRoom(item)} - baseUrl={baseUrl} testID={`new-message-view-item-${item.name}`} style={style} - user={user} theme={theme} /> ); @@ -313,12 +334,10 @@ class NewMessageView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ serverVersion: state.server.version, isMasterDetail: state.app.isMasterDetail, - baseUrl: state.server.server, maxUsers: state.settings.DirectMesssage_maxUsers || 1, - user: getUserSelector(state), createTeamPermission: state.permissions['create-team'], createDirectMessagePermission: state.permissions['create-d'], createPublicChannelPermission: state.permissions['create-c'], @@ -326,8 +345,8 @@ const mapStateToProps = state => ({ createDiscussionPermission: state.permissions['start-discussion'] }); -const mapDispatchToProps = dispatch => ({ - create: params => dispatch(createChannelRequest(params)) +const mapDispatchToProps = (dispatch: Dispatch) => ({ + create: (params: { group: boolean }) => dispatch(createChannelRequest(params)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewMessageView)); diff --git a/app/views/NewServerView/ServerInput/Item.tsx b/app/views/NewServerView/ServerInput/Item.tsx index 9fb44719..cc8a9e3a 100644 --- a/app/views/NewServerView/ServerInput/Item.tsx +++ b/app/views/NewServerView/ServerInput/Item.tsx @@ -6,7 +6,7 @@ import { themes } from '../../../constants/colors'; import { CustomIcon } from '../../../lib/Icons'; import sharedStyles from '../../Styles'; import Touch from '../../../utils/touch'; -import { IServer } from '../index'; +import { TServerHistory } from '../../../definitions/IServerHistory'; const styles = StyleSheet.create({ container: { @@ -28,10 +28,10 @@ const styles = StyleSheet.create({ }); interface IItem { - item: IServer; + item: TServerHistory; theme: string; onPress(url: string): void; - onDelete(item: IServer): void; + onDelete(item: TServerHistory): void; } const Item = ({ item, theme, onPress, onDelete }: IItem): JSX.Element => ( diff --git a/app/views/NewServerView/ServerInput/index.tsx b/app/views/NewServerView/ServerInput/index.tsx index 1da15136..e2b14fd6 100644 --- a/app/views/NewServerView/ServerInput/index.tsx +++ b/app/views/NewServerView/ServerInput/index.tsx @@ -5,8 +5,8 @@ import TextInput from '../../../containers/TextInput'; import * as List from '../../../containers/List'; import { themes } from '../../../constants/colors'; import I18n from '../../../i18n'; +import { TServerHistory } from '../../../definitions/IServerHistory'; import Item from './Item'; -import { IServer } from '../index'; const styles = StyleSheet.create({ container: { @@ -33,8 +33,8 @@ interface IServerInput extends TextInputProps { theme: string; serversHistory: any[]; onSubmit(): void; - onDelete(item: IServer): void; - onPressServerHistory(serverHistory: IServer): void; + onDelete(item: TServerHistory): void; + onPressServerHistory(serverHistory: TServerHistory): void; } const ServerInput = ({ diff --git a/app/views/NewServerView/index.tsx b/app/views/NewServerView/index.tsx index f1458c93..93e493be 100644 --- a/app/views/NewServerView/index.tsx +++ b/app/views/NewServerView/index.tsx @@ -8,7 +8,6 @@ import { TouchableOpacity } from 'react-native-gesture-handler'; import Orientation from 'react-native-orientation-locker'; import { StackNavigationProp } from '@react-navigation/stack'; import { Dispatch } from 'redux'; -import Model from '@nozbe/watermelondb/Model'; import UserPreferences from '../../lib/userPreferences'; import EventEmitter from '../../utils/events'; @@ -33,6 +32,8 @@ import { isTablet } from '../../utils/deviceInfo'; import { verticalScale, moderateScale } from '../../utils/scaling'; import { withDimensions } from '../../dimensions'; import ServerInput from './ServerInput'; +import { OutsideParamList } from '../../stacks/types'; +import { TServerHistory } from '../../definitions/IServerHistory'; const styles = StyleSheet.create({ onboardingImage: { @@ -67,13 +68,8 @@ const styles = StyleSheet.create({ } }); -export interface IServer extends Model { - url: string; - username: string; -} - interface INewServerView { - navigation: StackNavigationProp; + navigation: StackNavigationProp; theme: string; connecting: boolean; connectServer(server: string, username?: string, fromServerHistory?: boolean): void; @@ -89,7 +85,7 @@ interface IState { text: string; connectingOpen: boolean; certificate: any; - serversHistory: IServer[]; + serversHistory: TServerHistory[]; } interface ISubmitParams { @@ -165,7 +161,7 @@ class NewServerView extends React.Component { const likeString = sanitizeLikeString(text); whereClause = [...whereClause, Q.where('url', Q.like(`%${likeString}%`))]; } - const serversHistory = (await serversHistoryCollection.query(...whereClause).fetch()) as IServer[]; + const serversHistory = (await serversHistoryCollection.query(...whereClause).fetch()) as TServerHistory[]; this.setState({ serversHistory }); } catch { // Do nothing @@ -189,7 +185,7 @@ class NewServerView extends React.Component { connectServer(server); }; - onPressServerHistory = (serverHistory: IServer) => { + onPressServerHistory = (serverHistory: TServerHistory) => { this.setState({ text: serverHistory.url }, () => this.submit({ fromServerHistory: true, username: serverHistory?.username })); }; @@ -273,23 +269,22 @@ class NewServerView extends React.Component { uriToPath = (uri: string) => uri.replace('file://', ''); handleRemove = () => { - // TODO: Remove ts-ignore when migrate the showConfirmationAlert - // @ts-ignore showConfirmationAlert({ message: I18n.t('You_will_unset_a_certificate_for_this_server'), confirmationText: I18n.t('Remove'), + // @ts-ignore onPress: this.setState({ certificate: null }) // We not need delete file from DocumentPicker because it is a temp file }); }; - deleteServerHistory = async (item: IServer) => { + deleteServerHistory = async (item: TServerHistory) => { const db = database.servers; try { await db.write(async () => { await item.destroyPermanently(); }); this.setState((prevstate: IState) => ({ - serversHistory: prevstate.serversHistory.filter((server: IServer) => server.id !== item.id) + serversHistory: prevstate.serversHistory.filter((server: TServerHistory) => server.id !== item.id) })); } catch { // Nothing diff --git a/app/views/NotificationPreferencesView/index.tsx b/app/views/NotificationPreferencesView/index.tsx index a020c163..5e33cec4 100644 --- a/app/views/NotificationPreferencesView/index.tsx +++ b/app/views/NotificationPreferencesView/index.tsx @@ -17,6 +17,7 @@ import SafeAreaView from '../../containers/SafeAreaView'; import log, { events, logEvent } from '../../utils/log'; import sharedStyles from '../Styles'; import { OPTIONS } from './options'; +import { ChatsStackParamList } from '../../stacks/types'; const styles = StyleSheet.create({ pickerText: { @@ -26,16 +27,8 @@ const styles = StyleSheet.create({ }); interface INotificationPreferencesView { - navigation: StackNavigationProp; - route: RouteProp< - { - NotificationPreferencesView: { - rid: string; - room: Model; - }; - }, - 'NotificationPreferencesView' - >; + navigation: StackNavigationProp; + route: RouteProp; theme: string; } diff --git a/app/views/NotificationPreferencesView/options.ts b/app/views/NotificationPreferencesView/options.ts index 4035c038..a2b3251c 100644 --- a/app/views/NotificationPreferencesView/options.ts +++ b/app/views/NotificationPreferencesView/options.ts @@ -1,4 +1,4 @@ -interface IOptionsField { +export interface IOptionsField { label: string; value: string | number; second?: number; diff --git a/app/views/PickerView.tsx b/app/views/PickerView.tsx index 002979b2..db2a7a26 100644 --- a/app/views/PickerView.tsx +++ b/app/views/PickerView.tsx @@ -11,6 +11,8 @@ import * as List from '../containers/List'; import SearchBox from '../containers/SearchBox'; import SafeAreaView from '../containers/SafeAreaView'; import sharedStyles from './Styles'; +import { ChatsStackParamList } from '../stacks/types'; +import { IOptionsField } from './NotificationPreferencesView/options'; const styles = StyleSheet.create({ search: { @@ -25,37 +27,21 @@ const styles = StyleSheet.create({ } }); -interface IData { - label: string; - value: string; - second?: string; -} - interface IItem { - item: IData; + item: IOptionsField; selected: boolean; onItemPress: () => void; theme: string; } interface IPickerViewState { - data: IData[]; + data: IOptionsField[]; value: string; } -interface IParams { - title: string; - value: string; - data: IData[]; - onChangeText: (value: string) => IData[]; - goBack: boolean; - onChange: Function; - onChangeValue: (value: string) => void; -} - interface IPickerViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ PickerView: IParams }, 'PickerView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; } @@ -69,7 +55,7 @@ const Item = React.memo(({ item, selected, onItemPress, theme }: IItem) => ( )); class PickerView extends React.PureComponent { - private onSearch: (text: string) => IData[]; + private onSearch?: ((text: string) => IOptionsField[]) | ((term?: string | undefined) => Promise); static navigationOptions = ({ route }: IPickerViewProps) => ({ title: route.params?.title ?? I18n.t('Select_an_option') @@ -126,13 +112,13 @@ class PickerView extends React.PureComponent {this.renderSearch()}
(Component: React.ComponentType
) => +export const withActionSheet = (Component: any): any => forwardRef((props: any, ref: ForwardedRef) => ( {(contexts: any) => } )); diff --git a/app/containers/Avatar/Avatar.tsx b/app/containers/Avatar/Avatar.tsx index 286bcc06..0ad2634f 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 9c95db83..2ce06647 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 ed7fd3b9..78152e52 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/Button/index.tsx b/app/containers/Button/index.tsx index 9e475a67..8c99dcce 100644 --- a/app/containers/Button/index.tsx +++ b/app/containers/Button/index.tsx @@ -70,6 +70,7 @@ export default class Button extends React.PureComponent, a disabled && styles.disabled, style ]} + accessibilityLabel={title} {...otherProps}> {loading ? ( diff --git a/app/containers/EmojiPicker/index.tsx b/app/containers/EmojiPicker/index.tsx index 64f5dbfe..12217cf9 100644 --- a/app/containers/EmojiPicker/index.tsx +++ b/app/containers/EmojiPicker/index.tsx @@ -31,7 +31,7 @@ interface IEmojiPickerProps { customEmojis?: any; style: object; theme?: string; - onEmojiSelected?: Function; + onEmojiSelected?: ((emoji: any) => void) | ((keyboardId: any, params?: any) => void); tabEmojiStyle?: object; } @@ -201,4 +201,5 @@ const mapStateToProps = (state: any) => ({ customEmojis: state.customEmojis }); -export default connect(mapStateToProps)(withTheme(EmojiPicker)); +// TODO - remove this as any, at the new PR to fix the HOC erros +export default connect(mapStateToProps)(withTheme(EmojiPicker)) as any; diff --git a/app/containers/HeaderButton/HeaderButtonContainer.tsx b/app/containers/HeaderButton/HeaderButtonContainer.tsx index 2d4c45b6..f757d43d 100644 --- a/app/containers/HeaderButton/HeaderButtonContainer.tsx +++ b/app/containers/HeaderButton/HeaderButtonContainer.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; interface IHeaderButtonContainer { - children: JSX.Element; + children: React.ReactNode; left?: boolean; } diff --git a/app/containers/LoginServices.tsx b/app/containers/LoginServices.tsx index bf175dd7..aab5c889 100644 --- a/app/containers/LoginServices.tsx +++ b/app/containers/LoginServices.tsx @@ -423,4 +423,4 @@ const mapStateToProps = (state: any) => ({ services: state.login.services }); -export default connect(mapStateToProps)(withTheme(LoginServices)); +export default connect(mapStateToProps)(withTheme(LoginServices)) as any; diff --git a/app/containers/MessageActions/index.tsx b/app/containers/MessageActions/index.tsx index eb9be967..4d32abf3 100644 --- a/app/containers/MessageActions/index.tsx +++ b/app/containers/MessageActions/index.tsx @@ -305,8 +305,6 @@ const MessageActions = React.memo( }; const handleDelete = (message: any) => { - // TODO - migrate this function for ts when fix the lint erros - // @ts-ignore showConfirmationAlert({ message: I18n.t('You_will_not_be_able_to_recover_this_message'), confirmationText: I18n.t('Delete'), diff --git a/app/containers/MessageBox/EmojiKeyboard.tsx b/app/containers/MessageBox/EmojiKeyboard.tsx index bbb0e20a..91acc45d 100644 --- a/app/containers/MessageBox/EmojiKeyboard.tsx +++ b/app/containers/MessageBox/EmojiKeyboard.tsx @@ -13,7 +13,7 @@ interface IMessageBoxEmojiKeyboard { } export default class EmojiKeyboard extends React.PureComponent { - private readonly baseUrl: any; + private readonly baseUrl: string; constructor(props: IMessageBoxEmojiKeyboard) { super(props); diff --git a/app/containers/MessageBox/RecordAudio.tsx b/app/containers/MessageBox/RecordAudio.tsx index fa6c509e..e219e642 100644 --- a/app/containers/MessageBox/RecordAudio.tsx +++ b/app/containers/MessageBox/RecordAudio.tsx @@ -13,6 +13,7 @@ import { events, logEvent } from '../../utils/log'; interface IMessageBoxRecordAudioProps { theme: string; + permissionToUpload: boolean; recordingCallback: Function; onFinish: Function; } @@ -192,9 +193,11 @@ export default class RecordAudio extends React.PureComponent { @@ -179,41 +182,13 @@ class MessageBox extends Component { showCommandPreview: false, command: {}, tshow: false, - mentionLoading: false + mentionLoading: false, + permissionToUpload: true }; this.text = ''; this.selection = { start: 0, end: 0 }; this.focused = false; - // MessageBox Actions - this.options = [ - { - title: I18n.t('Take_a_photo'), - icon: 'camera-photo', - onPress: this.takePhoto - }, - { - title: I18n.t('Take_a_video'), - icon: 'camera', - onPress: this.takeVideo - }, - { - title: I18n.t('Choose_from_library'), - icon: 'image', - onPress: this.chooseFromLibrary - }, - { - title: I18n.t('Choose_file'), - icon: 'attach', - onPress: this.chooseFile - }, - { - title: I18n.t('Create_Discussion'), - icon: 'discussions', - onPress: this.createDiscussion - } - ]; - const libPickerLabels = { cropperChooseText: I18n.t('Choose'), cropperCancelText: I18n.t('Cancel'), @@ -277,6 +252,8 @@ class MessageBox extends Component { this.onChangeText(usedCannedResponse); } + this.setOptions(); + this.unsubscribeFocus = navigation.addListener('focus', () => { // didFocus // We should wait pushed views be dismissed @@ -321,10 +298,20 @@ class MessageBox extends Component { } } - shouldComponentUpdate(nextProps: any, nextState: any) { - const { showEmojiKeyboard, showSend, recording, mentions, commandPreview, tshow, mentionLoading, trackingType } = this.state; + shouldComponentUpdate(nextProps: IMessageBoxProps, nextState: IMessageBoxState) { + const { + showEmojiKeyboard, + showSend, + recording, + mentions, + commandPreview, + tshow, + mentionLoading, + trackingType, + permissionToUpload + } = this.state; - const { roomType, replying, editing, isFocused, message, theme, usedCannedResponse } = this.props; + const { roomType, replying, editing, isFocused, message, theme, usedCannedResponse, uploadFilePermission } = this.props; if (nextProps.theme !== theme) { return true; } @@ -358,6 +345,9 @@ class MessageBox extends Component { if (nextState.tshow !== tshow) { return true; } + if (nextState.permissionToUpload !== permissionToUpload) { + return true; + } if (!dequal(nextState.mentions, mentions)) { return true; } @@ -367,12 +357,22 @@ class MessageBox extends Component { if (!dequal(nextProps.message?.id, message?.id)) { return true; } + if (!dequal(nextProps.uploadFilePermission, uploadFilePermission)) { + return true; + } if (nextProps.usedCannedResponse !== usedCannedResponse) { return true; } return false; } + componentDidUpdate(prevProps: IMessageBoxProps) { + const { uploadFilePermission } = this.props; + if (!dequal(prevProps.uploadFilePermission, uploadFilePermission)) { + this.setOptions(); + } + } + componentWillUnmount() { console.countReset(`${this.constructor.name}.render calls`); if (this.onChangeText && this.onChangeText.stop) { @@ -404,6 +404,19 @@ class MessageBox extends Component { } } + setOptions = async () => { + const { uploadFilePermission, rid } = this.props; + + // Servers older than 4.2 + if (!uploadFilePermission) { + this.setState({ permissionToUpload: true }); + return; + } + + const permissionToUpload = await RocketChat.hasPermission([uploadFilePermission], rid); + this.setState({ permissionToUpload: permissionToUpload[0] }); + }; + onChangeText: any = (text: string): void => { const isTextEmpty = text.length === 0; this.setShowSend(!isTextEmpty); @@ -666,8 +679,9 @@ class MessageBox extends Component { }; canUploadFile = (file: any) => { + const { permissionToUpload } = this.state; const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props; - const result = canUploadFile(file, FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize); + const result = canUploadFile(file, FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize, permissionToUpload); if (result.success) { return true; } @@ -766,8 +780,41 @@ class MessageBox extends Component { showMessageBoxActions = () => { logEvent(events.ROOM_SHOW_BOX_ACTIONS); + const { permissionToUpload } = this.state; const { showActionSheet } = this.props; - showActionSheet({ options: this.options }); + + const options = []; + if (permissionToUpload) { + options.push( + { + title: I18n.t('Take_a_photo'), + icon: 'camera-photo', + onPress: this.takePhoto + }, + { + title: I18n.t('Take_a_video'), + icon: 'camera', + onPress: this.takeVideo + }, + { + title: I18n.t('Choose_from_library'), + icon: 'image', + onPress: this.chooseFromLibrary + }, + { + title: I18n.t('Choose_file'), + icon: 'attach', + onPress: this.chooseFile + } + ); + } + + options.push({ + title: I18n.t('Create_Discussion'), + icon: 'discussions', + onPress: this.createDiscussion + }); + showActionSheet({ options }); }; editCancel = () => { @@ -968,8 +1015,17 @@ class MessageBox extends Component { }; renderContent = () => { - const { recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview, mentionLoading } = - this.state; + const { + recording, + showEmojiKeyboard, + showSend, + mentions, + trackingType, + commandPreview, + showCommandPreview, + mentionLoading, + permissionToUpload + } = this.state; const { editing, message, @@ -995,7 +1051,12 @@ class MessageBox extends Component { const recordAudio = showSend || !Message_AudioRecorderEnabled ? null : ( - + ); const commandsPreviewAndMentions = !recording ? ( @@ -1117,11 +1178,12 @@ const mapStateToProps = (state: any) => ({ user: getUserSelector(state), FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize, - Message_AudioRecorderEnabled: state.settings.Message_AudioRecorderEnabled + Message_AudioRecorderEnabled: state.settings.Message_AudioRecorderEnabled, + uploadFilePermission: state.permissions['mobile-upload-file'] }); const dispatchToProps = { typing: (rid: any, status: any) => userTypingAction(rid, status) }; // @ts-ignore -export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withActionSheet(MessageBox)); +export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withActionSheet(MessageBox)) as any; diff --git a/app/containers/Passcode/Base/Button.tsx b/app/containers/Passcode/Base/Button.tsx index f7e6c1a9..50a1cf41 100644 --- a/app/containers/Passcode/Base/Button.tsx +++ b/app/containers/Passcode/Base/Button.tsx @@ -7,28 +7,28 @@ import Touch from '../../../utils/touch'; import { CustomIcon } from '../../../lib/Icons'; interface IPasscodeButton { - text: string; - icon: string; + text?: string; + icon?: string; theme: string; - disabled: boolean; - onPress: Function; + disabled?: boolean; + onPress?: Function; } -const Button = React.memo(({ text, disabled, theme, onPress, icon }: Partial) => { - const press = () => onPress && onPress(text!); +const Button = React.memo(({ text, disabled, theme, onPress, icon }: IPasscodeButton) => { + const press = () => onPress && onPress(text); return ( {icon ? ( - + ) : ( - {text} + {text} )} ); diff --git a/app/containers/Passcode/Base/index.tsx b/app/containers/Passcode/Base/index.tsx index dd1d90e8..c6591770 100644 --- a/app/containers/Passcode/Base/index.tsx +++ b/app/containers/Passcode/Base/index.tsx @@ -20,7 +20,7 @@ interface IPasscodeBase { previousPasscode?: string; title: string; subtitle?: string; - showBiometry?: string; + showBiometry?: boolean; onEndProcess: Function; onError?: Function; onBiometryPress?(): void; diff --git a/app/containers/Passcode/PasscodeEnter.tsx b/app/containers/Passcode/PasscodeEnter.tsx index cc284b24..0a9b6b1f 100644 --- a/app/containers/Passcode/PasscodeEnter.tsx +++ b/app/containers/Passcode/PasscodeEnter.tsx @@ -15,7 +15,7 @@ import I18n from '../../i18n'; interface IPasscodePasscodeEnter { theme: string; - hasBiometry: string; + hasBiometry: boolean; finishProcess: Function; } diff --git a/app/containers/SearchBox.tsx b/app/containers/SearchBox.tsx index 4a08c91c..6668e0f7 100644 --- a/app/containers/SearchBox.tsx +++ b/app/containers/SearchBox.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; +import { StyleSheet, Text, TextInputProps, View } from 'react-native'; import Touchable from 'react-native-platform-touchable'; import TextInput from '../presentation/TextInput'; @@ -45,7 +45,7 @@ const styles = StyleSheet.create({ }); interface ISearchBox { - onChangeText: () => void; + onChangeText: TextInputProps['onChangeText']; onSubmitEditing: () => void; hasCancel: boolean; onCancelPress: Function; diff --git a/app/containers/Status/Status.tsx b/app/containers/Status/Status.tsx index e62bc806..dd780bbd 100644 --- a/app/containers/Status/Status.tsx +++ b/app/containers/Status/Status.tsx @@ -8,6 +8,7 @@ interface IStatus { status: string; size: number; style?: StyleProp; + testID?: string; } const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: IStatus) => { diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index b9aaf962..9d4d005e 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -43,7 +43,11 @@ const Content = React.memo( content = {I18n.t('Sent_an_attachment')}; } else if (props.isEncrypted) { content = ( - {I18n.t('Encrypted_message')} + + {I18n.t('Encrypted_message')} + ); } else { const { baseUrl, user, onLinkPress } = useContext(MessageContext); diff --git a/app/containers/message/Reply.tsx b/app/containers/message/Reply.tsx index fbc8984f..8d812605 100644 --- a/app/containers/message/Reply.tsx +++ b/app/containers/message/Reply.tsx @@ -13,6 +13,7 @@ import { themes } from '../../constants/colors'; import MessageContext from './Context'; import { fileDownloadAndPreview } from '../../utils/fileDownload'; import { formatAttachmentUrl } from '../../lib/utils'; +import { IAttachment } from '../../definitions/IAttachment'; import RCActivityIndicator from '../ActivityIndicator'; const styles = StyleSheet.create({ @@ -90,43 +91,26 @@ const styles = StyleSheet.create({ } }); -interface IMessageReplyAttachment { - author_name: string; - message_link: string; - ts: string; - text: string; - title: string; - short: boolean; - value: string; - title_link: string; - author_link: string; - type: string; - color: string; - description: string; - fields: IMessageReplyAttachment[]; - thumb_url: string; -} - interface IMessageTitle { - attachment: Partial; + attachment: IAttachment; timeFormat: string; theme: string; } interface IMessageDescription { - attachment: Partial; + attachment: IAttachment; getCustomEmoji: Function; theme: string; } interface IMessageFields { - attachment: Partial; + attachment: IAttachment; theme: string; getCustomEmoji: Function; } interface IMessageReply { - attachment: IMessageReplyAttachment; + attachment: IAttachment; timeFormat: string; index: number; theme: string; @@ -198,7 +182,7 @@ const Fields = React.memo( {field.title} {/* @ts-ignore*/} void; + +export interface ITranslations { + _id: string; + language: string; + value: string; +} + +export interface ILastMessage { + _id: string; + rid: string; + tshow: boolean; + tmid: string; + msg: string; + ts: Date; + u: IUserMessage; + _updatedAt: Date; + urls: string[]; + mentions: IUserMention[]; + channels: IUserChannel[]; + md: MarkdownAST; + attachments: IAttachment[]; + reactions: IReaction[]; + unread: boolean; + status: boolean; +} + +export interface IMessage { + msg?: string; + t?: SubscriptionType; + ts: Date; + u: IUserMessage; + alias: string; + parseUrls: boolean; + groupable?: boolean; + avatar?: string; + emoji?: string; + attachments?: IAttachment[]; + urls?: string[]; + _updatedAt: Date; + status?: number; + pinned?: boolean; + starred?: boolean; + editedBy?: IEditedBy; + reactions?: IReaction[]; + role?: string; + drid?: string; + dcount?: number; + dlm?: Date; + tmid?: string; + tcount?: number; + tlm?: Date; + replies?: string[]; + mentions?: IUserMention[]; + channels?: IUserChannel[]; + unread?: boolean; + autoTranslate?: boolean; + translations?: ITranslations[]; + tmsg?: string; + blocks?: any; + e2e?: string; + tshow?: boolean; + md?: MarkdownAST; + subscription: { id: string }; +} + +export type TMessageModel = IMessage & Model; diff --git a/app/definitions/INotification.ts b/app/definitions/INotification.ts new file mode 100644 index 00000000..77467d9d --- /dev/null +++ b/app/definitions/INotification.ts @@ -0,0 +1,12 @@ +export interface INotification { + message: string; + style: string; + ejson: string; + collapse_key: string; + notId: string; + msgcnt: string; + title: string; + from: string; + image: string; + soundname: string; +} diff --git a/app/definitions/IPermission.ts b/app/definitions/IPermission.ts new file mode 100644 index 00000000..0ccc1346 --- /dev/null +++ b/app/definitions/IPermission.ts @@ -0,0 +1,9 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface IPermission { + id: string; + roles: string[]; + _updatedAt: Date; +} + +export type TPermissionModel = IPermission & Model; diff --git a/app/definitions/IReaction.ts b/app/definitions/IReaction.ts new file mode 100644 index 00000000..a28f5d06 --- /dev/null +++ b/app/definitions/IReaction.ts @@ -0,0 +1,5 @@ +export interface IReaction { + _id: string; + emoji: string; + usernames: string[]; +} diff --git a/app/definitions/IRole.ts b/app/definitions/IRole.ts new file mode 100644 index 00000000..1fec4215 --- /dev/null +++ b/app/definitions/IRole.ts @@ -0,0 +1,8 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface IRole { + id: string; + description?: string; +} + +export type TRoleModel = IRole & Model; diff --git a/app/definitions/IRoom.ts b/app/definitions/IRoom.ts new file mode 100644 index 00000000..d55cf34a --- /dev/null +++ b/app/definitions/IRoom.ts @@ -0,0 +1,20 @@ +import Model from '@nozbe/watermelondb/Model'; + +import { IServedBy } from './IServedBy'; + +export interface IRoom { + id: string; + customFields: string[]; + broadcast: boolean; + encrypted: boolean; + ro: boolean; + v?: string[]; + servedBy?: IServedBy; + departmentId?: string; + livechatData?: any; + tags?: string[]; + e2eKeyId?: string; + avatarETag?: string; +} + +export type TRoomModel = IRoom & Model; diff --git a/app/definitions/IServedBy.ts b/app/definitions/IServedBy.ts new file mode 100644 index 00000000..4bf31aad --- /dev/null +++ b/app/definitions/IServedBy.ts @@ -0,0 +1,5 @@ +export interface IServedBy { + _id: string; + username: string; + ts: Date; +} diff --git a/app/definitions/IServer.ts b/app/definitions/IServer.ts new file mode 100644 index 00000000..0c3bf57d --- /dev/null +++ b/app/definitions/IServer.ts @@ -0,0 +1,20 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface IServer { + name: string; + iconURL: string; + useRealName: boolean; + FileUpload_MediaTypeWhiteList: string; + FileUpload_MaxFileSize: number; + roomsUpdatedAt: Date; + version: string; + lastLocalAuthenticatedSession: Date; + autoLock: boolean; + autoLockTime?: number; + biometry?: boolean; + uniqueID: string; + enterpriseModules: string; + E2E_Enable: boolean; +} + +export type TServerModel = IServer & Model; diff --git a/app/definitions/IServerHistory.ts b/app/definitions/IServerHistory.ts new file mode 100644 index 00000000..296cba4e --- /dev/null +++ b/app/definitions/IServerHistory.ts @@ -0,0 +1,10 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface IServerHistory { + id: string; + url: string; + username: string; + updatedAt: Date; +} + +export type TServerHistory = IServerHistory & Model; diff --git a/app/definitions/ISettings.ts b/app/definitions/ISettings.ts new file mode 100644 index 00000000..1fbb63ac --- /dev/null +++ b/app/definitions/ISettings.ts @@ -0,0 +1,12 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface ISettings { + id: string; + valueAsString?: string; + valueAsBoolean?: boolean; + valueAsNumber?: number; + valueAsArray?: string[]; + _updatedAt?: Date; +} + +export type TSettingsModel = ISettings & Model; diff --git a/app/definitions/ISlashCommand.ts b/app/definitions/ISlashCommand.ts new file mode 100644 index 00000000..a859448d --- /dev/null +++ b/app/definitions/ISlashCommand.ts @@ -0,0 +1,12 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface ISlashCommand { + id: string; + params?: string; + description?: string; + clientOnly?: boolean; + providesPreview?: boolean; + appId?: string; +} + +export type TSlashCommandModel = ISlashCommand & Model; diff --git a/app/definitions/ISubscription.ts b/app/definitions/ISubscription.ts new file mode 100644 index 00000000..1f241599 --- /dev/null +++ b/app/definitions/ISubscription.ts @@ -0,0 +1,91 @@ +import Model from '@nozbe/watermelondb/Model'; +import Relation from '@nozbe/watermelondb/Relation'; + +import { ILastMessage, TMessageModel } from './IMessage'; +import { IServedBy } from './IServedBy'; +import { TThreadModel } from './IThread'; +import { TThreadMessageModel } from './IThreadMessage'; +import { TUploadModel } from './IUpload'; + +export enum SubscriptionType { + GROUP = 'p', + DIRECT = 'd', + CHANNEL = 'c', + OMNICHANNEL = 'l', + THREAD = 'thread' +} + +export interface IVisitor { + _id: string; + username: string; + token: string; + status: string; + lastMessageTs: Date; +} + +export interface ISubscription { + _id: string; // _id belongs watermelonDB + id: string; // id from server + f: boolean; + t: SubscriptionType; + ts: Date; + ls: Date; + name: string; + fname?: string; + rid: string; // the same as id + open: boolean; + alert: boolean; + roles?: string[]; + unread: number; + userMentions: number; + groupMentions: number; + tunread?: string[]; + tunreadUser?: string[]; + tunreadGroup?: string[]; + roomUpdatedAt: Date; + ro: boolean; + lastOpen?: Date; + description?: string; + announcement?: string; + bannerClosed?: boolean; + topic?: string; + blocked?: boolean; + blocker?: boolean; + reactWhenReadOnly?: boolean; + archived: boolean; + joinCodeRequired?: boolean; + muted?: string[]; + ignored?: string[]; + broadcast?: boolean; + prid?: string; + draftMessage?: string; + lastThreadSync?: Date; + jitsiTimeout?: number; + autoTranslate?: boolean; + autoTranslateLanguage: string; + lastMessage?: ILastMessage; + hideUnreadStatus?: boolean; + sysMes?: string[] | boolean; + uids?: string[]; + usernames?: string[]; + visitor?: IVisitor; + departmentId?: string; + servedBy?: IServedBy; + livechatData?: any; + tags?: string[]; + E2EKey?: string; + encrypted?: boolean; + e2eKeyId?: string; + avatarETag?: string; + teamId?: string; + teamMain?: boolean; + search?: boolean; + username?: string; + // https://nozbe.github.io/WatermelonDB/Relation.html#relation-api + messages: Relation; + threads: Relation; + threadMessages: Relation; + uploads: Relation; +} + +export type TSubscriptionModel = ISubscription & Model; diff --git a/app/definition/ITeam.js b/app/definitions/ITeam.ts similarity index 79% rename from app/definition/ITeam.js rename to app/definitions/ITeam.ts index 10919715..8cf8bddc 100644 --- a/app/definition/ITeam.js +++ b/app/definitions/ITeam.ts @@ -1,5 +1,5 @@ // https://github.com/RocketChat/Rocket.Chat/blob/develop/definition/ITeam.ts -export const TEAM_TYPE = { +exports.TEAM_TYPE = { PUBLIC: 0, PRIVATE: 1 }; diff --git a/app/definitions/ITheme.ts b/app/definitions/ITheme.ts new file mode 100644 index 00000000..208a0b2d --- /dev/null +++ b/app/definitions/ITheme.ts @@ -0,0 +1,8 @@ +export type TThemeMode = 'automatic' | 'light' | 'dark'; + +export type TDarkLevel = 'black' | 'dark'; + +export interface IThemePreference { + currentTheme: TThemeMode; + darkLevel: TDarkLevel; +} diff --git a/app/definitions/IThread.ts b/app/definitions/IThread.ts new file mode 100644 index 00000000..ad151283 --- /dev/null +++ b/app/definitions/IThread.ts @@ -0,0 +1,78 @@ +import Model from '@nozbe/watermelondb/Model'; +import { MarkdownAST } from '@rocket.chat/message-parser'; + +import { IAttachment } from './IAttachment'; +import { IEditedBy, IUserChannel, IUserMention, IUserMessage } from './IMessage'; +import { IReaction } from './IReaction'; +import { SubscriptionType } from './ISubscription'; + +export interface IUrl { + title: string; + description: string; + image: string; + url: string; +} + +interface IFileThread { + _id: string; + name: string; + type: string; +} + +export interface IThreadResult { + _id: string; + rid: string; + ts: string; + msg: string; + file?: IFileThread; + files?: IFileThread[]; + groupable?: boolean; + attachments?: IAttachment[]; + md?: MarkdownAST; + u: IUserMessage; + _updatedAt: string; + urls: IUrl[]; + mentions: IUserMention[]; + channels: IUserChannel[]; + replies: string[]; + tcount: number; + tlm: string; +} + +export interface IThread { + id: string; + msg?: string; + t?: SubscriptionType; + rid: string; + _updatedAt: Date; + ts: Date; + u: IUserMessage; + alias?: string; + parseUrls?: boolean; + groupable?: boolean; + avatar?: string; + emoji?: string; + attachments?: IAttachment[]; + urls?: IUrl[]; + status?: number; + pinned?: boolean; + starred?: boolean; + editedBy?: IEditedBy; + reactions?: IReaction[]; + role?: string; + drid?: string; + dcount?: number; + dlm?: number; + tmid?: string; + tcount?: number; + tlm?: Date; + replies?: string[]; + mentions?: IUserMention[]; + channels?: IUserChannel[]; + unread?: boolean; + autoTranslate?: boolean; + translations?: any; + e2e?: string; +} + +export type TThreadModel = IThread & Model; diff --git a/app/definitions/IThreadMessage.ts b/app/definitions/IThreadMessage.ts new file mode 100644 index 00000000..c773e4dc --- /dev/null +++ b/app/definitions/IThreadMessage.ts @@ -0,0 +1,44 @@ +import Model from '@nozbe/watermelondb/Model'; + +import { IAttachment } from './IAttachment'; +import { IEditedBy, ITranslations, IUserChannel, IUserMention, IUserMessage } from './IMessage'; +import { IReaction } from './IReaction'; +import { SubscriptionType } from './ISubscription'; + +export interface IThreadMessage { + msg?: string; + t?: SubscriptionType; + rid: string; + ts: Date; + u: IUserMessage; + alias?: string; + parseUrls?: boolean; + groupable?: boolean; + avatar?: string; + emoji?: string; + attachments?: IAttachment[]; + urls?: string[]; + _updatedAt?: Date; + status?: number; + pinned?: boolean; + starred?: boolean; + editedBy?: IEditedBy; + reactions?: IReaction[]; + role?: string; + drid?: string; + dcount?: number; + dlm?: Date; + tmid?: string; + tcount?: number; + tlm?: Date; + replies?: string[]; + mentions?: IUserMention[]; + channels?: IUserChannel[]; + unread?: boolean; + autoTranslate?: boolean; + translations?: ITranslations[]; + e2e?: string; + subscription?: { id: string }; +} + +export type TThreadMessageModel = IThreadMessage & Model; diff --git a/app/definitions/IUpload.ts b/app/definitions/IUpload.ts new file mode 100644 index 00000000..6ff03c51 --- /dev/null +++ b/app/definitions/IUpload.ts @@ -0,0 +1,16 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface IUpload { + id: string; + path?: string; + name?: string; + description?: string; + size: number; + type?: string; + store?: string; + progress: number; + error: boolean; + subscription: { id: string }; +} + +export type TUploadModel = IUpload & Model; diff --git a/app/definitions/IUrl.ts b/app/definitions/IUrl.ts new file mode 100644 index 00000000..9b72fda2 --- /dev/null +++ b/app/definitions/IUrl.ts @@ -0,0 +1,6 @@ +export interface IUrl { + title: string; + description: string; + image: string; + url: string; +} diff --git a/app/definitions/IUser.ts b/app/definitions/IUser.ts new file mode 100644 index 00000000..012ef808 --- /dev/null +++ b/app/definitions/IUser.ts @@ -0,0 +1,10 @@ +import Model from '@nozbe/watermelondb/Model'; + +export interface IUser { + _id: string; + name?: string; + username: string; + avatarETag?: string; +} + +export type TUserModel = IUser & Model; diff --git a/app/definitions/index.ts b/app/definitions/index.ts new file mode 100644 index 00000000..80eeb88c --- /dev/null +++ b/app/definitions/index.ts @@ -0,0 +1,19 @@ +import { RouteProp } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { Dispatch } from 'redux'; + +export * from './IAttachment'; +export * from './IMessage'; +export * from './INotification'; +export * from './IRoom'; +export * from './IServer'; +export * from './ISubscription'; + +export interface IBaseScreen, S extends string> { + navigation: StackNavigationProp; + route: RouteProp; + dispatch: Dispatch; + theme: string; +} + +export * from './redux'; diff --git a/app/definitions/redux/index.ts b/app/definitions/redux/index.ts new file mode 100644 index 00000000..e95763e2 --- /dev/null +++ b/app/definitions/redux/index.ts @@ -0,0 +1,31 @@ +import { TActionSelectedUsers } from '../../actions/selectedUsers'; +import { TActionActiveUsers } from '../../actions/activeUsers'; +// REDUCERS +import { IActiveUsers } from '../../reducers/activeUsers'; +import { ISelectedUsers } from '../../reducers/selectedUsers'; + +export interface IApplicationState { + settings: any; + login: any; + meteor: any; + server: any; + selectedUsers: ISelectedUsers; + createChannel: any; + app: any; + room: any; + rooms: any; + sortPreferences: any; + share: any; + customEmojis: any; + activeUsers: IActiveUsers; + usersTyping: any; + inviteLinks: any; + createDiscussion: any; + inquiry: any; + enterpriseModules: any; + encryption: any; + permissions: any; + roles: any; +} + +export type TApplicationActions = TActionActiveUsers & TActionSelectedUsers; diff --git a/app/dimensions.tsx b/app/dimensions.tsx index dc164362..67600968 100644 --- a/app/dimensions.tsx +++ b/app/dimensions.tsx @@ -22,7 +22,7 @@ export interface IDimensionsContextProps { export const DimensionsContext = React.createContext>(Dimensions.get('window')); -export function withDimensions(Component: any) { +export function withDimensions(Component: any): any { const DimensionsComponent = (props: any) => ( {contexts => } ); diff --git a/app/ee/omnichannel/lib/subscriptions/inquiry.js b/app/ee/omnichannel/lib/subscriptions/inquiry.js index 00d32082..d10d5c89 100644 --- a/app/ee/omnichannel/lib/subscriptions/inquiry.js +++ b/app/ee/omnichannel/lib/subscriptions/inquiry.js @@ -6,7 +6,6 @@ import { inquiryQueueAdd, inquiryQueueRemove, inquiryQueueUpdate, inquiryRequest const removeListener = listener => listener.stop(); let connectedListener; -let disconnectedListener; let queueListener; const streamTopic = 'stream-livechat-inquiry-queue-observer'; @@ -48,10 +47,6 @@ export default function subscribeInquiry() { connectedListener.then(removeListener); connectedListener = false; } - if (disconnectedListener) { - disconnectedListener.then(removeListener); - disconnectedListener = false; - } if (queueListener) { queueListener.then(removeListener); queueListener = false; @@ -59,7 +54,6 @@ export default function subscribeInquiry() { }; connectedListener = RocketChat.onStreamData('connected', handleConnection); - disconnectedListener = RocketChat.onStreamData('close', handleConnection); queueListener = RocketChat.onStreamData(streamTopic, handleQueueMessageReceived); try { diff --git a/app/ee/omnichannel/views/QueueListView.js b/app/ee/omnichannel/views/QueueListView.js index defe9233..5d537cee 100644 --- a/app/ee/omnichannel/views/QueueListView.js +++ b/app/ee/omnichannel/views/QueueListView.js @@ -161,4 +161,5 @@ const mapStateToProps = state => ({ showAvatar: state.sortPreferences.showAvatar, displayMode: state.sortPreferences.displayMode }); + export default connect(mapStateToProps)(withDimensions(withTheme(QueueListView))); diff --git a/app/externalModules.d.ts b/app/externalModules.d.ts index f68cb5e3..02c57204 100644 --- a/app/externalModules.d.ts +++ b/app/externalModules.d.ts @@ -13,3 +13,4 @@ declare module 'react-native-mime-types'; declare module 'react-native-restart'; declare module 'react-native-prompt-android'; declare module 'react-native-jitsi-meet'; +declare module 'rn-root-view'; diff --git a/app/i18n/locales/ar.json b/app/i18n/locales/ar.json index 896d4d0d..0f6eebaa 100644 --- a/app/i18n/locales/ar.json +++ b/app/i18n/locales/ar.json @@ -328,7 +328,6 @@ "N_users": "{{n}} مستخدمين", "N_channels": "{{n}} القنوات", "Name": "اسم", - "Navigation_history": "تاريخ التصفح", "Never": "أبداً", "New_Message": "رسالة جديدة", "New_Password": "كلمة مرور جديدة", diff --git a/app/i18n/locales/de.json b/app/i18n/locales/de.json index 5a6fe2bd..8bb4dee5 100644 --- a/app/i18n/locales/de.json +++ b/app/i18n/locales/de.json @@ -330,7 +330,6 @@ "N_users": "{{n}} Benutzer", "N_channels": "{{n}} Kanäle", "Name": "Name", - "Navigation_history": "Navigations-Verlauf", "Never": "Niemals", "New_Message": "Neue Nachricht", "New_Password": "Neues Kennwort", diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index e4ae9fe1..754a6304 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -21,6 +21,7 @@ "error-save-video": "Error while saving video", "error-field-unavailable": "{{field}} is already in use :(", "error-file-too-large": "File is too large", + "error-not-permission-to-upload-file": "You don't have permission to upload files", "error-importer-not-defined": "The importer was not defined correctly, it is missing the Import class.", "error-input-is-not-a-valid-field": "{{input}} is not a valid {{field}}", "error-invalid-actionlink": "Invalid action link", @@ -330,7 +331,6 @@ "N_users": "{{n}} users", "N_channels": "{{n}} channels", "Name": "Name", - "Navigation_history": "Navigation history", "Never": "Never", "New_Message": "New Message", "New_Password": "New Password", @@ -787,4 +787,4 @@ "Unsupported_format": "Unsupported format", "Downloaded_file": "Downloaded file", "Error_Download_file": "Error while downloading file" -} +} \ No newline at end of file diff --git a/app/i18n/locales/fr.json b/app/i18n/locales/fr.json index 811486b0..81e8e21b 100644 --- a/app/i18n/locales/fr.json +++ b/app/i18n/locales/fr.json @@ -330,7 +330,6 @@ "N_users": "{{n}} utilisateurs", "N_channels": "{{n}} canaux", "Name": "Nom", - "Navigation_history": "Historique de navigation", "Never": "Jamais", "New_Message": "Nouveau message", "New_Password": "Nouveau mot de passe", @@ -782,5 +781,8 @@ "No_canned_responses": "Pas de réponses standardisées", "Send_email_confirmation": "Envoyer un e-mail de confirmation", "sending_email_confirmation": "envoi d'e-mail de confirmation", - "Enable_Message_Parser": "Activer le parseur de messages" + "Enable_Message_Parser": "Activer le parseur de messages", + "Unsupported_format": "Format non supporté", + "Downloaded_file": "Fichier téléchargé", + "Error_Download_file": "Erreur lors du téléchargement du fichier" } \ No newline at end of file diff --git a/app/i18n/locales/it.json b/app/i18n/locales/it.json index 7b1f1f66..5e9e4f9f 100644 --- a/app/i18n/locales/it.json +++ b/app/i18n/locales/it.json @@ -322,7 +322,6 @@ "N_people_reacted": "{{n}} persone hanno reagito", "N_users": "{{n}} utenti", "Name": "Nome", - "Navigation_history": "Cronologia di navigazione", "Never": "Mai", "New_Message": "Nuovo messaggio", "New_Password": "Nuova password", diff --git a/app/i18n/locales/nl.json b/app/i18n/locales/nl.json index df4e3aaf..aa63fdb7 100644 --- a/app/i18n/locales/nl.json +++ b/app/i18n/locales/nl.json @@ -330,7 +330,6 @@ "N_users": "{{n}} gebruikers", "N_channels": "{{n}} kanalen", "Name": "Naam", - "Navigation_history": "Navigatie geschiedenis", "Never": "Nooit", "New_Message": "Nieuw bericht", "New_Password": "Nieuw wachtwoord", @@ -782,5 +781,8 @@ "No_canned_responses": "Geen standaardantwoorden", "Send_email_confirmation": "Stuur e-mailbevestiging", "sending_email_confirmation": "e-mailbevestiging aan het verzenden", - "Enable_Message_Parser": "Berichtparser inschakelen" + "Enable_Message_Parser": "Berichtparser inschakelen", + "Unsupported_format": "Niet ondersteund formaat", + "Downloaded_file": "Gedownload bestand", + "Error_Download_file": "Fout tijdens het downloaden van bestand" } \ No newline at end of file diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 24905e9f..8aa2f5ae 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -309,7 +309,6 @@ "N_users": "{{n}} usuários", "N_channels": "{{n}} canais", "Name": "Nome", - "Navigation_history": "Histórico de navegação", "Never": "Nunca", "New_Message": "Nova Mensagem", "New_Password": "Nova Senha", @@ -737,4 +736,4 @@ "Unsupported_format": "Formato não suportado", "Downloaded_file": "Arquivo baixado", "Error_Download_file": "Erro ao baixar o arquivo" -} +} \ No newline at end of file diff --git a/app/i18n/locales/pt-PT.json b/app/i18n/locales/pt-PT.json index f3d23c51..f2cd45e7 100644 --- a/app/i18n/locales/pt-PT.json +++ b/app/i18n/locales/pt-PT.json @@ -329,7 +329,6 @@ "N_users": "{{n}} utilizadores", "N_channels": "{{n}} canais", "Name": "Nome", - "Navigation_history": "Histórico de navegação", "Never": "Nunca", "New_Message": "Nova Mensagem", "New_Password": "Nova Palavra-passe", diff --git a/app/i18n/locales/ru.json b/app/i18n/locales/ru.json index 071e8f3e..bdf2c161 100644 --- a/app/i18n/locales/ru.json +++ b/app/i18n/locales/ru.json @@ -330,7 +330,6 @@ "N_users": "{{n}} пользователи", "N_channels": "{{n}} каналов", "Name": "Имя", - "Navigation_history": "История навигации", "Never": "Никогда", "New_Message": "Новое сообщение", "New_Password": "Новый пароль", @@ -782,5 +781,8 @@ "No_canned_responses": "Нет заготовленных ответов", "Send_email_confirmation": "Отправить электронное письмо с подтверждением", "sending_email_confirmation": "отправка подтверждения по электронной почте", - "Enable_Message_Parser": "Включить парсер сообщений" + "Enable_Message_Parser": "Включить парсер сообщений", + "Unsupported_format": "Неподдерживаемый формат", + "Downloaded_file": "Скачанный файл", + "Error_Download_file": "Ошибка при скачивании файла" } \ No newline at end of file diff --git a/app/i18n/locales/tr.json b/app/i18n/locales/tr.json index aeb4caba..cc565192 100644 --- a/app/i18n/locales/tr.json +++ b/app/i18n/locales/tr.json @@ -323,7 +323,6 @@ "N_people_reacted": "{{n}} kişi tepki verdi", "N_users": "{{n}} kullanıcı", "Name": "İsim", - "Navigation_history": "Gezinti geçmişi", "Never": "Asla", "New_Message": "Yeni İleti", "New_Password": "Yeni Şifre", diff --git a/app/i18n/locales/zh-CN.json b/app/i18n/locales/zh-CN.json index e0136890..24e166b9 100644 --- a/app/i18n/locales/zh-CN.json +++ b/app/i18n/locales/zh-CN.json @@ -320,7 +320,6 @@ "N_people_reacted": "{{n}} 人回复", "N_users": "{{n}} 位用户", "Name": "名称", - "Navigation_history": "浏览历史记录", "Never": "从不", "New_Message": "新信息", "New_Password": "新密码", diff --git a/app/i18n/locales/zh-TW.json b/app/i18n/locales/zh-TW.json index 85d2690c..258f8fdf 100644 --- a/app/i18n/locales/zh-TW.json +++ b/app/i18n/locales/zh-TW.json @@ -321,7 +321,6 @@ "N_people_reacted": "{{n}} 人回复", "N_users": "{{n}} 位使用者", "Name": "名稱", - "Navigation_history": "瀏覽歷史記錄", "Never": "從不", "New_Message": "新訊息", "New_Password": "新密碼", diff --git a/app/index.tsx b/app/index.tsx index e6457e23..cc0a06a2 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -30,6 +30,8 @@ import InAppNotification from './containers/InAppNotification'; import { ActionSheetProvider } from './containers/ActionSheet'; import debounce from './utils/debounce'; import { isFDroidBuild } from './constants/environment'; +import { IThemePreference } from './definitions/ITheme'; +import { ICommand } from './definitions/ICommand'; RNScreens.enableScreens(); @@ -42,10 +44,7 @@ interface IDimensions { interface IState { theme: string; - themePreferences: { - currentTheme: 'automatic' | 'light'; - darkLevel: string; - }; + themePreferences: IThemePreference; width: number; height: number; scale: number; @@ -175,7 +174,7 @@ export default class Root extends React.Component<{}, IState> { setTheme = (newTheme = {}) => { // change theme state this.setState( - prevState => newThemeState(prevState, newTheme), + prevState => newThemeState(prevState, newTheme as IThemePreference), () => { const { themePreferences } = this.state; // subscribe to Appearance changes @@ -191,7 +190,7 @@ export default class Root extends React.Component<{}, IState> { initTablet = () => { const { width } = this.state; this.setMasterDetail(width); - this.onKeyCommands = KeyCommandsEmitter.addListener('onKeyCommand', (command: unknown) => { + this.onKeyCommands = KeyCommandsEmitter.addListener('onKeyCommand', (command: ICommand) => { EventEmitter.emit(KEY_COMMAND, { event: command }); }); }; diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index 589aa6bd..b680a919 100644 --- a/app/lib/methods/getPermissions.js +++ b/app/lib/methods/getPermissions.js @@ -55,7 +55,8 @@ const PERMISSIONS = [ 'convert-team', 'edit-omnichannel-contact', 'edit-livechat-room-customfields', - 'view-canned-responses' + 'view-canned-responses', + 'mobile-upload-file' ]; export async function setPermissions() { diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.js index 4dd84adf..c2fc9fcd 100644 --- a/app/lib/methods/subscriptions/rooms.js +++ b/app/lib/methods/subscriptions/rooms.js @@ -8,7 +8,6 @@ import messagesStatus from '../../../constants/messagesStatus'; import log from '../../../utils/log'; import random from '../../../utils/random'; import store from '../../createStore'; -import { roomsRequest } from '../../../actions/rooms'; import { handlePayloadUserInteraction } from '../actions'; import buildMessage from '../helpers/buildMessage'; import RocketChat from '../../rocketchat'; @@ -21,8 +20,6 @@ import { E2E_MESSAGE_TYPE } from '../../encryption/constants'; const removeListener = listener => listener.stop(); -let connectedListener; -let disconnectedListener; let streamListener; let subServer; let queue = {}; @@ -255,10 +252,6 @@ const debouncedUpdate = subscription => { }; export default function subscribeRooms() { - const handleConnection = () => { - store.dispatch(roomsRequest()); - }; - const handleStreamMessageReceived = protectedFunction(async ddpMessage => { const db = database.active; @@ -388,14 +381,6 @@ export default function subscribeRooms() { }); const stop = () => { - if (connectedListener) { - connectedListener.then(removeListener); - connectedListener = false; - } - if (disconnectedListener) { - disconnectedListener.then(removeListener); - disconnectedListener = false; - } if (streamListener) { streamListener.then(removeListener); streamListener = false; @@ -407,8 +392,6 @@ export default function subscribeRooms() { } }; - connectedListener = this.sdk.onStreamData('connected', handleConnection); - // disconnectedListener = this.sdk.onStreamData('close', handleConnection); streamListener = this.sdk.onStreamData('stream-notify-user', handleStreamMessageReceived); try { diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 7fd058d8..1cb627a4 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -24,7 +24,7 @@ import { selectServerFailure } from '../actions/server'; import { useSsl } from '../utils/url'; import EventEmitter from '../utils/events'; import { updatePermission } from '../actions/permissions'; -import { TEAM_TYPE } from '../definition/ITeam'; +import { TEAM_TYPE } from '../definitions/ITeam'; import { updateSettings } from '../actions/settings'; import { compareServerVersion, methods } from './utils'; import reduxStore from './createStore'; @@ -239,37 +239,34 @@ const RocketChat = { this.code = null; } - this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) }); + // The app can't reconnect if reopen interval is 5s while in development + this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server), reopen: __DEV__ ? 20000 : 5000 }); this.getSettings(); - const sdkConnect = () => - this.sdk - .connect() - .then(() => { - const { server: currentServer } = reduxStore.getState().server; - if (user && user.token && server === currentServer) { - reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError)); - } - }) - .catch(err => { - console.log('connect error', err); - - // when `connect` raises an error, we try again in 10 seconds - this.connectTimeout = setTimeout(() => { - if (this.sdk?.client?.host === server) { - sdkConnect(); - } - }, 10000); - }); - - sdkConnect(); + this.sdk + .connect() + .then(() => { + console.log('connected'); + }) + .catch(err => { + console.log('connect error', err); + }); this.connectingListener = this.sdk.onStreamData('connecting', () => { reduxStore.dispatch(connectRequest()); }); this.connectedListener = this.sdk.onStreamData('connected', () => { + const { connected } = reduxStore.getState().meteor; + if (connected) { + return; + } reduxStore.dispatch(connectSuccess()); + const { server: currentServer } = reduxStore.getState().server; + const { user } = reduxStore.getState().login; + if (user?.token && server === currentServer) { + reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError)); + } }); this.closeListener = this.sdk.onStreamData('close', () => { @@ -848,17 +845,21 @@ const RocketChat = { // RC 3.13.0 return this.post('teams.removeRoom', { roomId, teamId }); }, - leaveTeam({ teamName, rooms }) { + leaveTeam({ teamId, rooms }) { // RC 3.13.0 - return this.post('teams.leave', { teamName, rooms }); + return this.post('teams.leave', { + teamId, + // RC 4.2.0 + ...(rooms?.length && { rooms }) + }); }, - removeTeamMember({ teamId, teamName, userId, rooms }) { + removeTeamMember({ teamId, userId, rooms }) { // RC 3.13.0 return this.post('teams.removeMember', { teamId, - teamName, userId, - rooms + // RC 4.2.0 + ...(rooms?.length && { rooms }) }); }, updateTeamRoom({ roomId, isDefault }) { @@ -1151,10 +1152,6 @@ const RocketChat = { // RC 0.36.0 return this.methodCallWrapper('livechat:transfer', transferData); }, - getPagesLivechat(rid, offset) { - // RC 2.3.0 - return this.sdk.get(`livechat/visitors.pagesVisited/${rid}?count=50&offset=${offset}`); - }, getDepartmentInfo(departmentId) { // RC 2.2.0 return this.sdk.get(`livechat/department/${departmentId}?includeAgents=false`); @@ -1533,16 +1530,7 @@ const RocketChat = { return this.sdk.get(`${this.roomTypeToApiType(type)}.files`, { roomId, offset, - sort: { uploadedAt: -1 }, - fields: { - name: 1, - description: 1, - size: 1, - type: 1, - uploadedAt: 1, - url: 1, - userId: 1 - } + sort: { uploadedAt: -1 } }); }, getMessages(roomId, type, query, offset) { diff --git a/app/lib/userPreferences.js b/app/lib/userPreferences.ts similarity index 75% rename from app/lib/userPreferences.js rename to app/lib/userPreferences.ts index 3377856d..6f4818da 100644 --- a/app/lib/userPreferences.js +++ b/app/lib/userPreferences.ts @@ -7,11 +7,12 @@ const MMKV = new MMKVStorage.Loader() .initialize(); class UserPreferences { + private mmkv: MMKVStorage.API; constructor() { this.mmkv = MMKV; } - async getStringAsync(key) { + async getStringAsync(key: string) { try { const value = await this.mmkv.getStringAsync(key); return value; @@ -20,11 +21,11 @@ class UserPreferences { } } - setStringAsync(key, value) { + setStringAsync(key: string, value: string) { return this.mmkv.setStringAsync(key, value); } - async getBoolAsync(key) { + async getBoolAsync(key: string) { try { const value = await this.mmkv.getBoolAsync(key); return value; @@ -33,11 +34,11 @@ class UserPreferences { } } - setBoolAsync(key, value) { + setBoolAsync(key: string, value: boolean) { return this.mmkv.setBoolAsync(key, value); } - async getMapAsync(key) { + async getMapAsync(key: string) { try { const value = await this.mmkv.getMapAsync(key); return value; @@ -46,11 +47,11 @@ class UserPreferences { } } - setMapAsync(key, value) { + setMapAsync(key: string, value: object) { return this.mmkv.setMapAsync(key, value); } - removeItem(key) { + removeItem(key: string) { return this.mmkv.removeItem(key); } } diff --git a/app/navigationTypes.ts b/app/navigationTypes.ts new file mode 100644 index 00000000..cbf17f42 --- /dev/null +++ b/app/navigationTypes.ts @@ -0,0 +1,45 @@ +import { NavigatorScreenParams } from '@react-navigation/core'; + +import { ISubscription } from './definitions/ISubscription'; +import { IServer } from './definitions/IServer'; +import { IAttachment } from './definitions/IAttachment'; +import { MasterDetailInsideStackParamList } from './stacks/MasterDetailStack/types'; +import { OutsideParamList, InsideStackParamList } from './stacks/types'; + +export type SetUsernameStackParamList = { + SetUsernameView: { + title: string; + }; +}; + +export type StackParamList = { + AuthLoading: undefined; + OutsideStack: NavigatorScreenParams; + InsideStack: NavigatorScreenParams; + MasterDetailStack: NavigatorScreenParams; + SetUsernameStack: NavigatorScreenParams; +}; + +export type ShareInsideStackParamList = { + ShareListView: undefined; + ShareView: { + attachments: IAttachment[]; + isShareView?: boolean; + isShareExtension: boolean; + serverInfo: IServer; + text: string; + room: ISubscription; + thread: any; // TODO: Change + }; + SelectServerView: undefined; +}; + +export type ShareOutsideStackParamList = { + WithoutServersView: undefined; +}; + +export type ShareAppStackParamList = { + AuthLoading?: undefined; + OutsideStack?: NavigatorScreenParams; + InsideStack?: NavigatorScreenParams; +}; diff --git a/app/notifications/push/index.js b/app/notifications/push/index.js deleted file mode 100644 index 074e22bf..00000000 --- a/app/notifications/push/index.js +++ /dev/null @@ -1,50 +0,0 @@ -import EJSON from 'ejson'; - -import store from '../../lib/createStore'; -import { deepLinkingOpen } from '../../actions/deepLinking'; -import { isFDroidBuild } from '../../constants/environment'; -import PushNotification from './push'; - -export const onNotification = notification => { - if (notification) { - const data = notification.getData(); - if (data) { - try { - const { rid, name, sender, type, host, messageType, messageId } = EJSON.parse(data.ejson); - - const types = { - c: 'channel', - d: 'direct', - p: 'group', - l: 'channels' - }; - let roomName = type === 'd' ? sender.username : name; - if (type === 'l') { - roomName = sender.name; - } - - const params = { - host, - rid, - messageId, - path: `${types[type]}/${roomName}`, - isCall: messageType === 'jitsi_call_started' - }; - store.dispatch(deepLinkingOpen(params)); - } catch (e) { - console.warn(e); - } - } - } -}; - -export const getDeviceToken = () => PushNotification.getDeviceToken(); -export const setBadgeCount = count => PushNotification.setBadgeCount(count); -export const initializePushNotifications = () => { - if (!isFDroidBuild) { - setBadgeCount(); - return PushNotification.configure({ - onNotification - }); - } -}; diff --git a/app/notifications/push/index.ts b/app/notifications/push/index.ts new file mode 100644 index 00000000..af25c9ee --- /dev/null +++ b/app/notifications/push/index.ts @@ -0,0 +1,57 @@ +import EJSON from 'ejson'; + +import store from '../../lib/createStore'; +import { deepLinkingOpen } from '../../actions/deepLinking'; +import { isFDroidBuild } from '../../constants/environment'; +import PushNotification from './push'; +import { INotification, SubscriptionType } from '../../definitions'; + +interface IEjson { + rid: string; + name: string; + sender: { username: string; name: string }; + type: string; + host: string; + messageType: string; + messageId: string; +} + +export const onNotification = (notification: INotification): void => { + if (notification) { + try { + const { rid, name, sender, type, host, messageType, messageId }: IEjson = EJSON.parse(notification.ejson); + + const types: Record = { + c: 'channel', + d: 'direct', + p: 'group', + l: 'channels' + }; + let roomName = type === SubscriptionType.DIRECT ? sender.username : name; + if (type === SubscriptionType.OMNICHANNEL) { + roomName = sender.name; + } + + const params = { + host, + rid, + messageId, + path: `${types[type]}/${roomName}`, + isCall: messageType === 'jitsi_call_started' + }; + // TODO REDUX MIGRATION TO TS + store.dispatch(deepLinkingOpen(params)); + } catch (e) { + console.warn(e); + } + } +}; + +export const getDeviceToken = (): string => PushNotification.getDeviceToken(); +export const setBadgeCount = (count?: number): void => PushNotification.setBadgeCount(count); +export const initializePushNotifications = (): Promise | undefined => { + if (!isFDroidBuild) { + setBadgeCount(); + return PushNotification.configure(onNotification); + } +}; diff --git a/app/notifications/push/push.android.js b/app/notifications/push/push.android.js deleted file mode 100644 index 51e767ad..00000000 --- a/app/notifications/push/push.android.js +++ /dev/null @@ -1,32 +0,0 @@ -import { NotificationsAndroid, PendingNotifications } from 'react-native-notifications'; - -class PushNotification { - constructor() { - this.onRegister = null; - this.onNotification = null; - this.deviceToken = null; - - NotificationsAndroid.setRegistrationTokenUpdateListener(deviceToken => { - this.deviceToken = deviceToken; - }); - - NotificationsAndroid.setNotificationOpenedListener(notification => { - this.onNotification(notification); - }); - } - - getDeviceToken() { - return this.deviceToken; - } - - setBadgeCount = () => {}; - - configure(params) { - this.onRegister = params.onRegister; - this.onNotification = params.onNotification; - NotificationsAndroid.refreshToken(); - return PendingNotifications.getInitialNotification(); - } -} - -export default new PushNotification(); diff --git a/app/notifications/push/push.ios.js b/app/notifications/push/push.ios.js deleted file mode 100644 index 068be4ee..00000000 --- a/app/notifications/push/push.ios.js +++ /dev/null @@ -1,61 +0,0 @@ -import NotificationsIOS, { NotificationAction, NotificationCategory } from 'react-native-notifications'; - -import reduxStore from '../../lib/createStore'; -import I18n from '../../i18n'; - -const replyAction = new NotificationAction({ - activationMode: 'background', - title: I18n.t('Reply'), - textInput: { - buttonTitle: I18n.t('Reply'), - placeholder: I18n.t('Type_message') - }, - identifier: 'REPLY_ACTION' -}); - -class PushNotification { - constructor() { - this.onRegister = null; - this.onNotification = null; - this.deviceToken = null; - - NotificationsIOS.addEventListener('remoteNotificationsRegistered', deviceToken => { - this.deviceToken = deviceToken; - }); - - NotificationsIOS.addEventListener('notificationOpened', (notification, completion) => { - const { background } = reduxStore.getState().app; - if (background) { - this.onNotification(notification); - } - completion(); - }); - - const actions = []; - actions.push( - new NotificationCategory({ - identifier: 'MESSAGE', - actions: [replyAction] - }) - ); - NotificationsIOS.requestPermissions(actions); - } - - getDeviceToken() { - return this.deviceToken; - } - - setBadgeCount = (count = 0) => { - NotificationsIOS.setBadgesCount(count); - }; - - async configure(params) { - this.onRegister = params.onRegister; - this.onNotification = params.onNotification; - - const initial = await NotificationsIOS.getInitialNotification(); - // NotificationsIOS.consumeBackgroundQueue(); - return Promise.resolve(initial); - } -} -export default new PushNotification(); diff --git a/app/notifications/push/push.ios.ts b/app/notifications/push/push.ios.ts new file mode 100644 index 00000000..92c2d3a6 --- /dev/null +++ b/app/notifications/push/push.ios.ts @@ -0,0 +1,63 @@ +// @ts-ignore +// TODO BUMP LIB VERSION +import NotificationsIOS, { NotificationAction, NotificationCategory, Notification } from 'react-native-notifications'; + +import reduxStore from '../../lib/createStore'; +import I18n from '../../i18n'; +import { INotification } from '../../definitions/INotification'; + +class PushNotification { + onNotification: (notification: Notification) => void; + deviceToken: string; + + constructor() { + this.onNotification = () => {}; + this.deviceToken = ''; + + NotificationsIOS.addEventListener('remoteNotificationsRegistered', (deviceToken: string) => { + this.deviceToken = deviceToken; + }); + + NotificationsIOS.addEventListener('notificationOpened', (notification: Notification, completion: () => void) => { + // TODO REDUX MIGRATION TO TS + const { background } = reduxStore.getState().app; + if (background) { + this.onNotification(notification?.getData()); + } + completion(); + }); + + const actions = [ + new NotificationCategory({ + identifier: 'MESSAGE', + actions: [ + new NotificationAction({ + activationMode: 'background', + title: I18n.t('Reply'), + textInput: { + buttonTitle: I18n.t('Reply'), + placeholder: I18n.t('Type_message') + }, + identifier: 'REPLY_ACTION' + }) + ] + }) + ]; + NotificationsIOS.requestPermissions(actions); + } + + getDeviceToken() { + return this.deviceToken; + } + + setBadgeCount = (count = 0) => { + NotificationsIOS.setBadgesCount(count); + }; + + async configure(onNotification: (notification: INotification) => void) { + this.onNotification = onNotification; + const initial = await NotificationsIOS.getInitialNotification(); + return Promise.resolve(initial); + } +} +export default new PushNotification(); diff --git a/app/notifications/push/push.ts b/app/notifications/push/push.ts new file mode 100644 index 00000000..16aa8cf5 --- /dev/null +++ b/app/notifications/push/push.ts @@ -0,0 +1,36 @@ +// @ts-ignore +// TODO BUMP LIB VERSION +import { NotificationsAndroid, PendingNotifications, Notification } from 'react-native-notifications'; + +import { INotification } from '../../definitions/INotification'; + +class PushNotification { + onNotification: (notification: Notification) => void; + deviceToken: string; + constructor() { + this.onNotification = () => {}; + this.deviceToken = ''; + + NotificationsAndroid.setRegistrationTokenUpdateListener((deviceToken: string) => { + this.deviceToken = deviceToken; + }); + + NotificationsAndroid.setNotificationOpenedListener((notification: Notification) => { + this.onNotification(notification?.getData()); + }); + } + + getDeviceToken() { + return this.deviceToken; + } + + setBadgeCount = (_?: number) => {}; + + configure(onNotification: (notification: INotification) => void) { + this.onNotification = onNotification; + NotificationsAndroid.refreshToken(); + return PendingNotifications.getInitialNotification(); + } +} + +export default new PushNotification(); diff --git a/app/presentation/DirectoryItem/index.tsx b/app/presentation/DirectoryItem/index.tsx index b8d9811a..234c1e31 100644 --- a/app/presentation/DirectoryItem/index.tsx +++ b/app/presentation/DirectoryItem/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Text, View } from 'react-native'; +import { Text, View, ViewStyle } from 'react-native'; import Touch from '../../utils/touch'; import Avatar from '../../containers/Avatar'; @@ -10,7 +10,7 @@ import { themes } from '../../constants/colors'; export { ROW_HEIGHT }; interface IDirectoryItemLabel { - text: string; + text?: string; theme: string; } @@ -21,9 +21,9 @@ interface IDirectoryItem { type: string; onPress(): void; testID: string; - style: any; - rightLabel: string; - rid: string; + style?: ViewStyle; + rightLabel?: string; + rid?: string; theme: string; teamMain?: boolean; } @@ -32,7 +32,7 @@ const DirectoryItemLabel = React.memo(({ text, theme }: IDirectoryItemLabel) => if (!text) { return null; } - return {text}; + return {text}; }); const DirectoryItem = ({ diff --git a/app/presentation/KeyboardView.tsx b/app/presentation/KeyboardView.tsx index 5319df82..aa4f1182 100644 --- a/app/presentation/KeyboardView.tsx +++ b/app/presentation/KeyboardView.tsx @@ -4,7 +4,7 @@ import { KeyboardAwareScrollView, KeyboardAwareScrollViewProps } from '@codler/r import scrollPersistTaps from '../utils/scrollPersistTaps'; interface IKeyboardViewProps extends KeyboardAwareScrollViewProps { - keyboardVerticalOffset: number; + keyboardVerticalOffset?: number; scrollEnabled?: boolean; children: React.ReactNode; } diff --git a/app/presentation/RoomItem/Actions.tsx b/app/presentation/RoomItem/Actions.tsx index 19c63baa..2b53955a 100644 --- a/app/presentation/RoomItem/Actions.tsx +++ b/app/presentation/RoomItem/Actions.tsx @@ -5,7 +5,7 @@ import { RectButton } from 'react-native-gesture-handler'; import { isRTL } from '../../i18n'; import { CustomIcon } from '../../lib/Icons'; import { themes } from '../../constants/colors'; -import { DISPLAY_MODE_CONDENSED } from '../../constants/constantDisplayMode'; +import { DisplayMode } from '../../constants/constantDisplayMode'; import styles, { ACTION_WIDTH, LONG_SWIPE, ROW_HEIGHT_CONDENSED } from './styles'; interface ILeftActions { @@ -40,7 +40,7 @@ export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleR reverse ); - const isCondensed = displayMode === DISPLAY_MODE_CONDENSED; + const isCondensed = displayMode === DisplayMode.Condensed; const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null; return ( @@ -87,7 +87,7 @@ export const RightActions = React.memo( reverse ); - const isCondensed = displayMode === DISPLAY_MODE_CONDENSED; + const isCondensed = displayMode === DisplayMode.Condensed; const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null; return ( diff --git a/app/presentation/RoomItem/IconOrAvatar.js b/app/presentation/RoomItem/IconOrAvatar.js index cedd3b0f..29343477 100644 --- a/app/presentation/RoomItem/IconOrAvatar.js +++ b/app/presentation/RoomItem/IconOrAvatar.js @@ -3,7 +3,7 @@ import { View } from 'react-native'; import PropTypes from 'prop-types'; import Avatar from '../../containers/Avatar'; -import { DISPLAY_MODE_CONDENSED, DISPLAY_MODE_EXPANDED } from '../../constants/constantDisplayMode'; +import { DisplayMode } from '../../constants/constantDisplayMode'; import TypeIcon from './TypeIcon'; import styles from './styles'; @@ -22,11 +22,11 @@ const IconOrAvatar = ({ }) => { if (showAvatar) { return ( - + ); } - if (displayMode === DISPLAY_MODE_EXPANDED && showLastMessage) { + if (displayMode === DisplayMode.Expanded && showLastMessage) { return ( - {showLastMessage && displayMode === DISPLAY_MODE_EXPANDED ? ( + {showLastMessage && displayMode === DisplayMode.Expanded ? ( <> {showAvatar ? ( diff --git a/app/presentation/RoomItem/Wrapper.tsx b/app/presentation/RoomItem/Wrapper.tsx index cb4d6e1b..30c3283d 100644 --- a/app/presentation/RoomItem/Wrapper.tsx +++ b/app/presentation/RoomItem/Wrapper.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { View } from 'react-native'; import { themes } from '../../constants/colors'; -import { DISPLAY_MODE_CONDENSED } from '../../constants/constantDisplayMode'; +import { DisplayMode } from '../../constants/constantDisplayMode'; import IconOrAvatar from './IconOrAvatar'; import styles from './styles'; @@ -25,7 +25,7 @@ interface IWrapper { const Wrapper = ({ accessibilityLabel, theme, children, displayMode, ...props }: IWrapper) => ( {children} diff --git a/app/presentation/UserItem.tsx b/app/presentation/UserItem.tsx index 3dadc2bf..b2e9d0b1 100644 --- a/app/presentation/UserItem.tsx +++ b/app/presentation/UserItem.tsx @@ -46,7 +46,7 @@ interface IUserItem { testID: string; onLongPress?: () => void; style?: StyleProp; - icon: string; + icon?: string | null; theme: string; } diff --git a/app/reducers/activeUsers.js b/app/reducers/activeUsers.js deleted file mode 100644 index 8f6c5b38..00000000 --- a/app/reducers/activeUsers.js +++ /dev/null @@ -1,15 +0,0 @@ -import { SET_ACTIVE_USERS } from '../actions/actionsTypes'; - -const initialState = {}; - -export default function activeUsers(state = initialState, action) { - switch (action.type) { - case SET_ACTIVE_USERS: - return { - ...state, - ...action.activeUsers - }; - default: - return state; - } -} diff --git a/app/reducers/activeUsers.test.ts b/app/reducers/activeUsers.test.ts new file mode 100644 index 00000000..fbe35207 --- /dev/null +++ b/app/reducers/activeUsers.test.ts @@ -0,0 +1,16 @@ +import { setActiveUsers } from '../actions/activeUsers'; +import { IActiveUsers, initialState } from './activeUsers'; +import { mockedStore } from './mockedStore'; + +describe('test reducer', () => { + it('should return initial state', () => { + const state = mockedStore.getState().activeUsers; + expect(state).toEqual(initialState); + }); + it('should return modified store after action', () => { + const activeUsers: IActiveUsers = { any: { status: 'online', statusText: 'any' } }; + mockedStore.dispatch(setActiveUsers(activeUsers)); + const state = mockedStore.getState().activeUsers; + expect(state).toEqual({ ...activeUsers }); + }); +}); diff --git a/app/reducers/activeUsers.ts b/app/reducers/activeUsers.ts new file mode 100644 index 00000000..9877a5ce --- /dev/null +++ b/app/reducers/activeUsers.ts @@ -0,0 +1,26 @@ +import { TApplicationActions } from '../definitions'; +import { SET_ACTIVE_USERS } from '../actions/actionsTypes'; + +type TUserStatus = 'online' | 'offline'; +export interface IActiveUser { + status: TUserStatus; + statusText?: string; +} + +export interface IActiveUsers { + [key: string]: IActiveUser; +} + +export const initialState: IActiveUsers = {}; + +export default function activeUsers(state = initialState, action: TApplicationActions): IActiveUsers { + switch (action.type) { + case SET_ACTIVE_USERS: + return { + ...state, + ...action.activeUsers + }; + default: + return state; + } +} diff --git a/app/reducers/mockedStore.ts b/app/reducers/mockedStore.ts new file mode 100644 index 00000000..5a03297f --- /dev/null +++ b/app/reducers/mockedStore.ts @@ -0,0 +1,7 @@ +import { applyMiddleware, compose, createStore } from 'redux'; +import createSagaMiddleware from 'redux-saga'; + +import reducers from '.'; + +const enhancers = compose(applyMiddleware(createSagaMiddleware())); +export const mockedStore = createStore(reducers, enhancers); diff --git a/app/reducers/selectedUsers.test.ts b/app/reducers/selectedUsers.test.ts new file mode 100644 index 00000000..329be4f9 --- /dev/null +++ b/app/reducers/selectedUsers.test.ts @@ -0,0 +1,36 @@ +import { addUser, reset, setLoading, removeUser } from '../actions/selectedUsers'; +import { mockedStore } from './mockedStore'; +import { initialState } from './selectedUsers'; + +describe('test selectedUsers reducer', () => { + it('should return initial state', () => { + const state = mockedStore.getState().selectedUsers; + expect(state).toEqual(initialState); + }); + + it('should return modified store after addUser', () => { + const user = { _id: 'xxx', name: 'xxx', fname: 'xxx' }; + mockedStore.dispatch(addUser(user)); + const state = mockedStore.getState().selectedUsers.users; + expect(state).toEqual([user]); + }); + + it('should return empty store after remove user', () => { + const user = { _id: 'xxx', name: 'xxx', fname: 'xxx' }; + mockedStore.dispatch(removeUser(user)); + const state = mockedStore.getState().selectedUsers.users; + expect(state).toEqual([]); + }); + + it('should return initial state after reset', () => { + mockedStore.dispatch(reset()); + const state = mockedStore.getState().selectedUsers; + expect(state).toEqual(initialState); + }); + + it('should return loading after call action', () => { + mockedStore.dispatch(setLoading(true)); + const state = mockedStore.getState().selectedUsers.loading; + expect(state).toEqual(true); + }); +}); diff --git a/app/reducers/selectedUsers.js b/app/reducers/selectedUsers.ts similarity index 55% rename from app/reducers/selectedUsers.js rename to app/reducers/selectedUsers.ts index 42d7982c..f6573ac9 100644 --- a/app/reducers/selectedUsers.js +++ b/app/reducers/selectedUsers.ts @@ -1,11 +1,26 @@ +import { TApplicationActions } from '../definitions'; import { SELECTED_USERS } from '../actions/actionsTypes'; -const initialState = { +export interface ISelectedUser { + _id: string; + name: string; + fname: string; + search?: boolean; + // username is used when is from searching + username?: string; +} + +export interface ISelectedUsers { + users: ISelectedUser[]; + loading: boolean; +} + +export const initialState: ISelectedUsers = { users: [], loading: false }; -export default function (state = initialState, action) { +export default function (state = initialState, action: TApplicationActions): ISelectedUsers { switch (action.type) { case SELECTED_USERS.ADD_USER: return { diff --git a/app/reducers/sortPreferences.js b/app/reducers/sortPreferences.js index 31b50185..4ad9e797 100644 --- a/app/reducers/sortPreferences.js +++ b/app/reducers/sortPreferences.js @@ -1,13 +1,13 @@ import { SORT_PREFERENCES } from '../actions/actionsTypes'; -import { DISPLAY_MODE_EXPANDED } from '../constants/constantDisplayMode'; +import { DisplayMode, SortBy } from '../constants/constantDisplayMode'; const initialState = { - sortBy: 'activity', + sortBy: SortBy.Activity, groupByType: false, showFavorites: false, showUnread: false, showAvatar: true, - displayMode: DISPLAY_MODE_EXPANDED + displayMode: DisplayMode.Expanded }; export default (state = initialState, action) => { diff --git a/app/sagas/room.js b/app/sagas/room.js index e3437a4a..f45bf123 100644 --- a/app/sagas/room.js +++ b/app/sagas/room.js @@ -67,7 +67,7 @@ const handleLeaveRoom = function* handleLeaveRoom({ room, roomType, selected }) if (roomType === 'channel') { result = yield RocketChat.leaveRoom(room.rid, room.t); } else if (roomType === 'team') { - result = yield RocketChat.leaveTeam({ teamName: room.name, ...(selected && { rooms: selected }) }); + result = yield RocketChat.leaveTeam({ teamId: room.teamId, ...(selected && { rooms: selected }) }); } if (result?.success) { diff --git a/app/share.tsx b/app/share.tsx index ceb85477..fbfcd0b5 100644 --- a/app/share.tsx +++ b/app/share.tsx @@ -14,6 +14,7 @@ import { defaultHeader, getActiveRouteName, navigationTheme, themedHeader } from import RocketChat, { THEME_PREFERENCES_KEY } from './lib/rocketchat'; import { ThemeContext } from './theme'; import { localAuthenticate } from './utils/localAuthentication'; +import { IThemePreference } from './definitions/ITheme'; import ScreenLockedView from './views/ScreenLockedView'; // Outside Stack import WithoutServersView from './views/WithoutServersView'; @@ -25,6 +26,7 @@ import { setCurrentScreen } from './utils/log'; import AuthLoadingView from './views/AuthLoadingView'; import { DimensionsContext } from './dimensions'; import debounce from './utils/debounce'; +import { ShareInsideStackParamList, ShareOutsideStackParamList, ShareAppStackParamList } from './navigationTypes'; interface IDimensions { width: number; @@ -35,10 +37,7 @@ interface IDimensions { interface IState { theme: string; - themePreferences: { - currentTheme: 'automatic' | 'light'; - darkLevel: string; - }; + themePreferences: IThemePreference; root: any; width: number; height: number; @@ -46,7 +45,7 @@ interface IState { fontScale: number; } -const Inside = createStackNavigator(); +const Inside = createStackNavigator(); const InsideStack = () => { const { theme } = useContext(ThemeContext); @@ -65,24 +64,19 @@ const InsideStack = () => { ); }; -const Outside = createStackNavigator(); +const Outside = createStackNavigator(); const OutsideStack = () => { const { theme } = useContext(ThemeContext); return ( - + ); }; // App -const Stack = createStackNavigator(); +const Stack = createStackNavigator(); export const App = ({ root }: any) => ( <> @@ -112,7 +106,7 @@ class Root extends React.Component<{}, IState> { this.init(); } - componentWillUnmount() { + componentWillUnmount(): void { RocketChat.closeShareExtension(); unsubscribeTheme(); } @@ -139,7 +133,7 @@ class Root extends React.Component<{}, IState> { setTheme = (newTheme = {}) => { // change theme state this.setState( - prevState => newThemeState(prevState, newTheme), + prevState => newThemeState(prevState, newTheme as IThemePreference), () => { const { themePreferences } = this.state; // subscribe to Appearance changes diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.tsx similarity index 82% rename from app/stacks/InsideStack.js rename to app/stacks/InsideStack.tsx index 800c44e5..ec3ae318 100644 --- a/app/stacks/InsideStack.js +++ b/app/stacks/InsideStack.tsx @@ -1,12 +1,11 @@ import React from 'react'; import { I18nManager } from 'react-native'; -import { createStackNavigator } from '@react-navigation/stack'; +import { createStackNavigator, StackNavigationOptions } from '@react-navigation/stack'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { ThemeContext } from '../theme'; import { ModalAnimation, StackAnimation, defaultHeader, themedHeader } from '../utils/navigation'; import Sidebar from '../views/SidebarView'; - // Chats Stack import RoomView from '../views/RoomView'; import RoomsListView from '../views/RoomsListView'; @@ -22,7 +21,6 @@ import MessagesView from '../views/MessagesView'; import AutoTranslateView from '../views/AutoTranslateView'; import DirectoryView from '../views/DirectoryView'; import NotificationPrefView from '../views/NotificationPreferencesView'; -import VisitorNavigationView from '../views/VisitorNavigationView'; import ForwardLivechatView from '../views/ForwardLivechatView'; import LivechatEditView from '../views/LivechatEditView'; import PickerView from '../views/PickerView'; @@ -37,10 +35,8 @@ import { themes } from '../constants/colors'; import ProfileView from '../views/ProfileView'; import UserPreferencesView from '../views/UserPreferencesView'; import UserNotificationPrefView from '../views/UserNotificationPreferencesView'; - // Display Preferences View import DisplayPrefsView from '../views/DisplayPrefsView'; - // Settings Stack import SettingsView from '../views/SettingsView'; import SecurityPrivacyView from '../views/SecurityPrivacyView'; @@ -49,21 +45,16 @@ import LanguageView from '../views/LanguageView'; import ThemeView from '../views/ThemeView'; import DefaultBrowserView from '../views/DefaultBrowserView'; import ScreenLockConfigView from '../views/ScreenLockConfigView'; - // Admin Stack import AdminPanelView from '../views/AdminPanelView'; - // NewMessage Stack import NewMessageView from '../views/NewMessageView'; import CreateChannelView from '../views/CreateChannelView'; - // E2ESaveYourPassword Stack import E2ESaveYourPasswordView from '../views/E2ESaveYourPasswordView'; import E2EHowItWorksView from '../views/E2EHowItWorksView'; - // E2EEnterYourPassword Stack import E2EEnterYourPasswordView from '../views/E2EEnterYourPasswordView'; - // InsideStackNavigator import AttachmentView from '../views/AttachmentView'; import ModalBlockView from '../views/ModalBlockView'; @@ -76,13 +67,26 @@ import AddChannelTeamView from '../views/AddChannelTeamView'; import AddExistingChannelView from '../views/AddExistingChannelView'; import SelectListView from '../views/SelectListView'; import DiscussionsView from '../views/DiscussionsView'; +import { + AdminPanelStackParamList, + ChatsStackParamList, + DisplayPrefStackParamList, + DrawerParamList, + E2EEnterYourPasswordStackParamList, + E2ESaveYourPasswordStackParamList, + InsideStackParamList, + NewMessageStackParamList, + ProfileStackParamList, + SettingsStackParamList +} from './types'; // ChatsStackNavigator -const ChatsStack = createStackNavigator(); +const ChatsStack = createStackNavigator(); const ChatsStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + @@ -111,11 +115,6 @@ const ChatsStackNavigator = () => { component={NotificationPrefView} options={NotificationPrefView.navigationOptions} /> - { component={ThreadMessagesView} options={ThreadMessagesView.navigationOptions} /> - + - + { - - + + ); }; // ProfileStackNavigator -const ProfileStack = createStackNavigator(); +const ProfileStack = createStackNavigator(); const ProfileStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + - + { }; // SettingsStackNavigator -const SettingsStack = createStackNavigator(); +const SettingsStack = createStackNavigator(); const SettingsStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + - + { }; // AdminPanelStackNavigator -const AdminPanelStack = createStackNavigator(); +const AdminPanelStack = createStackNavigator(); const AdminPanelStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + ); }; // DisplayPreferenceNavigator -const DisplayPrefStack = createStackNavigator(); +const DisplayPrefStack = createStackNavigator(); const DisplayPrefStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + ); }; // DrawerNavigator -const Drawer = createDrawerNavigator(); +const Drawer = createDrawerNavigator(); const DrawerNavigator = () => { const { theme } = React.useContext(ThemeContext); @@ -259,12 +242,13 @@ const DrawerNavigator = () => { }; // NewMessageStackNavigator -const NewMessageStack = createStackNavigator(); +const NewMessageStack = createStackNavigator(); const NewMessageStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + { }; // E2ESaveYourPasswordStackNavigator -const E2ESaveYourPasswordStack = createStackNavigator(); +const E2ESaveYourPasswordStack = createStackNavigator(); const E2ESaveYourPasswordStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + { }; // E2EEnterYourPasswordStackNavigator -const E2EEnterYourPasswordStack = createStackNavigator(); +const E2EEnterYourPasswordStack = createStackNavigator(); const E2EEnterYourPasswordStackNavigator = () => { const { theme } = React.useContext(ThemeContext); return ( - + { }; // InsideStackNavigator -const InsideStack = createStackNavigator(); +const InsideStack = createStackNavigator(); const InsideStackNavigator = () => { const { theme } = React.useContext(ThemeContext); diff --git a/app/stacks/MasterDetailStack/ModalContainer.js b/app/stacks/MasterDetailStack/ModalContainer.js deleted file mode 100644 index 7be11f8c..00000000 --- a/app/stacks/MasterDetailStack/ModalContainer.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { StyleSheet, TouchableWithoutFeedback, View } from 'react-native'; -import PropTypes from 'prop-types'; - -import sharedStyles from '../../views/Styles'; -import { themes } from '../../constants/colors'; - -const styles = StyleSheet.create({ - root: { - flex: 1, - alignItems: 'center', - justifyContent: 'center' - }, - backdrop: { - ...StyleSheet.absoluteFill - } -}); - -export const ModalContainer = ({ navigation, children, theme }) => ( - - navigation.pop()}> - - - {children} - -); - -ModalContainer.propTypes = { - navigation: PropTypes.object, - children: PropTypes.element, - theme: PropTypes.string -}; diff --git a/app/stacks/MasterDetailStack/ModalContainer.tsx b/app/stacks/MasterDetailStack/ModalContainer.tsx new file mode 100644 index 00000000..376ff876 --- /dev/null +++ b/app/stacks/MasterDetailStack/ModalContainer.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { StyleSheet, TouchableWithoutFeedback, useWindowDimensions, View } from 'react-native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { NavigationContainerProps } from '@react-navigation/core'; + +import sharedStyles from '../../views/Styles'; +import { themes } from '../../constants/colors'; + +interface IModalContainer extends NavigationContainerProps { + navigation: StackNavigationProp; + children: React.ReactNode; + theme: string; +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + alignItems: 'center', + justifyContent: 'center' + }, + backdrop: { + ...StyleSheet.absoluteFillObject + } +}); + +export const ModalContainer = ({ navigation, children, theme }: IModalContainer): JSX.Element => { + const { height } = useWindowDimensions(); + const modalHeight = sharedStyles.modalFormSheet.height; + return ( + + navigation.pop()}> + + + height ? height : modalHeight + }}> + {children} + + + ); +}; diff --git a/app/stacks/MasterDetailStack/index.js b/app/stacks/MasterDetailStack/index.tsx similarity index 86% rename from app/stacks/MasterDetailStack/index.js rename to app/stacks/MasterDetailStack/index.tsx index 5537d14d..2cb94671 100644 --- a/app/stacks/MasterDetailStack/index.js +++ b/app/stacks/MasterDetailStack/index.tsx @@ -1,12 +1,10 @@ import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; import { useIsFocused } from '@react-navigation/native'; -import { createStackNavigator } from '@react-navigation/stack'; +import { createStackNavigator, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { ThemeContext } from '../../theme'; import { FadeFromCenterModal, StackAnimation, defaultHeader, themedHeader } from '../../utils/navigation'; - // Chats Stack import RoomView from '../../views/RoomView'; import RoomsListView from '../../views/RoomsListView'; @@ -22,7 +20,6 @@ import MessagesView from '../../views/MessagesView'; import AutoTranslateView from '../../views/AutoTranslateView'; import DirectoryView from '../../views/DirectoryView'; import NotificationPrefView from '../../views/NotificationPreferencesView'; -import VisitorNavigationView from '../../views/VisitorNavigationView'; import ForwardLivechatView from '../../views/ForwardLivechatView'; import CannedResponsesListView from '../../views/CannedResponsesListView'; import CannedResponseDetail from '../../views/CannedResponseDetail'; @@ -46,7 +43,6 @@ import UserPreferencesView from '../../views/UserPreferencesView'; import UserNotificationPrefView from '../../views/UserNotificationPreferencesView'; import SecurityPrivacyView from '../../views/SecurityPrivacyView'; import E2EEncryptionSecurityView from '../../views/E2EEncryptionSecurityView'; - // InsideStackNavigator import AttachmentView from '../../views/AttachmentView'; import ModalBlockView from '../../views/ModalBlockView'; @@ -64,9 +60,15 @@ import AddExistingChannelView from '../../views/AddExistingChannelView'; import SelectListView from '../../views/SelectListView'; import DiscussionsView from '../../views/DiscussionsView'; import { ModalContainer } from './ModalContainer'; +import { + MasterDetailChatsStackParamList, + MasterDetailDrawerParamList, + MasterDetailInsideStackParamList, + ModalStackParamList +} from './types'; // ChatsStackNavigator -const ChatsStack = createStackNavigator(); +const ChatsStack = createStackNavigator(); const ChatsStackNavigator = React.memo(() => { const { theme } = React.useContext(ThemeContext); @@ -83,28 +85,35 @@ const ChatsStackNavigator = React.memo(() => { }, [isFocused]); return ( - + ); }); // DrawerNavigator -const Drawer = createDrawerNavigator(); +const Drawer = createDrawerNavigator(); const DrawerNavigator = React.memo(() => ( } drawerType='permanent'> )); -const ModalStack = createStackNavigator(); -const ModalStackNavigator = React.memo(({ navigation }) => { +export interface INavigation { + navigation: StackNavigationProp; +} + +const ModalStack = createStackNavigator(); +const ModalStackNavigator = React.memo(({ navigation }: INavigation) => { const { theme } = React.useContext(ThemeContext); return ( - + { /> - + { component={NotificationPrefView} options={NotificationPrefView.navigationOptions} /> - - - + + @@ -228,21 +220,13 @@ const ModalStackNavigator = React.memo(({ navigation }) => { component={E2EEnterYourPasswordView} options={E2EEnterYourPasswordView.navigationOptions} /> - + - + { ); }); -ModalStackNavigator.propTypes = { - navigation: PropTypes.object -}; - // InsideStackNavigator -const InsideStack = createStackNavigator(); +const InsideStack = createStackNavigator(); const InsideStackNavigator = React.memo(() => { const { theme } = React.useContext(ThemeContext); return ( - + diff --git a/app/stacks/MasterDetailStack/types.ts b/app/stacks/MasterDetailStack/types.ts new file mode 100644 index 00000000..242c13ba --- /dev/null +++ b/app/stacks/MasterDetailStack/types.ts @@ -0,0 +1,200 @@ +import { TextInputProps } from 'react-native'; +import { NavigatorScreenParams } from '@react-navigation/core'; + +import { IAttachment } from '../../definitions/IAttachment'; +import { IMessage } from '../../definitions/IMessage'; +import { ISubscription, SubscriptionType } from '../../definitions/ISubscription'; + +export type MasterDetailChatsStackParamList = { + RoomView: { + rid: string; + t: SubscriptionType; + tmid?: string; + message?: string; + name?: string; + fname?: string; + prid?: string; + room: ISubscription; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; + }; +}; + +export type MasterDetailDrawerParamList = { + ChatsStackNavigator: NavigatorScreenParams; +}; + +export type ModalStackParamList = { + RoomActionsView: { + room: ISubscription; + member: any; + rid: string; + t: SubscriptionType; + joined: boolean; + }; + RoomInfoView: { + room: ISubscription; + member: any; + rid: string; + t: SubscriptionType; + }; + SelectListView: { + data: any; + title: string; + infoText: string; + nextAction: Function; + showAlert: boolean; + isSearch: boolean; + onSearch: Function; + isRadio?: boolean; + }; + RoomInfoEditView: { + rid: string; + }; + RoomMembersView: { + rid: string; + room: ISubscription; + }; + SearchMessagesView: { + rid: string; + t: SubscriptionType; + encrypted?: boolean; + showCloseModal?: boolean; + }; + SelectedUsersView: { + maxUsers: number; + showButton: boolean; + title: string; + buttonText: string; + nextAction: Function; + }; + InviteUsersView: { + rid: string; + }; + AddChannelTeamView: { + teamId?: string; + teamChannels: []; // TODO: Change + }; + AddExistingChannelView: { + teamId?: boolean; + }; + InviteUsersEditView: { + rid: string; + }; + MessagesView: { + rid: string; + t: SubscriptionType; + name: string; + }; + AutoTranslateView: { + rid: string; + room: ISubscription; + }; + DirectoryView: undefined; + QueueListView: undefined; + NotificationPrefView: { + rid: string; + room: ISubscription; + }; + ForwardLivechatView: { + rid: string; + }; + CannedResponsesListView: { + rid: string; + }; + CannedResponseDetail: { + cannedResponse: { + shortcut: string; + text: string; + scopeName: string; + tags: string[]; + }; + room: ISubscription; + }; + LivechatEditView: { + room: ISubscription; + roomUser: any; // TODO: Change + }; + PickerView: { + title: string; + data: []; // TODO: Change + value: any; // TODO: Change + onChangeText: TextInputProps['onChangeText']; + goBack: Function; + onChangeValue: Function; + }; + ThreadMessagesView: { + rid: string; + t: SubscriptionType; + }; + TeamChannelsView: { + teamId: string; + }; + MarkdownTableView: { + renderRows: Function; + tableWidth: number; + }; + ReadReceiptsView: { + messageId: string; + }; + SettingsView: undefined; + LanguageView: undefined; + ThemeView: undefined; + DefaultBrowserView: undefined; + ScreenLockConfigView: undefined; + StatusView: undefined; + ProfileView: undefined; + DisplayPrefsView: undefined; + AdminPanelView: undefined; + NewMessageView: undefined; + SelectedUsersViewCreateChannel: { + maxUsers: number; + showButton: boolean; + title: string; + buttonText: string; + nextAction: Function; + }; // TODO: Change + CreateChannelView: { + isTeam?: boolean; // TODO: To check + teamId?: string; + }; + CreateDiscussionView: { + channel: ISubscription; + message: IMessage; + showCloseModal: boolean; + }; + E2ESaveYourPasswordView: undefined; + E2EHowItWorksView: { + showCloseModal: boolean; + }; + E2EEnterYourPasswordView: undefined; + UserPreferencesView: undefined; + UserNotificationPrefView: undefined; + SecurityPrivacyView: undefined; + E2EEncryptionSecurityView: undefined; +}; + +export type MasterDetailInsideStackParamList = { + DrawerNavigator: NavigatorScreenParams>; // TODO: Change + ModalStackNavigator: NavigatorScreenParams; + AttachmentView: { + attachment: IAttachment; + }; + ModalBlockView: { + data: any; // TODO: Change + }; + JitsiMeetView: { + rid: string; + url: string; + onlyAudio?: boolean; + }; + ShareView: { + attachments: IAttachment[]; + isShareView?: boolean; + serverInfo: {}; + text: string; + room: ISubscription; + thread: any; // TODO: Change + }; +}; diff --git a/app/stacks/OutsideStack.js b/app/stacks/OutsideStack.tsx similarity index 81% rename from app/stacks/OutsideStack.js rename to app/stacks/OutsideStack.tsx index 392850c3..fb791330 100644 --- a/app/stacks/OutsideStack.js +++ b/app/stacks/OutsideStack.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import { createStackNavigator } from '@react-navigation/stack'; +import { createStackNavigator, StackNavigationOptions } from '@react-navigation/stack'; import { connect } from 'react-redux'; import { ThemeContext } from '../theme'; import { ModalAnimation, StackAnimation, defaultHeader, themedHeader } from '../utils/navigation'; - // Outside Stack import NewServerView from '../views/NewServerView'; import WorkspaceView from '../views/WorkspaceView'; @@ -14,37 +13,34 @@ import SendEmailConfirmationView from '../views/SendEmailConfirmationView'; import RegisterView from '../views/RegisterView'; import LegalView from '../views/LegalView'; import AuthenticationWebView from '../views/AuthenticationWebView'; +import { OutsideModalParamList, OutsideParamList } from './types'; // Outside -const Outside = createStackNavigator(); +const Outside = createStackNavigator(); const _OutsideStack = () => { const { theme } = React.useContext(ThemeContext); return ( - + - + ); }; -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ root: state.app.root }); const OutsideStack = connect(mapStateToProps)(_OutsideStack); // OutsideStackModal -const OutsideModal = createStackNavigator(); +const OutsideModal = createStackNavigator(); const OutsideStackModal = () => { const { theme } = React.useContext(ThemeContext); diff --git a/app/stacks/types.ts b/app/stacks/types.ts new file mode 100644 index 00000000..c6016f2e --- /dev/null +++ b/app/stacks/types.ts @@ -0,0 +1,272 @@ +import { NavigatorScreenParams } from '@react-navigation/core'; +import { TextInputProps } from 'react-native'; +import Model from '@nozbe/watermelondb/Model'; + +import { IOptionsField } from '../views/NotificationPreferencesView/options'; +import { IServer } from '../definitions/IServer'; +import { IAttachment } from '../definitions/IAttachment'; +import { IMessage } from '../definitions/IMessage'; +import { ISubscription, SubscriptionType, TSubscriptionModel } from '../definitions/ISubscription'; + +export type ChatsStackParamList = { + RoomsListView: undefined; + RoomView: { + rid: string; + t: SubscriptionType; + tmid?: string; + message?: string; + name?: string; + fname?: string; + prid?: string; + room?: ISubscription; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; + }; + RoomActionsView: { + room: ISubscription; + member: any; + rid: string; + t: SubscriptionType; + joined: boolean; + }; + SelectListView: { + data: any; + title: string; + infoText: string; + nextAction: Function; + showAlert: boolean; + isSearch: boolean; + onSearch: Function; + isRadio?: boolean; + }; + RoomInfoView: { + room: ISubscription; + member: any; + rid: string; + t: SubscriptionType; + }; + RoomInfoEditView: { + rid: string; + }; + RoomMembersView: { + rid: string; + room: ISubscription; + }; + SearchMessagesView: { + rid: string; + t: SubscriptionType; + encrypted?: boolean; + showCloseModal?: boolean; + }; + SelectedUsersView: { + maxUsers?: number; + showButton?: boolean; + title?: string; + buttonText?: string; + nextAction?: Function; + }; + InviteUsersView: { + rid: string; + }; + InviteUsersEditView: { + rid: string; + }; + MessagesView: { + rid: string; + t: SubscriptionType; + name: string; + }; + AutoTranslateView: { + rid: string; + room: TSubscriptionModel; + }; + DirectoryView: undefined; + NotificationPrefView: { + rid: string; + room: Model; + }; + ForwardLivechatView: { + rid: string; + }; + LivechatEditView: { + room: ISubscription; + roomUser: any; // TODO: Change + }; + PickerView: { + title: string; + data: IOptionsField[]; + value?: any; // TODO: Change + onChangeText?: ((text: string) => IOptionsField[]) | ((term?: string) => Promise); + goBack?: boolean; + onChangeValue: Function; + }; + ThreadMessagesView: { + rid: string; + t: SubscriptionType; + }; + TeamChannelsView: { + teamId: string; + }; + CreateChannelView: { + isTeam?: boolean; // TODO: To check + teamId?: string; + }; + AddChannelTeamView: { + teamId?: string; + teamChannels: []; // TODO: Change + }; + AddExistingChannelView: { + teamId?: string; + teamChannels: []; // TODO: Change + }; + MarkdownTableView: { + renderRows: (drawExtraBorders?: boolean) => JSX.Element; + tableWidth: number; + }; + ReadReceiptsView: { + messageId: string; + }; + QueueListView: undefined; + CannedResponsesListView: { + rid: string; + }; + CannedResponseDetail: { + cannedResponse: { + shortcut: string; + text: string; + scopeName: string; + tags: string[]; + }; + room: ISubscription; + }; +}; + +export type ProfileStackParamList = { + ProfileView: undefined; + UserPreferencesView: undefined; + UserNotificationPrefView: undefined; + PickerView: { + title: string; + data: IOptionsField[]; + value: any; // TODO: Change + onChangeText?: TextInputProps['onChangeText']; + goBack?: Function; + onChangeValue: Function; + }; +}; + +export type SettingsStackParamList = { + SettingsView: undefined; + SecurityPrivacyView: undefined; + E2EEncryptionSecurityView: undefined; + LanguageView: undefined; + ThemeView: undefined; + DefaultBrowserView: undefined; + ScreenLockConfigView: undefined; + ProfileView: undefined; + DisplayPrefsView: undefined; +}; + +export type AdminPanelStackParamList = { + AdminPanelView: undefined; +}; + +export type DisplayPrefStackParamList = { + DisplayPrefsView: undefined; +}; + +export type DrawerParamList = { + ChatsStackNavigator: NavigatorScreenParams; + ProfileStackNavigator: NavigatorScreenParams; + SettingsStackNavigator: NavigatorScreenParams; + AdminPanelStackNavigator: NavigatorScreenParams; + DisplayPrefStackNavigator: NavigatorScreenParams; +}; + +export type NewMessageStackParamList = { + NewMessageView: undefined; + SelectedUsersViewCreateChannel: { + maxUsers?: number; + showButton?: boolean; + title?: string; + buttonText?: string; + nextAction?: Function; + }; // TODO: Change + CreateChannelView: { + isTeam?: boolean; // TODO: To check + teamId?: string; + }; + CreateDiscussionView: { + channel: ISubscription; + message: IMessage; + showCloseModal: boolean; + }; +}; + +export type E2ESaveYourPasswordStackParamList = { + E2ESaveYourPasswordView: undefined; + E2EHowItWorksView?: { + showCloseModal?: boolean; + }; +}; + +export type E2EEnterYourPasswordStackParamList = { + E2EEnterYourPasswordView: undefined; +}; + +export type InsideStackParamList = { + DrawerNavigator: NavigatorScreenParams; + NewMessageStackNavigator: NavigatorScreenParams; + E2ESaveYourPasswordStackNavigator: NavigatorScreenParams; + E2EEnterYourPasswordStackNavigator: NavigatorScreenParams; + AttachmentView: { + attachment: IAttachment; + }; + StatusView: undefined; + ShareView: { + attachments: IAttachment[]; + isShareView?: boolean; + isShareExtension: boolean; + serverInfo: IServer; + text: string; + room: ISubscription; + thread: any; // TODO: Change + }; + ModalBlockView: { + data: any; // TODO: Change; + }; + JitsiMeetView: { + rid: string; + url: string; + onlyAudio?: boolean; + }; +}; + +export type OutsideParamList = { + NewServerView: undefined; + WorkspaceView: undefined; + LoginView: { + title: string; + username?: string; + }; + ForgotPasswordView: { + title: string; + }; + SendEmailConfirmationView: { + user?: string; + }; + RegisterView: { + title: string; + }; + LegalView: undefined; +}; + +export type OutsideModalParamList = { + OutsideStack: NavigatorScreenParams; + AuthenticationWebView: { + authType: string; + url: string; + ssoToken?: string; + }; +}; diff --git a/app/theme.tsx b/app/theme.tsx index 8618dbd9..6bfd248d 100644 --- a/app/theme.tsx +++ b/app/theme.tsx @@ -1,18 +1,17 @@ import React from 'react'; import hoistNonReactStatics from 'hoist-non-react-statics'; +import { IThemePreference } from './definitions/ITheme'; + interface IThemeContextProps { theme: string; - themePreferences?: { - currentTheme: 'automatic' | 'light'; - darkLevel: string; - }; + themePreferences?: IThemePreference; setTheme?: (newTheme?: {}) => void; } export const ThemeContext = React.createContext({ theme: 'light' }); -export function withTheme(Component: React.ComponentType): (props: any) => JSX.Element { +export function withTheme(Component: any): any { const ThemedComponent = (props: any) => ( {contexts => } ); diff --git a/app/utils/appGroup.js b/app/utils/appGroup.ts similarity index 83% rename from app/utils/appGroup.js rename to app/utils/appGroup.ts index 63fb428a..f92227c0 100644 --- a/app/utils/appGroup.js +++ b/app/utils/appGroup.ts @@ -4,7 +4,7 @@ import { isIOS } from './deviceInfo'; const { AppGroup } = NativeModules; -const appGroup = { +const appGroup: { path: string } = { path: isIOS ? AppGroup.path : '' }; diff --git a/app/utils/avatar.js b/app/utils/avatar.ts similarity index 72% rename from app/utils/avatar.js rename to app/utils/avatar.ts index 4cc15cda..7e4b2819 100644 --- a/app/utils/avatar.js +++ b/app/utils/avatar.ts @@ -1,6 +1,8 @@ import { compareServerVersion, methods } from '../lib/utils'; +import { SubscriptionType } from '../definitions/ISubscription'; +import { IAvatar } from '../containers/Avatar/interfaces'; -const formatUrl = (url, size, query) => `${url}?format=png&size=${size}${query}`; +const formatUrl = (url: string, size: number, query: string) => `${url}?format=png&size=${size}${query}`; export const avatarURL = ({ type, @@ -13,9 +15,9 @@ export const avatarURL = ({ rid, blockUnauthenticatedAccess, serverVersion -}) => { +}: IAvatar): string => { let room; - if (type === 'd') { + if (type === SubscriptionType.DIRECT) { room = text; } else if (rid && !compareServerVersion(serverVersion, '3.6.0', methods.lowerThan)) { room = `room/${rid}`; diff --git a/app/utils/base64-js/index.js b/app/utils/base64-js/index.ts similarity index 87% rename from app/utils/base64-js/index.js rename to app/utils/base64-js/index.ts index 5616f71d..71fac91c 100644 --- a/app/utils/base64-js/index.js +++ b/app/utils/base64-js/index.ts @@ -1,8 +1,8 @@ /* eslint-disable no-bitwise */ // https://github.com/beatgammit/base64-js/blob/master/index.js -const lookup = []; -const revLookup = []; +const lookup: string[] = []; +const revLookup: number[] = []; const Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array; const code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; @@ -16,7 +16,7 @@ for (let i = 0, len = code.length; i < len; i += 1) { revLookup['-'.charCodeAt(0)] = 62; revLookup['_'.charCodeAt(0)] = 63; -const getLens = b64 => { +const getLens = (b64: string) => { const len = b64.length; // We're encoding some strings not multiple of 4, so, disable this check @@ -37,16 +37,17 @@ const getLens = b64 => { }; // base64 is 4/3 + up to two characters of the original data -export const byteLength = b64 => { +export const byteLength = (b64: string) => { const lens = getLens(b64); const validLen = lens[0]; const placeHoldersLen = lens[1]; return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; }; -const _byteLength = (b64, validLen, placeHoldersLen) => ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; +const _byteLength = (b64: string, validLen: number, placeHoldersLen: number) => + ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; -export const toByteArray = b64 => { +export const toByteArray = (b64: string) => { let tmp; const lens = getLens(b64); const validLen = lens[0]; @@ -92,10 +93,10 @@ export const toByteArray = b64 => { return arr; }; -const tripletToBase64 = num => +const tripletToBase64 = (num: number) => lookup[(num >> 18) & 0x3f] + lookup[(num >> 12) & 0x3f] + lookup[(num >> 6) & 0x3f] + lookup[num & 0x3f]; -const encodeChunk = (uint8, start, end) => { +const encodeChunk = (uint8: number[] | Uint8Array, start: number, end: number) => { let tmp; const output = []; for (let i = start; i < end; i += 3) { @@ -105,7 +106,7 @@ const encodeChunk = (uint8, start, end) => { return output.join(''); }; -export const fromByteArray = uint8 => { +export const fromByteArray = (uint8: number[] | Uint8Array) => { let tmp; const len = uint8.length; const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes diff --git a/app/utils/debounce.js b/app/utils/debounce.js deleted file mode 100644 index 106c61d0..00000000 --- a/app/utils/debounce.js +++ /dev/null @@ -1,20 +0,0 @@ -export default function debounce(func, wait, immediate) { - let timeout; - function _debounce(...args) { - const context = this; - const later = function __debounce() { - timeout = null; - if (!immediate) { - func.apply(context, args); - } - }; - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - func.apply(context, args); - } - } - _debounce.stop = () => clearTimeout(timeout); - return _debounce; -} diff --git a/app/utils/debounce.ts b/app/utils/debounce.ts new file mode 100644 index 00000000..e0c28b23 --- /dev/null +++ b/app/utils/debounce.ts @@ -0,0 +1,22 @@ +export default function debounce(func: Function, wait?: number, immediate?: boolean) { + let timeout: number | null; + function _debounce(...args: any[]) { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-this-alias + const context = this; + const later = function __debounce() { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + const callNow = immediate && !timeout; + clearTimeout(timeout!); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + } + _debounce.stop = () => clearTimeout(timeout!); + return _debounce; +} diff --git a/app/utils/deviceInfo.js b/app/utils/deviceInfo.ts similarity index 92% rename from app/utils/deviceInfo.js rename to app/utils/deviceInfo.ts index 7961b440..9cb7ac72 100644 --- a/app/utils/deviceInfo.js +++ b/app/utils/deviceInfo.ts @@ -9,7 +9,7 @@ export const getBundleId = DeviceInfo.getBundleId(); export const getDeviceModel = DeviceInfo.getModel(); // Theme is supported by system on iOS 13+ or Android 10+ -export const supportSystemTheme = () => { +export const supportSystemTheme = (): boolean => { const systemVersion = parseInt(DeviceInfo.getSystemVersion(), 10); return systemVersion >= (isIOS ? 13 : 10); }; diff --git a/app/utils/events.js b/app/utils/events.ts similarity index 53% rename from app/utils/events.js rename to app/utils/events.ts index 8e67fc82..fc0b975a 100644 --- a/app/utils/events.js +++ b/app/utils/events.ts @@ -1,11 +1,25 @@ +import { ICommand } from '../definitions/ICommand'; import log from './log'; +type TEventEmitterEmmitArgs = + | { rid: string } + | { message: string } + | { method: string } + | { invalid: boolean } + | { force: boolean } + | { hasBiometry: boolean } + | { event: string | ICommand } + | { cancel: () => void } + | { submit: (param: string) => void }; + class EventEmitter { + private events: { [key: string]: any }; + constructor() { this.events = {}; } - addEventListener(event, listener) { + addEventListener(event: string, listener: Function) { if (typeof this.events[event] !== 'object') { this.events[event] = []; } @@ -13,7 +27,7 @@ class EventEmitter { return listener; } - removeListener(event, listener) { + removeListener(event: string, listener: Function) { if (typeof this.events[event] === 'object') { const idx = this.events[event].indexOf(listener); if (idx > -1) { @@ -25,9 +39,9 @@ class EventEmitter { } } - emit(event, ...args) { + emit(event: string, ...args: TEventEmitterEmmitArgs[]) { if (typeof this.events[event] === 'object') { - this.events[event].forEach(listener => { + this.events[event].forEach((listener: Function) => { try { listener.apply(this, args); } catch (e) { diff --git a/app/utils/fetch.js b/app/utils/fetch.ts similarity index 75% rename from app/utils/fetch.js rename to app/utils/fetch.ts index 84f5669a..c8758da8 100644 --- a/app/utils/fetch.js +++ b/app/utils/fetch.ts @@ -4,15 +4,20 @@ import { settings as RocketChatSettings } from '@rocket.chat/sdk'; import RocketChat from '../lib/rocketchat'; +interface CustomHeaders { + 'User-Agent': string; + Authorization?: string; +} + // this form is required by Rocket.Chat's parser in "app/statistics/server/lib/UAParserCustom.js" -export const headers = { +export const headers: CustomHeaders = { 'User-Agent': `RC Mobile; ${ Platform.OS } ${DeviceInfo.getSystemVersion()}; v${DeviceInfo.getVersion()} (${DeviceInfo.getBuildNumber()})` }; let _basicAuth; -export const setBasicAuth = basicAuth => { +export const setBasicAuth = (basicAuth: string): void => { _basicAuth = basicAuth; if (basicAuth) { RocketChatSettings.customHeaders = { ...headers, Authorization: `Basic ${_basicAuth}` }; @@ -24,12 +29,15 @@ export const BASIC_AUTH_KEY = 'BASIC_AUTH_KEY'; RocketChatSettings.customHeaders = headers; -export default (url, options = {}) => { +export default (url: string, options: { headers?: Headers; signal?: AbortSignal } = {}): Promise => { let customOptions = { ...options, headers: RocketChatSettings.customHeaders }; if (options && options.headers) { customOptions = { ...customOptions, headers: { ...options.headers, ...customOptions.headers } }; } + // TODO: Refactor when migrate rocketchat.js + // @ts-ignore if (RocketChat.controller) { + // @ts-ignore const { signal } = RocketChat.controller; customOptions = { ...customOptions, signal }; } diff --git a/app/utils/fileDownload/index.ts b/app/utils/fileDownload/index.ts index dda1a78f..279d3b3a 100644 --- a/app/utils/fileDownload/index.ts +++ b/app/utils/fileDownload/index.ts @@ -5,13 +5,7 @@ import EventEmitter from '../events'; import { LISTENER } from '../../containers/Toast'; import I18n from '../../i18n'; import { DOCUMENTS_PATH, DOWNLOAD_PATH } from '../../constants/localPath'; - -interface IAttachment { - title: string; - title_link: string; - type: string; - description: string; -} +import { IAttachment } from '../../definitions/IAttachment'; export const getLocalFilePathFromFile = (localPath: string, attachment: IAttachment): string => `${localPath}${attachment.title}`; diff --git a/app/utils/fileUpload/index.ios.js b/app/utils/fileUpload/index.ios.ts similarity index 65% rename from app/utils/fileUpload/index.ios.js rename to app/utils/fileUpload/index.ios.ts index a9764055..ae5cfabc 100644 --- a/app/utils/fileUpload/index.ios.js +++ b/app/utils/fileUpload/index.ios.ts @@ -1,19 +1,25 @@ +import { IFileUpload } from './interfaces'; + class Upload { + public xhr: XMLHttpRequest; + + public formData: FormData; + constructor() { this.xhr = new XMLHttpRequest(); this.formData = new FormData(); } - then = callback => { + then = (callback: (param: { respInfo: XMLHttpRequest }) => XMLHttpRequest) => { this.xhr.onload = () => callback({ respInfo: this.xhr }); this.xhr.send(this.formData); }; - catch = callback => { + catch = (callback: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null) => { this.xhr.onerror = callback; }; - uploadProgress = callback => { + uploadProgress = (callback: (param: number, arg1: number) => any) => { this.xhr.upload.onprogress = ({ total, loaded }) => callback(loaded, total); }; @@ -24,7 +30,7 @@ class Upload { } class FileUpload { - fetch = (method, url, headers, data) => { + fetch = (method: string, url: string, headers: { [x: string]: string }, data: IFileUpload[]) => { const upload = new Upload(); upload.xhr.open(method, url); @@ -35,6 +41,7 @@ class FileUpload { data.forEach(item => { if (item.uri) { upload.formData.append(item.name, { + // @ts-ignore uri: item.uri, type: item.type, name: item.filename diff --git a/app/utils/fileUpload/index.android.js b/app/utils/fileUpload/index.ts similarity index 64% rename from app/utils/fileUpload/index.android.js rename to app/utils/fileUpload/index.ts index 5c45c27b..1d2bdb31 100644 --- a/app/utils/fileUpload/index.android.js +++ b/app/utils/fileUpload/index.ts @@ -1,7 +1,11 @@ import RNFetchBlob from 'rn-fetch-blob'; +import { IFileUpload } from './interfaces'; + +type TMethods = 'POST' | 'GET' | 'DELETE' | 'PUT' | 'post' | 'get' | 'delete' | 'put'; + class FileUpload { - fetch = (method, url, headers, data) => { + fetch = (method: TMethods, url: string, headers: { [key: string]: string }, data: IFileUpload[]) => { const formData = data.map(item => { if (item.uri) { return { diff --git a/app/utils/fileUpload/interfaces.ts b/app/utils/fileUpload/interfaces.ts new file mode 100644 index 00000000..a3002f72 --- /dev/null +++ b/app/utils/fileUpload/interfaces.ts @@ -0,0 +1,7 @@ +export interface IFileUpload { + name: string; + uri?: string; + type: string; + filename: string; + data: any; +} diff --git a/app/utils/goRoom.js b/app/utils/goRoom.ts similarity index 58% rename from app/utils/goRoom.js rename to app/utils/goRoom.ts index 1025a17d..dc8a3188 100644 --- a/app/utils/goRoom.js +++ b/app/utils/goRoom.ts @@ -1,7 +1,17 @@ +import { ChatsStackParamList } from '../stacks/types'; import Navigation from '../lib/Navigation'; import RocketChat from '../lib/rocketchat'; +import { ISubscription, SubscriptionType } from '../definitions/ISubscription'; -const navigate = ({ item, isMasterDetail, ...props }) => { +const navigate = ({ + item, + isMasterDetail, + ...props +}: { + item: IItem; + isMasterDetail: boolean; + navigationMethod?: () => ChatsStackParamList; +}) => { let navigationMethod = props.navigationMethod ?? Navigation.navigate; if (isMasterDetail) { @@ -20,7 +30,22 @@ const navigate = ({ item, isMasterDetail, ...props }) => { }); }; -export const goRoom = async ({ item = {}, isMasterDetail = false, ...props }) => { +interface IItem extends Partial { + rid: string; + name: string; + t: SubscriptionType; +} + +export const goRoom = async ({ + item, + isMasterDetail = false, + ...props +}: { + item: IItem; + isMasterDetail: boolean; + navigationMethod?: any; + jumpToMessageId?: string; +}): Promise => { if (item.t === 'd' && item.search) { // if user is using the search we need first to join/create room try { @@ -30,8 +55,8 @@ export const goRoom = async ({ item = {}, isMasterDetail = false, ...props }) => return navigate({ item: { rid: result.room._id, - name: username, - t: 'd' + name: username!, + t: SubscriptionType.DIRECT }, isMasterDetail, ...props diff --git a/app/utils/info.js b/app/utils/info.js deleted file mode 100644 index 5d72f200..00000000 --- a/app/utils/info.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Alert } from 'react-native'; - -import I18n from '../i18n'; - -export const showErrorAlert = (message, title, onPress = () => {}) => - Alert.alert(title, message, [{ text: 'OK', onPress }], { cancelable: true }); - -export const showConfirmationAlert = ({ title, message, confirmationText, dismissText = I18n.t('Cancel'), onPress, onCancel }) => - Alert.alert( - title || I18n.t('Are_you_sure_question_mark'), - message, - [ - { - text: dismissText, - onPress: onCancel, - style: 'cancel' - }, - { - text: confirmationText, - style: 'destructive', - onPress - } - ], - { cancelable: false } - ); diff --git a/app/utils/info.ts b/app/utils/info.ts new file mode 100644 index 00000000..da882ee4 --- /dev/null +++ b/app/utils/info.ts @@ -0,0 +1,41 @@ +import { Alert } from 'react-native'; + +import I18n from '../i18n'; + +export const showErrorAlert = (message: string, title?: string, onPress = () => {}): void => + Alert.alert(title!, message, [{ text: 'OK', onPress }], { cancelable: true }); + +interface IShowConfirmationAlert { + title?: string; + message: string; + confirmationText: string; + dismissText?: string; + onPress: () => void; + onCancel?: () => void; +} + +export const showConfirmationAlert = ({ + title, + message, + confirmationText, + dismissText = I18n.t('Cancel'), + onPress, + onCancel +}: IShowConfirmationAlert): void => + Alert.alert( + title || I18n.t('Are_you_sure_question_mark'), + message, + [ + { + text: dismissText, + onPress: onCancel, + style: 'cancel' + }, + { + text: confirmationText, + style: 'destructive', + onPress + } + ], + { cancelable: false } + ); diff --git a/app/utils/isReadOnly.js b/app/utils/isReadOnly.ts similarity index 59% rename from app/utils/isReadOnly.js rename to app/utils/isReadOnly.ts index 62ae4fff..d94b73c4 100644 --- a/app/utils/isReadOnly.js +++ b/app/utils/isReadOnly.ts @@ -1,16 +1,21 @@ import RocketChat from '../lib/rocketchat'; import reduxStore from '../lib/createStore'; +import { ISubscription } from '../definitions/ISubscription'; -const canPostReadOnly = async ({ rid }) => { +const canPostReadOnly = async ({ rid }: { rid: string }) => { // TODO: this is not reactive. If this permission changes, the component won't be updated const postReadOnlyPermission = reduxStore.getState().permissions['post-readonly']; const permission = await RocketChat.hasPermission([postReadOnlyPermission], rid); return permission[0]; }; -const isMuted = (room, user) => room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username); +const isMuted = (room: ISubscription, user: { username: string }) => + room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username); -export const isReadOnly = async (room, user) => { +export const isReadOnly = async ( + room: ISubscription, + user: { id?: string; username: string; token?: string } +): Promise => { if (room.archived) { return true; } diff --git a/app/utils/isValidEmail.js b/app/utils/isValidEmail.ts similarity index 78% rename from app/utils/isValidEmail.js rename to app/utils/isValidEmail.ts index a8bd490f..e230fc32 100644 --- a/app/utils/isValidEmail.js +++ b/app/utils/isValidEmail.ts @@ -1,4 +1,4 @@ -export default function isValidEmail(email) { +export default function isValidEmail(email: string): boolean { /* eslint-disable no-useless-escape */ const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; diff --git a/app/utils/layoutAnimation.js b/app/utils/layoutAnimation.ts similarity index 100% rename from app/utils/layoutAnimation.js rename to app/utils/layoutAnimation.ts diff --git a/app/utils/localAuthentication.js b/app/utils/localAuthentication.ts similarity index 73% rename from app/utils/localAuthentication.js rename to app/utils/localAuthentication.ts index 29f25685..f4359963 100644 --- a/app/utils/localAuthentication.js +++ b/app/utils/localAuthentication.ts @@ -16,16 +16,17 @@ import { } from '../constants/localAuthentication'; import I18n from '../i18n'; import { setLocalAuthenticated } from '../actions/login'; +import { TServerModel } from '../definitions/IServer'; import EventEmitter from './events'; import { isIOS } from './deviceInfo'; -export const saveLastLocalAuthenticationSession = async (server, serverRecord) => { +export const saveLastLocalAuthenticationSession = async (server: string, serverRecord?: TServerModel): Promise => { const serversDB = database.servers; const serversCollection = serversDB.get('servers'); - await serversDB.action(async () => { + await serversDB.write(async () => { try { if (!serverRecord) { - serverRecord = await serversCollection.find(server); + serverRecord = (await serversCollection.find(server)) as TServerModel; } await serverRecord.update(record => { record.lastLocalAuthenticatedSession = new Date(); @@ -36,31 +37,31 @@ export const saveLastLocalAuthenticationSession = async (server, serverRecord) = }); }; -export const resetAttempts = () => AsyncStorage.multiRemove([LOCKED_OUT_TIMER_KEY, ATTEMPTS_KEY]); +export const resetAttempts = (): Promise => AsyncStorage.multiRemove([LOCKED_OUT_TIMER_KEY, ATTEMPTS_KEY]); -const openModal = hasBiometry => - new Promise(resolve => { +const openModal = (hasBiometry: boolean) => + new Promise(resolve => { EventEmitter.emit(LOCAL_AUTHENTICATE_EMITTER, { submit: () => resolve(), hasBiometry }); }); -const openChangePasscodeModal = ({ force }) => - new Promise((resolve, reject) => { +const openChangePasscodeModal = ({ force }: { force: boolean }) => + new Promise((resolve, reject) => { EventEmitter.emit(CHANGE_PASSCODE_EMITTER, { - submit: passcode => resolve(passcode), + submit: (passcode: string) => resolve(passcode), cancel: () => reject(), force }); }); -export const changePasscode = async ({ force = false }) => { +export const changePasscode = async ({ force = false }: { force: boolean }): Promise => { const passcode = await openChangePasscodeModal({ force }); await UserPreferences.setStringAsync(PASSCODE_KEY, sha256(passcode)); }; -export const biometryAuth = force => +export const biometryAuth = (force?: boolean): Promise => LocalAuthentication.authenticateAsync({ disableDeviceFallback: true, cancelLabel: force ? I18n.t('Dont_activate') : I18n.t('Local_authentication_biometry_fallback'), @@ -71,11 +72,11 @@ export const biometryAuth = force => * It'll help us to get the permission to use FaceID * and enable/disable the biometry when user put their first passcode */ -const checkBiometry = async serverRecord => { +const checkBiometry = async (serverRecord: TServerModel) => { const serversDB = database.servers; const result = await biometryAuth(true); - await serversDB.action(async () => { + await serversDB.write(async () => { try { await serverRecord.update(record => { record.biometry = !!result?.success; @@ -86,7 +87,13 @@ const checkBiometry = async serverRecord => { }); }; -export const checkHasPasscode = async ({ force = true, serverRecord }) => { +export const checkHasPasscode = async ({ + force = true, + serverRecord +}: { + force?: boolean; + serverRecord: TServerModel; +}): Promise<{ newPasscode?: boolean } | void> => { const storedPasscode = await UserPreferences.getStringAsync(PASSCODE_KEY); if (!storedPasscode) { await changePasscode({ force }); @@ -96,13 +103,13 @@ export const checkHasPasscode = async ({ force = true, serverRecord }) => { return Promise.resolve(); }; -export const localAuthenticate = async server => { +export const localAuthenticate = async (server: string): Promise => { const serversDB = database.servers; const serversCollection = serversDB.get('servers'); - let serverRecord; + let serverRecord: TServerModel; try { - serverRecord = await serversCollection.find(server); + serverRecord = (await serversCollection.find(server)) as TServerModel; } catch (error) { return Promise.reject(); } @@ -125,7 +132,7 @@ export const localAuthenticate = async server => { const diffToLastSession = moment().diff(serverRecord?.lastLocalAuthenticatedSession, 'seconds'); // if last authenticated session is older than configured auto lock time, authentication is required - if (diffToLastSession >= serverRecord?.autoLockTime) { + if (diffToLastSession >= serverRecord.autoLockTime!) { // set isLocalAuthenticated to false store.dispatch(setLocalAuthenticated(false)); @@ -150,7 +157,7 @@ export const localAuthenticate = async server => { } }; -export const supportedBiometryLabel = async () => { +export const supportedBiometryLabel = async (): Promise => { try { const enrolled = await LocalAuthentication.isEnrolledAsync(); diff --git a/app/utils/log/events.js b/app/utils/log/events.ts similarity index 99% rename from app/utils/log/events.js rename to app/utils/log/events.ts index fc7a3497..82dd3079 100644 --- a/app/utils/log/events.js +++ b/app/utils/log/events.ts @@ -253,7 +253,6 @@ export default { RA_GO_AUTOTRANSLATE: 'ra_go_autotranslate', RA_GO_NOTIFICATIONPREF: 'ra_go_notification_pref', RA_GO_FORWARDLIVECHAT: 'ra_go_forward_livechat', - RA_GO_VISITORNAVIGATION: 'ra_go_visitor_navigation', RA_SHARE: 'ra_share', RA_LEAVE: 'ra_leave', RA_LEAVE_F: 'ra_leave_f', diff --git a/app/utils/log/index.js b/app/utils/log/index.ts similarity index 66% rename from app/utils/log/index.js rename to app/utils/log/index.ts index 41074832..d52bd0e9 100644 --- a/app/utils/log/index.js +++ b/app/utils/log/index.ts @@ -4,13 +4,13 @@ import { isFDroidBuild } from '../../constants/environment'; import events from './events'; const analytics = firebaseAnalytics || ''; -let bugsnag = ''; -let crashlytics; +let bugsnag: any = ''; +let crashlytics: any; let reportCrashErrors = true; let reportAnalyticsEvents = true; -export const getReportCrashErrorsValue = () => reportCrashErrors; -export const getReportAnalyticsEventsValue = () => reportAnalyticsEvents; +export const getReportCrashErrorsValue = (): boolean => reportCrashErrors; +export const getReportAnalyticsEventsValue = (): boolean => reportAnalyticsEvents; if (!isFDroidBuild) { bugsnag = require('@bugsnag/react-native').default; @@ -18,7 +18,7 @@ if (!isFDroidBuild) { onBreadcrumb() { return reportAnalyticsEvents; }, - onError(error) { + onError(error: { breadcrumbs: string[] }) { if (!reportAnalyticsEvents) { error.breadcrumbs = []; } @@ -34,13 +34,13 @@ export { events }; let metadata = {}; -export const logServerVersion = serverVersion => { +export const logServerVersion = (serverVersion: string): void => { metadata = { serverVersion }; }; -export const logEvent = (eventName, payload) => { +export const logEvent = (eventName: string, payload?: { [key: string]: any }): void => { try { if (!isFDroidBuild) { analytics().logEvent(eventName, payload); @@ -51,26 +51,26 @@ export const logEvent = (eventName, payload) => { } }; -export const setCurrentScreen = currentScreen => { +export const setCurrentScreen = (currentScreen: string): void => { if (!isFDroidBuild) { analytics().setCurrentScreen(currentScreen); bugsnag.leaveBreadcrumb(currentScreen, { type: 'navigation' }); } }; -export const toggleCrashErrorsReport = value => { +export const toggleCrashErrorsReport = (value: boolean): boolean => { crashlytics().setCrashlyticsCollectionEnabled(value); return (reportCrashErrors = value); }; -export const toggleAnalyticsEventsReport = value => { +export const toggleAnalyticsEventsReport = (value: boolean): boolean => { analytics().setAnalyticsCollectionEnabled(value); return (reportAnalyticsEvents = value); }; -export default e => { +export default (e: any): void => { if (e instanceof Error && bugsnag && e.message !== 'Aborted' && !__DEV__) { - bugsnag.notify(e, event => { + bugsnag.notify(e, (event: { addMetadata: (arg0: string, arg1: {}) => void }) => { event.addMetadata('details', { ...metadata }); }); if (!isFDroidBuild) { diff --git a/app/utils/media.js b/app/utils/media.ts similarity index 55% rename from app/utils/media.js rename to app/utils/media.ts index b05f95a9..78b1c29f 100644 --- a/app/utils/media.js +++ b/app/utils/media.ts @@ -1,20 +1,30 @@ -export const canUploadFile = (file, allowList, maxFileSize) => { +import { IAttachment } from '../views/ShareView/interfaces'; + +export const canUploadFile = ( + file: IAttachment, + allowList: string, + maxFileSize: number, + permissionToUploadFile: boolean +): { success: boolean; error?: string } => { if (!(file && file.path)) { return { success: true }; } if (maxFileSize > -1 && file.size > maxFileSize) { return { success: false, error: 'error-file-too-large' }; } + if (!permissionToUploadFile) { + return { success: false, error: 'error-not-permission-to-upload-file' }; + } // if white list is empty, all media types are enabled if (!allowList || allowList === '*') { return { success: true }; } const allowedMime = allowList.split(','); - if (allowedMime.includes(file.mime)) { + if (allowedMime.includes(file.mime!)) { return { success: true }; } const wildCardGlob = '/*'; - const wildCards = allowedMime.filter(item => item.indexOf(wildCardGlob) > 0); + const wildCards = allowedMime.filter((item: string) => item.indexOf(wildCardGlob) > 0); if (file.mime && wildCards.includes(file.mime.replace(/(\/.*)$/, wildCardGlob))) { return { success: true }; } diff --git a/app/utils/messageTypes.js b/app/utils/messageTypes.ts similarity index 100% rename from app/utils/messageTypes.js rename to app/utils/messageTypes.ts diff --git a/app/utils/moment.js b/app/utils/moment.ts similarity index 57% rename from app/utils/moment.js rename to app/utils/moment.ts index 064b0f7f..3379429c 100644 --- a/app/utils/moment.js +++ b/app/utils/moment.ts @@ -1,4 +1,4 @@ -const localeKeys = { +const localeKeys: { [key: string]: string } = { en: 'en', ru: 'ru', 'pt-BR': 'pt-br', @@ -13,4 +13,4 @@ const localeKeys = { 'zh-TW': 'zh-tw' }; -export const toMomentLocale = locale => localeKeys[locale]; +export const toMomentLocale = (locale: string): string => localeKeys[locale]; diff --git a/app/utils/navigation/animations.js b/app/utils/navigation/animations.ts similarity index 71% rename from app/utils/navigation/animations.js rename to app/utils/navigation/animations.ts index 9f99764c..a9f18408 100644 --- a/app/utils/navigation/animations.js +++ b/app/utils/navigation/animations.ts @@ -1,12 +1,14 @@ import { Animated, Easing } from 'react-native'; -import { HeaderStyleInterpolators, TransitionPresets } from '@react-navigation/stack'; +import { HeaderStyleInterpolators, TransitionPreset, TransitionPresets } from '@react-navigation/stack'; +// eslint-disable-next-line import/no-unresolved +import { StackCardStyleInterpolator, TransitionSpec } from '@react-navigation/stack/lib/typescript/src/types'; import { isAndroid } from '../deviceInfo'; import conditional from './conditional'; const { multiply } = Animated; -const forFadeFromCenter = ({ current, closing }) => { +const forFadeFromCenter: StackCardStyleInterpolator = ({ current, closing }) => { const opacity = conditional( closing, current.progress, @@ -23,7 +25,7 @@ const forFadeFromCenter = ({ current, closing }) => { }; }; -const FadeIn = { +const FadeIn: TransitionSpec = { animation: 'timing', config: { duration: 250, @@ -31,7 +33,7 @@ const FadeIn = { } }; -const FadeOut = { +const FadeOut: TransitionSpec = { animation: 'timing', config: { duration: 150, @@ -48,7 +50,7 @@ export const FadeFromCenterModal = { cardStyleInterpolator: forFadeFromCenter }; -const forStackAndroid = ({ current, inverted, layouts: { screen } }) => { +const forStackAndroid: StackCardStyleInterpolator = ({ current, inverted, layouts: { screen } }) => { const translateX = multiply( current.progress.interpolate({ inputRange: [0, 1], @@ -70,7 +72,7 @@ const forStackAndroid = ({ current, inverted, layouts: { screen } }) => { }; }; -const StackAndroid = { +const StackAndroid: TransitionPreset = { gestureDirection: 'horizontal', transitionSpec: { open: FadeIn, diff --git a/app/utils/navigation/conditional.js b/app/utils/navigation/conditional.ts similarity index 87% rename from app/utils/navigation/conditional.js rename to app/utils/navigation/conditional.ts index 015c52ae..84c76d83 100644 --- a/app/utils/navigation/conditional.js +++ b/app/utils/navigation/conditional.ts @@ -10,7 +10,11 @@ const { add, multiply } = Animated; * @param main Animated Node to use if the condition is `true` * @param fallback Animated Node to use if the condition is `false` */ -export default function conditional(condition, main, fallback) { +export default function conditional( + condition: Animated.AnimatedInterpolation, + main: Animated.Animated, + fallback: Animated.Animated +): Animated.AnimatedAddition { // To implement this behavior, we multiply the main node with the condition. // So if condition is 0, result will be 0, and if condition is 1, result will be main node. // Then we multiple reverse of the condition (0 if condition is 1) with the fallback. diff --git a/app/utils/openLink.js b/app/utils/openLink.ts similarity index 83% rename from app/utils/openLink.js rename to app/utils/openLink.ts index 92df16a7..4048b3ad 100644 --- a/app/utils/openLink.js +++ b/app/utils/openLink.ts @@ -14,7 +14,7 @@ const scheme = { brave: 'brave:' }; -const appSchemeURL = (url, browser) => { +const appSchemeURL = (url: string, browser: string): string => { let schemeUrl = url; const parsedUrl = parse(url, true); const { protocol } = parsedUrl; @@ -35,7 +35,7 @@ const appSchemeURL = (url, browser) => { return schemeUrl; }; -const openLink = async (url, theme = 'light') => { +const openLink = async (url: string, theme = 'light'): Promise => { try { const browser = await UserPreferences.getStringAsync(DEFAULT_BROWSER_KEY); @@ -43,11 +43,12 @@ const openLink = async (url, theme = 'light') => { await WebBrowser.openBrowserAsync(url, { toolbarColor: themes[theme].headerBackground, controlsColor: themes[theme].headerTintColor, - collapseToolbar: true, + // https://github.com/expo/expo/pull/4923 + enableBarCollapsing: true, showTitle: true }); } else { - const schemeUrl = appSchemeURL(url, browser.replace(':', '')); + const schemeUrl = appSchemeURL(url, browser!.replace(':', '')); await Linking.openURL(schemeUrl); } } catch { diff --git a/app/utils/random.js b/app/utils/random.ts similarity index 80% rename from app/utils/random.js rename to app/utils/random.ts index 8f6adb88..2d2cd178 100644 --- a/app/utils/random.js +++ b/app/utils/random.ts @@ -1,4 +1,4 @@ -export default function random(length) { +export default function random(length: number): string { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < length; i += 1) { diff --git a/app/utils/review.js b/app/utils/review.ts similarity index 96% rename from app/utils/review.js rename to app/utils/review.ts index 15f1cf96..bbbb498b 100644 --- a/app/utils/review.js +++ b/app/utils/review.ts @@ -15,7 +15,7 @@ const reviewDelay = 2000; const numberOfDays = 7; const numberOfPositiveEvent = 5; -const daysBetween = (date1, date2) => { +const daysBetween = (date1: Date, date2: Date): number => { const one_day = 1000 * 60 * 60 * 24; const date1_ms = date1.getTime(); const date2_ms = date2.getTime(); @@ -32,7 +32,7 @@ const onCancelPress = () => { } }; -export const onReviewPress = async () => { +export const onReviewPress = async (): Promise => { logEvent(events.SE_REVIEW_THIS_APP); await onCancelPress(); try { diff --git a/app/utils/room.js b/app/utils/room.js deleted file mode 100644 index 67df65a9..00000000 --- a/app/utils/room.js +++ /dev/null @@ -1,53 +0,0 @@ -import moment from 'moment'; - -import { themes } from '../constants/colors'; -import I18n from '../i18n'; - -export const isBlocked = room => { - if (room) { - const { t, blocked, blocker } = room; - if (t === 'd' && (blocked || blocker)) { - return true; - } - } - return false; -}; - -export const capitalize = s => { - if (typeof s !== 'string') { - return ''; - } - return s.charAt(0).toUpperCase() + s.slice(1); -}; - -export const formatDate = date => - moment(date).calendar(null, { - lastDay: `[${I18n.t('Yesterday')}]`, - sameDay: 'LT', - lastWeek: 'dddd', - sameElse: 'L' - }); - -export const formatDateThreads = date => - moment(date).calendar(null, { - sameDay: 'LT', - lastDay: `[${I18n.t('Yesterday')}] LT`, - lastWeek: 'dddd LT', - sameElse: 'LL' - }); - -export const getBadgeColor = ({ subscription, messageId, theme }) => { - if (subscription?.tunreadUser?.includes(messageId)) { - return themes[theme].mentionMeColor; - } - if (subscription?.tunreadGroup?.includes(messageId)) { - return themes[theme].mentionGroupColor; - } - if (subscription?.tunread?.includes(messageId)) { - return themes[theme].tunreadColor; - } -}; - -export const makeThreadName = messageRecord => messageRecord.msg || messageRecord?.attachments[0]?.title; - -export const isTeamRoom = ({ teamId, joined }) => teamId && joined; diff --git a/app/utils/room.ts b/app/utils/room.ts new file mode 100644 index 00000000..3e4e0ef4 --- /dev/null +++ b/app/utils/room.ts @@ -0,0 +1,65 @@ +import moment from 'moment'; + +import { themes } from '../constants/colors'; +import I18n from '../i18n'; +import { IAttachment } from '../definitions/IAttachment'; +import { ISubscription, SubscriptionType } from '../definitions/ISubscription'; + +export const isBlocked = (room: ISubscription): boolean => { + if (room) { + const { t, blocked, blocker } = room; + if (t === SubscriptionType.DIRECT && (blocked || blocker)) { + return true; + } + } + return false; +}; + +export const capitalize = (s: string): string => { + if (typeof s !== 'string') { + return ''; + } + return s.charAt(0).toUpperCase() + s.slice(1); +}; + +export const formatDate = (date: Date): string => + moment(date).calendar(null, { + lastDay: `[${I18n.t('Yesterday')}]`, + sameDay: 'LT', + lastWeek: 'dddd', + sameElse: 'L' + }); + +export const formatDateThreads = (date: Date): string => + moment(date).calendar(null, { + sameDay: 'LT', + lastDay: `[${I18n.t('Yesterday')}] LT`, + lastWeek: 'dddd LT', + sameElse: 'LL' + }); + +export const getBadgeColor = ({ + subscription, + messageId, + theme +}: { + // TODO: Refactor when migrate model folder + subscription: any; + messageId: string; + theme: string; +}): string | undefined => { + if (subscription?.tunreadUser?.includes(messageId)) { + return themes[theme].mentionMeColor; + } + if (subscription?.tunreadGroup?.includes(messageId)) { + return themes[theme].mentionGroupColor; + } + if (subscription?.tunread?.includes(messageId)) { + return themes[theme].tunreadColor; + } +}; + +export const makeThreadName = (messageRecord: { id?: string; msg?: string; attachments?: IAttachment[] }): string | undefined => + messageRecord.msg || messageRecord.attachments![0].title; + +export const isTeamRoom = ({ teamId, joined }: { teamId: string; joined: boolean }): boolean => !!teamId && joined; diff --git a/app/utils/server.js b/app/utils/server.ts similarity index 84% rename from app/utils/server.js rename to app/utils/server.ts index e7be96b3..52064757 100644 --- a/app/utils/server.js +++ b/app/utils/server.ts @@ -3,7 +3,7 @@ url = 'https://open.rocket.chat/method' hostname = 'open.rocket.chat' */ -export const extractHostname = url => { +export const extractHostname = (url: string): string => { let hostname; if (url.indexOf('//') > -1) { diff --git a/app/utils/shortnameToUnicode/ascii.js b/app/utils/shortnameToUnicode/ascii.ts similarity index 98% rename from app/utils/shortnameToUnicode/ascii.js rename to app/utils/shortnameToUnicode/ascii.ts index 4d9d04cd..7eca5f7d 100644 --- a/app/utils/shortnameToUnicode/ascii.js +++ b/app/utils/shortnameToUnicode/ascii.ts @@ -3,7 +3,7 @@ /* eslint-disable object-curly-spacing */ /* eslint-disable comma-spacing */ /* eslint-disable key-spacing */ -const ascii = { +const ascii: { [key: string]: string } = { '*\\0/*': '🙆', '*\\O/*': '🙆', '-___-': '😑', diff --git a/app/utils/shortnameToUnicode/emojis.js b/app/utils/shortnameToUnicode/emojis.ts similarity index 99% rename from app/utils/shortnameToUnicode/emojis.js rename to app/utils/shortnameToUnicode/emojis.ts index 14cd6133..6a8a63c3 100644 --- a/app/utils/shortnameToUnicode/emojis.js +++ b/app/utils/shortnameToUnicode/emojis.ts @@ -3,7 +3,7 @@ /* eslint-disable object-curly-spacing */ /* eslint-disable comma-spacing */ /* eslint-disable key-spacing */ -const emojis = { +const emojis: { [key: string]: string } = { ':england:': '🏴', ':scotland:': '🏴', ':wales:': '🏴', diff --git a/app/utils/shortnameToUnicode/index.js b/app/utils/shortnameToUnicode/index.ts similarity index 80% rename from app/utils/shortnameToUnicode/index.js rename to app/utils/shortnameToUnicode/index.ts index 0a54aa3a..b533da8f 100644 --- a/app/utils/shortnameToUnicode/index.js +++ b/app/utils/shortnameToUnicode/index.ts @@ -2,11 +2,11 @@ import emojis from './emojis'; import ascii, { asciiRegexp } from './ascii'; const shortnamePattern = new RegExp(/:[-+_a-z0-9]+:/, 'gi'); -const replaceShortNameWithUnicode = shortname => emojis[shortname] || shortname; +const replaceShortNameWithUnicode = (shortname: string) => emojis[shortname] || shortname; const regAscii = new RegExp(`((\\s|^)${asciiRegexp}(?=\\s|$|[!,.?]))`, 'gi'); -const unescapeHTML = string => { - const unescaped = { +const unescapeHTML = (string: string) => { + const unescaped: { [key: string]: string } = { '&': '&', '&': '&', '&': '&', @@ -27,7 +27,7 @@ const unescapeHTML = string => { return string.replace(/&(?:amp|#38|#x26|lt|#60|#x3C|gt|#62|#x3E|apos|#39|#x27|quot|#34|#x22);/gi, match => unescaped[match]); }; -const shortnameToUnicode = str => { +const shortnameToUnicode = (str: string): string => { str = str.replace(shortnamePattern, replaceShortNameWithUnicode); str = str.replace(regAscii, (entire, m1, m2, m3) => { diff --git a/app/utils/sslPinning.js b/app/utils/sslPinning.ts similarity index 54% rename from app/utils/sslPinning.js rename to app/utils/sslPinning.ts index 50f944e6..42245c98 100644 --- a/app/utils/sslPinning.js +++ b/app/utils/sslPinning.ts @@ -7,6 +7,26 @@ import I18n from '../i18n'; import { extractHostname } from './server'; const { SSLPinning } = NativeModules; +const { documentDirectory } = FileSystem; + +const extractFileScheme = (path: string) => path.replace('file://', ''); // file:// isn't allowed by obj-C + +const getPath = (name: string) => `${documentDirectory}/${name}`; + +interface ICertificate { + path: string; + password: string; +} + +const persistCertificate = async (name: string, password: string) => { + const certificatePath = getPath(name); + const certificate: ICertificate = { + path: extractFileScheme(certificatePath), + password + }; + await UserPreferences.setMapAsync(name, certificate); + return certificate; +}; const RCSSLPinning = Platform.select({ ios: { @@ -14,6 +34,7 @@ const RCSSLPinning = Platform.select({ new Promise(async (resolve, reject) => { try { const res = await DocumentPicker.pick({ + // @ts-ignore type: ['com.rsa.pkcs-12'] }); const { uri, name } = res; @@ -25,17 +46,9 @@ const RCSSLPinning = Platform.select({ text: 'OK', onPress: async password => { try { - const certificatePath = `${FileSystem.documentDirectory}/${name}`; - + const certificatePath = getPath(name); await FileSystem.copyAsync({ from: uri, to: certificatePath }); - - const certificate = { - path: certificatePath.replace('file://', ''), // file:// isn't allowed by obj-C - password - }; - - await UserPreferences.setMapAsync(name, certificate); - + await persistCertificate(name, password!); resolve(name); } catch (e) { reject(e); @@ -49,16 +62,19 @@ const RCSSLPinning = Platform.select({ reject(e); } }), - setCertificate: async (alias, server) => { - if (alias) { - const certificate = await UserPreferences.getMapAsync(alias); + setCertificate: async (name: string, server: string) => { + if (name) { + let certificate = (await UserPreferences.getMapAsync(name)) as ICertificate; + if (!certificate.path.match(extractFileScheme(documentDirectory!))) { + certificate = await persistCertificate(name, certificate.password); + } await UserPreferences.setMapAsync(extractHostname(server), certificate); } } }, android: { pickCertificate: () => SSLPinning?.pickCertificate(), - setCertificate: alias => SSLPinning?.setCertificate(alias) + setCertificate: name => SSLPinning?.setCertificate(name) } }); diff --git a/app/utils/theme.js b/app/utils/theme.ts similarity index 71% rename from app/utils/theme.js rename to app/utils/theme.ts index c9038941..0e9d8e05 100644 --- a/app/utils/theme.js +++ b/app/utils/theme.ts @@ -2,12 +2,13 @@ import { Appearance } from 'react-native-appearance'; import changeNavigationBarColor from 'react-native-navigation-bar-color'; import setRootViewColor from 'rn-root-view'; +import { IThemePreference, TThemeMode } from '../definitions/ITheme'; import { themes } from '../constants/colors'; import { isAndroid } from './deviceInfo'; -let themeListener; +let themeListener: { remove: () => void } | null; -export const defaultTheme = () => { +export const defaultTheme = (): TThemeMode => { const systemTheme = Appearance.getColorScheme(); if (systemTheme && systemTheme !== 'no-preference') { return systemTheme; @@ -15,7 +16,7 @@ export const defaultTheme = () => { return 'light'; }; -export const getTheme = themePreferences => { +export const getTheme = (themePreferences: IThemePreference): string => { const { darkLevel, currentTheme } = themePreferences; let theme = currentTheme; if (currentTheme === 'automatic') { @@ -24,7 +25,7 @@ export const getTheme = themePreferences => { return theme === 'dark' ? darkLevel : 'light'; }; -export const newThemeState = (prevState, newTheme) => { +export const newThemeState = (prevState: { themePreferences: IThemePreference }, newTheme: IThemePreference) => { // new theme preferences const themePreferences = { ...prevState.themePreferences, @@ -35,12 +36,13 @@ export const newThemeState = (prevState, newTheme) => { return { themePreferences, theme: getTheme(themePreferences) }; }; -export const setNativeTheme = async themePreferences => { +export const setNativeTheme = async (themePreferences: IThemePreference): Promise => { const theme = getTheme(themePreferences); if (isAndroid) { const iconsLight = theme === 'light'; try { - await changeNavigationBarColor(themes[theme].navbarBackground, iconsLight); + // The late param as default is true @ react-native-navigation-bar-color/src/index.js line 8 + await changeNavigationBarColor(themes[theme].navbarBackground, iconsLight, true); } catch (error) { // Do nothing } @@ -55,7 +57,7 @@ export const unsubscribeTheme = () => { } }; -export const subscribeTheme = (themePreferences, setTheme) => { +export const subscribeTheme = (themePreferences: IThemePreference, setTheme: () => void): void => { const { currentTheme } = themePreferences; if (!themeListener && currentTheme === 'automatic') { // not use listener params because we use getTheme diff --git a/app/utils/throttle.js b/app/utils/throttle.js deleted file mode 100644 index 88751335..00000000 --- a/app/utils/throttle.js +++ /dev/null @@ -1,26 +0,0 @@ -export default function throttle(fn, threshhold = 250, scope) { - let last; - let deferTimer; - - const _throttle = (...args) => { - const context = scope || this; - - const now = +new Date(); - - if (last && now < last + threshhold) { - // hold on to it - clearTimeout(deferTimer); - deferTimer = setTimeout(() => { - last = now; - fn.apply(context, args); - }, threshhold); - } else { - last = now; - fn.apply(context, args); - } - }; - - _throttle.stop = () => clearTimeout(deferTimer); - - return _throttle; -} diff --git a/app/utils/touch.js b/app/utils/touch.tsx similarity index 55% rename from app/utils/touch.js rename to app/utils/touch.tsx index 0bfece04..3573c87c 100644 --- a/app/utils/touch.js +++ b/app/utils/touch.tsx @@ -1,19 +1,27 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { RectButton } from 'react-native-gesture-handler'; +import { RectButton, RectButtonProps } from 'react-native-gesture-handler'; import { themes } from '../constants/colors'; -class Touch extends React.Component { - setNativeProps(props) { +interface ITouchProps extends RectButtonProps { + children: React.ReactNode; + theme: string; + accessibilityLabel?: string; + testID?: string; +} + +class Touch extends React.Component { + private ref: any; + + setNativeProps(props: ITouchProps): void { this.ref.setNativeProps(props); } - getRef = ref => { + getRef = (ref: RectButton): void => { this.ref = ref; }; - render() { + render(): JSX.Element { const { children, onPress, theme, underlayColor, ...props } = this.props; return ( @@ -30,11 +38,4 @@ class Touch extends React.Component { } } -Touch.propTypes = { - children: PropTypes.node, - onPress: PropTypes.func, - theme: PropTypes.string, - underlayColor: PropTypes.string -}; - export default Touch; diff --git a/app/utils/twoFactor.js b/app/utils/twoFactor.ts similarity index 68% rename from app/utils/twoFactor.js rename to app/utils/twoFactor.ts index 6f2fa9c9..a52ff93f 100644 --- a/app/utils/twoFactor.js +++ b/app/utils/twoFactor.ts @@ -3,13 +3,18 @@ import { settings } from '@rocket.chat/sdk'; import { TWO_FACTOR } from '../containers/TwoFactor'; import EventEmitter from './events'; -export const twoFactor = ({ method, invalid }) => +interface ITwoFactor { + method: string; + invalid: boolean; +} + +export const twoFactor = ({ method, invalid }: ITwoFactor): Promise<{ twoFactorCode: string; twoFactorMethod: string }> => new Promise((resolve, reject) => { EventEmitter.emit(TWO_FACTOR, { method, invalid, cancel: () => reject(), - submit: code => { + submit: (code: string) => { settings.customHeaders = { ...settings.customHeaders, 'x-2fa-code': code, diff --git a/app/utils/url.js b/app/utils/url.ts similarity index 77% rename from app/utils/url.js rename to app/utils/url.ts index 623524d7..50179597 100644 --- a/app/utils/url.js +++ b/app/utils/url.ts @@ -1,4 +1,4 @@ -export const isValidURL = url => { +export const isValidURL = (url: string): boolean => { const pattern = new RegExp( '^(https?:\\/\\/)?' + // protocol '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name @@ -12,4 +12,4 @@ export const isValidURL = url => { }; // Use useSsl: false only if server url starts with http:// -export const useSsl = url => !/http:\/\//.test(url); +export const useSsl = (url: string): boolean => !/http:\/\//.test(url); diff --git a/app/views/AddChannelTeamView.tsx b/app/views/AddChannelTeamView.tsx index d477f9ba..8a72d3c9 100644 --- a/app/views/AddChannelTeamView.tsx +++ b/app/views/AddChannelTeamView.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { RouteProp } from '@react-navigation/native'; import { connect } from 'react-redux'; +import { CompositeNavigationProp } from '@react-navigation/core'; import * as List from '../containers/List'; import StatusBar from '../containers/StatusBar'; @@ -9,16 +10,24 @@ import { useTheme } from '../theme'; import * as HeaderButton from '../containers/HeaderButton'; import SafeAreaView from '../containers/SafeAreaView'; import I18n from '../i18n'; - -type TNavigation = StackNavigationProp; +import { ChatsStackParamList, DrawerParamList, NewMessageStackParamList } from '../stacks/types'; interface IAddChannelTeamView { - route: RouteProp<{ AddChannelTeamView: { teamId: string; teamChannels: object[] } }, 'AddChannelTeamView'>; - navigation: TNavigation; + navigation: CompositeNavigationProp< + StackNavigationProp, + CompositeNavigationProp, StackNavigationProp> + >; + route: RouteProp; isMasterDetail: boolean; } -const setHeader = (navigation: TNavigation, isMasterDetail: boolean) => { +const setHeader = ({ + navigation, + isMasterDetail +}: { + navigation: StackNavigationProp; + isMasterDetail: boolean; +}) => { const options: StackNavigationOptions = { headerTitle: I18n.t('Add_Channel_to_Team') }; @@ -35,7 +44,7 @@ const AddChannelTeamView = ({ navigation, route, isMasterDetail }: IAddChannelTe const { theme } = useTheme(); useEffect(() => { - setHeader(navigation, isMasterDetail); + setHeader({ navigation, isMasterDetail }); }, []); return ( diff --git a/app/views/AddExistingChannelView.tsx b/app/views/AddExistingChannelView.tsx index 5efdbf34..86ab9b9c 100644 --- a/app/views/AddExistingChannelView.tsx +++ b/app/views/AddExistingChannelView.tsx @@ -21,6 +21,7 @@ import { animateNextTransition } from '../utils/layoutAnimation'; import { goRoom } from '../utils/goRoom'; import { showErrorAlert } from '../utils/info'; import debounce from '../utils/debounce'; +import { ChatsStackParamList } from '../stacks/types'; interface IAddExistingChannelViewState { // TODO: refactor with Room Model @@ -31,8 +32,8 @@ interface IAddExistingChannelViewState { } interface IAddExistingChannelViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ AddExistingChannelView: { teamId: string } }, 'AddExistingChannelView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; isMasterDetail: boolean; addTeamChannelPermission: string[]; @@ -41,7 +42,7 @@ interface IAddExistingChannelViewProps { const QUERY_SIZE = 50; class AddExistingChannelView extends React.Component { - private teamId: string; + private teamId?: string; constructor(props: IAddExistingChannelViewProps) { super(props); this.query(); diff --git a/app/views/AdminPanelView/index.tsx b/app/views/AdminPanelView/index.tsx index 80f728e1..f0af5dfa 100644 --- a/app/views/AdminPanelView/index.tsx +++ b/app/views/AdminPanelView/index.tsx @@ -9,6 +9,7 @@ import * as HeaderButton from '../../containers/HeaderButton'; import { withTheme } from '../../theme'; import { getUserSelector } from '../../selectors/login'; import SafeAreaView from '../../containers/SafeAreaView'; +import { AdminPanelStackParamList } from '../../stacks/types'; interface IAdminPanelViewProps { baseUrl: string; @@ -16,7 +17,7 @@ interface IAdminPanelViewProps { } interface INavigationOptions { - navigation: DrawerScreenProps; + navigation: DrawerScreenProps; isMasterDetail: boolean; } diff --git a/app/views/AttachmentView.tsx b/app/views/AttachmentView.tsx index 90adf8b4..d0bd021c 100644 --- a/app/views/AttachmentView.tsx +++ b/app/views/AttachmentView.tsx @@ -24,6 +24,8 @@ import { getUserSelector } from '../selectors/login'; import { withDimensions } from '../dimensions'; import { getHeaderHeight } from '../containers/Header'; import StatusBar from '../containers/StatusBar'; +import { InsideStackParamList } from '../stacks/types'; +import { IAttachment } from '../definitions/IAttachment'; const styles = StyleSheet.create({ container: { @@ -31,24 +33,14 @@ const styles = StyleSheet.create({ } }); -// TODO: refactor when react-navigation is done -export interface IAttachment { - title: string; - title_link?: string; - image_url?: string; - image_type?: string; - video_url?: string; - video_type?: string; -} - interface IAttachmentViewState { attachment: IAttachment; loading: boolean; } interface IAttachmentViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ AttachmentView: { attachment: IAttachment } }, 'AttachmentView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; baseUrl: string; width: number; @@ -131,7 +123,11 @@ class AttachmentView extends React.Component; - route: IRoute; + navigation: StackNavigationProp; + route: RouteProp; } interface IAuthenticationWebView extends INavigationOption { diff --git a/app/views/AutoTranslateView/index.tsx b/app/views/AutoTranslateView/index.tsx index 92a77543..95442689 100644 --- a/app/views/AutoTranslateView/index.tsx +++ b/app/views/AutoTranslateView/index.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { FlatList, StyleSheet, Switch } from 'react-native'; +import { RouteProp } from '@react-navigation/core'; +import { ChatsStackParamList } from '../../stacks/types'; import RocketChat from '../../lib/rocketchat'; import I18n from '../../i18n'; import StatusBar from '../../containers/StatusBar'; @@ -9,6 +11,7 @@ import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; import { withTheme } from '../../theme'; import SafeAreaView from '../../containers/SafeAreaView'; import { events, logEvent } from '../../utils/log'; +import { ISubscription } from '../../definitions/ISubscription'; const styles = StyleSheet.create({ list: { @@ -16,19 +19,8 @@ const styles = StyleSheet.create({ } }); -interface IRoom { - observe: Function; - autoTranslateLanguage: boolean; - autoTranslate: boolean; -} - interface IAutoTranslateViewProps { - route: { - params: { - rid?: string; - room?: IRoom; - }; - }; + route: RouteProp; theme: string; } @@ -50,7 +42,7 @@ class AutoTranslateView extends React.Component { if (room && room.observe) { this.roomObservable = room.observe(); - this.subscription = this.roomObservable.subscribe((changes: IRoom) => { + this.subscription = this.roomObservable.subscribe((changes: ISubscription) => { if (this.mounted) { const { selectedLanguage, enableAutoTranslate } = this.state; if (selectedLanguage !== changes.autoTranslateLanguage) { diff --git a/app/views/CreateChannelView.tsx b/app/views/CreateChannelView.tsx index 45b2cc2f..e8d719ab 100644 --- a/app/views/CreateChannelView.tsx +++ b/app/views/CreateChannelView.tsx @@ -25,6 +25,7 @@ import { events, logEvent } from '../utils/log'; import SafeAreaView from '../containers/SafeAreaView'; import RocketChat from '../lib/rocketchat'; import sharedStyles from './Styles'; +import { ChatsStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -91,8 +92,8 @@ interface ICreateChannelViewState { } interface ICreateChannelViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ CreateChannelView: { isTeam: boolean; teamId: string } }, 'CreateChannelView'>; + navigation: StackNavigationProp; + route: RouteProp; baseUrl: string; create: (data: ICreateFunction) => void; removeUser: (user: IOtherUser) => void; @@ -118,7 +119,7 @@ interface ISwitch extends SwitchProps { } class CreateChannelView extends React.Component { - private teamId: string; + private teamId?: string; constructor(props: ICreateChannelViewProps) { super(props); @@ -240,7 +241,7 @@ class CreateChannelView extends React.Component - // TODO: remove this ts-ignore when migrate the file: app/utils/avatar.js - // @ts-ignore avatarURL({ text: RocketChat.getRoomAvatar(item), type: item.t, diff --git a/app/views/CreateDiscussionView/SelectUsers.tsx b/app/views/CreateDiscussionView/SelectUsers.tsx index 65a4e0a4..d63c5ae6 100644 --- a/app/views/CreateDiscussionView/SelectUsers.tsx +++ b/app/views/CreateDiscussionView/SelectUsers.tsx @@ -12,6 +12,7 @@ import { MultiSelect } from '../../containers/UIKit/MultiSelect'; import { themes } from '../../constants/colors'; import styles from './styles'; import { ICreateDiscussionViewSelectUsers } from './interfaces'; +import { SubscriptionType } from '../../definitions/ISubscription'; interface IUser { name: string; @@ -62,11 +63,9 @@ const SelectUsers = ({ }, 300); const getAvatar = (item: any) => - // TODO: remove this ts-ignore when migrate the file: app/utils/avatar.js - // @ts-ignore avatarURL({ text: RocketChat.getRoomAvatar(item), - type: 'd', + type: SubscriptionType.DIRECT, user: { id: userId, token }, server, avatarETag: item.avatarETag, diff --git a/app/views/CreateDiscussionView/index.tsx b/app/views/CreateDiscussionView/index.tsx index 2933b2c7..53d741d2 100644 --- a/app/views/CreateDiscussionView/index.tsx +++ b/app/views/CreateDiscussionView/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { ScrollView, Switch, Text } from 'react-native'; +import { StackNavigationOptions } from '@react-navigation/stack'; import Loading from '../../containers/Loading'; import KeyboardView from '../../presentation/KeyboardView'; @@ -89,7 +90,7 @@ class CreateChannelView extends React.Component { ) : null, headerLeft: showCloseModal ? () => : undefined - }); + } as StackNavigationOptions); }; submit = () => { diff --git a/app/views/CreateDiscussionView/interfaces.ts b/app/views/CreateDiscussionView/interfaces.ts index 46883311..6009881c 100644 --- a/app/views/CreateDiscussionView/interfaces.ts +++ b/app/views/CreateDiscussionView/interfaces.ts @@ -1,14 +1,12 @@ +import { RouteProp } from '@react-navigation/core'; +import { StackNavigationProp } from '@react-navigation/stack'; + +import { NewMessageStackParamList } from '../../stacks/types'; +import { SubscriptionType } from '../../definitions/ISubscription'; + export interface ICreateChannelViewProps { - navigation: any; - route: { - params?: { - channel: string; - message: { - msg: string; - }; - showCloseModal: boolean; - }; - }; + navigation: StackNavigationProp; + route: RouteProp; server: string; user: { id: string; @@ -18,7 +16,7 @@ export interface ICreateChannelViewProps { loading: boolean; result: { rid: string; - t: string; + t: SubscriptionType; prid: string; }; failure: boolean; diff --git a/app/views/DefaultBrowserView.tsx b/app/views/DefaultBrowserView.tsx index 0282e0df..cfe977d2 100644 --- a/app/views/DefaultBrowserView.tsx +++ b/app/views/DefaultBrowserView.tsx @@ -107,7 +107,7 @@ class DefaultBrowserView extends React.Component { logEvent(events.DB_CHANGE_DEFAULT_BROWSER, { browser: newBrowser }); try { - const browser = newBrowser !== 'systemDefault:' ? newBrowser : null; + const browser = newBrowser || 'systemDefault:'; await UserPreferences.setStringAsync(DEFAULT_BROWSER_KEY, browser); this.setState({ browser }); } catch { diff --git a/app/views/DirectoryView/Options.tsx b/app/views/DirectoryView/Options.tsx index fcc0f7bf..11206806 100644 --- a/app/views/DirectoryView/Options.tsx +++ b/app/views/DirectoryView/Options.tsx @@ -63,7 +63,11 @@ export default class DirectoryOptions extends PureComponent changeType(itemType)} style={styles.dropdownItemButton} theme={theme}> + changeType(itemType)} + style={styles.dropdownItemButton} + theme={theme} + accessibilityLabel={I18n.t(text)}> {I18n.t(text)} @@ -90,7 +94,7 @@ export default class DirectoryOptions extends PureComponent - + ; baseUrl: string; isFederationEnabled: boolean; user: { diff --git a/app/views/DisplayPrefsView.js b/app/views/DisplayPrefsView.tsx similarity index 76% rename from app/views/DisplayPrefsView.js rename to app/views/DisplayPrefsView.tsx index 09da4edc..959682c4 100644 --- a/app/views/DisplayPrefsView.js +++ b/app/views/DisplayPrefsView.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; import { Switch } from 'react-native'; import { RadioButton } from 'react-native-ui-lib'; +import { StackNavigationProp } from '@react-navigation/stack'; import { useDispatch, useSelector } from 'react-redux'; import { setPreference } from '../actions/sortPreferences'; @@ -15,13 +15,30 @@ import * as HeaderButton from '../containers/HeaderButton'; import SafeAreaView from '../containers/SafeAreaView'; import { ICON_SIZE } from '../containers/List/constants'; import log, { events, logEvent } from '../utils/log'; -import { DISPLAY_MODE_CONDENSED, DISPLAY_MODE_EXPANDED } from '../constants/constantDisplayMode'; +import { DisplayMode, SortBy } from '../constants/constantDisplayMode'; +import { SettingsStackParamList } from '../stacks/types'; -const DisplayPrefsView = props => { +interface IParam { + sortBy: SortBy; + groupByType: boolean; + showFavorites: boolean; + showUnread: boolean; + showAvatar: boolean; + displayMode: DisplayMode; +} + +interface IDisplayPrefsView { + navigation: StackNavigationProp; + isMasterDetail: boolean; +} + +const DisplayPrefsView = (props: IDisplayPrefsView): JSX.Element => { const { theme } = useTheme(); - const { sortBy, groupByType, showFavorites, showUnread, showAvatar, displayMode } = useSelector(state => state.sortPreferences); - const { isMasterDetail } = useSelector(state => state.app); + const { sortBy, groupByType, showFavorites, showUnread, showAvatar, displayMode } = useSelector( + (state: any) => state.sortPreferences + ); + const { isMasterDetail } = useSelector((state: any) => state.app); const dispatch = useDispatch(); useEffect(() => { @@ -36,7 +53,7 @@ const DisplayPrefsView = props => { } }, []); - const setSortPreference = async param => { + const setSortPreference = async (param: Partial) => { try { dispatch(setPreference(param)); await RocketChat.saveSortPreference(param); @@ -47,12 +64,12 @@ const DisplayPrefsView = props => { const sortByName = async () => { logEvent(events.DP_SORT_CHANNELS_BY_NAME); - await setSortPreference({ sortBy: 'alphabetical' }); + await setSortPreference({ sortBy: SortBy.Alphabetical }); }; const sortByActivity = async () => { logEvent(events.DP_SORT_CHANNELS_BY_ACTIVITY); - await setSortPreference({ sortBy: 'activity' }); + await setSortPreference({ sortBy: SortBy.Activity }); }; const toggleGroupByType = async () => { @@ -77,23 +94,23 @@ const DisplayPrefsView = props => { const displayExpanded = async () => { logEvent(events.DP_DISPLAY_EXPANDED); - await setSortPreference({ displayMode: DISPLAY_MODE_EXPANDED }); + await setSortPreference({ displayMode: DisplayMode.Expanded }); }; const displayCondensed = async () => { logEvent(events.DP_DISPLAY_CONDENSED); - await setSortPreference({ displayMode: DISPLAY_MODE_CONDENSED }); + await setSortPreference({ displayMode: DisplayMode.Condensed }); }; - const renderCheckBox = value => ( + const renderCheckBox = (value: boolean) => ( ); - const renderAvatarSwitch = value => ( + const renderAvatarSwitch = (value: boolean) => ( toggleAvatar()} testID='display-pref-view-avatar-switch' /> ); - const renderRadio = value => ( + const renderRadio = (value: boolean) => ( { left={() => } title='Expanded' testID='display-pref-view-expanded' - right={() => renderRadio(displayMode === DISPLAY_MODE_EXPANDED)} + right={() => renderRadio(displayMode === DisplayMode.Expanded)} onPress={displayExpanded} /> @@ -119,7 +136,7 @@ const DisplayPrefsView = props => { left={() => } title='Condensed' testID='display-pref-view-condensed' - right={() => renderRadio(displayMode === DISPLAY_MODE_CONDENSED)} + right={() => renderRadio(displayMode === DisplayMode.Condensed)} onPress={displayCondensed} /> @@ -139,7 +156,7 @@ const DisplayPrefsView = props => { testID='display-pref-view-activity' left={() => } onPress={sortByActivity} - right={() => renderRadio(sortBy === 'activity')} + right={() => renderRadio(sortBy === SortBy.Activity)} /> { testID='display-pref-view-name' left={() => } onPress={sortByName} - right={() => renderRadio(sortBy === 'alphabetical')} + right={() => renderRadio(sortBy === SortBy.Alphabetical)} /> @@ -184,9 +201,6 @@ const DisplayPrefsView = props => { ); }; -DisplayPrefsView.propTypes = { - navigation: PropTypes.object, - isMasterDetail: PropTypes.bool -}; +DisplayPrefsView.propTypes = {}; export default DisplayPrefsView; diff --git a/app/views/E2EEncryptionSecurityView.tsx b/app/views/E2EEncryptionSecurityView.tsx index d5b0b27a..34759043 100644 --- a/app/views/E2EEncryptionSecurityView.tsx +++ b/app/views/E2EEncryptionSecurityView.tsx @@ -75,8 +75,6 @@ class E2EEncryptionSecurityView extends React.Component { - // TODO: Remove ts-ignore when migrate the showConfirmationAlert - // @ts-ignore showConfirmationAlert({ title: I18n.t('Are_you_sure_question_mark'), message: I18n.t('E2E_encryption_reset_message'), diff --git a/app/views/E2EEnterYourPasswordView.tsx b/app/views/E2EEnterYourPasswordView.tsx index dd9cdfa8..6d63f90d 100644 --- a/app/views/E2EEnterYourPasswordView.tsx +++ b/app/views/E2EEnterYourPasswordView.tsx @@ -17,6 +17,7 @@ import KeyboardView from '../presentation/KeyboardView'; import StatusBar from '../containers/StatusBar'; import { events, logEvent } from '../utils/log'; import sharedStyles from './Styles'; +import { E2EEnterYourPasswordStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -36,7 +37,7 @@ interface IE2EEnterYourPasswordViewState { interface IE2EEnterYourPasswordViewProps { encryptionDecodeKey: (password: string) => void; theme: string; - navigation: StackNavigationProp; + navigation: StackNavigationProp; } class E2EEnterYourPasswordView extends React.Component { diff --git a/app/views/E2EHowItWorksView.tsx b/app/views/E2EHowItWorksView.tsx index 0fbdf77a..fce1a2d0 100644 --- a/app/views/E2EHowItWorksView.tsx +++ b/app/views/E2EHowItWorksView.tsx @@ -9,6 +9,7 @@ import * as HeaderButton from '../containers/HeaderButton'; import Markdown from '../containers/markdown'; import { withTheme } from '../theme'; import I18n from '../i18n'; +import { E2ESaveYourPasswordStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -23,8 +24,8 @@ const styles = StyleSheet.create({ }); interface INavigation { - navigation: StackNavigationProp; - route: RouteProp<{ E2EHowItWorksView: { showCloseModal: boolean } }, 'E2EHowItWorksView'>; + navigation: StackNavigationProp; + route: RouteProp; } interface IE2EHowItWorksViewProps extends INavigation { diff --git a/app/views/E2ESaveYourPasswordView.tsx b/app/views/E2ESaveYourPasswordView.tsx index 1c4e13a5..3d9a32ee 100644 --- a/app/views/E2ESaveYourPasswordView.tsx +++ b/app/views/E2ESaveYourPasswordView.tsx @@ -19,6 +19,7 @@ import Button from '../containers/Button'; import { withTheme } from '../theme'; import I18n from '../i18n'; import sharedStyles from './Styles'; +import { E2ESaveYourPasswordStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -60,7 +61,7 @@ interface IE2ESaveYourPasswordViewState { interface IE2ESaveYourPasswordViewProps { server: string; - navigation: StackNavigationProp; + navigation: StackNavigationProp; encryptionSetBanner(): void; theme: string; } diff --git a/app/views/ForgotPasswordView.tsx b/app/views/ForgotPasswordView.tsx index c08a1acd..375d089d 100644 --- a/app/views/ForgotPasswordView.tsx +++ b/app/views/ForgotPasswordView.tsx @@ -14,6 +14,7 @@ import { themes } from '../constants/colors'; import FormContainer, { FormContainerInner } from '../containers/FormContainer'; import { events, logEvent } from '../utils/log'; import sharedStyles from './Styles'; +import { OutsideParamList } from '../stacks/types'; interface IForgotPasswordViewState { email: string; @@ -22,8 +23,8 @@ interface IForgotPasswordViewState { } interface IForgotPasswordViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ ForgotPasswordView: { title: string } }, 'ForgotPasswordView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; } diff --git a/app/views/ForwardLivechatView.tsx b/app/views/ForwardLivechatView.tsx index 42a29782..ea17466d 100644 --- a/app/views/ForwardLivechatView.tsx +++ b/app/views/ForwardLivechatView.tsx @@ -14,6 +14,7 @@ import OrSeparator from '../containers/OrSeparator'; import Input from '../containers/UIKit/MultiSelect/Input'; import { forwardRoom as forwardRoomAction } from '../actions/room'; import { ILivechatDepartment } from './definition/ILivechatDepartment'; +import { ChatsStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -47,8 +48,8 @@ interface IParsedData { } interface IForwardLivechatViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ ForwardLivechatView: { rid: string } }, 'ForwardLivechatView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; forwardRoom: (rid: string, transferData: ITransferData) => void; } diff --git a/app/views/InviteUsersEditView/index.tsx b/app/views/InviteUsersEditView/index.tsx index 62ce5121..4ae1a67d 100644 --- a/app/views/InviteUsersEditView/index.tsx +++ b/app/views/InviteUsersEditView/index.tsx @@ -19,6 +19,7 @@ import { withTheme } from '../../theme'; import SafeAreaView from '../../containers/SafeAreaView'; import { events, logEvent } from '../../utils/log'; import styles from './styles'; +import { ChatsStackParamList } from '../../stacks/types'; const OPTIONS = { days: [ @@ -67,9 +68,9 @@ const OPTIONS = { ] }; -interface IInviteUsersEditView { - navigation: StackNavigationProp; - route: RouteProp<{ InviteUsersEditView: { rid: string } }, 'InviteUsersEditView'>; +interface IInviteUsersEditViewProps { + navigation: StackNavigationProp; + route: RouteProp; theme: string; createInviteLink(rid: string): void; inviteLinksSetParams(params: { [key: string]: number }): void; @@ -77,14 +78,14 @@ interface IInviteUsersEditView { maxUses: number; } -class InviteUsersView extends React.Component { +class InviteUsersEditView extends React.Component { static navigationOptions = (): StackNavigationOptions => ({ title: I18n.t('Invite_users') }); private rid: string; - constructor(props: IInviteUsersEditView) { + constructor(props: IInviteUsersEditViewProps) { super(props); this.rid = props.route.params?.rid; } @@ -160,4 +161,4 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ createInviteLink: (rid: string) => dispatch(inviteLinksCreateAction(rid)) }); -export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersView)); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersEditView)); diff --git a/app/views/InviteUsersView/index.tsx b/app/views/InviteUsersView/index.tsx index cfcd3fa1..b7bf3071 100644 --- a/app/views/InviteUsersView/index.tsx +++ b/app/views/InviteUsersView/index.tsx @@ -6,6 +6,7 @@ import { StackNavigationProp, StackNavigationOptions } from '@react-navigation/s import { RouteProp } from '@react-navigation/core'; import { Dispatch } from 'redux'; +import { ChatsStackParamList } from '../../stacks/types'; import { inviteLinksClear as inviteLinksClearAction, inviteLinksCreate as inviteLinksCreateAction @@ -22,9 +23,9 @@ import SafeAreaView from '../../containers/SafeAreaView'; import { events, logEvent } from '../../utils/log'; import styles from './styles'; -interface IInviteUsersView { - navigation: StackNavigationProp; - route: RouteProp; +interface IInviteUsersViewProps { + navigation: StackNavigationProp; + route: RouteProp; theme: string; timeDateFormat: string; invite: { @@ -36,14 +37,14 @@ interface IInviteUsersView { createInviteLink(rid: string): void; clearInviteLink(): void; } -class InviteUsersView extends React.Component { +class InviteUsersView extends React.Component { private rid: string; static navigationOptions: StackNavigationOptions = { title: I18n.t('Invite_users') }; - constructor(props: IInviteUsersView) { + constructor(props: IInviteUsersViewProps) { super(props); this.rid = props.route.params?.rid; } diff --git a/app/views/JitsiMeetView.tsx b/app/views/JitsiMeetView.tsx index 44034cda..aa6658d2 100644 --- a/app/views/JitsiMeetView.tsx +++ b/app/views/JitsiMeetView.tsx @@ -12,6 +12,7 @@ import ActivityIndicator from '../containers/ActivityIndicator'; import { events, logEvent } from '../utils/log'; import { isAndroid, isIOS } from '../utils/deviceInfo'; import { withTheme } from '../theme'; +import { InsideStackParamList } from '../stacks/types'; const formatUrl = (url: string, baseUrl: string, uriSize: number, avatarAuthURLFragment: string) => `${baseUrl}/avatar/${url}?format=png&width=${uriSize}&height=${uriSize}${avatarAuthURLFragment}`; @@ -25,8 +26,8 @@ interface IJitsiMeetViewState { } interface IJitsiMeetViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ JitsiMeetView: { rid: string; url: string; onlyAudio?: boolean } }, 'JitsiMeetView'>; + navigation: StackNavigationProp; + route: RouteProp; baseUrl: string; theme: string; user: { diff --git a/app/views/LoginView.tsx b/app/views/LoginView.tsx index 4643687e..e43505f3 100644 --- a/app/views/LoginView.tsx +++ b/app/views/LoginView.tsx @@ -15,6 +15,7 @@ import TextInput from '../containers/TextInput'; import { loginRequest as loginRequestAction } from '../actions/login'; import LoginServices from '../containers/LoginServices'; import sharedStyles from './Styles'; +import { OutsideParamList } from '../stacks/types'; const styles = StyleSheet.create({ registerDisabled: { @@ -47,9 +48,9 @@ const styles = StyleSheet.create({ } }); -interface IProps { - navigation: StackNavigationProp; - route: RouteProp; +interface ILoginViewProps { + navigation: StackNavigationProp; + route: RouteProp; Site_Name: string; Accounts_RegistrationForm: string; Accounts_RegistrationForm_LinkReplacementText: string; @@ -67,15 +68,15 @@ interface IProps { inviteLinkToken: string; } -class LoginView extends React.Component { +class LoginView extends React.Component { private passwordInput: any; - static navigationOptions = ({ route, navigation }: Partial) => ({ + static navigationOptions = ({ route, navigation }: ILoginViewProps) => ({ title: route?.params?.title ?? 'Rocket.Chat', headerRight: () => }); - constructor(props: IProps) { + constructor(props: ILoginViewProps) { super(props); this.state = { user: props.route.params?.username ?? '', @@ -83,7 +84,7 @@ class LoginView extends React.Component { }; } - UNSAFE_componentWillReceiveProps(nextProps: IProps) { + UNSAFE_componentWillReceiveProps(nextProps: ILoginViewProps) { const { error } = this.props; if (nextProps.failure && !dequal(error, nextProps.error)) { if (nextProps.error?.error === 'error-invalid-email') { diff --git a/app/views/MarkdownTableView.tsx b/app/views/MarkdownTableView.tsx index a65994ee..e260199e 100644 --- a/app/views/MarkdownTableView.tsx +++ b/app/views/MarkdownTableView.tsx @@ -7,12 +7,10 @@ import I18n from '../i18n'; import { isIOS } from '../utils/deviceInfo'; import { themes } from '../constants/colors'; import { withTheme } from '../theme'; +import { ChatsStackParamList } from '../stacks/types'; interface IMarkdownTableViewProps { - route: RouteProp< - { MarkdownTableView: { renderRows: (drawExtraBorders?: boolean) => JSX.Element; tableWidth: number } }, - 'MarkdownTableView' - >; + route: RouteProp; theme: string; } diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index a948edcd..c9e3d601 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -3,8 +3,9 @@ import { FlatList, Text, View } from 'react-native'; import { connect } from 'react-redux'; import { dequal } from 'dequal'; import { StackNavigationProp } from '@react-navigation/stack'; -import { RouteProp } from '@react-navigation/core'; +import { CompositeNavigationProp, RouteProp } from '@react-navigation/core'; +import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import Message from '../../containers/message'; import ActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; @@ -18,22 +19,19 @@ import { withActionSheet } from '../../containers/ActionSheet'; import SafeAreaView from '../../containers/SafeAreaView'; import getThreadName from '../../lib/methods/getThreadName'; import styles from './styles'; - -type TMessagesViewRouteParams = { - MessagesView: { - rid: string; - t: string; - name: string; - }; -}; +import { ChatsStackParamList } from '../../stacks/types'; +import { ISubscription, SubscriptionType } from '../../definitions/ISubscription'; interface IMessagesViewProps { user: { id: string; }; baseUrl: string; - navigation: StackNavigationProp; - route: RouteProp; + navigation: CompositeNavigationProp< + StackNavigationProp, + StackNavigationProp + >; + route: RouteProp; customEmojis: { [key: string]: string }; theme: string; showActionSheet: Function; @@ -41,6 +39,14 @@ interface IMessagesViewProps { isMasterDetail: boolean; } +interface IRoomInfoParam { + room: ISubscription; + member: any; + rid: string; + t: SubscriptionType; + joined: boolean; +} + interface IMessagesViewState { loading: boolean; messages: []; @@ -65,17 +71,22 @@ interface IMessageItem { } interface IParams { - rid?: string; - jumpToMessageId: string; - t?: string; - room: any; + rid: string; + t: SubscriptionType; tmid?: string; + message?: string; name?: string; + fname?: string; + prid?: string; + room: ISubscription; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; } class MessagesView extends React.Component { - private rid?: string; - private t?: string; + private rid: string; + private t: SubscriptionType; private content: any; private room: any; @@ -121,7 +132,7 @@ class MessagesView extends React.Component { }); }; - navToRoomInfo = (navParam: { rid: string }) => { + navToRoomInfo = (navParam: IRoomInfoParam) => { const { navigation, user } = this.props; if (navParam.rid === user.id) { return; @@ -147,7 +158,7 @@ class MessagesView extends React.Component { ...params, tmid: item.tmid, name: await getThreadName(this.rid, item.tmid, item._id), - t: 'thread' + t: SubscriptionType.THREAD }; navigation.push('RoomView', params); } else { diff --git a/app/views/ModalBlockView.js b/app/views/ModalBlockView.tsx similarity index 70% rename from app/views/ModalBlockView.js rename to app/views/ModalBlockView.tsx index c87bf331..1a517745 100644 --- a/app/views/ModalBlockView.js +++ b/app/views/ModalBlockView.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import { connect } from 'react-redux'; import { KeyboardAwareScrollView } from '@codler/react-native-keyboard-aware-scroll-view'; @@ -15,6 +16,7 @@ import { CONTAINER_TYPES, MODAL_ACTIONS } from '../lib/methods/actions'; import { textParser } from '../containers/UIKit/utils'; import Navigation from '../lib/Navigation'; import sharedStyles from './Styles'; +import { MasterDetailInsideStackParamList } from '../stacks/MasterDetailStack/types'; const styles = StyleSheet.create({ container: { @@ -30,14 +32,49 @@ const styles = StyleSheet.create({ } }); -Object.fromEntries = Object.fromEntries || (arr => arr.reduce((acc, [k, v]) => ((acc[k] = v), acc), {})); -const groupStateByBlockIdMap = (obj, [key, { blockId, value }]) => { +interface IValueBlockId { + value: string; + blockId: string; +} + +type TElementToState = [string, IValueBlockId]; +interface IActions { + actionId: string; + value: any; + blockId?: string; +} + +interface IValues { + [key: string]: { + [key: string]: string; + }; +} +interface IModalBlockViewState { + data: any; + loading: boolean; + errors?: any; +} + +interface IModalBlockViewProps { + navigation: StackNavigationProp; + route: RouteProp; + theme: string; + language: string; + user: { + id: string; + token: string; + }; +} + +// eslint-disable-next-line no-sequences +Object.fromEntries = Object.fromEntries || ((arr: any[]) => arr.reduce((acc, [k, v]) => ((acc[k] = v), acc), {})); +const groupStateByBlockIdMap = (obj: any, [key, { blockId, value }]: TElementToState) => { obj[blockId] = obj[blockId] || {}; obj[blockId][key] = value; return obj; }; -const groupStateByBlockId = obj => Object.entries(obj).reduce(groupStateByBlockIdMap, {}); -const filterInputFields = ({ element, elements = [] }) => { +const groupStateByBlockId = (obj: { [key: string]: any }) => Object.entries(obj).reduce(groupStateByBlockIdMap, {}); +const filterInputFields = ({ element, elements = [] }: { element: any; elements?: any[] }) => { if (element && element.initialValue) { return true; } @@ -45,7 +82,8 @@ const filterInputFields = ({ element, elements = [] }) => { return true; } }; -const mapElementToState = ({ element, blockId, elements = [] }) => { + +const mapElementToState = ({ element, blockId, elements = [] }: { element: any; blockId: string; elements?: any[] }): any => { if (elements.length) { return elements .map(e => ({ element: e, blockId })) @@ -54,10 +92,15 @@ const mapElementToState = ({ element, blockId, elements = [] }) => { } return [element.actionId, { value: element.initialValue, blockId }]; }; -const reduceState = (obj, el) => (Array.isArray(el[0]) ? { ...obj, ...Object.fromEntries(el) } : { ...obj, [el[0]]: el[1] }); +const reduceState = (obj: any, el: any) => + Array.isArray(el[0]) ? { ...obj, ...Object.fromEntries(el) } : { ...obj, [el[0]]: el[1] }; -class ModalBlockView extends React.Component { - static navigationOptions = ({ route }) => { +class ModalBlockView extends React.Component { + private submitting: boolean; + + private values: IValues; + + static navigationOptions = ({ route }: Pick): StackNavigationOptions => { const data = route.params?.data; const { view } = data; const { title } = view; @@ -66,18 +109,7 @@ class ModalBlockView extends React.Component { }; }; - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string, - language: PropTypes.string, - user: PropTypes.shape({ - id: PropTypes.string, - token: PropTypes.string - }) - }; - - constructor(props) { + constructor(props: IModalBlockViewProps) { super(props); this.submitting = false; const data = props.route.params?.data; @@ -95,7 +127,7 @@ class ModalBlockView extends React.Component { EventEmitter.addEventListener(viewId, this.handleUpdate); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: IModalBlockViewProps) { const { navigation, route } = this.props; const oldData = prevProps.route.params?.data ?? {}; const newData = route.params?.data ?? {}; @@ -128,7 +160,7 @@ class ModalBlockView extends React.Component { /> ) - : null, + : undefined, headerRight: submit ? () => ( @@ -140,13 +172,13 @@ class ModalBlockView extends React.Component { /> ) - : null + : undefined }); }; - handleUpdate = ({ type, ...data }) => { + handleUpdate = ({ type, ...data }: { type: string }) => { if ([MODAL_ACTIONS.ERRORS].includes(type)) { - const { errors } = data; + const { errors }: any = data; this.setState({ errors }); } else { this.setState({ data }); @@ -154,7 +186,7 @@ class ModalBlockView extends React.Component { } }; - cancel = async ({ closeModal }) => { + cancel = async ({ closeModal }: { closeModal?: () => void }) => { const { data } = this.state; const { appId, viewId, view } = data; @@ -210,7 +242,7 @@ class ModalBlockView extends React.Component { this.setState({ loading: false }); }; - action = async ({ actionId, value, blockId }) => { + action = async ({ actionId, value, blockId }: IActions) => { const { data } = this.state; const { mid, appId, viewId } = data; await RocketChat.triggerBlockAction({ @@ -227,7 +259,7 @@ class ModalBlockView extends React.Component { this.changeState({ actionId, value, blockId }); }; - changeState = ({ actionId, value, blockId = 'default' }) => { + changeState = ({ actionId, value, blockId = 'default' }: IActions) => { this.values[actionId] = { blockId, value @@ -266,7 +298,7 @@ class ModalBlockView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ language: state.login.user && state.login.user.language }); diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.tsx similarity index 81% rename from app/views/NewMessageView.js rename to app/views/NewMessageView.tsx index 020588ff..cd182251 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { FlatList, StyleSheet, Text, View } from 'react-native'; +import { Dispatch } from 'redux'; import { connect } from 'react-redux'; import { Q } from '@nozbe/watermelondb'; import { dequal } from 'dequal'; -import * as List from '../containers/List'; +import * as List from '../containers/List'; import Touch from '../utils/touch'; import database from '../lib/database'; import RocketChat from '../lib/rocketchat'; @@ -18,7 +19,6 @@ import * as HeaderButton from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; import { themes } from '../constants/colors'; import { withTheme } from '../theme'; -import { getUserSelector } from '../selectors/login'; import Navigation from '../lib/Navigation'; import { createChannelRequest } from '../actions/createChannel'; import { goRoom } from '../utils/goRoom'; @@ -47,33 +47,54 @@ const styles = StyleSheet.create({ } }); -class NewMessageView extends React.Component { - static navigationOptions = ({ navigation }) => ({ +interface IButton { + onPress: () => void; + testID: string; + title: string; + icon: string; + first?: boolean; +} + +interface ISearch { + _id: string; + status: string; + username: string; + avatarETag: string; + outside: boolean; + rid: string; + name: string; + t: string; + search: boolean; +} + +interface INewMessageViewState { + search: ISearch[]; + // TODO: Refactor when migrate room + chats: any[]; + permissions: boolean[]; +} + +interface INewMessageViewProps { + navigation: StackNavigationProp; + create: (params: { group: boolean }) => void; + maxUsers: number; + theme: string; + isMasterDetail: boolean; + serverVersion: string; + createTeamPermission: string[]; + createDirectMessagePermission: string[]; + createPublicChannelPermission: string[]; + createPrivateChannelPermission: string[]; + createDiscussionPermission: string[]; +} + +class NewMessageView extends React.Component { + static navigationOptions = ({ navigation }: INewMessageViewProps): StackNavigationOptions => ({ headerLeft: () => , title: I18n.t('New_Message') }); - static propTypes = { - navigation: PropTypes.object, - baseUrl: PropTypes.string, - user: PropTypes.shape({ - id: PropTypes.string, - token: PropTypes.string, - roles: PropTypes.array - }), - create: PropTypes.func, - maxUsers: PropTypes.number, - theme: PropTypes.string, - isMasterDetail: PropTypes.bool, - serverVersion: PropTypes.string, - createTeamPermission: PropTypes.array, - createDirectMessagePermission: PropTypes.array, - createPublicChannelPermission: PropTypes.array, - createPrivateChannelPermission: PropTypes.array, - createDiscussionPermission: PropTypes.array - }; - - constructor(props) { + constructor(props: INewMessageViewProps) { super(props); this.init(); this.state = { @@ -102,7 +123,7 @@ class NewMessageView extends React.Component { this.handleHasPermission(); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: INewMessageViewProps) { const { createTeamPermission, createPublicChannelPermission, @@ -122,7 +143,7 @@ class NewMessageView extends React.Component { } } - onSearchChangeText(text) { + onSearchChangeText(text: string) { this.search(text); } @@ -131,7 +152,7 @@ class NewMessageView extends React.Component { return navigation.pop(); }; - search = async text => { + search = async (text: string) => { const result = await RocketChat.search({ text, filterRooms: false }); this.setState({ search: result @@ -162,7 +183,8 @@ class NewMessageView extends React.Component { }); }; - goRoom = item => { + // TODO: Refactor when migrate room + goRoom = (item: any) => { logEvent(events.NEW_MSG_CHAT_WITH_USER); const { isMasterDetail, navigation } = this.props; if (isMasterDetail) { @@ -171,7 +193,7 @@ class NewMessageView extends React.Component { goRoom({ item, isMasterDetail }); }; - renderButton = ({ onPress, testID, title, icon, first }) => { + renderButton = ({ onPress, testID, title, icon, first }: IButton) => { const { theme } = this.props; return ( @@ -218,7 +240,7 @@ class NewMessageView extends React.Component { return ( - this.onSearchChangeText(text)} testID='new-message-view-search' /> + this.onSearchChangeText(text)} testID='new-message-view-search' /> {permissions[0] || permissions[1] ? this.renderButton({ @@ -258,9 +280,10 @@ class NewMessageView extends React.Component { ); }; - renderItem = ({ item, index }) => { + // TODO: Refactor when migrate room + renderItem = ({ item, index }: { item: ISearch | any; index: number }) => { const { search, chats } = this.state; - const { baseUrl, user, theme } = this.props; + const { theme } = this.props; let style = { borderColor: themes[theme].separatorColor }; if (index === 0) { @@ -277,10 +300,8 @@ class NewMessageView extends React.Component { name={item.search ? item.name : item.fname} username={item.search ? item.username : item.name} onPress={() => this.goRoom(item)} - baseUrl={baseUrl} testID={`new-message-view-item-${item.name}`} style={style} - user={user} theme={theme} /> ); @@ -313,12 +334,10 @@ class NewMessageView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ serverVersion: state.server.version, isMasterDetail: state.app.isMasterDetail, - baseUrl: state.server.server, maxUsers: state.settings.DirectMesssage_maxUsers || 1, - user: getUserSelector(state), createTeamPermission: state.permissions['create-team'], createDirectMessagePermission: state.permissions['create-d'], createPublicChannelPermission: state.permissions['create-c'], @@ -326,8 +345,8 @@ const mapStateToProps = state => ({ createDiscussionPermission: state.permissions['start-discussion'] }); -const mapDispatchToProps = dispatch => ({ - create: params => dispatch(createChannelRequest(params)) +const mapDispatchToProps = (dispatch: Dispatch) => ({ + create: (params: { group: boolean }) => dispatch(createChannelRequest(params)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewMessageView)); diff --git a/app/views/NewServerView/ServerInput/Item.tsx b/app/views/NewServerView/ServerInput/Item.tsx index 9fb44719..cc8a9e3a 100644 --- a/app/views/NewServerView/ServerInput/Item.tsx +++ b/app/views/NewServerView/ServerInput/Item.tsx @@ -6,7 +6,7 @@ import { themes } from '../../../constants/colors'; import { CustomIcon } from '../../../lib/Icons'; import sharedStyles from '../../Styles'; import Touch from '../../../utils/touch'; -import { IServer } from '../index'; +import { TServerHistory } from '../../../definitions/IServerHistory'; const styles = StyleSheet.create({ container: { @@ -28,10 +28,10 @@ const styles = StyleSheet.create({ }); interface IItem { - item: IServer; + item: TServerHistory; theme: string; onPress(url: string): void; - onDelete(item: IServer): void; + onDelete(item: TServerHistory): void; } const Item = ({ item, theme, onPress, onDelete }: IItem): JSX.Element => ( diff --git a/app/views/NewServerView/ServerInput/index.tsx b/app/views/NewServerView/ServerInput/index.tsx index 1da15136..e2b14fd6 100644 --- a/app/views/NewServerView/ServerInput/index.tsx +++ b/app/views/NewServerView/ServerInput/index.tsx @@ -5,8 +5,8 @@ import TextInput from '../../../containers/TextInput'; import * as List from '../../../containers/List'; import { themes } from '../../../constants/colors'; import I18n from '../../../i18n'; +import { TServerHistory } from '../../../definitions/IServerHistory'; import Item from './Item'; -import { IServer } from '../index'; const styles = StyleSheet.create({ container: { @@ -33,8 +33,8 @@ interface IServerInput extends TextInputProps { theme: string; serversHistory: any[]; onSubmit(): void; - onDelete(item: IServer): void; - onPressServerHistory(serverHistory: IServer): void; + onDelete(item: TServerHistory): void; + onPressServerHistory(serverHistory: TServerHistory): void; } const ServerInput = ({ diff --git a/app/views/NewServerView/index.tsx b/app/views/NewServerView/index.tsx index f1458c93..93e493be 100644 --- a/app/views/NewServerView/index.tsx +++ b/app/views/NewServerView/index.tsx @@ -8,7 +8,6 @@ import { TouchableOpacity } from 'react-native-gesture-handler'; import Orientation from 'react-native-orientation-locker'; import { StackNavigationProp } from '@react-navigation/stack'; import { Dispatch } from 'redux'; -import Model from '@nozbe/watermelondb/Model'; import UserPreferences from '../../lib/userPreferences'; import EventEmitter from '../../utils/events'; @@ -33,6 +32,8 @@ import { isTablet } from '../../utils/deviceInfo'; import { verticalScale, moderateScale } from '../../utils/scaling'; import { withDimensions } from '../../dimensions'; import ServerInput from './ServerInput'; +import { OutsideParamList } from '../../stacks/types'; +import { TServerHistory } from '../../definitions/IServerHistory'; const styles = StyleSheet.create({ onboardingImage: { @@ -67,13 +68,8 @@ const styles = StyleSheet.create({ } }); -export interface IServer extends Model { - url: string; - username: string; -} - interface INewServerView { - navigation: StackNavigationProp; + navigation: StackNavigationProp; theme: string; connecting: boolean; connectServer(server: string, username?: string, fromServerHistory?: boolean): void; @@ -89,7 +85,7 @@ interface IState { text: string; connectingOpen: boolean; certificate: any; - serversHistory: IServer[]; + serversHistory: TServerHistory[]; } interface ISubmitParams { @@ -165,7 +161,7 @@ class NewServerView extends React.Component { const likeString = sanitizeLikeString(text); whereClause = [...whereClause, Q.where('url', Q.like(`%${likeString}%`))]; } - const serversHistory = (await serversHistoryCollection.query(...whereClause).fetch()) as IServer[]; + const serversHistory = (await serversHistoryCollection.query(...whereClause).fetch()) as TServerHistory[]; this.setState({ serversHistory }); } catch { // Do nothing @@ -189,7 +185,7 @@ class NewServerView extends React.Component { connectServer(server); }; - onPressServerHistory = (serverHistory: IServer) => { + onPressServerHistory = (serverHistory: TServerHistory) => { this.setState({ text: serverHistory.url }, () => this.submit({ fromServerHistory: true, username: serverHistory?.username })); }; @@ -273,23 +269,22 @@ class NewServerView extends React.Component { uriToPath = (uri: string) => uri.replace('file://', ''); handleRemove = () => { - // TODO: Remove ts-ignore when migrate the showConfirmationAlert - // @ts-ignore showConfirmationAlert({ message: I18n.t('You_will_unset_a_certificate_for_this_server'), confirmationText: I18n.t('Remove'), + // @ts-ignore onPress: this.setState({ certificate: null }) // We not need delete file from DocumentPicker because it is a temp file }); }; - deleteServerHistory = async (item: IServer) => { + deleteServerHistory = async (item: TServerHistory) => { const db = database.servers; try { await db.write(async () => { await item.destroyPermanently(); }); this.setState((prevstate: IState) => ({ - serversHistory: prevstate.serversHistory.filter((server: IServer) => server.id !== item.id) + serversHistory: prevstate.serversHistory.filter((server: TServerHistory) => server.id !== item.id) })); } catch { // Nothing diff --git a/app/views/NotificationPreferencesView/index.tsx b/app/views/NotificationPreferencesView/index.tsx index a020c163..5e33cec4 100644 --- a/app/views/NotificationPreferencesView/index.tsx +++ b/app/views/NotificationPreferencesView/index.tsx @@ -17,6 +17,7 @@ import SafeAreaView from '../../containers/SafeAreaView'; import log, { events, logEvent } from '../../utils/log'; import sharedStyles from '../Styles'; import { OPTIONS } from './options'; +import { ChatsStackParamList } from '../../stacks/types'; const styles = StyleSheet.create({ pickerText: { @@ -26,16 +27,8 @@ const styles = StyleSheet.create({ }); interface INotificationPreferencesView { - navigation: StackNavigationProp; - route: RouteProp< - { - NotificationPreferencesView: { - rid: string; - room: Model; - }; - }, - 'NotificationPreferencesView' - >; + navigation: StackNavigationProp; + route: RouteProp; theme: string; } diff --git a/app/views/NotificationPreferencesView/options.ts b/app/views/NotificationPreferencesView/options.ts index 4035c038..a2b3251c 100644 --- a/app/views/NotificationPreferencesView/options.ts +++ b/app/views/NotificationPreferencesView/options.ts @@ -1,4 +1,4 @@ -interface IOptionsField { +export interface IOptionsField { label: string; value: string | number; second?: number; diff --git a/app/views/PickerView.tsx b/app/views/PickerView.tsx index 002979b2..db2a7a26 100644 --- a/app/views/PickerView.tsx +++ b/app/views/PickerView.tsx @@ -11,6 +11,8 @@ import * as List from '../containers/List'; import SearchBox from '../containers/SearchBox'; import SafeAreaView from '../containers/SafeAreaView'; import sharedStyles from './Styles'; +import { ChatsStackParamList } from '../stacks/types'; +import { IOptionsField } from './NotificationPreferencesView/options'; const styles = StyleSheet.create({ search: { @@ -25,37 +27,21 @@ const styles = StyleSheet.create({ } }); -interface IData { - label: string; - value: string; - second?: string; -} - interface IItem { - item: IData; + item: IOptionsField; selected: boolean; onItemPress: () => void; theme: string; } interface IPickerViewState { - data: IData[]; + data: IOptionsField[]; value: string; } -interface IParams { - title: string; - value: string; - data: IData[]; - onChangeText: (value: string) => IData[]; - goBack: boolean; - onChange: Function; - onChangeValue: (value: string) => void; -} - interface IPickerViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ PickerView: IParams }, 'PickerView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; } @@ -69,7 +55,7 @@ const Item = React.memo(({ item, selected, onItemPress, theme }: IItem) => ( )); class PickerView extends React.PureComponent { - private onSearch: (text: string) => IData[]; + private onSearch?: ((text: string) => IOptionsField[]) | ((term?: string | undefined) => Promise); static navigationOptions = ({ route }: IPickerViewProps) => ({ title: route.params?.title ?? I18n.t('Select_an_option') @@ -126,13 +112,13 @@ class PickerView extends React.PureComponent {this.renderSearch()}
): (props: any) => JSX.Element { +export function withTheme(Component: any): any { const ThemedComponent = (props: any) => ( {contexts => } ); diff --git a/app/utils/appGroup.js b/app/utils/appGroup.ts similarity index 83% rename from app/utils/appGroup.js rename to app/utils/appGroup.ts index 63fb428a..f92227c0 100644 --- a/app/utils/appGroup.js +++ b/app/utils/appGroup.ts @@ -4,7 +4,7 @@ import { isIOS } from './deviceInfo'; const { AppGroup } = NativeModules; -const appGroup = { +const appGroup: { path: string } = { path: isIOS ? AppGroup.path : '' }; diff --git a/app/utils/avatar.js b/app/utils/avatar.ts similarity index 72% rename from app/utils/avatar.js rename to app/utils/avatar.ts index 4cc15cda..7e4b2819 100644 --- a/app/utils/avatar.js +++ b/app/utils/avatar.ts @@ -1,6 +1,8 @@ import { compareServerVersion, methods } from '../lib/utils'; +import { SubscriptionType } from '../definitions/ISubscription'; +import { IAvatar } from '../containers/Avatar/interfaces'; -const formatUrl = (url, size, query) => `${url}?format=png&size=${size}${query}`; +const formatUrl = (url: string, size: number, query: string) => `${url}?format=png&size=${size}${query}`; export const avatarURL = ({ type, @@ -13,9 +15,9 @@ export const avatarURL = ({ rid, blockUnauthenticatedAccess, serverVersion -}) => { +}: IAvatar): string => { let room; - if (type === 'd') { + if (type === SubscriptionType.DIRECT) { room = text; } else if (rid && !compareServerVersion(serverVersion, '3.6.0', methods.lowerThan)) { room = `room/${rid}`; diff --git a/app/utils/base64-js/index.js b/app/utils/base64-js/index.ts similarity index 87% rename from app/utils/base64-js/index.js rename to app/utils/base64-js/index.ts index 5616f71d..71fac91c 100644 --- a/app/utils/base64-js/index.js +++ b/app/utils/base64-js/index.ts @@ -1,8 +1,8 @@ /* eslint-disable no-bitwise */ // https://github.com/beatgammit/base64-js/blob/master/index.js -const lookup = []; -const revLookup = []; +const lookup: string[] = []; +const revLookup: number[] = []; const Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array; const code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; @@ -16,7 +16,7 @@ for (let i = 0, len = code.length; i < len; i += 1) { revLookup['-'.charCodeAt(0)] = 62; revLookup['_'.charCodeAt(0)] = 63; -const getLens = b64 => { +const getLens = (b64: string) => { const len = b64.length; // We're encoding some strings not multiple of 4, so, disable this check @@ -37,16 +37,17 @@ const getLens = b64 => { }; // base64 is 4/3 + up to two characters of the original data -export const byteLength = b64 => { +export const byteLength = (b64: string) => { const lens = getLens(b64); const validLen = lens[0]; const placeHoldersLen = lens[1]; return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; }; -const _byteLength = (b64, validLen, placeHoldersLen) => ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; +const _byteLength = (b64: string, validLen: number, placeHoldersLen: number) => + ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; -export const toByteArray = b64 => { +export const toByteArray = (b64: string) => { let tmp; const lens = getLens(b64); const validLen = lens[0]; @@ -92,10 +93,10 @@ export const toByteArray = b64 => { return arr; }; -const tripletToBase64 = num => +const tripletToBase64 = (num: number) => lookup[(num >> 18) & 0x3f] + lookup[(num >> 12) & 0x3f] + lookup[(num >> 6) & 0x3f] + lookup[num & 0x3f]; -const encodeChunk = (uint8, start, end) => { +const encodeChunk = (uint8: number[] | Uint8Array, start: number, end: number) => { let tmp; const output = []; for (let i = start; i < end; i += 3) { @@ -105,7 +106,7 @@ const encodeChunk = (uint8, start, end) => { return output.join(''); }; -export const fromByteArray = uint8 => { +export const fromByteArray = (uint8: number[] | Uint8Array) => { let tmp; const len = uint8.length; const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes diff --git a/app/utils/debounce.js b/app/utils/debounce.js deleted file mode 100644 index 106c61d0..00000000 --- a/app/utils/debounce.js +++ /dev/null @@ -1,20 +0,0 @@ -export default function debounce(func, wait, immediate) { - let timeout; - function _debounce(...args) { - const context = this; - const later = function __debounce() { - timeout = null; - if (!immediate) { - func.apply(context, args); - } - }; - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - func.apply(context, args); - } - } - _debounce.stop = () => clearTimeout(timeout); - return _debounce; -} diff --git a/app/utils/debounce.ts b/app/utils/debounce.ts new file mode 100644 index 00000000..e0c28b23 --- /dev/null +++ b/app/utils/debounce.ts @@ -0,0 +1,22 @@ +export default function debounce(func: Function, wait?: number, immediate?: boolean) { + let timeout: number | null; + function _debounce(...args: any[]) { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-this-alias + const context = this; + const later = function __debounce() { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + const callNow = immediate && !timeout; + clearTimeout(timeout!); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + } + _debounce.stop = () => clearTimeout(timeout!); + return _debounce; +} diff --git a/app/utils/deviceInfo.js b/app/utils/deviceInfo.ts similarity index 92% rename from app/utils/deviceInfo.js rename to app/utils/deviceInfo.ts index 7961b440..9cb7ac72 100644 --- a/app/utils/deviceInfo.js +++ b/app/utils/deviceInfo.ts @@ -9,7 +9,7 @@ export const getBundleId = DeviceInfo.getBundleId(); export const getDeviceModel = DeviceInfo.getModel(); // Theme is supported by system on iOS 13+ or Android 10+ -export const supportSystemTheme = () => { +export const supportSystemTheme = (): boolean => { const systemVersion = parseInt(DeviceInfo.getSystemVersion(), 10); return systemVersion >= (isIOS ? 13 : 10); }; diff --git a/app/utils/events.js b/app/utils/events.ts similarity index 53% rename from app/utils/events.js rename to app/utils/events.ts index 8e67fc82..fc0b975a 100644 --- a/app/utils/events.js +++ b/app/utils/events.ts @@ -1,11 +1,25 @@ +import { ICommand } from '../definitions/ICommand'; import log from './log'; +type TEventEmitterEmmitArgs = + | { rid: string } + | { message: string } + | { method: string } + | { invalid: boolean } + | { force: boolean } + | { hasBiometry: boolean } + | { event: string | ICommand } + | { cancel: () => void } + | { submit: (param: string) => void }; + class EventEmitter { + private events: { [key: string]: any }; + constructor() { this.events = {}; } - addEventListener(event, listener) { + addEventListener(event: string, listener: Function) { if (typeof this.events[event] !== 'object') { this.events[event] = []; } @@ -13,7 +27,7 @@ class EventEmitter { return listener; } - removeListener(event, listener) { + removeListener(event: string, listener: Function) { if (typeof this.events[event] === 'object') { const idx = this.events[event].indexOf(listener); if (idx > -1) { @@ -25,9 +39,9 @@ class EventEmitter { } } - emit(event, ...args) { + emit(event: string, ...args: TEventEmitterEmmitArgs[]) { if (typeof this.events[event] === 'object') { - this.events[event].forEach(listener => { + this.events[event].forEach((listener: Function) => { try { listener.apply(this, args); } catch (e) { diff --git a/app/utils/fetch.js b/app/utils/fetch.ts similarity index 75% rename from app/utils/fetch.js rename to app/utils/fetch.ts index 84f5669a..c8758da8 100644 --- a/app/utils/fetch.js +++ b/app/utils/fetch.ts @@ -4,15 +4,20 @@ import { settings as RocketChatSettings } from '@rocket.chat/sdk'; import RocketChat from '../lib/rocketchat'; +interface CustomHeaders { + 'User-Agent': string; + Authorization?: string; +} + // this form is required by Rocket.Chat's parser in "app/statistics/server/lib/UAParserCustom.js" -export const headers = { +export const headers: CustomHeaders = { 'User-Agent': `RC Mobile; ${ Platform.OS } ${DeviceInfo.getSystemVersion()}; v${DeviceInfo.getVersion()} (${DeviceInfo.getBuildNumber()})` }; let _basicAuth; -export const setBasicAuth = basicAuth => { +export const setBasicAuth = (basicAuth: string): void => { _basicAuth = basicAuth; if (basicAuth) { RocketChatSettings.customHeaders = { ...headers, Authorization: `Basic ${_basicAuth}` }; @@ -24,12 +29,15 @@ export const BASIC_AUTH_KEY = 'BASIC_AUTH_KEY'; RocketChatSettings.customHeaders = headers; -export default (url, options = {}) => { +export default (url: string, options: { headers?: Headers; signal?: AbortSignal } = {}): Promise => { let customOptions = { ...options, headers: RocketChatSettings.customHeaders }; if (options && options.headers) { customOptions = { ...customOptions, headers: { ...options.headers, ...customOptions.headers } }; } + // TODO: Refactor when migrate rocketchat.js + // @ts-ignore if (RocketChat.controller) { + // @ts-ignore const { signal } = RocketChat.controller; customOptions = { ...customOptions, signal }; } diff --git a/app/utils/fileDownload/index.ts b/app/utils/fileDownload/index.ts index dda1a78f..279d3b3a 100644 --- a/app/utils/fileDownload/index.ts +++ b/app/utils/fileDownload/index.ts @@ -5,13 +5,7 @@ import EventEmitter from '../events'; import { LISTENER } from '../../containers/Toast'; import I18n from '../../i18n'; import { DOCUMENTS_PATH, DOWNLOAD_PATH } from '../../constants/localPath'; - -interface IAttachment { - title: string; - title_link: string; - type: string; - description: string; -} +import { IAttachment } from '../../definitions/IAttachment'; export const getLocalFilePathFromFile = (localPath: string, attachment: IAttachment): string => `${localPath}${attachment.title}`; diff --git a/app/utils/fileUpload/index.ios.js b/app/utils/fileUpload/index.ios.ts similarity index 65% rename from app/utils/fileUpload/index.ios.js rename to app/utils/fileUpload/index.ios.ts index a9764055..ae5cfabc 100644 --- a/app/utils/fileUpload/index.ios.js +++ b/app/utils/fileUpload/index.ios.ts @@ -1,19 +1,25 @@ +import { IFileUpload } from './interfaces'; + class Upload { + public xhr: XMLHttpRequest; + + public formData: FormData; + constructor() { this.xhr = new XMLHttpRequest(); this.formData = new FormData(); } - then = callback => { + then = (callback: (param: { respInfo: XMLHttpRequest }) => XMLHttpRequest) => { this.xhr.onload = () => callback({ respInfo: this.xhr }); this.xhr.send(this.formData); }; - catch = callback => { + catch = (callback: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null) => { this.xhr.onerror = callback; }; - uploadProgress = callback => { + uploadProgress = (callback: (param: number, arg1: number) => any) => { this.xhr.upload.onprogress = ({ total, loaded }) => callback(loaded, total); }; @@ -24,7 +30,7 @@ class Upload { } class FileUpload { - fetch = (method, url, headers, data) => { + fetch = (method: string, url: string, headers: { [x: string]: string }, data: IFileUpload[]) => { const upload = new Upload(); upload.xhr.open(method, url); @@ -35,6 +41,7 @@ class FileUpload { data.forEach(item => { if (item.uri) { upload.formData.append(item.name, { + // @ts-ignore uri: item.uri, type: item.type, name: item.filename diff --git a/app/utils/fileUpload/index.android.js b/app/utils/fileUpload/index.ts similarity index 64% rename from app/utils/fileUpload/index.android.js rename to app/utils/fileUpload/index.ts index 5c45c27b..1d2bdb31 100644 --- a/app/utils/fileUpload/index.android.js +++ b/app/utils/fileUpload/index.ts @@ -1,7 +1,11 @@ import RNFetchBlob from 'rn-fetch-blob'; +import { IFileUpload } from './interfaces'; + +type TMethods = 'POST' | 'GET' | 'DELETE' | 'PUT' | 'post' | 'get' | 'delete' | 'put'; + class FileUpload { - fetch = (method, url, headers, data) => { + fetch = (method: TMethods, url: string, headers: { [key: string]: string }, data: IFileUpload[]) => { const formData = data.map(item => { if (item.uri) { return { diff --git a/app/utils/fileUpload/interfaces.ts b/app/utils/fileUpload/interfaces.ts new file mode 100644 index 00000000..a3002f72 --- /dev/null +++ b/app/utils/fileUpload/interfaces.ts @@ -0,0 +1,7 @@ +export interface IFileUpload { + name: string; + uri?: string; + type: string; + filename: string; + data: any; +} diff --git a/app/utils/goRoom.js b/app/utils/goRoom.ts similarity index 58% rename from app/utils/goRoom.js rename to app/utils/goRoom.ts index 1025a17d..dc8a3188 100644 --- a/app/utils/goRoom.js +++ b/app/utils/goRoom.ts @@ -1,7 +1,17 @@ +import { ChatsStackParamList } from '../stacks/types'; import Navigation from '../lib/Navigation'; import RocketChat from '../lib/rocketchat'; +import { ISubscription, SubscriptionType } from '../definitions/ISubscription'; -const navigate = ({ item, isMasterDetail, ...props }) => { +const navigate = ({ + item, + isMasterDetail, + ...props +}: { + item: IItem; + isMasterDetail: boolean; + navigationMethod?: () => ChatsStackParamList; +}) => { let navigationMethod = props.navigationMethod ?? Navigation.navigate; if (isMasterDetail) { @@ -20,7 +30,22 @@ const navigate = ({ item, isMasterDetail, ...props }) => { }); }; -export const goRoom = async ({ item = {}, isMasterDetail = false, ...props }) => { +interface IItem extends Partial { + rid: string; + name: string; + t: SubscriptionType; +} + +export const goRoom = async ({ + item, + isMasterDetail = false, + ...props +}: { + item: IItem; + isMasterDetail: boolean; + navigationMethod?: any; + jumpToMessageId?: string; +}): Promise => { if (item.t === 'd' && item.search) { // if user is using the search we need first to join/create room try { @@ -30,8 +55,8 @@ export const goRoom = async ({ item = {}, isMasterDetail = false, ...props }) => return navigate({ item: { rid: result.room._id, - name: username, - t: 'd' + name: username!, + t: SubscriptionType.DIRECT }, isMasterDetail, ...props diff --git a/app/utils/info.js b/app/utils/info.js deleted file mode 100644 index 5d72f200..00000000 --- a/app/utils/info.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Alert } from 'react-native'; - -import I18n from '../i18n'; - -export const showErrorAlert = (message, title, onPress = () => {}) => - Alert.alert(title, message, [{ text: 'OK', onPress }], { cancelable: true }); - -export const showConfirmationAlert = ({ title, message, confirmationText, dismissText = I18n.t('Cancel'), onPress, onCancel }) => - Alert.alert( - title || I18n.t('Are_you_sure_question_mark'), - message, - [ - { - text: dismissText, - onPress: onCancel, - style: 'cancel' - }, - { - text: confirmationText, - style: 'destructive', - onPress - } - ], - { cancelable: false } - ); diff --git a/app/utils/info.ts b/app/utils/info.ts new file mode 100644 index 00000000..da882ee4 --- /dev/null +++ b/app/utils/info.ts @@ -0,0 +1,41 @@ +import { Alert } from 'react-native'; + +import I18n from '../i18n'; + +export const showErrorAlert = (message: string, title?: string, onPress = () => {}): void => + Alert.alert(title!, message, [{ text: 'OK', onPress }], { cancelable: true }); + +interface IShowConfirmationAlert { + title?: string; + message: string; + confirmationText: string; + dismissText?: string; + onPress: () => void; + onCancel?: () => void; +} + +export const showConfirmationAlert = ({ + title, + message, + confirmationText, + dismissText = I18n.t('Cancel'), + onPress, + onCancel +}: IShowConfirmationAlert): void => + Alert.alert( + title || I18n.t('Are_you_sure_question_mark'), + message, + [ + { + text: dismissText, + onPress: onCancel, + style: 'cancel' + }, + { + text: confirmationText, + style: 'destructive', + onPress + } + ], + { cancelable: false } + ); diff --git a/app/utils/isReadOnly.js b/app/utils/isReadOnly.ts similarity index 59% rename from app/utils/isReadOnly.js rename to app/utils/isReadOnly.ts index 62ae4fff..d94b73c4 100644 --- a/app/utils/isReadOnly.js +++ b/app/utils/isReadOnly.ts @@ -1,16 +1,21 @@ import RocketChat from '../lib/rocketchat'; import reduxStore from '../lib/createStore'; +import { ISubscription } from '../definitions/ISubscription'; -const canPostReadOnly = async ({ rid }) => { +const canPostReadOnly = async ({ rid }: { rid: string }) => { // TODO: this is not reactive. If this permission changes, the component won't be updated const postReadOnlyPermission = reduxStore.getState().permissions['post-readonly']; const permission = await RocketChat.hasPermission([postReadOnlyPermission], rid); return permission[0]; }; -const isMuted = (room, user) => room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username); +const isMuted = (room: ISubscription, user: { username: string }) => + room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username); -export const isReadOnly = async (room, user) => { +export const isReadOnly = async ( + room: ISubscription, + user: { id?: string; username: string; token?: string } +): Promise => { if (room.archived) { return true; } diff --git a/app/utils/isValidEmail.js b/app/utils/isValidEmail.ts similarity index 78% rename from app/utils/isValidEmail.js rename to app/utils/isValidEmail.ts index a8bd490f..e230fc32 100644 --- a/app/utils/isValidEmail.js +++ b/app/utils/isValidEmail.ts @@ -1,4 +1,4 @@ -export default function isValidEmail(email) { +export default function isValidEmail(email: string): boolean { /* eslint-disable no-useless-escape */ const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; diff --git a/app/utils/layoutAnimation.js b/app/utils/layoutAnimation.ts similarity index 100% rename from app/utils/layoutAnimation.js rename to app/utils/layoutAnimation.ts diff --git a/app/utils/localAuthentication.js b/app/utils/localAuthentication.ts similarity index 73% rename from app/utils/localAuthentication.js rename to app/utils/localAuthentication.ts index 29f25685..f4359963 100644 --- a/app/utils/localAuthentication.js +++ b/app/utils/localAuthentication.ts @@ -16,16 +16,17 @@ import { } from '../constants/localAuthentication'; import I18n from '../i18n'; import { setLocalAuthenticated } from '../actions/login'; +import { TServerModel } from '../definitions/IServer'; import EventEmitter from './events'; import { isIOS } from './deviceInfo'; -export const saveLastLocalAuthenticationSession = async (server, serverRecord) => { +export const saveLastLocalAuthenticationSession = async (server: string, serverRecord?: TServerModel): Promise => { const serversDB = database.servers; const serversCollection = serversDB.get('servers'); - await serversDB.action(async () => { + await serversDB.write(async () => { try { if (!serverRecord) { - serverRecord = await serversCollection.find(server); + serverRecord = (await serversCollection.find(server)) as TServerModel; } await serverRecord.update(record => { record.lastLocalAuthenticatedSession = new Date(); @@ -36,31 +37,31 @@ export const saveLastLocalAuthenticationSession = async (server, serverRecord) = }); }; -export const resetAttempts = () => AsyncStorage.multiRemove([LOCKED_OUT_TIMER_KEY, ATTEMPTS_KEY]); +export const resetAttempts = (): Promise => AsyncStorage.multiRemove([LOCKED_OUT_TIMER_KEY, ATTEMPTS_KEY]); -const openModal = hasBiometry => - new Promise(resolve => { +const openModal = (hasBiometry: boolean) => + new Promise(resolve => { EventEmitter.emit(LOCAL_AUTHENTICATE_EMITTER, { submit: () => resolve(), hasBiometry }); }); -const openChangePasscodeModal = ({ force }) => - new Promise((resolve, reject) => { +const openChangePasscodeModal = ({ force }: { force: boolean }) => + new Promise((resolve, reject) => { EventEmitter.emit(CHANGE_PASSCODE_EMITTER, { - submit: passcode => resolve(passcode), + submit: (passcode: string) => resolve(passcode), cancel: () => reject(), force }); }); -export const changePasscode = async ({ force = false }) => { +export const changePasscode = async ({ force = false }: { force: boolean }): Promise => { const passcode = await openChangePasscodeModal({ force }); await UserPreferences.setStringAsync(PASSCODE_KEY, sha256(passcode)); }; -export const biometryAuth = force => +export const biometryAuth = (force?: boolean): Promise => LocalAuthentication.authenticateAsync({ disableDeviceFallback: true, cancelLabel: force ? I18n.t('Dont_activate') : I18n.t('Local_authentication_biometry_fallback'), @@ -71,11 +72,11 @@ export const biometryAuth = force => * It'll help us to get the permission to use FaceID * and enable/disable the biometry when user put their first passcode */ -const checkBiometry = async serverRecord => { +const checkBiometry = async (serverRecord: TServerModel) => { const serversDB = database.servers; const result = await biometryAuth(true); - await serversDB.action(async () => { + await serversDB.write(async () => { try { await serverRecord.update(record => { record.biometry = !!result?.success; @@ -86,7 +87,13 @@ const checkBiometry = async serverRecord => { }); }; -export const checkHasPasscode = async ({ force = true, serverRecord }) => { +export const checkHasPasscode = async ({ + force = true, + serverRecord +}: { + force?: boolean; + serverRecord: TServerModel; +}): Promise<{ newPasscode?: boolean } | void> => { const storedPasscode = await UserPreferences.getStringAsync(PASSCODE_KEY); if (!storedPasscode) { await changePasscode({ force }); @@ -96,13 +103,13 @@ export const checkHasPasscode = async ({ force = true, serverRecord }) => { return Promise.resolve(); }; -export const localAuthenticate = async server => { +export const localAuthenticate = async (server: string): Promise => { const serversDB = database.servers; const serversCollection = serversDB.get('servers'); - let serverRecord; + let serverRecord: TServerModel; try { - serverRecord = await serversCollection.find(server); + serverRecord = (await serversCollection.find(server)) as TServerModel; } catch (error) { return Promise.reject(); } @@ -125,7 +132,7 @@ export const localAuthenticate = async server => { const diffToLastSession = moment().diff(serverRecord?.lastLocalAuthenticatedSession, 'seconds'); // if last authenticated session is older than configured auto lock time, authentication is required - if (diffToLastSession >= serverRecord?.autoLockTime) { + if (diffToLastSession >= serverRecord.autoLockTime!) { // set isLocalAuthenticated to false store.dispatch(setLocalAuthenticated(false)); @@ -150,7 +157,7 @@ export const localAuthenticate = async server => { } }; -export const supportedBiometryLabel = async () => { +export const supportedBiometryLabel = async (): Promise => { try { const enrolled = await LocalAuthentication.isEnrolledAsync(); diff --git a/app/utils/log/events.js b/app/utils/log/events.ts similarity index 99% rename from app/utils/log/events.js rename to app/utils/log/events.ts index fc7a3497..82dd3079 100644 --- a/app/utils/log/events.js +++ b/app/utils/log/events.ts @@ -253,7 +253,6 @@ export default { RA_GO_AUTOTRANSLATE: 'ra_go_autotranslate', RA_GO_NOTIFICATIONPREF: 'ra_go_notification_pref', RA_GO_FORWARDLIVECHAT: 'ra_go_forward_livechat', - RA_GO_VISITORNAVIGATION: 'ra_go_visitor_navigation', RA_SHARE: 'ra_share', RA_LEAVE: 'ra_leave', RA_LEAVE_F: 'ra_leave_f', diff --git a/app/utils/log/index.js b/app/utils/log/index.ts similarity index 66% rename from app/utils/log/index.js rename to app/utils/log/index.ts index 41074832..d52bd0e9 100644 --- a/app/utils/log/index.js +++ b/app/utils/log/index.ts @@ -4,13 +4,13 @@ import { isFDroidBuild } from '../../constants/environment'; import events from './events'; const analytics = firebaseAnalytics || ''; -let bugsnag = ''; -let crashlytics; +let bugsnag: any = ''; +let crashlytics: any; let reportCrashErrors = true; let reportAnalyticsEvents = true; -export const getReportCrashErrorsValue = () => reportCrashErrors; -export const getReportAnalyticsEventsValue = () => reportAnalyticsEvents; +export const getReportCrashErrorsValue = (): boolean => reportCrashErrors; +export const getReportAnalyticsEventsValue = (): boolean => reportAnalyticsEvents; if (!isFDroidBuild) { bugsnag = require('@bugsnag/react-native').default; @@ -18,7 +18,7 @@ if (!isFDroidBuild) { onBreadcrumb() { return reportAnalyticsEvents; }, - onError(error) { + onError(error: { breadcrumbs: string[] }) { if (!reportAnalyticsEvents) { error.breadcrumbs = []; } @@ -34,13 +34,13 @@ export { events }; let metadata = {}; -export const logServerVersion = serverVersion => { +export const logServerVersion = (serverVersion: string): void => { metadata = { serverVersion }; }; -export const logEvent = (eventName, payload) => { +export const logEvent = (eventName: string, payload?: { [key: string]: any }): void => { try { if (!isFDroidBuild) { analytics().logEvent(eventName, payload); @@ -51,26 +51,26 @@ export const logEvent = (eventName, payload) => { } }; -export const setCurrentScreen = currentScreen => { +export const setCurrentScreen = (currentScreen: string): void => { if (!isFDroidBuild) { analytics().setCurrentScreen(currentScreen); bugsnag.leaveBreadcrumb(currentScreen, { type: 'navigation' }); } }; -export const toggleCrashErrorsReport = value => { +export const toggleCrashErrorsReport = (value: boolean): boolean => { crashlytics().setCrashlyticsCollectionEnabled(value); return (reportCrashErrors = value); }; -export const toggleAnalyticsEventsReport = value => { +export const toggleAnalyticsEventsReport = (value: boolean): boolean => { analytics().setAnalyticsCollectionEnabled(value); return (reportAnalyticsEvents = value); }; -export default e => { +export default (e: any): void => { if (e instanceof Error && bugsnag && e.message !== 'Aborted' && !__DEV__) { - bugsnag.notify(e, event => { + bugsnag.notify(e, (event: { addMetadata: (arg0: string, arg1: {}) => void }) => { event.addMetadata('details', { ...metadata }); }); if (!isFDroidBuild) { diff --git a/app/utils/media.js b/app/utils/media.ts similarity index 55% rename from app/utils/media.js rename to app/utils/media.ts index b05f95a9..78b1c29f 100644 --- a/app/utils/media.js +++ b/app/utils/media.ts @@ -1,20 +1,30 @@ -export const canUploadFile = (file, allowList, maxFileSize) => { +import { IAttachment } from '../views/ShareView/interfaces'; + +export const canUploadFile = ( + file: IAttachment, + allowList: string, + maxFileSize: number, + permissionToUploadFile: boolean +): { success: boolean; error?: string } => { if (!(file && file.path)) { return { success: true }; } if (maxFileSize > -1 && file.size > maxFileSize) { return { success: false, error: 'error-file-too-large' }; } + if (!permissionToUploadFile) { + return { success: false, error: 'error-not-permission-to-upload-file' }; + } // if white list is empty, all media types are enabled if (!allowList || allowList === '*') { return { success: true }; } const allowedMime = allowList.split(','); - if (allowedMime.includes(file.mime)) { + if (allowedMime.includes(file.mime!)) { return { success: true }; } const wildCardGlob = '/*'; - const wildCards = allowedMime.filter(item => item.indexOf(wildCardGlob) > 0); + const wildCards = allowedMime.filter((item: string) => item.indexOf(wildCardGlob) > 0); if (file.mime && wildCards.includes(file.mime.replace(/(\/.*)$/, wildCardGlob))) { return { success: true }; } diff --git a/app/utils/messageTypes.js b/app/utils/messageTypes.ts similarity index 100% rename from app/utils/messageTypes.js rename to app/utils/messageTypes.ts diff --git a/app/utils/moment.js b/app/utils/moment.ts similarity index 57% rename from app/utils/moment.js rename to app/utils/moment.ts index 064b0f7f..3379429c 100644 --- a/app/utils/moment.js +++ b/app/utils/moment.ts @@ -1,4 +1,4 @@ -const localeKeys = { +const localeKeys: { [key: string]: string } = { en: 'en', ru: 'ru', 'pt-BR': 'pt-br', @@ -13,4 +13,4 @@ const localeKeys = { 'zh-TW': 'zh-tw' }; -export const toMomentLocale = locale => localeKeys[locale]; +export const toMomentLocale = (locale: string): string => localeKeys[locale]; diff --git a/app/utils/navigation/animations.js b/app/utils/navigation/animations.ts similarity index 71% rename from app/utils/navigation/animations.js rename to app/utils/navigation/animations.ts index 9f99764c..a9f18408 100644 --- a/app/utils/navigation/animations.js +++ b/app/utils/navigation/animations.ts @@ -1,12 +1,14 @@ import { Animated, Easing } from 'react-native'; -import { HeaderStyleInterpolators, TransitionPresets } from '@react-navigation/stack'; +import { HeaderStyleInterpolators, TransitionPreset, TransitionPresets } from '@react-navigation/stack'; +// eslint-disable-next-line import/no-unresolved +import { StackCardStyleInterpolator, TransitionSpec } from '@react-navigation/stack/lib/typescript/src/types'; import { isAndroid } from '../deviceInfo'; import conditional from './conditional'; const { multiply } = Animated; -const forFadeFromCenter = ({ current, closing }) => { +const forFadeFromCenter: StackCardStyleInterpolator = ({ current, closing }) => { const opacity = conditional( closing, current.progress, @@ -23,7 +25,7 @@ const forFadeFromCenter = ({ current, closing }) => { }; }; -const FadeIn = { +const FadeIn: TransitionSpec = { animation: 'timing', config: { duration: 250, @@ -31,7 +33,7 @@ const FadeIn = { } }; -const FadeOut = { +const FadeOut: TransitionSpec = { animation: 'timing', config: { duration: 150, @@ -48,7 +50,7 @@ export const FadeFromCenterModal = { cardStyleInterpolator: forFadeFromCenter }; -const forStackAndroid = ({ current, inverted, layouts: { screen } }) => { +const forStackAndroid: StackCardStyleInterpolator = ({ current, inverted, layouts: { screen } }) => { const translateX = multiply( current.progress.interpolate({ inputRange: [0, 1], @@ -70,7 +72,7 @@ const forStackAndroid = ({ current, inverted, layouts: { screen } }) => { }; }; -const StackAndroid = { +const StackAndroid: TransitionPreset = { gestureDirection: 'horizontal', transitionSpec: { open: FadeIn, diff --git a/app/utils/navigation/conditional.js b/app/utils/navigation/conditional.ts similarity index 87% rename from app/utils/navigation/conditional.js rename to app/utils/navigation/conditional.ts index 015c52ae..84c76d83 100644 --- a/app/utils/navigation/conditional.js +++ b/app/utils/navigation/conditional.ts @@ -10,7 +10,11 @@ const { add, multiply } = Animated; * @param main Animated Node to use if the condition is `true` * @param fallback Animated Node to use if the condition is `false` */ -export default function conditional(condition, main, fallback) { +export default function conditional( + condition: Animated.AnimatedInterpolation, + main: Animated.Animated, + fallback: Animated.Animated +): Animated.AnimatedAddition { // To implement this behavior, we multiply the main node with the condition. // So if condition is 0, result will be 0, and if condition is 1, result will be main node. // Then we multiple reverse of the condition (0 if condition is 1) with the fallback. diff --git a/app/utils/openLink.js b/app/utils/openLink.ts similarity index 83% rename from app/utils/openLink.js rename to app/utils/openLink.ts index 92df16a7..4048b3ad 100644 --- a/app/utils/openLink.js +++ b/app/utils/openLink.ts @@ -14,7 +14,7 @@ const scheme = { brave: 'brave:' }; -const appSchemeURL = (url, browser) => { +const appSchemeURL = (url: string, browser: string): string => { let schemeUrl = url; const parsedUrl = parse(url, true); const { protocol } = parsedUrl; @@ -35,7 +35,7 @@ const appSchemeURL = (url, browser) => { return schemeUrl; }; -const openLink = async (url, theme = 'light') => { +const openLink = async (url: string, theme = 'light'): Promise => { try { const browser = await UserPreferences.getStringAsync(DEFAULT_BROWSER_KEY); @@ -43,11 +43,12 @@ const openLink = async (url, theme = 'light') => { await WebBrowser.openBrowserAsync(url, { toolbarColor: themes[theme].headerBackground, controlsColor: themes[theme].headerTintColor, - collapseToolbar: true, + // https://github.com/expo/expo/pull/4923 + enableBarCollapsing: true, showTitle: true }); } else { - const schemeUrl = appSchemeURL(url, browser.replace(':', '')); + const schemeUrl = appSchemeURL(url, browser!.replace(':', '')); await Linking.openURL(schemeUrl); } } catch { diff --git a/app/utils/random.js b/app/utils/random.ts similarity index 80% rename from app/utils/random.js rename to app/utils/random.ts index 8f6adb88..2d2cd178 100644 --- a/app/utils/random.js +++ b/app/utils/random.ts @@ -1,4 +1,4 @@ -export default function random(length) { +export default function random(length: number): string { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < length; i += 1) { diff --git a/app/utils/review.js b/app/utils/review.ts similarity index 96% rename from app/utils/review.js rename to app/utils/review.ts index 15f1cf96..bbbb498b 100644 --- a/app/utils/review.js +++ b/app/utils/review.ts @@ -15,7 +15,7 @@ const reviewDelay = 2000; const numberOfDays = 7; const numberOfPositiveEvent = 5; -const daysBetween = (date1, date2) => { +const daysBetween = (date1: Date, date2: Date): number => { const one_day = 1000 * 60 * 60 * 24; const date1_ms = date1.getTime(); const date2_ms = date2.getTime(); @@ -32,7 +32,7 @@ const onCancelPress = () => { } }; -export const onReviewPress = async () => { +export const onReviewPress = async (): Promise => { logEvent(events.SE_REVIEW_THIS_APP); await onCancelPress(); try { diff --git a/app/utils/room.js b/app/utils/room.js deleted file mode 100644 index 67df65a9..00000000 --- a/app/utils/room.js +++ /dev/null @@ -1,53 +0,0 @@ -import moment from 'moment'; - -import { themes } from '../constants/colors'; -import I18n from '../i18n'; - -export const isBlocked = room => { - if (room) { - const { t, blocked, blocker } = room; - if (t === 'd' && (blocked || blocker)) { - return true; - } - } - return false; -}; - -export const capitalize = s => { - if (typeof s !== 'string') { - return ''; - } - return s.charAt(0).toUpperCase() + s.slice(1); -}; - -export const formatDate = date => - moment(date).calendar(null, { - lastDay: `[${I18n.t('Yesterday')}]`, - sameDay: 'LT', - lastWeek: 'dddd', - sameElse: 'L' - }); - -export const formatDateThreads = date => - moment(date).calendar(null, { - sameDay: 'LT', - lastDay: `[${I18n.t('Yesterday')}] LT`, - lastWeek: 'dddd LT', - sameElse: 'LL' - }); - -export const getBadgeColor = ({ subscription, messageId, theme }) => { - if (subscription?.tunreadUser?.includes(messageId)) { - return themes[theme].mentionMeColor; - } - if (subscription?.tunreadGroup?.includes(messageId)) { - return themes[theme].mentionGroupColor; - } - if (subscription?.tunread?.includes(messageId)) { - return themes[theme].tunreadColor; - } -}; - -export const makeThreadName = messageRecord => messageRecord.msg || messageRecord?.attachments[0]?.title; - -export const isTeamRoom = ({ teamId, joined }) => teamId && joined; diff --git a/app/utils/room.ts b/app/utils/room.ts new file mode 100644 index 00000000..3e4e0ef4 --- /dev/null +++ b/app/utils/room.ts @@ -0,0 +1,65 @@ +import moment from 'moment'; + +import { themes } from '../constants/colors'; +import I18n from '../i18n'; +import { IAttachment } from '../definitions/IAttachment'; +import { ISubscription, SubscriptionType } from '../definitions/ISubscription'; + +export const isBlocked = (room: ISubscription): boolean => { + if (room) { + const { t, blocked, blocker } = room; + if (t === SubscriptionType.DIRECT && (blocked || blocker)) { + return true; + } + } + return false; +}; + +export const capitalize = (s: string): string => { + if (typeof s !== 'string') { + return ''; + } + return s.charAt(0).toUpperCase() + s.slice(1); +}; + +export const formatDate = (date: Date): string => + moment(date).calendar(null, { + lastDay: `[${I18n.t('Yesterday')}]`, + sameDay: 'LT', + lastWeek: 'dddd', + sameElse: 'L' + }); + +export const formatDateThreads = (date: Date): string => + moment(date).calendar(null, { + sameDay: 'LT', + lastDay: `[${I18n.t('Yesterday')}] LT`, + lastWeek: 'dddd LT', + sameElse: 'LL' + }); + +export const getBadgeColor = ({ + subscription, + messageId, + theme +}: { + // TODO: Refactor when migrate model folder + subscription: any; + messageId: string; + theme: string; +}): string | undefined => { + if (subscription?.tunreadUser?.includes(messageId)) { + return themes[theme].mentionMeColor; + } + if (subscription?.tunreadGroup?.includes(messageId)) { + return themes[theme].mentionGroupColor; + } + if (subscription?.tunread?.includes(messageId)) { + return themes[theme].tunreadColor; + } +}; + +export const makeThreadName = (messageRecord: { id?: string; msg?: string; attachments?: IAttachment[] }): string | undefined => + messageRecord.msg || messageRecord.attachments![0].title; + +export const isTeamRoom = ({ teamId, joined }: { teamId: string; joined: boolean }): boolean => !!teamId && joined; diff --git a/app/utils/server.js b/app/utils/server.ts similarity index 84% rename from app/utils/server.js rename to app/utils/server.ts index e7be96b3..52064757 100644 --- a/app/utils/server.js +++ b/app/utils/server.ts @@ -3,7 +3,7 @@ url = 'https://open.rocket.chat/method' hostname = 'open.rocket.chat' */ -export const extractHostname = url => { +export const extractHostname = (url: string): string => { let hostname; if (url.indexOf('//') > -1) { diff --git a/app/utils/shortnameToUnicode/ascii.js b/app/utils/shortnameToUnicode/ascii.ts similarity index 98% rename from app/utils/shortnameToUnicode/ascii.js rename to app/utils/shortnameToUnicode/ascii.ts index 4d9d04cd..7eca5f7d 100644 --- a/app/utils/shortnameToUnicode/ascii.js +++ b/app/utils/shortnameToUnicode/ascii.ts @@ -3,7 +3,7 @@ /* eslint-disable object-curly-spacing */ /* eslint-disable comma-spacing */ /* eslint-disable key-spacing */ -const ascii = { +const ascii: { [key: string]: string } = { '*\\0/*': '🙆', '*\\O/*': '🙆', '-___-': '😑', diff --git a/app/utils/shortnameToUnicode/emojis.js b/app/utils/shortnameToUnicode/emojis.ts similarity index 99% rename from app/utils/shortnameToUnicode/emojis.js rename to app/utils/shortnameToUnicode/emojis.ts index 14cd6133..6a8a63c3 100644 --- a/app/utils/shortnameToUnicode/emojis.js +++ b/app/utils/shortnameToUnicode/emojis.ts @@ -3,7 +3,7 @@ /* eslint-disable object-curly-spacing */ /* eslint-disable comma-spacing */ /* eslint-disable key-spacing */ -const emojis = { +const emojis: { [key: string]: string } = { ':england:': '🏴', ':scotland:': '🏴', ':wales:': '🏴', diff --git a/app/utils/shortnameToUnicode/index.js b/app/utils/shortnameToUnicode/index.ts similarity index 80% rename from app/utils/shortnameToUnicode/index.js rename to app/utils/shortnameToUnicode/index.ts index 0a54aa3a..b533da8f 100644 --- a/app/utils/shortnameToUnicode/index.js +++ b/app/utils/shortnameToUnicode/index.ts @@ -2,11 +2,11 @@ import emojis from './emojis'; import ascii, { asciiRegexp } from './ascii'; const shortnamePattern = new RegExp(/:[-+_a-z0-9]+:/, 'gi'); -const replaceShortNameWithUnicode = shortname => emojis[shortname] || shortname; +const replaceShortNameWithUnicode = (shortname: string) => emojis[shortname] || shortname; const regAscii = new RegExp(`((\\s|^)${asciiRegexp}(?=\\s|$|[!,.?]))`, 'gi'); -const unescapeHTML = string => { - const unescaped = { +const unescapeHTML = (string: string) => { + const unescaped: { [key: string]: string } = { '&': '&', '&': '&', '&': '&', @@ -27,7 +27,7 @@ const unescapeHTML = string => { return string.replace(/&(?:amp|#38|#x26|lt|#60|#x3C|gt|#62|#x3E|apos|#39|#x27|quot|#34|#x22);/gi, match => unescaped[match]); }; -const shortnameToUnicode = str => { +const shortnameToUnicode = (str: string): string => { str = str.replace(shortnamePattern, replaceShortNameWithUnicode); str = str.replace(regAscii, (entire, m1, m2, m3) => { diff --git a/app/utils/sslPinning.js b/app/utils/sslPinning.ts similarity index 54% rename from app/utils/sslPinning.js rename to app/utils/sslPinning.ts index 50f944e6..42245c98 100644 --- a/app/utils/sslPinning.js +++ b/app/utils/sslPinning.ts @@ -7,6 +7,26 @@ import I18n from '../i18n'; import { extractHostname } from './server'; const { SSLPinning } = NativeModules; +const { documentDirectory } = FileSystem; + +const extractFileScheme = (path: string) => path.replace('file://', ''); // file:// isn't allowed by obj-C + +const getPath = (name: string) => `${documentDirectory}/${name}`; + +interface ICertificate { + path: string; + password: string; +} + +const persistCertificate = async (name: string, password: string) => { + const certificatePath = getPath(name); + const certificate: ICertificate = { + path: extractFileScheme(certificatePath), + password + }; + await UserPreferences.setMapAsync(name, certificate); + return certificate; +}; const RCSSLPinning = Platform.select({ ios: { @@ -14,6 +34,7 @@ const RCSSLPinning = Platform.select({ new Promise(async (resolve, reject) => { try { const res = await DocumentPicker.pick({ + // @ts-ignore type: ['com.rsa.pkcs-12'] }); const { uri, name } = res; @@ -25,17 +46,9 @@ const RCSSLPinning = Platform.select({ text: 'OK', onPress: async password => { try { - const certificatePath = `${FileSystem.documentDirectory}/${name}`; - + const certificatePath = getPath(name); await FileSystem.copyAsync({ from: uri, to: certificatePath }); - - const certificate = { - path: certificatePath.replace('file://', ''), // file:// isn't allowed by obj-C - password - }; - - await UserPreferences.setMapAsync(name, certificate); - + await persistCertificate(name, password!); resolve(name); } catch (e) { reject(e); @@ -49,16 +62,19 @@ const RCSSLPinning = Platform.select({ reject(e); } }), - setCertificate: async (alias, server) => { - if (alias) { - const certificate = await UserPreferences.getMapAsync(alias); + setCertificate: async (name: string, server: string) => { + if (name) { + let certificate = (await UserPreferences.getMapAsync(name)) as ICertificate; + if (!certificate.path.match(extractFileScheme(documentDirectory!))) { + certificate = await persistCertificate(name, certificate.password); + } await UserPreferences.setMapAsync(extractHostname(server), certificate); } } }, android: { pickCertificate: () => SSLPinning?.pickCertificate(), - setCertificate: alias => SSLPinning?.setCertificate(alias) + setCertificate: name => SSLPinning?.setCertificate(name) } }); diff --git a/app/utils/theme.js b/app/utils/theme.ts similarity index 71% rename from app/utils/theme.js rename to app/utils/theme.ts index c9038941..0e9d8e05 100644 --- a/app/utils/theme.js +++ b/app/utils/theme.ts @@ -2,12 +2,13 @@ import { Appearance } from 'react-native-appearance'; import changeNavigationBarColor from 'react-native-navigation-bar-color'; import setRootViewColor from 'rn-root-view'; +import { IThemePreference, TThemeMode } from '../definitions/ITheme'; import { themes } from '../constants/colors'; import { isAndroid } from './deviceInfo'; -let themeListener; +let themeListener: { remove: () => void } | null; -export const defaultTheme = () => { +export const defaultTheme = (): TThemeMode => { const systemTheme = Appearance.getColorScheme(); if (systemTheme && systemTheme !== 'no-preference') { return systemTheme; @@ -15,7 +16,7 @@ export const defaultTheme = () => { return 'light'; }; -export const getTheme = themePreferences => { +export const getTheme = (themePreferences: IThemePreference): string => { const { darkLevel, currentTheme } = themePreferences; let theme = currentTheme; if (currentTheme === 'automatic') { @@ -24,7 +25,7 @@ export const getTheme = themePreferences => { return theme === 'dark' ? darkLevel : 'light'; }; -export const newThemeState = (prevState, newTheme) => { +export const newThemeState = (prevState: { themePreferences: IThemePreference }, newTheme: IThemePreference) => { // new theme preferences const themePreferences = { ...prevState.themePreferences, @@ -35,12 +36,13 @@ export const newThemeState = (prevState, newTheme) => { return { themePreferences, theme: getTheme(themePreferences) }; }; -export const setNativeTheme = async themePreferences => { +export const setNativeTheme = async (themePreferences: IThemePreference): Promise => { const theme = getTheme(themePreferences); if (isAndroid) { const iconsLight = theme === 'light'; try { - await changeNavigationBarColor(themes[theme].navbarBackground, iconsLight); + // The late param as default is true @ react-native-navigation-bar-color/src/index.js line 8 + await changeNavigationBarColor(themes[theme].navbarBackground, iconsLight, true); } catch (error) { // Do nothing } @@ -55,7 +57,7 @@ export const unsubscribeTheme = () => { } }; -export const subscribeTheme = (themePreferences, setTheme) => { +export const subscribeTheme = (themePreferences: IThemePreference, setTheme: () => void): void => { const { currentTheme } = themePreferences; if (!themeListener && currentTheme === 'automatic') { // not use listener params because we use getTheme diff --git a/app/utils/throttle.js b/app/utils/throttle.js deleted file mode 100644 index 88751335..00000000 --- a/app/utils/throttle.js +++ /dev/null @@ -1,26 +0,0 @@ -export default function throttle(fn, threshhold = 250, scope) { - let last; - let deferTimer; - - const _throttle = (...args) => { - const context = scope || this; - - const now = +new Date(); - - if (last && now < last + threshhold) { - // hold on to it - clearTimeout(deferTimer); - deferTimer = setTimeout(() => { - last = now; - fn.apply(context, args); - }, threshhold); - } else { - last = now; - fn.apply(context, args); - } - }; - - _throttle.stop = () => clearTimeout(deferTimer); - - return _throttle; -} diff --git a/app/utils/touch.js b/app/utils/touch.tsx similarity index 55% rename from app/utils/touch.js rename to app/utils/touch.tsx index 0bfece04..3573c87c 100644 --- a/app/utils/touch.js +++ b/app/utils/touch.tsx @@ -1,19 +1,27 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { RectButton } from 'react-native-gesture-handler'; +import { RectButton, RectButtonProps } from 'react-native-gesture-handler'; import { themes } from '../constants/colors'; -class Touch extends React.Component { - setNativeProps(props) { +interface ITouchProps extends RectButtonProps { + children: React.ReactNode; + theme: string; + accessibilityLabel?: string; + testID?: string; +} + +class Touch extends React.Component { + private ref: any; + + setNativeProps(props: ITouchProps): void { this.ref.setNativeProps(props); } - getRef = ref => { + getRef = (ref: RectButton): void => { this.ref = ref; }; - render() { + render(): JSX.Element { const { children, onPress, theme, underlayColor, ...props } = this.props; return ( @@ -30,11 +38,4 @@ class Touch extends React.Component { } } -Touch.propTypes = { - children: PropTypes.node, - onPress: PropTypes.func, - theme: PropTypes.string, - underlayColor: PropTypes.string -}; - export default Touch; diff --git a/app/utils/twoFactor.js b/app/utils/twoFactor.ts similarity index 68% rename from app/utils/twoFactor.js rename to app/utils/twoFactor.ts index 6f2fa9c9..a52ff93f 100644 --- a/app/utils/twoFactor.js +++ b/app/utils/twoFactor.ts @@ -3,13 +3,18 @@ import { settings } from '@rocket.chat/sdk'; import { TWO_FACTOR } from '../containers/TwoFactor'; import EventEmitter from './events'; -export const twoFactor = ({ method, invalid }) => +interface ITwoFactor { + method: string; + invalid: boolean; +} + +export const twoFactor = ({ method, invalid }: ITwoFactor): Promise<{ twoFactorCode: string; twoFactorMethod: string }> => new Promise((resolve, reject) => { EventEmitter.emit(TWO_FACTOR, { method, invalid, cancel: () => reject(), - submit: code => { + submit: (code: string) => { settings.customHeaders = { ...settings.customHeaders, 'x-2fa-code': code, diff --git a/app/utils/url.js b/app/utils/url.ts similarity index 77% rename from app/utils/url.js rename to app/utils/url.ts index 623524d7..50179597 100644 --- a/app/utils/url.js +++ b/app/utils/url.ts @@ -1,4 +1,4 @@ -export const isValidURL = url => { +export const isValidURL = (url: string): boolean => { const pattern = new RegExp( '^(https?:\\/\\/)?' + // protocol '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name @@ -12,4 +12,4 @@ export const isValidURL = url => { }; // Use useSsl: false only if server url starts with http:// -export const useSsl = url => !/http:\/\//.test(url); +export const useSsl = (url: string): boolean => !/http:\/\//.test(url); diff --git a/app/views/AddChannelTeamView.tsx b/app/views/AddChannelTeamView.tsx index d477f9ba..8a72d3c9 100644 --- a/app/views/AddChannelTeamView.tsx +++ b/app/views/AddChannelTeamView.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { RouteProp } from '@react-navigation/native'; import { connect } from 'react-redux'; +import { CompositeNavigationProp } from '@react-navigation/core'; import * as List from '../containers/List'; import StatusBar from '../containers/StatusBar'; @@ -9,16 +10,24 @@ import { useTheme } from '../theme'; import * as HeaderButton from '../containers/HeaderButton'; import SafeAreaView from '../containers/SafeAreaView'; import I18n from '../i18n'; - -type TNavigation = StackNavigationProp; +import { ChatsStackParamList, DrawerParamList, NewMessageStackParamList } from '../stacks/types'; interface IAddChannelTeamView { - route: RouteProp<{ AddChannelTeamView: { teamId: string; teamChannels: object[] } }, 'AddChannelTeamView'>; - navigation: TNavigation; + navigation: CompositeNavigationProp< + StackNavigationProp, + CompositeNavigationProp, StackNavigationProp> + >; + route: RouteProp; isMasterDetail: boolean; } -const setHeader = (navigation: TNavigation, isMasterDetail: boolean) => { +const setHeader = ({ + navigation, + isMasterDetail +}: { + navigation: StackNavigationProp; + isMasterDetail: boolean; +}) => { const options: StackNavigationOptions = { headerTitle: I18n.t('Add_Channel_to_Team') }; @@ -35,7 +44,7 @@ const AddChannelTeamView = ({ navigation, route, isMasterDetail }: IAddChannelTe const { theme } = useTheme(); useEffect(() => { - setHeader(navigation, isMasterDetail); + setHeader({ navigation, isMasterDetail }); }, []); return ( diff --git a/app/views/AddExistingChannelView.tsx b/app/views/AddExistingChannelView.tsx index 5efdbf34..86ab9b9c 100644 --- a/app/views/AddExistingChannelView.tsx +++ b/app/views/AddExistingChannelView.tsx @@ -21,6 +21,7 @@ import { animateNextTransition } from '../utils/layoutAnimation'; import { goRoom } from '../utils/goRoom'; import { showErrorAlert } from '../utils/info'; import debounce from '../utils/debounce'; +import { ChatsStackParamList } from '../stacks/types'; interface IAddExistingChannelViewState { // TODO: refactor with Room Model @@ -31,8 +32,8 @@ interface IAddExistingChannelViewState { } interface IAddExistingChannelViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ AddExistingChannelView: { teamId: string } }, 'AddExistingChannelView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; isMasterDetail: boolean; addTeamChannelPermission: string[]; @@ -41,7 +42,7 @@ interface IAddExistingChannelViewProps { const QUERY_SIZE = 50; class AddExistingChannelView extends React.Component { - private teamId: string; + private teamId?: string; constructor(props: IAddExistingChannelViewProps) { super(props); this.query(); diff --git a/app/views/AdminPanelView/index.tsx b/app/views/AdminPanelView/index.tsx index 80f728e1..f0af5dfa 100644 --- a/app/views/AdminPanelView/index.tsx +++ b/app/views/AdminPanelView/index.tsx @@ -9,6 +9,7 @@ import * as HeaderButton from '../../containers/HeaderButton'; import { withTheme } from '../../theme'; import { getUserSelector } from '../../selectors/login'; import SafeAreaView from '../../containers/SafeAreaView'; +import { AdminPanelStackParamList } from '../../stacks/types'; interface IAdminPanelViewProps { baseUrl: string; @@ -16,7 +17,7 @@ interface IAdminPanelViewProps { } interface INavigationOptions { - navigation: DrawerScreenProps; + navigation: DrawerScreenProps; isMasterDetail: boolean; } diff --git a/app/views/AttachmentView.tsx b/app/views/AttachmentView.tsx index 90adf8b4..d0bd021c 100644 --- a/app/views/AttachmentView.tsx +++ b/app/views/AttachmentView.tsx @@ -24,6 +24,8 @@ import { getUserSelector } from '../selectors/login'; import { withDimensions } from '../dimensions'; import { getHeaderHeight } from '../containers/Header'; import StatusBar from '../containers/StatusBar'; +import { InsideStackParamList } from '../stacks/types'; +import { IAttachment } from '../definitions/IAttachment'; const styles = StyleSheet.create({ container: { @@ -31,24 +33,14 @@ const styles = StyleSheet.create({ } }); -// TODO: refactor when react-navigation is done -export interface IAttachment { - title: string; - title_link?: string; - image_url?: string; - image_type?: string; - video_url?: string; - video_type?: string; -} - interface IAttachmentViewState { attachment: IAttachment; loading: boolean; } interface IAttachmentViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ AttachmentView: { attachment: IAttachment } }, 'AttachmentView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; baseUrl: string; width: number; @@ -131,7 +123,11 @@ class AttachmentView extends React.Component; - route: IRoute; + navigation: StackNavigationProp; + route: RouteProp; } interface IAuthenticationWebView extends INavigationOption { diff --git a/app/views/AutoTranslateView/index.tsx b/app/views/AutoTranslateView/index.tsx index 92a77543..95442689 100644 --- a/app/views/AutoTranslateView/index.tsx +++ b/app/views/AutoTranslateView/index.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { FlatList, StyleSheet, Switch } from 'react-native'; +import { RouteProp } from '@react-navigation/core'; +import { ChatsStackParamList } from '../../stacks/types'; import RocketChat from '../../lib/rocketchat'; import I18n from '../../i18n'; import StatusBar from '../../containers/StatusBar'; @@ -9,6 +11,7 @@ import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; import { withTheme } from '../../theme'; import SafeAreaView from '../../containers/SafeAreaView'; import { events, logEvent } from '../../utils/log'; +import { ISubscription } from '../../definitions/ISubscription'; const styles = StyleSheet.create({ list: { @@ -16,19 +19,8 @@ const styles = StyleSheet.create({ } }); -interface IRoom { - observe: Function; - autoTranslateLanguage: boolean; - autoTranslate: boolean; -} - interface IAutoTranslateViewProps { - route: { - params: { - rid?: string; - room?: IRoom; - }; - }; + route: RouteProp; theme: string; } @@ -50,7 +42,7 @@ class AutoTranslateView extends React.Component { if (room && room.observe) { this.roomObservable = room.observe(); - this.subscription = this.roomObservable.subscribe((changes: IRoom) => { + this.subscription = this.roomObservable.subscribe((changes: ISubscription) => { if (this.mounted) { const { selectedLanguage, enableAutoTranslate } = this.state; if (selectedLanguage !== changes.autoTranslateLanguage) { diff --git a/app/views/CreateChannelView.tsx b/app/views/CreateChannelView.tsx index 45b2cc2f..e8d719ab 100644 --- a/app/views/CreateChannelView.tsx +++ b/app/views/CreateChannelView.tsx @@ -25,6 +25,7 @@ import { events, logEvent } from '../utils/log'; import SafeAreaView from '../containers/SafeAreaView'; import RocketChat from '../lib/rocketchat'; import sharedStyles from './Styles'; +import { ChatsStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -91,8 +92,8 @@ interface ICreateChannelViewState { } interface ICreateChannelViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ CreateChannelView: { isTeam: boolean; teamId: string } }, 'CreateChannelView'>; + navigation: StackNavigationProp; + route: RouteProp; baseUrl: string; create: (data: ICreateFunction) => void; removeUser: (user: IOtherUser) => void; @@ -118,7 +119,7 @@ interface ISwitch extends SwitchProps { } class CreateChannelView extends React.Component { - private teamId: string; + private teamId?: string; constructor(props: ICreateChannelViewProps) { super(props); @@ -240,7 +241,7 @@ class CreateChannelView extends React.Component - // TODO: remove this ts-ignore when migrate the file: app/utils/avatar.js - // @ts-ignore avatarURL({ text: RocketChat.getRoomAvatar(item), type: item.t, diff --git a/app/views/CreateDiscussionView/SelectUsers.tsx b/app/views/CreateDiscussionView/SelectUsers.tsx index 65a4e0a4..d63c5ae6 100644 --- a/app/views/CreateDiscussionView/SelectUsers.tsx +++ b/app/views/CreateDiscussionView/SelectUsers.tsx @@ -12,6 +12,7 @@ import { MultiSelect } from '../../containers/UIKit/MultiSelect'; import { themes } from '../../constants/colors'; import styles from './styles'; import { ICreateDiscussionViewSelectUsers } from './interfaces'; +import { SubscriptionType } from '../../definitions/ISubscription'; interface IUser { name: string; @@ -62,11 +63,9 @@ const SelectUsers = ({ }, 300); const getAvatar = (item: any) => - // TODO: remove this ts-ignore when migrate the file: app/utils/avatar.js - // @ts-ignore avatarURL({ text: RocketChat.getRoomAvatar(item), - type: 'd', + type: SubscriptionType.DIRECT, user: { id: userId, token }, server, avatarETag: item.avatarETag, diff --git a/app/views/CreateDiscussionView/index.tsx b/app/views/CreateDiscussionView/index.tsx index 2933b2c7..53d741d2 100644 --- a/app/views/CreateDiscussionView/index.tsx +++ b/app/views/CreateDiscussionView/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { ScrollView, Switch, Text } from 'react-native'; +import { StackNavigationOptions } from '@react-navigation/stack'; import Loading from '../../containers/Loading'; import KeyboardView from '../../presentation/KeyboardView'; @@ -89,7 +90,7 @@ class CreateChannelView extends React.Component { ) : null, headerLeft: showCloseModal ? () => : undefined - }); + } as StackNavigationOptions); }; submit = () => { diff --git a/app/views/CreateDiscussionView/interfaces.ts b/app/views/CreateDiscussionView/interfaces.ts index 46883311..6009881c 100644 --- a/app/views/CreateDiscussionView/interfaces.ts +++ b/app/views/CreateDiscussionView/interfaces.ts @@ -1,14 +1,12 @@ +import { RouteProp } from '@react-navigation/core'; +import { StackNavigationProp } from '@react-navigation/stack'; + +import { NewMessageStackParamList } from '../../stacks/types'; +import { SubscriptionType } from '../../definitions/ISubscription'; + export interface ICreateChannelViewProps { - navigation: any; - route: { - params?: { - channel: string; - message: { - msg: string; - }; - showCloseModal: boolean; - }; - }; + navigation: StackNavigationProp; + route: RouteProp; server: string; user: { id: string; @@ -18,7 +16,7 @@ export interface ICreateChannelViewProps { loading: boolean; result: { rid: string; - t: string; + t: SubscriptionType; prid: string; }; failure: boolean; diff --git a/app/views/DefaultBrowserView.tsx b/app/views/DefaultBrowserView.tsx index 0282e0df..cfe977d2 100644 --- a/app/views/DefaultBrowserView.tsx +++ b/app/views/DefaultBrowserView.tsx @@ -107,7 +107,7 @@ class DefaultBrowserView extends React.Component { logEvent(events.DB_CHANGE_DEFAULT_BROWSER, { browser: newBrowser }); try { - const browser = newBrowser !== 'systemDefault:' ? newBrowser : null; + const browser = newBrowser || 'systemDefault:'; await UserPreferences.setStringAsync(DEFAULT_BROWSER_KEY, browser); this.setState({ browser }); } catch { diff --git a/app/views/DirectoryView/Options.tsx b/app/views/DirectoryView/Options.tsx index fcc0f7bf..11206806 100644 --- a/app/views/DirectoryView/Options.tsx +++ b/app/views/DirectoryView/Options.tsx @@ -63,7 +63,11 @@ export default class DirectoryOptions extends PureComponent changeType(itemType)} style={styles.dropdownItemButton} theme={theme}> + changeType(itemType)} + style={styles.dropdownItemButton} + theme={theme} + accessibilityLabel={I18n.t(text)}> {I18n.t(text)} @@ -90,7 +94,7 @@ export default class DirectoryOptions extends PureComponent - + ; baseUrl: string; isFederationEnabled: boolean; user: { diff --git a/app/views/DisplayPrefsView.js b/app/views/DisplayPrefsView.tsx similarity index 76% rename from app/views/DisplayPrefsView.js rename to app/views/DisplayPrefsView.tsx index 09da4edc..959682c4 100644 --- a/app/views/DisplayPrefsView.js +++ b/app/views/DisplayPrefsView.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; import { Switch } from 'react-native'; import { RadioButton } from 'react-native-ui-lib'; +import { StackNavigationProp } from '@react-navigation/stack'; import { useDispatch, useSelector } from 'react-redux'; import { setPreference } from '../actions/sortPreferences'; @@ -15,13 +15,30 @@ import * as HeaderButton from '../containers/HeaderButton'; import SafeAreaView from '../containers/SafeAreaView'; import { ICON_SIZE } from '../containers/List/constants'; import log, { events, logEvent } from '../utils/log'; -import { DISPLAY_MODE_CONDENSED, DISPLAY_MODE_EXPANDED } from '../constants/constantDisplayMode'; +import { DisplayMode, SortBy } from '../constants/constantDisplayMode'; +import { SettingsStackParamList } from '../stacks/types'; -const DisplayPrefsView = props => { +interface IParam { + sortBy: SortBy; + groupByType: boolean; + showFavorites: boolean; + showUnread: boolean; + showAvatar: boolean; + displayMode: DisplayMode; +} + +interface IDisplayPrefsView { + navigation: StackNavigationProp; + isMasterDetail: boolean; +} + +const DisplayPrefsView = (props: IDisplayPrefsView): JSX.Element => { const { theme } = useTheme(); - const { sortBy, groupByType, showFavorites, showUnread, showAvatar, displayMode } = useSelector(state => state.sortPreferences); - const { isMasterDetail } = useSelector(state => state.app); + const { sortBy, groupByType, showFavorites, showUnread, showAvatar, displayMode } = useSelector( + (state: any) => state.sortPreferences + ); + const { isMasterDetail } = useSelector((state: any) => state.app); const dispatch = useDispatch(); useEffect(() => { @@ -36,7 +53,7 @@ const DisplayPrefsView = props => { } }, []); - const setSortPreference = async param => { + const setSortPreference = async (param: Partial) => { try { dispatch(setPreference(param)); await RocketChat.saveSortPreference(param); @@ -47,12 +64,12 @@ const DisplayPrefsView = props => { const sortByName = async () => { logEvent(events.DP_SORT_CHANNELS_BY_NAME); - await setSortPreference({ sortBy: 'alphabetical' }); + await setSortPreference({ sortBy: SortBy.Alphabetical }); }; const sortByActivity = async () => { logEvent(events.DP_SORT_CHANNELS_BY_ACTIVITY); - await setSortPreference({ sortBy: 'activity' }); + await setSortPreference({ sortBy: SortBy.Activity }); }; const toggleGroupByType = async () => { @@ -77,23 +94,23 @@ const DisplayPrefsView = props => { const displayExpanded = async () => { logEvent(events.DP_DISPLAY_EXPANDED); - await setSortPreference({ displayMode: DISPLAY_MODE_EXPANDED }); + await setSortPreference({ displayMode: DisplayMode.Expanded }); }; const displayCondensed = async () => { logEvent(events.DP_DISPLAY_CONDENSED); - await setSortPreference({ displayMode: DISPLAY_MODE_CONDENSED }); + await setSortPreference({ displayMode: DisplayMode.Condensed }); }; - const renderCheckBox = value => ( + const renderCheckBox = (value: boolean) => ( ); - const renderAvatarSwitch = value => ( + const renderAvatarSwitch = (value: boolean) => ( toggleAvatar()} testID='display-pref-view-avatar-switch' /> ); - const renderRadio = value => ( + const renderRadio = (value: boolean) => ( { left={() => } title='Expanded' testID='display-pref-view-expanded' - right={() => renderRadio(displayMode === DISPLAY_MODE_EXPANDED)} + right={() => renderRadio(displayMode === DisplayMode.Expanded)} onPress={displayExpanded} /> @@ -119,7 +136,7 @@ const DisplayPrefsView = props => { left={() => } title='Condensed' testID='display-pref-view-condensed' - right={() => renderRadio(displayMode === DISPLAY_MODE_CONDENSED)} + right={() => renderRadio(displayMode === DisplayMode.Condensed)} onPress={displayCondensed} /> @@ -139,7 +156,7 @@ const DisplayPrefsView = props => { testID='display-pref-view-activity' left={() => } onPress={sortByActivity} - right={() => renderRadio(sortBy === 'activity')} + right={() => renderRadio(sortBy === SortBy.Activity)} /> { testID='display-pref-view-name' left={() => } onPress={sortByName} - right={() => renderRadio(sortBy === 'alphabetical')} + right={() => renderRadio(sortBy === SortBy.Alphabetical)} /> @@ -184,9 +201,6 @@ const DisplayPrefsView = props => { ); }; -DisplayPrefsView.propTypes = { - navigation: PropTypes.object, - isMasterDetail: PropTypes.bool -}; +DisplayPrefsView.propTypes = {}; export default DisplayPrefsView; diff --git a/app/views/E2EEncryptionSecurityView.tsx b/app/views/E2EEncryptionSecurityView.tsx index d5b0b27a..34759043 100644 --- a/app/views/E2EEncryptionSecurityView.tsx +++ b/app/views/E2EEncryptionSecurityView.tsx @@ -75,8 +75,6 @@ class E2EEncryptionSecurityView extends React.Component { - // TODO: Remove ts-ignore when migrate the showConfirmationAlert - // @ts-ignore showConfirmationAlert({ title: I18n.t('Are_you_sure_question_mark'), message: I18n.t('E2E_encryption_reset_message'), diff --git a/app/views/E2EEnterYourPasswordView.tsx b/app/views/E2EEnterYourPasswordView.tsx index dd9cdfa8..6d63f90d 100644 --- a/app/views/E2EEnterYourPasswordView.tsx +++ b/app/views/E2EEnterYourPasswordView.tsx @@ -17,6 +17,7 @@ import KeyboardView from '../presentation/KeyboardView'; import StatusBar from '../containers/StatusBar'; import { events, logEvent } from '../utils/log'; import sharedStyles from './Styles'; +import { E2EEnterYourPasswordStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -36,7 +37,7 @@ interface IE2EEnterYourPasswordViewState { interface IE2EEnterYourPasswordViewProps { encryptionDecodeKey: (password: string) => void; theme: string; - navigation: StackNavigationProp; + navigation: StackNavigationProp; } class E2EEnterYourPasswordView extends React.Component { diff --git a/app/views/E2EHowItWorksView.tsx b/app/views/E2EHowItWorksView.tsx index 0fbdf77a..fce1a2d0 100644 --- a/app/views/E2EHowItWorksView.tsx +++ b/app/views/E2EHowItWorksView.tsx @@ -9,6 +9,7 @@ import * as HeaderButton from '../containers/HeaderButton'; import Markdown from '../containers/markdown'; import { withTheme } from '../theme'; import I18n from '../i18n'; +import { E2ESaveYourPasswordStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -23,8 +24,8 @@ const styles = StyleSheet.create({ }); interface INavigation { - navigation: StackNavigationProp; - route: RouteProp<{ E2EHowItWorksView: { showCloseModal: boolean } }, 'E2EHowItWorksView'>; + navigation: StackNavigationProp; + route: RouteProp; } interface IE2EHowItWorksViewProps extends INavigation { diff --git a/app/views/E2ESaveYourPasswordView.tsx b/app/views/E2ESaveYourPasswordView.tsx index 1c4e13a5..3d9a32ee 100644 --- a/app/views/E2ESaveYourPasswordView.tsx +++ b/app/views/E2ESaveYourPasswordView.tsx @@ -19,6 +19,7 @@ import Button from '../containers/Button'; import { withTheme } from '../theme'; import I18n from '../i18n'; import sharedStyles from './Styles'; +import { E2ESaveYourPasswordStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -60,7 +61,7 @@ interface IE2ESaveYourPasswordViewState { interface IE2ESaveYourPasswordViewProps { server: string; - navigation: StackNavigationProp; + navigation: StackNavigationProp; encryptionSetBanner(): void; theme: string; } diff --git a/app/views/ForgotPasswordView.tsx b/app/views/ForgotPasswordView.tsx index c08a1acd..375d089d 100644 --- a/app/views/ForgotPasswordView.tsx +++ b/app/views/ForgotPasswordView.tsx @@ -14,6 +14,7 @@ import { themes } from '../constants/colors'; import FormContainer, { FormContainerInner } from '../containers/FormContainer'; import { events, logEvent } from '../utils/log'; import sharedStyles from './Styles'; +import { OutsideParamList } from '../stacks/types'; interface IForgotPasswordViewState { email: string; @@ -22,8 +23,8 @@ interface IForgotPasswordViewState { } interface IForgotPasswordViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ ForgotPasswordView: { title: string } }, 'ForgotPasswordView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; } diff --git a/app/views/ForwardLivechatView.tsx b/app/views/ForwardLivechatView.tsx index 42a29782..ea17466d 100644 --- a/app/views/ForwardLivechatView.tsx +++ b/app/views/ForwardLivechatView.tsx @@ -14,6 +14,7 @@ import OrSeparator from '../containers/OrSeparator'; import Input from '../containers/UIKit/MultiSelect/Input'; import { forwardRoom as forwardRoomAction } from '../actions/room'; import { ILivechatDepartment } from './definition/ILivechatDepartment'; +import { ChatsStackParamList } from '../stacks/types'; const styles = StyleSheet.create({ container: { @@ -47,8 +48,8 @@ interface IParsedData { } interface IForwardLivechatViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ ForwardLivechatView: { rid: string } }, 'ForwardLivechatView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; forwardRoom: (rid: string, transferData: ITransferData) => void; } diff --git a/app/views/InviteUsersEditView/index.tsx b/app/views/InviteUsersEditView/index.tsx index 62ce5121..4ae1a67d 100644 --- a/app/views/InviteUsersEditView/index.tsx +++ b/app/views/InviteUsersEditView/index.tsx @@ -19,6 +19,7 @@ import { withTheme } from '../../theme'; import SafeAreaView from '../../containers/SafeAreaView'; import { events, logEvent } from '../../utils/log'; import styles from './styles'; +import { ChatsStackParamList } from '../../stacks/types'; const OPTIONS = { days: [ @@ -67,9 +68,9 @@ const OPTIONS = { ] }; -interface IInviteUsersEditView { - navigation: StackNavigationProp; - route: RouteProp<{ InviteUsersEditView: { rid: string } }, 'InviteUsersEditView'>; +interface IInviteUsersEditViewProps { + navigation: StackNavigationProp; + route: RouteProp; theme: string; createInviteLink(rid: string): void; inviteLinksSetParams(params: { [key: string]: number }): void; @@ -77,14 +78,14 @@ interface IInviteUsersEditView { maxUses: number; } -class InviteUsersView extends React.Component { +class InviteUsersEditView extends React.Component { static navigationOptions = (): StackNavigationOptions => ({ title: I18n.t('Invite_users') }); private rid: string; - constructor(props: IInviteUsersEditView) { + constructor(props: IInviteUsersEditViewProps) { super(props); this.rid = props.route.params?.rid; } @@ -160,4 +161,4 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ createInviteLink: (rid: string) => dispatch(inviteLinksCreateAction(rid)) }); -export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersView)); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersEditView)); diff --git a/app/views/InviteUsersView/index.tsx b/app/views/InviteUsersView/index.tsx index cfcd3fa1..b7bf3071 100644 --- a/app/views/InviteUsersView/index.tsx +++ b/app/views/InviteUsersView/index.tsx @@ -6,6 +6,7 @@ import { StackNavigationProp, StackNavigationOptions } from '@react-navigation/s import { RouteProp } from '@react-navigation/core'; import { Dispatch } from 'redux'; +import { ChatsStackParamList } from '../../stacks/types'; import { inviteLinksClear as inviteLinksClearAction, inviteLinksCreate as inviteLinksCreateAction @@ -22,9 +23,9 @@ import SafeAreaView from '../../containers/SafeAreaView'; import { events, logEvent } from '../../utils/log'; import styles from './styles'; -interface IInviteUsersView { - navigation: StackNavigationProp; - route: RouteProp; +interface IInviteUsersViewProps { + navigation: StackNavigationProp; + route: RouteProp; theme: string; timeDateFormat: string; invite: { @@ -36,14 +37,14 @@ interface IInviteUsersView { createInviteLink(rid: string): void; clearInviteLink(): void; } -class InviteUsersView extends React.Component { +class InviteUsersView extends React.Component { private rid: string; static navigationOptions: StackNavigationOptions = { title: I18n.t('Invite_users') }; - constructor(props: IInviteUsersView) { + constructor(props: IInviteUsersViewProps) { super(props); this.rid = props.route.params?.rid; } diff --git a/app/views/JitsiMeetView.tsx b/app/views/JitsiMeetView.tsx index 44034cda..aa6658d2 100644 --- a/app/views/JitsiMeetView.tsx +++ b/app/views/JitsiMeetView.tsx @@ -12,6 +12,7 @@ import ActivityIndicator from '../containers/ActivityIndicator'; import { events, logEvent } from '../utils/log'; import { isAndroid, isIOS } from '../utils/deviceInfo'; import { withTheme } from '../theme'; +import { InsideStackParamList } from '../stacks/types'; const formatUrl = (url: string, baseUrl: string, uriSize: number, avatarAuthURLFragment: string) => `${baseUrl}/avatar/${url}?format=png&width=${uriSize}&height=${uriSize}${avatarAuthURLFragment}`; @@ -25,8 +26,8 @@ interface IJitsiMeetViewState { } interface IJitsiMeetViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ JitsiMeetView: { rid: string; url: string; onlyAudio?: boolean } }, 'JitsiMeetView'>; + navigation: StackNavigationProp; + route: RouteProp; baseUrl: string; theme: string; user: { diff --git a/app/views/LoginView.tsx b/app/views/LoginView.tsx index 4643687e..e43505f3 100644 --- a/app/views/LoginView.tsx +++ b/app/views/LoginView.tsx @@ -15,6 +15,7 @@ import TextInput from '../containers/TextInput'; import { loginRequest as loginRequestAction } from '../actions/login'; import LoginServices from '../containers/LoginServices'; import sharedStyles from './Styles'; +import { OutsideParamList } from '../stacks/types'; const styles = StyleSheet.create({ registerDisabled: { @@ -47,9 +48,9 @@ const styles = StyleSheet.create({ } }); -interface IProps { - navigation: StackNavigationProp; - route: RouteProp; +interface ILoginViewProps { + navigation: StackNavigationProp; + route: RouteProp; Site_Name: string; Accounts_RegistrationForm: string; Accounts_RegistrationForm_LinkReplacementText: string; @@ -67,15 +68,15 @@ interface IProps { inviteLinkToken: string; } -class LoginView extends React.Component { +class LoginView extends React.Component { private passwordInput: any; - static navigationOptions = ({ route, navigation }: Partial) => ({ + static navigationOptions = ({ route, navigation }: ILoginViewProps) => ({ title: route?.params?.title ?? 'Rocket.Chat', headerRight: () => }); - constructor(props: IProps) { + constructor(props: ILoginViewProps) { super(props); this.state = { user: props.route.params?.username ?? '', @@ -83,7 +84,7 @@ class LoginView extends React.Component { }; } - UNSAFE_componentWillReceiveProps(nextProps: IProps) { + UNSAFE_componentWillReceiveProps(nextProps: ILoginViewProps) { const { error } = this.props; if (nextProps.failure && !dequal(error, nextProps.error)) { if (nextProps.error?.error === 'error-invalid-email') { diff --git a/app/views/MarkdownTableView.tsx b/app/views/MarkdownTableView.tsx index a65994ee..e260199e 100644 --- a/app/views/MarkdownTableView.tsx +++ b/app/views/MarkdownTableView.tsx @@ -7,12 +7,10 @@ import I18n from '../i18n'; import { isIOS } from '../utils/deviceInfo'; import { themes } from '../constants/colors'; import { withTheme } from '../theme'; +import { ChatsStackParamList } from '../stacks/types'; interface IMarkdownTableViewProps { - route: RouteProp< - { MarkdownTableView: { renderRows: (drawExtraBorders?: boolean) => JSX.Element; tableWidth: number } }, - 'MarkdownTableView' - >; + route: RouteProp; theme: string; } diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index a948edcd..c9e3d601 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -3,8 +3,9 @@ import { FlatList, Text, View } from 'react-native'; import { connect } from 'react-redux'; import { dequal } from 'dequal'; import { StackNavigationProp } from '@react-navigation/stack'; -import { RouteProp } from '@react-navigation/core'; +import { CompositeNavigationProp, RouteProp } from '@react-navigation/core'; +import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import Message from '../../containers/message'; import ActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; @@ -18,22 +19,19 @@ import { withActionSheet } from '../../containers/ActionSheet'; import SafeAreaView from '../../containers/SafeAreaView'; import getThreadName from '../../lib/methods/getThreadName'; import styles from './styles'; - -type TMessagesViewRouteParams = { - MessagesView: { - rid: string; - t: string; - name: string; - }; -}; +import { ChatsStackParamList } from '../../stacks/types'; +import { ISubscription, SubscriptionType } from '../../definitions/ISubscription'; interface IMessagesViewProps { user: { id: string; }; baseUrl: string; - navigation: StackNavigationProp; - route: RouteProp; + navigation: CompositeNavigationProp< + StackNavigationProp, + StackNavigationProp + >; + route: RouteProp; customEmojis: { [key: string]: string }; theme: string; showActionSheet: Function; @@ -41,6 +39,14 @@ interface IMessagesViewProps { isMasterDetail: boolean; } +interface IRoomInfoParam { + room: ISubscription; + member: any; + rid: string; + t: SubscriptionType; + joined: boolean; +} + interface IMessagesViewState { loading: boolean; messages: []; @@ -65,17 +71,22 @@ interface IMessageItem { } interface IParams { - rid?: string; - jumpToMessageId: string; - t?: string; - room: any; + rid: string; + t: SubscriptionType; tmid?: string; + message?: string; name?: string; + fname?: string; + prid?: string; + room: ISubscription; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; } class MessagesView extends React.Component { - private rid?: string; - private t?: string; + private rid: string; + private t: SubscriptionType; private content: any; private room: any; @@ -121,7 +132,7 @@ class MessagesView extends React.Component { }); }; - navToRoomInfo = (navParam: { rid: string }) => { + navToRoomInfo = (navParam: IRoomInfoParam) => { const { navigation, user } = this.props; if (navParam.rid === user.id) { return; @@ -147,7 +158,7 @@ class MessagesView extends React.Component { ...params, tmid: item.tmid, name: await getThreadName(this.rid, item.tmid, item._id), - t: 'thread' + t: SubscriptionType.THREAD }; navigation.push('RoomView', params); } else { diff --git a/app/views/ModalBlockView.js b/app/views/ModalBlockView.tsx similarity index 70% rename from app/views/ModalBlockView.js rename to app/views/ModalBlockView.tsx index c87bf331..1a517745 100644 --- a/app/views/ModalBlockView.js +++ b/app/views/ModalBlockView.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import { connect } from 'react-redux'; import { KeyboardAwareScrollView } from '@codler/react-native-keyboard-aware-scroll-view'; @@ -15,6 +16,7 @@ import { CONTAINER_TYPES, MODAL_ACTIONS } from '../lib/methods/actions'; import { textParser } from '../containers/UIKit/utils'; import Navigation from '../lib/Navigation'; import sharedStyles from './Styles'; +import { MasterDetailInsideStackParamList } from '../stacks/MasterDetailStack/types'; const styles = StyleSheet.create({ container: { @@ -30,14 +32,49 @@ const styles = StyleSheet.create({ } }); -Object.fromEntries = Object.fromEntries || (arr => arr.reduce((acc, [k, v]) => ((acc[k] = v), acc), {})); -const groupStateByBlockIdMap = (obj, [key, { blockId, value }]) => { +interface IValueBlockId { + value: string; + blockId: string; +} + +type TElementToState = [string, IValueBlockId]; +interface IActions { + actionId: string; + value: any; + blockId?: string; +} + +interface IValues { + [key: string]: { + [key: string]: string; + }; +} +interface IModalBlockViewState { + data: any; + loading: boolean; + errors?: any; +} + +interface IModalBlockViewProps { + navigation: StackNavigationProp; + route: RouteProp; + theme: string; + language: string; + user: { + id: string; + token: string; + }; +} + +// eslint-disable-next-line no-sequences +Object.fromEntries = Object.fromEntries || ((arr: any[]) => arr.reduce((acc, [k, v]) => ((acc[k] = v), acc), {})); +const groupStateByBlockIdMap = (obj: any, [key, { blockId, value }]: TElementToState) => { obj[blockId] = obj[blockId] || {}; obj[blockId][key] = value; return obj; }; -const groupStateByBlockId = obj => Object.entries(obj).reduce(groupStateByBlockIdMap, {}); -const filterInputFields = ({ element, elements = [] }) => { +const groupStateByBlockId = (obj: { [key: string]: any }) => Object.entries(obj).reduce(groupStateByBlockIdMap, {}); +const filterInputFields = ({ element, elements = [] }: { element: any; elements?: any[] }) => { if (element && element.initialValue) { return true; } @@ -45,7 +82,8 @@ const filterInputFields = ({ element, elements = [] }) => { return true; } }; -const mapElementToState = ({ element, blockId, elements = [] }) => { + +const mapElementToState = ({ element, blockId, elements = [] }: { element: any; blockId: string; elements?: any[] }): any => { if (elements.length) { return elements .map(e => ({ element: e, blockId })) @@ -54,10 +92,15 @@ const mapElementToState = ({ element, blockId, elements = [] }) => { } return [element.actionId, { value: element.initialValue, blockId }]; }; -const reduceState = (obj, el) => (Array.isArray(el[0]) ? { ...obj, ...Object.fromEntries(el) } : { ...obj, [el[0]]: el[1] }); +const reduceState = (obj: any, el: any) => + Array.isArray(el[0]) ? { ...obj, ...Object.fromEntries(el) } : { ...obj, [el[0]]: el[1] }; -class ModalBlockView extends React.Component { - static navigationOptions = ({ route }) => { +class ModalBlockView extends React.Component { + private submitting: boolean; + + private values: IValues; + + static navigationOptions = ({ route }: Pick): StackNavigationOptions => { const data = route.params?.data; const { view } = data; const { title } = view; @@ -66,18 +109,7 @@ class ModalBlockView extends React.Component { }; }; - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string, - language: PropTypes.string, - user: PropTypes.shape({ - id: PropTypes.string, - token: PropTypes.string - }) - }; - - constructor(props) { + constructor(props: IModalBlockViewProps) { super(props); this.submitting = false; const data = props.route.params?.data; @@ -95,7 +127,7 @@ class ModalBlockView extends React.Component { EventEmitter.addEventListener(viewId, this.handleUpdate); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: IModalBlockViewProps) { const { navigation, route } = this.props; const oldData = prevProps.route.params?.data ?? {}; const newData = route.params?.data ?? {}; @@ -128,7 +160,7 @@ class ModalBlockView extends React.Component { /> ) - : null, + : undefined, headerRight: submit ? () => ( @@ -140,13 +172,13 @@ class ModalBlockView extends React.Component { /> ) - : null + : undefined }); }; - handleUpdate = ({ type, ...data }) => { + handleUpdate = ({ type, ...data }: { type: string }) => { if ([MODAL_ACTIONS.ERRORS].includes(type)) { - const { errors } = data; + const { errors }: any = data; this.setState({ errors }); } else { this.setState({ data }); @@ -154,7 +186,7 @@ class ModalBlockView extends React.Component { } }; - cancel = async ({ closeModal }) => { + cancel = async ({ closeModal }: { closeModal?: () => void }) => { const { data } = this.state; const { appId, viewId, view } = data; @@ -210,7 +242,7 @@ class ModalBlockView extends React.Component { this.setState({ loading: false }); }; - action = async ({ actionId, value, blockId }) => { + action = async ({ actionId, value, blockId }: IActions) => { const { data } = this.state; const { mid, appId, viewId } = data; await RocketChat.triggerBlockAction({ @@ -227,7 +259,7 @@ class ModalBlockView extends React.Component { this.changeState({ actionId, value, blockId }); }; - changeState = ({ actionId, value, blockId = 'default' }) => { + changeState = ({ actionId, value, blockId = 'default' }: IActions) => { this.values[actionId] = { blockId, value @@ -266,7 +298,7 @@ class ModalBlockView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ language: state.login.user && state.login.user.language }); diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.tsx similarity index 81% rename from app/views/NewMessageView.js rename to app/views/NewMessageView.tsx index 020588ff..cd182251 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { FlatList, StyleSheet, Text, View } from 'react-native'; +import { Dispatch } from 'redux'; import { connect } from 'react-redux'; import { Q } from '@nozbe/watermelondb'; import { dequal } from 'dequal'; -import * as List from '../containers/List'; +import * as List from '../containers/List'; import Touch from '../utils/touch'; import database from '../lib/database'; import RocketChat from '../lib/rocketchat'; @@ -18,7 +19,6 @@ import * as HeaderButton from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; import { themes } from '../constants/colors'; import { withTheme } from '../theme'; -import { getUserSelector } from '../selectors/login'; import Navigation from '../lib/Navigation'; import { createChannelRequest } from '../actions/createChannel'; import { goRoom } from '../utils/goRoom'; @@ -47,33 +47,54 @@ const styles = StyleSheet.create({ } }); -class NewMessageView extends React.Component { - static navigationOptions = ({ navigation }) => ({ +interface IButton { + onPress: () => void; + testID: string; + title: string; + icon: string; + first?: boolean; +} + +interface ISearch { + _id: string; + status: string; + username: string; + avatarETag: string; + outside: boolean; + rid: string; + name: string; + t: string; + search: boolean; +} + +interface INewMessageViewState { + search: ISearch[]; + // TODO: Refactor when migrate room + chats: any[]; + permissions: boolean[]; +} + +interface INewMessageViewProps { + navigation: StackNavigationProp; + create: (params: { group: boolean }) => void; + maxUsers: number; + theme: string; + isMasterDetail: boolean; + serverVersion: string; + createTeamPermission: string[]; + createDirectMessagePermission: string[]; + createPublicChannelPermission: string[]; + createPrivateChannelPermission: string[]; + createDiscussionPermission: string[]; +} + +class NewMessageView extends React.Component { + static navigationOptions = ({ navigation }: INewMessageViewProps): StackNavigationOptions => ({ headerLeft: () => , title: I18n.t('New_Message') }); - static propTypes = { - navigation: PropTypes.object, - baseUrl: PropTypes.string, - user: PropTypes.shape({ - id: PropTypes.string, - token: PropTypes.string, - roles: PropTypes.array - }), - create: PropTypes.func, - maxUsers: PropTypes.number, - theme: PropTypes.string, - isMasterDetail: PropTypes.bool, - serverVersion: PropTypes.string, - createTeamPermission: PropTypes.array, - createDirectMessagePermission: PropTypes.array, - createPublicChannelPermission: PropTypes.array, - createPrivateChannelPermission: PropTypes.array, - createDiscussionPermission: PropTypes.array - }; - - constructor(props) { + constructor(props: INewMessageViewProps) { super(props); this.init(); this.state = { @@ -102,7 +123,7 @@ class NewMessageView extends React.Component { this.handleHasPermission(); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: INewMessageViewProps) { const { createTeamPermission, createPublicChannelPermission, @@ -122,7 +143,7 @@ class NewMessageView extends React.Component { } } - onSearchChangeText(text) { + onSearchChangeText(text: string) { this.search(text); } @@ -131,7 +152,7 @@ class NewMessageView extends React.Component { return navigation.pop(); }; - search = async text => { + search = async (text: string) => { const result = await RocketChat.search({ text, filterRooms: false }); this.setState({ search: result @@ -162,7 +183,8 @@ class NewMessageView extends React.Component { }); }; - goRoom = item => { + // TODO: Refactor when migrate room + goRoom = (item: any) => { logEvent(events.NEW_MSG_CHAT_WITH_USER); const { isMasterDetail, navigation } = this.props; if (isMasterDetail) { @@ -171,7 +193,7 @@ class NewMessageView extends React.Component { goRoom({ item, isMasterDetail }); }; - renderButton = ({ onPress, testID, title, icon, first }) => { + renderButton = ({ onPress, testID, title, icon, first }: IButton) => { const { theme } = this.props; return ( @@ -218,7 +240,7 @@ class NewMessageView extends React.Component { return ( - this.onSearchChangeText(text)} testID='new-message-view-search' /> + this.onSearchChangeText(text)} testID='new-message-view-search' /> {permissions[0] || permissions[1] ? this.renderButton({ @@ -258,9 +280,10 @@ class NewMessageView extends React.Component { ); }; - renderItem = ({ item, index }) => { + // TODO: Refactor when migrate room + renderItem = ({ item, index }: { item: ISearch | any; index: number }) => { const { search, chats } = this.state; - const { baseUrl, user, theme } = this.props; + const { theme } = this.props; let style = { borderColor: themes[theme].separatorColor }; if (index === 0) { @@ -277,10 +300,8 @@ class NewMessageView extends React.Component { name={item.search ? item.name : item.fname} username={item.search ? item.username : item.name} onPress={() => this.goRoom(item)} - baseUrl={baseUrl} testID={`new-message-view-item-${item.name}`} style={style} - user={user} theme={theme} /> ); @@ -313,12 +334,10 @@ class NewMessageView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ serverVersion: state.server.version, isMasterDetail: state.app.isMasterDetail, - baseUrl: state.server.server, maxUsers: state.settings.DirectMesssage_maxUsers || 1, - user: getUserSelector(state), createTeamPermission: state.permissions['create-team'], createDirectMessagePermission: state.permissions['create-d'], createPublicChannelPermission: state.permissions['create-c'], @@ -326,8 +345,8 @@ const mapStateToProps = state => ({ createDiscussionPermission: state.permissions['start-discussion'] }); -const mapDispatchToProps = dispatch => ({ - create: params => dispatch(createChannelRequest(params)) +const mapDispatchToProps = (dispatch: Dispatch) => ({ + create: (params: { group: boolean }) => dispatch(createChannelRequest(params)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewMessageView)); diff --git a/app/views/NewServerView/ServerInput/Item.tsx b/app/views/NewServerView/ServerInput/Item.tsx index 9fb44719..cc8a9e3a 100644 --- a/app/views/NewServerView/ServerInput/Item.tsx +++ b/app/views/NewServerView/ServerInput/Item.tsx @@ -6,7 +6,7 @@ import { themes } from '../../../constants/colors'; import { CustomIcon } from '../../../lib/Icons'; import sharedStyles from '../../Styles'; import Touch from '../../../utils/touch'; -import { IServer } from '../index'; +import { TServerHistory } from '../../../definitions/IServerHistory'; const styles = StyleSheet.create({ container: { @@ -28,10 +28,10 @@ const styles = StyleSheet.create({ }); interface IItem { - item: IServer; + item: TServerHistory; theme: string; onPress(url: string): void; - onDelete(item: IServer): void; + onDelete(item: TServerHistory): void; } const Item = ({ item, theme, onPress, onDelete }: IItem): JSX.Element => ( diff --git a/app/views/NewServerView/ServerInput/index.tsx b/app/views/NewServerView/ServerInput/index.tsx index 1da15136..e2b14fd6 100644 --- a/app/views/NewServerView/ServerInput/index.tsx +++ b/app/views/NewServerView/ServerInput/index.tsx @@ -5,8 +5,8 @@ import TextInput from '../../../containers/TextInput'; import * as List from '../../../containers/List'; import { themes } from '../../../constants/colors'; import I18n from '../../../i18n'; +import { TServerHistory } from '../../../definitions/IServerHistory'; import Item from './Item'; -import { IServer } from '../index'; const styles = StyleSheet.create({ container: { @@ -33,8 +33,8 @@ interface IServerInput extends TextInputProps { theme: string; serversHistory: any[]; onSubmit(): void; - onDelete(item: IServer): void; - onPressServerHistory(serverHistory: IServer): void; + onDelete(item: TServerHistory): void; + onPressServerHistory(serverHistory: TServerHistory): void; } const ServerInput = ({ diff --git a/app/views/NewServerView/index.tsx b/app/views/NewServerView/index.tsx index f1458c93..93e493be 100644 --- a/app/views/NewServerView/index.tsx +++ b/app/views/NewServerView/index.tsx @@ -8,7 +8,6 @@ import { TouchableOpacity } from 'react-native-gesture-handler'; import Orientation from 'react-native-orientation-locker'; import { StackNavigationProp } from '@react-navigation/stack'; import { Dispatch } from 'redux'; -import Model from '@nozbe/watermelondb/Model'; import UserPreferences from '../../lib/userPreferences'; import EventEmitter from '../../utils/events'; @@ -33,6 +32,8 @@ import { isTablet } from '../../utils/deviceInfo'; import { verticalScale, moderateScale } from '../../utils/scaling'; import { withDimensions } from '../../dimensions'; import ServerInput from './ServerInput'; +import { OutsideParamList } from '../../stacks/types'; +import { TServerHistory } from '../../definitions/IServerHistory'; const styles = StyleSheet.create({ onboardingImage: { @@ -67,13 +68,8 @@ const styles = StyleSheet.create({ } }); -export interface IServer extends Model { - url: string; - username: string; -} - interface INewServerView { - navigation: StackNavigationProp; + navigation: StackNavigationProp; theme: string; connecting: boolean; connectServer(server: string, username?: string, fromServerHistory?: boolean): void; @@ -89,7 +85,7 @@ interface IState { text: string; connectingOpen: boolean; certificate: any; - serversHistory: IServer[]; + serversHistory: TServerHistory[]; } interface ISubmitParams { @@ -165,7 +161,7 @@ class NewServerView extends React.Component { const likeString = sanitizeLikeString(text); whereClause = [...whereClause, Q.where('url', Q.like(`%${likeString}%`))]; } - const serversHistory = (await serversHistoryCollection.query(...whereClause).fetch()) as IServer[]; + const serversHistory = (await serversHistoryCollection.query(...whereClause).fetch()) as TServerHistory[]; this.setState({ serversHistory }); } catch { // Do nothing @@ -189,7 +185,7 @@ class NewServerView extends React.Component { connectServer(server); }; - onPressServerHistory = (serverHistory: IServer) => { + onPressServerHistory = (serverHistory: TServerHistory) => { this.setState({ text: serverHistory.url }, () => this.submit({ fromServerHistory: true, username: serverHistory?.username })); }; @@ -273,23 +269,22 @@ class NewServerView extends React.Component { uriToPath = (uri: string) => uri.replace('file://', ''); handleRemove = () => { - // TODO: Remove ts-ignore when migrate the showConfirmationAlert - // @ts-ignore showConfirmationAlert({ message: I18n.t('You_will_unset_a_certificate_for_this_server'), confirmationText: I18n.t('Remove'), + // @ts-ignore onPress: this.setState({ certificate: null }) // We not need delete file from DocumentPicker because it is a temp file }); }; - deleteServerHistory = async (item: IServer) => { + deleteServerHistory = async (item: TServerHistory) => { const db = database.servers; try { await db.write(async () => { await item.destroyPermanently(); }); this.setState((prevstate: IState) => ({ - serversHistory: prevstate.serversHistory.filter((server: IServer) => server.id !== item.id) + serversHistory: prevstate.serversHistory.filter((server: TServerHistory) => server.id !== item.id) })); } catch { // Nothing diff --git a/app/views/NotificationPreferencesView/index.tsx b/app/views/NotificationPreferencesView/index.tsx index a020c163..5e33cec4 100644 --- a/app/views/NotificationPreferencesView/index.tsx +++ b/app/views/NotificationPreferencesView/index.tsx @@ -17,6 +17,7 @@ import SafeAreaView from '../../containers/SafeAreaView'; import log, { events, logEvent } from '../../utils/log'; import sharedStyles from '../Styles'; import { OPTIONS } from './options'; +import { ChatsStackParamList } from '../../stacks/types'; const styles = StyleSheet.create({ pickerText: { @@ -26,16 +27,8 @@ const styles = StyleSheet.create({ }); interface INotificationPreferencesView { - navigation: StackNavigationProp; - route: RouteProp< - { - NotificationPreferencesView: { - rid: string; - room: Model; - }; - }, - 'NotificationPreferencesView' - >; + navigation: StackNavigationProp; + route: RouteProp; theme: string; } diff --git a/app/views/NotificationPreferencesView/options.ts b/app/views/NotificationPreferencesView/options.ts index 4035c038..a2b3251c 100644 --- a/app/views/NotificationPreferencesView/options.ts +++ b/app/views/NotificationPreferencesView/options.ts @@ -1,4 +1,4 @@ -interface IOptionsField { +export interface IOptionsField { label: string; value: string | number; second?: number; diff --git a/app/views/PickerView.tsx b/app/views/PickerView.tsx index 002979b2..db2a7a26 100644 --- a/app/views/PickerView.tsx +++ b/app/views/PickerView.tsx @@ -11,6 +11,8 @@ import * as List from '../containers/List'; import SearchBox from '../containers/SearchBox'; import SafeAreaView from '../containers/SafeAreaView'; import sharedStyles from './Styles'; +import { ChatsStackParamList } from '../stacks/types'; +import { IOptionsField } from './NotificationPreferencesView/options'; const styles = StyleSheet.create({ search: { @@ -25,37 +27,21 @@ const styles = StyleSheet.create({ } }); -interface IData { - label: string; - value: string; - second?: string; -} - interface IItem { - item: IData; + item: IOptionsField; selected: boolean; onItemPress: () => void; theme: string; } interface IPickerViewState { - data: IData[]; + data: IOptionsField[]; value: string; } -interface IParams { - title: string; - value: string; - data: IData[]; - onChangeText: (value: string) => IData[]; - goBack: boolean; - onChange: Function; - onChangeValue: (value: string) => void; -} - interface IPickerViewProps { - navigation: StackNavigationProp; - route: RouteProp<{ PickerView: IParams }, 'PickerView'>; + navigation: StackNavigationProp; + route: RouteProp; theme: string; } @@ -69,7 +55,7 @@ const Item = React.memo(({ item, selected, onItemPress, theme }: IItem) => ( )); class PickerView extends React.PureComponent { - private onSearch: (text: string) => IData[]; + private onSearch?: ((text: string) => IOptionsField[]) | ((term?: string | undefined) => Promise); static navigationOptions = ({ route }: IPickerViewProps) => ({ title: route.params?.title ?? I18n.t('Select_an_option') @@ -126,13 +112,13 @@ class PickerView extends React.PureComponent {this.renderSearch()}