diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 62fa05a82..f85a69ddf 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 000000000..1ab897cd0 --- /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 000000000..95aaf3c17 --- /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/build.gradle b/android/build.gradle index 31650a07c..626e7564e 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 f7f08bb27..b73aaf8e3 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/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index 35e69c700..11414150c 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/Provider.tsx b/app/containers/ActionSheet/Provider.tsx index 0e58ae572..8e786b05f 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/Button/index.tsx b/app/containers/Button/index.tsx index 9e475a679..8c99dccee 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 64f5dbfe3..12217cf95 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 2d4c45b6f..f757d43d7 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 bf175dd70..aab5c889e 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/MessageBox/EmojiKeyboard.tsx b/app/containers/MessageBox/EmojiKeyboard.tsx index bbb0e20ad..91acc45d1 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 fa6c509ef..e219e6423 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/index.tsx b/app/containers/Passcode/Base/index.tsx index dd1d90e8d..c65917706 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 cc284b24a..0a9b6b1f9 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 4a08c91ce..6668e0f76 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 e62bc8063..dd780bbdd 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 b9aaf9620..9d4d005e0 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/definitions/IAttachment.ts b/app/definitions/IAttachment.ts new file mode 100644 index 000000000..168106177 --- /dev/null +++ b/app/definitions/IAttachment.ts @@ -0,0 +1,10 @@ +export interface IAttachment { + title: string; + type: string; + description: string; + title_link?: string; + image_url?: string; + image_type?: string; + video_url?: string; + video_type?: string; +} diff --git a/app/definitions/IMessage.ts b/app/definitions/IMessage.ts new file mode 100644 index 000000000..aca651c10 --- /dev/null +++ b/app/definitions/IMessage.ts @@ -0,0 +1,3 @@ +export interface IMessage { + msg: string; +} diff --git a/app/definitions/IRocketChatRecord.ts b/app/definitions/IRocketChatRecord.ts new file mode 100644 index 000000000..48d91fa84 --- /dev/null +++ b/app/definitions/IRocketChatRecord.ts @@ -0,0 +1,4 @@ +export interface IRocketChatRecord { + id: string; + updatedAt: Date; +} diff --git a/app/definitions/IRoom.ts b/app/definitions/IRoom.ts new file mode 100644 index 000000000..786c1d7c8 --- /dev/null +++ b/app/definitions/IRoom.ts @@ -0,0 +1,27 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export enum RoomType { + GROUP = 'p', + DIRECT = 'd', + CHANNEL = 'c', + OMNICHANNEL = 'l', + THREAD = 'thread' +} + +export interface IRoom extends IRocketChatRecord { + rid: string; + t: RoomType; + name: string; + fname: string; + prid?: string; + tmid?: string; + topic?: string; + teamMain?: boolean; + teamId?: string; + encrypted?: boolean; + visitor?: boolean; + autoTranslateLanguage?: boolean; + autoTranslate?: boolean; + observe?: Function; + usedCannedResponse: string; +} diff --git a/app/definitions/IServer.ts b/app/definitions/IServer.ts new file mode 100644 index 000000000..534a29c9c --- /dev/null +++ b/app/definitions/IServer.ts @@ -0,0 +1,16 @@ +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 | null; + biometry: boolean | null; + uniqueID: string; + enterpriseModules: string; + E2E_Enable: boolean; +} 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 10919715d..8cf8bddce 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/dimensions.tsx b/app/dimensions.tsx index dc164362a..676009683 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 00d320828..d10d5c892 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 defe9233f..5d537ceef 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/i18n/locales/ar.json b/app/i18n/locales/ar.json index 896d4d0d1..0f6eebaa9 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 5a6fe2bda..8bb4dee5a 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 18dd52bc5..e1d66b3dc 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", diff --git a/app/i18n/locales/fr.json b/app/i18n/locales/fr.json index 811486b06..81e8e21b3 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 7b1f1f666..5e9e4f9f7 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 df4e3aafd..aa63fdb74 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 24905e9f0..94e22a631 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", diff --git a/app/i18n/locales/pt-PT.json b/app/i18n/locales/pt-PT.json index f3d23c51e..f2cd45e7b 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 071e8f3e4..bdf2c161a 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 aeb4caba8..cc5651927 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 e01368904..24e166b9d 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 85d2690ca..258f8fdf7 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/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index 589aa6bd1..b680a9196 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 4dd84adfc..c2fc9fcdf 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 fac6d2c1f..239487b1b 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', () => { @@ -1141,10 +1138,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`); @@ -1523,16 +1516,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/navigationTypes.ts b/app/navigationTypes.ts new file mode 100644 index 000000000..568b75d0f --- /dev/null +++ b/app/navigationTypes.ts @@ -0,0 +1,45 @@ +import { NavigatorScreenParams } from '@react-navigation/core'; + +import { IRoom } from './definitions/IRoom'; +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: IRoom; + thread: any; // TODO: Change + }; + SelectServerView: undefined; +}; + +export type ShareOutsideStackParamList = { + WithoutServersView: undefined; +}; + +export type ShareAppStackParamList = { + AuthLoading?: undefined; + OutsideStack?: NavigatorScreenParams; + InsideStack?: NavigatorScreenParams; +}; diff --git a/app/presentation/DirectoryItem/index.tsx b/app/presentation/DirectoryItem/index.tsx index b8d9811a8..234c1e312 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 5319df82b..aa4f1182d 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/UserItem.tsx b/app/presentation/UserItem.tsx index 3dadc2bf7..b2e9d0b16 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/share.tsx b/app/share.tsx index ceb85477d..daee2fba0 100644 --- a/app/share.tsx +++ b/app/share.tsx @@ -25,6 +25,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; @@ -46,7 +47,7 @@ interface IState { fontScale: number; } -const Inside = createStackNavigator(); +const Inside = createStackNavigator(); const InsideStack = () => { const { theme } = useContext(ThemeContext); @@ -65,24 +66,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 +108,7 @@ class Root extends React.Component<{}, IState> { this.init(); } - componentWillUnmount() { + componentWillUnmount(): void { RocketChat.closeShareExtension(); unsubscribeTheme(); } diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.tsx similarity index 81% rename from app/stacks/InsideStack.js rename to app/stacks/InsideStack.tsx index b3de1b610..fce844a4a 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'; @@ -75,20 +66,33 @@ import QueueListView from '../ee/omnichannel/views/QueueListView'; import AddChannelTeamView from '../views/AddChannelTeamView'; import AddExistingChannelView from '../views/AddExistingChannelView'; import SelectListView from '../views/SelectListView'; +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 ( - + - + { 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); @@ -257,12 +240,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.tsx similarity index 60% rename from app/stacks/MasterDetailStack/ModalContainer.js rename to app/stacks/MasterDetailStack/ModalContainer.tsx index 7be11f8c7..aeea3d88c 100644 --- a/app/stacks/MasterDetailStack/ModalContainer.js +++ b/app/stacks/MasterDetailStack/ModalContainer.tsx @@ -1,10 +1,17 @@ import React from 'react'; import { StyleSheet, TouchableWithoutFeedback, View } from 'react-native'; -import PropTypes from 'prop-types'; +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, @@ -12,11 +19,11 @@ const styles = StyleSheet.create({ justifyContent: 'center' }, backdrop: { - ...StyleSheet.absoluteFill + ...StyleSheet.absoluteFillObject } }); -export const ModalContainer = ({ navigation, children, theme }) => ( +export const ModalContainer = ({ navigation, children, theme }: IModalContainer): JSX.Element => ( navigation.pop()}> @@ -24,9 +31,3 @@ export const ModalContainer = ({ navigation, children, theme }) => ( {children} ); - -ModalContainer.propTypes = { - navigation: PropTypes.object, - children: PropTypes.element, - theme: PropTypes.string -}; 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 71828a6a2..4a945fc0e 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'; @@ -63,9 +59,15 @@ import AddChannelTeamView from '../../views/AddChannelTeamView'; import AddExistingChannelView from '../../views/AddExistingChannelView'; import SelectListView from '../../views/SelectListView'; 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); @@ -82,28 +84,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} /> - - - + + @@ -226,21 +218,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 000000000..202ad6014 --- /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 { IRoom, RoomType } from '../../definitions/IRoom'; + +export type MasterDetailChatsStackParamList = { + RoomView: { + rid: string; + t: RoomType; + tmid?: string; + message?: string; + name?: string; + fname?: string; + prid?: string; + room: IRoom; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; + }; +}; + +export type MasterDetailDrawerParamList = { + ChatsStackNavigator: NavigatorScreenParams; +}; + +export type ModalStackParamList = { + RoomActionsView: { + room: IRoom; + member: any; + rid: string; + t: RoomType; + joined: boolean; + }; + RoomInfoView: { + room: IRoom; + member: any; + rid: string; + t: RoomType; + }; + SelectListView: { + data: any; + title: string; + infoText: string; + nextAction: Function; + showAlert: boolean; + isSearch: boolean; + onSearch: Function; + isRadio?: boolean; + }; + RoomInfoEditView: { + rid: string; + }; + RoomMembersView: { + rid: string; + room: IRoom; + }; + SearchMessagesView: { + rid: string; + t: RoomType; + 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: RoomType; + name: string; + }; + AutoTranslateView: { + rid: string; + room: IRoom; + }; + DirectoryView: undefined; + QueueListView: undefined; + NotificationPrefView: { + rid: string; + room: IRoom; + }; + ForwardLivechatView: { + rid: string; + }; + CannedResponsesListView: { + rid: string; + }; + CannedResponseDetail: { + cannedResponse: { + shortcut: string; + text: string; + scopeName: string; + tags: string[]; + }; + room: IRoom; + }; + LivechatEditView: { + room: IRoom; + 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: RoomType; + }; + 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: IRoom; + 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: IRoom; + thread: any; // TODO: Change + }; +}; diff --git a/app/stacks/OutsideStack.js b/app/stacks/OutsideStack.tsx similarity index 82% rename from app/stacks/OutsideStack.js rename to app/stacks/OutsideStack.tsx index a61411c6b..10f9bd63a 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 000000000..3107c69ce --- /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 { IRoom, RoomType } from '../definitions/IRoom'; + +export type ChatsStackParamList = { + RoomsListView: undefined; + RoomView: { + rid: string; + t: RoomType; + tmid?: string; + message?: string; + name?: string; + fname?: string; + prid?: string; + room: IRoom; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; + }; + RoomActionsView: { + room: IRoom; + member: any; + rid: string; + t: RoomType; + joined: boolean; + }; + SelectListView: { + data: any; + title: string; + infoText: string; + nextAction: Function; + showAlert: boolean; + isSearch: boolean; + onSearch: Function; + isRadio?: boolean; + }; + RoomInfoView: { + room: IRoom; + member: any; + rid: string; + t: RoomType; + }; + RoomInfoEditView: { + rid: string; + }; + RoomMembersView: { + rid: string; + room: IRoom; + }; + SearchMessagesView: { + rid: string; + t: RoomType; + 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: RoomType; + name: string; + }; + AutoTranslateView: { + rid: string; + room: IRoom; + }; + DirectoryView: undefined; + NotificationPrefView: { + rid: string; + room: Model; + }; + ForwardLivechatView: { + rid: string; + }; + LivechatEditView: { + room: IRoom; + 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: RoomType; + }; + 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: IRoom; + }; +}; + +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: IRoom; + 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: IRoom; + 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 4accff2cd..635ffda1c 100644 --- a/app/theme.tsx +++ b/app/theme.tsx @@ -12,7 +12,7 @@ interface IThemeContextProps { export const ThemeContext = React.createContext({ theme: 'light' }); -export function withTheme(Component: any) { +export function withTheme(Component: any): any { const ThemedComponent = (props: any) => ( {contexts => } ); diff --git a/app/utils/fileDownload/index.ts b/app/utils/fileDownload/index.ts index dda1a78ff..279d3b3a5 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/log/events.js b/app/utils/log/events.js index fc7a3497a..82dd3079d 100644 --- a/app/utils/log/events.js +++ b/app/utils/log/events.js @@ -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/media.js b/app/utils/media.js index b05f95a94..07f6f58d7 100644 --- a/app/utils/media.js +++ b/app/utils/media.js @@ -1,10 +1,13 @@ -export const canUploadFile = (file, allowList, maxFileSize) => { +export const canUploadFile = (file, allowList, maxFileSize, permissionToUploadFile) => { 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 }; diff --git a/app/utils/sslPinning.js b/app/utils/sslPinning.js index 50f944e63..c3e2128c9 100644 --- a/app/utils/sslPinning.js +++ b/app/utils/sslPinning.js @@ -7,6 +7,21 @@ import I18n from '../i18n'; import { extractHostname } from './server'; const { SSLPinning } = NativeModules; +const { documentDirectory } = FileSystem; + +const extractFileScheme = path => path.replace('file://', ''); // file:// isn't allowed by obj-C + +const getPath = name => `${documentDirectory}/${name}`; + +const persistCertificate = async (name, password) => { + const certificatePath = getPath(name); + const certificate = { + path: extractFileScheme(certificatePath), + password + }; + await UserPreferences.setMapAsync(name, certificate); + return certificate; +}; const RCSSLPinning = Platform.select({ ios: { @@ -25,17 +40,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 +56,19 @@ const RCSSLPinning = Platform.select({ reject(e); } }), - setCertificate: async (alias, server) => { - if (alias) { - const certificate = await UserPreferences.getMapAsync(alias); + setCertificate: async (name, server) => { + if (name) { + let certificate = await UserPreferences.getMapAsync(name); + 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/views/AddChannelTeamView.tsx b/app/views/AddChannelTeamView.tsx index d477f9bad..8a72d3c96 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 5efdbf34d..86ab9b9cf 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 80f728e12..f0af5dfa0 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 90adf8b42..09e2d5d61 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; diff --git a/app/views/AuthenticationWebView.tsx b/app/views/AuthenticationWebView.tsx index 870af9560..ac304fbfb 100644 --- a/app/views/AuthenticationWebView.tsx +++ b/app/views/AuthenticationWebView.tsx @@ -4,7 +4,9 @@ import { connect } from 'react-redux'; import parse from 'url-parse'; import { StackNavigationProp } from '@react-navigation/stack'; import { WebViewMessage } from 'react-native-webview/lib/WebViewTypes'; +import { RouteProp } from '@react-navigation/core'; +import { OutsideModalParamList } from '../stacks/types'; import RocketChat from '../lib/rocketchat'; import { isIOS } from '../utils/deviceInfo'; import StatusBar from '../containers/StatusBar'; @@ -41,17 +43,9 @@ window.addEventListener('popstate', function() { }); `; -interface IRoute { - params: { - authType: string; - url: string; - ssoToken?: string; - }; -} - interface INavigationOption { - navigation: StackNavigationProp; - 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 92a77543a..f840ca12a 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 { IRoom } from '../../definitions/IRoom'; 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; } diff --git a/app/views/CreateChannelView.tsx b/app/views/CreateChannelView.tsx index 45b2cc2f2..e8d719ab4 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 { ) : null, headerLeft: showCloseModal ? () => : undefined - }); + } as StackNavigationOptions); }; submit = () => { diff --git a/app/views/CreateDiscussionView/interfaces.ts b/app/views/CreateDiscussionView/interfaces.ts index 468833119..e9d076b16 100644 --- a/app/views/CreateDiscussionView/interfaces.ts +++ b/app/views/CreateDiscussionView/interfaces.ts @@ -1,14 +1,11 @@ +import { RouteProp } from '@react-navigation/core'; +import { StackNavigationProp } from '@react-navigation/stack'; + +import { NewMessageStackParamList } from '../../stacks/types'; + export interface ICreateChannelViewProps { - navigation: any; - route: { - params?: { - channel: string; - message: { - msg: string; - }; - showCloseModal: boolean; - }; - }; + navigation: StackNavigationProp; + route: RouteProp; server: string; user: { id: string; diff --git a/app/views/DirectoryView/Options.tsx b/app/views/DirectoryView/Options.tsx index fcc0f7bf6..112068061 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/E2EEnterYourPasswordView.tsx b/app/views/E2EEnterYourPasswordView.tsx index dd9cdfa8a..6d63f90dd 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 0fbdf77a1..fce1a2d0c 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 1c4e13a5a..3d9a32ee1 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 c08a1acdd..375d089d4 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 42a29782d..ea17466dd 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 62ce51212..4ae1a67df 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 cfcd3fa11..b7bf30710 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 44034cda2..aa6658d20 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 4643687e2..e43505f3f 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 a65994eef..e260199e0 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 a948edcd1..14532051b 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 { IRoom, RoomType } from '../../definitions/IRoom'; 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: IRoom; + member: any; + rid: string; + t: RoomType; + 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: RoomType; tmid?: string; + message?: string; name?: string; + fname?: string; + prid?: string; + room: IRoom; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; } class MessagesView extends React.Component { - private rid?: string; - private t?: string; + private rid: string; + private t: RoomType; 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: RoomType.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 c87bf3317..1a517745a 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 4cf92e094..cd1822513 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.tsx @@ -1,6 +1,7 @@ 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'; @@ -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/index.tsx b/app/views/NewServerView/index.tsx index f1458c93a..aaacdf90f 100644 --- a/app/views/NewServerView/index.tsx +++ b/app/views/NewServerView/index.tsx @@ -33,6 +33,7 @@ import { isTablet } from '../../utils/deviceInfo'; import { verticalScale, moderateScale } from '../../utils/scaling'; import { withDimensions } from '../../dimensions'; import ServerInput from './ServerInput'; +import { OutsideParamList } from '../../stacks/types'; const styles = StyleSheet.create({ onboardingImage: { @@ -73,7 +74,7 @@ export interface IServer extends Model { } interface INewServerView { - navigation: StackNavigationProp; + navigation: StackNavigationProp; theme: string; connecting: boolean; connectServer(server: string, username?: string, fromServerHistory?: boolean): void; diff --git a/app/views/NotificationPreferencesView/index.tsx b/app/views/NotificationPreferencesView/index.tsx index a020c1631..5e33cec49 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 4035c0380..a2b3251c6 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 002979b20..db2a7a265 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()} item.value} + keyExtractor={item => item.value as string} renderItem={({ item }) => ( this.onChangeValue(item.value)} + onItemPress={() => this.onChangeValue(item.value as string)} /> )} ItemSeparatorComponent={List.Separator} diff --git a/app/views/ProfileView/interfaces.ts b/app/views/ProfileView/interfaces.ts index 00117203e..bfec50c0d 100644 --- a/app/views/ProfileView/interfaces.ts +++ b/app/views/ProfileView/interfaces.ts @@ -1,6 +1,8 @@ import { StackNavigationProp } from '@react-navigation/stack'; import React from 'react'; +import { ProfileStackParamList } from '../../stacks/types'; + export interface IUser { id: string; name: string; @@ -31,14 +33,12 @@ export interface IAvatarButton { } export interface INavigationOptions { - navigation: StackNavigationProp; + navigation: StackNavigationProp; isMasterDetail?: boolean; } export interface IProfileViewProps { user: IUser; - navigation: StackNavigationProp; - isMasterDetail?: boolean; baseUrl: string; Accounts_AllowEmailChange: boolean; Accounts_AllowPasswordChange: boolean; diff --git a/app/views/ReadReceiptView/index.tsx b/app/views/ReadReceiptView/index.tsx index 9f1a00675..a40327b21 100644 --- a/app/views/ReadReceiptView/index.tsx +++ b/app/views/ReadReceiptView/index.tsx @@ -16,6 +16,7 @@ import { withTheme } from '../../theme'; import { themes } from '../../constants/colors'; import SafeAreaView from '../../containers/SafeAreaView'; import styles from './styles'; +import { ChatsStackParamList } from '../../stacks/types'; interface IReceipts { _id: string; @@ -36,8 +37,8 @@ interface IReadReceiptViewState { } interface INavigationOption { - navigation: StackNavigationProp; - route: RouteProp<{ ReadReceiptView: { messageId: string } }, 'ReadReceiptView'>; + navigation: StackNavigationProp; + route: RouteProp; isMasterDetail: boolean; } diff --git a/app/views/RegisterView.tsx b/app/views/RegisterView.tsx index 045712163..ae5f46bd9 100644 --- a/app/views/RegisterView.tsx +++ b/app/views/RegisterView.tsx @@ -5,6 +5,7 @@ import { RouteProp } from '@react-navigation/core'; import { connect } from 'react-redux'; import RNPickerSelect from 'react-native-picker-select'; +import { OutsideParamList } from '../stacks/types'; import log, { events, logEvent } from '../utils/log'; import Button from '../containers/Button'; import I18n from '../i18n'; @@ -51,8 +52,8 @@ const styles = StyleSheet.create({ }); interface IProps { - navigation: StackNavigationProp; - route: RouteProp; + navigation: StackNavigationProp; + route: RouteProp; server: string; Site_Name: string; Gitlab_URL: string; diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index c3bd105af..cb034bf3d 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -918,7 +918,6 @@ class RoomActionsView extends React.Component { event: this.convertTeamToChannel }) } - testID='room-actions-convert-channel-to-team' left={() => } showActionIndicator /> @@ -1198,23 +1197,6 @@ class RoomActionsView extends React.Component { > ) : null} - - {['l'].includes(t) && !this.isOmnichannelPreview ? ( - <> - - this.onPressTouchable({ - route: 'VisitorNavigationView', - params: { rid } - }) - } - left={() => } - showActionIndicator - /> - - > - ) : null} {this.renderLastSection()} diff --git a/app/views/ScreenLockConfigView.js b/app/views/ScreenLockConfigView.tsx similarity index 84% rename from app/views/ScreenLockConfigView.js rename to app/views/ScreenLockConfigView.tsx index 8e7de6cae..57638f417 100644 --- a/app/views/ScreenLockConfigView.js +++ b/app/views/ScreenLockConfigView.tsx @@ -1,7 +1,9 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Switch } from 'react-native'; import { connect } from 'react-redux'; +import { StackNavigationOptions } from '@react-navigation/stack'; +import Model from '@nozbe/watermelondb/Model'; +import { Subscription } from 'rxjs'; import I18n from '../i18n'; import { withTheme } from '../theme'; @@ -16,19 +18,42 @@ import { events, logEvent } from '../utils/log'; const DEFAULT_BIOMETRY = false; -class ScreenLockConfigView extends React.Component { - static navigationOptions = () => ({ +interface IServerRecords extends Model { + autoLock?: boolean; + autoLockTime?: number; + biometry?: boolean; +} + +interface IItem { + title: string; + value: number; + disabled?: boolean; +} + +interface IScreenLockConfigViewProps { + theme: string; + server: string; + Force_Screen_Lock: boolean; + Force_Screen_Lock_After: number; +} + +interface IScreenLockConfigViewState { + autoLock?: boolean; + autoLockTime?: number | null; + biometry?: boolean; + biometryLabel: null; +} + +class ScreenLockConfigView extends React.Component { + private serverRecord?: IServerRecords; + + private observable?: Subscription; + + static navigationOptions = (): StackNavigationOptions => ({ title: I18n.t('Screen_lock') }); - static propTypes = { - theme: PropTypes.string, - server: PropTypes.string, - Force_Screen_Lock: PropTypes.string, - Force_Screen_Lock_After: PropTypes.string - }; - - constructor(props) { + constructor(props: IScreenLockConfigViewProps) { super(props); this.state = { autoLock: false, @@ -104,7 +129,7 @@ class ScreenLockConfigView extends React.Component { logEvent(events.SLC_SAVE_SCREEN_LOCK); const { autoLock, autoLockTime, biometry } = this.state; const serversDB = database.servers; - await serversDB.action(async () => { + await serversDB.write(async () => { await this.serverRecord?.update(record => { record.autoLock = autoLock; record.autoLockTime = autoLockTime === null ? DEFAULT_AUTO_LOCK : autoLockTime; @@ -113,7 +138,7 @@ class ScreenLockConfigView extends React.Component { }); }; - changePasscode = async ({ force }) => { + changePasscode = async ({ force }: { force: boolean }) => { logEvent(events.SLC_CHANGE_PASSCODE); await changePasscode({ force }); }; @@ -144,12 +169,12 @@ class ScreenLockConfigView extends React.Component { ); }; - isSelected = value => { + isSelected = (value: number) => { const { autoLockTime } = this.state; return autoLockTime === value; }; - changeAutoLockTime = autoLockTime => { + changeAutoLockTime = (autoLockTime: number) => { logEvent(events.SLC_CHANGE_AUTOLOCK_TIME); this.setState({ autoLockTime }, () => this.save()); }; @@ -159,7 +184,7 @@ class ScreenLockConfigView extends React.Component { return ; }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: IItem }) => { const { title, value, disabled } = item; return ( <> @@ -194,7 +219,7 @@ class ScreenLockConfigView extends React.Component { if (!autoLock) { return null; } - let items = this.defaultAutoLockOptions; + let items: IItem[] = this.defaultAutoLockOptions; if (Force_Screen_Lock && Force_Screen_Lock_After > 0) { items = [ { @@ -262,7 +287,7 @@ class ScreenLockConfigView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ server: state.server.server, Force_Screen_Lock: state.settings.Force_Screen_Lock, Force_Screen_Lock_After: state.settings.Force_Screen_Lock_After diff --git a/app/views/ScreenLockedView.js b/app/views/ScreenLockedView.tsx similarity index 76% rename from app/views/ScreenLockedView.js rename to app/views/ScreenLockedView.tsx index 644e76ff0..6e20152b5 100644 --- a/app/views/ScreenLockedView.js +++ b/app/views/ScreenLockedView.tsx @@ -1,19 +1,25 @@ import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; import Modal from 'react-native-modal'; import useDeepCompareEffect from 'use-deep-compare-effect'; import isEmpty from 'lodash/isEmpty'; import Orientation from 'react-native-orientation-locker'; -import { withTheme } from '../theme'; +import { useTheme } from '../theme'; import EventEmitter from '../utils/events'; import { LOCAL_AUTHENTICATE_EMITTER } from '../constants/localAuthentication'; import { isTablet } from '../utils/deviceInfo'; import { PasscodeEnter } from '../containers/Passcode'; -const ScreenLockedView = ({ theme }) => { +interface IData { + submit?: () => void; + hasBiometry?: boolean; +} + +const ScreenLockedView = (): JSX.Element => { const [visible, setVisible] = useState(false); - const [data, setData] = useState({}); + const [data, setData] = useState({}); + + const { theme } = useTheme(); useDeepCompareEffect(() => { if (!isEmpty(data)) { @@ -23,7 +29,7 @@ const ScreenLockedView = ({ theme }) => { } }, [data]); - const showScreenLock = args => { + const showScreenLock = (args: IData) => { setData(args); }; @@ -56,13 +62,9 @@ const ScreenLockedView = ({ theme }) => { style={{ margin: 0 }} animationIn='fadeIn' animationOut='fadeOut'> - + ); }; -ScreenLockedView.propTypes = { - theme: PropTypes.string -}; - -export default withTheme(ScreenLockedView); +export default ScreenLockedView; diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index a9be99918..a85df6745 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -1,11 +1,13 @@ import React from 'react'; import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; -import { RouteProp } from '@react-navigation/core'; +import { CompositeNavigationProp, RouteProp } from '@react-navigation/core'; import { FlatList, Text, View } from 'react-native'; import { Q } from '@nozbe/watermelondb'; import { connect } from 'react-redux'; import { dequal } from 'dequal'; +import { IRoom, RoomType } from '../../definitions/IRoom'; +import { IAttachment } from '../../definitions/IAttachment'; import RCTextInput from '../../containers/TextInput'; import ActivityIndicator from '../../containers/ActivityIndicator'; import Markdown from '../../containers/markdown'; @@ -13,7 +15,7 @@ import debounce from '../../utils/debounce'; import RocketChat from '../../lib/rocketchat'; import Message from '../../containers/message'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; -import { IMessage, IMessageAttachments } from '../../containers/message/interfaces'; +import { IMessage } from '../../containers/message/interfaces'; import I18n from '../../i18n'; import StatusBar from '../../containers/StatusBar'; import log from '../../utils/log'; @@ -29,26 +31,30 @@ import getRoomInfo from '../../lib/methods/getRoomInfo'; import { isIOS } from '../../utils/deviceInfo'; import { compareServerVersion, methods } from '../../lib/utils'; import styles from './styles'; +import { InsideStackParamList, ChatsStackParamList } from '../../stacks/types'; const QUERY_SIZE = 50; -type TRouteParams = { - SearchMessagesView: { - showCloseModal?: boolean; - rid: string; - t?: string; - encrypted?: boolean; - }; -}; - interface ISearchMessagesViewState { loading: boolean; messages: IMessage[]; searchText: string; } + +interface IRoomInfoParam { + room: IRoom; + member: any; + rid: string; + t: RoomType; + joined: boolean; +} + interface INavigationOption { - navigation: StackNavigationProp; - route: RouteProp; + navigation: CompositeNavigationProp< + StackNavigationProp, + StackNavigationProp + >; + route: RouteProp; } interface ISearchMessagesViewProps extends INavigationOption { @@ -183,12 +189,12 @@ class SearchMessagesView extends React.Component { + showAttachment = (attachment: IAttachment) => { const { navigation } = this.props; navigation.navigate('AttachmentView', { attachment }); }; - navToRoomInfo = (navParam: IMessage) => { + navToRoomInfo = (navParam: IRoomInfoParam) => { const { navigation, user } = this.props; if (navParam.rid === user.id) { return; diff --git a/app/views/SecurityPrivacyView.js b/app/views/SecurityPrivacyView.tsx similarity index 82% rename from app/views/SecurityPrivacyView.js rename to app/views/SecurityPrivacyView.tsx index 5a6adcd48..ffd61d31e 100644 --- a/app/views/SecurityPrivacyView.js +++ b/app/views/SecurityPrivacyView.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Switch } from 'react-native'; -import PropTypes from 'prop-types'; +import { StackNavigationProp } from '@react-navigation/stack'; import AsyncStorage from '@react-native-community/async-storage'; import { useSelector } from 'react-redux'; @@ -20,11 +20,15 @@ import { import SafeAreaView from '../containers/SafeAreaView'; import { isFDroidBuild } from '../constants/environment'; -const SecurityPrivacyView = ({ navigation }) => { +interface ISecurityPrivacyViewProps { + navigation: StackNavigationProp; +} + +const SecurityPrivacyView = ({ navigation }: ISecurityPrivacyViewProps): JSX.Element => { const [crashReportState, setCrashReportState] = useState(getReportCrashErrorsValue()); const [analyticsEventsState, setAnalyticsEventsState] = useState(getReportAnalyticsEventsValue()); - const e2eEnabled = useSelector(state => state.settings.E2E_Enable); + const e2eEnabled = useSelector((state: any) => state.settings.E2E_Enable); useEffect(() => { navigation.setOptions({ @@ -32,21 +36,22 @@ const SecurityPrivacyView = ({ navigation }) => { }); }, []); - const toggleCrashReport = value => { - logEvent(events.SE_TOGGLE_CRASH_REPORT); + const toggleCrashReport = (value: boolean) => { + logEvent(events.SP_TOGGLE_CRASH_REPORT); AsyncStorage.setItem(CRASH_REPORT_KEY, JSON.stringify(value)); setCrashReportState(value); toggleCrashErrorsReport(value); }; - const toggleAnalyticsEvents = value => { - logEvent(events.SE_TOGGLE_ANALYTICS_EVENTS); + const toggleAnalyticsEvents = (value: boolean) => { + logEvent(events.SP_TOGGLE_ANALYTICS_EVENTS); AsyncStorage.setItem(ANALYTICS_EVENTS_KEY, JSON.stringify(value)); setAnalyticsEventsState(value); toggleAnalyticsEventsReport(value); }; - const navigateToScreen = screen => { + const navigateToScreen = (screen: 'E2EEncryptionSecurityView' | 'ScreenLockConfigView') => { + // @ts-ignore logEvent(events[`SP_GO_${screen.replace('View', '').toUpperCase()}`]); navigation.navigate(screen); }; @@ -106,8 +111,4 @@ const SecurityPrivacyView = ({ navigation }) => { ); }; -SecurityPrivacyView.propTypes = { - navigation: PropTypes.object -}; - export default SecurityPrivacyView; diff --git a/app/views/SelectListView.js b/app/views/SelectListView.tsx similarity index 77% rename from app/views/SelectListView.js rename to app/views/SelectListView.tsx index 9d2f533da..e6d67266b 100644 --- a/app/views/SelectListView.js +++ b/app/views/SelectListView.tsx @@ -1,8 +1,9 @@ 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 { connect } from 'react-redux'; import { RadioButton } from 'react-native-ui-lib'; +import { RouteProp } from '@react-navigation/native'; import log from '../utils/log'; import * as List from '../containers/List'; @@ -25,15 +26,58 @@ const styles = StyleSheet.create({ } }); -class SelectListView extends React.Component { - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string, - isMasterDetail: PropTypes.bool - }; +interface IData { + rid: string; + name: string; + t?: string; + teamMain?: boolean; + alert?: boolean; +} - constructor(props) { +interface ISelectListViewState { + data: IData[]; + dataFiltered: IData[]; + isSearching: boolean; + selected: string[]; +} + +interface ISelectListViewProps { + navigation: StackNavigationProp; + route: RouteProp< + { + SelectView: { + data: IData[]; + title: string; + infoText: string; + nextAction(selected: string[]): void; + showAlert(): void; + isSearch: boolean; + onSearch(text: string): IData[]; + isRadio: boolean; + }; + }, + 'SelectView' + >; + theme: string; + isMasterDetail: boolean; +} + +class SelectListView extends React.Component { + private title: string; + + private infoText: string; + + private nextAction: (selected: string[]) => void; + + private showAlert: () => void; + + private isSearch: boolean; + + private onSearch: (text: string) => IData[]; + + private isRadio: boolean; + + constructor(props: ISelectListViewProps) { super(props); const data = props.route?.params?.data; this.title = props.route?.params?.title; @@ -56,7 +100,7 @@ class SelectListView extends React.Component { const { navigation, isMasterDetail } = this.props; const { selected } = this.state; - const options = { + const options: StackNavigationOptions = { headerTitle: I18n.t(this.title) }; @@ -87,7 +131,7 @@ class SelectListView extends React.Component { return ( this.search(text)} + onChangeText={(text: string) => this.search(text)} testID='select-list-view-search' onCancelPress={() => this.setState({ isSearching: false })} /> @@ -95,7 +139,7 @@ class SelectListView extends React.Component { ); }; - search = async text => { + search = async (text: string) => { try { this.setState({ isSearching: true }); const result = await this.onSearch(text); @@ -105,12 +149,12 @@ class SelectListView extends React.Component { } }; - isChecked = rid => { + isChecked = (rid: string) => { const { selected } = this.state; return selected.includes(rid); }; - toggleItem = rid => { + toggleItem = (rid: string) => { const { selected } = this.state; animateNextTransition(); @@ -126,7 +170,7 @@ class SelectListView extends React.Component { } }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: IData }) => { const { theme } = this.props; const { selected } = this.state; @@ -187,7 +231,7 @@ class SelectListView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ isMasterDetail: state.app.isMasterDetail }); diff --git a/app/views/SelectServerView.js b/app/views/SelectServerView.tsx similarity index 61% rename from app/views/SelectServerView.js rename to app/views/SelectServerView.tsx index 0b43747af..e26645d9b 100644 --- a/app/views/SelectServerView.js +++ b/app/views/SelectServerView.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { FlatList } from 'react-native'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { connect } from 'react-redux'; -import { Q } from '@nozbe/watermelondb'; +import { Q, Model } from '@nozbe/watermelondb'; import I18n from '../i18n'; import StatusBar from '../containers/StatusBar'; @@ -12,29 +12,39 @@ import database from '../lib/database'; import SafeAreaView from '../containers/SafeAreaView'; import * as List from '../containers/List'; -const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); -const keyExtractor = item => item.id; +const getItemLayout = (data: any, index: number) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); +const keyExtractor = (item: IServer) => item.id; -class SelectServerView extends React.Component { - static navigationOptions = () => ({ +interface IServer extends Model { + id: string; + iconURL?: string; + name?: string; +} + +interface ISelectServerViewState { + servers: IServer[]; +} + +interface ISelectServerViewProps { + navigation: StackNavigationProp; + server: string; +} + +class SelectServerView extends React.Component { + static navigationOptions = (): StackNavigationOptions => ({ title: I18n.t('Select_Server') }); - static propTypes = { - server: PropTypes.string, - navigation: PropTypes.object - }; - - state = { servers: [] }; + state = { servers: [] as IServer[] }; async componentDidMount() { const serversDB = database.servers; const serversCollection = serversDB.get('servers'); - const servers = await serversCollection.query(Q.where('rooms_updated_at', Q.notEq(null))).fetch(); + const servers: IServer[] = await serversCollection.query(Q.where('rooms_updated_at', Q.notEq(null))).fetch(); this.setState({ servers }); } - select = async server => { + select = async (server: string) => { const { server: currentServer, navigation } = this.props; navigation.navigate('ShareListView'); @@ -43,7 +53,7 @@ class SelectServerView extends React.Component { } }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: IServer }) => { const { server } = this.props; return this.select(item.id)} item={item} hasCheck={item.id === server} />; }; @@ -62,7 +72,6 @@ class SelectServerView extends React.Component { ItemSeparatorComponent={List.Separator} ListHeaderComponent={List.Separator} ListFooterComponent={List.Separator} - enableEmptySections removeClippedSubviews keyboardShouldPersistTaps='always' /> @@ -71,7 +80,7 @@ class SelectServerView extends React.Component { } } -const mapStateToProps = ({ share }) => ({ +const mapStateToProps = ({ share }: any) => ({ server: share.server.server }); diff --git a/app/views/SelectedUsersView.js b/app/views/SelectedUsersView.tsx similarity index 73% rename from app/views/SelectedUsersView.js rename to app/views/SelectedUsersView.tsx index b7b254511..8d4a19fc4 100644 --- a/app/views/SelectedUsersView.js +++ b/app/views/SelectedUsersView.tsx @@ -1,9 +1,11 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import { FlatList, View } from 'react-native'; import { connect } from 'react-redux'; import orderBy from 'lodash/orderBy'; import { Q } from '@nozbe/watermelondb'; +import { Subscription } from 'rxjs'; import * as List from '../containers/List'; import database from '../lib/database'; @@ -22,33 +24,51 @@ import { addUser as addUserAction, removeUser as removeUserAction, reset as rese import { showErrorAlert } from '../utils/info'; import SafeAreaView from '../containers/SafeAreaView'; import sharedStyles from './Styles'; +import { ChatsStackParamList } from '../stacks/types'; const ITEM_WIDTH = 250; -const getItemLayout = (_, index) => ({ length: ITEM_WIDTH, offset: ITEM_WIDTH * index, index }); +const getItemLayout = (_: any, index: number) => ({ length: ITEM_WIDTH, offset: ITEM_WIDTH * index, index }); -class SelectedUsersView extends React.Component { - static propTypes = { - baseUrl: PropTypes.string, - addUser: PropTypes.func.isRequired, - removeUser: PropTypes.func.isRequired, - reset: PropTypes.func.isRequired, - users: PropTypes.array, - loading: PropTypes.bool, - user: PropTypes.shape({ - id: PropTypes.string, - token: PropTypes.string, - username: PropTypes.string, - name: PropTypes.string - }), - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string +interface IUser { + _id: string; + name: string; + fname: string; + search?: boolean; + // username is used when is from searching + username?: string; +} +interface ISelectedUsersViewState { + maxUsers?: number; + search: IUser[]; + chats: IUser[]; +} + +interface ISelectedUsersViewProps { + navigation: StackNavigationProp; + route: RouteProp; + baseUrl: string; + addUser(user: IUser): void; + removeUser(user: IUser): void; + reset(): void; + users: IUser[]; + loading: boolean; + user: { + id: string; + token: string; + username: string; + name: string; }; + theme: string; +} - constructor(props) { +class SelectedUsersView extends React.Component { + private flatlist?: FlatList; + + private querySubscription?: Subscription; + + constructor(props: ISelectedUsersViewProps) { super(props); this.init(); - this.flatlist = React.createRef(); const maxUsers = props.route.params?.maxUsers; this.state = { maxUsers, @@ -62,7 +82,7 @@ class SelectedUsersView extends React.Component { this.setHeader(props.route.params?.showButton); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: ISelectedUsersViewProps) { if (this.isGroupChat()) { const { users } = this.props; if (prevProps.users.length !== users.length) { @@ -80,7 +100,7 @@ class SelectedUsersView extends React.Component { } // showButton can be sent as route params or updated by the component - setHeader = showButton => { + setHeader = (showButton?: boolean) => { const { navigation, route } = this.props; const title = route.params?.title ?? I18n.t('Select_Users'); const buttonText = route.params?.buttonText ?? I18n.t('Next'); @@ -107,7 +127,8 @@ class SelectedUsersView extends React.Component { .query(Q.where('t', 'd')) .observeWithColumns(['room_updated_at']); - this.querySubscription = observable.subscribe(data => { + // TODO: Refactor when migrate room + this.querySubscription = observable.subscribe((data: any) => { const chats = orderBy(data, ['roomUpdatedAt'], ['desc']); this.setState({ chats }); }); @@ -116,11 +137,11 @@ class SelectedUsersView extends React.Component { } }; - onSearchChangeText(text) { + onSearchChangeText(text: string) { this.search(text); } - search = async text => { + search = async (text: string) => { const result = await RocketChat.search({ text, filterRooms: false }); this.setState({ search: result @@ -129,15 +150,15 @@ class SelectedUsersView extends React.Component { isGroupChat = () => { const { maxUsers } = this.state; - return maxUsers > 2; + return maxUsers! > 2; }; - isChecked = username => { + isChecked = (username: string) => { const { users } = this.props; return users.findIndex(el => el.name === username) !== -1; }; - toggleUser = user => { + toggleUser = (user: IUser) => { const { maxUsers } = this.state; const { addUser, @@ -163,29 +184,29 @@ class SelectedUsersView extends React.Component { } }; - _onPressItem = (id, item = {}) => { + _onPressItem = (id: string, item = {} as IUser) => { if (item.search) { - this.toggleUser({ _id: item._id, name: item.username, fname: item.name }); + this.toggleUser({ _id: item._id, name: item.username!, fname: item.name }); } else { this.toggleUser({ _id: item._id, name: item.name, fname: item.fname }); } }; - _onPressSelectedItem = item => this.toggleUser(item); + _onPressSelectedItem = (item: IUser) => this.toggleUser(item); renderHeader = () => { const { theme } = this.props; return ( - this.onSearchChangeText(text)} testID='select-users-view-search' /> + this.onSearchChangeText(text)} testID='select-users-view-search' /> {this.renderSelected()} ); }; - setFlatListRef = ref => (this.flatlist = ref); + setFlatListRef = (ref: FlatList) => (this.flatlist = ref); - onContentSizeChange = () => this.flatlist.scrollToEnd({ animated: true }); + onContentSizeChange = () => this.flatlist?.scrollToEnd({ animated: true }); renderSelected = () => { const { users, theme } = this.props; @@ -204,35 +225,32 @@ class SelectedUsersView extends React.Component { style={[sharedStyles.separatorTop, { borderColor: themes[theme].separatorColor }]} contentContainerStyle={{ marginVertical: 5 }} renderItem={this.renderSelectedItem} - enableEmptySections keyboardShouldPersistTaps='always' horizontal /> ); }; - renderSelectedItem = ({ item }) => { - const { baseUrl, user, theme } = this.props; + renderSelectedItem = ({ item }: { item: IUser }) => { + const { theme } = this.props; return ( this._onPressSelectedItem(item)} testID={`selected-user-${item.name}`} - baseUrl={baseUrl} style={{ paddingRight: 15 }} - user={user} theme={theme} /> ); }; - renderItem = ({ item, index }) => { + renderItem = ({ item, index }: { item: IUser; index: number }) => { const { search, chats } = this.state; - const { baseUrl, user, theme } = this.props; + const { theme } = this.props; const name = item.search ? item.name : item.fname; - const username = item.search ? item.username : item.name; + const username = item.search ? item.username! : item.name; let style = { borderColor: themes[theme].separatorColor }; if (index === 0) { style = { ...style, ...sharedStyles.separatorTop }; @@ -250,9 +268,7 @@ class SelectedUsersView extends React.Component { onPress={() => this._onPressItem(item._id, item)} testID={`select-users-view-item-${item.name}`} icon={this.isChecked(username) ? 'check' : null} - baseUrl={baseUrl} style={style} - user={user} theme={theme} /> ); @@ -275,7 +291,6 @@ class SelectedUsersView extends React.Component { ItemSeparatorComponent={List.Separator} ListHeaderComponent={this.renderHeader} contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} - enableEmptySections keyboardShouldPersistTaps='always' /> ); @@ -293,16 +308,16 @@ class SelectedUsersView extends React.Component { }; } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ baseUrl: state.server.server, users: state.selectedUsers.users, loading: state.selectedUsers.loading, user: getUserSelector(state) }); -const mapDispatchToProps = dispatch => ({ - addUser: user => dispatch(addUserAction(user)), - removeUser: user => dispatch(removeUserAction(user)), +const mapDispatchToProps = (dispatch: any) => ({ + addUser: (user: any) => dispatch(addUserAction(user)), + removeUser: (user: any) => dispatch(removeUserAction(user)), reset: () => dispatch(resetAction()) }); diff --git a/app/views/SendEmailConfirmationView.tsx b/app/views/SendEmailConfirmationView.tsx index 892673acc..a3aad4979 100644 --- a/app/views/SendEmailConfirmationView.tsx +++ b/app/views/SendEmailConfirmationView.tsx @@ -1,6 +1,8 @@ import React, { useEffect, useState } from 'react'; import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/core'; +import { OutsideParamList } from '../stacks/types'; import TextInput from '../containers/TextInput'; import Button from '../containers/Button'; import { showErrorAlert } from '../utils/info'; @@ -12,16 +14,12 @@ import FormContainer, { FormContainerInner } from '../containers/FormContainer'; import log, { events, logEvent } from '../utils/log'; import sharedStyles from './Styles'; -interface ISendEmailConfirmationView { - navigation: StackNavigationProp; - route: { - params: { - user?: string; - }; - }; +interface ISendEmailConfirmationViewProps { + navigation: StackNavigationProp; + route: RouteProp; } -const SendEmailConfirmationView = ({ navigation, route }: ISendEmailConfirmationView): JSX.Element => { +const SendEmailConfirmationView = ({ navigation, route }: ISendEmailConfirmationViewProps): JSX.Element => { const [email, setEmail] = useState(''); const [invalidEmail, setInvalidEmail] = useState(true); const [isFetching, setIsFetching] = useState(false); diff --git a/app/views/SetUsernameView.js b/app/views/SetUsernameView.tsx similarity index 77% rename from app/views/SetUsernameView.js rename to app/views/SetUsernameView.tsx index 158c6d013..221561697 100644 --- a/app/views/SetUsernameView.js +++ b/app/views/SetUsernameView.tsx @@ -1,8 +1,10 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; +import { Dispatch } from 'redux'; import { ScrollView, StyleSheet, Text } from 'react-native'; import { connect } from 'react-redux'; import Orientation from 'react-native-orientation-locker'; +import { RouteProp } from '@react-navigation/native'; import { loginRequest as loginRequestAction } from '../actions/login'; import TextInput from '../containers/TextInput'; @@ -27,21 +29,27 @@ const styles = StyleSheet.create({ } }); -class SetUsernameView extends React.Component { - static navigationOptions = ({ route }) => ({ +interface ISetUsernameViewState { + username: string; + saving: boolean; +} + +interface ISetUsernameViewProps { + navigation: StackNavigationProp; + route: RouteProp<{ SetUsernameView: { title: string } }, 'SetUsernameView'>; + server: string; + userId: string; + loginRequest: ({ resume }: { resume: string }) => void; + token: string; + theme: string; +} + +class SetUsernameView extends React.Component { + static navigationOptions = ({ route }: Pick): StackNavigationOptions => ({ title: route.params?.title }); - static propTypes = { - navigation: PropTypes.object, - server: PropTypes.string, - userId: PropTypes.string, - loginRequest: PropTypes.func, - token: PropTypes.string, - theme: PropTypes.string - }; - - constructor(props) { + constructor(props: ISetUsernameViewProps) { super(props); this.state = { username: '', @@ -61,7 +69,7 @@ class SetUsernameView extends React.Component { } } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: ISetUsernameViewProps, nextState: ISetUsernameViewState) { const { username, saving } = this.state; const { theme } = this.props; if (nextProps.theme !== theme) { @@ -88,7 +96,7 @@ class SetUsernameView extends React.Component { try { await RocketChat.saveUserProfile({ username }); await loginRequest({ resume: token }); - } catch (e) { + } catch (e: any) { showErrorAlert(e.message, I18n.t('Oops')); } this.setState({ saving: false }); @@ -136,13 +144,13 @@ class SetUsernameView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ server: state.server.server, token: getUserSelector(state).token }); -const mapDispatchToProps = dispatch => ({ - loginRequest: params => dispatch(loginRequestAction(params)) +const mapDispatchToProps = (dispatch: Dispatch) => ({ + loginRequest: (params: { resume: string }) => dispatch(loginRequestAction(params)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SetUsernameView)); diff --git a/app/views/SettingsView/index.tsx b/app/views/SettingsView/index.tsx index 02c17169b..edad2822e 100644 --- a/app/views/SettingsView/index.tsx +++ b/app/views/SettingsView/index.tsx @@ -5,6 +5,7 @@ import FastImage from '@rocket.chat/react-native-fast-image'; import CookieManager from '@react-native-cookies/cookies'; import { StackNavigationProp } from '@react-navigation/stack'; +import { SettingsStackParamList } from '../../stacks/types'; import { logout as logoutAction } from '../../actions/login'; import { selectServerRequest as selectServerRequestAction } from '../../actions/server'; import { themes } from '../../constants/colors'; @@ -29,8 +30,8 @@ import database from '../../lib/database'; import { isFDroidBuild } from '../../constants/environment'; import { getUserSelector } from '../../selectors/login'; -interface IProps { - navigation: StackNavigationProp; +interface ISettingsViewProps { + navigation: StackNavigationProp; server: { version: string; server: string; @@ -46,8 +47,8 @@ interface IProps { appStart: Function; } -class SettingsView extends React.Component { - static navigationOptions = ({ navigation, isMasterDetail }: Partial) => ({ +class SettingsView extends React.Component { + static navigationOptions = ({ navigation, isMasterDetail }: ISettingsViewProps) => ({ headerLeft: () => isMasterDetail ? ( @@ -117,7 +118,7 @@ class SettingsView extends React.Component { }); }; - navigateToScreen = (screen: string) => { + navigateToScreen = (screen: keyof SettingsStackParamList) => { /* @ts-ignore */ logEvent(events[`SE_GO_${screen.replace('View', '').toUpperCase()}`]); const { navigation } = this.props; diff --git a/app/views/ShareListView/Header/Header.ios.js b/app/views/ShareListView/Header/Header.ios.tsx similarity index 83% rename from app/views/ShareListView/Header/Header.ios.js rename to app/views/ShareListView/Header/Header.ios.tsx index d68bacff2..c1f5e1166 100644 --- a/app/views/ShareListView/Header/Header.ios.js +++ b/app/views/ShareListView/Header/Header.ios.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import PropTypes from 'prop-types'; import { Keyboard, StyleSheet, View } from 'react-native'; import ShareExtension from 'rn-extensions-share'; @@ -8,6 +7,7 @@ import * as HeaderButton from '../../../containers/HeaderButton'; import { themes } from '../../../constants/colors'; import sharedStyles from '../../Styles'; import { animateNextTransition } from '../../../utils/layoutAnimation'; +import { IShareListHeaderIos } from './interface'; const styles = StyleSheet.create({ container: { @@ -16,10 +16,10 @@ const styles = StyleSheet.create({ } }); -const Header = React.memo(({ searching, onChangeSearchText, initSearch, cancelSearch, theme }) => { +const Header = React.memo(({ searching, onChangeSearchText, initSearch, cancelSearch, theme }: IShareListHeaderIos) => { const [text, setText] = useState(''); - const onChangeText = searchText => { + const onChangeText = (searchText: string) => { onChangeSearchText(searchText); setText(searchText); }; @@ -59,12 +59,4 @@ const Header = React.memo(({ searching, onChangeSearchText, initSearch, cancelSe ); }); -Header.propTypes = { - searching: PropTypes.bool, - onChangeSearchText: PropTypes.func, - initSearch: PropTypes.func, - cancelSearch: PropTypes.func, - theme: PropTypes.string -}; - export default Header; diff --git a/app/views/ShareListView/Header/Header.android.js b/app/views/ShareListView/Header/Header.tsx similarity index 86% rename from app/views/ShareListView/Header/Header.android.js rename to app/views/ShareListView/Header/Header.tsx index 727fa5364..616f3f487 100644 --- a/app/views/ShareListView/Header/Header.android.js +++ b/app/views/ShareListView/Header/Header.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; -import PropTypes from 'prop-types'; import TextInput from '../../../presentation/TextInput'; import I18n from '../../../i18n'; import { themes } from '../../../constants/colors'; import sharedStyles from '../../Styles'; +import { IShareListHeader } from './interface'; const styles = StyleSheet.create({ container: { @@ -24,7 +24,7 @@ const styles = StyleSheet.create({ } }); -const Header = React.memo(({ searching, onChangeSearchText, theme }) => { +const Header = React.memo(({ searching, onChangeSearchText, theme }: IShareListHeader) => { const titleColorStyle = { color: themes[theme].headerTintColor }; const isLight = theme === 'light'; if (searching) { @@ -43,10 +43,4 @@ const Header = React.memo(({ searching, onChangeSearchText, theme }) => { return {I18n.t('Send_to')}; }); -Header.propTypes = { - searching: PropTypes.bool, - onChangeSearchText: PropTypes.func, - theme: PropTypes.string -}; - export default Header; diff --git a/app/views/ShareListView/Header/index.js b/app/views/ShareListView/Header/index.tsx similarity index 52% rename from app/views/ShareListView/Header/index.js rename to app/views/ShareListView/Header/index.tsx index d66c9a804..e1feab435 100644 --- a/app/views/ShareListView/Header/index.js +++ b/app/views/ShareListView/Header/index.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import PropTypes from 'prop-types'; import Header from './Header'; +import { IShareListHeader } from './interface'; -const ShareListHeader = React.memo(({ searching, initSearch, cancelSearch, search, theme }) => { - const onSearchChangeText = text => { - search(text.trim()); +const ShareListHeader = React.memo(({ searching, initSearch, cancelSearch, onChangeSearchText, theme }: IShareListHeader) => { + const onSearchChangeText = (text: string) => { + onChangeSearchText(text.trim()); }; return ( @@ -19,12 +19,4 @@ const ShareListHeader = React.memo(({ searching, initSearch, cancelSearch, searc ); }); -ShareListHeader.propTypes = { - searching: PropTypes.bool, - initSearch: PropTypes.func, - cancelSearch: PropTypes.func, - search: PropTypes.func, - theme: PropTypes.string -}; - export default ShareListHeader; diff --git a/app/views/ShareListView/Header/interface.ts b/app/views/ShareListView/Header/interface.ts new file mode 100644 index 000000000..25266fb59 --- /dev/null +++ b/app/views/ShareListView/Header/interface.ts @@ -0,0 +1,13 @@ +import { TextInputProps } from 'react-native'; + +type RequiredOnChangeText = Required>; + +export interface IShareListHeader { + searching: boolean; + onChangeSearchText: RequiredOnChangeText['onChangeText']; + theme: string; + initSearch?: () => void; + cancelSearch?: () => void; +} + +export type IShareListHeaderIos = Required; diff --git a/app/views/ShareListView/index.js b/app/views/ShareListView/index.tsx similarity index 79% rename from app/views/ShareListView/index.js rename to app/views/ShareListView/index.tsx index e0a82a50d..1c9f0e415 100644 --- a/app/views/ShareListView/index.js +++ b/app/views/ShareListView/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { BackHandler, FlatList, Keyboard, PermissionsAndroid, ScrollView, Text, View } from 'react-native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { BackHandler, FlatList, Keyboard, PermissionsAndroid, ScrollView, Text, View, Rationale } from 'react-native'; import ShareExtension from 'rn-extensions-share'; import * as FileSystem from 'expo-file-system'; import { connect } from 'react-redux'; @@ -25,24 +25,75 @@ import { sanitizeLikeString } from '../../lib/database/utils'; import styles from './styles'; import ShareListHeader from './Header'; -const permission = { +interface IFile { + value: string; + type: string; +} + +interface IAttachment { + filename: string; + description: string; + size: number; + mime: any; + path: string; +} + +interface IChat { + rid: string; + t: string; + name: string; + fname: string; + blocked: boolean; + blocker: boolean; + prid: string; + uids: string[]; + usernames: string[]; + topic: string; + description: string; +} + +interface IServerInfo { + useRealName: boolean; +} +interface IState { + searching: boolean; + searchText: string; + searchResults: IChat[]; + chats: IChat[]; + serversCount: number; + attachments: IAttachment[]; + text: string; + loading: boolean; + serverInfo: IServerInfo; + needsPermission: boolean; +} + +interface INavigationOption { + navigation: StackNavigationProp; +} + +interface IShareListViewProps extends INavigationOption { + server: string; + token: string; + userId: string; + theme: string; +} + +const permission: Rationale = { title: I18n.t('Read_External_Permission'), - message: I18n.t('Read_External_Permission_Message') + message: I18n.t('Read_External_Permission_Message'), + buttonPositive: 'Ok' }; -const getItemLayout = (data, index) => ({ length: data.length, offset: ROW_HEIGHT * index, index }); -const keyExtractor = item => item.rid; +const getItemLayout = (data: any, index: number) => ({ length: data.length, offset: ROW_HEIGHT * index, index }); +const keyExtractor = (item: IChat) => item.rid; -class ShareListView extends React.Component { - static propTypes = { - navigation: PropTypes.object, - server: PropTypes.string, - token: PropTypes.string, - userId: PropTypes.string, - theme: PropTypes.string - }; +class ShareListView extends React.Component { + private unsubscribeFocus: (() => void) | undefined; - constructor(props) { + private unsubscribeBlur: (() => void) | undefined; + + constructor(props: IShareListViewProps) { super(props); this.state = { searching: false, @@ -53,7 +104,7 @@ class ShareListView extends React.Component { attachments: [], text: '', loading: true, - serverInfo: null, + serverInfo: {} as IServerInfo, needsPermission: isAndroid || false }; this.setHeader(); @@ -70,7 +121,7 @@ class ShareListView extends React.Component { async componentDidMount() { const { server } = this.props; try { - const data = await ShareExtension.data(); + const data = (await ShareExtension.data()) as IFile[]; if (isAndroid) { await this.askForPermission(data); } @@ -85,7 +136,7 @@ class ShareListView extends React.Component { size: file.size, mime: mime.lookup(file.uri), path: file.uri - })); + })) as IAttachment[]; const text = data.filter(item => item.type === 'text').reduce((acc, item) => `${item.value}\n${acc}`, ''); this.setState({ text, @@ -98,14 +149,14 @@ class ShareListView extends React.Component { this.getSubscriptions(server); } - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: IShareListViewProps) { const { server } = this.props; if (nextProps.server !== server) { this.getSubscriptions(nextProps.server); } } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: IShareListViewProps, nextState: IState) { const { searching, needsPermission } = this.state; if (nextState.searching !== searching) { return true; @@ -151,7 +202,7 @@ class ShareListView extends React.Component { searching={searching} initSearch={this.initSearch} cancelSearch={this.cancelSearch} - search={this.search} + onChangeSearchText={this.search} theme={theme} /> ) @@ -168,7 +219,7 @@ class ShareListView extends React.Component { ) : ( ), - headerTitle: () => , + headerTitle: () => , headerRight: () => searching ? null : ( @@ -178,16 +229,16 @@ class ShareListView extends React.Component { }); }; - // eslint-disable-next-line react/sort-comp - internalSetState = (...args) => { + internalSetState = (...args: object[]) => { const { navigation } = this.props; if (navigation.isFocused()) { animateNextTransition(); } + // @ts-ignore this.setState(...args); }; - query = async text => { + query = async (text?: string) => { const db = database.active; const defaultWhereClause = [ Q.where('archived', false), @@ -195,15 +246,16 @@ class ShareListView extends React.Component { Q.experimentalSkip(0), Q.experimentalTake(20), Q.experimentalSortBy('room_updated_at', Q.desc) - ]; + ] as (Q.WhereDescription | Q.Skip | Q.Take | Q.SortBy | Q.Or)[]; if (text) { const likeString = sanitizeLikeString(text); defaultWhereClause.push(Q.or(Q.where('name', Q.like(`%${likeString}%`)), Q.where('fname', Q.like(`%${likeString}%`)))); } - const data = await db + const data = (await db .get('subscriptions') .query(...defaultWhereClause) - .fetch(); + .fetch()) as IChat[]; + return data.map(item => ({ rid: item.rid, t: item.t, @@ -218,7 +270,7 @@ class ShareListView extends React.Component { })); }; - getSubscriptions = async server => { + getSubscriptions = async (server: string) => { const serversDB = database.servers; if (server) { @@ -242,7 +294,7 @@ class ShareListView extends React.Component { } }; - askForPermission = async data => { + askForPermission = async (data: IFile[]) => { const mediaIndex = data.findIndex(item => item.type === 'media'); if (mediaIndex !== -1) { const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, permission); @@ -255,15 +307,14 @@ class ShareListView extends React.Component { return Promise.resolve(); }; - uriToPath = uri => decodeURIComponent(isIOS ? uri.replace(/^file:\/\//, '') : uri); + uriToPath = (uri: string) => decodeURIComponent(isIOS ? uri.replace(/^file:\/\//, '') : uri); - getRoomTitle = item => { + getRoomTitle = (item: IChat) => { const { serverInfo } = this.state; - const { useRealName } = serverInfo; - return ((item.prid || useRealName) && item.fname) || item.name; + return ((item.prid || serverInfo?.useRealName) && item.fname) || item.name; }; - shareMessage = room => { + shareMessage = (room: IChat) => { const { attachments, text, serverInfo } = this.state; const { navigation } = this.props; @@ -276,7 +327,7 @@ class ShareListView extends React.Component { }); }; - search = async text => { + search = async (text: string) => { const result = await this.query(text); this.internalSetState({ searchResults: result, @@ -303,7 +354,7 @@ class ShareListView extends React.Component { return false; }; - renderSectionHeader = header => { + renderSectionHeader = (header: string) => { const { searching } = this.state; const { theme } = this.props; if (searching) { @@ -320,10 +371,9 @@ class ShareListView extends React.Component { ); }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: IChat }) => { const { serverInfo } = this.state; - const { useRealName } = serverInfo; - const { userId, token, server, theme } = this.props; + const { theme } = this.props; let description; switch (item.t) { case 'c': @@ -333,7 +383,7 @@ class ShareListView extends React.Component { description = item.topic || item.description; break; case 'd': - description = useRealName ? item.name : item.fname; + description = serverInfo?.useRealName ? item.name : item.fname; break; default: description = item.fname; @@ -341,12 +391,7 @@ class ShareListView extends React.Component { } return ( ({ +const mapStateToProps = ({ share }: any) => ({ userId: share.user && share.user.id, token: share.user && share.user.token, server: share.server.server diff --git a/app/views/ShareListView/styles.js b/app/views/ShareListView/styles.ts similarity index 100% rename from app/views/ShareListView/styles.js rename to app/views/ShareListView/styles.ts diff --git a/app/views/ShareView/index.tsx b/app/views/ShareView/index.tsx index e10b21483..a3ad287aa 100644 --- a/app/views/ShareView/index.tsx +++ b/app/views/ShareView/index.tsx @@ -4,7 +4,9 @@ import { RouteProp } from '@react-navigation/native'; import { NativeModules, Text, View } from 'react-native'; import { connect } from 'react-redux'; import ShareExtension from 'rn-extensions-share'; +import { Q } from '@nozbe/watermelondb'; +import { InsideStackParamList } from '../../stacks/types'; import { themes } from '../../constants/colors'; import I18n from '../../i18n'; import Loading from '../../containers/Loading'; @@ -25,7 +27,8 @@ import Thumbs from './Thumbs'; import Preview from './Preview'; import Header from './Header'; import styles from './styles'; -import { IAttachment, IServer } from './interfaces'; +import { IAttachment } from './interfaces'; +import { IRoom } from '../../definitions/IRoom'; interface IShareViewState { selected: IAttachment; @@ -33,30 +36,15 @@ interface IShareViewState { readOnly: boolean; attachments: IAttachment[]; text: string; - // TODO: Refactor when migrate room - room: any; - thread: any; + room: IRoom; + thread: any; // change maxFileSize: number; mediaAllowList: number; } interface IShareViewProps { - // TODO: Refactor after react-navigation - navigation: StackNavigationProp; - route: RouteProp< - { - ShareView: { - attachments: IAttachment[]; - isShareView?: boolean; - isShareExtension: boolean; - serverInfo: IServer; - text: string; - room: any; - thread: any; // change - }; - }, - 'ShareView' - >; + navigation: StackNavigationProp; + route: RouteProp; theme: string; user: { id: string; @@ -154,6 +142,17 @@ class ShareView extends Component { } }; + getPermissionMobileUpload = async () => { + const { room } = this.state; + const db = database.active; + const permissionsCollection = db.get('permissions'); + const uploadFilePermissionFetch = await permissionsCollection.query(Q.where('id', Q.like('mobile-upload-file'))).fetch(); + const uploadFilePermission = uploadFilePermissionFetch[0]?.roles; + const permissionToUpload = await RocketChat.hasPermission([uploadFilePermission], room.rid); + // uploadFilePermission as undefined is considered that there isn't this permission, so all can upload file. + return !uploadFilePermission || permissionToUpload[0]; + }; + getReadOnly = async () => { const { room } = this.state; const { user } = this.props; @@ -163,10 +162,12 @@ class ShareView extends Component { getAttachments = async () => { const { mediaAllowList, maxFileSize } = this.state; + const permissionToUploadFile = await this.getPermissionMobileUpload(); + const items = await Promise.all( this.files.map(async item => { // Check server settings - const { success: canUpload, error } = canUploadFile(item, mediaAllowList, maxFileSize); + const { success: canUpload, error } = canUploadFile(item, mediaAllowList, maxFileSize, permissionToUploadFile); item.canUpload = canUpload; item.error = error; diff --git a/app/views/ShareView/interfaces.ts b/app/views/ShareView/interfaces.ts index 09cb4d9eb..a2231450d 100644 --- a/app/views/ShareView/interfaces.ts +++ b/app/views/ShareView/interfaces.ts @@ -13,21 +13,3 @@ export interface IUseDimensions { width: number; height: number; } - -// TODO: move this to specific folder -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 | null; - biometry: boolean | null; - uniqueID: string; - enterpriseModules: string; - E2E_Enable: boolean; -} diff --git a/app/views/SidebarView/SidebarItem.tsx b/app/views/SidebarView/SidebarItem.tsx index 7590e82ca..bfbf2d2db 100644 --- a/app/views/SidebarView/SidebarItem.tsx +++ b/app/views/SidebarView/SidebarItem.tsx @@ -25,7 +25,7 @@ const Item = React.memo(({ left, right, text, onPress, testID, current, theme }: style={[styles.item, current && { backgroundColor: themes[theme].borderColor }]}>
(Component: React.ComponentType
) => +export const withActionSheet = (Component: any): any => forwardRef((props: any, ref: ForwardedRef) => ( {(contexts: any) => } )); diff --git a/app/containers/Button/index.tsx b/app/containers/Button/index.tsx index 9e475a679..8c99dccee 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 64f5dbfe3..12217cf95 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 2d4c45b6f..f757d43d7 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 bf175dd70..aab5c889e 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/MessageBox/EmojiKeyboard.tsx b/app/containers/MessageBox/EmojiKeyboard.tsx index bbb0e20ad..91acc45d1 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 fa6c509ef..e219e6423 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/index.tsx b/app/containers/Passcode/Base/index.tsx index dd1d90e8d..c65917706 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 cc284b24a..0a9b6b1f9 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 4a08c91ce..6668e0f76 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 e62bc8063..dd780bbdd 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 b9aaf9620..9d4d005e0 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/definitions/IAttachment.ts b/app/definitions/IAttachment.ts new file mode 100644 index 000000000..168106177 --- /dev/null +++ b/app/definitions/IAttachment.ts @@ -0,0 +1,10 @@ +export interface IAttachment { + title: string; + type: string; + description: string; + title_link?: string; + image_url?: string; + image_type?: string; + video_url?: string; + video_type?: string; +} diff --git a/app/definitions/IMessage.ts b/app/definitions/IMessage.ts new file mode 100644 index 000000000..aca651c10 --- /dev/null +++ b/app/definitions/IMessage.ts @@ -0,0 +1,3 @@ +export interface IMessage { + msg: string; +} diff --git a/app/definitions/IRocketChatRecord.ts b/app/definitions/IRocketChatRecord.ts new file mode 100644 index 000000000..48d91fa84 --- /dev/null +++ b/app/definitions/IRocketChatRecord.ts @@ -0,0 +1,4 @@ +export interface IRocketChatRecord { + id: string; + updatedAt: Date; +} diff --git a/app/definitions/IRoom.ts b/app/definitions/IRoom.ts new file mode 100644 index 000000000..786c1d7c8 --- /dev/null +++ b/app/definitions/IRoom.ts @@ -0,0 +1,27 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export enum RoomType { + GROUP = 'p', + DIRECT = 'd', + CHANNEL = 'c', + OMNICHANNEL = 'l', + THREAD = 'thread' +} + +export interface IRoom extends IRocketChatRecord { + rid: string; + t: RoomType; + name: string; + fname: string; + prid?: string; + tmid?: string; + topic?: string; + teamMain?: boolean; + teamId?: string; + encrypted?: boolean; + visitor?: boolean; + autoTranslateLanguage?: boolean; + autoTranslate?: boolean; + observe?: Function; + usedCannedResponse: string; +} diff --git a/app/definitions/IServer.ts b/app/definitions/IServer.ts new file mode 100644 index 000000000..534a29c9c --- /dev/null +++ b/app/definitions/IServer.ts @@ -0,0 +1,16 @@ +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 | null; + biometry: boolean | null; + uniqueID: string; + enterpriseModules: string; + E2E_Enable: boolean; +} 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 10919715d..8cf8bddce 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/dimensions.tsx b/app/dimensions.tsx index dc164362a..676009683 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 00d320828..d10d5c892 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 defe9233f..5d537ceef 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/i18n/locales/ar.json b/app/i18n/locales/ar.json index 896d4d0d1..0f6eebaa9 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 5a6fe2bda..8bb4dee5a 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 18dd52bc5..e1d66b3dc 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", diff --git a/app/i18n/locales/fr.json b/app/i18n/locales/fr.json index 811486b06..81e8e21b3 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 7b1f1f666..5e9e4f9f7 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 df4e3aafd..aa63fdb74 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 24905e9f0..94e22a631 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", diff --git a/app/i18n/locales/pt-PT.json b/app/i18n/locales/pt-PT.json index f3d23c51e..f2cd45e7b 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 071e8f3e4..bdf2c161a 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 aeb4caba8..cc5651927 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 e01368904..24e166b9d 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 85d2690ca..258f8fdf7 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/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index 589aa6bd1..b680a9196 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 4dd84adfc..c2fc9fcdf 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 fac6d2c1f..239487b1b 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', () => { @@ -1141,10 +1138,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`); @@ -1523,16 +1516,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/navigationTypes.ts b/app/navigationTypes.ts new file mode 100644 index 000000000..568b75d0f --- /dev/null +++ b/app/navigationTypes.ts @@ -0,0 +1,45 @@ +import { NavigatorScreenParams } from '@react-navigation/core'; + +import { IRoom } from './definitions/IRoom'; +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: IRoom; + thread: any; // TODO: Change + }; + SelectServerView: undefined; +}; + +export type ShareOutsideStackParamList = { + WithoutServersView: undefined; +}; + +export type ShareAppStackParamList = { + AuthLoading?: undefined; + OutsideStack?: NavigatorScreenParams; + InsideStack?: NavigatorScreenParams; +}; diff --git a/app/presentation/DirectoryItem/index.tsx b/app/presentation/DirectoryItem/index.tsx index b8d9811a8..234c1e312 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 5319df82b..aa4f1182d 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/UserItem.tsx b/app/presentation/UserItem.tsx index 3dadc2bf7..b2e9d0b16 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/share.tsx b/app/share.tsx index ceb85477d..daee2fba0 100644 --- a/app/share.tsx +++ b/app/share.tsx @@ -25,6 +25,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; @@ -46,7 +47,7 @@ interface IState { fontScale: number; } -const Inside = createStackNavigator(); +const Inside = createStackNavigator(); const InsideStack = () => { const { theme } = useContext(ThemeContext); @@ -65,24 +66,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 +108,7 @@ class Root extends React.Component<{}, IState> { this.init(); } - componentWillUnmount() { + componentWillUnmount(): void { RocketChat.closeShareExtension(); unsubscribeTheme(); } diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.tsx similarity index 81% rename from app/stacks/InsideStack.js rename to app/stacks/InsideStack.tsx index b3de1b610..fce844a4a 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'; @@ -75,20 +66,33 @@ import QueueListView from '../ee/omnichannel/views/QueueListView'; import AddChannelTeamView from '../views/AddChannelTeamView'; import AddExistingChannelView from '../views/AddExistingChannelView'; import SelectListView from '../views/SelectListView'; +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 ( - + - + { 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); @@ -257,12 +240,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.tsx similarity index 60% rename from app/stacks/MasterDetailStack/ModalContainer.js rename to app/stacks/MasterDetailStack/ModalContainer.tsx index 7be11f8c7..aeea3d88c 100644 --- a/app/stacks/MasterDetailStack/ModalContainer.js +++ b/app/stacks/MasterDetailStack/ModalContainer.tsx @@ -1,10 +1,17 @@ import React from 'react'; import { StyleSheet, TouchableWithoutFeedback, View } from 'react-native'; -import PropTypes from 'prop-types'; +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, @@ -12,11 +19,11 @@ const styles = StyleSheet.create({ justifyContent: 'center' }, backdrop: { - ...StyleSheet.absoluteFill + ...StyleSheet.absoluteFillObject } }); -export const ModalContainer = ({ navigation, children, theme }) => ( +export const ModalContainer = ({ navigation, children, theme }: IModalContainer): JSX.Element => ( navigation.pop()}> @@ -24,9 +31,3 @@ export const ModalContainer = ({ navigation, children, theme }) => ( {children} ); - -ModalContainer.propTypes = { - navigation: PropTypes.object, - children: PropTypes.element, - theme: PropTypes.string -}; 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 71828a6a2..4a945fc0e 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'; @@ -63,9 +59,15 @@ import AddChannelTeamView from '../../views/AddChannelTeamView'; import AddExistingChannelView from '../../views/AddExistingChannelView'; import SelectListView from '../../views/SelectListView'; 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); @@ -82,28 +84,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} /> - - - + + @@ -226,21 +218,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 000000000..202ad6014 --- /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 { IRoom, RoomType } from '../../definitions/IRoom'; + +export type MasterDetailChatsStackParamList = { + RoomView: { + rid: string; + t: RoomType; + tmid?: string; + message?: string; + name?: string; + fname?: string; + prid?: string; + room: IRoom; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; + }; +}; + +export type MasterDetailDrawerParamList = { + ChatsStackNavigator: NavigatorScreenParams; +}; + +export type ModalStackParamList = { + RoomActionsView: { + room: IRoom; + member: any; + rid: string; + t: RoomType; + joined: boolean; + }; + RoomInfoView: { + room: IRoom; + member: any; + rid: string; + t: RoomType; + }; + SelectListView: { + data: any; + title: string; + infoText: string; + nextAction: Function; + showAlert: boolean; + isSearch: boolean; + onSearch: Function; + isRadio?: boolean; + }; + RoomInfoEditView: { + rid: string; + }; + RoomMembersView: { + rid: string; + room: IRoom; + }; + SearchMessagesView: { + rid: string; + t: RoomType; + 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: RoomType; + name: string; + }; + AutoTranslateView: { + rid: string; + room: IRoom; + }; + DirectoryView: undefined; + QueueListView: undefined; + NotificationPrefView: { + rid: string; + room: IRoom; + }; + ForwardLivechatView: { + rid: string; + }; + CannedResponsesListView: { + rid: string; + }; + CannedResponseDetail: { + cannedResponse: { + shortcut: string; + text: string; + scopeName: string; + tags: string[]; + }; + room: IRoom; + }; + LivechatEditView: { + room: IRoom; + 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: RoomType; + }; + 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: IRoom; + 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: IRoom; + thread: any; // TODO: Change + }; +}; diff --git a/app/stacks/OutsideStack.js b/app/stacks/OutsideStack.tsx similarity index 82% rename from app/stacks/OutsideStack.js rename to app/stacks/OutsideStack.tsx index a61411c6b..10f9bd63a 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 000000000..3107c69ce --- /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 { IRoom, RoomType } from '../definitions/IRoom'; + +export type ChatsStackParamList = { + RoomsListView: undefined; + RoomView: { + rid: string; + t: RoomType; + tmid?: string; + message?: string; + name?: string; + fname?: string; + prid?: string; + room: IRoom; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; + }; + RoomActionsView: { + room: IRoom; + member: any; + rid: string; + t: RoomType; + joined: boolean; + }; + SelectListView: { + data: any; + title: string; + infoText: string; + nextAction: Function; + showAlert: boolean; + isSearch: boolean; + onSearch: Function; + isRadio?: boolean; + }; + RoomInfoView: { + room: IRoom; + member: any; + rid: string; + t: RoomType; + }; + RoomInfoEditView: { + rid: string; + }; + RoomMembersView: { + rid: string; + room: IRoom; + }; + SearchMessagesView: { + rid: string; + t: RoomType; + 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: RoomType; + name: string; + }; + AutoTranslateView: { + rid: string; + room: IRoom; + }; + DirectoryView: undefined; + NotificationPrefView: { + rid: string; + room: Model; + }; + ForwardLivechatView: { + rid: string; + }; + LivechatEditView: { + room: IRoom; + 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: RoomType; + }; + 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: IRoom; + }; +}; + +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: IRoom; + 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: IRoom; + 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 4accff2cd..635ffda1c 100644 --- a/app/theme.tsx +++ b/app/theme.tsx @@ -12,7 +12,7 @@ interface IThemeContextProps { export const ThemeContext = React.createContext({ theme: 'light' }); -export function withTheme(Component: any) { +export function withTheme(Component: any): any { const ThemedComponent = (props: any) => ( {contexts => } ); diff --git a/app/utils/fileDownload/index.ts b/app/utils/fileDownload/index.ts index dda1a78ff..279d3b3a5 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/log/events.js b/app/utils/log/events.js index fc7a3497a..82dd3079d 100644 --- a/app/utils/log/events.js +++ b/app/utils/log/events.js @@ -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/media.js b/app/utils/media.js index b05f95a94..07f6f58d7 100644 --- a/app/utils/media.js +++ b/app/utils/media.js @@ -1,10 +1,13 @@ -export const canUploadFile = (file, allowList, maxFileSize) => { +export const canUploadFile = (file, allowList, maxFileSize, permissionToUploadFile) => { 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 }; diff --git a/app/utils/sslPinning.js b/app/utils/sslPinning.js index 50f944e63..c3e2128c9 100644 --- a/app/utils/sslPinning.js +++ b/app/utils/sslPinning.js @@ -7,6 +7,21 @@ import I18n from '../i18n'; import { extractHostname } from './server'; const { SSLPinning } = NativeModules; +const { documentDirectory } = FileSystem; + +const extractFileScheme = path => path.replace('file://', ''); // file:// isn't allowed by obj-C + +const getPath = name => `${documentDirectory}/${name}`; + +const persistCertificate = async (name, password) => { + const certificatePath = getPath(name); + const certificate = { + path: extractFileScheme(certificatePath), + password + }; + await UserPreferences.setMapAsync(name, certificate); + return certificate; +}; const RCSSLPinning = Platform.select({ ios: { @@ -25,17 +40,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 +56,19 @@ const RCSSLPinning = Platform.select({ reject(e); } }), - setCertificate: async (alias, server) => { - if (alias) { - const certificate = await UserPreferences.getMapAsync(alias); + setCertificate: async (name, server) => { + if (name) { + let certificate = await UserPreferences.getMapAsync(name); + 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/views/AddChannelTeamView.tsx b/app/views/AddChannelTeamView.tsx index d477f9bad..8a72d3c96 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 5efdbf34d..86ab9b9cf 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 80f728e12..f0af5dfa0 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 90adf8b42..09e2d5d61 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; diff --git a/app/views/AuthenticationWebView.tsx b/app/views/AuthenticationWebView.tsx index 870af9560..ac304fbfb 100644 --- a/app/views/AuthenticationWebView.tsx +++ b/app/views/AuthenticationWebView.tsx @@ -4,7 +4,9 @@ import { connect } from 'react-redux'; import parse from 'url-parse'; import { StackNavigationProp } from '@react-navigation/stack'; import { WebViewMessage } from 'react-native-webview/lib/WebViewTypes'; +import { RouteProp } from '@react-navigation/core'; +import { OutsideModalParamList } from '../stacks/types'; import RocketChat from '../lib/rocketchat'; import { isIOS } from '../utils/deviceInfo'; import StatusBar from '../containers/StatusBar'; @@ -41,17 +43,9 @@ window.addEventListener('popstate', function() { }); `; -interface IRoute { - params: { - authType: string; - url: string; - ssoToken?: string; - }; -} - interface INavigationOption { - navigation: StackNavigationProp; - 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 92a77543a..f840ca12a 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 { IRoom } from '../../definitions/IRoom'; 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; } diff --git a/app/views/CreateChannelView.tsx b/app/views/CreateChannelView.tsx index 45b2cc2f2..e8d719ab4 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 { ) : null, headerLeft: showCloseModal ? () => : undefined - }); + } as StackNavigationOptions); }; submit = () => { diff --git a/app/views/CreateDiscussionView/interfaces.ts b/app/views/CreateDiscussionView/interfaces.ts index 468833119..e9d076b16 100644 --- a/app/views/CreateDiscussionView/interfaces.ts +++ b/app/views/CreateDiscussionView/interfaces.ts @@ -1,14 +1,11 @@ +import { RouteProp } from '@react-navigation/core'; +import { StackNavigationProp } from '@react-navigation/stack'; + +import { NewMessageStackParamList } from '../../stacks/types'; + export interface ICreateChannelViewProps { - navigation: any; - route: { - params?: { - channel: string; - message: { - msg: string; - }; - showCloseModal: boolean; - }; - }; + navigation: StackNavigationProp; + route: RouteProp; server: string; user: { id: string; diff --git a/app/views/DirectoryView/Options.tsx b/app/views/DirectoryView/Options.tsx index fcc0f7bf6..112068061 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/E2EEnterYourPasswordView.tsx b/app/views/E2EEnterYourPasswordView.tsx index dd9cdfa8a..6d63f90dd 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 0fbdf77a1..fce1a2d0c 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 1c4e13a5a..3d9a32ee1 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 c08a1acdd..375d089d4 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 42a29782d..ea17466dd 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 62ce51212..4ae1a67df 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 cfcd3fa11..b7bf30710 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 44034cda2..aa6658d20 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 4643687e2..e43505f3f 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 a65994eef..e260199e0 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 a948edcd1..14532051b 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 { IRoom, RoomType } from '../../definitions/IRoom'; 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: IRoom; + member: any; + rid: string; + t: RoomType; + 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: RoomType; tmid?: string; + message?: string; name?: string; + fname?: string; + prid?: string; + room: IRoom; + jumpToMessageId?: string; + jumpToThreadId?: string; + roomUserId?: string; } class MessagesView extends React.Component { - private rid?: string; - private t?: string; + private rid: string; + private t: RoomType; 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: RoomType.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 c87bf3317..1a517745a 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 4cf92e094..cd1822513 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.tsx @@ -1,6 +1,7 @@ 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'; @@ -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/index.tsx b/app/views/NewServerView/index.tsx index f1458c93a..aaacdf90f 100644 --- a/app/views/NewServerView/index.tsx +++ b/app/views/NewServerView/index.tsx @@ -33,6 +33,7 @@ import { isTablet } from '../../utils/deviceInfo'; import { verticalScale, moderateScale } from '../../utils/scaling'; import { withDimensions } from '../../dimensions'; import ServerInput from './ServerInput'; +import { OutsideParamList } from '../../stacks/types'; const styles = StyleSheet.create({ onboardingImage: { @@ -73,7 +74,7 @@ export interface IServer extends Model { } interface INewServerView { - navigation: StackNavigationProp; + navigation: StackNavigationProp; theme: string; connecting: boolean; connectServer(server: string, username?: string, fromServerHistory?: boolean): void; diff --git a/app/views/NotificationPreferencesView/index.tsx b/app/views/NotificationPreferencesView/index.tsx index a020c1631..5e33cec49 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 4035c0380..a2b3251c6 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 002979b20..db2a7a265 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()} item.value} + keyExtractor={item => item.value as string} renderItem={({ item }) => ( this.onChangeValue(item.value)} + onItemPress={() => this.onChangeValue(item.value as string)} /> )} ItemSeparatorComponent={List.Separator} diff --git a/app/views/ProfileView/interfaces.ts b/app/views/ProfileView/interfaces.ts index 00117203e..bfec50c0d 100644 --- a/app/views/ProfileView/interfaces.ts +++ b/app/views/ProfileView/interfaces.ts @@ -1,6 +1,8 @@ import { StackNavigationProp } from '@react-navigation/stack'; import React from 'react'; +import { ProfileStackParamList } from '../../stacks/types'; + export interface IUser { id: string; name: string; @@ -31,14 +33,12 @@ export interface IAvatarButton { } export interface INavigationOptions { - navigation: StackNavigationProp; + navigation: StackNavigationProp; isMasterDetail?: boolean; } export interface IProfileViewProps { user: IUser; - navigation: StackNavigationProp; - isMasterDetail?: boolean; baseUrl: string; Accounts_AllowEmailChange: boolean; Accounts_AllowPasswordChange: boolean; diff --git a/app/views/ReadReceiptView/index.tsx b/app/views/ReadReceiptView/index.tsx index 9f1a00675..a40327b21 100644 --- a/app/views/ReadReceiptView/index.tsx +++ b/app/views/ReadReceiptView/index.tsx @@ -16,6 +16,7 @@ import { withTheme } from '../../theme'; import { themes } from '../../constants/colors'; import SafeAreaView from '../../containers/SafeAreaView'; import styles from './styles'; +import { ChatsStackParamList } from '../../stacks/types'; interface IReceipts { _id: string; @@ -36,8 +37,8 @@ interface IReadReceiptViewState { } interface INavigationOption { - navigation: StackNavigationProp; - route: RouteProp<{ ReadReceiptView: { messageId: string } }, 'ReadReceiptView'>; + navigation: StackNavigationProp; + route: RouteProp; isMasterDetail: boolean; } diff --git a/app/views/RegisterView.tsx b/app/views/RegisterView.tsx index 045712163..ae5f46bd9 100644 --- a/app/views/RegisterView.tsx +++ b/app/views/RegisterView.tsx @@ -5,6 +5,7 @@ import { RouteProp } from '@react-navigation/core'; import { connect } from 'react-redux'; import RNPickerSelect from 'react-native-picker-select'; +import { OutsideParamList } from '../stacks/types'; import log, { events, logEvent } from '../utils/log'; import Button from '../containers/Button'; import I18n from '../i18n'; @@ -51,8 +52,8 @@ const styles = StyleSheet.create({ }); interface IProps { - navigation: StackNavigationProp; - route: RouteProp; + navigation: StackNavigationProp; + route: RouteProp; server: string; Site_Name: string; Gitlab_URL: string; diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index c3bd105af..cb034bf3d 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -918,7 +918,6 @@ class RoomActionsView extends React.Component { event: this.convertTeamToChannel }) } - testID='room-actions-convert-channel-to-team' left={() => } showActionIndicator /> @@ -1198,23 +1197,6 @@ class RoomActionsView extends React.Component { > ) : null} - - {['l'].includes(t) && !this.isOmnichannelPreview ? ( - <> - - this.onPressTouchable({ - route: 'VisitorNavigationView', - params: { rid } - }) - } - left={() => } - showActionIndicator - /> - - > - ) : null} {this.renderLastSection()} diff --git a/app/views/ScreenLockConfigView.js b/app/views/ScreenLockConfigView.tsx similarity index 84% rename from app/views/ScreenLockConfigView.js rename to app/views/ScreenLockConfigView.tsx index 8e7de6cae..57638f417 100644 --- a/app/views/ScreenLockConfigView.js +++ b/app/views/ScreenLockConfigView.tsx @@ -1,7 +1,9 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Switch } from 'react-native'; import { connect } from 'react-redux'; +import { StackNavigationOptions } from '@react-navigation/stack'; +import Model from '@nozbe/watermelondb/Model'; +import { Subscription } from 'rxjs'; import I18n from '../i18n'; import { withTheme } from '../theme'; @@ -16,19 +18,42 @@ import { events, logEvent } from '../utils/log'; const DEFAULT_BIOMETRY = false; -class ScreenLockConfigView extends React.Component { - static navigationOptions = () => ({ +interface IServerRecords extends Model { + autoLock?: boolean; + autoLockTime?: number; + biometry?: boolean; +} + +interface IItem { + title: string; + value: number; + disabled?: boolean; +} + +interface IScreenLockConfigViewProps { + theme: string; + server: string; + Force_Screen_Lock: boolean; + Force_Screen_Lock_After: number; +} + +interface IScreenLockConfigViewState { + autoLock?: boolean; + autoLockTime?: number | null; + biometry?: boolean; + biometryLabel: null; +} + +class ScreenLockConfigView extends React.Component { + private serverRecord?: IServerRecords; + + private observable?: Subscription; + + static navigationOptions = (): StackNavigationOptions => ({ title: I18n.t('Screen_lock') }); - static propTypes = { - theme: PropTypes.string, - server: PropTypes.string, - Force_Screen_Lock: PropTypes.string, - Force_Screen_Lock_After: PropTypes.string - }; - - constructor(props) { + constructor(props: IScreenLockConfigViewProps) { super(props); this.state = { autoLock: false, @@ -104,7 +129,7 @@ class ScreenLockConfigView extends React.Component { logEvent(events.SLC_SAVE_SCREEN_LOCK); const { autoLock, autoLockTime, biometry } = this.state; const serversDB = database.servers; - await serversDB.action(async () => { + await serversDB.write(async () => { await this.serverRecord?.update(record => { record.autoLock = autoLock; record.autoLockTime = autoLockTime === null ? DEFAULT_AUTO_LOCK : autoLockTime; @@ -113,7 +138,7 @@ class ScreenLockConfigView extends React.Component { }); }; - changePasscode = async ({ force }) => { + changePasscode = async ({ force }: { force: boolean }) => { logEvent(events.SLC_CHANGE_PASSCODE); await changePasscode({ force }); }; @@ -144,12 +169,12 @@ class ScreenLockConfigView extends React.Component { ); }; - isSelected = value => { + isSelected = (value: number) => { const { autoLockTime } = this.state; return autoLockTime === value; }; - changeAutoLockTime = autoLockTime => { + changeAutoLockTime = (autoLockTime: number) => { logEvent(events.SLC_CHANGE_AUTOLOCK_TIME); this.setState({ autoLockTime }, () => this.save()); }; @@ -159,7 +184,7 @@ class ScreenLockConfigView extends React.Component { return ; }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: IItem }) => { const { title, value, disabled } = item; return ( <> @@ -194,7 +219,7 @@ class ScreenLockConfigView extends React.Component { if (!autoLock) { return null; } - let items = this.defaultAutoLockOptions; + let items: IItem[] = this.defaultAutoLockOptions; if (Force_Screen_Lock && Force_Screen_Lock_After > 0) { items = [ { @@ -262,7 +287,7 @@ class ScreenLockConfigView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ server: state.server.server, Force_Screen_Lock: state.settings.Force_Screen_Lock, Force_Screen_Lock_After: state.settings.Force_Screen_Lock_After diff --git a/app/views/ScreenLockedView.js b/app/views/ScreenLockedView.tsx similarity index 76% rename from app/views/ScreenLockedView.js rename to app/views/ScreenLockedView.tsx index 644e76ff0..6e20152b5 100644 --- a/app/views/ScreenLockedView.js +++ b/app/views/ScreenLockedView.tsx @@ -1,19 +1,25 @@ import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; import Modal from 'react-native-modal'; import useDeepCompareEffect from 'use-deep-compare-effect'; import isEmpty from 'lodash/isEmpty'; import Orientation from 'react-native-orientation-locker'; -import { withTheme } from '../theme'; +import { useTheme } from '../theme'; import EventEmitter from '../utils/events'; import { LOCAL_AUTHENTICATE_EMITTER } from '../constants/localAuthentication'; import { isTablet } from '../utils/deviceInfo'; import { PasscodeEnter } from '../containers/Passcode'; -const ScreenLockedView = ({ theme }) => { +interface IData { + submit?: () => void; + hasBiometry?: boolean; +} + +const ScreenLockedView = (): JSX.Element => { const [visible, setVisible] = useState(false); - const [data, setData] = useState({}); + const [data, setData] = useState({}); + + const { theme } = useTheme(); useDeepCompareEffect(() => { if (!isEmpty(data)) { @@ -23,7 +29,7 @@ const ScreenLockedView = ({ theme }) => { } }, [data]); - const showScreenLock = args => { + const showScreenLock = (args: IData) => { setData(args); }; @@ -56,13 +62,9 @@ const ScreenLockedView = ({ theme }) => { style={{ margin: 0 }} animationIn='fadeIn' animationOut='fadeOut'> - + ); }; -ScreenLockedView.propTypes = { - theme: PropTypes.string -}; - -export default withTheme(ScreenLockedView); +export default ScreenLockedView; diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index a9be99918..a85df6745 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -1,11 +1,13 @@ import React from 'react'; import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; -import { RouteProp } from '@react-navigation/core'; +import { CompositeNavigationProp, RouteProp } from '@react-navigation/core'; import { FlatList, Text, View } from 'react-native'; import { Q } from '@nozbe/watermelondb'; import { connect } from 'react-redux'; import { dequal } from 'dequal'; +import { IRoom, RoomType } from '../../definitions/IRoom'; +import { IAttachment } from '../../definitions/IAttachment'; import RCTextInput from '../../containers/TextInput'; import ActivityIndicator from '../../containers/ActivityIndicator'; import Markdown from '../../containers/markdown'; @@ -13,7 +15,7 @@ import debounce from '../../utils/debounce'; import RocketChat from '../../lib/rocketchat'; import Message from '../../containers/message'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; -import { IMessage, IMessageAttachments } from '../../containers/message/interfaces'; +import { IMessage } from '../../containers/message/interfaces'; import I18n from '../../i18n'; import StatusBar from '../../containers/StatusBar'; import log from '../../utils/log'; @@ -29,26 +31,30 @@ import getRoomInfo from '../../lib/methods/getRoomInfo'; import { isIOS } from '../../utils/deviceInfo'; import { compareServerVersion, methods } from '../../lib/utils'; import styles from './styles'; +import { InsideStackParamList, ChatsStackParamList } from '../../stacks/types'; const QUERY_SIZE = 50; -type TRouteParams = { - SearchMessagesView: { - showCloseModal?: boolean; - rid: string; - t?: string; - encrypted?: boolean; - }; -}; - interface ISearchMessagesViewState { loading: boolean; messages: IMessage[]; searchText: string; } + +interface IRoomInfoParam { + room: IRoom; + member: any; + rid: string; + t: RoomType; + joined: boolean; +} + interface INavigationOption { - navigation: StackNavigationProp; - route: RouteProp; + navigation: CompositeNavigationProp< + StackNavigationProp, + StackNavigationProp + >; + route: RouteProp; } interface ISearchMessagesViewProps extends INavigationOption { @@ -183,12 +189,12 @@ class SearchMessagesView extends React.Component { + showAttachment = (attachment: IAttachment) => { const { navigation } = this.props; navigation.navigate('AttachmentView', { attachment }); }; - navToRoomInfo = (navParam: IMessage) => { + navToRoomInfo = (navParam: IRoomInfoParam) => { const { navigation, user } = this.props; if (navParam.rid === user.id) { return; diff --git a/app/views/SecurityPrivacyView.js b/app/views/SecurityPrivacyView.tsx similarity index 82% rename from app/views/SecurityPrivacyView.js rename to app/views/SecurityPrivacyView.tsx index 5a6adcd48..ffd61d31e 100644 --- a/app/views/SecurityPrivacyView.js +++ b/app/views/SecurityPrivacyView.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Switch } from 'react-native'; -import PropTypes from 'prop-types'; +import { StackNavigationProp } from '@react-navigation/stack'; import AsyncStorage from '@react-native-community/async-storage'; import { useSelector } from 'react-redux'; @@ -20,11 +20,15 @@ import { import SafeAreaView from '../containers/SafeAreaView'; import { isFDroidBuild } from '../constants/environment'; -const SecurityPrivacyView = ({ navigation }) => { +interface ISecurityPrivacyViewProps { + navigation: StackNavigationProp; +} + +const SecurityPrivacyView = ({ navigation }: ISecurityPrivacyViewProps): JSX.Element => { const [crashReportState, setCrashReportState] = useState(getReportCrashErrorsValue()); const [analyticsEventsState, setAnalyticsEventsState] = useState(getReportAnalyticsEventsValue()); - const e2eEnabled = useSelector(state => state.settings.E2E_Enable); + const e2eEnabled = useSelector((state: any) => state.settings.E2E_Enable); useEffect(() => { navigation.setOptions({ @@ -32,21 +36,22 @@ const SecurityPrivacyView = ({ navigation }) => { }); }, []); - const toggleCrashReport = value => { - logEvent(events.SE_TOGGLE_CRASH_REPORT); + const toggleCrashReport = (value: boolean) => { + logEvent(events.SP_TOGGLE_CRASH_REPORT); AsyncStorage.setItem(CRASH_REPORT_KEY, JSON.stringify(value)); setCrashReportState(value); toggleCrashErrorsReport(value); }; - const toggleAnalyticsEvents = value => { - logEvent(events.SE_TOGGLE_ANALYTICS_EVENTS); + const toggleAnalyticsEvents = (value: boolean) => { + logEvent(events.SP_TOGGLE_ANALYTICS_EVENTS); AsyncStorage.setItem(ANALYTICS_EVENTS_KEY, JSON.stringify(value)); setAnalyticsEventsState(value); toggleAnalyticsEventsReport(value); }; - const navigateToScreen = screen => { + const navigateToScreen = (screen: 'E2EEncryptionSecurityView' | 'ScreenLockConfigView') => { + // @ts-ignore logEvent(events[`SP_GO_${screen.replace('View', '').toUpperCase()}`]); navigation.navigate(screen); }; @@ -106,8 +111,4 @@ const SecurityPrivacyView = ({ navigation }) => { ); }; -SecurityPrivacyView.propTypes = { - navigation: PropTypes.object -}; - export default SecurityPrivacyView; diff --git a/app/views/SelectListView.js b/app/views/SelectListView.tsx similarity index 77% rename from app/views/SelectListView.js rename to app/views/SelectListView.tsx index 9d2f533da..e6d67266b 100644 --- a/app/views/SelectListView.js +++ b/app/views/SelectListView.tsx @@ -1,8 +1,9 @@ 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 { connect } from 'react-redux'; import { RadioButton } from 'react-native-ui-lib'; +import { RouteProp } from '@react-navigation/native'; import log from '../utils/log'; import * as List from '../containers/List'; @@ -25,15 +26,58 @@ const styles = StyleSheet.create({ } }); -class SelectListView extends React.Component { - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string, - isMasterDetail: PropTypes.bool - }; +interface IData { + rid: string; + name: string; + t?: string; + teamMain?: boolean; + alert?: boolean; +} - constructor(props) { +interface ISelectListViewState { + data: IData[]; + dataFiltered: IData[]; + isSearching: boolean; + selected: string[]; +} + +interface ISelectListViewProps { + navigation: StackNavigationProp; + route: RouteProp< + { + SelectView: { + data: IData[]; + title: string; + infoText: string; + nextAction(selected: string[]): void; + showAlert(): void; + isSearch: boolean; + onSearch(text: string): IData[]; + isRadio: boolean; + }; + }, + 'SelectView' + >; + theme: string; + isMasterDetail: boolean; +} + +class SelectListView extends React.Component { + private title: string; + + private infoText: string; + + private nextAction: (selected: string[]) => void; + + private showAlert: () => void; + + private isSearch: boolean; + + private onSearch: (text: string) => IData[]; + + private isRadio: boolean; + + constructor(props: ISelectListViewProps) { super(props); const data = props.route?.params?.data; this.title = props.route?.params?.title; @@ -56,7 +100,7 @@ class SelectListView extends React.Component { const { navigation, isMasterDetail } = this.props; const { selected } = this.state; - const options = { + const options: StackNavigationOptions = { headerTitle: I18n.t(this.title) }; @@ -87,7 +131,7 @@ class SelectListView extends React.Component { return ( this.search(text)} + onChangeText={(text: string) => this.search(text)} testID='select-list-view-search' onCancelPress={() => this.setState({ isSearching: false })} /> @@ -95,7 +139,7 @@ class SelectListView extends React.Component { ); }; - search = async text => { + search = async (text: string) => { try { this.setState({ isSearching: true }); const result = await this.onSearch(text); @@ -105,12 +149,12 @@ class SelectListView extends React.Component { } }; - isChecked = rid => { + isChecked = (rid: string) => { const { selected } = this.state; return selected.includes(rid); }; - toggleItem = rid => { + toggleItem = (rid: string) => { const { selected } = this.state; animateNextTransition(); @@ -126,7 +170,7 @@ class SelectListView extends React.Component { } }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: IData }) => { const { theme } = this.props; const { selected } = this.state; @@ -187,7 +231,7 @@ class SelectListView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ isMasterDetail: state.app.isMasterDetail }); diff --git a/app/views/SelectServerView.js b/app/views/SelectServerView.tsx similarity index 61% rename from app/views/SelectServerView.js rename to app/views/SelectServerView.tsx index 0b43747af..e26645d9b 100644 --- a/app/views/SelectServerView.js +++ b/app/views/SelectServerView.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { FlatList } from 'react-native'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { connect } from 'react-redux'; -import { Q } from '@nozbe/watermelondb'; +import { Q, Model } from '@nozbe/watermelondb'; import I18n from '../i18n'; import StatusBar from '../containers/StatusBar'; @@ -12,29 +12,39 @@ import database from '../lib/database'; import SafeAreaView from '../containers/SafeAreaView'; import * as List from '../containers/List'; -const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); -const keyExtractor = item => item.id; +const getItemLayout = (data: any, index: number) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); +const keyExtractor = (item: IServer) => item.id; -class SelectServerView extends React.Component { - static navigationOptions = () => ({ +interface IServer extends Model { + id: string; + iconURL?: string; + name?: string; +} + +interface ISelectServerViewState { + servers: IServer[]; +} + +interface ISelectServerViewProps { + navigation: StackNavigationProp; + server: string; +} + +class SelectServerView extends React.Component { + static navigationOptions = (): StackNavigationOptions => ({ title: I18n.t('Select_Server') }); - static propTypes = { - server: PropTypes.string, - navigation: PropTypes.object - }; - - state = { servers: [] }; + state = { servers: [] as IServer[] }; async componentDidMount() { const serversDB = database.servers; const serversCollection = serversDB.get('servers'); - const servers = await serversCollection.query(Q.where('rooms_updated_at', Q.notEq(null))).fetch(); + const servers: IServer[] = await serversCollection.query(Q.where('rooms_updated_at', Q.notEq(null))).fetch(); this.setState({ servers }); } - select = async server => { + select = async (server: string) => { const { server: currentServer, navigation } = this.props; navigation.navigate('ShareListView'); @@ -43,7 +53,7 @@ class SelectServerView extends React.Component { } }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: IServer }) => { const { server } = this.props; return this.select(item.id)} item={item} hasCheck={item.id === server} />; }; @@ -62,7 +72,6 @@ class SelectServerView extends React.Component { ItemSeparatorComponent={List.Separator} ListHeaderComponent={List.Separator} ListFooterComponent={List.Separator} - enableEmptySections removeClippedSubviews keyboardShouldPersistTaps='always' /> @@ -71,7 +80,7 @@ class SelectServerView extends React.Component { } } -const mapStateToProps = ({ share }) => ({ +const mapStateToProps = ({ share }: any) => ({ server: share.server.server }); diff --git a/app/views/SelectedUsersView.js b/app/views/SelectedUsersView.tsx similarity index 73% rename from app/views/SelectedUsersView.js rename to app/views/SelectedUsersView.tsx index b7b254511..8d4a19fc4 100644 --- a/app/views/SelectedUsersView.js +++ b/app/views/SelectedUsersView.tsx @@ -1,9 +1,11 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import { FlatList, View } from 'react-native'; import { connect } from 'react-redux'; import orderBy from 'lodash/orderBy'; import { Q } from '@nozbe/watermelondb'; +import { Subscription } from 'rxjs'; import * as List from '../containers/List'; import database from '../lib/database'; @@ -22,33 +24,51 @@ import { addUser as addUserAction, removeUser as removeUserAction, reset as rese import { showErrorAlert } from '../utils/info'; import SafeAreaView from '../containers/SafeAreaView'; import sharedStyles from './Styles'; +import { ChatsStackParamList } from '../stacks/types'; const ITEM_WIDTH = 250; -const getItemLayout = (_, index) => ({ length: ITEM_WIDTH, offset: ITEM_WIDTH * index, index }); +const getItemLayout = (_: any, index: number) => ({ length: ITEM_WIDTH, offset: ITEM_WIDTH * index, index }); -class SelectedUsersView extends React.Component { - static propTypes = { - baseUrl: PropTypes.string, - addUser: PropTypes.func.isRequired, - removeUser: PropTypes.func.isRequired, - reset: PropTypes.func.isRequired, - users: PropTypes.array, - loading: PropTypes.bool, - user: PropTypes.shape({ - id: PropTypes.string, - token: PropTypes.string, - username: PropTypes.string, - name: PropTypes.string - }), - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string +interface IUser { + _id: string; + name: string; + fname: string; + search?: boolean; + // username is used when is from searching + username?: string; +} +interface ISelectedUsersViewState { + maxUsers?: number; + search: IUser[]; + chats: IUser[]; +} + +interface ISelectedUsersViewProps { + navigation: StackNavigationProp; + route: RouteProp; + baseUrl: string; + addUser(user: IUser): void; + removeUser(user: IUser): void; + reset(): void; + users: IUser[]; + loading: boolean; + user: { + id: string; + token: string; + username: string; + name: string; }; + theme: string; +} - constructor(props) { +class SelectedUsersView extends React.Component { + private flatlist?: FlatList; + + private querySubscription?: Subscription; + + constructor(props: ISelectedUsersViewProps) { super(props); this.init(); - this.flatlist = React.createRef(); const maxUsers = props.route.params?.maxUsers; this.state = { maxUsers, @@ -62,7 +82,7 @@ class SelectedUsersView extends React.Component { this.setHeader(props.route.params?.showButton); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: ISelectedUsersViewProps) { if (this.isGroupChat()) { const { users } = this.props; if (prevProps.users.length !== users.length) { @@ -80,7 +100,7 @@ class SelectedUsersView extends React.Component { } // showButton can be sent as route params or updated by the component - setHeader = showButton => { + setHeader = (showButton?: boolean) => { const { navigation, route } = this.props; const title = route.params?.title ?? I18n.t('Select_Users'); const buttonText = route.params?.buttonText ?? I18n.t('Next'); @@ -107,7 +127,8 @@ class SelectedUsersView extends React.Component { .query(Q.where('t', 'd')) .observeWithColumns(['room_updated_at']); - this.querySubscription = observable.subscribe(data => { + // TODO: Refactor when migrate room + this.querySubscription = observable.subscribe((data: any) => { const chats = orderBy(data, ['roomUpdatedAt'], ['desc']); this.setState({ chats }); }); @@ -116,11 +137,11 @@ class SelectedUsersView extends React.Component { } }; - onSearchChangeText(text) { + onSearchChangeText(text: string) { this.search(text); } - search = async text => { + search = async (text: string) => { const result = await RocketChat.search({ text, filterRooms: false }); this.setState({ search: result @@ -129,15 +150,15 @@ class SelectedUsersView extends React.Component { isGroupChat = () => { const { maxUsers } = this.state; - return maxUsers > 2; + return maxUsers! > 2; }; - isChecked = username => { + isChecked = (username: string) => { const { users } = this.props; return users.findIndex(el => el.name === username) !== -1; }; - toggleUser = user => { + toggleUser = (user: IUser) => { const { maxUsers } = this.state; const { addUser, @@ -163,29 +184,29 @@ class SelectedUsersView extends React.Component { } }; - _onPressItem = (id, item = {}) => { + _onPressItem = (id: string, item = {} as IUser) => { if (item.search) { - this.toggleUser({ _id: item._id, name: item.username, fname: item.name }); + this.toggleUser({ _id: item._id, name: item.username!, fname: item.name }); } else { this.toggleUser({ _id: item._id, name: item.name, fname: item.fname }); } }; - _onPressSelectedItem = item => this.toggleUser(item); + _onPressSelectedItem = (item: IUser) => this.toggleUser(item); renderHeader = () => { const { theme } = this.props; return ( - this.onSearchChangeText(text)} testID='select-users-view-search' /> + this.onSearchChangeText(text)} testID='select-users-view-search' /> {this.renderSelected()} ); }; - setFlatListRef = ref => (this.flatlist = ref); + setFlatListRef = (ref: FlatList) => (this.flatlist = ref); - onContentSizeChange = () => this.flatlist.scrollToEnd({ animated: true }); + onContentSizeChange = () => this.flatlist?.scrollToEnd({ animated: true }); renderSelected = () => { const { users, theme } = this.props; @@ -204,35 +225,32 @@ class SelectedUsersView extends React.Component { style={[sharedStyles.separatorTop, { borderColor: themes[theme].separatorColor }]} contentContainerStyle={{ marginVertical: 5 }} renderItem={this.renderSelectedItem} - enableEmptySections keyboardShouldPersistTaps='always' horizontal /> ); }; - renderSelectedItem = ({ item }) => { - const { baseUrl, user, theme } = this.props; + renderSelectedItem = ({ item }: { item: IUser }) => { + const { theme } = this.props; return ( this._onPressSelectedItem(item)} testID={`selected-user-${item.name}`} - baseUrl={baseUrl} style={{ paddingRight: 15 }} - user={user} theme={theme} /> ); }; - renderItem = ({ item, index }) => { + renderItem = ({ item, index }: { item: IUser; index: number }) => { const { search, chats } = this.state; - const { baseUrl, user, theme } = this.props; + const { theme } = this.props; const name = item.search ? item.name : item.fname; - const username = item.search ? item.username : item.name; + const username = item.search ? item.username! : item.name; let style = { borderColor: themes[theme].separatorColor }; if (index === 0) { style = { ...style, ...sharedStyles.separatorTop }; @@ -250,9 +268,7 @@ class SelectedUsersView extends React.Component { onPress={() => this._onPressItem(item._id, item)} testID={`select-users-view-item-${item.name}`} icon={this.isChecked(username) ? 'check' : null} - baseUrl={baseUrl} style={style} - user={user} theme={theme} /> ); @@ -275,7 +291,6 @@ class SelectedUsersView extends React.Component { ItemSeparatorComponent={List.Separator} ListHeaderComponent={this.renderHeader} contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} - enableEmptySections keyboardShouldPersistTaps='always' /> ); @@ -293,16 +308,16 @@ class SelectedUsersView extends React.Component { }; } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ baseUrl: state.server.server, users: state.selectedUsers.users, loading: state.selectedUsers.loading, user: getUserSelector(state) }); -const mapDispatchToProps = dispatch => ({ - addUser: user => dispatch(addUserAction(user)), - removeUser: user => dispatch(removeUserAction(user)), +const mapDispatchToProps = (dispatch: any) => ({ + addUser: (user: any) => dispatch(addUserAction(user)), + removeUser: (user: any) => dispatch(removeUserAction(user)), reset: () => dispatch(resetAction()) }); diff --git a/app/views/SendEmailConfirmationView.tsx b/app/views/SendEmailConfirmationView.tsx index 892673acc..a3aad4979 100644 --- a/app/views/SendEmailConfirmationView.tsx +++ b/app/views/SendEmailConfirmationView.tsx @@ -1,6 +1,8 @@ import React, { useEffect, useState } from 'react'; import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/core'; +import { OutsideParamList } from '../stacks/types'; import TextInput from '../containers/TextInput'; import Button from '../containers/Button'; import { showErrorAlert } from '../utils/info'; @@ -12,16 +14,12 @@ import FormContainer, { FormContainerInner } from '../containers/FormContainer'; import log, { events, logEvent } from '../utils/log'; import sharedStyles from './Styles'; -interface ISendEmailConfirmationView { - navigation: StackNavigationProp; - route: { - params: { - user?: string; - }; - }; +interface ISendEmailConfirmationViewProps { + navigation: StackNavigationProp; + route: RouteProp; } -const SendEmailConfirmationView = ({ navigation, route }: ISendEmailConfirmationView): JSX.Element => { +const SendEmailConfirmationView = ({ navigation, route }: ISendEmailConfirmationViewProps): JSX.Element => { const [email, setEmail] = useState(''); const [invalidEmail, setInvalidEmail] = useState(true); const [isFetching, setIsFetching] = useState(false); diff --git a/app/views/SetUsernameView.js b/app/views/SetUsernameView.tsx similarity index 77% rename from app/views/SetUsernameView.js rename to app/views/SetUsernameView.tsx index 158c6d013..221561697 100644 --- a/app/views/SetUsernameView.js +++ b/app/views/SetUsernameView.tsx @@ -1,8 +1,10 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; +import { Dispatch } from 'redux'; import { ScrollView, StyleSheet, Text } from 'react-native'; import { connect } from 'react-redux'; import Orientation from 'react-native-orientation-locker'; +import { RouteProp } from '@react-navigation/native'; import { loginRequest as loginRequestAction } from '../actions/login'; import TextInput from '../containers/TextInput'; @@ -27,21 +29,27 @@ const styles = StyleSheet.create({ } }); -class SetUsernameView extends React.Component { - static navigationOptions = ({ route }) => ({ +interface ISetUsernameViewState { + username: string; + saving: boolean; +} + +interface ISetUsernameViewProps { + navigation: StackNavigationProp; + route: RouteProp<{ SetUsernameView: { title: string } }, 'SetUsernameView'>; + server: string; + userId: string; + loginRequest: ({ resume }: { resume: string }) => void; + token: string; + theme: string; +} + +class SetUsernameView extends React.Component { + static navigationOptions = ({ route }: Pick): StackNavigationOptions => ({ title: route.params?.title }); - static propTypes = { - navigation: PropTypes.object, - server: PropTypes.string, - userId: PropTypes.string, - loginRequest: PropTypes.func, - token: PropTypes.string, - theme: PropTypes.string - }; - - constructor(props) { + constructor(props: ISetUsernameViewProps) { super(props); this.state = { username: '', @@ -61,7 +69,7 @@ class SetUsernameView extends React.Component { } } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: ISetUsernameViewProps, nextState: ISetUsernameViewState) { const { username, saving } = this.state; const { theme } = this.props; if (nextProps.theme !== theme) { @@ -88,7 +96,7 @@ class SetUsernameView extends React.Component { try { await RocketChat.saveUserProfile({ username }); await loginRequest({ resume: token }); - } catch (e) { + } catch (e: any) { showErrorAlert(e.message, I18n.t('Oops')); } this.setState({ saving: false }); @@ -136,13 +144,13 @@ class SetUsernameView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ server: state.server.server, token: getUserSelector(state).token }); -const mapDispatchToProps = dispatch => ({ - loginRequest: params => dispatch(loginRequestAction(params)) +const mapDispatchToProps = (dispatch: Dispatch) => ({ + loginRequest: (params: { resume: string }) => dispatch(loginRequestAction(params)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SetUsernameView)); diff --git a/app/views/SettingsView/index.tsx b/app/views/SettingsView/index.tsx index 02c17169b..edad2822e 100644 --- a/app/views/SettingsView/index.tsx +++ b/app/views/SettingsView/index.tsx @@ -5,6 +5,7 @@ import FastImage from '@rocket.chat/react-native-fast-image'; import CookieManager from '@react-native-cookies/cookies'; import { StackNavigationProp } from '@react-navigation/stack'; +import { SettingsStackParamList } from '../../stacks/types'; import { logout as logoutAction } from '../../actions/login'; import { selectServerRequest as selectServerRequestAction } from '../../actions/server'; import { themes } from '../../constants/colors'; @@ -29,8 +30,8 @@ import database from '../../lib/database'; import { isFDroidBuild } from '../../constants/environment'; import { getUserSelector } from '../../selectors/login'; -interface IProps { - navigation: StackNavigationProp; +interface ISettingsViewProps { + navigation: StackNavigationProp; server: { version: string; server: string; @@ -46,8 +47,8 @@ interface IProps { appStart: Function; } -class SettingsView extends React.Component { - static navigationOptions = ({ navigation, isMasterDetail }: Partial) => ({ +class SettingsView extends React.Component { + static navigationOptions = ({ navigation, isMasterDetail }: ISettingsViewProps) => ({ headerLeft: () => isMasterDetail ? ( @@ -117,7 +118,7 @@ class SettingsView extends React.Component { }); }; - navigateToScreen = (screen: string) => { + navigateToScreen = (screen: keyof SettingsStackParamList) => { /* @ts-ignore */ logEvent(events[`SE_GO_${screen.replace('View', '').toUpperCase()}`]); const { navigation } = this.props; diff --git a/app/views/ShareListView/Header/Header.ios.js b/app/views/ShareListView/Header/Header.ios.tsx similarity index 83% rename from app/views/ShareListView/Header/Header.ios.js rename to app/views/ShareListView/Header/Header.ios.tsx index d68bacff2..c1f5e1166 100644 --- a/app/views/ShareListView/Header/Header.ios.js +++ b/app/views/ShareListView/Header/Header.ios.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import PropTypes from 'prop-types'; import { Keyboard, StyleSheet, View } from 'react-native'; import ShareExtension from 'rn-extensions-share'; @@ -8,6 +7,7 @@ import * as HeaderButton from '../../../containers/HeaderButton'; import { themes } from '../../../constants/colors'; import sharedStyles from '../../Styles'; import { animateNextTransition } from '../../../utils/layoutAnimation'; +import { IShareListHeaderIos } from './interface'; const styles = StyleSheet.create({ container: { @@ -16,10 +16,10 @@ const styles = StyleSheet.create({ } }); -const Header = React.memo(({ searching, onChangeSearchText, initSearch, cancelSearch, theme }) => { +const Header = React.memo(({ searching, onChangeSearchText, initSearch, cancelSearch, theme }: IShareListHeaderIos) => { const [text, setText] = useState(''); - const onChangeText = searchText => { + const onChangeText = (searchText: string) => { onChangeSearchText(searchText); setText(searchText); }; @@ -59,12 +59,4 @@ const Header = React.memo(({ searching, onChangeSearchText, initSearch, cancelSe ); }); -Header.propTypes = { - searching: PropTypes.bool, - onChangeSearchText: PropTypes.func, - initSearch: PropTypes.func, - cancelSearch: PropTypes.func, - theme: PropTypes.string -}; - export default Header; diff --git a/app/views/ShareListView/Header/Header.android.js b/app/views/ShareListView/Header/Header.tsx similarity index 86% rename from app/views/ShareListView/Header/Header.android.js rename to app/views/ShareListView/Header/Header.tsx index 727fa5364..616f3f487 100644 --- a/app/views/ShareListView/Header/Header.android.js +++ b/app/views/ShareListView/Header/Header.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; -import PropTypes from 'prop-types'; import TextInput from '../../../presentation/TextInput'; import I18n from '../../../i18n'; import { themes } from '../../../constants/colors'; import sharedStyles from '../../Styles'; +import { IShareListHeader } from './interface'; const styles = StyleSheet.create({ container: { @@ -24,7 +24,7 @@ const styles = StyleSheet.create({ } }); -const Header = React.memo(({ searching, onChangeSearchText, theme }) => { +const Header = React.memo(({ searching, onChangeSearchText, theme }: IShareListHeader) => { const titleColorStyle = { color: themes[theme].headerTintColor }; const isLight = theme === 'light'; if (searching) { @@ -43,10 +43,4 @@ const Header = React.memo(({ searching, onChangeSearchText, theme }) => { return {I18n.t('Send_to')}; }); -Header.propTypes = { - searching: PropTypes.bool, - onChangeSearchText: PropTypes.func, - theme: PropTypes.string -}; - export default Header; diff --git a/app/views/ShareListView/Header/index.js b/app/views/ShareListView/Header/index.tsx similarity index 52% rename from app/views/ShareListView/Header/index.js rename to app/views/ShareListView/Header/index.tsx index d66c9a804..e1feab435 100644 --- a/app/views/ShareListView/Header/index.js +++ b/app/views/ShareListView/Header/index.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import PropTypes from 'prop-types'; import Header from './Header'; +import { IShareListHeader } from './interface'; -const ShareListHeader = React.memo(({ searching, initSearch, cancelSearch, search, theme }) => { - const onSearchChangeText = text => { - search(text.trim()); +const ShareListHeader = React.memo(({ searching, initSearch, cancelSearch, onChangeSearchText, theme }: IShareListHeader) => { + const onSearchChangeText = (text: string) => { + onChangeSearchText(text.trim()); }; return ( @@ -19,12 +19,4 @@ const ShareListHeader = React.memo(({ searching, initSearch, cancelSearch, searc ); }); -ShareListHeader.propTypes = { - searching: PropTypes.bool, - initSearch: PropTypes.func, - cancelSearch: PropTypes.func, - search: PropTypes.func, - theme: PropTypes.string -}; - export default ShareListHeader; diff --git a/app/views/ShareListView/Header/interface.ts b/app/views/ShareListView/Header/interface.ts new file mode 100644 index 000000000..25266fb59 --- /dev/null +++ b/app/views/ShareListView/Header/interface.ts @@ -0,0 +1,13 @@ +import { TextInputProps } from 'react-native'; + +type RequiredOnChangeText = Required>; + +export interface IShareListHeader { + searching: boolean; + onChangeSearchText: RequiredOnChangeText['onChangeText']; + theme: string; + initSearch?: () => void; + cancelSearch?: () => void; +} + +export type IShareListHeaderIos = Required; diff --git a/app/views/ShareListView/index.js b/app/views/ShareListView/index.tsx similarity index 79% rename from app/views/ShareListView/index.js rename to app/views/ShareListView/index.tsx index e0a82a50d..1c9f0e415 100644 --- a/app/views/ShareListView/index.js +++ b/app/views/ShareListView/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { BackHandler, FlatList, Keyboard, PermissionsAndroid, ScrollView, Text, View } from 'react-native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { BackHandler, FlatList, Keyboard, PermissionsAndroid, ScrollView, Text, View, Rationale } from 'react-native'; import ShareExtension from 'rn-extensions-share'; import * as FileSystem from 'expo-file-system'; import { connect } from 'react-redux'; @@ -25,24 +25,75 @@ import { sanitizeLikeString } from '../../lib/database/utils'; import styles from './styles'; import ShareListHeader from './Header'; -const permission = { +interface IFile { + value: string; + type: string; +} + +interface IAttachment { + filename: string; + description: string; + size: number; + mime: any; + path: string; +} + +interface IChat { + rid: string; + t: string; + name: string; + fname: string; + blocked: boolean; + blocker: boolean; + prid: string; + uids: string[]; + usernames: string[]; + topic: string; + description: string; +} + +interface IServerInfo { + useRealName: boolean; +} +interface IState { + searching: boolean; + searchText: string; + searchResults: IChat[]; + chats: IChat[]; + serversCount: number; + attachments: IAttachment[]; + text: string; + loading: boolean; + serverInfo: IServerInfo; + needsPermission: boolean; +} + +interface INavigationOption { + navigation: StackNavigationProp; +} + +interface IShareListViewProps extends INavigationOption { + server: string; + token: string; + userId: string; + theme: string; +} + +const permission: Rationale = { title: I18n.t('Read_External_Permission'), - message: I18n.t('Read_External_Permission_Message') + message: I18n.t('Read_External_Permission_Message'), + buttonPositive: 'Ok' }; -const getItemLayout = (data, index) => ({ length: data.length, offset: ROW_HEIGHT * index, index }); -const keyExtractor = item => item.rid; +const getItemLayout = (data: any, index: number) => ({ length: data.length, offset: ROW_HEIGHT * index, index }); +const keyExtractor = (item: IChat) => item.rid; -class ShareListView extends React.Component { - static propTypes = { - navigation: PropTypes.object, - server: PropTypes.string, - token: PropTypes.string, - userId: PropTypes.string, - theme: PropTypes.string - }; +class ShareListView extends React.Component { + private unsubscribeFocus: (() => void) | undefined; - constructor(props) { + private unsubscribeBlur: (() => void) | undefined; + + constructor(props: IShareListViewProps) { super(props); this.state = { searching: false, @@ -53,7 +104,7 @@ class ShareListView extends React.Component { attachments: [], text: '', loading: true, - serverInfo: null, + serverInfo: {} as IServerInfo, needsPermission: isAndroid || false }; this.setHeader(); @@ -70,7 +121,7 @@ class ShareListView extends React.Component { async componentDidMount() { const { server } = this.props; try { - const data = await ShareExtension.data(); + const data = (await ShareExtension.data()) as IFile[]; if (isAndroid) { await this.askForPermission(data); } @@ -85,7 +136,7 @@ class ShareListView extends React.Component { size: file.size, mime: mime.lookup(file.uri), path: file.uri - })); + })) as IAttachment[]; const text = data.filter(item => item.type === 'text').reduce((acc, item) => `${item.value}\n${acc}`, ''); this.setState({ text, @@ -98,14 +149,14 @@ class ShareListView extends React.Component { this.getSubscriptions(server); } - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: IShareListViewProps) { const { server } = this.props; if (nextProps.server !== server) { this.getSubscriptions(nextProps.server); } } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: IShareListViewProps, nextState: IState) { const { searching, needsPermission } = this.state; if (nextState.searching !== searching) { return true; @@ -151,7 +202,7 @@ class ShareListView extends React.Component { searching={searching} initSearch={this.initSearch} cancelSearch={this.cancelSearch} - search={this.search} + onChangeSearchText={this.search} theme={theme} /> ) @@ -168,7 +219,7 @@ class ShareListView extends React.Component { ) : ( ), - headerTitle: () => , + headerTitle: () => , headerRight: () => searching ? null : ( @@ -178,16 +229,16 @@ class ShareListView extends React.Component { }); }; - // eslint-disable-next-line react/sort-comp - internalSetState = (...args) => { + internalSetState = (...args: object[]) => { const { navigation } = this.props; if (navigation.isFocused()) { animateNextTransition(); } + // @ts-ignore this.setState(...args); }; - query = async text => { + query = async (text?: string) => { const db = database.active; const defaultWhereClause = [ Q.where('archived', false), @@ -195,15 +246,16 @@ class ShareListView extends React.Component { Q.experimentalSkip(0), Q.experimentalTake(20), Q.experimentalSortBy('room_updated_at', Q.desc) - ]; + ] as (Q.WhereDescription | Q.Skip | Q.Take | Q.SortBy | Q.Or)[]; if (text) { const likeString = sanitizeLikeString(text); defaultWhereClause.push(Q.or(Q.where('name', Q.like(`%${likeString}%`)), Q.where('fname', Q.like(`%${likeString}%`)))); } - const data = await db + const data = (await db .get('subscriptions') .query(...defaultWhereClause) - .fetch(); + .fetch()) as IChat[]; + return data.map(item => ({ rid: item.rid, t: item.t, @@ -218,7 +270,7 @@ class ShareListView extends React.Component { })); }; - getSubscriptions = async server => { + getSubscriptions = async (server: string) => { const serversDB = database.servers; if (server) { @@ -242,7 +294,7 @@ class ShareListView extends React.Component { } }; - askForPermission = async data => { + askForPermission = async (data: IFile[]) => { const mediaIndex = data.findIndex(item => item.type === 'media'); if (mediaIndex !== -1) { const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, permission); @@ -255,15 +307,14 @@ class ShareListView extends React.Component { return Promise.resolve(); }; - uriToPath = uri => decodeURIComponent(isIOS ? uri.replace(/^file:\/\//, '') : uri); + uriToPath = (uri: string) => decodeURIComponent(isIOS ? uri.replace(/^file:\/\//, '') : uri); - getRoomTitle = item => { + getRoomTitle = (item: IChat) => { const { serverInfo } = this.state; - const { useRealName } = serverInfo; - return ((item.prid || useRealName) && item.fname) || item.name; + return ((item.prid || serverInfo?.useRealName) && item.fname) || item.name; }; - shareMessage = room => { + shareMessage = (room: IChat) => { const { attachments, text, serverInfo } = this.state; const { navigation } = this.props; @@ -276,7 +327,7 @@ class ShareListView extends React.Component { }); }; - search = async text => { + search = async (text: string) => { const result = await this.query(text); this.internalSetState({ searchResults: result, @@ -303,7 +354,7 @@ class ShareListView extends React.Component { return false; }; - renderSectionHeader = header => { + renderSectionHeader = (header: string) => { const { searching } = this.state; const { theme } = this.props; if (searching) { @@ -320,10 +371,9 @@ class ShareListView extends React.Component { ); }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: IChat }) => { const { serverInfo } = this.state; - const { useRealName } = serverInfo; - const { userId, token, server, theme } = this.props; + const { theme } = this.props; let description; switch (item.t) { case 'c': @@ -333,7 +383,7 @@ class ShareListView extends React.Component { description = item.topic || item.description; break; case 'd': - description = useRealName ? item.name : item.fname; + description = serverInfo?.useRealName ? item.name : item.fname; break; default: description = item.fname; @@ -341,12 +391,7 @@ class ShareListView extends React.Component { } return ( ({ +const mapStateToProps = ({ share }: any) => ({ userId: share.user && share.user.id, token: share.user && share.user.token, server: share.server.server diff --git a/app/views/ShareListView/styles.js b/app/views/ShareListView/styles.ts similarity index 100% rename from app/views/ShareListView/styles.js rename to app/views/ShareListView/styles.ts diff --git a/app/views/ShareView/index.tsx b/app/views/ShareView/index.tsx index e10b21483..a3ad287aa 100644 --- a/app/views/ShareView/index.tsx +++ b/app/views/ShareView/index.tsx @@ -4,7 +4,9 @@ import { RouteProp } from '@react-navigation/native'; import { NativeModules, Text, View } from 'react-native'; import { connect } from 'react-redux'; import ShareExtension from 'rn-extensions-share'; +import { Q } from '@nozbe/watermelondb'; +import { InsideStackParamList } from '../../stacks/types'; import { themes } from '../../constants/colors'; import I18n from '../../i18n'; import Loading from '../../containers/Loading'; @@ -25,7 +27,8 @@ import Thumbs from './Thumbs'; import Preview from './Preview'; import Header from './Header'; import styles from './styles'; -import { IAttachment, IServer } from './interfaces'; +import { IAttachment } from './interfaces'; +import { IRoom } from '../../definitions/IRoom'; interface IShareViewState { selected: IAttachment; @@ -33,30 +36,15 @@ interface IShareViewState { readOnly: boolean; attachments: IAttachment[]; text: string; - // TODO: Refactor when migrate room - room: any; - thread: any; + room: IRoom; + thread: any; // change maxFileSize: number; mediaAllowList: number; } interface IShareViewProps { - // TODO: Refactor after react-navigation - navigation: StackNavigationProp; - route: RouteProp< - { - ShareView: { - attachments: IAttachment[]; - isShareView?: boolean; - isShareExtension: boolean; - serverInfo: IServer; - text: string; - room: any; - thread: any; // change - }; - }, - 'ShareView' - >; + navigation: StackNavigationProp; + route: RouteProp; theme: string; user: { id: string; @@ -154,6 +142,17 @@ class ShareView extends Component { } }; + getPermissionMobileUpload = async () => { + const { room } = this.state; + const db = database.active; + const permissionsCollection = db.get('permissions'); + const uploadFilePermissionFetch = await permissionsCollection.query(Q.where('id', Q.like('mobile-upload-file'))).fetch(); + const uploadFilePermission = uploadFilePermissionFetch[0]?.roles; + const permissionToUpload = await RocketChat.hasPermission([uploadFilePermission], room.rid); + // uploadFilePermission as undefined is considered that there isn't this permission, so all can upload file. + return !uploadFilePermission || permissionToUpload[0]; + }; + getReadOnly = async () => { const { room } = this.state; const { user } = this.props; @@ -163,10 +162,12 @@ class ShareView extends Component { getAttachments = async () => { const { mediaAllowList, maxFileSize } = this.state; + const permissionToUploadFile = await this.getPermissionMobileUpload(); + const items = await Promise.all( this.files.map(async item => { // Check server settings - const { success: canUpload, error } = canUploadFile(item, mediaAllowList, maxFileSize); + const { success: canUpload, error } = canUploadFile(item, mediaAllowList, maxFileSize, permissionToUploadFile); item.canUpload = canUpload; item.error = error; diff --git a/app/views/ShareView/interfaces.ts b/app/views/ShareView/interfaces.ts index 09cb4d9eb..a2231450d 100644 --- a/app/views/ShareView/interfaces.ts +++ b/app/views/ShareView/interfaces.ts @@ -13,21 +13,3 @@ export interface IUseDimensions { width: number; height: number; } - -// TODO: move this to specific folder -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 | null; - biometry: boolean | null; - uniqueID: string; - enterpriseModules: string; - E2E_Enable: boolean; -} diff --git a/app/views/SidebarView/SidebarItem.tsx b/app/views/SidebarView/SidebarItem.tsx index 7590e82ca..bfbf2d2db 100644 --- a/app/views/SidebarView/SidebarItem.tsx +++ b/app/views/SidebarView/SidebarItem.tsx @@ -25,7 +25,7 @@ const Item = React.memo(({ left, right, text, onPress, testID, current, theme }: style={[styles.item, current && { backgroundColor: themes[theme].borderColor }]}>