Merge branch 'develop' into new.add-discusions-roomactionsview

This commit is contained in:
Gerzon Z 2021-11-23 12:02:43 -04:00 committed by GitHub
commit 2b7f589847
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 1438 additions and 942 deletions

View File

@ -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']
}
}
},

View File

@ -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

View File

@ -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());

View File

@ -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
}
};

View File

@ -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}/`;

View File

@ -17,7 +17,7 @@ export const useActionSheet = () => useContext(context);
const { Provider, Consumer } = context;
export const withActionSheet = (Component: React.FC) =>
export const withActionSheet = <P extends object>(Component: React.ComponentType<P>) =>
forwardRef((props: any, ref: ForwardedRef<any>) => (
<Consumer>{(contexts: any) => <Component {...props} {...contexts} ref={ref} />}</Consumer>
));

View File

@ -16,7 +16,7 @@ export interface IAvatar {
onPress(): void;
getCustomEmoji(): any;
avatarETag: string;
isStatic: boolean;
isStatic: boolean | string;
rid: string;
blockUnauthenticatedAccess: boolean;
serverVersion: string;

View File

@ -27,13 +27,11 @@ export const FormContainerInner = ({ children }: { children: React.ReactNode }):
);
const FormContainer = ({ children, theme, testID, ...props }: IFormContainer): JSX.Element => (
// @ts-ignore
<KeyboardView
style={{ backgroundColor: themes[theme].backgroundColor }}
contentContainerStyle={sharedStyles.container}
keyboardVerticalOffset={128}>
<StatusBar />
{/* @ts-ignore*/}
<ScrollView
style={sharedStyles.container}
contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}

View File

@ -8,7 +8,7 @@ import I18n from '../../i18n';
interface IPasscodeChoose {
theme: string;
force: boolean;
force?: boolean;
finishProcess: Function;
}

View File

@ -1,4 +1,5 @@
import React from 'react';
import { StyleProp, TextStyle } from 'react-native';
import { CustomIcon } from '../../lib/Icons';
import { STATUS_COLORS } from '../../constants/colors';
@ -6,14 +7,14 @@ import { STATUS_COLORS } from '../../constants/colors';
interface IStatus {
status: string;
size: number;
style: any;
style?: StyleProp<TextStyle>;
}
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<TextStyle> = [
{
width: size,
height: size,

View File

@ -58,7 +58,7 @@ interface IRCTextInputProps extends TextInputProps {
};
loading?: boolean;
containerStyle?: StyleProp<ViewStyle>;
inputStyle?: TextStyle;
inputStyle?: StyleProp<TextStyle>;
inputRef?: React.Ref<unknown>;
testID?: string;
iconLeft?: string;

View File

@ -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;

View File

@ -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<IMessageReplyAttachment>;
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}>
<View style={styles.attachmentContainer}>
<Title attachment={attachment} timeFormat={timeFormat} theme={theme} />
<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*/}

View File

@ -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

View File

@ -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;

View File

@ -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';

View File

@ -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"
}
"Enable_Message_Parser": "Enable Message Parser",
"Unsupported_format": "Unsupported format",
"Downloaded_file": "Downloaded file",
"Error_Download_file": "Error while downloading file"
}

View File

@ -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"
}
"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"
}

View File

@ -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 };
}
}
}

View File

@ -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 });

View File

@ -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 {

View File

@ -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');

View File

@ -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 (

View File

@ -1,5 +1,3 @@
// @ts-ignore
// eslint-disable-next-line import/no-unresolved
export * from './ImageViewer';
export * from './types';
export * from './ImageComponent';

View File

@ -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>
);

View File

@ -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;
}

View File

@ -122,6 +122,11 @@ const handleOpen = function* handleOpen({ params }) {
host = id;
}
});
if (!host && params.fullURL) {
RocketChat.callJitsiWithoutServer(params.fullURL);
return;
}
}
if (params.type === 'oauth') {

View File

@ -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') });
}
};

View File

@ -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',

View File

@ -1,4 +0,0 @@
export default {
keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'interactive'
};

View File

@ -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;

View File

@ -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
});

View File

@ -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']
});

View File

@ -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

View File

@ -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;

View File

@ -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));

View File

@ -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

View File

@ -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

View File

@ -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 => {

View File

@ -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));

View File

@ -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));

View File

@ -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>
);

View File

@ -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));

View File

@ -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'));

View File

@ -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));

View File

@ -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));

View File

@ -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
});

View File

@ -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));

View File

@ -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;

View File

@ -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,

View File

@ -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 />

View File

@ -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',

View File

@ -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 });

View File

@ -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));

View File

@ -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;
};
}

View File

@ -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
});

View File

@ -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),

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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,

View File

@ -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)
});

View File

@ -19,7 +19,7 @@ const commonOptions = [
export const OPTIONS = {
desktopNotifications: commonOptions,
mobileNotifications: commonOptions,
pushNotifications: commonOptions,
emailNotificationMode: [
{
label: 'Email_Notification_Mode_All',

View File

@ -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;

View File

@ -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;
}

View File

@ -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)});

View File

@ -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"}]});

View File

@ -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

View File

@ -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",

View File

@ -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"