diff --git a/.eslintrc.js b/.eslintrc.js index f5b6d39c8..085f3a89d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,7 +2,7 @@ module.exports = { settings: { 'import/resolver': { node: { - extensions: ['.js', '.ios.js', '.android.js', '.native.js', '.ts', '.tsx'] + extensions: ['.ts', '.tsx', '.js', '.ios.js', '.android.js', '.native.js'] } } }, diff --git a/.github/workflows/ios_detox.yml b/.github/workflows/ios_detox.yml deleted file mode 100644 index c8c33c583..000000000 --- a/.github/workflows/ios_detox.yml +++ /dev/null @@ -1,221 +0,0 @@ -name: iOS Detox - -on: [pull_request] - -jobs: - detox-build: - runs-on: macos-latest - timeout-minutes: 60 - - env: - DEVELOPER_DIR: /Applications/Xcode_11.5.app - - steps: - - name: Checkout - uses: actions/checkout@v1 - with: - fetch-depth: 1 - - - name: Generate Detox app cache key - run: echo $(git rev-parse HEAD:app) > "./app-git-revision.txt" - - - name: Cache Detox app - uses: actions/cache@v1 - id: detoxappcache - with: - path: ios/build/Build/Products/Release-iphonesimulator - key: iOSDetoxRelease-v4-${{ hashFiles('yarn.lock') }}-${{ hashFiles('ios/Podfile.lock') }}-${{ hashFiles('./app-git-revision.txt') }} - - - name: Node - if: steps.detoxappcache.outputs.cache-hit != 'true' - uses: actions/setup-node@v1 - - - name: Cache node modules - if: steps.detoxappcache.outputs.cache-hit != 'true' - uses: actions/cache@v1 - id: npmcache - with: - path: node_modules - key: node-modules-${{ hashFiles('**/yarn.lock') }} - - - name: Rebuild detox - if: steps.detoxappcache.outputs.cache-hit != 'true' && steps.npmcache.outputs.cache-hit == 'true' - run: yarn detox clean-framework-cache && yarn detox build-framework-cache - - - name: Install Dependencies - if: steps.detoxappcache.outputs.cache-hit != 'true' && steps.npmcache.outputs.cache-hit != 'true' - run: yarn install - - - run: yarn detox build e2e --configuration ios.sim.release - if: steps.detoxappcache.outputs.cache-hit != 'true' - - detox-test-rooms: - needs: detox-build - runs-on: macos-latest - timeout-minutes: 60 - - env: - DEVELOPER_DIR: /Applications/Xcode_11.5.app - - steps: - - name: Checkout - uses: actions/checkout@v1 - with: - fetch-depth: 1 - - - name: Generate Detox app cache key - run: echo $(git rev-parse HEAD:app) > "./app-git-revision.txt" - - - name: Cache Detox app - uses: actions/cache@v1 - id: detoxappcache - with: - path: ios/build/Build/Products/Release-iphonesimulator - key: iOSDetoxRelease-v4-${{ hashFiles('yarn.lock') }}-${{ hashFiles('ios/Podfile.lock') }}-${{ hashFiles('./app-git-revision.txt') }} - - - name: Check for Detox app - if: steps.detoxappcache.outputs.cache-hit != 'true' - run: exit 1 - - - name: Node - uses: actions/setup-node@v1 - - - name: Cache node modules - uses: actions/cache@v1 - id: npmcache - with: - path: node_modules - key: node-modules-${{ hashFiles('**/yarn.lock') }} - - - name: Rebuild detox - if: steps.npmcache.outputs.cache-hit == 'true' - run: yarn detox clean-framework-cache && yarn detox build-framework-cache - - - name: Install Dependencies - if: steps.npmcache.outputs.cache-hit != 'true' - run: yarn install - - - run: brew tap wix/brew - - run: brew install applesimutils - - run: yarn detox test e2e/tests/room --configuration ios.sim.release --cleanup - - - name: Upload test artifacts - if: ${{ failure() }} - uses: actions/upload-artifact@v2 - with: - name: artifacts - path: artifacts - - detox-test-assorted: - needs: detox-build - runs-on: macos-latest - timeout-minutes: 60 - - env: - DEVELOPER_DIR: /Applications/Xcode_11.5.app - - steps: - - name: Checkout - uses: actions/checkout@v1 - with: - fetch-depth: 1 - - - name: Generate Detox app cache key - run: echo $(git rev-parse HEAD:app) > "./app-git-revision.txt" - - - name: Cache Detox app - uses: actions/cache@v1 - id: detoxappcache - with: - path: ios/build/Build/Products/Release-iphonesimulator - key: iOSDetoxRelease-v4-${{ hashFiles('yarn.lock') }}-${{ hashFiles('ios/Podfile.lock') }}-${{ hashFiles('./app-git-revision.txt') }} - - - name: Check for Detox app - if: steps.detoxappcache.outputs.cache-hit != 'true' - run: exit 1 - - - name: Node - uses: actions/setup-node@v1 - - - name: Cache node modules - uses: actions/cache@v1 - id: npmcache - with: - path: node_modules - key: node-modules-${{ hashFiles('**/yarn.lock') }} - - - name: Rebuild detox - if: steps.npmcache.outputs.cache-hit == 'true' - run: yarn detox clean-framework-cache && yarn detox build-framework-cache - - - name: Install Dependencies - if: steps.npmcache.outputs.cache-hit != 'true' - run: yarn install - - - run: brew tap wix/brew - - run: brew install applesimutils - - run: yarn detox test e2e/tests/assorted --configuration ios.sim.release --cleanup - - - name: Upload test artifacts - if: ${{ failure() }} - uses: actions/upload-artifact@v2 - with: - name: artifacts - path: artifacts - - detox-test-onboarding: - needs: detox-build - runs-on: macos-latest - timeout-minutes: 60 - - env: - DEVELOPER_DIR: /Applications/Xcode_11.5.app - - steps: - - name: Checkout - uses: actions/checkout@v1 - with: - fetch-depth: 1 - - - name: Generate Detox app cache key - run: echo $(git rev-parse HEAD:app) > "./app-git-revision.txt" - - - name: Cache Detox app - uses: actions/cache@v1 - id: detoxappcache - with: - path: ios/build/Build/Products/Release-iphonesimulator - key: iOSDetoxRelease-v4-${{ hashFiles('yarn.lock') }}-${{ hashFiles('ios/Podfile.lock') }}-${{ hashFiles('./app-git-revision.txt') }} - - - name: Check for Detox app - if: steps.detoxappcache.outputs.cache-hit != 'true' - run: exit 1 - - - name: Node - uses: actions/setup-node@v1 - - - name: Cache node modules - uses: actions/cache@v1 - id: npmcache - with: - path: node_modules - key: node-modules-${{ hashFiles('**/yarn.lock') }} - - - name: Rebuild detox - if: steps.npmcache.outputs.cache-hit == 'true' - run: yarn detox clean-framework-cache && yarn detox build-framework-cache - - - name: Install Dependencies - if: steps.npmcache.outputs.cache-hit != 'true' - run: yarn install - - - run: brew tap wix/brew - - run: brew install applesimutils - - run: yarn detox test e2e/tests/onboarding --configuration ios.sim.release --cleanup - - - name: Upload test artifacts - if: ${{ failure() }} - uses: actions/upload-artifact@v2 - with: - name: artifacts - path: artifacts diff --git a/__tests__/Storyshots.test.js b/__tests__/Storyshots.test.js index 0395eef24..60f449144 100644 --- a/__tests__/Storyshots.test.js +++ b/__tests__/Storyshots.test.js @@ -1,5 +1,21 @@ import initStoryshots from '@storybook/addon-storyshots'; +jest.mock('rn-fetch-blob', () => ({ + fs: { + dirs: { + DocumentDir: '/data/com.rocket.chat/documents', + DownloadDir: '/data/com.rocket.chat/downloads' + }, + exists: jest.fn(() => null) + }, + fetch: jest.fn(() => null), + config: jest.fn(() => null) +})); + +jest.mock('react-native-file-viewer', () => ({ + open: jest.fn(() => null) +})); + jest.mock('../app/lib/database', () => jest.fn(() => null)); global.Date.now = jest.fn(() => new Date('2019-10-10').getTime()); diff --git a/app/constants/colors.ts b/app/constants/colors.ts index ab358679c..b28416b86 100644 --- a/app/constants/colors.ts +++ b/app/constants/colors.ts @@ -65,6 +65,7 @@ export const themes: any = { previewBackground: '#1F2329', previewTintColor: '#ffffff', backdropOpacity: 0.3, + attachmentLoadingOpacity: 0.7, ...mentions }, dark: { @@ -112,6 +113,7 @@ export const themes: any = { previewBackground: '#030b1b', previewTintColor: '#ffffff', backdropOpacity: 0.9, + attachmentLoadingOpacity: 0.3, ...mentions }, black: { @@ -159,6 +161,7 @@ export const themes: any = { previewBackground: '#000000', previewTintColor: '#ffffff', backdropOpacity: 0.9, + attachmentLoadingOpacity: 0.3, ...mentions } }; diff --git a/app/constants/localPath.ts b/app/constants/localPath.ts new file mode 100644 index 000000000..704e2b6b6 --- /dev/null +++ b/app/constants/localPath.ts @@ -0,0 +1,4 @@ +import RNFetchBlob from 'rn-fetch-blob'; + +export const DOCUMENTS_PATH = `${RNFetchBlob.fs.dirs.DocumentDir}/`; +export const DOWNLOAD_PATH = `${RNFetchBlob.fs.dirs.DownloadDir}/`; diff --git a/app/containers/ActionSheet/Provider.tsx b/app/containers/ActionSheet/Provider.tsx index 8b75577d5..0e58ae572 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.FC) => +export const withActionSheet =

(Component: React.ComponentType

) => forwardRef((props: any, ref: ForwardedRef) => ( {(contexts: any) => } )); diff --git a/app/containers/Avatar/interfaces.ts b/app/containers/Avatar/interfaces.ts index 692c4d0a3..ed7fd3b9e 100644 --- a/app/containers/Avatar/interfaces.ts +++ b/app/containers/Avatar/interfaces.ts @@ -16,7 +16,7 @@ export interface IAvatar { onPress(): void; getCustomEmoji(): any; avatarETag: string; - isStatic: boolean; + isStatic: boolean | string; rid: string; blockUnauthenticatedAccess: boolean; serverVersion: string; diff --git a/app/containers/FormContainer.tsx b/app/containers/FormContainer.tsx index 3a784b38c..ff233952a 100644 --- a/app/containers/FormContainer.tsx +++ b/app/containers/FormContainer.tsx @@ -27,13 +27,11 @@ export const FormContainerInner = ({ children }: { children: React.ReactNode }): ); const FormContainer = ({ children, theme, testID, ...props }: IFormContainer): JSX.Element => ( - // @ts-ignore - {/* @ts-ignore*/} ; } const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: IStatus) => { const name = `status-${status}`; const isNameValid = CustomIcon.hasIcon(name); const iconName = isNameValid ? name : 'status-offline'; - const calculatedStyle = [ + const calculatedStyle: StyleProp = [ { width: size, height: size, diff --git a/app/containers/TextInput.tsx b/app/containers/TextInput.tsx index 5f9aaecf8..28ae1c2f1 100644 --- a/app/containers/TextInput.tsx +++ b/app/containers/TextInput.tsx @@ -58,7 +58,7 @@ interface IRCTextInputProps extends TextInputProps { }; loading?: boolean; containerStyle?: StyleProp; - inputStyle?: TextStyle; + inputStyle?: StyleProp; inputRef?: React.Ref; testID?: string; iconLeft?: string; diff --git a/app/containers/UIKit/MultiSelect/Input.tsx b/app/containers/UIKit/MultiSelect/Input.tsx index e03837a2b..ed4835c5e 100644 --- a/app/containers/UIKit/MultiSelect/Input.tsx +++ b/app/containers/UIKit/MultiSelect/Input.tsx @@ -8,10 +8,10 @@ import ActivityIndicator from '../../ActivityIndicator'; import styles from './styles'; interface IInput { - children: JSX.Element; + children?: JSX.Element; onPress: Function; theme: string; - inputStyle: object; + inputStyle?: object; disabled?: boolean | object; placeholder?: string; loading?: boolean; diff --git a/app/containers/message/Reply.tsx b/app/containers/message/Reply.tsx index 5f4ca7657..fbc8984fc 100644 --- a/app/containers/message/Reply.tsx +++ b/app/containers/message/Reply.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import moment from 'moment'; import { transparentize } from 'color2k'; @@ -11,6 +11,9 @@ import openLink from '../../utils/openLink'; import sharedStyles from '../../views/Styles'; import { themes } from '../../constants/colors'; import MessageContext from './Context'; +import { fileDownloadAndPreview } from '../../utils/fileDownload'; +import { formatAttachmentUrl } from '../../lib/utils'; +import RCActivityIndicator from '../ActivityIndicator'; const styles = StyleSheet.create({ button: { @@ -28,6 +31,9 @@ const styles = StyleSheet.create({ flexDirection: 'column', padding: 15 }, + backdrop: { + ...StyleSheet.absoluteFillObject + }, authorContainer: { flex: 1, flexDirection: 'row', @@ -120,7 +126,7 @@ interface IMessageFields { } interface IMessageReply { - attachment: Partial; + attachment: IMessageReplyAttachment; timeFormat: string; index: number; theme: string; @@ -209,12 +215,14 @@ const Fields = React.memo( const Reply = React.memo( ({ attachment, timeFormat, index, getCustomEmoji, theme }: IMessageReply) => { + const [loading, setLoading] = useState(false); + if (!attachment) { return null; } const { baseUrl, user, jumpToMessage } = useContext(MessageContext); - const onPress = () => { + const onPress = async () => { let url = attachment.title_link || attachment.author_link; if (attachment.message_link) { return jumpToMessage(attachment.message_link); @@ -223,10 +231,11 @@ const Reply = React.memo( return; } if (attachment.type === 'file') { - if (!url.startsWith('http')) { - url = `${baseUrl}${url}`; - } - url = `${url}?rc_uid=${user.id}&rc_token=${user.token}`; + setLoading(true); + url = formatAttachmentUrl(attachment.title_link, user.id, user.token, baseUrl); + await fileDownloadAndPreview(url, attachment); + setLoading(false); + return; } openLink(url, theme); }; @@ -254,12 +263,23 @@ const Reply = React.memo( borderColor } ]} - background={Touchable.Ripple(themes[theme].bannerBackground)}> + background={Touchable.Ripple(themes[theme].bannerBackground)} + disabled={loading}> <UrlImage image={attachment.thumb_url} /> <Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> + {loading ? ( + <View style={[styles.backdrop]}> + <View + style={[ + styles.backdrop, + { backgroundColor: themes[theme].bannerBackground, opacity: themes[theme].attachmentLoadingOpacity } + ]}></View> + <RCActivityIndicator theme={theme} /> + </View> + ) : null} </View> </Touchable> {/* @ts-ignore*/} diff --git a/app/containers/message/Video.tsx b/app/containers/message/Video.tsx index 5a9761412..afa5a68b2 100644 --- a/app/containers/message/Video.tsx +++ b/app/containers/message/Video.tsx @@ -1,15 +1,19 @@ -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import { StyleSheet } from 'react-native'; import { dequal } from 'dequal'; import Touchable from './Touchable'; import Markdown from '../markdown'; -import openLink from '../../utils/openLink'; import { isIOS } from '../../utils/deviceInfo'; import { CustomIcon } from '../../lib/Icons'; import { formatAttachmentUrl } from '../../lib/utils'; import { themes } from '../../constants/colors'; import MessageContext from './Context'; +import { fileDownload } from '../../utils/fileDownload'; +import EventEmitter from '../../utils/events'; +import { LISTENER } from '../Toast'; +import I18n from '../../i18n'; +import RCActivityIndicator from '../ActivityIndicator'; const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])]; const isTypeSupported = (type: any) => SUPPORTED_TYPES.indexOf(type) !== -1; @@ -27,6 +31,9 @@ const styles = StyleSheet.create({ interface IMessageVideo { file: { + title: string; + title_link: string; + type: string; video_type: string; video_url: string; description: string; @@ -39,15 +46,34 @@ interface IMessageVideo { const Video = React.memo( ({ file, showAttachment, getCustomEmoji, theme }: IMessageVideo) => { const { baseUrl, user } = useContext(MessageContext); + const [loading, setLoading] = useState(false); + if (!baseUrl) { return null; } - const onPress = () => { + const onPress = async () => { if (isTypeSupported(file.video_type)) { return showAttachment(file); } - const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl); - openLink(uri, theme); + + if (!isIOS) { + const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl); + await downloadVideo(uri); + return; + } + EventEmitter.emit(LISTENER, { message: I18n.t('Unsupported_format') }); + }; + + const downloadVideo = async (uri: string) => { + setLoading(true); + const fileDownloaded = await fileDownload(uri, file); + setLoading(false); + + if (fileDownloaded) { + EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') }); + return; + } + EventEmitter.emit(LISTENER, { message: I18n.t('error-save-video') }); }; return ( @@ -56,7 +82,11 @@ const Video = React.memo( onPress={onPress} style={[styles.button, { backgroundColor: themes[theme].videoBackground }]} background={Touchable.Ripple(themes[theme].bannerBackground)}> - <CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} /> + {loading ? ( + <RCActivityIndicator theme={theme} /> + ) : ( + <CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} /> + )} </Touchable> {/* @ts-ignore*/} <Markdown diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index 60286a978..9be2ef064 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -59,6 +59,7 @@ export interface IUser { export type UserMention = Pick<IUser, '_id' | 'username' | 'name'>; export interface IMessageContent { + _id: string; isTemp: boolean; isInfo: boolean; tmid: string; diff --git a/app/externalModules.d.ts b/app/externalModules.d.ts index 3f7d7a6d8..f68cb5e39 100644 --- a/app/externalModules.d.ts +++ b/app/externalModules.d.ts @@ -9,3 +9,7 @@ declare module '@rocket.chat/ui-kit'; declare module '@rocket.chat/sdk'; declare module 'react-native-config-reader'; declare module 'react-native-keycommands'; +declare module 'react-native-mime-types'; +declare module 'react-native-restart'; +declare module 'react-native-prompt-android'; +declare module 'react-native-jitsi-meet'; diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 5d6eb5819..e4ae9fe13 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -783,5 +783,8 @@ "No_canned_responses": "No canned responses", "Send_email_confirmation": "Send email confirmation", "sending_email_confirmation": "sending email confirmation", - "Enable_Message_Parser": "Enable Message Parser" -} \ No newline at end of file + "Enable_Message_Parser": "Enable Message Parser", + "Unsupported_format": "Unsupported format", + "Downloaded_file": "Downloaded file", + "Error_Download_file": "Error while downloading file" +} diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index fcc05ca22..24905e9f0 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -733,5 +733,8 @@ "Sharing": "Compartilhando", "No_canned_responses": "Não há respostas predefinidas", "Send_email_confirmation": "Enviar email de confirmação", - "sending_email_confirmation": "enviando email de confirmação" -} \ No newline at end of file + "sending_email_confirmation": "enviando email de confirmação", + "Unsupported_format": "Formato não suportado", + "Downloaded_file": "Arquivo baixado", + "Error_Download_file": "Erro ao baixar o arquivo" +} diff --git a/app/index.tsx b/app/index.tsx index 31128db26..e6457e233 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -63,10 +63,12 @@ const parseDeepLinking = (url: string) => { } } const call = /^(https:\/\/)?jitsi.rocket.chat\//; + const fullURL = url; + if (url.match(call)) { url = url.replace(call, '').trim(); if (url) { - return { path: url, isCall: true }; + return { path: url, isCall: true, fullURL }; } } } diff --git a/app/lib/methods/callJitsi.js b/app/lib/methods/callJitsi.js index 19263f135..8d2fa7db4 100644 --- a/app/lib/methods/callJitsi.js +++ b/app/lib/methods/callJitsi.js @@ -36,6 +36,14 @@ async function jitsiURL({ room }) { return `${protocol}${domain}${prefix}${rname}${queryString}`; } +export function callJitsiWithoutServer(path) { + logEvent(events.RA_JITSI_VIDEO); + const { Jitsi_SSL } = reduxStore.getState().settings; + const protocol = Jitsi_SSL ? 'https://' : 'http://'; + const url = `${protocol}${path}`; + Navigation.navigate('JitsiMeetView', { url, onlyAudio: false }); +} + async function callJitsi(room, onlyAudio = false) { logEvent(onlyAudio ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO); const url = await jitsiURL.call(this, { room }); diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 33f87e868..7fd058d8f 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -54,7 +54,7 @@ import loadMissedMessages from './methods/loadMissedMessages'; import loadThreadMessages from './methods/loadThreadMessages'; import sendMessage, { resendMessage } from './methods/sendMessage'; import { cancelUpload, isUploadActive, sendFileMessage } from './methods/sendFileMessage'; -import callJitsi from './methods/callJitsi'; +import callJitsi, { callJitsiWithoutServer } from './methods/callJitsi'; import logout, { removeServer } from './methods/logout'; import UserPreferences from './userPreferences'; import { Encryption } from './encryption'; @@ -76,6 +76,7 @@ const RocketChat = { CURRENT_SERVER, CERTIFICATE_KEY, callJitsi, + callJitsiWithoutServer, async subscribeRooms() { if (!this.roomsSub) { try { diff --git a/app/presentation/ImageViewer/ImageComponent.ts b/app/presentation/ImageViewer/ImageComponent.ts index 47249811f..68d575775 100644 --- a/app/presentation/ImageViewer/ImageComponent.ts +++ b/app/presentation/ImageViewer/ImageComponent.ts @@ -1,6 +1,10 @@ +import React from 'react'; +import { Image } from 'react-native'; +import { FastImageProps } from '@rocket.chat/react-native-fast-image'; + import { types } from './types'; -export const ImageComponent = (type: string) => { +export const ImageComponent = (type?: string): React.ComponentType<Partial<Image> | FastImageProps> => { let Component; if (type === types.REACT_NATIVE_IMAGE) { const { Image } = require('react-native'); diff --git a/app/presentation/ImageViewer/ImageViewer.ios.tsx b/app/presentation/ImageViewer/ImageViewer.tsx similarity index 89% rename from app/presentation/ImageViewer/ImageViewer.ios.tsx rename to app/presentation/ImageViewer/ImageViewer.tsx index a666ee38a..117bf95dd 100644 --- a/app/presentation/ImageViewer/ImageViewer.ios.tsx +++ b/app/presentation/ImageViewer/ImageViewer.tsx @@ -16,13 +16,14 @@ const styles = StyleSheet.create({ interface IImageViewer { uri: string; - imageComponentType: string; + imageComponentType?: string; width: number; height: number; theme: string; + onLoadEnd?: () => void; } -export const ImageViewer = ({ uri, imageComponentType, theme, width, height, ...props }: IImageViewer) => { +export const ImageViewer = ({ uri, imageComponentType, theme, width, height, ...props }: IImageViewer): JSX.Element => { const backgroundColor = themes[theme].previewBackground; const Component = ImageComponent(imageComponentType); return ( diff --git a/app/presentation/ImageViewer/index.ts b/app/presentation/ImageViewer/index.ts index d8ae350b5..bf629ae84 100644 --- a/app/presentation/ImageViewer/index.ts +++ b/app/presentation/ImageViewer/index.ts @@ -1,5 +1,3 @@ -// @ts-ignore -// eslint-disable-next-line import/no-unresolved export * from './ImageViewer'; export * from './types'; export * from './ImageComponent'; diff --git a/app/presentation/KeyboardView.tsx b/app/presentation/KeyboardView.tsx index e5a4d3910..5319df82b 100644 --- a/app/presentation/KeyboardView.tsx +++ b/app/presentation/KeyboardView.tsx @@ -1,14 +1,12 @@ import React from 'react'; -import { KeyboardAwareScrollView } from '@codler/react-native-keyboard-aware-scroll-view'; +import { KeyboardAwareScrollView, KeyboardAwareScrollViewProps } from '@codler/react-native-keyboard-aware-scroll-view'; import scrollPersistTaps from '../utils/scrollPersistTaps'; -interface IKeyboardViewProps { - style: any; - contentContainerStyle: any; +interface IKeyboardViewProps extends KeyboardAwareScrollViewProps { keyboardVerticalOffset: number; - scrollEnabled: boolean; - children: JSX.Element; + scrollEnabled?: boolean; + children: React.ReactNode; } export default class KeyboardView extends React.PureComponent<IKeyboardViewProps, any> { @@ -22,9 +20,7 @@ export default class KeyboardView extends React.PureComponent<IKeyboardViewProps contentContainerStyle={contentContainerStyle} scrollEnabled={scrollEnabled} alwaysBounceVertical={false} - extraHeight={keyboardVerticalOffset} - // @ts-ignore - behavior='position'> + extraHeight={keyboardVerticalOffset}> {children} </KeyboardAwareScrollView> ); diff --git a/app/presentation/UserItem.tsx b/app/presentation/UserItem.tsx index 56e6b9fcb..3dadc2bf7 100644 --- a/app/presentation/UserItem.tsx +++ b/app/presentation/UserItem.tsx @@ -1,6 +1,6 @@ import React from 'react'; // @ts-ignore -import { Pressable, StyleSheet, Text, View } from 'react-native'; +import { Pressable, StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'; import Avatar from '../containers/Avatar'; import { CustomIcon } from '../lib/Icons'; @@ -44,8 +44,8 @@ interface IUserItem { username: string; onPress(): void; testID: string; - onLongPress(): void; - style: any; + onLongPress?: () => void; + style?: StyleProp<ViewStyle>; icon: string; theme: string; } diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index 101747ccd..240771b3e 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -122,6 +122,11 @@ const handleOpen = function* handleOpen({ params }) { host = id; } }); + + if (!host && params.fullURL) { + RocketChat.callJitsiWithoutServer(params.fullURL); + return; + } } if (params.type === 'oauth') { diff --git a/app/utils/fileDownload/index.ts b/app/utils/fileDownload/index.ts new file mode 100644 index 000000000..dda1a78ff --- /dev/null +++ b/app/utils/fileDownload/index.ts @@ -0,0 +1,59 @@ +import RNFetchBlob, { FetchBlobResponse } from 'rn-fetch-blob'; +import FileViewer from 'react-native-file-viewer'; + +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; +} + +export const getLocalFilePathFromFile = (localPath: string, attachment: IAttachment): string => `${localPath}${attachment.title}`; + +export const fileDownload = (url: string, attachment: IAttachment): Promise<FetchBlobResponse> => { + const path = getLocalFilePathFromFile(DOWNLOAD_PATH, attachment); + + const options = { + path, + timeout: 10000, + indicator: true, + overwrite: true, + addAndroidDownloads: { + path, + notification: true, + useDownloadManager: true + } + }; + + return RNFetchBlob.config(options).fetch('GET', url); +}; + +export const fileDownloadAndPreview = async (url: string, attachment: IAttachment): Promise<void> => { + try { + const path = getLocalFilePathFromFile(DOCUMENTS_PATH, attachment); + const file = await RNFetchBlob.config({ + timeout: 10000, + indicator: true, + path + }).fetch('GET', url); + + FileViewer.open(file.data, { + showOpenWithDialog: true, + showAppsSuggestions: true + }) + .then(res => res) + .catch(async () => { + const file = await fileDownload(url, attachment); + file + ? EventEmitter.emit(LISTENER, { message: I18n.t('Downloaded_file') }) + : EventEmitter.emit(LISTENER, { message: I18n.t('Error_Download_file') }); + }); + } catch (e) { + EventEmitter.emit(LISTENER, { message: I18n.t('Error_Download_file') }); + } +}; diff --git a/app/utils/log/events.js b/app/utils/log/events.js index 505a42e4c..fc7a3497a 100644 --- a/app/utils/log/events.js +++ b/app/utils/log/events.js @@ -155,6 +155,9 @@ export default { SE_CLEAR_LOCAL_SERVER_CACHE: 'se_clear_local_server_cache', SE_LOG_OUT: 'se_log_out', + // USER PREFERENCE VIEW + UP_GO_USER_NOTIFICATION_PREF: 'up_go_user_notification_pref', + // SECURITY PRIVACY VIEW SP_GO_E2EENCRYPTIONSECURITY: 'sp_go_e2e_encryption_security', SP_GO_SCREENLOCKCONFIG: 'sp_go_screen_lock_cfg', diff --git a/app/utils/scrollPersistTaps.js b/app/utils/scrollPersistTaps.js deleted file mode 100644 index a08e17af8..000000000 --- a/app/utils/scrollPersistTaps.js +++ /dev/null @@ -1,4 +0,0 @@ -export default { - keyboardShouldPersistTaps: 'always', - keyboardDismissMode: 'interactive' -}; diff --git a/app/utils/scrollPersistTaps.ts b/app/utils/scrollPersistTaps.ts new file mode 100644 index 000000000..c625ac2a9 --- /dev/null +++ b/app/utils/scrollPersistTaps.ts @@ -0,0 +1,8 @@ +import { KeyboardAwareScrollViewProps } from '@codler/react-native-keyboard-aware-scroll-view'; + +const scrollPersistTaps: Partial<KeyboardAwareScrollViewProps> = { + keyboardShouldPersistTaps: 'always', + keyboardDismissMode: 'interactive' +}; + +export default scrollPersistTaps; diff --git a/app/views/AddChannelTeamView.js b/app/views/AddChannelTeamView.tsx similarity index 77% rename from app/views/AddChannelTeamView.js rename to app/views/AddChannelTeamView.tsx index b93f5fb78..d477f9bad 100644 --- a/app/views/AddChannelTeamView.js +++ b/app/views/AddChannelTeamView.tsx @@ -1,5 +1,6 @@ import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import { connect } from 'react-redux'; import * as List from '../containers/List'; @@ -9,8 +10,16 @@ import * as HeaderButton from '../containers/HeaderButton'; import SafeAreaView from '../containers/SafeAreaView'; import I18n from '../i18n'; -const setHeader = (navigation, isMasterDetail) => { - const options = { +type TNavigation = StackNavigationProp<any, 'AddChannelTeamView'>; + +interface IAddChannelTeamView { + route: RouteProp<{ AddChannelTeamView: { teamId: string; teamChannels: object[] } }, 'AddChannelTeamView'>; + navigation: TNavigation; + isMasterDetail: boolean; +} + +const setHeader = (navigation: TNavigation, isMasterDetail: boolean) => { + const options: StackNavigationOptions = { headerTitle: I18n.t('Add_Channel_to_Team') }; @@ -21,7 +30,7 @@ const setHeader = (navigation, isMasterDetail) => { navigation.setOptions(options); }; -const AddChannelTeamView = ({ navigation, route, isMasterDetail }) => { +const AddChannelTeamView = ({ navigation, route, isMasterDetail }: IAddChannelTeamView) => { const { teamId, teamChannels } = route.params; const { theme } = useTheme(); @@ -66,13 +75,7 @@ const AddChannelTeamView = ({ navigation, route, isMasterDetail }) => { ); }; -AddChannelTeamView.propTypes = { - route: PropTypes.object, - navigation: PropTypes.object, - isMasterDetail: PropTypes.bool -}; - -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ isMasterDetail: state.app.isMasterDetail }); diff --git a/app/views/AddExistingChannelView.js b/app/views/AddExistingChannelView.tsx similarity index 78% rename from app/views/AddExistingChannelView.js rename to app/views/AddExistingChannelView.tsx index ed9a95ac4..5efdbf34d 100644 --- a/app/views/AddExistingChannelView.js +++ b/app/views/AddExistingChannelView.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import { FlatList, View } from 'react-native'; import { connect } from 'react-redux'; import { Q } from '@nozbe/watermelondb'; @@ -21,18 +22,27 @@ import { goRoom } from '../utils/goRoom'; import { showErrorAlert } from '../utils/info'; import debounce from '../utils/debounce'; +interface IAddExistingChannelViewState { + // TODO: refactor with Room Model + search: any[]; + channels: any[]; + selected: string[]; + loading: boolean; +} + +interface IAddExistingChannelViewProps { + navigation: StackNavigationProp<any, 'AddExistingChannelView'>; + route: RouteProp<{ AddExistingChannelView: { teamId: string } }, 'AddExistingChannelView'>; + theme: string; + isMasterDetail: boolean; + addTeamChannelPermission: string[]; +} + const QUERY_SIZE = 50; -class AddExistingChannelView extends React.Component { - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string, - isMasterDetail: PropTypes.bool, - addTeamChannelPermission: PropTypes.array - }; - - constructor(props) { +class AddExistingChannelView extends React.Component<IAddExistingChannelViewProps, IAddExistingChannelViewState> { + private teamId: string; + constructor(props: IAddExistingChannelViewProps) { super(props); this.query(); this.teamId = props.route?.params?.teamId; @@ -49,7 +59,7 @@ class AddExistingChannelView extends React.Component { const { navigation, isMasterDetail } = this.props; const { selected } = this.state; - const options = { + const options: StackNavigationOptions = { headerTitle: I18n.t('Add_Existing_Channel') }; @@ -82,9 +92,10 @@ class AddExistingChannelView extends React.Component { ) .fetch(); - const asyncFilter = async channelsArray => { + // TODO: Refactor with Room Model + const asyncFilter = async (channelsArray: any[]) => { const results = await Promise.all( - channelsArray.map(async channel => { + channelsArray.map(async (channel: any) => { if (channel.prid) { return false; } @@ -96,7 +107,7 @@ class AddExistingChannelView extends React.Component { }) ); - return channelsArray.filter((_v, index) => results[index]); + return channelsArray.filter((_v: any, index: number) => results[index]); }; const channelFiltered = await asyncFilter(channels); this.setState({ channels: channelFiltered }); @@ -105,7 +116,7 @@ class AddExistingChannelView extends React.Component { } }; - onSearchChangeText = debounce(text => { + onSearchChangeText = debounce((text: string) => { this.query(text); }, 300); @@ -126,7 +137,7 @@ class AddExistingChannelView extends React.Component { this.setState({ loading: false }); goRoom({ item: result, isMasterDetail }); } - } catch (e) { + } catch (e: any) { logEvent(events.CT_ADD_ROOM_TO_TEAM_F); showErrorAlert(I18n.t(e.data.error), I18n.t('Add_Existing_Channel'), () => {}); this.setState({ loading: false }); @@ -137,17 +148,17 @@ class AddExistingChannelView extends React.Component { const { theme } = this.props; return ( <View style={{ backgroundColor: themes[theme].auxiliaryBackground }}> - <SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='add-existing-channel-view-search' /> + <SearchBox onChangeText={(text: string) => this.onSearchChangeText(text)} testID='add-existing-channel-view-search' /> </View> ); }; - isChecked = rid => { + isChecked = (rid: string) => { const { selected } = this.state; return selected.includes(rid); }; - toggleChannel = rid => { + toggleChannel = (rid: string) => { const { selected } = this.state; animateNextTransition(); @@ -161,7 +172,8 @@ class AddExistingChannelView extends React.Component { } }; - renderItem = ({ item }) => { + // TODO: refactor with Room Model + renderItem = ({ item }: { item: any }) => { const isChecked = this.isChecked(item.rid); // TODO: reuse logic inside RoomTypeIcon const icon = item.t === 'p' && !item.teamId ? 'channel-private' : 'channel-public'; @@ -207,7 +219,7 @@ class AddExistingChannelView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ isMasterDetail: state.app.isMasterDetail, addTeamChannelPermission: state.permissions['add-team-channel'] }); diff --git a/app/views/AttachmentView.js b/app/views/AttachmentView.tsx similarity index 76% rename from app/views/AttachmentView.js rename to app/views/AttachmentView.tsx index 8f199b237..90adf8b42 100644 --- a/app/views/AttachmentView.js +++ b/app/views/AttachmentView.tsx @@ -1,12 +1,13 @@ import React from 'react'; import { PermissionsAndroid, StyleSheet, View } from 'react-native'; import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import CameraRoll from '@react-native-community/cameraroll'; import * as mime from 'react-native-mime-types'; import RNFetchBlob from 'rn-fetch-blob'; import { Video } from 'expo-av'; -import SHA256 from 'js-sha256'; +import { sha256 } from 'js-sha256'; import { withSafeAreaInsets } from 'react-native-safe-area-context'; import { LISTENER } from '../containers/Toast'; @@ -30,23 +31,41 @@ const styles = StyleSheet.create({ } }); -class AttachmentView extends React.Component { - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string, - baseUrl: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - insets: PropTypes.object, - user: PropTypes.shape({ - id: PropTypes.string, - token: PropTypes.string - }), - Allow_Save_Media_to_Gallery: PropTypes.bool - }; +// 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; +} - constructor(props) { +interface IAttachmentViewState { + attachment: IAttachment; + loading: boolean; +} + +interface IAttachmentViewProps { + navigation: StackNavigationProp<any, 'AttachmentView'>; + route: RouteProp<{ AttachmentView: { attachment: IAttachment } }, 'AttachmentView'>; + theme: string; + baseUrl: string; + width: number; + height: number; + insets: { left: number; bottom: number; right: number; top: number }; + user: { + id: string; + token: string; + }; + Allow_Save_Media_to_Gallery: boolean; +} + +class AttachmentView extends React.Component<IAttachmentViewProps, IAttachmentViewState> { + private unsubscribeBlur: (() => void) | undefined; + private videoRef: any; + + constructor(props: IAttachmentViewProps) { super(props); const attachment = props.route.params?.attachment; this.state = { attachment, loading: true }; @@ -79,21 +98,9 @@ class AttachmentView extends React.Component { } const options = { title, - headerLeft: () => ( - <HeaderButton.CloseModal - testID='close-attachment-view' - navigation={navigation} - buttonStyle={{ color: themes[theme].previewTintColor }} - /> - ), + headerLeft: () => <HeaderButton.CloseModal testID='close-attachment-view' navigation={navigation} />, headerRight: () => - Allow_Save_Media_to_Gallery ? ( - <HeaderButton.Download - testID='save-image' - onPress={this.handleSave} - buttonStyle={{ color: themes[theme].previewTintColor }} - /> - ) : null, + Allow_Save_Media_to_Gallery ? <HeaderButton.Download testID='save-image' onPress={this.handleSave} /> : null, headerBackground: () => <View style={{ flex: 1, backgroundColor: themes[theme].previewBackground }} />, headerTintColor: themes[theme].previewTintColor, headerTitleStyle: { color: themes[theme].previewTintColor, marginHorizontal: 10 } @@ -101,7 +108,7 @@ class AttachmentView extends React.Component { navigation.setOptions(options); }; - getVideoRef = ref => (this.videoRef = ref); + getVideoRef = (ref: Video) => (this.videoRef = ref); handleSave = async () => { const { attachment } = this.state; @@ -113,7 +120,8 @@ class AttachmentView extends React.Component { if (isAndroid) { const rationale = { title: I18n.t('Write_External_Permission'), - message: I18n.t('Write_External_Permission_Message') + message: I18n.t('Write_External_Permission_Message'), + buttonPositive: 'Ok' }; const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, rationale); if (!(result || result === PermissionsAndroid.RESULTS.GRANTED)) { @@ -125,7 +133,7 @@ class AttachmentView extends React.Component { try { const extension = image_url ? `.${mime.extension(image_type) || 'jpg'}` : `.${mime.extension(video_type) || 'mp4'}`; const documentDir = `${RNFetchBlob.fs.dirs.DocumentDir}/`; - const path = `${documentDir + SHA256(url) + extension}`; + const path = `${documentDir + sha256(url!) + extension}`; const file = await RNFetchBlob.config({ path }).fetch('GET', mediaAttachment); await CameraRoll.save(path, { album: 'Rocket.Chat' }); await file.flush(); @@ -136,7 +144,7 @@ class AttachmentView extends React.Component { this.setState({ loading: false }); }; - renderImage = uri => { + renderImage = (uri: string) => { const { theme, width, height, insets } = this.props; const headerHeight = getHeaderHeight(width > height); return ( @@ -150,7 +158,7 @@ class AttachmentView extends React.Component { ); }; - renderVideo = uri => ( + renderVideo = (uri: string) => ( <Video source={{ uri }} rate={1.0} @@ -190,7 +198,7 @@ class AttachmentView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ baseUrl: state.server.server, user: getUserSelector(state), Allow_Save_Media_to_Gallery: state.settings.Allow_Save_Media_to_Gallery ?? true diff --git a/app/views/ChangePasscodeView.js b/app/views/ChangePasscodeView.tsx similarity index 78% rename from app/views/ChangePasscodeView.js rename to app/views/ChangePasscodeView.tsx index ef29abe18..5ed58a27d 100644 --- a/app/views/ChangePasscodeView.js +++ b/app/views/ChangePasscodeView.tsx @@ -1,15 +1,13 @@ import React, { useEffect, useState } from 'react'; import { StyleSheet } from 'react-native'; -import PropTypes from 'prop-types'; import Orientation from 'react-native-orientation-locker'; import useDeepCompareEffect from 'use-deep-compare-effect'; import isEmpty from 'lodash/isEmpty'; import Modal from 'react-native-modal'; import Touchable from 'react-native-platform-touchable'; -import { withTheme } from '../theme'; +import { useTheme } from '../theme'; import { hasNotch, isTablet } from '../utils/deviceInfo'; -import { TYPE } from '../containers/Passcode/constants'; import { PasscodeChoose } from '../containers/Passcode'; import EventEmitter from '../utils/events'; import { CustomIcon } from '../lib/Icons'; @@ -27,9 +25,17 @@ const styles = StyleSheet.create({ } }); -const ChangePasscodeView = React.memo(({ theme }) => { +interface IArgs { + submit(passcode: string): void; + cancel(): void; + force: boolean; +} + +const ChangePasscodeView = React.memo(() => { const [visible, setVisible] = useState(false); - const [data, setData] = useState({}); + const [data, setData] = useState<Partial<IArgs>>({}); + + const { theme } = useTheme(); useDeepCompareEffect(() => { if (!isEmpty(data)) { @@ -39,11 +45,11 @@ const ChangePasscodeView = React.memo(({ theme }) => { } }, [data]); - const showChangePasscode = args => { + const showChangePasscode = (args: IArgs) => { setData(args); }; - const onSubmit = passcode => { + const onSubmit = (passcode: string) => { const { submit } = data; if (submit) { submit(passcode); @@ -74,7 +80,7 @@ const ChangePasscodeView = React.memo(({ theme }) => { return ( <Modal useNativeDriver isVisible={visible} hideModalContentWhileAnimating style={styles.modal}> - <PasscodeChoose theme={theme} type={TYPE.choose} finishProcess={onSubmit} force={data?.force} /> + <PasscodeChoose theme={theme} finishProcess={onSubmit} force={data?.force} /> {!data?.force ? ( <Touchable onPress={onCancel} style={styles.close}> <CustomIcon name='close' color={themes[theme].passcodePrimary} size={30} /> @@ -84,8 +90,4 @@ const ChangePasscodeView = React.memo(({ theme }) => { ); }); -ChangePasscodeView.propTypes = { - theme: PropTypes.string -}; - -export default withTheme(ChangePasscodeView); +export default ChangePasscodeView; diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.tsx similarity index 82% rename from app/views/CreateChannelView.js rename to app/views/CreateChannelView.tsx index 0aa3c7035..45b2cc2f2 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { FlatList, ScrollView, StyleSheet, Switch, Text, View } from 'react-native'; +import { Dispatch } from 'redux'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; +import { FlatList, ScrollView, StyleSheet, Switch, Text, View, SwitchProps } from 'react-native'; import { dequal } from 'dequal'; import * as List from '../containers/List'; @@ -66,30 +68,59 @@ const styles = StyleSheet.create({ } }); -class CreateChannelView extends React.Component { - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - baseUrl: PropTypes.string, - create: PropTypes.func.isRequired, - removeUser: PropTypes.func.isRequired, - error: PropTypes.object, - failure: PropTypes.bool, - isFetching: PropTypes.bool, - encryptionEnabled: PropTypes.bool, - users: PropTypes.array.isRequired, - user: PropTypes.shape({ - id: PropTypes.string, - token: PropTypes.string, - roles: PropTypes.array - }), - theme: PropTypes.string, - teamId: PropTypes.string, - createPublicChannelPermission: PropTypes.array, - createPrivateChannelPermission: PropTypes.array - }; +interface IOtherUser { + _id: string; + name: string; + fname: string; +} - constructor(props) { +interface ICreateFunction extends Omit<ICreateChannelViewState, 'channelName' | 'permissions'> { + name: string; + users: string[]; + teamId: string; +} + +interface ICreateChannelViewState { + channelName: string; + type: boolean; + readOnly: boolean; + encrypted: boolean; + broadcast: boolean; + isTeam: boolean; + permissions: boolean[]; +} + +interface ICreateChannelViewProps { + navigation: StackNavigationProp<any, 'CreateChannelView'>; + route: RouteProp<{ CreateChannelView: { isTeam: boolean; teamId: string } }, 'CreateChannelView'>; + baseUrl: string; + create: (data: ICreateFunction) => void; + removeUser: (user: IOtherUser) => void; + error: object; + failure: boolean; + isFetching: boolean; + encryptionEnabled: boolean; + users: IOtherUser[]; + user: { + id: string; + token: string; + roles: string[]; + }; + theme: string; + teamId: string; + createPublicChannelPermission: string[]; + createPrivateChannelPermission: string[]; +} + +interface ISwitch extends SwitchProps { + id: string; + label: string; +} + +class CreateChannelView extends React.Component<ICreateChannelViewProps, ICreateChannelViewState> { + private teamId: string; + + constructor(props: ICreateChannelViewProps) { super(props); const { route } = this.props; const isTeam = route?.params?.isTeam || false; @@ -110,7 +141,7 @@ class CreateChannelView extends React.Component { this.handleHasPermission(); } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: ICreateChannelViewProps, nextState: ICreateChannelViewState) { const { channelName, type, readOnly, broadcast, encrypted, permissions } = this.state; const { users, isFetching, encryptionEnabled, theme, createPublicChannelPermission, createPrivateChannelPermission } = this.props; @@ -153,7 +184,7 @@ class CreateChannelView extends React.Component { return false; } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: ICreateChannelViewProps) { const { createPublicChannelPermission, createPrivateChannelPermission } = this.props; if ( !dequal(createPublicChannelPermission, prevProps.createPublicChannelPermission) || @@ -172,7 +203,7 @@ class CreateChannelView extends React.Component { }); }; - toggleRightButton = channelName => { + toggleRightButton = (channelName: string) => { const { navigation } = this.props; navigation.setOptions({ headerRight: () => @@ -184,7 +215,7 @@ class CreateChannelView extends React.Component { }); }; - onChangeText = channelName => { + onChangeText = (channelName: string) => { this.toggleRightButton(channelName); this.setState({ channelName }); }; @@ -215,13 +246,13 @@ class CreateChannelView extends React.Component { Review.pushPositiveEvent(); }; - removeUser = user => { + removeUser = (user: IOtherUser) => { logEvent(events.CR_REMOVE_USER); const { removeUser } = this.props; removeUser(user); }; - renderSwitch = ({ id, value, label, onValueChange, disabled = false }) => { + renderSwitch = ({ id, value, label, onValueChange, disabled = false }: ISwitch) => { const { theme } = this.props; return ( <View style={[styles.switchContainer, { backgroundColor: themes[theme].backgroundColor }]}> @@ -253,7 +284,7 @@ class CreateChannelView extends React.Component { value: permissions[1] ? type : false, disabled: isDisabled, label: isTeam ? 'Private_Team' : 'Private_Channel', - onValueChange: value => { + onValueChange: (value: boolean) => { logEvent(events.CR_TOGGLE_TYPE); // If we set the channel as public, encrypted status should be false this.setState(({ encrypted }) => ({ type: value, encrypted: value && encrypted })); @@ -313,8 +344,8 @@ class CreateChannelView extends React.Component { }); } - renderItem = ({ item }) => { - const { baseUrl, user, theme } = this.props; + renderItem = ({ item }: { item: IOtherUser }) => { + const { theme } = this.props; return ( <UserItem @@ -323,8 +354,6 @@ class CreateChannelView extends React.Component { onPress={() => this.removeUser(item)} testID={`create-channel-view-item-${item.name}`} icon='check' - baseUrl={baseUrl} - user={user} theme={theme} /> ); @@ -348,7 +377,6 @@ class CreateChannelView extends React.Component { ]} renderItem={this.renderItem} ItemSeparatorComponent={List.Separator} - enableEmptySections keyboardShouldPersistTaps='always' /> ); @@ -371,7 +399,6 @@ class CreateChannelView extends React.Component { <TextInput autoFocus style={[styles.input, { backgroundColor: themes[theme].backgroundColor }]} - label={isTeam ? I18n.t('Team_Name') : I18n.t('Channel_Name')} value={channelName} onChangeText={this.onChangeText} placeholder={isTeam ? I18n.t('Team_Name') : I18n.t('Channel_Name')} @@ -406,7 +433,7 @@ class CreateChannelView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ baseUrl: state.server.server, isFetching: state.createChannel.isFetching, encryptionEnabled: state.encryption.enabled, @@ -416,9 +443,9 @@ const mapStateToProps = state => ({ createPrivateChannelPermission: state.permissions['create-p'] }); -const mapDispatchToProps = dispatch => ({ - create: data => dispatch(createChannelRequestAction(data)), - removeUser: user => dispatch(removeUserAction(user)) +const mapDispatchToProps = (dispatch: Dispatch) => ({ + create: (data: ICreateFunction) => dispatch(createChannelRequestAction(data)), + removeUser: (user: IOtherUser) => dispatch(removeUserAction(user)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(CreateChannelView)); diff --git a/app/views/CreateDiscussionView/index.tsx b/app/views/CreateDiscussionView/index.tsx index 98b461f0a..2933b2c78 100644 --- a/app/views/CreateDiscussionView/index.tsx +++ b/app/views/CreateDiscussionView/index.tsx @@ -150,14 +150,12 @@ class CreateChannelView extends React.Component<ICreateChannelViewProps, any> { const { name, users, encrypted } = this.state; const { server, user, loading, blockUnauthenticatedAccess, theme, serverVersion } = this.props; return ( - // @ts-ignore <KeyboardView style={{ backgroundColor: themes[theme].auxiliaryBackground }} contentContainerStyle={styles.container} keyboardVerticalOffset={128}> <StatusBar /> <SafeAreaView testID='create-discussion-view' style={styles.container}> - {/* @ts-ignore*/} <ScrollView {...scrollPersistTaps}> <Text style={[styles.description, { color: themes[theme].auxiliaryText }]}>{I18n.t('Discussion_Desc')}</Text> <SelectChannel diff --git a/app/views/DefaultBrowserView.js b/app/views/DefaultBrowserView.tsx similarity index 78% rename from app/views/DefaultBrowserView.js rename to app/views/DefaultBrowserView.tsx index 245e16c82..0282e0df2 100644 --- a/app/views/DefaultBrowserView.js +++ b/app/views/DefaultBrowserView.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions } from '@react-navigation/stack'; import { FlatList, Linking } from 'react-native'; import I18n from '../i18n'; @@ -13,7 +13,14 @@ import SafeAreaView from '../containers/SafeAreaView'; import UserPreferences from '../lib/userPreferences'; import { events, logEvent } from '../utils/log'; -const DEFAULT_BROWSERS = [ +type TValue = 'inApp' | 'systemDefault:' | 'googlechrome:' | 'firefox:' | 'brave:'; + +interface IBrowsersValues { + title: string; + value: TValue; +} + +const DEFAULT_BROWSERS: IBrowsersValues[] = [ { title: 'In_app', value: 'inApp' @@ -24,7 +31,7 @@ const DEFAULT_BROWSERS = [ } ]; -const BROWSERS = [ +const BROWSERS: IBrowsersValues[] = [ { title: 'Chrome', value: 'googlechrome:' @@ -39,16 +46,23 @@ const BROWSERS = [ } ]; -class DefaultBrowserView extends React.Component { - static navigationOptions = () => ({ +interface IDefaultBrowserViewState { + browser: any; + supported: any[]; +} + +interface IDefaultBrowserViewProps { + theme: string; +} + +class DefaultBrowserView extends React.Component<IDefaultBrowserViewProps, IDefaultBrowserViewState> { + private mounted?: boolean; + + static navigationOptions = (): StackNavigationOptions => ({ title: I18n.t('Default_browser') }); - static propTypes = { - theme: PropTypes.string - }; - - constructor(props) { + constructor(props: IDefaultBrowserViewProps) { super(props); this.state = { browser: null, @@ -74,6 +88,7 @@ class DefaultBrowserView extends React.Component { this.setState(({ supported }) => ({ supported: [...supported, browser] })); } else { const { supported } = this.state; + // @ts-ignore this.state.supported = [...supported, browser]; } } @@ -81,7 +96,7 @@ class DefaultBrowserView extends React.Component { }); }; - isSelected = value => { + isSelected = (value: TValue) => { const { browser } = this.state; if (!browser && value === 'systemDefault:') { return true; @@ -89,7 +104,7 @@ class DefaultBrowserView extends React.Component { return browser === value; }; - changeDefaultBrowser = async newBrowser => { + changeDefaultBrowser = async (newBrowser: TValue) => { logEvent(events.DB_CHANGE_DEFAULT_BROWSER, { browser: newBrowser }); try { const browser = newBrowser !== 'systemDefault:' ? newBrowser : null; @@ -105,7 +120,7 @@ class DefaultBrowserView extends React.Component { return <List.Icon name='check' color={themes[theme].tintColor} />; }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: IBrowsersValues }) => { const { title, value } = item; return ( <List.Item diff --git a/app/views/DisplayPrefsView.js b/app/views/DisplayPrefsView.js index d1e29d139..09da4edc9 100644 --- a/app/views/DisplayPrefsView.js +++ b/app/views/DisplayPrefsView.js @@ -21,19 +21,19 @@ const DisplayPrefsView = props => { const { theme } = useTheme(); const { sortBy, groupByType, showFavorites, showUnread, showAvatar, displayMode } = useSelector(state => state.sortPreferences); + const { isMasterDetail } = useSelector(state => state.app); const dispatch = useDispatch(); useEffect(() => { - const { navigation, isMasterDetail } = props; + const { navigation } = props; navigation.setOptions({ - title: I18n.t('Display'), - headerLeft: () => - isMasterDetail ? ( - <HeaderButton.CloseModal navigation={navigation} testID='display-view-close' /> - ) : ( - <HeaderButton.Drawer navigation={navigation} testID='display-view-drawer' /> - ) + title: I18n.t('Display') }); + if (!isMasterDetail) { + navigation.setOptions({ + headerLeft: () => <HeaderButton.Drawer navigation={navigation} testID='display-view-drawer' /> + }); + } }, []); const setSortPreference = async param => { diff --git a/app/views/E2EEncryptionSecurityView.js b/app/views/E2EEncryptionSecurityView.tsx similarity index 81% rename from app/views/E2EEncryptionSecurityView.js rename to app/views/E2EEncryptionSecurityView.tsx index 67a42453b..d5b0b27a2 100644 --- a/app/views/E2EEncryptionSecurityView.js +++ b/app/views/E2EEncryptionSecurityView.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; -import PropTypes from 'prop-types'; +import { StyleSheet, Text, View, TextInput as TextInputComp } from 'react-native'; +import { StackNavigationOptions } from '@react-navigation/stack'; import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; import StatusBar from '../containers/StatusBar'; import * as List from '../containers/List'; @@ -41,20 +42,41 @@ const styles = StyleSheet.create({ } }); -class E2EEncryptionSecurityView extends React.Component { +interface IE2EEncryptionSecurityViewState { + newPassword: string; +} + +interface IE2EEncryptionSecurityViewProps { + theme: string; + user: { + roles: string[]; + id: string; + }; + server: string; + encryptionEnabled: boolean; + logout(): void; +} + +class E2EEncryptionSecurityView extends React.Component<IE2EEncryptionSecurityViewProps, IE2EEncryptionSecurityViewState> { + private newPasswordInputRef: any = React.createRef(); + + static navigationOptions = (): StackNavigationOptions => ({ + title: I18n.t('E2E_Encryption') + }); + state = { newPassword: '' }; - newPasswordInputRef = React.createRef(); + onChangePasswordText = debounce((text: string) => this.setState({ newPassword: text }), 300); - onChangePasswordText = debounce(text => this.setState({ newPassword: text }), 300); - - setNewPasswordRef = ref => (this.newPasswordInputRef = ref); + setNewPasswordRef = (ref: TextInputComp) => (this.newPasswordInputRef = ref); changePassword = () => { const { newPassword } = this.state; if (!newPassword.trim()) { return; } + // TODO: Remove ts-ignore when migrate the showConfirmationAlert + // @ts-ignore showConfirmationAlert({ title: I18n.t('Are_you_sure_question_mark'), message: I18n.t('E2E_encryption_change_password_message'), @@ -76,6 +98,8 @@ class E2EEncryptionSecurityView extends React.Component { }; resetOwnKey = () => { + // TODO: Remove ts-ignore when migrate the showConfirmationAlert + // @ts-ignore showConfirmationAlert({ title: I18n.t('Are_you_sure_question_mark'), message: I18n.t('E2E_encryption_reset_message'), @@ -170,29 +194,14 @@ class E2EEncryptionSecurityView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ server: state.server.server, user: getUserSelector(state), encryptionEnabled: state.encryption.enabled }); -const mapDispatchToProps = dispatch => ({ +const mapDispatchToProps = (dispatch: Dispatch) => ({ logout: () => dispatch(logoutAction(true)) }); -E2EEncryptionSecurityView.navigationOptions = () => ({ - title: I18n.t('E2E_Encryption') -}); - -E2EEncryptionSecurityView.propTypes = { - theme: PropTypes.string, - user: PropTypes.shape({ - roles: PropTypes.array, - id: PropTypes.string - }), - server: PropTypes.string, - encryptionEnabled: PropTypes.bool, - logout: PropTypes.func -}; - export default connect(mapStateToProps, mapDispatchToProps)(withTheme(E2EEncryptionSecurityView)); diff --git a/app/views/E2EEnterYourPasswordView.js b/app/views/E2EEnterYourPasswordView.tsx similarity index 75% rename from app/views/E2EEnterYourPasswordView.js rename to app/views/E2EEnterYourPasswordView.tsx index ebf5a890d..dd9cdfa8a 100644 --- a/app/views/E2EEnterYourPasswordView.js +++ b/app/views/E2EEnterYourPasswordView.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { ScrollView, StyleSheet, Text } from 'react-native'; import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import I18n from '../i18n'; import { withTheme } from '../theme'; @@ -27,18 +28,26 @@ const styles = StyleSheet.create({ ...sharedStyles.textRegular } }); -class E2EEnterYourPasswordView extends React.Component { - static navigationOptions = ({ navigation }) => ({ + +interface IE2EEnterYourPasswordViewState { + password: string; +} + +interface IE2EEnterYourPasswordViewProps { + encryptionDecodeKey: (password: string) => void; + theme: string; + navigation: StackNavigationProp<any, 'E2EEnterYourPasswordView'>; +} + +class E2EEnterYourPasswordView extends React.Component<IE2EEnterYourPasswordViewProps, IE2EEnterYourPasswordViewState> { + private passwordInput?: TextInput; + + static navigationOptions = ({ navigation }: Pick<IE2EEnterYourPasswordViewProps, 'navigation'>): StackNavigationOptions => ({ headerLeft: () => <HeaderButton.CloseModal navigation={navigation} testID='e2e-enter-your-password-view-close' />, title: I18n.t('Enter_Your_E2E_Password') }); - static propTypes = { - encryptionDecodeKey: PropTypes.func, - theme: PropTypes.string - }; - - constructor(props) { + constructor(props: IE2EEnterYourPasswordViewProps) { super(props); this.state = { password: '' @@ -65,12 +74,12 @@ class E2EEnterYourPasswordView extends React.Component { <ScrollView {...scrollPersistTaps} style={sharedStyles.container} - contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}> + contentContainerStyle={sharedStyles.containerScrollView}> <SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} testID='e2e-enter-your-password-view'> <TextInput - inputRef={e => { + inputRef={(e: TextInput) => { this.passwordInput = e; }} placeholder={I18n.t('Password')} @@ -99,7 +108,7 @@ class E2EEnterYourPasswordView extends React.Component { } } -const mapDispatchToProps = dispatch => ({ - encryptionDecodeKey: password => dispatch(encryptionDecodeKeyAction(password)) +const mapDispatchToProps = (dispatch: Dispatch) => ({ + encryptionDecodeKey: (password: string) => dispatch(encryptionDecodeKeyAction(password)) }); export default connect(null, mapDispatchToProps)(withTheme(E2EEnterYourPasswordView)); diff --git a/app/views/E2EHowItWorksView.js b/app/views/E2EHowItWorksView.tsx similarity index 68% rename from app/views/E2EHowItWorksView.js rename to app/views/E2EHowItWorksView.tsx index 0ac1c756b..0fbdf77a1 100644 --- a/app/views/E2EHowItWorksView.js +++ b/app/views/E2EHowItWorksView.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import { StyleSheet } from 'react-native'; import SafeAreaView from '../containers/SafeAreaView'; @@ -21,8 +22,17 @@ const styles = StyleSheet.create({ } }); -class E2EHowItWorksView extends React.Component { - static navigationOptions = ({ route, navigation }) => { +interface INavigation { + navigation: StackNavigationProp<any, 'E2EHowItWorksView'>; + route: RouteProp<{ E2EHowItWorksView: { showCloseModal: boolean } }, 'E2EHowItWorksView'>; +} + +interface IE2EHowItWorksViewProps extends INavigation { + theme: string; +} + +class E2EHowItWorksView extends React.Component<IE2EHowItWorksViewProps, any> { + static navigationOptions = ({ route, navigation }: INavigation) => { const showCloseModal = route.params?.showCloseModal; return { title: I18n.t('How_It_Works'), @@ -30,20 +40,21 @@ class E2EHowItWorksView extends React.Component { }; }; - static propTypes = { - theme: PropTypes.string - }; - render() { const { theme } = this.props; const infoStyle = [styles.info, { color: themes[theme].bodyText }]; + // TODO: Refactor when migrate Markdown return ( <SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} testID='e2e-how-it-works-view'> + {/* @ts-ignore */} <Markdown msg={I18n.t('E2E_How_It_Works_info1')} style={infoStyle} theme={theme} /> + {/* @ts-ignore */} <Markdown msg={I18n.t('E2E_How_It_Works_info2')} style={infoStyle} theme={theme} /> + {/* @ts-ignore */} <Markdown msg={I18n.t('E2E_How_It_Works_info3')} style={infoStyle} theme={theme} /> + {/* @ts-ignore */} <Markdown msg={I18n.t('E2E_How_It_Works_info4')} style={infoStyle} theme={theme} /> </SafeAreaView> ); diff --git a/app/views/E2ESaveYourPasswordView.js b/app/views/E2ESaveYourPasswordView.tsx similarity index 85% rename from app/views/E2ESaveYourPasswordView.js rename to app/views/E2ESaveYourPasswordView.tsx index 167d21dc2..1c4e13a5a 100644 --- a/app/views/E2ESaveYourPasswordView.js +++ b/app/views/E2ESaveYourPasswordView.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { Dispatch } from 'redux'; import { connect } from 'react-redux'; import { Clipboard, ScrollView, StyleSheet, Text, View } from 'react-native'; @@ -53,20 +54,26 @@ const styles = StyleSheet.create({ } }); -class E2ESaveYourPasswordView extends React.Component { - static navigationOptions = ({ navigation }) => ({ +interface IE2ESaveYourPasswordViewState { + password: string; +} + +interface IE2ESaveYourPasswordViewProps { + server: string; + navigation: StackNavigationProp<any, 'E2ESaveYourPasswordView'>; + encryptionSetBanner(): void; + theme: string; +} + +class E2ESaveYourPasswordView extends React.Component<IE2ESaveYourPasswordViewProps, IE2ESaveYourPasswordViewState> { + private mounted: boolean; + + static navigationOptions = ({ navigation }: Pick<IE2ESaveYourPasswordViewProps, 'navigation'>) => ({ headerLeft: () => <HeaderButton.CloseModal navigation={navigation} testID='e2e-save-your-password-view-close' />, title: I18n.t('Save_Your_E2E_Password') }); - static propTypes = { - server: PropTypes.string, - navigation: PropTypes.object, - encryptionSetBanner: PropTypes.func, - theme: PropTypes.string - }; - - constructor(props) { + constructor(props: IE2ESaveYourPasswordViewProps) { super(props); this.mounted = false; this.state = { password: '' }; @@ -83,8 +90,9 @@ class E2ESaveYourPasswordView extends React.Component { // Set stored password on local state const password = await UserPreferences.getStringAsync(`${server}-${E2E_RANDOM_PASSWORD_KEY}`); if (this.mounted) { - this.setState({ password }); + this.setState({ password: password! }); } else { + // @ts-ignore this.state.password = password; } } catch { @@ -164,10 +172,10 @@ class E2ESaveYourPasswordView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ server: state.server.server }); -const mapDispatchToProps = dispatch => ({ +const mapDispatchToProps = (dispatch: Dispatch) => ({ encryptionSetBanner: () => dispatch(encryptionSetBannerAction()) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(E2ESaveYourPasswordView)); diff --git a/app/views/ForgotPasswordView.js b/app/views/ForgotPasswordView.tsx similarity index 79% rename from app/views/ForgotPasswordView.js rename to app/views/ForgotPasswordView.tsx index bd92b56bc..c08a1acdd 100644 --- a/app/views/ForgotPasswordView.js +++ b/app/views/ForgotPasswordView.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Text } from 'react-native'; -import PropTypes from 'prop-types'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import TextInput from '../containers/TextInput'; import Button from '../containers/Button'; @@ -14,23 +15,30 @@ import FormContainer, { FormContainerInner } from '../containers/FormContainer'; import { events, logEvent } from '../utils/log'; import sharedStyles from './Styles'; -class ForgotPasswordView extends React.Component { - static navigationOptions = ({ route }) => ({ +interface IForgotPasswordViewState { + email: string; + invalidEmail: boolean; + isFetching: boolean; +} + +interface IForgotPasswordViewProps { + navigation: StackNavigationProp<any, 'ForgotPasswordView'>; + route: RouteProp<{ ForgotPasswordView: { title: string } }, 'ForgotPasswordView'>; + theme: string; +} + +class ForgotPasswordView extends React.Component<IForgotPasswordViewProps, IForgotPasswordViewState> { + static navigationOptions = ({ route }: Pick<IForgotPasswordViewProps, 'route'>) => ({ title: route.params?.title ?? 'Rocket.Chat' }); - static propTypes = { - navigation: PropTypes.object, - theme: PropTypes.string - }; - state = { email: '', invalidEmail: true, isFetching: false }; - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: IForgotPasswordViewProps, nextState: IForgotPasswordViewState) { const { email, invalidEmail, isFetching } = this.state; const { theme } = this.props; if (nextProps.theme !== theme) { @@ -48,7 +56,7 @@ class ForgotPasswordView extends React.Component { return false; } - validate = email => { + validate = (email: string) => { if (!isValidEmail(email)) { this.setState({ invalidEmail: true }); return; @@ -70,7 +78,7 @@ class ForgotPasswordView extends React.Component { navigation.pop(); showErrorAlert(I18n.t('Forgot_password_If_this_email_is_registered'), I18n.t('Alert')); } - } catch (e) { + } catch (e: any) { logEvent(events.FP_FORGOT_PASSWORD_F); const msg = (e.data && e.data.error) || I18n.t('There_was_an_error_while_action', { action: I18n.t('resetting_password') }); showErrorAlert(msg, I18n.t('Alert')); diff --git a/app/views/ForwardLivechatView.js b/app/views/ForwardLivechatView.tsx similarity index 62% rename from app/views/ForwardLivechatView.js rename to app/views/ForwardLivechatView.tsx index 4f61272d4..42a29782d 100644 --- a/app/views/ForwardLivechatView.js +++ b/app/views/ForwardLivechatView.tsx @@ -1,7 +1,10 @@ import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import { StyleSheet, View } from 'react-native'; +import { Dispatch } from 'redux'; import { connect } from 'react-redux'; +import isEmpty from 'lodash/isEmpty'; import I18n from '../i18n'; import { withTheme } from '../theme'; @@ -10,6 +13,7 @@ import RocketChat from '../lib/rocketchat'; import OrSeparator from '../containers/OrSeparator'; import Input from '../containers/UIKit/MultiSelect/Input'; import { forwardRoom as forwardRoomAction } from '../actions/room'; +import { ILivechatDepartment } from './definition/ILivechatDepartment'; const styles = StyleSheet.create({ container: { @@ -18,12 +22,43 @@ const styles = StyleSheet.create({ } }); -const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }) => { - const [departments, setDepartments] = useState([]); - const [departmentId, setDepartment] = useState(); - const [users, setUsers] = useState([]); +// TODO: Refactor when migrate room +interface IRoom { + departmentId?: any; + servedBy?: { + _id: string; + }; +} + +interface ITransferData { + roomId: string; + userId?: string; + departmentId?: string; +} + +interface IUser { + username: string; + _id: string; +} + +interface IParsedData { + label: string; + value: string; +} + +interface IForwardLivechatViewProps { + navigation: StackNavigationProp<any, 'ForwardLivechatView'>; + route: RouteProp<{ ForwardLivechatView: { rid: string } }, 'ForwardLivechatView'>; + theme: string; + forwardRoom: (rid: string, transferData: ITransferData) => void; +} + +const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }: IForwardLivechatViewProps) => { + const [departments, setDepartments] = useState<IParsedData[]>([]); + const [departmentId, setDepartment] = useState(''); + const [users, setUsers] = useState<IParsedData[]>([]); const [userId, setUser] = useState(); - const [room, setRoom] = useState(); + const [room, setRoom] = useState<IRoom>({}); const rid = route.params?.rid; @@ -31,7 +66,9 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }) => { try { const result = await RocketChat.getDepartments(); if (result.success) { - setDepartments(result.departments.map(department => ({ label: department.name, value: department._id }))); + setDepartments( + result.departments.map((department: ILivechatDepartment) => ({ label: department.name, value: department._id })) + ); } } catch { // do nothing @@ -47,7 +84,7 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }) => { term }); if (result.success) { - const parsedUsers = result.items.map(user => ({ label: user.username, value: user._id })); + const parsedUsers = result.items.map((user: IUser) => ({ label: user.username, value: user._id })); setUsers(parsedUsers); return parsedUsers; } @@ -69,7 +106,7 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }) => { }; const submit = () => { - const transferData = { roomId: rid }; + const transferData: ITransferData = { roomId: rid }; if (!departmentId && !userId) { return; @@ -85,11 +122,14 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }) => { }; useEffect(() => { + navigation.setOptions({ + title: I18n.t('Forward_Chat') + }); getRoom(); }, []); useEffect(() => { - if (room) { + if (!isEmpty(room)) { getUsers(); getDepartments(); } @@ -129,18 +169,9 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }) => { </View> ); }; -ForwardLivechatView.propTypes = { - forwardRoom: PropTypes.func, - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string -}; -ForwardLivechatView.navigationOptions = { - title: I18n.t('Forward_Chat') -}; -const mapDispatchToProps = dispatch => ({ - forwardRoom: (rid, transferData) => dispatch(forwardRoomAction(rid, transferData)) +const mapDispatchToProps = (dispatch: Dispatch) => ({ + forwardRoom: (rid: string, transferData: ITransferData) => dispatch(forwardRoomAction(rid, transferData)) }); export default connect(null, mapDispatchToProps)(withTheme(ForwardLivechatView)); diff --git a/app/views/InviteUsersEditView/index.js b/app/views/InviteUsersEditView/index.tsx similarity index 68% rename from app/views/InviteUsersEditView/index.js rename to app/views/InviteUsersEditView/index.tsx index 105a8809d..62ce51212 100644 --- a/app/views/InviteUsersEditView/index.js +++ b/app/views/InviteUsersEditView/index.tsx @@ -1,8 +1,10 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { View } from 'react-native'; +import { TextInputProps, View } from 'react-native'; import { connect } from 'react-redux'; import RNPickerSelect from 'react-native-picker-select'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/core'; +import { Dispatch } from 'redux'; import { inviteLinksCreate as inviteLinksCreateAction, @@ -65,25 +67,29 @@ const OPTIONS = { ] }; -class InviteUsersView extends React.Component { - static navigationOptions = () => ({ +interface IInviteUsersEditView { + navigation: StackNavigationProp<any, 'InviteUsersEditView'>; + route: RouteProp<{ InviteUsersEditView: { rid: string } }, 'InviteUsersEditView'>; + theme: string; + createInviteLink(rid: string): void; + inviteLinksSetParams(params: { [key: string]: number }): void; + days: number; + maxUses: number; +} + +class InviteUsersView extends React.Component<IInviteUsersEditView, any> { + static navigationOptions = (): StackNavigationOptions => ({ title: I18n.t('Invite_users') }); - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string, - createInviteLink: PropTypes.func, - inviteLinksSetParams: PropTypes.func - }; + private rid: string; - constructor(props) { + constructor(props: IInviteUsersEditView) { super(props); this.rid = props.route.params?.rid; } - onValueChangePicker = (key, value) => { + onValueChangePicker = (key: string, value: number) => { logEvent(events.IU_EDIT_SET_LINK_PARAM); const { inviteLinksSetParams } = this.props; const params = { @@ -99,9 +105,10 @@ class InviteUsersView extends React.Component { navigation.pop(); }; - renderPicker = (key, first) => { + renderPicker = (key: 'days' | 'maxUses', first: string) => { const { props } = this; const { theme } = props; + const textInputStyle: TextInputProps = { style: { ...styles.pickerText, color: themes[theme].actionTintColor } }; const firstEl = [ { label: I18n.t(first), @@ -112,7 +119,7 @@ class InviteUsersView extends React.Component { <RNPickerSelect style={{ viewContainer: styles.viewContainer }} value={props[key]} - textInputProps={{ style: { ...styles.pickerText, color: themes[theme].actionTintColor } }} + textInputProps={textInputStyle} useNativeAndroidPickerStyle={false} placeholder={{}} onValueChange={value => this.onValueChangePicker(key, value)} @@ -143,14 +150,14 @@ class InviteUsersView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ days: state.inviteLinks.days, maxUses: state.inviteLinks.maxUses }); -const mapDispatchToProps = dispatch => ({ - inviteLinksSetParams: params => dispatch(inviteLinksSetParamsAction(params)), - createInviteLink: rid => dispatch(inviteLinksCreateAction(rid)) +const mapDispatchToProps = (dispatch: Dispatch) => ({ + inviteLinksSetParams: (params: object) => dispatch(inviteLinksSetParamsAction(params)), + createInviteLink: (rid: string) => dispatch(inviteLinksCreateAction(rid)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersView)); diff --git a/app/views/InviteUsersEditView/styles.js b/app/views/InviteUsersEditView/styles.ts similarity index 100% rename from app/views/InviteUsersEditView/styles.js rename to app/views/InviteUsersEditView/styles.ts diff --git a/app/views/JitsiMeetView.js b/app/views/JitsiMeetView.tsx similarity index 68% rename from app/views/JitsiMeetView.js rename to app/views/JitsiMeetView.tsx index e80cdae85..44034cda2 100644 --- a/app/views/JitsiMeetView.js +++ b/app/views/JitsiMeetView.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { StyleSheet } from 'react-native'; -import PropTypes from 'prop-types'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import JitsiMeet, { JitsiMeetView as RNJitsiMeetView } from 'react-native-jitsi-meet'; import BackgroundTimer from 'react-native-background-timer'; import { connect } from 'react-redux'; @@ -12,23 +13,36 @@ import { events, logEvent } from '../utils/log'; import { isAndroid, isIOS } from '../utils/deviceInfo'; import { withTheme } from '../theme'; -const formatUrl = (url, baseUrl, uriSize, avatarAuthURLFragment) => +const formatUrl = (url: string, baseUrl: string, uriSize: number, avatarAuthURLFragment: string) => `${baseUrl}/avatar/${url}?format=png&width=${uriSize}&height=${uriSize}${avatarAuthURLFragment}`; -class JitsiMeetView extends React.Component { - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - baseUrl: PropTypes.string, - theme: PropTypes.string, - user: PropTypes.shape({ - id: PropTypes.string, - username: PropTypes.string, - name: PropTypes.string, - token: PropTypes.string - }) - }; - constructor(props) { +interface IJitsiMeetViewState { + userInfo: { + displayName: string; + avatar: string; + }; + loading: boolean; +} + +interface IJitsiMeetViewProps { + navigation: StackNavigationProp<any, 'JitsiMeetView'>; + route: RouteProp<{ JitsiMeetView: { rid: string; url: string; onlyAudio?: boolean } }, 'JitsiMeetView'>; + baseUrl: string; + theme: string; + user: { + id: string; + username: string; + name: string; + token: string; + }; +} + +class JitsiMeetView extends React.Component<IJitsiMeetViewProps, IJitsiMeetViewState> { + private rid: string; + private url: string; + private jitsiTimeout: number | null; + + constructor(props: IJitsiMeetViewProps) { super(props); this.rid = props.route.params?.rid; this.url = props.route.params?.url; @@ -81,15 +95,17 @@ class JitsiMeetView extends React.Component { // call is not ended and is available to web users. onConferenceJoined = () => { logEvent(events.JM_CONFERENCE_JOIN); - RocketChat.updateJitsiTimeout(this.rid).catch(e => console.log(e)); - if (this.jitsiTimeout) { - BackgroundTimer.clearInterval(this.jitsiTimeout); - BackgroundTimer.stopBackgroundTimer(); - this.jitsiTimeout = null; + if (this.rid) { + RocketChat.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e)); + if (this.jitsiTimeout) { + BackgroundTimer.clearInterval(this.jitsiTimeout); + BackgroundTimer.stopBackgroundTimer(); + this.jitsiTimeout = null; + } + this.jitsiTimeout = BackgroundTimer.setInterval(() => { + RocketChat.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e)); + }, 10000); } - this.jitsiTimeout = BackgroundTimer.setInterval(() => { - RocketChat.updateJitsiTimeout(this.rid).catch(e => console.log(e)); - }, 10000); }; onConferenceTerminated = () => { @@ -118,7 +134,7 @@ class JitsiMeetView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ user: getUserSelector(state), baseUrl: state.server.server }); diff --git a/app/views/LanguageView/index.js b/app/views/LanguageView/index.tsx similarity index 78% rename from app/views/LanguageView/index.js rename to app/views/LanguageView/index.tsx index 29f65bf55..955572e24 100644 --- a/app/views/LanguageView/index.js +++ b/app/views/LanguageView/index.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { FlatList } from 'react-native'; import { connect } from 'react-redux'; import RNRestart from 'react-native-restart'; +import { Dispatch } from 'redux'; import RocketChat from '../../lib/rocketchat'; import I18n, { LANGUAGES, isRTL } from '../../i18n'; @@ -18,26 +18,33 @@ import { getUserSelector } from '../../selectors/login'; import database from '../../lib/database'; import SafeAreaView from '../../containers/SafeAreaView'; -class LanguageView extends React.Component { +interface ILanguageViewProps { + user: { + id: string; + language: string; + }; + setUser(user: object): void; + appStart(params: any): void; + theme: string; +} + +interface ILanguageViewState { + language: string; +} + +class LanguageView extends React.Component<ILanguageViewProps, ILanguageViewState> { static navigationOptions = () => ({ title: I18n.t('Change_Language') }); - static propTypes = { - user: PropTypes.object, - setUser: PropTypes.func, - appStart: PropTypes.func, - theme: PropTypes.string - }; - - constructor(props) { + constructor(props: ILanguageViewProps) { super(props); this.state = { language: props.user ? props.user.language : 'en' }; } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: ILanguageViewProps, nextState: ILanguageViewState) { const { language } = this.state; const { user, theme } = this.props; if (nextProps.theme !== theme) { @@ -52,12 +59,12 @@ class LanguageView extends React.Component { return false; } - formIsChanged = language => { + formIsChanged = (language: string) => { const { user } = this.props; return user.language !== language; }; - submit = async language => { + submit = async (language: string) => { if (!this.formIsChanged(language)) { return; } @@ -78,11 +85,11 @@ class LanguageView extends React.Component { } }; - changeLanguage = async language => { + changeLanguage = async (language: string) => { logEvent(events.LANG_SET_LANGUAGE); const { user, setUser } = this.props; - const params = {}; + const params: { language?: string } = {}; // language if (user.language !== language) { @@ -95,10 +102,10 @@ class LanguageView extends React.Component { const serversDB = database.servers; const usersCollection = serversDB.get('users'); - await serversDB.action(async () => { + await serversDB.write(async () => { try { const userRecord = await usersCollection.find(user.id); - await userRecord.update(record => { + await userRecord.update((record: any) => { record.language = params.language; }); } catch (e) { @@ -117,7 +124,7 @@ class LanguageView extends React.Component { return <List.Icon name='check' color={themes[theme].tintColor} />; }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: { value: string; label: string } }) => { const { value, label } = item; const { language } = this.state; const isSelected = language === value; @@ -151,13 +158,13 @@ class LanguageView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ user: getUserSelector(state) }); -const mapDispatchToProps = dispatch => ({ - setUser: params => dispatch(setUserAction(params)), - appStart: params => dispatch(appStartAction(params)) +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setUser: (params: any) => dispatch(setUserAction(params)), + appStart: (params: any) => dispatch(appStartAction(params)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LanguageView)); diff --git a/app/views/MarkdownTableView.js b/app/views/MarkdownTableView.tsx similarity index 63% rename from app/views/MarkdownTableView.js rename to app/views/MarkdownTableView.tsx index a95118d30..a65994eef 100644 --- a/app/views/MarkdownTableView.js +++ b/app/views/MarkdownTableView.tsx @@ -1,22 +1,26 @@ import React from 'react'; import { ScrollView } from 'react-native'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import I18n from '../i18n'; import { isIOS } from '../utils/deviceInfo'; import { themes } from '../constants/colors'; import { withTheme } from '../theme'; -class MarkdownTableView extends React.Component { - static navigationOptions = () => ({ +interface IMarkdownTableViewProps { + route: RouteProp< + { MarkdownTableView: { renderRows: (drawExtraBorders?: boolean) => JSX.Element; tableWidth: number } }, + 'MarkdownTableView' + >; + theme: string; +} + +class MarkdownTableView extends React.Component<IMarkdownTableViewProps> { + static navigationOptions = (): StackNavigationOptions => ({ title: I18n.t('Table') }); - static propTypes = { - route: PropTypes.object, - theme: PropTypes.string - }; - render() { const { route, theme } = this.props; const renderRows = route.params?.renderRows; diff --git a/app/views/MessagesView/index.js b/app/views/MessagesView/index.tsx similarity index 74% rename from app/views/MessagesView/index.js rename to app/views/MessagesView/index.tsx index f0d0d89d6..a948edcd1 100644 --- a/app/views/MessagesView/index.js +++ b/app/views/MessagesView/index.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import PropTypes from 'prop-types'; 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 Message from '../../containers/message'; import ActivityIndicator from '../../containers/ActivityIndicator'; @@ -18,20 +19,67 @@ import SafeAreaView from '../../containers/SafeAreaView'; import getThreadName from '../../lib/methods/getThreadName'; import styles from './styles'; -class MessagesView extends React.Component { - static propTypes = { - user: PropTypes.object, - baseUrl: PropTypes.string, - navigation: PropTypes.object, - route: PropTypes.object, - customEmojis: PropTypes.object, - theme: PropTypes.string, - showActionSheet: PropTypes.func, - useRealName: PropTypes.bool, - isMasterDetail: PropTypes.bool +type TMessagesViewRouteParams = { + MessagesView: { + rid: string; + t: string; + name: string; }; +}; - constructor(props) { +interface IMessagesViewProps { + user: { + id: string; + }; + baseUrl: string; + navigation: StackNavigationProp<any, 'MessagesView'>; + route: RouteProp<TMessagesViewRouteParams, 'MessagesView'>; + customEmojis: { [key: string]: string }; + theme: string; + showActionSheet: Function; + useRealName: boolean; + isMasterDetail: boolean; +} + +interface IMessagesViewState { + loading: boolean; + messages: []; + fileLoading: boolean; + total: number; +} + +interface IMessageItem { + u?: string; + user?: string; + editedAt?: Date; + attachments?: any; + _id: string; + tmid?: string; + ts?: Date; + uploadedAt?: Date; + name?: string; + description?: string; + msg?: string; + starred: string; + pinned: boolean; +} + +interface IParams { + rid?: string; + jumpToMessageId: string; + t?: string; + room: any; + tmid?: string; + name?: string; +} + +class MessagesView extends React.Component<IMessagesViewProps, any> { + private rid?: string; + private t?: string; + private content: any; + private room: any; + + constructor(props: IMessagesViewProps) { super(props); this.state = { loading: false, @@ -48,7 +96,7 @@ class MessagesView extends React.Component { this.load(); } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: IMessagesViewProps, nextState: any) { const { loading, messages, fileLoading } = this.state; const { theme } = this.props; if (nextProps.theme !== theme) { @@ -73,7 +121,7 @@ class MessagesView extends React.Component { }); }; - navToRoomInfo = navParam => { + navToRoomInfo = (navParam: { rid: string }) => { const { navigation, user } = this.props; if (navParam.rid === user.id) { return; @@ -81,9 +129,9 @@ class MessagesView extends React.Component { navigation.navigate('RoomInfoView', navParam); }; - jumpToMessage = async ({ item }) => { + jumpToMessage = async ({ item }: { item: IMessageItem }) => { const { navigation, isMasterDetail } = this.props; - let params = { + let params: IParams = { rid: this.rid, jumpToMessageId: item._id, t: this.t, @@ -107,9 +155,9 @@ class MessagesView extends React.Component { } }; - defineMessagesViewContent = name => { + defineMessagesViewContent = (name: string) => { const { user, baseUrl, theme, useRealName } = this.props; - const renderItemCommonProps = item => ({ + const renderItemCommonProps = (item: IMessageItem) => ({ item, baseUrl, user, @@ -137,7 +185,7 @@ class MessagesView extends React.Component { }, noDataMsg: I18n.t('No_files'), testID: 'room-files-view', - renderItem: item => ( + renderItem: (item: IMessageItem) => ( <Message {...renderItemCommonProps(item)} item={{ @@ -165,7 +213,7 @@ class MessagesView extends React.Component { }, noDataMsg: I18n.t('No_mentioned_messages'), testID: 'mentioned-messages-view', - renderItem: item => <Message {...renderItemCommonProps(item)} msg={item.msg} theme={theme} /> + renderItem: (item: IMessageItem) => <Message {...renderItemCommonProps(item)} msg={item.msg} theme={theme} /> }, // Starred Messages Screen Starred: { @@ -176,15 +224,15 @@ class MessagesView extends React.Component { }, noDataMsg: I18n.t('No_starred_messages'), testID: 'starred-messages-view', - renderItem: item => ( + renderItem: (item: IMessageItem) => ( <Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} /> ), - action: message => ({ + action: (message: IMessageItem) => ({ title: I18n.t('Unstar'), icon: message.starred ? 'star-filled' : 'star', onPress: this.handleActionPress }), - handleActionPress: message => RocketChat.toggleStarMessage(message._id, message.starred) + handleActionPress: (message: IMessageItem) => RocketChat.toggleStarMessage(message._id, message.starred) }, // Pinned Messages Screen Pinned: { @@ -195,12 +243,13 @@ class MessagesView extends React.Component { }, noDataMsg: I18n.t('No_pinned_messages'), testID: 'pinned-messages-view', - renderItem: item => ( + renderItem: (item: IMessageItem) => ( <Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} /> ), action: () => ({ title: I18n.t('Unpin'), icon: 'pin', onPress: this.handleActionPress }), - handleActionPress: message => RocketChat.togglePinMessage(message._id, message.pinned) + handleActionPress: (message: IMessageItem) => RocketChat.togglePinMessage(message._id, message.pinned) } + // @ts-ignore }[name]; }; @@ -227,7 +276,7 @@ class MessagesView extends React.Component { } }; - getCustomEmoji = name => { + getCustomEmoji = (name: string) => { const { customEmojis } = this.props; const emoji = customEmojis[name]; if (emoji) { @@ -236,12 +285,12 @@ class MessagesView extends React.Component { return null; }; - showAttachment = attachment => { + showAttachment = (attachment: any) => { const { navigation } = this.props; navigation.navigate('AttachmentView', { attachment }); }; - onLongPress = message => { + onLongPress = (message: IMessageItem) => { this.setState({ message }, this.showActionSheet); }; @@ -257,8 +306,8 @@ class MessagesView extends React.Component { try { const result = await this.content.handleActionPress(message); if (result.success) { - this.setState(prevState => ({ - messages: prevState.messages.filter(item => item._id !== message._id), + this.setState((prevState: IMessagesViewState) => ({ + messages: prevState.messages.filter((item: IMessageItem) => item._id !== message._id), total: prevState.total - 1 })); } @@ -267,7 +316,7 @@ class MessagesView extends React.Component { } }; - setFileLoading = fileLoading => { + setFileLoading = (fileLoading: boolean) => { this.setState({ fileLoading }); }; @@ -280,7 +329,7 @@ class MessagesView extends React.Component { ); }; - renderItem = ({ item }) => this.content.renderItem(item); + renderItem = ({ item }: { item: IMessageItem }) => this.content.renderItem(item); render() { const { messages, loading } = this.state; @@ -306,7 +355,7 @@ class MessagesView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ baseUrl: state.server.server, user: getUserSelector(state), customEmojis: state.customEmojis, diff --git a/app/views/MessagesView/styles.js b/app/views/MessagesView/styles.ts similarity index 100% rename from app/views/MessagesView/styles.js rename to app/views/MessagesView/styles.ts diff --git a/app/views/NotificationPreferencesView/index.js b/app/views/NotificationPreferencesView/index.tsx similarity index 72% rename from app/views/NotificationPreferencesView/index.js rename to app/views/NotificationPreferencesView/index.tsx index f0d7437a6..a020c1631 100644 --- a/app/views/NotificationPreferencesView/index.js +++ b/app/views/NotificationPreferencesView/index.tsx @@ -1,6 +1,9 @@ import React from 'react'; import { StyleSheet, Switch, Text } from 'react-native'; -import PropTypes from 'prop-types'; +import { RouteProp } from '@react-navigation/core'; +import { StackNavigationProp } from '@react-navigation/stack'; +import Model from '@nozbe/watermelondb/Model'; +import { Observable, Subscription } from 'rxjs'; import database from '../../lib/database'; import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; @@ -22,18 +25,31 @@ const styles = StyleSheet.create({ } }); -class NotificationPreferencesView extends React.Component { +interface INotificationPreferencesView { + navigation: StackNavigationProp<any, 'NotificationPreferencesView'>; + route: RouteProp< + { + NotificationPreferencesView: { + rid: string; + room: Model; + }; + }, + 'NotificationPreferencesView' + >; + theme: string; +} + +class NotificationPreferencesView extends React.Component<INotificationPreferencesView, any> { static navigationOptions = () => ({ title: I18n.t('Notification_Preferences') }); - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string - }; + private mounted: boolean; + private rid: string | undefined; + private roomObservable?: Observable<Model>; + private subscription?: Subscription; - constructor(props) { + constructor(props: INotificationPreferencesView) { super(props); this.mounted = false; this.rid = props.route.params?.rid; @@ -43,10 +59,11 @@ class NotificationPreferencesView extends React.Component { }; if (room && room.observe) { this.roomObservable = room.observe(); - this.subscription = this.roomObservable.subscribe(changes => { + this.subscription = this.roomObservable.subscribe((changes: any) => { if (this.mounted) { this.setState({ room: changes }); } else { + // @ts-ignore this.state.room = changes; } }); @@ -63,7 +80,8 @@ class NotificationPreferencesView extends React.Component { } } - saveNotificationSettings = async (key, value, params) => { + saveNotificationSettings = async (key: string, value: string | boolean, params: any) => { + // @ts-ignore logEvent(events[`NP_${key.toUpperCase()}`]); const { room } = this.state; const db = database.active; @@ -71,7 +89,7 @@ class NotificationPreferencesView extends React.Component { try { await db.action(async () => { await room.update( - protectedFunction(r => { + protectedFunction((r: any) => { r[key] = value; }) ); @@ -88,36 +106,38 @@ class NotificationPreferencesView extends React.Component { await db.action(async () => { await room.update( - protectedFunction(r => { + protectedFunction((r: any) => { r[key] = room[key]; }) ); }); } catch (e) { + // @ts-ignore logEvent(events[`NP_${key.toUpperCase()}_F`]); log(e); } }; - onValueChangeSwitch = (key, value) => this.saveNotificationSettings(key, value, { [key]: value ? '1' : '0' }); + onValueChangeSwitch = (key: string, value: string | boolean) => + this.saveNotificationSettings(key, value, { [key]: value ? '1' : '0' }); - onValueChangePicker = (key, value) => this.saveNotificationSettings(key, value, { [key]: value.toString() }); + onValueChangePicker = (key: string, value: string) => this.saveNotificationSettings(key, value, { [key]: value.toString() }); - pickerSelection = (title, key) => { + pickerSelection = (title: string, key: string) => { const { room } = this.state; const { navigation } = this.props; navigation.navigate('PickerView', { title, data: OPTIONS[key], value: room[key], - onChangeValue: value => this.onValueChangePicker(key, value) + onChangeValue: (value: string) => this.onValueChangePicker(key, value) }); }; - renderPickerOption = key => { + renderPickerOption = (key: string) => { const { room } = this.state; const { theme } = this.props; - const text = room[key] ? OPTIONS[key].find(option => option.value === room[key]) : OPTIONS[key][0]; + const text = room[key] ? OPTIONS[key].find((option: any) => option.value === room[key]) : OPTIONS[key][0]; return ( <Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}> {I18n.t(text?.label, { defaultValue: text?.label, second: text?.second })} @@ -125,7 +145,7 @@ class NotificationPreferencesView extends React.Component { ); }; - renderSwitch = key => { + renderSwitch = (key: string) => { const { room } = this.state; return ( <Switch @@ -181,7 +201,7 @@ class NotificationPreferencesView extends React.Component { <List.Item title='Alert' testID='notification-preference-view-alert' - onPress={title => this.pickerSelection(title, 'desktopNotifications')} + onPress={(title: string) => this.pickerSelection(title, 'desktopNotifications')} right={() => this.renderPickerOption('desktopNotifications')} /> <List.Separator /> @@ -193,7 +213,7 @@ class NotificationPreferencesView extends React.Component { <List.Item title='Alert' testID='notification-preference-view-push-notification' - onPress={title => this.pickerSelection(title, 'mobilePushNotifications')} + onPress={(title: string) => this.pickerSelection(title, 'mobilePushNotifications')} right={() => this.renderPickerOption('mobilePushNotifications')} /> <List.Separator /> @@ -205,21 +225,21 @@ class NotificationPreferencesView extends React.Component { <List.Item title='Audio' testID='notification-preference-view-audio' - onPress={title => this.pickerSelection(title, 'audioNotifications')} + onPress={(title: string) => this.pickerSelection(title, 'audioNotifications')} right={() => this.renderPickerOption('audioNotifications')} /> <List.Separator /> <List.Item title='Sound' testID='notification-preference-view-sound' - onPress={title => this.pickerSelection(title, 'audioNotificationValue')} + onPress={(title: string) => this.pickerSelection(title, 'audioNotificationValue')} right={() => this.renderPickerOption('audioNotificationValue')} /> <List.Separator /> <List.Item title='Notification_Duration' testID='notification-preference-view-notification-duration' - onPress={title => this.pickerSelection(title, 'desktopNotificationDuration')} + onPress={(title: string) => this.pickerSelection(title, 'desktopNotificationDuration')} right={() => this.renderPickerOption('desktopNotificationDuration')} /> <List.Separator /> @@ -230,7 +250,7 @@ class NotificationPreferencesView extends React.Component { <List.Item title='Alert' testID='notification-preference-view-email-alert' - onPress={title => this.pickerSelection(title, 'emailNotifications')} + onPress={(title: string) => this.pickerSelection(title, 'emailNotifications')} right={() => this.renderPickerOption('emailNotifications')} /> <List.Separator /> diff --git a/app/views/NotificationPreferencesView/options.js b/app/views/NotificationPreferencesView/options.ts similarity index 80% rename from app/views/NotificationPreferencesView/options.js rename to app/views/NotificationPreferencesView/options.ts index 660ff6df0..4035c0380 100644 --- a/app/views/NotificationPreferencesView/options.js +++ b/app/views/NotificationPreferencesView/options.ts @@ -1,4 +1,18 @@ -export const OPTIONS = { +interface IOptionsField { + label: string; + value: string | number; + second?: number; +} +export interface INotificationOptions { + [desktopNotifications: string]: IOptionsField[]; + audioNotifications: IOptionsField[]; + mobilePushNotifications: IOptionsField[]; + emailNotifications: IOptionsField[]; + desktopNotificationDuration: IOptionsField[]; + audioNotificationValue: IOptionsField[]; +} + +export const OPTIONS: INotificationOptions = { desktopNotifications: [ { label: 'Default', diff --git a/app/views/PickerView.js b/app/views/PickerView.tsx similarity index 70% rename from app/views/PickerView.js rename to app/views/PickerView.tsx index ed62fe65e..002979b20 100644 --- a/app/views/PickerView.js +++ b/app/views/PickerView.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import { FlatList, StyleSheet, Text, View } from 'react-native'; import I18n from '../i18n'; @@ -24,7 +25,41 @@ const styles = StyleSheet.create({ } }); -const Item = React.memo(({ item, selected, onItemPress, theme }) => ( +interface IData { + label: string; + value: string; + second?: string; +} + +interface IItem { + item: IData; + selected: boolean; + onItemPress: () => void; + theme: string; +} + +interface IPickerViewState { + data: IData[]; + 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<any, 'PickerView'>; + route: RouteProp<{ PickerView: IParams }, 'PickerView'>; + theme: string; +} + +const Item = React.memo(({ item, selected, onItemPress, theme }: IItem) => ( <List.Item title={I18n.t(item.label, { defaultValue: item.label, second: item?.second })} right={selected && (() => <List.Icon name='check' color={themes[theme].tintColor} />)} @@ -32,25 +67,15 @@ const Item = React.memo(({ item, selected, onItemPress, theme }) => ( translateTitle={false} /> )); -Item.propTypes = { - item: PropTypes.object, - selected: PropTypes.bool, - onItemPress: PropTypes.func, - theme: PropTypes.string -}; -class PickerView extends React.PureComponent { - static navigationOptions = ({ route }) => ({ +class PickerView extends React.PureComponent<IPickerViewProps, IPickerViewState> { + private onSearch: (text: string) => IData[]; + + static navigationOptions = ({ route }: IPickerViewProps) => ({ title: route.params?.title ?? I18n.t('Select_an_option') }); - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string - }; - - constructor(props) { + constructor(props: IPickerViewProps) { super(props); const data = props.route.params?.data ?? []; const value = props.route.params?.value; @@ -59,7 +84,7 @@ class PickerView extends React.PureComponent { this.onSearch = props.route.params?.onChangeText; } - onChangeValue = value => { + onChangeValue = (value: string) => { const { navigation, route } = this.props; const goBack = route.params?.goBack ?? true; const onChange = route.params?.onChangeValue ?? (() => {}); @@ -70,7 +95,7 @@ class PickerView extends React.PureComponent { }; onChangeText = debounce( - async text => { + async (text: string) => { if (this.onSearch) { const data = await this.onSearch(text); this.setState({ data }); diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.tsx similarity index 87% rename from app/views/ProfileView/index.js rename to app/views/ProfileView/index.tsx index 630d8c7aa..d0ca64eec 100644 --- a/app/views/ProfileView/index.js +++ b/app/views/ProfileView/index.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Keyboard, ScrollView, View } from 'react-native'; import { connect } from 'react-redux'; import prompt from 'react-native-prompt-android'; -import SHA256 from 'js-sha256'; -import ImagePicker from 'react-native-image-crop-picker'; +import { sha256 } from 'js-sha256'; +import ImagePicker, { Image } from 'react-native-image-crop-picker'; import RNPickerSelect from 'react-native-picker-select'; import { dequal } from 'dequal'; import omit from 'lodash/omit'; +import { StackNavigationOptions } from '@react-navigation/stack'; import Touch from '../../utils/touch'; import KeyboardView from '../../presentation/KeyboardView'; @@ -31,43 +31,40 @@ import { withTheme } from '../../theme'; import { getUserSelector } from '../../selectors/login'; import SafeAreaView from '../../containers/SafeAreaView'; import styles from './styles'; +import { IAvatar, IAvatarButton, INavigationOptions, IParams, IProfileViewProps, IProfileViewState, IUser } from './interfaces'; -class ProfileView extends React.Component { - static navigationOptions = ({ navigation, isMasterDetail }) => { - const options = { +class ProfileView extends React.Component<IProfileViewProps, IProfileViewState> { + private name: any; + private username: any; + private email: any; + private avatarUrl: any; + private newPassword: any; + + static navigationOptions = ({ navigation, isMasterDetail }: INavigationOptions) => { + const options: StackNavigationOptions = { title: I18n.t('Profile') }; if (!isMasterDetail) { options.headerLeft = () => <HeaderButton.Drawer navigation={navigation} />; } options.headerRight = () => ( - <HeaderButton.Preferences onPress={() => navigation.navigate('UserPreferencesView')} testID='preferences-view-open' /> + <HeaderButton.Preferences onPress={() => navigation?.navigate('UserPreferencesView')} testID='preferences-view-open' /> ); return options; }; - static propTypes = { - baseUrl: PropTypes.string, - user: PropTypes.object, - Accounts_AllowEmailChange: PropTypes.bool, - Accounts_AllowPasswordChange: PropTypes.bool, - Accounts_AllowRealNameChange: PropTypes.bool, - Accounts_AllowUserAvatarChange: PropTypes.bool, - Accounts_AllowUsernameChange: PropTypes.bool, - Accounts_CustomFields: PropTypes.string, - setUser: PropTypes.func, - theme: PropTypes.string - }; - - state = { + state: IProfileViewState = { saving: false, - name: null, - username: null, - email: null, - newPassword: null, - currentPassword: null, - avatarUrl: null, - avatar: {}, + name: '', + username: '', + email: '', + newPassword: '', + currentPassword: '', + avatarUrl: '', + avatar: { + data: {}, + url: '' + }, avatarSuggestions: {}, customFields: {} }; @@ -83,7 +80,7 @@ class ProfileView extends React.Component { } } - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: IProfileViewProps) { const { user } = this.props; /* * We need to ignore status because on Android ImagePicker @@ -96,7 +93,7 @@ class ProfileView extends React.Component { } } - setAvatar = avatar => { + setAvatar = (avatar: IAvatar) => { const { Accounts_AllowUserAvatarChange } = this.props; if (!Accounts_AllowUserAvatarChange) { @@ -106,7 +103,7 @@ class ProfileView extends React.Component { this.setState({ avatar }); }; - init = user => { + init = (user?: IUser) => { const { user: userProps } = this.props; const { name, username, emails, customFields } = user || userProps; @@ -117,7 +114,10 @@ class ProfileView extends React.Component { newPassword: null, currentPassword: null, avatarUrl: null, - avatar: {}, + avatar: { + data: {}, + url: '' + }, customFields: customFields || {} }); }; @@ -142,12 +142,12 @@ class ProfileView extends React.Component { !newPassword && user.emails && user.emails[0].address === email && - !avatar.data && + !avatar!.data && !customFieldsChanged ); }; - handleError = (e, func, action) => { + handleError = (e: any, func: string, action: string) => { if (e.data && e.data.error.includes('[error-too-many-requests]')) { return showErrorAlert(e.data.error); } @@ -165,7 +165,7 @@ class ProfileView extends React.Component { const { name, username, email, newPassword, currentPassword, avatar, customFields } = this.state; const { user, setUser } = this.props; - const params = {}; + const params = {} as IParams; // Name if (user.name !== name) { @@ -189,7 +189,7 @@ class ProfileView extends React.Component { // currentPassword if (currentPassword) { - params.currentPassword = SHA256(currentPassword); + params.currentPassword = sha256(currentPassword); } const requirePassword = !!params.email || newPassword; @@ -202,7 +202,7 @@ class ProfileView extends React.Component { { text: I18n.t('Cancel'), onPress: () => {}, style: 'cancel' }, { text: I18n.t('Save'), - onPress: p => { + onPress: (p: string) => { this.setState({ currentPassword: p }); this.submit(); } @@ -217,7 +217,7 @@ class ProfileView extends React.Component { } try { - if (avatar.url) { + if (avatar!.url) { try { logEvent(events.PROFILE_SAVE_AVATAR); await RocketChat.setAvatarFromService(avatar); @@ -283,7 +283,7 @@ class ProfileView extends React.Component { }; try { logEvent(events.PROFILE_PICK_AVATAR); - const response = await ImagePicker.openPicker(options); + const response: Image = await ImagePicker.openPicker(options); this.setAvatar({ url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' }); } catch (error) { logEvent(events.PROFILE_PICK_AVATAR_F); @@ -291,12 +291,12 @@ class ProfileView extends React.Component { } }; - pickImageWithURL = avatarUrl => { + pickImageWithURL = (avatarUrl: string) => { logEvent(events.PROFILE_PICK_AVATAR_WITH_URL); this.setAvatar({ url: avatarUrl, data: avatarUrl, service: 'url' }); }; - renderAvatarButton = ({ key, child, onPress, disabled = false }) => { + renderAvatarButton = ({ key, child, onPress, disabled = false }: IAvatarButton) => { const { theme } = this.props; return ( <Touch @@ -331,7 +331,7 @@ class ProfileView extends React.Component { })} {this.renderAvatarButton({ child: <CustomIcon name='link' size={30} color={themes[theme].bodyText} />, - onPress: () => this.pickImageWithURL(avatarUrl), + onPress: () => this.pickImageWithURL(avatarUrl!), disabled: !avatarUrl, key: 'profile-view-avatar-url-button' })} @@ -365,19 +365,20 @@ class ProfileView extends React.Component { const parsedCustomFields = JSON.parse(Accounts_CustomFields); return Object.keys(parsedCustomFields).map((key, index, array) => { if (parsedCustomFields[key].type === 'select') { - const options = parsedCustomFields[key].options.map(option => ({ label: option, value: option })); + const options = parsedCustomFields[key].options.map((option: string) => ({ label: option, value: option })); return ( <RNPickerSelect key={key} items={options} onValueChange={value => { - const newValue = {}; + const newValue: { [key: string]: string } = {}; newValue[key] = value; this.setState({ customFields: { ...customFields, ...newValue } }); }} value={customFields[key]}> <RCTextInput inputRef={e => { + // @ts-ignore this[key] = e; }} label={key} @@ -393,6 +394,7 @@ class ProfileView extends React.Component { return ( <RCTextInput inputRef={e => { + // @ts-ignore this[key] = e; }} key={key} @@ -400,12 +402,13 @@ class ProfileView extends React.Component { placeholder={key} value={customFields[key]} onChangeText={value => { - const newValue = {}; + const newValue: { [key: string]: string } = {}; newValue[key] = value; this.setState({ customFields: { ...customFields, ...newValue } }); }} onSubmitEditing={() => { if (array.length - 1 > index) { + // @ts-ignore return this[array[index + 1]].focus(); } this.avatarUrl.focus(); @@ -421,6 +424,7 @@ class ProfileView extends React.Component { logoutOtherLocations = () => { logEvent(events.PL_OTHER_LOCATIONS); + // @ts-ignore showConfirmationAlert({ message: I18n.t('You_will_be_logged_out_from_other_locations'), confirmationText: I18n.t('Logout'), @@ -469,7 +473,7 @@ class ProfileView extends React.Component { label={I18n.t('Name')} placeholder={I18n.t('Name')} value={name} - onChangeText={value => this.setState({ name: value })} + onChangeText={(value: string) => this.setState({ name: value })} onSubmitEditing={() => { this.username.focus(); }} @@ -500,7 +504,7 @@ class ProfileView extends React.Component { }} label={I18n.t('Email')} placeholder={I18n.t('Email')} - value={email} + value={email!} onChangeText={value => this.setState({ email: value })} onSubmitEditing={() => { this.newPassword.focus(); @@ -516,10 +520,11 @@ class ProfileView extends React.Component { }} label={I18n.t('New_Password')} placeholder={I18n.t('New_Password')} - value={newPassword} + value={newPassword!} onChangeText={value => this.setState({ newPassword: value })} onSubmitEditing={() => { if (Accounts_CustomFields && Object.keys(customFields).length) { + // @ts-ignore return this[Object.keys(customFields)[0]].focus(); } this.avatarUrl.focus(); @@ -537,7 +542,7 @@ class ProfileView extends React.Component { }} label={I18n.t('Avatar_Url')} placeholder={I18n.t('Avatar_Url')} - value={avatarUrl} + value={avatarUrl!} onChangeText={value => this.setState({ avatarUrl: value })} onSubmitEditing={this.submit} testID='profile-view-avatar-url' @@ -568,7 +573,7 @@ class ProfileView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ user: getUserSelector(state), Accounts_AllowEmailChange: state.settings.Accounts_AllowEmailChange, Accounts_AllowPasswordChange: state.settings.Accounts_AllowPasswordChange, @@ -579,8 +584,8 @@ const mapStateToProps = state => ({ baseUrl: state.server.server }); -const mapDispatchToProps = dispatch => ({ - setUser: params => dispatch(setUserAction(params)) +const mapDispatchToProps = (dispatch: any) => ({ + setUser: (params: any) => dispatch(setUserAction(params)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(ProfileView)); diff --git a/app/views/ProfileView/interfaces.ts b/app/views/ProfileView/interfaces.ts new file mode 100644 index 000000000..00117203e --- /dev/null +++ b/app/views/ProfileView/interfaces.ts @@ -0,0 +1,79 @@ +import { StackNavigationProp } from '@react-navigation/stack'; +import React from 'react'; + +export interface IUser { + id: string; + name: string; + username: string; + emails: { + [index: number]: { + address: string; + }; + }; + customFields: { + [index: string | number]: string; + }; +} + +export interface IParams { + name: string; + username: string; + email: string | null; + newPassword: string; + currentPassword: string; +} + +export interface IAvatarButton { + key: React.Key; + child: React.ReactNode; + onPress: Function; + disabled: boolean; +} + +export interface INavigationOptions { + navigation: StackNavigationProp<any, 'ProfileView'>; + isMasterDetail?: boolean; +} + +export interface IProfileViewProps { + user: IUser; + navigation: StackNavigationProp<any, 'ProfileView'>; + isMasterDetail?: boolean; + baseUrl: string; + Accounts_AllowEmailChange: boolean; + Accounts_AllowPasswordChange: boolean; + Accounts_AllowRealNameChange: boolean; + Accounts_AllowUserAvatarChange: boolean; + Accounts_AllowUsernameChange: boolean; + Accounts_CustomFields: string; + setUser: Function; + theme: string; +} + +export interface IAvatar { + data: {} | string | null; + url?: string; + contentType?: string; + service?: any; +} + +export interface IProfileViewState { + saving: boolean; + name: string; + username: string; + email: string | null; + newPassword: string | null; + currentPassword: string | null; + avatarUrl: string | null; + avatar: IAvatar; + avatarSuggestions: { + [service: string]: { + url: string; + blob: string; + contentType: string; + }; + }; + customFields: { + [key: string | number]: string; + }; +} diff --git a/app/views/ProfileView/styles.js b/app/views/ProfileView/styles.ts similarity index 100% rename from app/views/ProfileView/styles.js rename to app/views/ProfileView/styles.ts diff --git a/app/views/ReadReceiptView/index.js b/app/views/ReadReceiptView/index.tsx similarity index 76% rename from app/views/ReadReceiptView/index.js rename to app/views/ReadReceiptView/index.tsx index f611cbc15..9f1a00675 100644 --- a/app/views/ReadReceiptView/index.js +++ b/app/views/ReadReceiptView/index.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { FlatList, Text, View, RefreshControl } from 'react-native'; import { dequal } from 'dequal'; import moment from 'moment'; import { connect } from 'react-redux'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/core'; import * as List from '../../containers/List'; import Avatar from '../../containers/Avatar'; @@ -16,9 +17,40 @@ import { themes } from '../../constants/colors'; import SafeAreaView from '../../containers/SafeAreaView'; import styles from './styles'; -class ReadReceiptView extends React.Component { - static navigationOptions = ({ navigation, isMasterDetail }) => { - const options = { +interface IReceipts { + _id: string; + roomId: string; + userId: string; + messageId: string; + ts: string; + user?: { + _id: string; + name: string; + username: string; + }; +} + +interface IReadReceiptViewState { + loading: boolean; + receipts: IReceipts[]; +} + +interface INavigationOption { + navigation: StackNavigationProp<any, 'ReadReceiptView'>; + route: RouteProp<{ ReadReceiptView: { messageId: string } }, 'ReadReceiptView'>; + isMasterDetail: boolean; +} + +interface IReadReceiptViewProps extends INavigationOption { + Message_TimeAndDateFormat: string; + theme: string; +} + +class ReadReceiptView extends React.Component<IReadReceiptViewProps, IReadReceiptViewState> { + private messageId: string; + + static navigationOptions = ({ navigation, isMasterDetail }: INavigationOption) => { + const options: StackNavigationOptions = { title: I18n.t('Read_Receipt') }; if (isMasterDetail) { @@ -27,13 +59,7 @@ class ReadReceiptView extends React.Component { return options; }; - static propTypes = { - route: PropTypes.object, - Message_TimeAndDateFormat: PropTypes.string, - theme: PropTypes.string - }; - - constructor(props) { + constructor(props: IReadReceiptViewProps) { super(props); this.messageId = props.route.params?.messageId; this.state = { @@ -46,7 +72,7 @@ class ReadReceiptView extends React.Component { this.load(); } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: IReadReceiptViewProps, nextState: IReadReceiptViewState) { const { loading, receipts } = this.state; const { theme } = this.props; if (nextProps.theme !== theme) { @@ -98,7 +124,7 @@ class ReadReceiptView extends React.Component { ); }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: IReceipts }) => { const { theme, Message_TimeAndDateFormat } = this.props; const time = moment(item.ts).format(Message_TimeAndDateFormat); if (!item?.user?.username) { @@ -152,7 +178,7 @@ class ReadReceiptView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ Message_TimeAndDateFormat: state.settings.Message_TimeAndDateFormat }); diff --git a/app/views/ReadReceiptView/styles.js b/app/views/ReadReceiptView/styles.ts similarity index 100% rename from app/views/ReadReceiptView/styles.js rename to app/views/ReadReceiptView/styles.ts diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.tsx similarity index 77% rename from app/views/SearchMessagesView/index.js rename to app/views/SearchMessagesView/index.tsx index c36425edd..a9be99918 100644 --- a/app/views/SearchMessagesView/index.js +++ b/app/views/SearchMessagesView/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/core'; import { FlatList, Text, View } from 'react-native'; import { Q } from '@nozbe/watermelondb'; import { connect } from 'react-redux'; @@ -12,6 +13,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 I18n from '../../i18n'; import StatusBar from '../../containers/StatusBar'; import log from '../../utils/log'; @@ -29,9 +31,52 @@ import { compareServerVersion, methods } from '../../lib/utils'; import styles from './styles'; const QUERY_SIZE = 50; -class SearchMessagesView extends React.Component { - static navigationOptions = ({ navigation, route }) => { - const options = { + +type TRouteParams = { + SearchMessagesView: { + showCloseModal?: boolean; + rid: string; + t?: string; + encrypted?: boolean; + }; +}; + +interface ISearchMessagesViewState { + loading: boolean; + messages: IMessage[]; + searchText: string; +} +interface INavigationOption { + navigation: StackNavigationProp<any, 'SearchMessagesView'>; + route: RouteProp<TRouteParams, 'SearchMessagesView'>; +} + +interface ISearchMessagesViewProps extends INavigationOption { + user: { id: string }; + baseUrl: string; + serverVersion: string; + customEmojis: { + [key: string]: { + name: string; + extension: string; + }; + }; + theme: string; + useRealName: boolean; +} +class SearchMessagesView extends React.Component<ISearchMessagesViewProps, ISearchMessagesViewState> { + private offset: number; + + private rid: string; + + private t: string | undefined; + + private encrypted: boolean | undefined; + + private room: { rid: any; name: any; fname: any; t: any } | null | undefined; + + static navigationOptions = ({ navigation, route }: INavigationOption) => { + const options: StackNavigationOptions = { title: I18n.t('Search') }; const showCloseModal = route.params?.showCloseModal; @@ -41,18 +86,7 @@ class SearchMessagesView extends React.Component { return options; }; - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - user: PropTypes.object, - baseUrl: PropTypes.string, - serverVersion: PropTypes.string, - customEmojis: PropTypes.object, - theme: PropTypes.string, - useRealName: PropTypes.bool - }; - - constructor(props) { + constructor(props: ISearchMessagesViewProps) { super(props); this.state = { loading: false, @@ -60,7 +94,7 @@ class SearchMessagesView extends React.Component { searchText: '' }; this.offset = 0; - this.rid = props.route.params?.rid; + this.rid = props.route.params.rid; this.t = props.route.params?.t; this.encrypted = props.route.params?.encrypted; } @@ -69,7 +103,7 @@ class SearchMessagesView extends React.Component { this.room = await getRoomInfo(this.rid); } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: ISearchMessagesViewProps, nextState: ISearchMessagesViewState) { const { loading, searchText, messages } = this.state; const { theme } = this.props; if (nextProps.theme !== theme) { @@ -88,11 +122,11 @@ class SearchMessagesView extends React.Component { } componentWillUnmount() { - this.search?.stop?.(); + this.searchDebounced?.stop?.(); } // Handle encrypted rooms search messages - searchMessages = async searchText => { + searchMessages = async (searchText: string) => { if (!searchText) { return []; } @@ -117,7 +151,7 @@ class SearchMessagesView extends React.Component { } }; - getMessages = async (searchText, debounced) => { + getMessages = async (searchText: string, debounced?: boolean) => { try { const messages = await this.searchMessages(searchText); this.setState(prevState => ({ @@ -130,17 +164,17 @@ class SearchMessagesView extends React.Component { } }; - search = searchText => { + search = (searchText: string) => { this.offset = 0; this.setState({ searchText, loading: true, messages: [] }); this.searchDebounced(searchText); }; - searchDebounced = debounce(async searchText => { + searchDebounced = debounce(async (searchText: string) => { await this.getMessages(searchText, true); }, 1000); - getCustomEmoji = name => { + getCustomEmoji = (name: string) => { const { customEmojis } = this.props; const emoji = customEmojis[name]; if (emoji) { @@ -149,12 +183,12 @@ class SearchMessagesView extends React.Component { return null; }; - showAttachment = attachment => { + showAttachment = (attachment: IMessageAttachments) => { const { navigation } = this.props; navigation.navigate('AttachmentView', { attachment }); }; - navToRoomInfo = navParam => { + navToRoomInfo = (navParam: IMessage) => { const { navigation, user } = this.props; if (navParam.rid === user.id) { return; @@ -162,9 +196,9 @@ class SearchMessagesView extends React.Component { navigation.navigate('RoomInfoView', navParam); }; - jumpToMessage = async ({ item }) => { + jumpToMessage = async ({ item }: { item: IMessage }) => { const { navigation } = this.props; - let params = { + let params: any = { rid: this.rid, jumpToMessageId: item._id, t: this.t, @@ -210,7 +244,7 @@ class SearchMessagesView extends React.Component { ); }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: IMessage }) => { const { user, baseUrl, theme, useRealName } = this.props; return ( <Message @@ -268,6 +302,7 @@ class SearchMessagesView extends React.Component { testID='search-message-view-input' theme={theme} /> + {/* @ts-ignore */} <Markdown msg={I18n.t('You_can_search_using_RegExp_eg')} username='' baseUrl='' theme={theme} /> <View style={[styles.divider, { backgroundColor: themes[theme].separatorColor }]} /> </View> @@ -277,7 +312,7 @@ class SearchMessagesView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ serverVersion: state.server.version, baseUrl: state.server.server, user: getUserSelector(state), diff --git a/app/views/SearchMessagesView/styles.js b/app/views/SearchMessagesView/styles.ts similarity index 100% rename from app/views/SearchMessagesView/styles.js rename to app/views/SearchMessagesView/styles.ts diff --git a/app/views/ShareView/Header.js b/app/views/ShareView/Header.tsx similarity index 91% rename from app/views/ShareView/Header.js rename to app/views/ShareView/Header.tsx index a52fbf453..41e5339e1 100644 --- a/app/views/ShareView/Header.js +++ b/app/views/ShareView/Header.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { StyleSheet, Text, View } from 'react-native'; import I18n from '../../i18n'; @@ -35,7 +34,13 @@ const styles = StyleSheet.create({ } }); -const Header = React.memo(({ room, thread, theme }) => { +interface IHeader { + room: { prid?: string; t?: string }; + thread: { id?: string }; + theme: string; +} + +const Header = React.memo(({ room, thread, theme }: IHeader) => { let type; if (thread?.id) { type = 'thread'; @@ -88,10 +93,5 @@ const Header = React.memo(({ room, thread, theme }) => { </View> ); }); -Header.propTypes = { - room: PropTypes.object, - thread: PropTypes.object, - theme: PropTypes.string -}; export default withTheme(Header); diff --git a/app/views/ShareView/Preview.js b/app/views/ShareView/Preview.tsx similarity index 87% rename from app/views/ShareView/Preview.js rename to app/views/ShareView/Preview.tsx index d559640f4..e7600dda6 100644 --- a/app/views/ShareView/Preview.js +++ b/app/views/ShareView/Preview.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Video } from 'expo-av'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { ScrollView, StyleSheet, Text } from 'react-native'; @@ -15,6 +14,7 @@ import I18n from '../../i18n'; import { isAndroid } from '../../utils/deviceInfo'; import { allowPreview } from './utils'; import { THUMBS_HEIGHT } from './constants'; +import { IAttachment, IUseDimensions } from './interfaces'; const MESSAGEBOX_HEIGHT = 56; @@ -35,7 +35,17 @@ const styles = StyleSheet.create({ } }); -const IconPreview = React.memo(({ iconName, title, description, theme, width, height, danger }) => ( +interface IIconPreview { + iconName: string; + title: string; + description?: string; + theme: string; + width: number; + height: number; + danger?: boolean; +} + +const IconPreview = React.memo(({ iconName, title, description, theme, width, height, danger }: IIconPreview) => ( <ScrollView style={{ backgroundColor: themes[theme].auxiliaryBackground }} contentContainerStyle={[styles.fileContainer, { width, height }]}> @@ -45,9 +55,16 @@ const IconPreview = React.memo(({ iconName, title, description, theme, width, he </ScrollView> )); -const Preview = React.memo(({ item, theme, isShareExtension, length }) => { +interface IPreview { + item: IAttachment; + theme: string; + isShareExtension: boolean; + length: number; +} + +const Preview = React.memo(({ item, theme, isShareExtension, length }: IPreview) => { const type = item?.mime; - const { width, height } = useDimensions(); + const { width, height } = useDimensions() as IUseDimensions; const { isLandscape } = useOrientation(); const insets = useSafeAreaInsets(); const headerHeight = getHeaderHeight(isLandscape); @@ -111,21 +128,5 @@ const Preview = React.memo(({ item, theme, isShareExtension, length }) => { /> ); }); -Preview.propTypes = { - item: PropTypes.object, - theme: PropTypes.string, - isShareExtension: PropTypes.bool, - length: PropTypes.number -}; - -IconPreview.propTypes = { - iconName: PropTypes.string, - title: PropTypes.string, - description: PropTypes.string, - theme: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - danger: PropTypes.bool -}; export default Preview; diff --git a/app/views/ShareView/Thumbs.js b/app/views/ShareView/Thumbs.tsx similarity index 77% rename from app/views/ShareView/Thumbs.js rename to app/views/ShareView/Thumbs.tsx index 758433e3f..da0808cc0 100644 --- a/app/views/ShareView/Thumbs.js +++ b/app/views/ShareView/Thumbs.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { FlatList, Image, StyleSheet, View } from 'react-native'; import { RectButton, TouchableNativeFeedback, TouchableOpacity } from 'react-native-gesture-handler'; @@ -9,6 +8,7 @@ import { CustomIcon } from '../../lib/Icons'; import { isIOS } from '../../utils/deviceInfo'; import { THUMBS_HEIGHT } from './constants'; import { allowPreview } from './utils'; +import { IAttachment } from './interfaces'; const THUMB_SIZE = 64; @@ -60,22 +60,34 @@ const styles = StyleSheet.create({ } }); -const ThumbButton = isIOS ? TouchableOpacity : TouchableNativeFeedback; +interface IThumbContent { + item: IAttachment; + theme: string; + isShareExtension: boolean; +} -const ThumbContent = React.memo(({ item, theme, isShareExtension }) => { +interface IThumb extends IThumbContent { + onPress(item: IAttachment): void; + onRemove(item: IAttachment): void; +} + +interface IThumbs extends Omit<IThumb, 'item'> { + attachments: IAttachment[]; +} + +const ThumbContent = React.memo(({ item, theme, isShareExtension }: IThumbContent) => { const type = item?.mime; if (type?.match(/image/)) { // Disallow preview of images too big in order to prevent memory issues on iOS share extension if (allowPreview(isShareExtension, item?.size)) { return <Image source={{ uri: item.path }} style={[styles.thumb, { borderColor: themes[theme].borderColor }]} />; - } else { - return ( - <View style={[styles.thumb, { borderColor: themes[theme].borderColor }]}> - <CustomIcon name='image' size={30} color={themes[theme].tintColor} /> - </View> - ); } + return ( + <View style={[styles.thumb, { borderColor: themes[theme].borderColor }]}> + <CustomIcon name='image' size={30} color={themes[theme].tintColor} /> + </View> + ); } if (type?.match(/video/)) { @@ -85,22 +97,23 @@ const ThumbContent = React.memo(({ item, theme, isShareExtension }) => { <CustomIcon name='camera' size={30} color={themes[theme].tintColor} /> </View> ); - } else { - const { uri } = item; - return ( - <> - <Image source={{ uri }} style={styles.thumb} /> - <CustomIcon name='camera-filled' size={20} color={themes[theme].buttonText} style={styles.videoThumbIcon} /> - </> - ); } + const { uri } = item; + return ( + <> + <Image source={{ uri }} style={styles.thumb} /> + <CustomIcon name='camera-filled' size={20} color={themes[theme].buttonText} style={styles.videoThumbIcon} /> + </> + ); } // Multiple files upload of files different than image/video is not implemented, so there's no thumb return null; }); -const Thumb = ({ item, theme, isShareExtension, onPress, onRemove }) => ( +const ThumbButton: typeof React.Component = isIOS ? TouchableOpacity : TouchableNativeFeedback; + +const Thumb = ({ item, theme, isShareExtension, onPress, onRemove }: IThumb) => ( <ThumbButton style={styles.item} onPress={() => onPress(item)} activeOpacity={0.7}> <> <ThumbContent item={item} theme={theme} isShareExtension={isShareExtension} /> @@ -121,7 +134,7 @@ const Thumb = ({ item, theme, isShareExtension, onPress, onRemove }) => ( </ThumbButton> ); -const Thumbs = React.memo(({ attachments, theme, isShareExtension, onPress, onRemove }) => { +const Thumbs = React.memo(({ attachments, theme, isShareExtension, onPress, onRemove }: IThumbs) => { if (attachments?.length > 1) { return ( <FlatList @@ -143,24 +156,5 @@ const Thumbs = React.memo(({ attachments, theme, isShareExtension, onPress, onRe } return null; }); -Thumbs.propTypes = { - attachments: PropTypes.array, - theme: PropTypes.string, - isShareExtension: PropTypes.bool, - onPress: PropTypes.func, - onRemove: PropTypes.func -}; -Thumb.propTypes = { - item: PropTypes.object, - theme: PropTypes.string, - isShareExtension: PropTypes.bool, - onPress: PropTypes.func, - onRemove: PropTypes.func -}; -ThumbContent.propTypes = { - item: PropTypes.object, - theme: PropTypes.string, - isShareExtension: PropTypes.bool -}; export default Thumbs; diff --git a/app/views/ShareView/constants.js b/app/views/ShareView/constants.ts similarity index 100% rename from app/views/ShareView/constants.js rename to app/views/ShareView/constants.ts diff --git a/app/views/ShareView/index.js b/app/views/ShareView/index.tsx similarity index 84% rename from app/views/ShareView/index.js rename to app/views/ShareView/index.tsx index 473b518df..e10b21483 100644 --- a/app/views/ShareView/index.js +++ b/app/views/ShareView/index.tsx @@ -1,5 +1,6 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import { NativeModules, Text, View } from 'react-native'; import { connect } from 'react-redux'; import ShareExtension from 'rn-extensions-share'; @@ -24,9 +25,61 @@ import Thumbs from './Thumbs'; import Preview from './Preview'; import Header from './Header'; import styles from './styles'; +import { IAttachment, IServer } from './interfaces'; -class ShareView extends Component { - constructor(props) { +interface IShareViewState { + selected: IAttachment; + loading: boolean; + readOnly: boolean; + attachments: IAttachment[]; + text: string; + // TODO: Refactor when migrate room + room: any; + thread: any; + maxFileSize: number; + mediaAllowList: number; +} + +interface IShareViewProps { + // TODO: Refactor after react-navigation + navigation: StackNavigationProp<any, 'ShareView'>; + route: RouteProp< + { + ShareView: { + attachments: IAttachment[]; + isShareView?: boolean; + isShareExtension: boolean; + serverInfo: IServer; + text: string; + room: any; + thread: any; // change + }; + }, + 'ShareView' + >; + theme: string; + user: { + id: string; + username: string; + token: string; + }; + server: string; + FileUpload_MediaTypeWhiteList?: number; + FileUpload_MaxFileSize?: number; +} + +interface IMessageBoxShareView { + text: string; + forceUpdate(): void; +} + +class ShareView extends Component<IShareViewProps, IShareViewState> { + private messagebox: React.RefObject<IMessageBoxShareView>; + private files: any[]; + private isShareExtension: boolean; + private serverInfo: any; + + constructor(props: IShareViewProps) { super(props); this.messagebox = React.createRef(); this.files = props.route.params?.attachments ?? []; @@ -34,7 +87,7 @@ class ShareView extends Component { this.serverInfo = props.route.params?.serverInfo ?? {}; this.state = { - selected: {}, + selected: {} as IAttachment, loading: false, readOnly: false, attachments: [], @@ -61,7 +114,7 @@ class ShareView extends Component { const { room, thread, readOnly, attachments } = this.state; const { navigation, theme } = this.props; - const options = { + const options: StackNavigationOptions = { headerTitle: () => <Header room={room} thread={thread} />, headerTitleAlign: 'left', headerTintColor: themes[theme].previewTintColor @@ -69,9 +122,7 @@ class ShareView extends Component { // if is share extension show default back button if (!this.isShareExtension) { - options.headerLeft = () => ( - <HeaderButton.CloseModal navigation={navigation} buttonStyle={{ color: themes[theme].previewTintColor }} /> - ); + options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />; } if (!attachments.length && !readOnly) { @@ -203,10 +254,10 @@ class ShareView extends Component { } }; - selectFile = item => { + selectFile = (item: IAttachment) => { const { attachments, selected } = this.state; if (attachments.length > 0) { - const { text } = this.messagebox.current; + const text = this.messagebox.current?.text; const newAttachments = attachments.map(att => { if (att.path === selected.path) { att.description = text; @@ -217,7 +268,7 @@ class ShareView extends Component { } }; - removeFile = item => { + removeFile = (item: IAttachment) => { const { selected, attachments } = this.state; let newSelected; if (item.path === selected.path) { @@ -235,7 +286,7 @@ class ShareView extends Component { }); }; - onChangeText = text => { + onChangeText = (text: string) => { this.setState({ text }); }; @@ -318,21 +369,7 @@ class ShareView extends Component { } } -ShareView.propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - theme: PropTypes.string, - user: PropTypes.shape({ - id: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, - token: PropTypes.string.isRequired - }), - server: PropTypes.string, - FileUpload_MediaTypeWhiteList: PropTypes.string, - FileUpload_MaxFileSize: PropTypes.string -}; - -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ user: getUserSelector(state), server: state.share.server.server || state.server.server, FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList, diff --git a/app/views/ShareView/interfaces.ts b/app/views/ShareView/interfaces.ts new file mode 100644 index 000000000..09cb4d9eb --- /dev/null +++ b/app/views/ShareView/interfaces.ts @@ -0,0 +1,33 @@ +export interface IAttachment { + filename: string; + description?: string; + size: number; + mime?: string; + path: string; + canUpload: boolean; + error?: any; + uri: string; +} + +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/ShareView/styles.js b/app/views/ShareView/styles.ts similarity index 100% rename from app/views/ShareView/styles.js rename to app/views/ShareView/styles.ts diff --git a/app/views/ShareView/utils.js b/app/views/ShareView/utils.js deleted file mode 100644 index f55cd8356..000000000 --- a/app/views/ShareView/utils.js +++ /dev/null @@ -1,4 +0,0 @@ -import { isAndroid } from '../../utils/deviceInfo'; - -// Limit preview to 3MB on iOS share extension -export const allowPreview = (isShareExtension, size) => isAndroid || !isShareExtension || size < 3000000; diff --git a/app/views/ShareView/utils.ts b/app/views/ShareView/utils.ts new file mode 100644 index 000000000..fa6150411 --- /dev/null +++ b/app/views/ShareView/utils.ts @@ -0,0 +1,5 @@ +import { isAndroid } from '../../utils/deviceInfo'; + +// Limit preview to 3MB on iOS share extension +export const allowPreview = (isShareExtension: boolean, size: number): boolean => + isAndroid || !isShareExtension || size < 3000000; diff --git a/app/views/SidebarView/SidebarItem.js b/app/views/SidebarView/SidebarItem.tsx similarity index 74% rename from app/views/SidebarView/SidebarItem.js rename to app/views/SidebarView/SidebarItem.tsx index 4e0f9c1a1..7590e82ca 100644 --- a/app/views/SidebarView/SidebarItem.js +++ b/app/views/SidebarView/SidebarItem.tsx @@ -1,13 +1,22 @@ import React from 'react'; import { Text, View } from 'react-native'; -import PropTypes from 'prop-types'; import Touch from '../../utils/touch'; import { themes } from '../../constants/colors'; import { withTheme } from '../../theme'; import styles from './styles'; -const Item = React.memo(({ left, right, text, onPress, testID, current, theme }) => ( +interface SidebarItemProps { + left: JSX.Element; + right: JSX.Element; + text: string; + current: boolean; + onPress(): void; + testID: string; + theme: string; +} + +const Item = React.memo(({ left, right, text, onPress, testID, current, theme }: SidebarItemProps) => ( <Touch key={testID} testID={testID} @@ -24,14 +33,4 @@ const Item = React.memo(({ left, right, text, onPress, testID, current, theme }) </Touch> )); -Item.propTypes = { - left: PropTypes.element, - right: PropTypes.element, - text: PropTypes.string, - current: PropTypes.bool, - onPress: PropTypes.func, - testID: PropTypes.string, - theme: PropTypes.string -}; - export default withTheme(Item); diff --git a/app/views/SidebarView/index.js b/app/views/SidebarView/index.tsx similarity index 84% rename from app/views/SidebarView/index.js rename to app/views/SidebarView/index.tsx index 5f27e7062..f97e3ea99 100644 --- a/app/views/SidebarView/index.js +++ b/app/views/SidebarView/index.tsx @@ -1,5 +1,6 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import { DrawerNavigationProp } from '@react-navigation/drawer'; +import { DrawerNavigationState } from '@react-navigation/native'; import { ScrollView, Text, TouchableWithoutFeedback, View } from 'react-native'; import { connect } from 'react-redux'; import { dequal } from 'dequal'; @@ -18,38 +19,52 @@ import Navigation from '../../lib/Navigation'; import SidebarItem from './SidebarItem'; import styles from './styles'; -const Separator = React.memo(({ theme }) => <View style={[styles.separator, { borderColor: themes[theme].separatorColor }]} />); -Separator.propTypes = { - theme: PropTypes.string -}; +interface ISeparatorProps { + theme: string; +} -class Sidebar extends Component { - static propTypes = { - baseUrl: PropTypes.string, - navigation: PropTypes.object, - Site_Name: PropTypes.string.isRequired, - user: PropTypes.object, - state: PropTypes.string, - theme: PropTypes.string, - loadingServer: PropTypes.bool, - useRealName: PropTypes.bool, - allowStatusMessage: PropTypes.bool, - isMasterDetail: PropTypes.bool, - viewStatisticsPermission: PropTypes.object, - viewRoomAdministrationPermission: PropTypes.object, - viewUserAdministrationPermission: PropTypes.object, - viewPrivilegedSettingPermission: PropTypes.object +// TODO: remove this +const Separator = React.memo(({ theme }: ISeparatorProps) => ( + <View style={[styles.separator, { borderColor: themes[theme].separatorColor }]} /> +)); + +interface ISidebarState { + showStatus: boolean; +} + +interface ISidebarProps { + baseUrl: string; + navigation: DrawerNavigationProp<any, 'Sidebar'>; + state: DrawerNavigationState<any>; + Site_Name: string; + user: { + statusText: string; + status: string; + username: string; + name: string; + roles: string[]; }; + theme: string; + loadingServer: boolean; + useRealName: boolean; + allowStatusMessage: boolean; + isMasterDetail: boolean; + viewStatisticsPermission: string[]; + viewRoomAdministrationPermission: string[]; + viewUserAdministrationPermission: string[]; + viewPrivilegedSettingPermission: string[]; +} - constructor(props) { +class Sidebar extends Component<ISidebarProps, ISidebarState> { + constructor(props: ISidebarProps) { super(props); this.state = { showStatus: false }; } - shouldComponentUpdate(nextProps, nextState) { - const { showStatus, isAdmin } = this.state; + shouldComponentUpdate(nextProps: ISidebarProps, nextState: ISidebarState) { + const { showStatus } = this.state; const { Site_Name, user, @@ -91,9 +106,6 @@ class Sidebar extends Component { if (nextProps.useRealName !== useRealName) { return true; } - if (nextState.isAdmin !== isAdmin) { - return true; - } if (!dequal(nextProps.viewStatisticsPermission, viewStatisticsPermission)) { return true; } @@ -127,7 +139,7 @@ class Sidebar extends Component { let isAdmin = false; if (roles) { - isAdmin = allPermissions.reduce((result, permission) => { + isAdmin = allPermissions.reduce((result: boolean, permission) => { if (permission) { return result || permission.some(r => roles.indexOf(r) !== -1); } @@ -137,7 +149,8 @@ class Sidebar extends Component { return isAdmin; } - sidebarNavigate = route => { + sidebarNavigate = (route: string) => { + // @ts-ignore logEvent(events[`SIDEBAR_GO_${route.replace('StackNavigator', '').replace('View', '').toUpperCase()}`]); Navigation.navigate(route); }; @@ -242,7 +255,7 @@ class Sidebar extends Component { ]} {...scrollPersistTaps}> <TouchableWithoutFeedback onPress={this.onPressUser} testID='sidebar-close-drawer'> - <View style={styles.header} theme={theme}> + <View style={styles.header}> <Avatar text={user.username} style={styles.avatar} size={30} /> <View style={styles.headerTextContainer}> <View style={styles.headerUsername}> @@ -278,7 +291,7 @@ class Sidebar extends Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ Site_Name: state.settings.Site_Name, user: getUserSelector(state), baseUrl: state.server.server, diff --git a/app/views/SidebarView/styles.js b/app/views/SidebarView/styles.ts similarity index 100% rename from app/views/SidebarView/styles.js rename to app/views/SidebarView/styles.ts diff --git a/app/views/UserNotificationPreferencesView/index.js b/app/views/UserNotificationPreferencesView/index.tsx similarity index 64% rename from app/views/UserNotificationPreferencesView/index.js rename to app/views/UserNotificationPreferencesView/index.tsx index 66b64ee5a..541d5da81 100644 --- a/app/views/UserNotificationPreferencesView/index.js +++ b/app/views/UserNotificationPreferencesView/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { StyleSheet, Text } from 'react-native'; -import PropTypes from 'prop-types'; +import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { connect } from 'react-redux'; import { themes } from '../../constants/colors'; @@ -22,20 +22,34 @@ const styles = StyleSheet.create({ } }); -class UserNotificationPreferencesView extends React.Component { - static navigationOptions = () => ({ +type TKey = 'desktopNotifications' | 'pushNotifications' | 'emailNotificationMode'; + +interface IUserNotificationPreferencesViewState { + preferences: { + desktopNotifications?: string; + pushNotifications?: string; + emailNotificationMode?: string; + }; + loading: boolean; +} + +interface IUserNotificationPreferencesViewProps { + navigation: StackNavigationProp<any, 'UserNotificationPreferencesView'>; + theme: string; + user: { + id: string; + }; +} + +class UserNotificationPreferencesView extends React.Component< + IUserNotificationPreferencesViewProps, + IUserNotificationPreferencesViewState +> { + static navigationOptions = (): StackNavigationOptions => ({ title: I18n.t('Notification_Preferences') }); - static propTypes = { - navigation: PropTypes.object, - theme: PropTypes.string, - user: PropTypes.shape({ - id: PropTypes.string - }) - }; - - constructor(props) { + constructor(props: IUserNotificationPreferencesViewProps) { super(props); this.state = { preferences: {}, @@ -51,43 +65,43 @@ class UserNotificationPreferencesView extends React.Component { this.setState({ preferences, loading: true }); } - findDefaultOption = key => { + findDefaultOption = (key: TKey) => { const { preferences } = this.state; const option = preferences[key] ? OPTIONS[key].find(item => item.value === preferences[key]) : OPTIONS[key][0]; return option; }; - renderPickerOption = key => { + renderPickerOption = (key: TKey) => { const { theme } = this.props; const text = this.findDefaultOption(key); - return ( - <Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}> - {I18n.t(text?.label, { defaultValue: text?.label, second: text?.second })} - </Text> - ); + return <Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}>{I18n.t(text?.label)}</Text>; }; - pickerSelection = (title, key) => { + pickerSelection = (title: string, key: TKey) => { const { preferences } = this.state; const { navigation } = this.props; let values = OPTIONS[key]; const defaultOption = this.findDefaultOption(key); if (OPTIONS[key][0]?.value !== 'default') { - values = [{ label: `${I18n.t('Default')} (${I18n.t(defaultOption.label)})` }, ...OPTIONS[key]]; + const defaultValue = { label: `${I18n.t('Default')} (${I18n.t(defaultOption?.label)})` } as { + label: string; + value: string; + }; + values = [defaultValue, ...OPTIONS[key]]; } navigation.navigate('PickerView', { title, data: values, value: preferences[key], - onChangeValue: value => this.onValueChangePicker(key, value ?? defaultOption.value) + onChangeValue: (value: string) => this.onValueChangePicker(key, value ?? defaultOption?.value) }); }; - onValueChangePicker = (key, value) => this.saveNotificationPreferences({ [key]: value.toString() }); + onValueChangePicker = (key: TKey, value: string) => this.saveNotificationPreferences({ [key]: value.toString() }); - saveNotificationPreferences = async params => { + saveNotificationPreferences = async (params: { [key: string]: string }) => { const { user } = this.props; const { id } = user; const result = await RocketChat.setUserPreferences(id, params); @@ -111,7 +125,7 @@ class UserNotificationPreferencesView extends React.Component { <List.Item title='Alert' testID='user-notification-preference-view-alert' - onPress={title => this.pickerSelection(title, 'desktopNotifications')} + onPress={(title: string) => this.pickerSelection(title, 'desktopNotifications')} right={() => this.renderPickerOption('desktopNotifications')} /> <List.Separator /> @@ -123,8 +137,8 @@ class UserNotificationPreferencesView extends React.Component { <List.Item title='Alert' testID='user-notification-preference-view-push-notification' - onPress={title => this.pickerSelection(title, 'mobileNotifications')} - right={() => this.renderPickerOption('mobileNotifications')} + onPress={(title: string) => this.pickerSelection(title, 'pushNotifications')} + right={() => this.renderPickerOption('pushNotifications')} /> <List.Separator /> <List.Info info='Push_Notifications_Alert_Info' /> @@ -135,7 +149,7 @@ class UserNotificationPreferencesView extends React.Component { <List.Item title='Alert' testID='user-notification-preference-view-email-alert' - onPress={title => this.pickerSelection(title, 'emailNotificationMode')} + onPress={(title: string) => this.pickerSelection(title, 'emailNotificationMode')} right={() => this.renderPickerOption('emailNotificationMode')} /> <List.Separator /> @@ -151,7 +165,7 @@ class UserNotificationPreferencesView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ user: getUserSelector(state) }); diff --git a/app/views/UserNotificationPreferencesView/options.js b/app/views/UserNotificationPreferencesView/options.ts similarity index 92% rename from app/views/UserNotificationPreferencesView/options.js rename to app/views/UserNotificationPreferencesView/options.ts index e5c2094b7..423b23e27 100644 --- a/app/views/UserNotificationPreferencesView/options.js +++ b/app/views/UserNotificationPreferencesView/options.ts @@ -19,7 +19,7 @@ const commonOptions = [ export const OPTIONS = { desktopNotifications: commonOptions, - mobileNotifications: commonOptions, + pushNotifications: commonOptions, emailNotificationMode: [ { label: 'Email_Notification_Mode_All', diff --git a/app/views/UserPreferencesView/index.js b/app/views/UserPreferencesView/index.tsx similarity index 80% rename from app/views/UserPreferencesView/index.js rename to app/views/UserPreferencesView/index.tsx index 476c49a7d..2abce444e 100644 --- a/app/views/UserPreferencesView/index.js +++ b/app/views/UserPreferencesView/index.tsx @@ -1,6 +1,6 @@ +import { StackNavigationProp } from '@react-navigation/stack'; import React, { useEffect, useState } from 'react'; import { Switch } from 'react-native'; -import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import I18n from '../../i18n'; @@ -12,7 +12,11 @@ import { SWITCH_TRACK_COLOR } from '../../constants/colors'; import { getUserSelector } from '../../selectors/login'; import RocketChat from '../../lib/rocketchat'; -const UserPreferencesView = ({ navigation }) => { +interface IUserPreferencesViewProps { + navigation: StackNavigationProp<any, 'UserPreferencesView'>; +} + +const UserPreferencesView = ({ navigation }: IUserPreferencesViewProps): JSX.Element => { const user = useSelector(state => getUserSelector(state)); const [enableParser, setEnableParser] = useState(user.enableMessageParserEarlyAdoption); @@ -22,12 +26,12 @@ const UserPreferencesView = ({ navigation }) => { }); }, []); - const navigateToScreen = (screen, params) => { - logEvent(events[`SE_GO_${screen.replace('View', '').toUpperCase()}`]); - navigation.navigate(screen, params); + const navigateToScreen = (screen: string) => { + logEvent(events.UP_GO_USER_NOTIFICATION_PREF); + navigation.navigate(screen); }; - const toggleMessageParser = async value => { + const toggleMessageParser = async (value: boolean) => { try { await RocketChat.saveUserPreferences({ id: user.id, enableMessageParserEarlyAdoption: value }); setEnableParser(value); @@ -68,8 +72,4 @@ const UserPreferencesView = ({ navigation }) => { ); }; -UserPreferencesView.propTypes = { - navigation: PropTypes.object -}; - export default UserPreferencesView; diff --git a/app/views/definition/ILivechatDepartment.ts b/app/views/definition/ILivechatDepartment.ts new file mode 100644 index 000000000..22c352007 --- /dev/null +++ b/app/views/definition/ILivechatDepartment.ts @@ -0,0 +1,15 @@ +export interface ILivechatDepartment { + _id: string; + name: string; + enabled: boolean; + description: string; + showOnRegistration: boolean; + showOnOfflineForm: boolean; + requestTagBeforeClosingChat: boolean; + email: string; + chatClosingTags: string[]; + offlineMessageChannelName: string; + numAgents: number; + _updatedAt?: Date; + businessHourId?: string; +} diff --git a/e2e/docker/docker-entrypoint-initdb.d/rocketchat_permissions.js b/e2e/docker/docker-entrypoint-initdb.d/rocketchat_permissions.js index 4b05300fa..1a528caae 100644 --- a/e2e/docker/docker-entrypoint-initdb.d/rocketchat_permissions.js +++ b/e2e/docker/docker-entrypoint-initdb.d/rocketchat_permissions.js @@ -80,7 +80,7 @@ db.getCollection("rocketchat_permissions").insert({"_id":"change-setting-Account db.getCollection("rocketchat_permissions").insert({"_id":"change-setting-Accounts_Default_User_Preferences_hideUsernames","_updatedAt":new Date(1591734395388),"group":"Accounts","groupPermissionId":"change-setting-Accounts","level":"settings","roles":[],"section":"Accounts_Default_User_Preferences","sectionPermissionId":"change-setting-Accounts_Default_User_Preferences","settingId":"Accounts_Default_User_Preferences_hideUsernames","sorter":NumberInt(60)}); db.getCollection("rocketchat_permissions").insert({"_id":"change-setting-Accounts_Default_User_Preferences_idleTimeLimit","_updatedAt":new Date(1591734395359),"group":"Accounts","groupPermissionId":"change-setting-Accounts","level":"settings","roles":[],"section":"Accounts_Default_User_Preferences","sectionPermissionId":"change-setting-Accounts_Default_User_Preferences","settingId":"Accounts_Default_User_Preferences_idleTimeLimit","sorter":NumberInt(48)}); db.getCollection("rocketchat_permissions").insert({"_id":"change-setting-Accounts_Default_User_Preferences_messageViewMode","_updatedAt":new Date(1591734395410),"group":"Accounts","groupPermissionId":"change-setting-Accounts","level":"settings","roles":[],"section":"Accounts_Default_User_Preferences","sectionPermissionId":"change-setting-Accounts_Default_User_Preferences","settingId":"Accounts_Default_User_Preferences_messageViewMode","sorter":NumberInt(71)}); -db.getCollection("rocketchat_permissions").insert({"_id":"change-setting-Accounts_Default_User_Preferences_mobileNotifications","_updatedAt":new Date(1591734395373),"group":"Accounts","groupPermissionId":"change-setting-Accounts","level":"settings","roles":[],"section":"Accounts_Default_User_Preferences","sectionPermissionId":"change-setting-Accounts_Default_User_Preferences","settingId":"Accounts_Default_User_Preferences_mobileNotifications","sorter":NumberInt(53)}); +db.getCollection("rocketchat_permissions").insert({"_id":"change-setting-Accounts_Default_User_Preferences_pushNotifications","_updatedAt":new Date(1591734395373),"group":"Accounts","groupPermissionId":"change-setting-Accounts","level":"settings","roles":[],"section":"Accounts_Default_User_Preferences","sectionPermissionId":"change-setting-Accounts_Default_User_Preferences","settingId":"Accounts_Default_User_Preferences_pushNotifications","sorter":NumberInt(53)}); db.getCollection("rocketchat_permissions").insert({"_id":"change-setting-Accounts_Default_User_Preferences_muteFocusedConversations","_updatedAt":new Date(1591734395420),"group":"Accounts","groupPermissionId":"change-setting-Accounts","level":"settings","roles":[],"section":"Accounts_Default_User_Preferences","sectionPermissionId":"change-setting-Accounts_Default_User_Preferences","settingId":"Accounts_Default_User_Preferences_muteFocusedConversations","sorter":NumberInt(75)}); db.getCollection("rocketchat_permissions").insert({"_id":"change-setting-Accounts_Default_User_Preferences_newMessageNotification","_updatedAt":new Date(1591734395417),"group":"Accounts","groupPermissionId":"change-setting-Accounts","level":"settings","roles":[],"section":"Accounts_Default_User_Preferences","sectionPermissionId":"change-setting-Accounts_Default_User_Preferences","settingId":"Accounts_Default_User_Preferences_newMessageNotification","sorter":NumberInt(74)}); db.getCollection("rocketchat_permissions").insert({"_id":"change-setting-Accounts_Default_User_Preferences_newRoomNotification","_updatedAt":new Date(1591734395415),"group":"Accounts","groupPermissionId":"change-setting-Accounts","level":"settings","roles":[],"section":"Accounts_Default_User_Preferences","sectionPermissionId":"change-setting-Accounts_Default_User_Preferences","settingId":"Accounts_Default_User_Preferences_newRoomNotification","sorter":NumberInt(73)}); diff --git a/e2e/docker/docker-entrypoint-initdb.d/rocketchat_settings.js b/e2e/docker/docker-entrypoint-initdb.d/rocketchat_settings.js index 13f672f76..dc30de33d 100644 --- a/e2e/docker/docker-entrypoint-initdb.d/rocketchat_settings.js +++ b/e2e/docker/docker-entrypoint-initdb.d/rocketchat_settings.js @@ -64,7 +64,7 @@ db.getCollection("rocketchat_settings").insert({"_id":"Accounts_Default_User_Pre db.getCollection("rocketchat_settings").insert({"_id":"Accounts_Default_User_Preferences_hideUsernames","_updatedAt":new Date(1591734377818),"autocomplete":true,"blocked":false,"createdAt":new Date(1584022362944),"group":"Accounts","hidden":false,"i18nDescription":"Accounts_Default_User_Preferences_hideUsernames_Description","i18nLabel":"Hide_usernames","packageValue":false,"public":true,"secret":false,"section":"Accounts_Default_User_Preferences","sorter":NumberInt(60),"ts":new Date(1589465206154),"type":"boolean","value":false,"valueSource":"packageValue"}); db.getCollection("rocketchat_settings").insert({"_id":"Accounts_Default_User_Preferences_idleTimeLimit","_updatedAt":new Date(1591734377768),"autocomplete":true,"blocked":false,"createdAt":new Date(1584022362881),"group":"Accounts","hidden":false,"i18nDescription":"Accounts_Default_User_Preferences_idleTimeLimit_Description","i18nLabel":"Idle_Time_Limit","packageValue":NumberInt(300),"public":true,"secret":false,"section":"Accounts_Default_User_Preferences","sorter":NumberInt(48),"ts":new Date(1589465206085),"type":"int","value":NumberInt(300),"valueSource":"packageValue"}); db.getCollection("rocketchat_settings").insert({"_id":"Accounts_Default_User_Preferences_messageViewMode","_updatedAt":new Date(1591734377861),"autocomplete":true,"blocked":false,"createdAt":new Date(1584022362992),"group":"Accounts","hidden":false,"i18nDescription":"Accounts_Default_User_Preferences_messageViewMode_Description","i18nLabel":"MessageBox_view_mode","packageValue":NumberInt(0),"public":true,"secret":false,"section":"Accounts_Default_User_Preferences","sorter":NumberInt(71),"ts":new Date(1589465206214),"type":"select","value":NumberInt(0),"valueSource":"packageValue","values":[{"key":NumberInt(0),"i18nLabel":"Normal"},{"key":NumberInt(1),"i18nLabel":"Cozy"},{"key":NumberInt(2),"i18nLabel":"Compact"}]}); -db.getCollection("rocketchat_settings").insert({"_id":"Accounts_Default_User_Preferences_mobileNotifications","_updatedAt":new Date(1591734377791),"autocomplete":true,"blocked":false,"createdAt":new Date(1584022362905),"group":"Accounts","hidden":false,"i18nDescription":"Accounts_Default_User_Preferences_mobileNotifications_Description","i18nLabel":"Accounts_Default_User_Preferences_mobileNotifications","packageValue":"all","public":true,"secret":false,"section":"Accounts_Default_User_Preferences","sorter":NumberInt(53),"ts":new Date(1589465206111),"type":"select","value":"all","valueSource":"packageValue","values":[{"key":"all","i18nLabel":"All_messages"},{"key":"mentions","i18nLabel":"Mentions"},{"key":"nothing","i18nLabel":"Nothing"}]}); +db.getCollection("rocketchat_settings").insert({"_id":"Accounts_Default_User_Preferences_pushNotifications","_updatedAt":new Date(1591734377791),"autocomplete":true,"blocked":false,"createdAt":new Date(1584022362905),"group":"Accounts","hidden":false,"i18nDescription":"Accounts_Default_User_Preferences_pushNotifications_Description","i18nLabel":"Accounts_Default_User_Preferences_pushNotifications","packageValue":"all","public":true,"secret":false,"section":"Accounts_Default_User_Preferences","sorter":NumberInt(53),"ts":new Date(1589465206111),"type":"select","value":"all","valueSource":"packageValue","values":[{"key":"all","i18nLabel":"All_messages"},{"key":"mentions","i18nLabel":"Mentions"},{"key":"nothing","i18nLabel":"Nothing"}]}); db.getCollection("rocketchat_settings").insert({"_id":"Accounts_Default_User_Preferences_muteFocusedConversations","_updatedAt":new Date(1591734377881),"autocomplete":true,"blocked":false,"createdAt":new Date(1584022363009),"group":"Accounts","hidden":false,"i18nDescription":"Accounts_Default_User_Preferences_muteFocusedConversations_Description","i18nLabel":"Mute_Focused_Conversations","packageValue":true,"public":true,"secret":false,"section":"Accounts_Default_User_Preferences","sorter":NumberInt(75),"ts":new Date(1589465206242),"type":"boolean","value":true,"valueSource":"packageValue"}); db.getCollection("rocketchat_settings").insert({"_id":"Accounts_Default_User_Preferences_newMessageNotification","_updatedAt":new Date(1591734377877),"autocomplete":true,"blocked":false,"createdAt":new Date(1584022363005),"group":"Accounts","hidden":false,"i18nDescription":"Accounts_Default_User_Preferences_newMessageNotification_Description","i18nLabel":"New_Message_Notification","packageValue":"chime","public":true,"secret":false,"section":"Accounts_Default_User_Preferences","sorter":NumberInt(74),"ts":new Date(1589465206236),"type":"select","value":"chime","valueSource":"packageValue","values":[{"key":"none","i18nLabel":"None"},{"key":"chime","i18nLabel":"Default"}]}); db.getCollection("rocketchat_settings").insert({"_id":"Accounts_Default_User_Preferences_newRoomNotification","_updatedAt":new Date(1591734377868),"autocomplete":true,"blocked":false,"createdAt":new Date(1584022363000),"group":"Accounts","hidden":false,"i18nDescription":"Accounts_Default_User_Preferences_newRoomNotification_Description","i18nLabel":"New_Room_Notification","packageValue":"door","public":true,"secret":false,"section":"Accounts_Default_User_Preferences","sorter":NumberInt(73),"ts":new Date(1589465206232),"type":"select","value":"door","valueSource":"packageValue","values":[{"key":"none","i18nLabel":"None"},{"key":"door","i18nLabel":"Default"}]}); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index aaf432c41..8ebb04855 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -532,6 +532,8 @@ PODS: - Firebase/Crashlytics (~> 6.27.0) - React - RNFBApp + - RNFileViewer (2.1.4): + - React-Core - RNGestureHandler (1.10.3): - React-Core - RNImageCropPicker (0.36.3): @@ -700,6 +702,7 @@ DEPENDENCIES: - "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)" - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" - "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)" + - RNFileViewer (from `../node_modules/react-native-file-viewer`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNLocalize (from `../node_modules/react-native-localize`) @@ -894,6 +897,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-firebase/app" RNFBCrashlytics: :path: "../node_modules/@react-native-firebase/crashlytics" + RNFileViewer: + :path: "../node_modules/react-native-file-viewer" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNImageCropPicker: @@ -1028,6 +1033,7 @@ SPEC CHECKSUMS: RNFBAnalytics: dae6d7b280ba61c96e1bbdd34aca3154388f025e RNFBApp: 6fd8a7e757135d4168bf033a8812c241af7363a0 RNFBCrashlytics: 88de72c2476b5868a892d9523b89b86c527c540e + RNFileViewer: 83cc066ad795b1f986791d03b56fe0ee14b6a69f RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211 RNImageCropPicker: 97289cd94fb01ab79db4e5c92938be4d0d63415d RNLocalize: 82a569022724d35461e2dc5b5d015a13c3ca995b diff --git a/package.json b/package.json index d421bfc7b..1a0a4851e 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "react-native-document-picker": "5.2.0", "react-native-easy-grid": "^0.2.2", "react-native-easy-toast": "^1.2.0", + "react-native-file-viewer": "^2.1.4", "react-native-gesture-handler": "^1.10.3", "react-native-image-crop-picker": "RocketChat/react-native-image-crop-picker", "react-native-image-progress": "^1.1.1", @@ -147,6 +148,7 @@ "@types/lodash": "^4.14.171", "@types/react": "^17.0.14", "@types/react-native": "^0.62.7", + "@types/react-native-background-timer": "^2.0.0", "@types/react-native-config-reader": "^4.1.0", "@types/react-native-platform-touchable": "^1.1.2", "@types/react-native-scrollable-tab-view": "^0.10.2", diff --git a/yarn.lock b/yarn.lock index f4ad938a8..4325aa75a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4372,6 +4372,11 @@ "@types/history" "*" "@types/react" "*" +"@types/react-native-background-timer@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/react-native-background-timer/-/react-native-background-timer-2.0.0.tgz#c44c57f8fbca9d9d5521fdd72a8f55232b79381e" + integrity sha512-y5VW82dL/ESOLg+5QQHyBdsFVA4ZklENxmOyxv8o06T+3HBG2JOSuz/CIPz1vKdB7dmWDGPZNuPosdtnp+xv2A== + "@types/react-native-config-reader@^4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@types/react-native-config-reader/-/react-native-config-reader-4.1.0.tgz#33066cd0452b86b605b41bed47b38470dd85d428" @@ -14254,6 +14259,11 @@ react-native-easy-toast@^1.2.0: dependencies: prop-types "^15.5.10" +react-native-file-viewer@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/react-native-file-viewer/-/react-native-file-viewer-2.1.4.tgz#987b2902f0f0ac87b42f3ac3d3037c8ae98f17a6" + integrity sha512-G3ko9lmqxT+lWhsDNy2K3Jes6xSMsUvlYwuwnRCNk2wC6hgYMeoeaiwDt8R3CdON781hB6Ej1eu3ir1QATtHXg== + react-native-flipper@^0.34.0: version "0.34.0" resolved "https://registry.yarnpkg.com/react-native-flipper/-/react-native-flipper-0.34.0.tgz#7df1f38ba5d97a9321125fe0fccbe47d99e6fa1d"