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}>
+ {loading ? (
+
+
+
+
+ ) : null}
{/* @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)}>
-
+ {loading ? (
+
+ ) : (
+
+ )}
{/* @ts-ignore*/}
;
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 | 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 {
@@ -22,9 +20,7 @@ export default class KeyboardView extends React.PureComponent
+ extraHeight={keyboardVerticalOffset}>
{children}
);
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;
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 => {
+ 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 => {
+ 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 = {
+ 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;
+
+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;
+ 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 {
+ 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 (
- this.onSearchChangeText(text)} testID='add-existing-channel-view-search' />
+ this.onSearchChangeText(text)} testID='add-existing-channel-view-search' />
);
};
- 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;
+ 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 {
+ 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: () => (
-
- ),
+ headerLeft: () => ,
headerRight: () =>
- Allow_Save_Media_to_Gallery ? (
-
- ) : null,
+ Allow_Save_Media_to_Gallery ? : null,
headerBackground: () => ,
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) => (