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: { settings: {
'import/resolver': { 'import/resolver': {
node: { 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'; 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)); jest.mock('../app/lib/database', () => jest.fn(() => null));
global.Date.now = jest.fn(() => new Date('2019-10-10').getTime()); global.Date.now = jest.fn(() => new Date('2019-10-10').getTime());

View File

@ -65,6 +65,7 @@ export const themes: any = {
previewBackground: '#1F2329', previewBackground: '#1F2329',
previewTintColor: '#ffffff', previewTintColor: '#ffffff',
backdropOpacity: 0.3, backdropOpacity: 0.3,
attachmentLoadingOpacity: 0.7,
...mentions ...mentions
}, },
dark: { dark: {
@ -112,6 +113,7 @@ export const themes: any = {
previewBackground: '#030b1b', previewBackground: '#030b1b',
previewTintColor: '#ffffff', previewTintColor: '#ffffff',
backdropOpacity: 0.9, backdropOpacity: 0.9,
attachmentLoadingOpacity: 0.3,
...mentions ...mentions
}, },
black: { black: {
@ -159,6 +161,7 @@ export const themes: any = {
previewBackground: '#000000', previewBackground: '#000000',
previewTintColor: '#ffffff', previewTintColor: '#ffffff',
backdropOpacity: 0.9, backdropOpacity: 0.9,
attachmentLoadingOpacity: 0.3,
...mentions ...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; 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>) => ( forwardRef((props: any, ref: ForwardedRef<any>) => (
<Consumer>{(contexts: any) => <Component {...props} {...contexts} ref={ref} />}</Consumer> <Consumer>{(contexts: any) => <Component {...props} {...contexts} ref={ref} />}</Consumer>
)); ));

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { StyleProp, TextStyle } from 'react-native';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { STATUS_COLORS } from '../../constants/colors'; import { STATUS_COLORS } from '../../constants/colors';
@ -6,14 +7,14 @@ import { STATUS_COLORS } from '../../constants/colors';
interface IStatus { interface IStatus {
status: string; status: string;
size: number; size: number;
style: any; style?: StyleProp<TextStyle>;
} }
const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: IStatus) => { const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: IStatus) => {
const name = `status-${status}`; const name = `status-${status}`;
const isNameValid = CustomIcon.hasIcon(name); const isNameValid = CustomIcon.hasIcon(name);
const iconName = isNameValid ? name : 'status-offline'; const iconName = isNameValid ? name : 'status-offline';
const calculatedStyle = [ const calculatedStyle: StyleProp<TextStyle> = [
{ {
width: size, width: size,
height: size, height: size,

View File

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

View File

@ -8,10 +8,10 @@ import ActivityIndicator from '../../ActivityIndicator';
import styles from './styles'; import styles from './styles';
interface IInput { interface IInput {
children: JSX.Element; children?: JSX.Element;
onPress: Function; onPress: Function;
theme: string; theme: string;
inputStyle: object; inputStyle?: object;
disabled?: boolean | object; disabled?: boolean | object;
placeholder?: string; placeholder?: string;
loading?: boolean; 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 { StyleSheet, Text, View } from 'react-native';
import moment from 'moment'; import moment from 'moment';
import { transparentize } from 'color2k'; import { transparentize } from 'color2k';
@ -11,6 +11,9 @@ import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context'; import MessageContext from './Context';
import { fileDownloadAndPreview } from '../../utils/fileDownload';
import { formatAttachmentUrl } from '../../lib/utils';
import RCActivityIndicator from '../ActivityIndicator';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -28,6 +31,9 @@ const styles = StyleSheet.create({
flexDirection: 'column', flexDirection: 'column',
padding: 15 padding: 15
}, },
backdrop: {
...StyleSheet.absoluteFillObject
},
authorContainer: { authorContainer: {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
@ -120,7 +126,7 @@ interface IMessageFields {
} }
interface IMessageReply { interface IMessageReply {
attachment: Partial<IMessageReplyAttachment>; attachment: IMessageReplyAttachment;
timeFormat: string; timeFormat: string;
index: number; index: number;
theme: string; theme: string;
@ -209,12 +215,14 @@ const Fields = React.memo(
const Reply = React.memo( const Reply = React.memo(
({ attachment, timeFormat, index, getCustomEmoji, theme }: IMessageReply) => { ({ attachment, timeFormat, index, getCustomEmoji, theme }: IMessageReply) => {
const [loading, setLoading] = useState(false);
if (!attachment) { if (!attachment) {
return null; return null;
} }
const { baseUrl, user, jumpToMessage } = useContext(MessageContext); const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
const onPress = () => { const onPress = async () => {
let url = attachment.title_link || attachment.author_link; let url = attachment.title_link || attachment.author_link;
if (attachment.message_link) { if (attachment.message_link) {
return jumpToMessage(attachment.message_link); return jumpToMessage(attachment.message_link);
@ -223,10 +231,11 @@ const Reply = React.memo(
return; return;
} }
if (attachment.type === 'file') { if (attachment.type === 'file') {
if (!url.startsWith('http')) { setLoading(true);
url = `${baseUrl}${url}`; url = formatAttachmentUrl(attachment.title_link, user.id, user.token, baseUrl);
} await fileDownloadAndPreview(url, attachment);
url = `${url}?rc_uid=${user.id}&rc_token=${user.token}`; setLoading(false);
return;
} }
openLink(url, theme); openLink(url, theme);
}; };
@ -254,12 +263,23 @@ const Reply = React.memo(
borderColor borderColor
} }
]} ]}
background={Touchable.Ripple(themes[theme].bannerBackground)}> background={Touchable.Ripple(themes[theme].bannerBackground)}
disabled={loading}>
<View style={styles.attachmentContainer}> <View style={styles.attachmentContainer}>
<Title attachment={attachment} timeFormat={timeFormat} theme={theme} /> <Title attachment={attachment} timeFormat={timeFormat} theme={theme} />
<UrlImage image={attachment.thumb_url} /> <UrlImage image={attachment.thumb_url} />
<Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> <Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
<Fields 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> </View>
</Touchable> </Touchable>
{/* @ts-ignore*/} {/* @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 { StyleSheet } from 'react-native';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import Touchable from './Touchable'; import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import openLink from '../../utils/openLink';
import { isIOS } from '../../utils/deviceInfo'; import { isIOS } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context'; 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 SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
const isTypeSupported = (type: any) => SUPPORTED_TYPES.indexOf(type) !== -1; const isTypeSupported = (type: any) => SUPPORTED_TYPES.indexOf(type) !== -1;
@ -27,6 +31,9 @@ const styles = StyleSheet.create({
interface IMessageVideo { interface IMessageVideo {
file: { file: {
title: string;
title_link: string;
type: string;
video_type: string; video_type: string;
video_url: string; video_url: string;
description: string; description: string;
@ -39,15 +46,34 @@ interface IMessageVideo {
const Video = React.memo( const Video = React.memo(
({ file, showAttachment, getCustomEmoji, theme }: IMessageVideo) => { ({ file, showAttachment, getCustomEmoji, theme }: IMessageVideo) => {
const { baseUrl, user } = useContext(MessageContext); const { baseUrl, user } = useContext(MessageContext);
const [loading, setLoading] = useState(false);
if (!baseUrl) { if (!baseUrl) {
return null; return null;
} }
const onPress = () => { const onPress = async () => {
if (isTypeSupported(file.video_type)) { if (isTypeSupported(file.video_type)) {
return showAttachment(file); 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 ( return (
@ -56,7 +82,11 @@ const Video = React.memo(
onPress={onPress} onPress={onPress}
style={[styles.button, { backgroundColor: themes[theme].videoBackground }]} style={[styles.button, { backgroundColor: themes[theme].videoBackground }]}
background={Touchable.Ripple(themes[theme].bannerBackground)}> 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> </Touchable>
{/* @ts-ignore*/} {/* @ts-ignore*/}
<Markdown <Markdown

View File

@ -59,6 +59,7 @@ export interface IUser {
export type UserMention = Pick<IUser, '_id' | 'username' | 'name'>; export type UserMention = Pick<IUser, '_id' | 'username' | 'name'>;
export interface IMessageContent { export interface IMessageContent {
_id: string;
isTemp: boolean; isTemp: boolean;
isInfo: boolean; isInfo: boolean;
tmid: string; tmid: string;

View File

@ -9,3 +9,7 @@ declare module '@rocket.chat/ui-kit';
declare module '@rocket.chat/sdk'; declare module '@rocket.chat/sdk';
declare module 'react-native-config-reader'; declare module 'react-native-config-reader';
declare module 'react-native-keycommands'; 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", "No_canned_responses": "No canned responses",
"Send_email_confirmation": "Send email confirmation", "Send_email_confirmation": "Send email confirmation",
"sending_email_confirmation": "sending 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", "Sharing": "Compartilhando",
"No_canned_responses": "Não há respostas predefinidas", "No_canned_responses": "Não há respostas predefinidas",
"Send_email_confirmation": "Enviar email de confirmação", "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 call = /^(https:\/\/)?jitsi.rocket.chat\//;
const fullURL = url;
if (url.match(call)) { if (url.match(call)) {
url = url.replace(call, '').trim(); url = url.replace(call, '').trim();
if (url) { 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}`; 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) { async function callJitsi(room, onlyAudio = false) {
logEvent(onlyAudio ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO); logEvent(onlyAudio ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO);
const url = await jitsiURL.call(this, { room }); const url = await jitsiURL.call(this, { room });

View File

@ -54,7 +54,7 @@ import loadMissedMessages from './methods/loadMissedMessages';
import loadThreadMessages from './methods/loadThreadMessages'; import loadThreadMessages from './methods/loadThreadMessages';
import sendMessage, { resendMessage } from './methods/sendMessage'; import sendMessage, { resendMessage } from './methods/sendMessage';
import { cancelUpload, isUploadActive, sendFileMessage } from './methods/sendFileMessage'; 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 logout, { removeServer } from './methods/logout';
import UserPreferences from './userPreferences'; import UserPreferences from './userPreferences';
import { Encryption } from './encryption'; import { Encryption } from './encryption';
@ -76,6 +76,7 @@ const RocketChat = {
CURRENT_SERVER, CURRENT_SERVER,
CERTIFICATE_KEY, CERTIFICATE_KEY,
callJitsi, callJitsi,
callJitsiWithoutServer,
async subscribeRooms() { async subscribeRooms() {
if (!this.roomsSub) { if (!this.roomsSub) {
try { 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'; import { types } from './types';
export const ImageComponent = (type: string) => { export const ImageComponent = (type?: string): React.ComponentType<Partial<Image> | FastImageProps> => {
let Component; let Component;
if (type === types.REACT_NATIVE_IMAGE) { if (type === types.REACT_NATIVE_IMAGE) {
const { Image } = require('react-native'); const { Image } = require('react-native');

View File

@ -16,13 +16,14 @@ const styles = StyleSheet.create({
interface IImageViewer { interface IImageViewer {
uri: string; uri: string;
imageComponentType: string; imageComponentType?: string;
width: number; width: number;
height: number; height: number;
theme: string; 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 backgroundColor = themes[theme].previewBackground;
const Component = ImageComponent(imageComponentType); const Component = ImageComponent(imageComponentType);
return ( return (

View File

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

View File

@ -1,14 +1,12 @@
import React from 'react'; 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'; import scrollPersistTaps from '../utils/scrollPersistTaps';
interface IKeyboardViewProps { interface IKeyboardViewProps extends KeyboardAwareScrollViewProps {
style: any;
contentContainerStyle: any;
keyboardVerticalOffset: number; keyboardVerticalOffset: number;
scrollEnabled: boolean; scrollEnabled?: boolean;
children: JSX.Element; children: React.ReactNode;
} }
export default class KeyboardView extends React.PureComponent<IKeyboardViewProps, any> { export default class KeyboardView extends React.PureComponent<IKeyboardViewProps, any> {
@ -22,9 +20,7 @@ export default class KeyboardView extends React.PureComponent<IKeyboardViewProps
contentContainerStyle={contentContainerStyle} contentContainerStyle={contentContainerStyle}
scrollEnabled={scrollEnabled} scrollEnabled={scrollEnabled}
alwaysBounceVertical={false} alwaysBounceVertical={false}
extraHeight={keyboardVerticalOffset} extraHeight={keyboardVerticalOffset}>
// @ts-ignore
behavior='position'>
{children} {children}
</KeyboardAwareScrollView> </KeyboardAwareScrollView>
); );

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
// @ts-ignore // @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 Avatar from '../containers/Avatar';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
@ -44,8 +44,8 @@ interface IUserItem {
username: string; username: string;
onPress(): void; onPress(): void;
testID: string; testID: string;
onLongPress(): void; onLongPress?: () => void;
style: any; style?: StyleProp<ViewStyle>;
icon: string; icon: string;
theme: string; theme: string;
} }

View File

@ -122,6 +122,11 @@ const handleOpen = function* handleOpen({ params }) {
host = id; host = id;
} }
}); });
if (!host && params.fullURL) {
RocketChat.callJitsiWithoutServer(params.fullURL);
return;
}
} }
if (params.type === 'oauth') { 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_CLEAR_LOCAL_SERVER_CACHE: 'se_clear_local_server_cache',
SE_LOG_OUT: 'se_log_out', SE_LOG_OUT: 'se_log_out',
// USER PREFERENCE VIEW
UP_GO_USER_NOTIFICATION_PREF: 'up_go_user_notification_pref',
// SECURITY PRIVACY VIEW // SECURITY PRIVACY VIEW
SP_GO_E2EENCRYPTIONSECURITY: 'sp_go_e2e_encryption_security', SP_GO_E2EENCRYPTIONSECURITY: 'sp_go_e2e_encryption_security',
SP_GO_SCREENLOCKCONFIG: 'sp_go_screen_lock_cfg', 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 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 { connect } from 'react-redux';
import * as List from '../containers/List'; import * as List from '../containers/List';
@ -9,8 +10,16 @@ import * as HeaderButton from '../containers/HeaderButton';
import SafeAreaView from '../containers/SafeAreaView'; import SafeAreaView from '../containers/SafeAreaView';
import I18n from '../i18n'; import I18n from '../i18n';
const setHeader = (navigation, isMasterDetail) => { type TNavigation = StackNavigationProp<any, 'AddChannelTeamView'>;
const options = {
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') headerTitle: I18n.t('Add_Channel_to_Team')
}; };
@ -21,7 +30,7 @@ const setHeader = (navigation, isMasterDetail) => {
navigation.setOptions(options); navigation.setOptions(options);
}; };
const AddChannelTeamView = ({ navigation, route, isMasterDetail }) => { const AddChannelTeamView = ({ navigation, route, isMasterDetail }: IAddChannelTeamView) => {
const { teamId, teamChannels } = route.params; const { teamId, teamChannels } = route.params;
const { theme } = useTheme(); const { theme } = useTheme();
@ -66,13 +75,7 @@ const AddChannelTeamView = ({ navigation, route, isMasterDetail }) => {
); );
}; };
AddChannelTeamView.propTypes = { const mapStateToProps = (state: any) => ({
route: PropTypes.object,
navigation: PropTypes.object,
isMasterDetail: PropTypes.bool
};
const mapStateToProps = state => ({
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });

View File

@ -1,5 +1,6 @@
import React from 'react'; 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 { FlatList, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
@ -21,18 +22,27 @@ import { goRoom } from '../utils/goRoom';
import { showErrorAlert } from '../utils/info'; import { showErrorAlert } from '../utils/info';
import debounce from '../utils/debounce'; 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; const QUERY_SIZE = 50;
class AddExistingChannelView extends React.Component { class AddExistingChannelView extends React.Component<IAddExistingChannelViewProps, IAddExistingChannelViewState> {
static propTypes = { private teamId: string;
navigation: PropTypes.object, constructor(props: IAddExistingChannelViewProps) {
route: PropTypes.object,
theme: PropTypes.string,
isMasterDetail: PropTypes.bool,
addTeamChannelPermission: PropTypes.array
};
constructor(props) {
super(props); super(props);
this.query(); this.query();
this.teamId = props.route?.params?.teamId; this.teamId = props.route?.params?.teamId;
@ -49,7 +59,7 @@ class AddExistingChannelView extends React.Component {
const { navigation, isMasterDetail } = this.props; const { navigation, isMasterDetail } = this.props;
const { selected } = this.state; const { selected } = this.state;
const options = { const options: StackNavigationOptions = {
headerTitle: I18n.t('Add_Existing_Channel') headerTitle: I18n.t('Add_Existing_Channel')
}; };
@ -82,9 +92,10 @@ class AddExistingChannelView extends React.Component {
) )
.fetch(); .fetch();
const asyncFilter = async channelsArray => { // TODO: Refactor with Room Model
const asyncFilter = async (channelsArray: any[]) => {
const results = await Promise.all( const results = await Promise.all(
channelsArray.map(async channel => { channelsArray.map(async (channel: any) => {
if (channel.prid) { if (channel.prid) {
return false; 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); const channelFiltered = await asyncFilter(channels);
this.setState({ channels: channelFiltered }); this.setState({ channels: channelFiltered });
@ -105,7 +116,7 @@ class AddExistingChannelView extends React.Component {
} }
}; };
onSearchChangeText = debounce(text => { onSearchChangeText = debounce((text: string) => {
this.query(text); this.query(text);
}, 300); }, 300);
@ -126,7 +137,7 @@ class AddExistingChannelView extends React.Component {
this.setState({ loading: false }); this.setState({ loading: false });
goRoom({ item: result, isMasterDetail }); goRoom({ item: result, isMasterDetail });
} }
} catch (e) { } catch (e: any) {
logEvent(events.CT_ADD_ROOM_TO_TEAM_F); logEvent(events.CT_ADD_ROOM_TO_TEAM_F);
showErrorAlert(I18n.t(e.data.error), I18n.t('Add_Existing_Channel'), () => {}); showErrorAlert(I18n.t(e.data.error), I18n.t('Add_Existing_Channel'), () => {});
this.setState({ loading: false }); this.setState({ loading: false });
@ -137,17 +148,17 @@ class AddExistingChannelView extends React.Component {
const { theme } = this.props; const { theme } = this.props;
return ( return (
<View style={{ backgroundColor: themes[theme].auxiliaryBackground }}> <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> </View>
); );
}; };
isChecked = rid => { isChecked = (rid: string) => {
const { selected } = this.state; const { selected } = this.state;
return selected.includes(rid); return selected.includes(rid);
}; };
toggleChannel = rid => { toggleChannel = (rid: string) => {
const { selected } = this.state; const { selected } = this.state;
animateNextTransition(); 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); const isChecked = this.isChecked(item.rid);
// TODO: reuse logic inside RoomTypeIcon // TODO: reuse logic inside RoomTypeIcon
const icon = item.t === 'p' && !item.teamId ? 'channel-private' : 'channel-public'; 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, isMasterDetail: state.app.isMasterDetail,
addTeamChannelPermission: state.permissions['add-team-channel'] addTeamChannelPermission: state.permissions['add-team-channel']
}); });

View File

@ -1,12 +1,13 @@
import React from 'react'; import React from 'react';
import { PermissionsAndroid, StyleSheet, View } from 'react-native'; import { PermissionsAndroid, StyleSheet, View } from 'react-native';
import { connect } from 'react-redux'; 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 CameraRoll from '@react-native-community/cameraroll';
import * as mime from 'react-native-mime-types'; import * as mime from 'react-native-mime-types';
import RNFetchBlob from 'rn-fetch-blob'; import RNFetchBlob from 'rn-fetch-blob';
import { Video } from 'expo-av'; import { Video } from 'expo-av';
import SHA256 from 'js-sha256'; import { sha256 } from 'js-sha256';
import { withSafeAreaInsets } from 'react-native-safe-area-context'; import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { LISTENER } from '../containers/Toast'; import { LISTENER } from '../containers/Toast';
@ -30,23 +31,41 @@ const styles = StyleSheet.create({
} }
}); });
class AttachmentView extends React.Component { // TODO: refactor when react-navigation is done
static propTypes = { export interface IAttachment {
navigation: PropTypes.object, title: string;
route: PropTypes.object, title_link?: string;
theme: PropTypes.string, image_url?: string;
baseUrl: PropTypes.string, image_type?: string;
width: PropTypes.number, video_url?: string;
height: PropTypes.number, video_type?: string;
insets: PropTypes.object, }
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
}),
Allow_Save_Media_to_Gallery: PropTypes.bool
};
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); super(props);
const attachment = props.route.params?.attachment; const attachment = props.route.params?.attachment;
this.state = { attachment, loading: true }; this.state = { attachment, loading: true };
@ -79,21 +98,9 @@ class AttachmentView extends React.Component {
} }
const options = { const options = {
title, title,
headerLeft: () => ( headerLeft: () => <HeaderButton.CloseModal testID='close-attachment-view' navigation={navigation} />,
<HeaderButton.CloseModal
testID='close-attachment-view'
navigation={navigation}
buttonStyle={{ color: themes[theme].previewTintColor }}
/>
),
headerRight: () => headerRight: () =>
Allow_Save_Media_to_Gallery ? ( Allow_Save_Media_to_Gallery ? <HeaderButton.Download testID='save-image' onPress={this.handleSave} /> : null,
<HeaderButton.Download
testID='save-image'
onPress={this.handleSave}
buttonStyle={{ color: themes[theme].previewTintColor }}
/>
) : null,
headerBackground: () => <View style={{ flex: 1, backgroundColor: themes[theme].previewBackground }} />, headerBackground: () => <View style={{ flex: 1, backgroundColor: themes[theme].previewBackground }} />,
headerTintColor: themes[theme].previewTintColor, headerTintColor: themes[theme].previewTintColor,
headerTitleStyle: { color: themes[theme].previewTintColor, marginHorizontal: 10 } headerTitleStyle: { color: themes[theme].previewTintColor, marginHorizontal: 10 }
@ -101,7 +108,7 @@ class AttachmentView extends React.Component {
navigation.setOptions(options); navigation.setOptions(options);
}; };
getVideoRef = ref => (this.videoRef = ref); getVideoRef = (ref: Video) => (this.videoRef = ref);
handleSave = async () => { handleSave = async () => {
const { attachment } = this.state; const { attachment } = this.state;
@ -113,7 +120,8 @@ class AttachmentView extends React.Component {
if (isAndroid) { if (isAndroid) {
const rationale = { const rationale = {
title: I18n.t('Write_External_Permission'), 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); const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, rationale);
if (!(result || result === PermissionsAndroid.RESULTS.GRANTED)) { if (!(result || result === PermissionsAndroid.RESULTS.GRANTED)) {
@ -125,7 +133,7 @@ class AttachmentView extends React.Component {
try { try {
const extension = image_url ? `.${mime.extension(image_type) || 'jpg'}` : `.${mime.extension(video_type) || 'mp4'}`; const extension = image_url ? `.${mime.extension(image_type) || 'jpg'}` : `.${mime.extension(video_type) || 'mp4'}`;
const documentDir = `${RNFetchBlob.fs.dirs.DocumentDir}/`; 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); const file = await RNFetchBlob.config({ path }).fetch('GET', mediaAttachment);
await CameraRoll.save(path, { album: 'Rocket.Chat' }); await CameraRoll.save(path, { album: 'Rocket.Chat' });
await file.flush(); await file.flush();
@ -136,7 +144,7 @@ class AttachmentView extends React.Component {
this.setState({ loading: false }); this.setState({ loading: false });
}; };
renderImage = uri => { renderImage = (uri: string) => {
const { theme, width, height, insets } = this.props; const { theme, width, height, insets } = this.props;
const headerHeight = getHeaderHeight(width > height); const headerHeight = getHeaderHeight(width > height);
return ( return (
@ -150,7 +158,7 @@ class AttachmentView extends React.Component {
); );
}; };
renderVideo = uri => ( renderVideo = (uri: string) => (
<Video <Video
source={{ uri }} source={{ uri }}
rate={1.0} rate={1.0}
@ -190,7 +198,7 @@ class AttachmentView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
baseUrl: state.server.server, baseUrl: state.server.server,
user: getUserSelector(state), user: getUserSelector(state),
Allow_Save_Media_to_Gallery: state.settings.Allow_Save_Media_to_Gallery ?? true 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 React, { useEffect, useState } from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import Orientation from 'react-native-orientation-locker'; import Orientation from 'react-native-orientation-locker';
import useDeepCompareEffect from 'use-deep-compare-effect'; import useDeepCompareEffect from 'use-deep-compare-effect';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { withTheme } from '../theme'; import { useTheme } from '../theme';
import { hasNotch, isTablet } from '../utils/deviceInfo'; import { hasNotch, isTablet } from '../utils/deviceInfo';
import { TYPE } from '../containers/Passcode/constants';
import { PasscodeChoose } from '../containers/Passcode'; import { PasscodeChoose } from '../containers/Passcode';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
import { CustomIcon } from '../lib/Icons'; 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 [visible, setVisible] = useState(false);
const [data, setData] = useState({}); const [data, setData] = useState<Partial<IArgs>>({});
const { theme } = useTheme();
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
if (!isEmpty(data)) { if (!isEmpty(data)) {
@ -39,11 +45,11 @@ const ChangePasscodeView = React.memo(({ theme }) => {
} }
}, [data]); }, [data]);
const showChangePasscode = args => { const showChangePasscode = (args: IArgs) => {
setData(args); setData(args);
}; };
const onSubmit = passcode => { const onSubmit = (passcode: string) => {
const { submit } = data; const { submit } = data;
if (submit) { if (submit) {
submit(passcode); submit(passcode);
@ -74,7 +80,7 @@ const ChangePasscodeView = React.memo(({ theme }) => {
return ( return (
<Modal useNativeDriver isVisible={visible} hideModalContentWhileAnimating style={styles.modal}> <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 ? ( {!data?.force ? (
<Touchable onPress={onCancel} style={styles.close}> <Touchable onPress={onCancel} style={styles.close}>
<CustomIcon name='close' color={themes[theme].passcodePrimary} size={30} /> <CustomIcon name='close' color={themes[theme].passcodePrimary} size={30} />
@ -84,8 +90,4 @@ const ChangePasscodeView = React.memo(({ theme }) => {
); );
}); });
ChangePasscodeView.propTypes = { export default ChangePasscodeView;
theme: PropTypes.string
};
export default withTheme(ChangePasscodeView);

View File

@ -1,7 +1,9 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import { Dispatch } from 'redux';
import { FlatList, ScrollView, StyleSheet, Switch, Text, View } from 'react-native'; 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 { dequal } from 'dequal';
import * as List from '../containers/List'; import * as List from '../containers/List';
@ -66,30 +68,59 @@ const styles = StyleSheet.create({
} }
}); });
class CreateChannelView extends React.Component { interface IOtherUser {
static propTypes = { _id: string;
navigation: PropTypes.object, name: string;
route: PropTypes.object, fname: string;
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
};
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); super(props);
const { route } = this.props; const { route } = this.props;
const isTeam = route?.params?.isTeam || false; const isTeam = route?.params?.isTeam || false;
@ -110,7 +141,7 @@ class CreateChannelView extends React.Component {
this.handleHasPermission(); this.handleHasPermission();
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: ICreateChannelViewProps, nextState: ICreateChannelViewState) {
const { channelName, type, readOnly, broadcast, encrypted, permissions } = this.state; const { channelName, type, readOnly, broadcast, encrypted, permissions } = this.state;
const { users, isFetching, encryptionEnabled, theme, createPublicChannelPermission, createPrivateChannelPermission } = const { users, isFetching, encryptionEnabled, theme, createPublicChannelPermission, createPrivateChannelPermission } =
this.props; this.props;
@ -153,7 +184,7 @@ class CreateChannelView extends React.Component {
return false; return false;
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: ICreateChannelViewProps) {
const { createPublicChannelPermission, createPrivateChannelPermission } = this.props; const { createPublicChannelPermission, createPrivateChannelPermission } = this.props;
if ( if (
!dequal(createPublicChannelPermission, prevProps.createPublicChannelPermission) || !dequal(createPublicChannelPermission, prevProps.createPublicChannelPermission) ||
@ -172,7 +203,7 @@ class CreateChannelView extends React.Component {
}); });
}; };
toggleRightButton = channelName => { toggleRightButton = (channelName: string) => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.setOptions({ navigation.setOptions({
headerRight: () => headerRight: () =>
@ -184,7 +215,7 @@ class CreateChannelView extends React.Component {
}); });
}; };
onChangeText = channelName => { onChangeText = (channelName: string) => {
this.toggleRightButton(channelName); this.toggleRightButton(channelName);
this.setState({ channelName }); this.setState({ channelName });
}; };
@ -215,13 +246,13 @@ class CreateChannelView extends React.Component {
Review.pushPositiveEvent(); Review.pushPositiveEvent();
}; };
removeUser = user => { removeUser = (user: IOtherUser) => {
logEvent(events.CR_REMOVE_USER); logEvent(events.CR_REMOVE_USER);
const { removeUser } = this.props; const { removeUser } = this.props;
removeUser(user); removeUser(user);
}; };
renderSwitch = ({ id, value, label, onValueChange, disabled = false }) => { renderSwitch = ({ id, value, label, onValueChange, disabled = false }: ISwitch) => {
const { theme } = this.props; const { theme } = this.props;
return ( return (
<View style={[styles.switchContainer, { backgroundColor: themes[theme].backgroundColor }]}> <View style={[styles.switchContainer, { backgroundColor: themes[theme].backgroundColor }]}>
@ -253,7 +284,7 @@ class CreateChannelView extends React.Component {
value: permissions[1] ? type : false, value: permissions[1] ? type : false,
disabled: isDisabled, disabled: isDisabled,
label: isTeam ? 'Private_Team' : 'Private_Channel', label: isTeam ? 'Private_Team' : 'Private_Channel',
onValueChange: value => { onValueChange: (value: boolean) => {
logEvent(events.CR_TOGGLE_TYPE); logEvent(events.CR_TOGGLE_TYPE);
// If we set the channel as public, encrypted status should be false // If we set the channel as public, encrypted status should be false
this.setState(({ encrypted }) => ({ type: value, encrypted: value && encrypted })); this.setState(({ encrypted }) => ({ type: value, encrypted: value && encrypted }));
@ -313,8 +344,8 @@ class CreateChannelView extends React.Component {
}); });
} }
renderItem = ({ item }) => { renderItem = ({ item }: { item: IOtherUser }) => {
const { baseUrl, user, theme } = this.props; const { theme } = this.props;
return ( return (
<UserItem <UserItem
@ -323,8 +354,6 @@ class CreateChannelView extends React.Component {
onPress={() => this.removeUser(item)} onPress={() => this.removeUser(item)}
testID={`create-channel-view-item-${item.name}`} testID={`create-channel-view-item-${item.name}`}
icon='check' icon='check'
baseUrl={baseUrl}
user={user}
theme={theme} theme={theme}
/> />
); );
@ -348,7 +377,6 @@ class CreateChannelView extends React.Component {
]} ]}
renderItem={this.renderItem} renderItem={this.renderItem}
ItemSeparatorComponent={List.Separator} ItemSeparatorComponent={List.Separator}
enableEmptySections
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
/> />
); );
@ -371,7 +399,6 @@ class CreateChannelView extends React.Component {
<TextInput <TextInput
autoFocus autoFocus
style={[styles.input, { backgroundColor: themes[theme].backgroundColor }]} style={[styles.input, { backgroundColor: themes[theme].backgroundColor }]}
label={isTeam ? I18n.t('Team_Name') : I18n.t('Channel_Name')}
value={channelName} value={channelName}
onChangeText={this.onChangeText} onChangeText={this.onChangeText}
placeholder={isTeam ? I18n.t('Team_Name') : I18n.t('Channel_Name')} 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, baseUrl: state.server.server,
isFetching: state.createChannel.isFetching, isFetching: state.createChannel.isFetching,
encryptionEnabled: state.encryption.enabled, encryptionEnabled: state.encryption.enabled,
@ -416,9 +443,9 @@ const mapStateToProps = state => ({
createPrivateChannelPermission: state.permissions['create-p'] createPrivateChannelPermission: state.permissions['create-p']
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
create: data => dispatch(createChannelRequestAction(data)), create: (data: ICreateFunction) => dispatch(createChannelRequestAction(data)),
removeUser: user => dispatch(removeUserAction(user)) removeUser: (user: IOtherUser) => dispatch(removeUserAction(user))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(CreateChannelView)); 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 { name, users, encrypted } = this.state;
const { server, user, loading, blockUnauthenticatedAccess, theme, serverVersion } = this.props; const { server, user, loading, blockUnauthenticatedAccess, theme, serverVersion } = this.props;
return ( return (
// @ts-ignore
<KeyboardView <KeyboardView
style={{ backgroundColor: themes[theme].auxiliaryBackground }} style={{ backgroundColor: themes[theme].auxiliaryBackground }}
contentContainerStyle={styles.container} contentContainerStyle={styles.container}
keyboardVerticalOffset={128}> keyboardVerticalOffset={128}>
<StatusBar /> <StatusBar />
<SafeAreaView testID='create-discussion-view' style={styles.container}> <SafeAreaView testID='create-discussion-view' style={styles.container}>
{/* @ts-ignore*/}
<ScrollView {...scrollPersistTaps}> <ScrollView {...scrollPersistTaps}>
<Text style={[styles.description, { color: themes[theme].auxiliaryText }]}>{I18n.t('Discussion_Desc')}</Text> <Text style={[styles.description, { color: themes[theme].auxiliaryText }]}>{I18n.t('Discussion_Desc')}</Text>
<SelectChannel <SelectChannel

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { StackNavigationOptions } from '@react-navigation/stack';
import { FlatList, Linking } from 'react-native'; import { FlatList, Linking } from 'react-native';
import I18n from '../i18n'; import I18n from '../i18n';
@ -13,7 +13,14 @@ import SafeAreaView from '../containers/SafeAreaView';
import UserPreferences from '../lib/userPreferences'; import UserPreferences from '../lib/userPreferences';
import { events, logEvent } from '../utils/log'; 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', title: 'In_app',
value: 'inApp' value: 'inApp'
@ -24,7 +31,7 @@ const DEFAULT_BROWSERS = [
} }
]; ];
const BROWSERS = [ const BROWSERS: IBrowsersValues[] = [
{ {
title: 'Chrome', title: 'Chrome',
value: 'googlechrome:' value: 'googlechrome:'
@ -39,16 +46,23 @@ const BROWSERS = [
} }
]; ];
class DefaultBrowserView extends React.Component { interface IDefaultBrowserViewState {
static navigationOptions = () => ({ 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') title: I18n.t('Default_browser')
}); });
static propTypes = { constructor(props: IDefaultBrowserViewProps) {
theme: PropTypes.string
};
constructor(props) {
super(props); super(props);
this.state = { this.state = {
browser: null, browser: null,
@ -74,6 +88,7 @@ class DefaultBrowserView extends React.Component {
this.setState(({ supported }) => ({ supported: [...supported, browser] })); this.setState(({ supported }) => ({ supported: [...supported, browser] }));
} else { } else {
const { supported } = this.state; const { supported } = this.state;
// @ts-ignore
this.state.supported = [...supported, browser]; this.state.supported = [...supported, browser];
} }
} }
@ -81,7 +96,7 @@ class DefaultBrowserView extends React.Component {
}); });
}; };
isSelected = value => { isSelected = (value: TValue) => {
const { browser } = this.state; const { browser } = this.state;
if (!browser && value === 'systemDefault:') { if (!browser && value === 'systemDefault:') {
return true; return true;
@ -89,7 +104,7 @@ class DefaultBrowserView extends React.Component {
return browser === value; return browser === value;
}; };
changeDefaultBrowser = async newBrowser => { changeDefaultBrowser = async (newBrowser: TValue) => {
logEvent(events.DB_CHANGE_DEFAULT_BROWSER, { browser: newBrowser }); logEvent(events.DB_CHANGE_DEFAULT_BROWSER, { browser: newBrowser });
try { try {
const browser = newBrowser !== 'systemDefault:' ? newBrowser : null; const browser = newBrowser !== 'systemDefault:' ? newBrowser : null;
@ -105,7 +120,7 @@ class DefaultBrowserView extends React.Component {
return <List.Icon name='check' color={themes[theme].tintColor} />; return <List.Icon name='check' color={themes[theme].tintColor} />;
}; };
renderItem = ({ item }) => { renderItem = ({ item }: { item: IBrowsersValues }) => {
const { title, value } = item; const { title, value } = item;
return ( return (
<List.Item <List.Item

View File

@ -21,19 +21,19 @@ const DisplayPrefsView = props => {
const { theme } = useTheme(); const { theme } = useTheme();
const { sortBy, groupByType, showFavorites, showUnread, showAvatar, displayMode } = useSelector(state => state.sortPreferences); const { sortBy, groupByType, showFavorites, showUnread, showAvatar, displayMode } = useSelector(state => state.sortPreferences);
const { isMasterDetail } = useSelector(state => state.app);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
const { navigation, isMasterDetail } = props; const { navigation } = props;
navigation.setOptions({ navigation.setOptions({
title: I18n.t('Display'), title: I18n.t('Display')
headerLeft: () =>
isMasterDetail ? (
<HeaderButton.CloseModal navigation={navigation} testID='display-view-close' />
) : (
<HeaderButton.Drawer navigation={navigation} testID='display-view-drawer' />
)
}); });
if (!isMasterDetail) {
navigation.setOptions({
headerLeft: () => <HeaderButton.Drawer navigation={navigation} testID='display-view-drawer' />
});
}
}, []); }, []);
const setSortPreference = async param => { const setSortPreference = async param => {

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View, TextInput as TextInputComp } from 'react-native';
import PropTypes from 'prop-types'; import { StackNavigationOptions } from '@react-navigation/stack';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import StatusBar from '../containers/StatusBar'; import StatusBar from '../containers/StatusBar';
import * as List from '../containers/List'; 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: '' }; state = { newPassword: '' };
newPasswordInputRef = React.createRef(); onChangePasswordText = debounce((text: string) => this.setState({ newPassword: text }), 300);
onChangePasswordText = debounce(text => this.setState({ newPassword: text }), 300); setNewPasswordRef = (ref: TextInputComp) => (this.newPasswordInputRef = ref);
setNewPasswordRef = ref => (this.newPasswordInputRef = ref);
changePassword = () => { changePassword = () => {
const { newPassword } = this.state; const { newPassword } = this.state;
if (!newPassword.trim()) { if (!newPassword.trim()) {
return; return;
} }
// TODO: Remove ts-ignore when migrate the showConfirmationAlert
// @ts-ignore
showConfirmationAlert({ showConfirmationAlert({
title: I18n.t('Are_you_sure_question_mark'), title: I18n.t('Are_you_sure_question_mark'),
message: I18n.t('E2E_encryption_change_password_message'), message: I18n.t('E2E_encryption_change_password_message'),
@ -76,6 +98,8 @@ class E2EEncryptionSecurityView extends React.Component {
}; };
resetOwnKey = () => { resetOwnKey = () => {
// TODO: Remove ts-ignore when migrate the showConfirmationAlert
// @ts-ignore
showConfirmationAlert({ showConfirmationAlert({
title: I18n.t('Are_you_sure_question_mark'), title: I18n.t('Are_you_sure_question_mark'),
message: I18n.t('E2E_encryption_reset_message'), 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, server: state.server.server,
user: getUserSelector(state), user: getUserSelector(state),
encryptionEnabled: state.encryption.enabled encryptionEnabled: state.encryption.enabled
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
logout: () => dispatch(logoutAction(true)) 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)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(E2EEncryptionSecurityView));

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { ScrollView, StyleSheet, Text } from 'react-native'; import { ScrollView, StyleSheet, Text } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import I18n from '../i18n'; import I18n from '../i18n';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
@ -27,18 +28,26 @@ const styles = StyleSheet.create({
...sharedStyles.textRegular ...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' />, headerLeft: () => <HeaderButton.CloseModal navigation={navigation} testID='e2e-enter-your-password-view-close' />,
title: I18n.t('Enter_Your_E2E_Password') title: I18n.t('Enter_Your_E2E_Password')
}); });
static propTypes = { constructor(props: IE2EEnterYourPasswordViewProps) {
encryptionDecodeKey: PropTypes.func,
theme: PropTypes.string
};
constructor(props) {
super(props); super(props);
this.state = { this.state = {
password: '' password: ''
@ -65,12 +74,12 @@ class E2EEnterYourPasswordView extends React.Component {
<ScrollView <ScrollView
{...scrollPersistTaps} {...scrollPersistTaps}
style={sharedStyles.container} style={sharedStyles.container}
contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}> contentContainerStyle={sharedStyles.containerScrollView}>
<SafeAreaView <SafeAreaView
style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}
testID='e2e-enter-your-password-view'> testID='e2e-enter-your-password-view'>
<TextInput <TextInput
inputRef={e => { inputRef={(e: TextInput) => {
this.passwordInput = e; this.passwordInput = e;
}} }}
placeholder={I18n.t('Password')} placeholder={I18n.t('Password')}
@ -99,7 +108,7 @@ class E2EEnterYourPasswordView extends React.Component {
} }
} }
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
encryptionDecodeKey: password => dispatch(encryptionDecodeKeyAction(password)) encryptionDecodeKey: (password: string) => dispatch(encryptionDecodeKeyAction(password))
}); });
export default connect(null, mapDispatchToProps)(withTheme(E2EEnterYourPasswordView)); export default connect(null, mapDispatchToProps)(withTheme(E2EEnterYourPasswordView));

View File

@ -1,5 +1,6 @@
import React from 'react'; 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 { StyleSheet } from 'react-native';
import SafeAreaView from '../containers/SafeAreaView'; import SafeAreaView from '../containers/SafeAreaView';
@ -21,8 +22,17 @@ const styles = StyleSheet.create({
} }
}); });
class E2EHowItWorksView extends React.Component { interface INavigation {
static navigationOptions = ({ route, navigation }) => { 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; const showCloseModal = route.params?.showCloseModal;
return { return {
title: I18n.t('How_It_Works'), title: I18n.t('How_It_Works'),
@ -30,20 +40,21 @@ class E2EHowItWorksView extends React.Component {
}; };
}; };
static propTypes = {
theme: PropTypes.string
};
render() { render() {
const { theme } = this.props; const { theme } = this.props;
const infoStyle = [styles.info, { color: themes[theme].bodyText }]; const infoStyle = [styles.info, { color: themes[theme].bodyText }];
// TODO: Refactor when migrate Markdown
return ( return (
<SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} testID='e2e-how-it-works-view'> <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} /> <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} /> <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} /> <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} /> <Markdown msg={I18n.t('E2E_How_It_Works_info4')} style={infoStyle} theme={theme} />
</SafeAreaView> </SafeAreaView>
); );

View File

@ -1,5 +1,6 @@
import React from 'react'; 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 { connect } from 'react-redux';
import { Clipboard, ScrollView, StyleSheet, Text, View } from 'react-native'; import { Clipboard, ScrollView, StyleSheet, Text, View } from 'react-native';
@ -53,20 +54,26 @@ const styles = StyleSheet.create({
} }
}); });
class E2ESaveYourPasswordView extends React.Component { interface IE2ESaveYourPasswordViewState {
static navigationOptions = ({ navigation }) => ({ 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' />, headerLeft: () => <HeaderButton.CloseModal navigation={navigation} testID='e2e-save-your-password-view-close' />,
title: I18n.t('Save_Your_E2E_Password') title: I18n.t('Save_Your_E2E_Password')
}); });
static propTypes = { constructor(props: IE2ESaveYourPasswordViewProps) {
server: PropTypes.string,
navigation: PropTypes.object,
encryptionSetBanner: PropTypes.func,
theme: PropTypes.string
};
constructor(props) {
super(props); super(props);
this.mounted = false; this.mounted = false;
this.state = { password: '' }; this.state = { password: '' };
@ -83,8 +90,9 @@ class E2ESaveYourPasswordView extends React.Component {
// Set stored password on local state // Set stored password on local state
const password = await UserPreferences.getStringAsync(`${server}-${E2E_RANDOM_PASSWORD_KEY}`); const password = await UserPreferences.getStringAsync(`${server}-${E2E_RANDOM_PASSWORD_KEY}`);
if (this.mounted) { if (this.mounted) {
this.setState({ password }); this.setState({ password: password! });
} else { } else {
// @ts-ignore
this.state.password = password; this.state.password = password;
} }
} catch { } catch {
@ -164,10 +172,10 @@ class E2ESaveYourPasswordView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
server: state.server.server server: state.server.server
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
encryptionSetBanner: () => dispatch(encryptionSetBannerAction()) encryptionSetBanner: () => dispatch(encryptionSetBannerAction())
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(E2ESaveYourPasswordView)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(E2ESaveYourPasswordView));

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Text } from 'react-native'; 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 TextInput from '../containers/TextInput';
import Button from '../containers/Button'; import Button from '../containers/Button';
@ -14,23 +15,30 @@ import FormContainer, { FormContainerInner } from '../containers/FormContainer';
import { events, logEvent } from '../utils/log'; import { events, logEvent } from '../utils/log';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
class ForgotPasswordView extends React.Component { interface IForgotPasswordViewState {
static navigationOptions = ({ route }) => ({ 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' title: route.params?.title ?? 'Rocket.Chat'
}); });
static propTypes = {
navigation: PropTypes.object,
theme: PropTypes.string
};
state = { state = {
email: '', email: '',
invalidEmail: true, invalidEmail: true,
isFetching: false isFetching: false
}; };
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: IForgotPasswordViewProps, nextState: IForgotPasswordViewState) {
const { email, invalidEmail, isFetching } = this.state; const { email, invalidEmail, isFetching } = this.state;
const { theme } = this.props; const { theme } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
@ -48,7 +56,7 @@ class ForgotPasswordView extends React.Component {
return false; return false;
} }
validate = email => { validate = (email: string) => {
if (!isValidEmail(email)) { if (!isValidEmail(email)) {
this.setState({ invalidEmail: true }); this.setState({ invalidEmail: true });
return; return;
@ -70,7 +78,7 @@ class ForgotPasswordView extends React.Component {
navigation.pop(); navigation.pop();
showErrorAlert(I18n.t('Forgot_password_If_this_email_is_registered'), I18n.t('Alert')); showErrorAlert(I18n.t('Forgot_password_If_this_email_is_registered'), I18n.t('Alert'));
} }
} catch (e) { } catch (e: any) {
logEvent(events.FP_FORGOT_PASSWORD_F); 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') }); 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')); showErrorAlert(msg, I18n.t('Alert'));

View File

@ -1,7 +1,10 @@
import React, { useEffect, useState } from 'react'; 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 { StyleSheet, View } from 'react-native';
import { Dispatch } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import isEmpty from 'lodash/isEmpty';
import I18n from '../i18n'; import I18n from '../i18n';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
@ -10,6 +13,7 @@ import RocketChat from '../lib/rocketchat';
import OrSeparator from '../containers/OrSeparator'; import OrSeparator from '../containers/OrSeparator';
import Input from '../containers/UIKit/MultiSelect/Input'; import Input from '../containers/UIKit/MultiSelect/Input';
import { forwardRoom as forwardRoomAction } from '../actions/room'; import { forwardRoom as forwardRoomAction } from '../actions/room';
import { ILivechatDepartment } from './definition/ILivechatDepartment';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -18,12 +22,43 @@ const styles = StyleSheet.create({
} }
}); });
const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }) => { // TODO: Refactor when migrate room
const [departments, setDepartments] = useState([]); interface IRoom {
const [departmentId, setDepartment] = useState(); departmentId?: any;
const [users, setUsers] = useState([]); 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 [userId, setUser] = useState();
const [room, setRoom] = useState(); const [room, setRoom] = useState<IRoom>({});
const rid = route.params?.rid; const rid = route.params?.rid;
@ -31,7 +66,9 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }) => {
try { try {
const result = await RocketChat.getDepartments(); const result = await RocketChat.getDepartments();
if (result.success) { 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 { } catch {
// do nothing // do nothing
@ -47,7 +84,7 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }) => {
term term
}); });
if (result.success) { 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); setUsers(parsedUsers);
return parsedUsers; return parsedUsers;
} }
@ -69,7 +106,7 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }) => {
}; };
const submit = () => { const submit = () => {
const transferData = { roomId: rid }; const transferData: ITransferData = { roomId: rid };
if (!departmentId && !userId) { if (!departmentId && !userId) {
return; return;
@ -85,11 +122,14 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }) => {
}; };
useEffect(() => { useEffect(() => {
navigation.setOptions({
title: I18n.t('Forward_Chat')
});
getRoom(); getRoom();
}, []); }, []);
useEffect(() => { useEffect(() => {
if (room) { if (!isEmpty(room)) {
getUsers(); getUsers();
getDepartments(); getDepartments();
} }
@ -129,18 +169,9 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }) => {
</View> </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 => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
forwardRoom: (rid, transferData) => dispatch(forwardRoomAction(rid, transferData)) forwardRoom: (rid: string, transferData: ITransferData) => dispatch(forwardRoomAction(rid, transferData))
}); });
export default connect(null, mapDispatchToProps)(withTheme(ForwardLivechatView)); export default connect(null, mapDispatchToProps)(withTheme(ForwardLivechatView));

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { TextInputProps, View } from 'react-native';
import { View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import RNPickerSelect from 'react-native-picker-select'; 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 { import {
inviteLinksCreate as inviteLinksCreateAction, inviteLinksCreate as inviteLinksCreateAction,
@ -65,25 +67,29 @@ const OPTIONS = {
] ]
}; };
class InviteUsersView extends React.Component { interface IInviteUsersEditView {
static navigationOptions = () => ({ 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') title: I18n.t('Invite_users')
}); });
static propTypes = { private rid: string;
navigation: PropTypes.object,
route: PropTypes.object,
theme: PropTypes.string,
createInviteLink: PropTypes.func,
inviteLinksSetParams: PropTypes.func
};
constructor(props) { constructor(props: IInviteUsersEditView) {
super(props); super(props);
this.rid = props.route.params?.rid; this.rid = props.route.params?.rid;
} }
onValueChangePicker = (key, value) => { onValueChangePicker = (key: string, value: number) => {
logEvent(events.IU_EDIT_SET_LINK_PARAM); logEvent(events.IU_EDIT_SET_LINK_PARAM);
const { inviteLinksSetParams } = this.props; const { inviteLinksSetParams } = this.props;
const params = { const params = {
@ -99,9 +105,10 @@ class InviteUsersView extends React.Component {
navigation.pop(); navigation.pop();
}; };
renderPicker = (key, first) => { renderPicker = (key: 'days' | 'maxUses', first: string) => {
const { props } = this; const { props } = this;
const { theme } = props; const { theme } = props;
const textInputStyle: TextInputProps = { style: { ...styles.pickerText, color: themes[theme].actionTintColor } };
const firstEl = [ const firstEl = [
{ {
label: I18n.t(first), label: I18n.t(first),
@ -112,7 +119,7 @@ class InviteUsersView extends React.Component {
<RNPickerSelect <RNPickerSelect
style={{ viewContainer: styles.viewContainer }} style={{ viewContainer: styles.viewContainer }}
value={props[key]} value={props[key]}
textInputProps={{ style: { ...styles.pickerText, color: themes[theme].actionTintColor } }} textInputProps={textInputStyle}
useNativeAndroidPickerStyle={false} useNativeAndroidPickerStyle={false}
placeholder={{}} placeholder={{}}
onValueChange={value => this.onValueChangePicker(key, value)} 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, days: state.inviteLinks.days,
maxUses: state.inviteLinks.maxUses maxUses: state.inviteLinks.maxUses
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
inviteLinksSetParams: params => dispatch(inviteLinksSetParamsAction(params)), inviteLinksSetParams: (params: object) => dispatch(inviteLinksSetParamsAction(params)),
createInviteLink: rid => dispatch(inviteLinksCreateAction(rid)) createInviteLink: (rid: string) => dispatch(inviteLinksCreateAction(rid))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersView)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersView));

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { StyleSheet } from 'react-native'; 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 JitsiMeet, { JitsiMeetView as RNJitsiMeetView } from 'react-native-jitsi-meet';
import BackgroundTimer from 'react-native-background-timer'; import BackgroundTimer from 'react-native-background-timer';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -12,23 +13,36 @@ import { events, logEvent } from '../utils/log';
import { isAndroid, isIOS } from '../utils/deviceInfo'; import { isAndroid, isIOS } from '../utils/deviceInfo';
import { withTheme } from '../theme'; 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}`; `${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); super(props);
this.rid = props.route.params?.rid; this.rid = props.route.params?.rid;
this.url = props.route.params?.url; 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. // call is not ended and is available to web users.
onConferenceJoined = () => { onConferenceJoined = () => {
logEvent(events.JM_CONFERENCE_JOIN); logEvent(events.JM_CONFERENCE_JOIN);
RocketChat.updateJitsiTimeout(this.rid).catch(e => console.log(e)); if (this.rid) {
if (this.jitsiTimeout) { RocketChat.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e));
BackgroundTimer.clearInterval(this.jitsiTimeout); if (this.jitsiTimeout) {
BackgroundTimer.stopBackgroundTimer(); BackgroundTimer.clearInterval(this.jitsiTimeout);
this.jitsiTimeout = null; 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 = () => { onConferenceTerminated = () => {
@ -118,7 +134,7 @@ class JitsiMeetView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
user: getUserSelector(state), user: getUserSelector(state),
baseUrl: state.server.server baseUrl: state.server.server
}); });

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { FlatList } from 'react-native'; import { FlatList } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import RNRestart from 'react-native-restart'; import RNRestart from 'react-native-restart';
import { Dispatch } from 'redux';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import I18n, { LANGUAGES, isRTL } from '../../i18n'; import I18n, { LANGUAGES, isRTL } from '../../i18n';
@ -18,26 +18,33 @@ import { getUserSelector } from '../../selectors/login';
import database from '../../lib/database'; import database from '../../lib/database';
import SafeAreaView from '../../containers/SafeAreaView'; 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 = () => ({ static navigationOptions = () => ({
title: I18n.t('Change_Language') title: I18n.t('Change_Language')
}); });
static propTypes = { constructor(props: ILanguageViewProps) {
user: PropTypes.object,
setUser: PropTypes.func,
appStart: PropTypes.func,
theme: PropTypes.string
};
constructor(props) {
super(props); super(props);
this.state = { this.state = {
language: props.user ? props.user.language : 'en' language: props.user ? props.user.language : 'en'
}; };
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: ILanguageViewProps, nextState: ILanguageViewState) {
const { language } = this.state; const { language } = this.state;
const { user, theme } = this.props; const { user, theme } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
@ -52,12 +59,12 @@ class LanguageView extends React.Component {
return false; return false;
} }
formIsChanged = language => { formIsChanged = (language: string) => {
const { user } = this.props; const { user } = this.props;
return user.language !== language; return user.language !== language;
}; };
submit = async language => { submit = async (language: string) => {
if (!this.formIsChanged(language)) { if (!this.formIsChanged(language)) {
return; return;
} }
@ -78,11 +85,11 @@ class LanguageView extends React.Component {
} }
}; };
changeLanguage = async language => { changeLanguage = async (language: string) => {
logEvent(events.LANG_SET_LANGUAGE); logEvent(events.LANG_SET_LANGUAGE);
const { user, setUser } = this.props; const { user, setUser } = this.props;
const params = {}; const params: { language?: string } = {};
// language // language
if (user.language !== language) { if (user.language !== language) {
@ -95,10 +102,10 @@ class LanguageView extends React.Component {
const serversDB = database.servers; const serversDB = database.servers;
const usersCollection = serversDB.get('users'); const usersCollection = serversDB.get('users');
await serversDB.action(async () => { await serversDB.write(async () => {
try { try {
const userRecord = await usersCollection.find(user.id); const userRecord = await usersCollection.find(user.id);
await userRecord.update(record => { await userRecord.update((record: any) => {
record.language = params.language; record.language = params.language;
}); });
} catch (e) { } catch (e) {
@ -117,7 +124,7 @@ class LanguageView extends React.Component {
return <List.Icon name='check' color={themes[theme].tintColor} />; return <List.Icon name='check' color={themes[theme].tintColor} />;
}; };
renderItem = ({ item }) => { renderItem = ({ item }: { item: { value: string; label: string } }) => {
const { value, label } = item; const { value, label } = item;
const { language } = this.state; const { language } = this.state;
const isSelected = language === value; const isSelected = language === value;
@ -151,13 +158,13 @@ class LanguageView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
user: getUserSelector(state) user: getUserSelector(state)
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
setUser: params => dispatch(setUserAction(params)), setUser: (params: any) => dispatch(setUserAction(params)),
appStart: params => dispatch(appStartAction(params)) appStart: (params: any) => dispatch(appStartAction(params))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LanguageView)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LanguageView));

View File

@ -1,22 +1,26 @@
import React from 'react'; import React from 'react';
import { ScrollView } from 'react-native'; 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 I18n from '../i18n';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
class MarkdownTableView extends React.Component { interface IMarkdownTableViewProps {
static navigationOptions = () => ({ 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') title: I18n.t('Table')
}); });
static propTypes = {
route: PropTypes.object,
theme: PropTypes.string
};
render() { render() {
const { route, theme } = this.props; const { route, theme } = this.props;
const renderRows = route.params?.renderRows; const renderRows = route.params?.renderRows;

View File

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { FlatList, Text, View } from 'react-native'; import { FlatList, Text, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import { StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/core';
import Message from '../../containers/message'; import Message from '../../containers/message';
import ActivityIndicator from '../../containers/ActivityIndicator'; import ActivityIndicator from '../../containers/ActivityIndicator';
@ -18,20 +19,67 @@ import SafeAreaView from '../../containers/SafeAreaView';
import getThreadName from '../../lib/methods/getThreadName'; import getThreadName from '../../lib/methods/getThreadName';
import styles from './styles'; import styles from './styles';
class MessagesView extends React.Component { type TMessagesViewRouteParams = {
static propTypes = { MessagesView: {
user: PropTypes.object, rid: string;
baseUrl: PropTypes.string, t: string;
navigation: PropTypes.object, name: string;
route: PropTypes.object,
customEmojis: PropTypes.object,
theme: PropTypes.string,
showActionSheet: PropTypes.func,
useRealName: PropTypes.bool,
isMasterDetail: PropTypes.bool
}; };
};
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); super(props);
this.state = { this.state = {
loading: false, loading: false,
@ -48,7 +96,7 @@ class MessagesView extends React.Component {
this.load(); this.load();
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: IMessagesViewProps, nextState: any) {
const { loading, messages, fileLoading } = this.state; const { loading, messages, fileLoading } = this.state;
const { theme } = this.props; const { theme } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
@ -73,7 +121,7 @@ class MessagesView extends React.Component {
}); });
}; };
navToRoomInfo = navParam => { navToRoomInfo = (navParam: { rid: string }) => {
const { navigation, user } = this.props; const { navigation, user } = this.props;
if (navParam.rid === user.id) { if (navParam.rid === user.id) {
return; return;
@ -81,9 +129,9 @@ class MessagesView extends React.Component {
navigation.navigate('RoomInfoView', navParam); navigation.navigate('RoomInfoView', navParam);
}; };
jumpToMessage = async ({ item }) => { jumpToMessage = async ({ item }: { item: IMessageItem }) => {
const { navigation, isMasterDetail } = this.props; const { navigation, isMasterDetail } = this.props;
let params = { let params: IParams = {
rid: this.rid, rid: this.rid,
jumpToMessageId: item._id, jumpToMessageId: item._id,
t: this.t, 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 { user, baseUrl, theme, useRealName } = this.props;
const renderItemCommonProps = item => ({ const renderItemCommonProps = (item: IMessageItem) => ({
item, item,
baseUrl, baseUrl,
user, user,
@ -137,7 +185,7 @@ class MessagesView extends React.Component {
}, },
noDataMsg: I18n.t('No_files'), noDataMsg: I18n.t('No_files'),
testID: 'room-files-view', testID: 'room-files-view',
renderItem: item => ( renderItem: (item: IMessageItem) => (
<Message <Message
{...renderItemCommonProps(item)} {...renderItemCommonProps(item)}
item={{ item={{
@ -165,7 +213,7 @@ class MessagesView extends React.Component {
}, },
noDataMsg: I18n.t('No_mentioned_messages'), noDataMsg: I18n.t('No_mentioned_messages'),
testID: 'mentioned-messages-view', 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 Messages Screen
Starred: { Starred: {
@ -176,15 +224,15 @@ class MessagesView extends React.Component {
}, },
noDataMsg: I18n.t('No_starred_messages'), noDataMsg: I18n.t('No_starred_messages'),
testID: 'starred-messages-view', testID: 'starred-messages-view',
renderItem: item => ( renderItem: (item: IMessageItem) => (
<Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} /> <Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} />
), ),
action: message => ({ action: (message: IMessageItem) => ({
title: I18n.t('Unstar'), title: I18n.t('Unstar'),
icon: message.starred ? 'star-filled' : 'star', icon: message.starred ? 'star-filled' : 'star',
onPress: this.handleActionPress onPress: this.handleActionPress
}), }),
handleActionPress: message => RocketChat.toggleStarMessage(message._id, message.starred) handleActionPress: (message: IMessageItem) => RocketChat.toggleStarMessage(message._id, message.starred)
}, },
// Pinned Messages Screen // Pinned Messages Screen
Pinned: { Pinned: {
@ -195,12 +243,13 @@ class MessagesView extends React.Component {
}, },
noDataMsg: I18n.t('No_pinned_messages'), noDataMsg: I18n.t('No_pinned_messages'),
testID: 'pinned-messages-view', testID: 'pinned-messages-view',
renderItem: item => ( renderItem: (item: IMessageItem) => (
<Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} /> <Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} />
), ),
action: () => ({ title: I18n.t('Unpin'), icon: 'pin', onPress: this.handleActionPress }), 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]; }[name];
}; };
@ -227,7 +276,7 @@ class MessagesView extends React.Component {
} }
}; };
getCustomEmoji = name => { getCustomEmoji = (name: string) => {
const { customEmojis } = this.props; const { customEmojis } = this.props;
const emoji = customEmojis[name]; const emoji = customEmojis[name];
if (emoji) { if (emoji) {
@ -236,12 +285,12 @@ class MessagesView extends React.Component {
return null; return null;
}; };
showAttachment = attachment => { showAttachment = (attachment: any) => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('AttachmentView', { attachment }); navigation.navigate('AttachmentView', { attachment });
}; };
onLongPress = message => { onLongPress = (message: IMessageItem) => {
this.setState({ message }, this.showActionSheet); this.setState({ message }, this.showActionSheet);
}; };
@ -257,8 +306,8 @@ class MessagesView extends React.Component {
try { try {
const result = await this.content.handleActionPress(message); const result = await this.content.handleActionPress(message);
if (result.success) { if (result.success) {
this.setState(prevState => ({ this.setState((prevState: IMessagesViewState) => ({
messages: prevState.messages.filter(item => item._id !== message._id), messages: prevState.messages.filter((item: IMessageItem) => item._id !== message._id),
total: prevState.total - 1 total: prevState.total - 1
})); }));
} }
@ -267,7 +316,7 @@ class MessagesView extends React.Component {
} }
}; };
setFileLoading = fileLoading => { setFileLoading = (fileLoading: boolean) => {
this.setState({ fileLoading }); 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() { render() {
const { messages, loading } = this.state; 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, baseUrl: state.server.server,
user: getUserSelector(state), user: getUserSelector(state),
customEmojis: state.customEmojis, customEmojis: state.customEmojis,

View File

@ -1,6 +1,9 @@
import React from 'react'; import React from 'react';
import { StyleSheet, Switch, Text } from 'react-native'; 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 database from '../../lib/database';
import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; 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 = () => ({ static navigationOptions = () => ({
title: I18n.t('Notification_Preferences') title: I18n.t('Notification_Preferences')
}); });
static propTypes = { private mounted: boolean;
navigation: PropTypes.object, private rid: string | undefined;
route: PropTypes.object, private roomObservable?: Observable<Model>;
theme: PropTypes.string private subscription?: Subscription;
};
constructor(props) { constructor(props: INotificationPreferencesView) {
super(props); super(props);
this.mounted = false; this.mounted = false;
this.rid = props.route.params?.rid; this.rid = props.route.params?.rid;
@ -43,10 +59,11 @@ class NotificationPreferencesView extends React.Component {
}; };
if (room && room.observe) { if (room && room.observe) {
this.roomObservable = room.observe(); this.roomObservable = room.observe();
this.subscription = this.roomObservable.subscribe(changes => { this.subscription = this.roomObservable.subscribe((changes: any) => {
if (this.mounted) { if (this.mounted) {
this.setState({ room: changes }); this.setState({ room: changes });
} else { } else {
// @ts-ignore
this.state.room = changes; 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()}`]); logEvent(events[`NP_${key.toUpperCase()}`]);
const { room } = this.state; const { room } = this.state;
const db = database.active; const db = database.active;
@ -71,7 +89,7 @@ class NotificationPreferencesView extends React.Component {
try { try {
await db.action(async () => { await db.action(async () => {
await room.update( await room.update(
protectedFunction(r => { protectedFunction((r: any) => {
r[key] = value; r[key] = value;
}) })
); );
@ -88,36 +106,38 @@ class NotificationPreferencesView extends React.Component {
await db.action(async () => { await db.action(async () => {
await room.update( await room.update(
protectedFunction(r => { protectedFunction((r: any) => {
r[key] = room[key]; r[key] = room[key];
}) })
); );
}); });
} catch (e) { } catch (e) {
// @ts-ignore
logEvent(events[`NP_${key.toUpperCase()}_F`]); logEvent(events[`NP_${key.toUpperCase()}_F`]);
log(e); 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 { room } = this.state;
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('PickerView', { navigation.navigate('PickerView', {
title, title,
data: OPTIONS[key], data: OPTIONS[key],
value: room[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 { room } = this.state;
const { theme } = this.props; 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 ( return (
<Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}> <Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}>
{I18n.t(text?.label, { defaultValue: text?.label, second: text?.second })} {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; const { room } = this.state;
return ( return (
<Switch <Switch
@ -181,7 +201,7 @@ class NotificationPreferencesView extends React.Component {
<List.Item <List.Item
title='Alert' title='Alert'
testID='notification-preference-view-alert' testID='notification-preference-view-alert'
onPress={title => this.pickerSelection(title, 'desktopNotifications')} onPress={(title: string) => this.pickerSelection(title, 'desktopNotifications')}
right={() => this.renderPickerOption('desktopNotifications')} right={() => this.renderPickerOption('desktopNotifications')}
/> />
<List.Separator /> <List.Separator />
@ -193,7 +213,7 @@ class NotificationPreferencesView extends React.Component {
<List.Item <List.Item
title='Alert' title='Alert'
testID='notification-preference-view-push-notification' testID='notification-preference-view-push-notification'
onPress={title => this.pickerSelection(title, 'mobilePushNotifications')} onPress={(title: string) => this.pickerSelection(title, 'mobilePushNotifications')}
right={() => this.renderPickerOption('mobilePushNotifications')} right={() => this.renderPickerOption('mobilePushNotifications')}
/> />
<List.Separator /> <List.Separator />
@ -205,21 +225,21 @@ class NotificationPreferencesView extends React.Component {
<List.Item <List.Item
title='Audio' title='Audio'
testID='notification-preference-view-audio' testID='notification-preference-view-audio'
onPress={title => this.pickerSelection(title, 'audioNotifications')} onPress={(title: string) => this.pickerSelection(title, 'audioNotifications')}
right={() => this.renderPickerOption('audioNotifications')} right={() => this.renderPickerOption('audioNotifications')}
/> />
<List.Separator /> <List.Separator />
<List.Item <List.Item
title='Sound' title='Sound'
testID='notification-preference-view-sound' testID='notification-preference-view-sound'
onPress={title => this.pickerSelection(title, 'audioNotificationValue')} onPress={(title: string) => this.pickerSelection(title, 'audioNotificationValue')}
right={() => this.renderPickerOption('audioNotificationValue')} right={() => this.renderPickerOption('audioNotificationValue')}
/> />
<List.Separator /> <List.Separator />
<List.Item <List.Item
title='Notification_Duration' title='Notification_Duration'
testID='notification-preference-view-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')} right={() => this.renderPickerOption('desktopNotificationDuration')}
/> />
<List.Separator /> <List.Separator />
@ -230,7 +250,7 @@ class NotificationPreferencesView extends React.Component {
<List.Item <List.Item
title='Alert' title='Alert'
testID='notification-preference-view-email-alert' testID='notification-preference-view-email-alert'
onPress={title => this.pickerSelection(title, 'emailNotifications')} onPress={(title: string) => this.pickerSelection(title, 'emailNotifications')}
right={() => this.renderPickerOption('emailNotifications')} right={() => this.renderPickerOption('emailNotifications')}
/> />
<List.Separator /> <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: [ desktopNotifications: [
{ {
label: 'Default', label: 'Default',

View File

@ -1,5 +1,6 @@
import React from 'react'; 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 { FlatList, StyleSheet, Text, View } from 'react-native';
import I18n from '../i18n'; 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 <List.Item
title={I18n.t(item.label, { defaultValue: item.label, second: item?.second })} title={I18n.t(item.label, { defaultValue: item.label, second: item?.second })}
right={selected && (() => <List.Icon name='check' color={themes[theme].tintColor} />)} right={selected && (() => <List.Icon name='check' color={themes[theme].tintColor} />)}
@ -32,25 +67,15 @@ const Item = React.memo(({ item, selected, onItemPress, theme }) => (
translateTitle={false} translateTitle={false}
/> />
)); ));
Item.propTypes = {
item: PropTypes.object,
selected: PropTypes.bool,
onItemPress: PropTypes.func,
theme: PropTypes.string
};
class PickerView extends React.PureComponent { class PickerView extends React.PureComponent<IPickerViewProps, IPickerViewState> {
static navigationOptions = ({ route }) => ({ private onSearch: (text: string) => IData[];
static navigationOptions = ({ route }: IPickerViewProps) => ({
title: route.params?.title ?? I18n.t('Select_an_option') title: route.params?.title ?? I18n.t('Select_an_option')
}); });
static propTypes = { constructor(props: IPickerViewProps) {
navigation: PropTypes.object,
route: PropTypes.object,
theme: PropTypes.string
};
constructor(props) {
super(props); super(props);
const data = props.route.params?.data ?? []; const data = props.route.params?.data ?? [];
const value = props.route.params?.value; const value = props.route.params?.value;
@ -59,7 +84,7 @@ class PickerView extends React.PureComponent {
this.onSearch = props.route.params?.onChangeText; this.onSearch = props.route.params?.onChangeText;
} }
onChangeValue = value => { onChangeValue = (value: string) => {
const { navigation, route } = this.props; const { navigation, route } = this.props;
const goBack = route.params?.goBack ?? true; const goBack = route.params?.goBack ?? true;
const onChange = route.params?.onChangeValue ?? (() => {}); const onChange = route.params?.onChangeValue ?? (() => {});
@ -70,7 +95,7 @@ class PickerView extends React.PureComponent {
}; };
onChangeText = debounce( onChangeText = debounce(
async text => { async (text: string) => {
if (this.onSearch) { if (this.onSearch) {
const data = await this.onSearch(text); const data = await this.onSearch(text);
this.setState({ data }); this.setState({ data });

View File

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Keyboard, ScrollView, View } from 'react-native'; import { Keyboard, ScrollView, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import prompt from 'react-native-prompt-android'; import prompt from 'react-native-prompt-android';
import SHA256 from 'js-sha256'; import { sha256 } from 'js-sha256';
import ImagePicker from 'react-native-image-crop-picker'; import ImagePicker, { Image } from 'react-native-image-crop-picker';
import RNPickerSelect from 'react-native-picker-select'; import RNPickerSelect from 'react-native-picker-select';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { StackNavigationOptions } from '@react-navigation/stack';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import KeyboardView from '../../presentation/KeyboardView'; import KeyboardView from '../../presentation/KeyboardView';
@ -31,43 +31,40 @@ import { withTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
import styles from './styles'; import styles from './styles';
import { IAvatar, IAvatarButton, INavigationOptions, IParams, IProfileViewProps, IProfileViewState, IUser } from './interfaces';
class ProfileView extends React.Component { class ProfileView extends React.Component<IProfileViewProps, IProfileViewState> {
static navigationOptions = ({ navigation, isMasterDetail }) => { private name: any;
const options = { private username: any;
private email: any;
private avatarUrl: any;
private newPassword: any;
static navigationOptions = ({ navigation, isMasterDetail }: INavigationOptions) => {
const options: StackNavigationOptions = {
title: I18n.t('Profile') title: I18n.t('Profile')
}; };
if (!isMasterDetail) { if (!isMasterDetail) {
options.headerLeft = () => <HeaderButton.Drawer navigation={navigation} />; options.headerLeft = () => <HeaderButton.Drawer navigation={navigation} />;
} }
options.headerRight = () => ( options.headerRight = () => (
<HeaderButton.Preferences onPress={() => navigation.navigate('UserPreferencesView')} testID='preferences-view-open' /> <HeaderButton.Preferences onPress={() => navigation?.navigate('UserPreferencesView')} testID='preferences-view-open' />
); );
return options; return options;
}; };
static propTypes = { state: IProfileViewState = {
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 = {
saving: false, saving: false,
name: null, name: '',
username: null, username: '',
email: null, email: '',
newPassword: null, newPassword: '',
currentPassword: null, currentPassword: '',
avatarUrl: null, avatarUrl: '',
avatar: {}, avatar: {
data: {},
url: ''
},
avatarSuggestions: {}, avatarSuggestions: {},
customFields: {} customFields: {}
}; };
@ -83,7 +80,7 @@ class ProfileView extends React.Component {
} }
} }
UNSAFE_componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps: IProfileViewProps) {
const { user } = this.props; const { user } = this.props;
/* /*
* We need to ignore status because on Android ImagePicker * 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; const { Accounts_AllowUserAvatarChange } = this.props;
if (!Accounts_AllowUserAvatarChange) { if (!Accounts_AllowUserAvatarChange) {
@ -106,7 +103,7 @@ class ProfileView extends React.Component {
this.setState({ avatar }); this.setState({ avatar });
}; };
init = user => { init = (user?: IUser) => {
const { user: userProps } = this.props; const { user: userProps } = this.props;
const { name, username, emails, customFields } = user || userProps; const { name, username, emails, customFields } = user || userProps;
@ -117,7 +114,10 @@ class ProfileView extends React.Component {
newPassword: null, newPassword: null,
currentPassword: null, currentPassword: null,
avatarUrl: null, avatarUrl: null,
avatar: {}, avatar: {
data: {},
url: ''
},
customFields: customFields || {} customFields: customFields || {}
}); });
}; };
@ -142,12 +142,12 @@ class ProfileView extends React.Component {
!newPassword && !newPassword &&
user.emails && user.emails &&
user.emails[0].address === email && user.emails[0].address === email &&
!avatar.data && !avatar!.data &&
!customFieldsChanged !customFieldsChanged
); );
}; };
handleError = (e, func, action) => { handleError = (e: any, func: string, action: string) => {
if (e.data && e.data.error.includes('[error-too-many-requests]')) { if (e.data && e.data.error.includes('[error-too-many-requests]')) {
return showErrorAlert(e.data.error); 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 { name, username, email, newPassword, currentPassword, avatar, customFields } = this.state;
const { user, setUser } = this.props; const { user, setUser } = this.props;
const params = {}; const params = {} as IParams;
// Name // Name
if (user.name !== name) { if (user.name !== name) {
@ -189,7 +189,7 @@ class ProfileView extends React.Component {
// currentPassword // currentPassword
if (currentPassword) { if (currentPassword) {
params.currentPassword = SHA256(currentPassword); params.currentPassword = sha256(currentPassword);
} }
const requirePassword = !!params.email || newPassword; const requirePassword = !!params.email || newPassword;
@ -202,7 +202,7 @@ class ProfileView extends React.Component {
{ text: I18n.t('Cancel'), onPress: () => {}, style: 'cancel' }, { text: I18n.t('Cancel'), onPress: () => {}, style: 'cancel' },
{ {
text: I18n.t('Save'), text: I18n.t('Save'),
onPress: p => { onPress: (p: string) => {
this.setState({ currentPassword: p }); this.setState({ currentPassword: p });
this.submit(); this.submit();
} }
@ -217,7 +217,7 @@ class ProfileView extends React.Component {
} }
try { try {
if (avatar.url) { if (avatar!.url) {
try { try {
logEvent(events.PROFILE_SAVE_AVATAR); logEvent(events.PROFILE_SAVE_AVATAR);
await RocketChat.setAvatarFromService(avatar); await RocketChat.setAvatarFromService(avatar);
@ -283,7 +283,7 @@ class ProfileView extends React.Component {
}; };
try { try {
logEvent(events.PROFILE_PICK_AVATAR); 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' }); this.setAvatar({ url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' });
} catch (error) { } catch (error) {
logEvent(events.PROFILE_PICK_AVATAR_F); 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); logEvent(events.PROFILE_PICK_AVATAR_WITH_URL);
this.setAvatar({ url: avatarUrl, data: avatarUrl, service: '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; const { theme } = this.props;
return ( return (
<Touch <Touch
@ -331,7 +331,7 @@ class ProfileView extends React.Component {
})} })}
{this.renderAvatarButton({ {this.renderAvatarButton({
child: <CustomIcon name='link' size={30} color={themes[theme].bodyText} />, child: <CustomIcon name='link' size={30} color={themes[theme].bodyText} />,
onPress: () => this.pickImageWithURL(avatarUrl), onPress: () => this.pickImageWithURL(avatarUrl!),
disabled: !avatarUrl, disabled: !avatarUrl,
key: 'profile-view-avatar-url-button' key: 'profile-view-avatar-url-button'
})} })}
@ -365,19 +365,20 @@ class ProfileView extends React.Component {
const parsedCustomFields = JSON.parse(Accounts_CustomFields); const parsedCustomFields = JSON.parse(Accounts_CustomFields);
return Object.keys(parsedCustomFields).map((key, index, array) => { return Object.keys(parsedCustomFields).map((key, index, array) => {
if (parsedCustomFields[key].type === 'select') { 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 ( return (
<RNPickerSelect <RNPickerSelect
key={key} key={key}
items={options} items={options}
onValueChange={value => { onValueChange={value => {
const newValue = {}; const newValue: { [key: string]: string } = {};
newValue[key] = value; newValue[key] = value;
this.setState({ customFields: { ...customFields, ...newValue } }); this.setState({ customFields: { ...customFields, ...newValue } });
}} }}
value={customFields[key]}> value={customFields[key]}>
<RCTextInput <RCTextInput
inputRef={e => { inputRef={e => {
// @ts-ignore
this[key] = e; this[key] = e;
}} }}
label={key} label={key}
@ -393,6 +394,7 @@ class ProfileView extends React.Component {
return ( return (
<RCTextInput <RCTextInput
inputRef={e => { inputRef={e => {
// @ts-ignore
this[key] = e; this[key] = e;
}} }}
key={key} key={key}
@ -400,12 +402,13 @@ class ProfileView extends React.Component {
placeholder={key} placeholder={key}
value={customFields[key]} value={customFields[key]}
onChangeText={value => { onChangeText={value => {
const newValue = {}; const newValue: { [key: string]: string } = {};
newValue[key] = value; newValue[key] = value;
this.setState({ customFields: { ...customFields, ...newValue } }); this.setState({ customFields: { ...customFields, ...newValue } });
}} }}
onSubmitEditing={() => { onSubmitEditing={() => {
if (array.length - 1 > index) { if (array.length - 1 > index) {
// @ts-ignore
return this[array[index + 1]].focus(); return this[array[index + 1]].focus();
} }
this.avatarUrl.focus(); this.avatarUrl.focus();
@ -421,6 +424,7 @@ class ProfileView extends React.Component {
logoutOtherLocations = () => { logoutOtherLocations = () => {
logEvent(events.PL_OTHER_LOCATIONS); logEvent(events.PL_OTHER_LOCATIONS);
// @ts-ignore
showConfirmationAlert({ showConfirmationAlert({
message: I18n.t('You_will_be_logged_out_from_other_locations'), message: I18n.t('You_will_be_logged_out_from_other_locations'),
confirmationText: I18n.t('Logout'), confirmationText: I18n.t('Logout'),
@ -469,7 +473,7 @@ class ProfileView extends React.Component {
label={I18n.t('Name')} label={I18n.t('Name')}
placeholder={I18n.t('Name')} placeholder={I18n.t('Name')}
value={name} value={name}
onChangeText={value => this.setState({ name: value })} onChangeText={(value: string) => this.setState({ name: value })}
onSubmitEditing={() => { onSubmitEditing={() => {
this.username.focus(); this.username.focus();
}} }}
@ -500,7 +504,7 @@ class ProfileView extends React.Component {
}} }}
label={I18n.t('Email')} label={I18n.t('Email')}
placeholder={I18n.t('Email')} placeholder={I18n.t('Email')}
value={email} value={email!}
onChangeText={value => this.setState({ email: value })} onChangeText={value => this.setState({ email: value })}
onSubmitEditing={() => { onSubmitEditing={() => {
this.newPassword.focus(); this.newPassword.focus();
@ -516,10 +520,11 @@ class ProfileView extends React.Component {
}} }}
label={I18n.t('New_Password')} label={I18n.t('New_Password')}
placeholder={I18n.t('New_Password')} placeholder={I18n.t('New_Password')}
value={newPassword} value={newPassword!}
onChangeText={value => this.setState({ newPassword: value })} onChangeText={value => this.setState({ newPassword: value })}
onSubmitEditing={() => { onSubmitEditing={() => {
if (Accounts_CustomFields && Object.keys(customFields).length) { if (Accounts_CustomFields && Object.keys(customFields).length) {
// @ts-ignore
return this[Object.keys(customFields)[0]].focus(); return this[Object.keys(customFields)[0]].focus();
} }
this.avatarUrl.focus(); this.avatarUrl.focus();
@ -537,7 +542,7 @@ class ProfileView extends React.Component {
}} }}
label={I18n.t('Avatar_Url')} label={I18n.t('Avatar_Url')}
placeholder={I18n.t('Avatar_Url')} placeholder={I18n.t('Avatar_Url')}
value={avatarUrl} value={avatarUrl!}
onChangeText={value => this.setState({ avatarUrl: value })} onChangeText={value => this.setState({ avatarUrl: value })}
onSubmitEditing={this.submit} onSubmitEditing={this.submit}
testID='profile-view-avatar-url' testID='profile-view-avatar-url'
@ -568,7 +573,7 @@ class ProfileView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
user: getUserSelector(state), user: getUserSelector(state),
Accounts_AllowEmailChange: state.settings.Accounts_AllowEmailChange, Accounts_AllowEmailChange: state.settings.Accounts_AllowEmailChange,
Accounts_AllowPasswordChange: state.settings.Accounts_AllowPasswordChange, Accounts_AllowPasswordChange: state.settings.Accounts_AllowPasswordChange,
@ -579,8 +584,8 @@ const mapStateToProps = state => ({
baseUrl: state.server.server baseUrl: state.server.server
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: any) => ({
setUser: params => dispatch(setUserAction(params)) setUser: (params: any) => dispatch(setUserAction(params))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(ProfileView)); 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 React from 'react';
import PropTypes from 'prop-types';
import { FlatList, Text, View, RefreshControl } from 'react-native'; import { FlatList, Text, View, RefreshControl } from 'react-native';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import moment from 'moment'; import moment from 'moment';
import { connect } from 'react-redux'; 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 * as List from '../../containers/List';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
@ -16,9 +17,40 @@ import { themes } from '../../constants/colors';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
import styles from './styles'; import styles from './styles';
class ReadReceiptView extends React.Component { interface IReceipts {
static navigationOptions = ({ navigation, isMasterDetail }) => { _id: string;
const options = { 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') title: I18n.t('Read_Receipt')
}; };
if (isMasterDetail) { if (isMasterDetail) {
@ -27,13 +59,7 @@ class ReadReceiptView extends React.Component {
return options; return options;
}; };
static propTypes = { constructor(props: IReadReceiptViewProps) {
route: PropTypes.object,
Message_TimeAndDateFormat: PropTypes.string,
theme: PropTypes.string
};
constructor(props) {
super(props); super(props);
this.messageId = props.route.params?.messageId; this.messageId = props.route.params?.messageId;
this.state = { this.state = {
@ -46,7 +72,7 @@ class ReadReceiptView extends React.Component {
this.load(); this.load();
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: IReadReceiptViewProps, nextState: IReadReceiptViewState) {
const { loading, receipts } = this.state; const { loading, receipts } = this.state;
const { theme } = this.props; const { theme } = this.props;
if (nextProps.theme !== theme) { 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 { theme, Message_TimeAndDateFormat } = this.props;
const time = moment(item.ts).format(Message_TimeAndDateFormat); const time = moment(item.ts).format(Message_TimeAndDateFormat);
if (!item?.user?.username) { 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 Message_TimeAndDateFormat: state.settings.Message_TimeAndDateFormat
}); });

View File

@ -1,5 +1,6 @@
import React from 'react'; 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 { FlatList, Text, View } from 'react-native';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -12,6 +13,7 @@ import debounce from '../../utils/debounce';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import Message from '../../containers/message'; import Message from '../../containers/message';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { IMessage, IMessageAttachments } from '../../containers/message/interfaces';
import I18n from '../../i18n'; import I18n from '../../i18n';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import log from '../../utils/log'; import log from '../../utils/log';
@ -29,9 +31,52 @@ import { compareServerVersion, methods } from '../../lib/utils';
import styles from './styles'; import styles from './styles';
const QUERY_SIZE = 50; const QUERY_SIZE = 50;
class SearchMessagesView extends React.Component {
static navigationOptions = ({ navigation, route }) => { type TRouteParams = {
const options = { 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') title: I18n.t('Search')
}; };
const showCloseModal = route.params?.showCloseModal; const showCloseModal = route.params?.showCloseModal;
@ -41,18 +86,7 @@ class SearchMessagesView extends React.Component {
return options; return options;
}; };
static propTypes = { constructor(props: ISearchMessagesViewProps) {
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) {
super(props); super(props);
this.state = { this.state = {
loading: false, loading: false,
@ -60,7 +94,7 @@ class SearchMessagesView extends React.Component {
searchText: '' searchText: ''
}; };
this.offset = 0; this.offset = 0;
this.rid = props.route.params?.rid; this.rid = props.route.params.rid;
this.t = props.route.params?.t; this.t = props.route.params?.t;
this.encrypted = props.route.params?.encrypted; this.encrypted = props.route.params?.encrypted;
} }
@ -69,7 +103,7 @@ class SearchMessagesView extends React.Component {
this.room = await getRoomInfo(this.rid); this.room = await getRoomInfo(this.rid);
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: ISearchMessagesViewProps, nextState: ISearchMessagesViewState) {
const { loading, searchText, messages } = this.state; const { loading, searchText, messages } = this.state;
const { theme } = this.props; const { theme } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
@ -88,11 +122,11 @@ class SearchMessagesView extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
this.search?.stop?.(); this.searchDebounced?.stop?.();
} }
// Handle encrypted rooms search messages // Handle encrypted rooms search messages
searchMessages = async searchText => { searchMessages = async (searchText: string) => {
if (!searchText) { if (!searchText) {
return []; return [];
} }
@ -117,7 +151,7 @@ class SearchMessagesView extends React.Component {
} }
}; };
getMessages = async (searchText, debounced) => { getMessages = async (searchText: string, debounced?: boolean) => {
try { try {
const messages = await this.searchMessages(searchText); const messages = await this.searchMessages(searchText);
this.setState(prevState => ({ this.setState(prevState => ({
@ -130,17 +164,17 @@ class SearchMessagesView extends React.Component {
} }
}; };
search = searchText => { search = (searchText: string) => {
this.offset = 0; this.offset = 0;
this.setState({ searchText, loading: true, messages: [] }); this.setState({ searchText, loading: true, messages: [] });
this.searchDebounced(searchText); this.searchDebounced(searchText);
}; };
searchDebounced = debounce(async searchText => { searchDebounced = debounce(async (searchText: string) => {
await this.getMessages(searchText, true); await this.getMessages(searchText, true);
}, 1000); }, 1000);
getCustomEmoji = name => { getCustomEmoji = (name: string) => {
const { customEmojis } = this.props; const { customEmojis } = this.props;
const emoji = customEmojis[name]; const emoji = customEmojis[name];
if (emoji) { if (emoji) {
@ -149,12 +183,12 @@ class SearchMessagesView extends React.Component {
return null; return null;
}; };
showAttachment = attachment => { showAttachment = (attachment: IMessageAttachments) => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('AttachmentView', { attachment }); navigation.navigate('AttachmentView', { attachment });
}; };
navToRoomInfo = navParam => { navToRoomInfo = (navParam: IMessage) => {
const { navigation, user } = this.props; const { navigation, user } = this.props;
if (navParam.rid === user.id) { if (navParam.rid === user.id) {
return; return;
@ -162,9 +196,9 @@ class SearchMessagesView extends React.Component {
navigation.navigate('RoomInfoView', navParam); navigation.navigate('RoomInfoView', navParam);
}; };
jumpToMessage = async ({ item }) => { jumpToMessage = async ({ item }: { item: IMessage }) => {
const { navigation } = this.props; const { navigation } = this.props;
let params = { let params: any = {
rid: this.rid, rid: this.rid,
jumpToMessageId: item._id, jumpToMessageId: item._id,
t: this.t, 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; const { user, baseUrl, theme, useRealName } = this.props;
return ( return (
<Message <Message
@ -268,6 +302,7 @@ class SearchMessagesView extends React.Component {
testID='search-message-view-input' testID='search-message-view-input'
theme={theme} theme={theme}
/> />
{/* @ts-ignore */}
<Markdown msg={I18n.t('You_can_search_using_RegExp_eg')} username='' baseUrl='' theme={theme} /> <Markdown msg={I18n.t('You_can_search_using_RegExp_eg')} username='' baseUrl='' theme={theme} />
<View style={[styles.divider, { backgroundColor: themes[theme].separatorColor }]} /> <View style={[styles.divider, { backgroundColor: themes[theme].separatorColor }]} />
</View> </View>
@ -277,7 +312,7 @@ class SearchMessagesView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
serverVersion: state.server.version, serverVersion: state.server.version,
baseUrl: state.server.server, baseUrl: state.server.server,
user: getUserSelector(state), user: getUserSelector(state),

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import I18n from '../../i18n'; 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; let type;
if (thread?.id) { if (thread?.id) {
type = 'thread'; type = 'thread';
@ -88,10 +93,5 @@ const Header = React.memo(({ room, thread, theme }) => {
</View> </View>
); );
}); });
Header.propTypes = {
room: PropTypes.object,
thread: PropTypes.object,
theme: PropTypes.string
};
export default withTheme(Header); export default withTheme(Header);

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Video } from 'expo-av'; import { Video } from 'expo-av';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { ScrollView, StyleSheet, Text } from 'react-native'; import { ScrollView, StyleSheet, Text } from 'react-native';
@ -15,6 +14,7 @@ import I18n from '../../i18n';
import { isAndroid } from '../../utils/deviceInfo'; import { isAndroid } from '../../utils/deviceInfo';
import { allowPreview } from './utils'; import { allowPreview } from './utils';
import { THUMBS_HEIGHT } from './constants'; import { THUMBS_HEIGHT } from './constants';
import { IAttachment, IUseDimensions } from './interfaces';
const MESSAGEBOX_HEIGHT = 56; 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 <ScrollView
style={{ backgroundColor: themes[theme].auxiliaryBackground }} style={{ backgroundColor: themes[theme].auxiliaryBackground }}
contentContainerStyle={[styles.fileContainer, { width, height }]}> contentContainerStyle={[styles.fileContainer, { width, height }]}>
@ -45,9 +55,16 @@ const IconPreview = React.memo(({ iconName, title, description, theme, width, he
</ScrollView> </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 type = item?.mime;
const { width, height } = useDimensions(); const { width, height } = useDimensions() as IUseDimensions;
const { isLandscape } = useOrientation(); const { isLandscape } = useOrientation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const headerHeight = getHeaderHeight(isLandscape); 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; export default Preview;

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { FlatList, Image, StyleSheet, View } from 'react-native'; import { FlatList, Image, StyleSheet, View } from 'react-native';
import { RectButton, TouchableNativeFeedback, TouchableOpacity } from 'react-native-gesture-handler'; import { RectButton, TouchableNativeFeedback, TouchableOpacity } from 'react-native-gesture-handler';
@ -9,6 +8,7 @@ import { CustomIcon } from '../../lib/Icons';
import { isIOS } from '../../utils/deviceInfo'; import { isIOS } from '../../utils/deviceInfo';
import { THUMBS_HEIGHT } from './constants'; import { THUMBS_HEIGHT } from './constants';
import { allowPreview } from './utils'; import { allowPreview } from './utils';
import { IAttachment } from './interfaces';
const THUMB_SIZE = 64; 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; const type = item?.mime;
if (type?.match(/image/)) { if (type?.match(/image/)) {
// Disallow preview of images too big in order to prevent memory issues on iOS share extension // Disallow preview of images too big in order to prevent memory issues on iOS share extension
if (allowPreview(isShareExtension, item?.size)) { if (allowPreview(isShareExtension, item?.size)) {
return <Image source={{ uri: item.path }} style={[styles.thumb, { borderColor: themes[theme].borderColor }]} />; 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/)) { if (type?.match(/video/)) {
@ -85,22 +97,23 @@ const ThumbContent = React.memo(({ item, theme, isShareExtension }) => {
<CustomIcon name='camera' size={30} color={themes[theme].tintColor} /> <CustomIcon name='camera' size={30} color={themes[theme].tintColor} />
</View> </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 // Multiple files upload of files different than image/video is not implemented, so there's no thumb
return null; 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}> <ThumbButton style={styles.item} onPress={() => onPress(item)} activeOpacity={0.7}>
<> <>
<ThumbContent item={item} theme={theme} isShareExtension={isShareExtension} /> <ThumbContent item={item} theme={theme} isShareExtension={isShareExtension} />
@ -121,7 +134,7 @@ const Thumb = ({ item, theme, isShareExtension, onPress, onRemove }) => (
</ThumbButton> </ThumbButton>
); );
const Thumbs = React.memo(({ attachments, theme, isShareExtension, onPress, onRemove }) => { const Thumbs = React.memo(({ attachments, theme, isShareExtension, onPress, onRemove }: IThumbs) => {
if (attachments?.length > 1) { if (attachments?.length > 1) {
return ( return (
<FlatList <FlatList
@ -143,24 +156,5 @@ const Thumbs = React.memo(({ attachments, theme, isShareExtension, onPress, onRe
} }
return null; 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; export default Thumbs;

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'; 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 { NativeModules, Text, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ShareExtension from 'rn-extensions-share'; import ShareExtension from 'rn-extensions-share';
@ -24,9 +25,61 @@ import Thumbs from './Thumbs';
import Preview from './Preview'; import Preview from './Preview';
import Header from './Header'; import Header from './Header';
import styles from './styles'; import styles from './styles';
import { IAttachment, IServer } from './interfaces';
class ShareView extends Component { interface IShareViewState {
constructor(props) { 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); super(props);
this.messagebox = React.createRef(); this.messagebox = React.createRef();
this.files = props.route.params?.attachments ?? []; this.files = props.route.params?.attachments ?? [];
@ -34,7 +87,7 @@ class ShareView extends Component {
this.serverInfo = props.route.params?.serverInfo ?? {}; this.serverInfo = props.route.params?.serverInfo ?? {};
this.state = { this.state = {
selected: {}, selected: {} as IAttachment,
loading: false, loading: false,
readOnly: false, readOnly: false,
attachments: [], attachments: [],
@ -61,7 +114,7 @@ class ShareView extends Component {
const { room, thread, readOnly, attachments } = this.state; const { room, thread, readOnly, attachments } = this.state;
const { navigation, theme } = this.props; const { navigation, theme } = this.props;
const options = { const options: StackNavigationOptions = {
headerTitle: () => <Header room={room} thread={thread} />, headerTitle: () => <Header room={room} thread={thread} />,
headerTitleAlign: 'left', headerTitleAlign: 'left',
headerTintColor: themes[theme].previewTintColor headerTintColor: themes[theme].previewTintColor
@ -69,9 +122,7 @@ class ShareView extends Component {
// if is share extension show default back button // if is share extension show default back button
if (!this.isShareExtension) { if (!this.isShareExtension) {
options.headerLeft = () => ( options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
<HeaderButton.CloseModal navigation={navigation} buttonStyle={{ color: themes[theme].previewTintColor }} />
);
} }
if (!attachments.length && !readOnly) { if (!attachments.length && !readOnly) {
@ -203,10 +254,10 @@ class ShareView extends Component {
} }
}; };
selectFile = item => { selectFile = (item: IAttachment) => {
const { attachments, selected } = this.state; const { attachments, selected } = this.state;
if (attachments.length > 0) { if (attachments.length > 0) {
const { text } = this.messagebox.current; const text = this.messagebox.current?.text;
const newAttachments = attachments.map(att => { const newAttachments = attachments.map(att => {
if (att.path === selected.path) { if (att.path === selected.path) {
att.description = text; att.description = text;
@ -217,7 +268,7 @@ class ShareView extends Component {
} }
}; };
removeFile = item => { removeFile = (item: IAttachment) => {
const { selected, attachments } = this.state; const { selected, attachments } = this.state;
let newSelected; let newSelected;
if (item.path === selected.path) { if (item.path === selected.path) {
@ -235,7 +286,7 @@ class ShareView extends Component {
}); });
}; };
onChangeText = text => { onChangeText = (text: string) => {
this.setState({ text }); this.setState({ text });
}; };
@ -318,21 +369,7 @@ class ShareView extends Component {
} }
} }
ShareView.propTypes = { const mapStateToProps = (state: any) => ({
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 => ({
user: getUserSelector(state), user: getUserSelector(state),
server: state.share.server.server || state.server.server, server: state.share.server.server || state.server.server,
FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList, 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 React from 'react';
import { Text, View } from 'react-native'; import { Text, View } from 'react-native';
import PropTypes from 'prop-types';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import styles from './styles'; 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 <Touch
key={testID} key={testID}
testID={testID} testID={testID}
@ -24,14 +33,4 @@ const Item = React.memo(({ left, right, text, onPress, testID, current, theme })
</Touch> </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); export default withTheme(Item);

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'; 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 { ScrollView, Text, TouchableWithoutFeedback, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
@ -18,38 +19,52 @@ import Navigation from '../../lib/Navigation';
import SidebarItem from './SidebarItem'; import SidebarItem from './SidebarItem';
import styles from './styles'; import styles from './styles';
const Separator = React.memo(({ theme }) => <View style={[styles.separator, { borderColor: themes[theme].separatorColor }]} />); interface ISeparatorProps {
Separator.propTypes = { theme: string;
theme: PropTypes.string }
};
class Sidebar extends Component { // TODO: remove this
static propTypes = { const Separator = React.memo(({ theme }: ISeparatorProps) => (
baseUrl: PropTypes.string, <View style={[styles.separator, { borderColor: themes[theme].separatorColor }]} />
navigation: PropTypes.object, ));
Site_Name: PropTypes.string.isRequired,
user: PropTypes.object, interface ISidebarState {
state: PropTypes.string, showStatus: boolean;
theme: PropTypes.string, }
loadingServer: PropTypes.bool,
useRealName: PropTypes.bool, interface ISidebarProps {
allowStatusMessage: PropTypes.bool, baseUrl: string;
isMasterDetail: PropTypes.bool, navigation: DrawerNavigationProp<any, 'Sidebar'>;
viewStatisticsPermission: PropTypes.object, state: DrawerNavigationState<any>;
viewRoomAdministrationPermission: PropTypes.object, Site_Name: string;
viewUserAdministrationPermission: PropTypes.object, user: {
viewPrivilegedSettingPermission: PropTypes.object 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); super(props);
this.state = { this.state = {
showStatus: false showStatus: false
}; };
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: ISidebarProps, nextState: ISidebarState) {
const { showStatus, isAdmin } = this.state; const { showStatus } = this.state;
const { const {
Site_Name, Site_Name,
user, user,
@ -91,9 +106,6 @@ class Sidebar extends Component {
if (nextProps.useRealName !== useRealName) { if (nextProps.useRealName !== useRealName) {
return true; return true;
} }
if (nextState.isAdmin !== isAdmin) {
return true;
}
if (!dequal(nextProps.viewStatisticsPermission, viewStatisticsPermission)) { if (!dequal(nextProps.viewStatisticsPermission, viewStatisticsPermission)) {
return true; return true;
} }
@ -127,7 +139,7 @@ class Sidebar extends Component {
let isAdmin = false; let isAdmin = false;
if (roles) { if (roles) {
isAdmin = allPermissions.reduce((result, permission) => { isAdmin = allPermissions.reduce((result: boolean, permission) => {
if (permission) { if (permission) {
return result || permission.some(r => roles.indexOf(r) !== -1); return result || permission.some(r => roles.indexOf(r) !== -1);
} }
@ -137,7 +149,8 @@ class Sidebar extends Component {
return isAdmin; return isAdmin;
} }
sidebarNavigate = route => { sidebarNavigate = (route: string) => {
// @ts-ignore
logEvent(events[`SIDEBAR_GO_${route.replace('StackNavigator', '').replace('View', '').toUpperCase()}`]); logEvent(events[`SIDEBAR_GO_${route.replace('StackNavigator', '').replace('View', '').toUpperCase()}`]);
Navigation.navigate(route); Navigation.navigate(route);
}; };
@ -242,7 +255,7 @@ class Sidebar extends Component {
]} ]}
{...scrollPersistTaps}> {...scrollPersistTaps}>
<TouchableWithoutFeedback onPress={this.onPressUser} testID='sidebar-close-drawer'> <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} /> <Avatar text={user.username} style={styles.avatar} size={30} />
<View style={styles.headerTextContainer}> <View style={styles.headerTextContainer}>
<View style={styles.headerUsername}> <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, Site_Name: state.settings.Site_Name,
user: getUserSelector(state), user: getUserSelector(state),
baseUrl: state.server.server, baseUrl: state.server.server,

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { StyleSheet, Text } from 'react-native'; import { StyleSheet, Text } from 'react-native';
import PropTypes from 'prop-types'; import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -22,20 +22,34 @@ const styles = StyleSheet.create({
} }
}); });
class UserNotificationPreferencesView extends React.Component { type TKey = 'desktopNotifications' | 'pushNotifications' | 'emailNotificationMode';
static navigationOptions = () => ({
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') title: I18n.t('Notification_Preferences')
}); });
static propTypes = { constructor(props: IUserNotificationPreferencesViewProps) {
navigation: PropTypes.object,
theme: PropTypes.string,
user: PropTypes.shape({
id: PropTypes.string
})
};
constructor(props) {
super(props); super(props);
this.state = { this.state = {
preferences: {}, preferences: {},
@ -51,43 +65,43 @@ class UserNotificationPreferencesView extends React.Component {
this.setState({ preferences, loading: true }); this.setState({ preferences, loading: true });
} }
findDefaultOption = key => { findDefaultOption = (key: TKey) => {
const { preferences } = this.state; const { preferences } = this.state;
const option = preferences[key] ? OPTIONS[key].find(item => item.value === preferences[key]) : OPTIONS[key][0]; const option = preferences[key] ? OPTIONS[key].find(item => item.value === preferences[key]) : OPTIONS[key][0];
return option; return option;
}; };
renderPickerOption = key => { renderPickerOption = (key: TKey) => {
const { theme } = this.props; const { theme } = this.props;
const text = this.findDefaultOption(key); const text = this.findDefaultOption(key);
return ( return <Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}>{I18n.t(text?.label)}</Text>;
<Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}>
{I18n.t(text?.label, { defaultValue: text?.label, second: text?.second })}
</Text>
);
}; };
pickerSelection = (title, key) => { pickerSelection = (title: string, key: TKey) => {
const { preferences } = this.state; const { preferences } = this.state;
const { navigation } = this.props; const { navigation } = this.props;
let values = OPTIONS[key]; let values = OPTIONS[key];
const defaultOption = this.findDefaultOption(key); const defaultOption = this.findDefaultOption(key);
if (OPTIONS[key][0]?.value !== 'default') { 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', { navigation.navigate('PickerView', {
title, title,
data: values, data: values,
value: preferences[key], 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 { user } = this.props;
const { id } = user; const { id } = user;
const result = await RocketChat.setUserPreferences(id, params); const result = await RocketChat.setUserPreferences(id, params);
@ -111,7 +125,7 @@ class UserNotificationPreferencesView extends React.Component {
<List.Item <List.Item
title='Alert' title='Alert'
testID='user-notification-preference-view-alert' testID='user-notification-preference-view-alert'
onPress={title => this.pickerSelection(title, 'desktopNotifications')} onPress={(title: string) => this.pickerSelection(title, 'desktopNotifications')}
right={() => this.renderPickerOption('desktopNotifications')} right={() => this.renderPickerOption('desktopNotifications')}
/> />
<List.Separator /> <List.Separator />
@ -123,8 +137,8 @@ class UserNotificationPreferencesView extends React.Component {
<List.Item <List.Item
title='Alert' title='Alert'
testID='user-notification-preference-view-push-notification' testID='user-notification-preference-view-push-notification'
onPress={title => this.pickerSelection(title, 'mobileNotifications')} onPress={(title: string) => this.pickerSelection(title, 'pushNotifications')}
right={() => this.renderPickerOption('mobileNotifications')} right={() => this.renderPickerOption('pushNotifications')}
/> />
<List.Separator /> <List.Separator />
<List.Info info='Push_Notifications_Alert_Info' /> <List.Info info='Push_Notifications_Alert_Info' />
@ -135,7 +149,7 @@ class UserNotificationPreferencesView extends React.Component {
<List.Item <List.Item
title='Alert' title='Alert'
testID='user-notification-preference-view-email-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')} right={() => this.renderPickerOption('emailNotificationMode')}
/> />
<List.Separator /> <List.Separator />
@ -151,7 +165,7 @@ class UserNotificationPreferencesView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
user: getUserSelector(state) user: getUserSelector(state)
}); });

View File

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

View File

@ -1,6 +1,6 @@
import { StackNavigationProp } from '@react-navigation/stack';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Switch } from 'react-native'; import { Switch } from 'react-native';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import I18n from '../../i18n'; import I18n from '../../i18n';
@ -12,7 +12,11 @@ import { SWITCH_TRACK_COLOR } from '../../constants/colors';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import RocketChat from '../../lib/rocketchat'; 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 user = useSelector(state => getUserSelector(state));
const [enableParser, setEnableParser] = useState(user.enableMessageParserEarlyAdoption); const [enableParser, setEnableParser] = useState(user.enableMessageParserEarlyAdoption);
@ -22,12 +26,12 @@ const UserPreferencesView = ({ navigation }) => {
}); });
}, []); }, []);
const navigateToScreen = (screen, params) => { const navigateToScreen = (screen: string) => {
logEvent(events[`SE_GO_${screen.replace('View', '').toUpperCase()}`]); logEvent(events.UP_GO_USER_NOTIFICATION_PREF);
navigation.navigate(screen, params); navigation.navigate(screen);
}; };
const toggleMessageParser = async value => { const toggleMessageParser = async (value: boolean) => {
try { try {
await RocketChat.saveUserPreferences({ id: user.id, enableMessageParserEarlyAdoption: value }); await RocketChat.saveUserPreferences({ id: user.id, enableMessageParserEarlyAdoption: value });
setEnableParser(value); setEnableParser(value);
@ -68,8 +72,4 @@ const UserPreferencesView = ({ navigation }) => {
); );
}; };
UserPreferencesView.propTypes = {
navigation: PropTypes.object
};
export default UserPreferencesView; 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_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_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_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_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_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)}); 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_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_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_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_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_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"}]}); 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) - Firebase/Crashlytics (~> 6.27.0)
- React - React
- RNFBApp - RNFBApp
- RNFileViewer (2.1.4):
- React-Core
- RNGestureHandler (1.10.3): - RNGestureHandler (1.10.3):
- React-Core - React-Core
- RNImageCropPicker (0.36.3): - RNImageCropPicker (0.36.3):
@ -700,6 +702,7 @@ DEPENDENCIES:
- "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)" - "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)"
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)" - "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
- "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)" - "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)"
- RNFileViewer (from `../node_modules/react-native-file-viewer`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
- RNLocalize (from `../node_modules/react-native-localize`) - RNLocalize (from `../node_modules/react-native-localize`)
@ -894,6 +897,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-firebase/app" :path: "../node_modules/@react-native-firebase/app"
RNFBCrashlytics: RNFBCrashlytics:
:path: "../node_modules/@react-native-firebase/crashlytics" :path: "../node_modules/@react-native-firebase/crashlytics"
RNFileViewer:
:path: "../node_modules/react-native-file-viewer"
RNGestureHandler: RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler" :path: "../node_modules/react-native-gesture-handler"
RNImageCropPicker: RNImageCropPicker:
@ -1028,6 +1033,7 @@ SPEC CHECKSUMS:
RNFBAnalytics: dae6d7b280ba61c96e1bbdd34aca3154388f025e RNFBAnalytics: dae6d7b280ba61c96e1bbdd34aca3154388f025e
RNFBApp: 6fd8a7e757135d4168bf033a8812c241af7363a0 RNFBApp: 6fd8a7e757135d4168bf033a8812c241af7363a0
RNFBCrashlytics: 88de72c2476b5868a892d9523b89b86c527c540e RNFBCrashlytics: 88de72c2476b5868a892d9523b89b86c527c540e
RNFileViewer: 83cc066ad795b1f986791d03b56fe0ee14b6a69f
RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211 RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211
RNImageCropPicker: 97289cd94fb01ab79db4e5c92938be4d0d63415d RNImageCropPicker: 97289cd94fb01ab79db4e5c92938be4d0d63415d
RNLocalize: 82a569022724d35461e2dc5b5d015a13c3ca995b RNLocalize: 82a569022724d35461e2dc5b5d015a13c3ca995b

View File

@ -87,6 +87,7 @@
"react-native-document-picker": "5.2.0", "react-native-document-picker": "5.2.0",
"react-native-easy-grid": "^0.2.2", "react-native-easy-grid": "^0.2.2",
"react-native-easy-toast": "^1.2.0", "react-native-easy-toast": "^1.2.0",
"react-native-file-viewer": "^2.1.4",
"react-native-gesture-handler": "^1.10.3", "react-native-gesture-handler": "^1.10.3",
"react-native-image-crop-picker": "RocketChat/react-native-image-crop-picker", "react-native-image-crop-picker": "RocketChat/react-native-image-crop-picker",
"react-native-image-progress": "^1.1.1", "react-native-image-progress": "^1.1.1",
@ -147,6 +148,7 @@
"@types/lodash": "^4.14.171", "@types/lodash": "^4.14.171",
"@types/react": "^17.0.14", "@types/react": "^17.0.14",
"@types/react-native": "^0.62.7", "@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-config-reader": "^4.1.0",
"@types/react-native-platform-touchable": "^1.1.2", "@types/react-native-platform-touchable": "^1.1.2",
"@types/react-native-scrollable-tab-view": "^0.10.2", "@types/react-native-scrollable-tab-view": "^0.10.2",

View File

@ -4372,6 +4372,11 @@
"@types/history" "*" "@types/history" "*"
"@types/react" "*" "@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": "@types/react-native-config-reader@^4.1.0":
version "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" 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: dependencies:
prop-types "^15.5.10" 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: react-native-flipper@^0.34.0:
version "0.34.0" version "0.34.0"
resolved "https://registry.yarnpkg.com/react-native-flipper/-/react-native-flipper-0.34.0.tgz#7df1f38ba5d97a9321125fe0fccbe47d99e6fa1d" resolved "https://registry.yarnpkg.com/react-native-flipper/-/react-native-flipper-0.34.0.tgz#7df1f38ba5d97a9321125fe0fccbe47d99e6fa1d"