[RELEASE] Merge beta into master (#1174)

This commit is contained in:
Diego Mello 2019-09-03 16:27:57 -03:00 committed by GitHub
parent 494890ad84
commit d524ccdb72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
229 changed files with 26258 additions and 18316 deletions

View File

@ -116,10 +116,26 @@ jobs:
steps: steps:
- checkout - checkout
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
echo 'export PATH="/home/circleci/.nvm/versions/node/v8.16.0/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
- restore_cache: - restore_cache:
name: Restore NPM cache name: Restore NPM cache
key: node-modules-{{ checksum "yarn.lock" }} key: node-modules-{{ checksum "yarn.lock" }}
- run:
name: Install React Native CLI
command: |
npm i -g react-native-cli
- run: - run:
name: Install NPM modules name: Install NPM modules
command: | command: |
@ -148,12 +164,32 @@ jobs:
fi fi
echo -e "VERSIONCODE=$CIRCLE_BUILD_NUM" >> ./gradle.properties echo -e "VERSIONCODE=$CIRCLE_BUILD_NUM" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties
- run: - run:
name: Set Google Services name: Set Google Services
command: | command: |
cd android/app
cp google-services.prod.json google-services.json cp google-services.prod.json google-services.json
working_directory: android/app
- run:
name: Upload sourcemaps to Bugsnag
command: |
if [[ $BUGSNAG_KEY ]]; then
yarn generate-source-maps-android
curl https://upload.bugsnag.com/react-native-source-map \
-F apiKey=$BUGSNAG_KEY \
-F appVersionCode=$CIRCLE_BUILD_NUM \
-F dev=false \
-F platform=android \
-F sourceMap=@android-release.bundle.map \
-F bundle=@android-release.bundle
fi
- run:
name: Config variables
command: |
echo -e "export default { BUGSNAG_API_KEY: '$BUGSNAG_KEY' };" > ./config.js
- run: - run:
name: Build Android App name: Build Android App
@ -215,6 +251,7 @@ jobs:
- run: - run:
name: Install NPM modules name: Install NPM modules
command: | command: |
yarn global add react-native react-native-cli
yarn yarn
- run: - run:
@ -229,11 +266,26 @@ jobs:
cp GoogleService-Info.prod.plist GoogleService-Info.plist cp GoogleService-Info.prod.plist GoogleService-Info.plist
working_directory: ios working_directory: ios
- run:
name: Upload sourcemaps to Bugsnag
command: |
if [[ $BUGSNAG_KEY ]]; then
yarn generate-source-maps-ios
curl https://upload.bugsnag.com/react-native-source-map \
-F apiKey=$BUGSNAG_KEY \
-F appBundleVersion=$CIRCLE_BUILD_NUM \
-F dev=false \
-F platform=ios \
-F sourceMap=@ios-release.bundle.map \
-F bundle=@ios-release.bundle
fi
- run: - run:
name: Fastlane Build name: Fastlane Build
no_output_timeout: 1200 no_output_timeout: 1200
command: | command: |
agvtool new-version -all $CIRCLE_BUILD_NUM agvtool new-version -all $CIRCLE_BUILD_NUM
/usr/libexec/PlistBuddy -c "Set BugsnagAPIKey $BUGSNAG_KEY" ./RocketChatRN/Info.plist
if [[ $MATCH_KEYCHAIN_NAME ]]; then if [[ $MATCH_KEYCHAIN_NAME ]]; then
bundle exec fastlane ios release bundle exec fastlane ios release
@ -288,7 +340,7 @@ jobs:
- run: - run:
name: Fastlane Tesflight Upload name: Fastlane Tesflight Upload
command: | command: |
bundle exec fastlane pilot upload --ipa ios/RocketChatRN.ipa --changelog "$(sh ../.circleci/changelog.sh)" bundle exec fastlane ios beta
working_directory: ios working_directory: ios
- save_cache: - save_cache:

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
apply plugin: "com.android.application" apply plugin: "com.android.application"
apply plugin: "io.fabric" apply plugin: "io.fabric"
apply plugin: "com.google.firebase.firebase-perf" apply plugin: "com.google.firebase.firebase-perf"
apply plugin: 'com.bugsnag.android.gradle'
import com.android.build.OutputFile import com.android.build.OutputFile
@ -135,8 +136,9 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "1.18.0" versionName "1.19.0"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
} }
signingConfigs { signingConfigs {

View File

@ -5,7 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
@ -52,6 +52,9 @@
<data android:mimeType="*/*" /> <data android:mimeType="*/*" />
</intent-filter> </intent-filter>
</activity> </activity>
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="${BugsnagAPIKey}" />
</application> </application>
</manifest> </manifest>

View File

@ -7,6 +7,7 @@ import org.unimodules.core.interfaces.Package;
public class BasePackageList { public class BasePackageList {
public List<Package> getPackageList() { public List<Package> getPackageList() {
return Arrays.<Package>asList( return Arrays.<Package>asList(
new expo.modules.av.AVPackage(),
new expo.modules.constants.ConstantsPackage(), new expo.modules.constants.ConstantsPackage(),
new expo.modules.filesystem.FileSystemPackage(), new expo.modules.filesystem.FileSystemPackage(),
new expo.modules.haptics.HapticsPackage(), new expo.modules.haptics.HapticsPackage(),

View File

@ -24,6 +24,7 @@ buildscript {
classpath 'com.google.gms:google-services:4.2.0' classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.28.1' classpath 'io.fabric.tools:gradle:1.28.1'
classpath 'com.google.firebase:perf-plugin:1.2.1' classpath 'com.google.firebase:perf-plugin:1.2.1'
classpath 'com.bugsnag:bugsnag-android-gradle-plugin:4.+'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
@ -50,16 +51,16 @@ allprojects {
} }
} }
// subprojects { subproject -> // subprojects { subproject ->
// afterEvaluate { // afterEvaluate {
// if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) { // if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) {
// android { // android {
// compileSdkVersion 28 // compileSdkVersion 28
// buildToolsVersion "28.0.3" // buildToolsVersion "28.0.3"
// defaultConfig { // defaultConfig {
// targetSdkVersion 28 // targetSdkVersion 28
// } // }
// } // }
// } // }
// } // }
// } // }

View File

@ -22,3 +22,4 @@ org.gradle.jvmargs=-Xmx2048M -XX\:MaxHeapSize\=32g
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
VERSIONCODE=999999999 VERSIONCODE=999999999
BugsnagAPIKey=""

View File

@ -18,4 +18,5 @@ if (__DEV__) {
Reactotron.clear(); Reactotron.clear();
console.warn = Reactotron.log; console.warn = Reactotron.log;
console.log = Reactotron.log; console.log = Reactotron.log;
console.disableYellowBox = true;
} }

View File

@ -74,3 +74,4 @@ export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']); export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
export const NOTIFICATION = createRequestTypes('NOTIFICATION', ['RECEIVED', 'REMOVE']); export const NOTIFICATION = createRequestTypes('NOTIFICATION', ['RECEIVED', 'REMOVE']);
export const TOGGLE_MARKDOWN = 'TOGGLE_MARKDOWN'; export const TOGGLE_MARKDOWN = 'TOGGLE_MARKDOWN';
export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT';

View File

@ -0,0 +1,8 @@
import * as types from './actionsTypes';
export function toggleCrashReport(value) {
return {
type: types.TOGGLE_CRASH_REPORT,
payload: value
};
}

View File

@ -23,10 +23,11 @@ export function selectServerFailure() {
}; };
} }
export function serverRequest(server) { export function serverRequest(server, certificate = null) {
return { return {
type: SERVER.REQUEST, type: SERVER.REQUEST,
server server,
certificate
}; };
} }

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { import {
View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet, SafeAreaView View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet, SafeAreaView
} from 'react-native'; } from 'react-native';
@ -6,7 +6,7 @@ import FastImage from 'react-native-fast-image';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import ImageViewer from 'react-native-image-zoom-viewer'; import ImageViewer from 'react-native-image-zoom-viewer';
import VideoPlayer from 'react-native-video-controls'; import { Video } from 'expo-av';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { COLOR_WHITE } from '../constants/colors'; import { COLOR_WHITE } from '../constants/colors';
@ -38,6 +38,18 @@ const styles = StyleSheet.create({
}, },
indicator: { indicator: {
flex: 1 flex: 1
},
video: {
flex: 1
},
loading: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
} }
}); });
@ -72,15 +84,26 @@ const ModalContent = React.memo(({
); );
} }
if (attachment && attachment.video_url) { if (attachment && attachment.video_url) {
const [loading, setLoading] = useState(true);
const uri = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl); const uri = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl);
return ( return (
<SafeAreaView style={styles.safeArea}> <>
<VideoPlayer <Video
source={{ uri }} source={{ uri }}
onBack={onClose} rate={1.0}
disableVolume volume={1.0}
isMuted={false}
resizeMode='cover'
shouldPlay
isLooping={false}
style={styles.video}
useNativeControls
onReadyForDisplay={() => setLoading(false)}
onLoadStart={() => setLoading(true)}
onError={console.log}
/> />
</SafeAreaView> { loading ? <ActivityIndicator size='large' style={styles.loading} /> : null }
</>
); );
} }
return null; return null;
@ -95,11 +118,11 @@ const FileModal = React.memo(({
onBackdropPress={onClose} onBackdropPress={onClose}
onBackButtonPress={onClose} onBackButtonPress={onClose}
onSwipeComplete={onClose} onSwipeComplete={onClose}
swipeDirection={['up', 'left', 'right', 'down']} swipeDirection={['up', 'down']}
> >
<ModalContent attachment={attachment} onClose={onClose} user={user} baseUrl={baseUrl} /> <ModalContent attachment={attachment} onClose={onClose} user={user} baseUrl={baseUrl} />
</Modal> </Modal>
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible); ), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible && prevProps.loading === nextProps.loading);
FileModal.propTypes = { FileModal.propTypes = {
isVisible: PropTypes.bool, isVisible: PropTypes.bool,

View File

@ -310,8 +310,8 @@ class MessageActions extends React.Component {
try { try {
await RocketChat.reportMessage(actionMessage._id); await RocketChat.reportMessage(actionMessage._id);
Alert.alert(I18n.t('Message_Reported')); Alert.alert(I18n.t('Message_Reported'));
} catch (err) { } catch (e) {
log('err_report_message', err); log(e);
} }
} }
@ -327,8 +327,8 @@ class MessageActions extends React.Component {
if (!translatedMessage) { if (!translatedMessage) {
await RocketChat.translateMessage(actionMessage, room.autoTranslateLanguage); await RocketChat.translateMessage(actionMessage, room.autoTranslateLanguage);
} }
} catch (err) { } catch (e) {
log('err_toggle_translation', err); log(e);
} }
} }

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Markdown from '../message/Markdown'; import Markdown from '../markdown';
import { getCustomEmoji } from '../message/utils'; import { getCustomEmoji } from '../message/utils';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
@ -50,6 +50,7 @@ const styles = StyleSheet.create({
class ReplyPreview extends Component { class ReplyPreview extends Component {
static propTypes = { static propTypes = {
useMarkdown: PropTypes.bool,
message: PropTypes.object.isRequired, message: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired,
close: PropTypes.func.isRequired, close: PropTypes.func.isRequired,
@ -68,7 +69,7 @@ class ReplyPreview extends Component {
render() { render() {
const { const {
message, Message_TimeFormat, baseUrl, username message, Message_TimeFormat, baseUrl, username, useMarkdown
} = this.props; } = this.props;
const time = moment(message.ts).format(Message_TimeFormat); const time = moment(message.ts).format(Message_TimeFormat);
return ( return (
@ -78,7 +79,7 @@ class ReplyPreview extends Component {
<Text style={styles.username}>{message.u.username}</Text> <Text style={styles.username}>{message.u.username}</Text>
<Text style={styles.time}>{time}</Text> <Text style={styles.time}>{time}</Text>
</View> </View>
<Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} numberOfLines={1} /> <Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} numberOfLines={1} useMarkdown={useMarkdown} />
</View> </View>
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} /> <CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
</View> </View>
@ -87,6 +88,7 @@ class ReplyPreview extends Component {
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
useMarkdown: state.markdown.useMarkdown,
Message_TimeFormat: state.settings.Message_TimeFormat, Message_TimeFormat: state.settings.Message_TimeFormat,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}); });

View File

@ -293,7 +293,7 @@ class MessageBox extends Component {
try { try {
RocketChat.executeCommandPreview(command, params, rid, item); RocketChat.executeCommandPreview(command, params, rid, item);
} catch (e) { } catch (e) {
log('onPressCommandPreview', e); log(e);
} }
} }
@ -362,7 +362,7 @@ class MessageBox extends Component {
try { try {
database.create('users', user, true); database.create('users', user, true);
} catch (e) { } catch (e) {
log('err_create_users', e); log(e);
} }
}); });
}); });
@ -468,7 +468,7 @@ class MessageBox extends Component {
this.setState({ commandPreview: preview.items }); this.setState({ commandPreview: preview.items });
} catch (e) { } catch (e) {
this.showCommandPreview = false; this.showCommandPreview = false;
log('command Preview', e); log(e);
} }
} }
@ -504,7 +504,7 @@ class MessageBox extends Component {
try { try {
await RocketChat.sendFileMessage(rid, fileInfo, tmid, server, user); await RocketChat.sendFileMessage(rid, fileInfo, tmid, server, user);
} catch (e) { } catch (e) {
log('err_send_media_message', e); log(e);
} }
} }
@ -513,7 +513,7 @@ class MessageBox extends Component {
const image = await ImagePicker.openCamera(this.imagePickerConfig); const image = await ImagePicker.openCamera(this.imagePickerConfig);
this.showUploadModal(image); this.showUploadModal(image);
} catch (e) { } catch (e) {
log('err_take_photo', e); log(e);
} }
} }
@ -522,7 +522,7 @@ class MessageBox extends Component {
const video = await ImagePicker.openCamera(this.videoPickerConfig); const video = await ImagePicker.openCamera(this.videoPickerConfig);
this.showUploadModal(video); this.showUploadModal(video);
} catch (e) { } catch (e) {
log('err_take_video', e); log(e);
} }
} }
@ -531,7 +531,7 @@ class MessageBox extends Component {
const image = await ImagePicker.openPicker(this.libraryPickerConfig); const image = await ImagePicker.openPicker(this.libraryPickerConfig);
this.showUploadModal(image); this.showUploadModal(image);
} catch (e) { } catch (e) {
log('err_choose_from_library', e); log(e);
} }
} }
@ -546,9 +546,9 @@ class MessageBox extends Component {
mime: res.type, mime: res.type,
path: res.uri path: res.uri
}); });
} catch (error) { } catch (e) {
if (!DocumentPicker.isCancel(error)) { if (!DocumentPicker.isCancel(e)) {
log('chooseFile', error); log(e);
} }
} }
} }
@ -618,7 +618,7 @@ class MessageBox extends Component {
if (e && e.error === 'error-file-too-large') { if (e && e.error === 'error-file-too-large') {
return Alert.alert(I18n.t(e.error)); return Alert.alert(I18n.t(e.error));
} }
log('err_finish_audio_message', e); log(e);
} }
} }
} }
@ -655,7 +655,7 @@ class MessageBox extends Component {
const messageWithoutCommand = message.substr(message.indexOf(' ') + 1); const messageWithoutCommand = message.substr(message.indexOf(' ') + 1);
RocketChat.runSlashCommand(command, roomId, messageWithoutCommand); RocketChat.runSlashCommand(command, roomId, messageWithoutCommand);
} catch (e) { } catch (e) {
log('slashCommand', e); log(e);
} }
this.clearInput(); this.clearInput();
return; return;

View File

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text } from 'react-native';
import styles from './styles';
const AtMention = React.memo(({
mention, mentions, username, navToRoomInfo
}) => {
let mentionStyle = styles.mention;
if (mention === 'all' || mention === 'here') {
mentionStyle = {
...mentionStyle,
...styles.mentionAll
};
} else if (mention === username) {
mentionStyle = {
...mentionStyle,
...styles.mentionLoggedUser
};
}
const handlePress = () => {
if (mentions && mentions.length && mentions.findIndex(m => m.username === mention) !== -1) {
const index = mentions.findIndex(m => m.username === mention);
const navParam = {
t: 'd',
rid: mentions[index]._id
};
navToRoomInfo(navParam);
}
};
return (
<Text
style={mentionStyle}
onPress={handlePress}
>
{`@${ mention }`}
</Text>
);
});
AtMention.propTypes = {
mention: PropTypes.string,
username: PropTypes.string,
navToRoomInfo: PropTypes.func,
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
};
export default AtMention;

View File

@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import styles from './styles';
const BlockQuote = React.memo(({ children }) => (
<View style={styles.container}>
<View style={styles.quote} />
<View style={styles.childContainer}>
{children}
</View>
</View>
));
BlockQuote.propTypes = {
children: PropTypes.node.isRequired
};
export default BlockQuote;

View File

@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text } from 'react-native';
import { emojify } from 'react-emojione';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import styles from './styles';
const Emoji = React.memo(({
emojiName, literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl
}) => {
const emojiUnicode = emojify(literal, { output: 'unicode' });
const emoji = getCustomEmoji && getCustomEmoji(emojiName);
if (emoji) {
return (
<CustomEmoji
baseUrl={baseUrl}
style={isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji}
emoji={emoji}
/>
);
}
return <Text style={isMessageContainsOnlyEmoji ? styles.textBig : styles.text}>{emojiUnicode}</Text>;
});
Emoji.propTypes = {
emojiName: PropTypes.string,
literal: PropTypes.string,
isMessageContainsOnlyEmoji: PropTypes.bool,
getCustomEmoji: PropTypes.func,
baseUrl: PropTypes.string
};
export default Emoji;

View File

@ -0,0 +1,38 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Text } from 'react-native';
import styles from './styles';
const Hashtag = React.memo(({
hashtag, channels, navToRoomInfo
}) => {
const handlePress = () => {
const index = channels.findIndex(channel => channel.name === hashtag);
const navParam = {
t: 'c',
rid: channels[index]._id
};
navToRoomInfo(navParam);
};
if (channels && channels.length && channels.findIndex(channel => channel.name === hashtag) !== -1) {
return (
<Text
style={styles.mention}
onPress={handlePress}
>
{`#${ hashtag }`}
</Text>
);
}
return `#${ hashtag }`;
});
Hashtag.propTypes = {
hashtag: PropTypes.string,
navToRoomInfo: PropTypes.func,
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
};
export default Hashtag;

View File

@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text } from 'react-native';
import styles from './styles';
import openLink from '../../utils/openLink';
const Link = React.memo(({
children, link
}) => {
const handlePress = () => {
if (!link) {
return;
}
openLink(link);
};
const childLength = React.Children.toArray(children).filter(o => o).length;
// if you have a [](https://rocket.chat) render https://rocket.chat
return (
<Text
onPress={handlePress}
style={styles.link}
>
{ childLength !== 0 ? children : link }
</Text>
);
});
Link.propTypes = {
children: PropTypes.node,
link: PropTypes.string
};
export default Link;

View File

@ -0,0 +1,39 @@
import PropTypes from 'prop-types';
import React from 'react';
const List = React.memo(({
children, ordered, start, tight
}) => {
let bulletWidth = 15;
if (ordered) {
const lastNumber = (start + children.length) - 1;
bulletWidth = (9 * lastNumber.toString().length) + 7;
}
const _children = React.Children.map(children, (child, index) => React.cloneElement(child, {
bulletWidth,
ordered,
tight,
index: start + index
}));
return (
<>
{_children}
</>
);
});
List.propTypes = {
children: PropTypes.node,
ordered: PropTypes.bool,
start: PropTypes.number,
tight: PropTypes.bool
};
List.defaultProps = {
start: 1
};
export default List;

View File

@ -0,0 +1,60 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
View
} from 'react-native';
const style = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'flex-start'
},
bullet: {
alignItems: 'flex-end',
marginRight: 5
},
contents: {
flex: 1
}
});
const ListItem = React.memo(({
children, level, bulletWidth, continue: _continue, ordered, index
}) => {
let bullet;
if (_continue) {
bullet = '';
} else if (ordered) {
bullet = `${ index }.`;
} else if (level % 2 === 0) {
bullet = '◦';
} else {
bullet = '•';
}
return (
<View style={style.container}>
<View style={[{ width: bulletWidth }, style.bullet]}>
<Text>
{bullet}
</Text>
</View>
<View style={style.contents}>
{children}
</View>
</View>
);
});
ListItem.propTypes = {
children: PropTypes.node,
bulletWidth: PropTypes.number,
level: PropTypes.number,
ordered: PropTypes.bool,
continue: PropTypes.bool,
index: PropTypes.number
};
export default ListItem;

View File

@ -0,0 +1,62 @@
import { PropTypes } from 'prop-types';
import React from 'react';
import {
ScrollView,
TouchableOpacity,
View,
Text
} from 'react-native';
import { CELL_WIDTH } from './TableCell';
import styles from './styles';
import Navigation from '../../lib/Navigation';
import I18n from '../../i18n';
const MAX_HEIGHT = 300;
const Table = React.memo(({
children, numColumns
}) => {
const getTableWidth = () => numColumns * CELL_WIDTH;
const renderRows = (drawExtraBorders = true) => {
const tableStyle = [styles.table];
if (drawExtraBorders) {
tableStyle.push(styles.tableExtraBorders);
}
const rows = React.Children.toArray(children);
rows[rows.length - 1] = React.cloneElement(rows[rows.length - 1], {
isLastRow: true
});
return (
<View style={tableStyle}>
{rows}
</View>
);
};
const onPress = () => Navigation.navigate('TableView', { renderRows, tableWidth: getTableWidth() });
return (
<TouchableOpacity onPress={onPress}>
<ScrollView
contentContainerStyle={{ width: getTableWidth() }}
scrollEnabled={false}
showsVerticalScrollIndicator={false}
style={[styles.containerTable, { maxWidth: getTableWidth(), maxHeight: MAX_HEIGHT }]}
>
{renderRows(false)}
</ScrollView>
<Text style={styles.textInfo}>{I18n.t('Full_table')}</Text>
</TouchableOpacity>
);
});
Table.propTypes = {
children: PropTypes.node.isRequired,
numColumns: PropTypes.number.isRequired
};
export default Table;

View File

@ -0,0 +1,39 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Text, View } from 'react-native';
import styles from './styles';
export const CELL_WIDTH = 100;
const TableCell = React.memo(({
isLastCell, align, children
}) => {
const cellStyle = [styles.cell];
if (!isLastCell) {
cellStyle.push(styles.cellRightBorder);
}
let textStyle = null;
if (align === 'center') {
textStyle = styles.alignCenter;
} else if (align === 'right') {
textStyle = styles.alignRight;
}
return (
<View style={[...cellStyle, { width: CELL_WIDTH }]}>
<Text style={textStyle}>
{children}
</Text>
</View>
);
});
TableCell.propTypes = {
align: PropTypes.oneOf(['', 'left', 'center', 'right']),
children: PropTypes.node,
isLastCell: PropTypes.bool
};
export default TableCell;

View File

@ -0,0 +1,28 @@
import PropTypes from 'prop-types';
import React from 'react';
import { View } from 'react-native';
import styles from './styles';
const TableRow = React.memo(({
isLastRow, children: _children
}) => {
const rowStyle = [styles.row];
if (!isLastRow) {
rowStyle.push(styles.rowBottomBorder);
}
const children = React.Children.toArray(_children);
children[children.length - 1] = React.cloneElement(children[children.length - 1], {
isLastCell: true
});
return <View style={rowStyle}>{children}</View>;
});
TableRow.propTypes = {
children: PropTypes.node,
isLastRow: PropTypes.bool
};
export default TableRow;

View File

@ -0,0 +1,295 @@
import React, { PureComponent } from 'react';
import { View, Text, Image } from 'react-native';
import { Parser, Node } from 'commonmark';
import Renderer from 'commonmark-react-renderer';
import PropTypes from 'prop-types';
import I18n from '../../i18n';
import MarkdownLink from './Link';
import MarkdownList from './List';
import MarkdownListItem from './ListItem';
import MarkdownAtMention from './AtMention';
import MarkdownHashtag from './Hashtag';
import MarkdownBlockQuote from './BlockQuote';
import MarkdownEmoji from './Emoji';
import MarkdownTable from './Table';
import MarkdownTableRow from './TableRow';
import MarkdownTableCell from './TableCell';
import styles from './styles';
// Support <http://link|Text>
const formatText = text => text.replace(
new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'),
(match, url, title) => `[${ title }](${ url })`
);
const emojiRanges = [
'\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]', // unicode emoji from https://www.regextester.com/106421
':.{1,40}:', // custom emoji
' |\n' // allow spaces and line breaks
].join('|');
const removeAllEmoji = str => str.replace(new RegExp(emojiRanges, 'g'), '');
const isOnlyEmoji = str => !removeAllEmoji(str).length;
const removeOneEmoji = str => str.replace(new RegExp(emojiRanges), '');
const emojiCount = (str) => {
let oldLength = 0;
let counter = 0;
while (oldLength !== str.length) {
oldLength = str.length;
str = removeOneEmoji(str);
if (oldLength !== str.length) {
counter += 1;
}
}
return counter;
};
export default class Markdown extends PureComponent {
static propTypes = {
msg: PropTypes.string,
getCustomEmoji: PropTypes.func,
baseUrl: PropTypes.string,
username: PropTypes.string,
tmid: PropTypes.string,
isEdited: PropTypes.bool,
numberOfLines: PropTypes.number,
useMarkdown: PropTypes.bool,
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
navToRoomInfo: PropTypes.func
};
constructor(props) {
super(props);
this.parser = this.createParser();
this.renderer = this.createRenderer();
}
createParser = () => new Parser();
createRenderer = () => new Renderer({
renderers: {
text: this.renderText,
emph: Renderer.forwardChildren,
strong: Renderer.forwardChildren,
del: Renderer.forwardChildren,
code: this.renderCodeInline,
link: this.renderLink,
image: this.renderImage,
atMention: this.renderAtMention,
emoji: this.renderEmoji,
hashtag: this.renderHashtag,
paragraph: this.renderParagraph,
heading: this.renderHeading,
codeBlock: this.renderCodeBlock,
blockQuote: this.renderBlockQuote,
list: this.renderList,
item: this.renderListItem,
hardBreak: this.renderBreak,
thematicBreak: this.renderBreak,
softBreak: this.renderBreak,
htmlBlock: this.renderText,
htmlInline: this.renderText,
table: this.renderTable,
table_row: this.renderTableRow,
table_cell: this.renderTableCell,
editedIndicator: this.renderEditedIndicator
},
renderParagraphsInLists: true
});
editedMessage = (ast) => {
const { isEdited } = this.props;
if (isEdited) {
const editIndicatorNode = new Node('edited_indicator');
if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) {
ast.lastChild.appendChild(editIndicatorNode);
} else {
const node = new Node('paragraph');
node.appendChild(editIndicatorNode);
ast.appendChild(node);
}
}
};
renderText = ({ context, literal }) => {
const { numberOfLines } = this.props;
return (
<Text
style={[
this.isMessageContainsOnlyEmoji ? styles.textBig : styles.text,
...context.map(type => styles[type])
]}
numberOfLines={numberOfLines}
>
{literal}
</Text>
);
}
renderCodeInline = ({ literal }) => <Text style={styles.codeInline}>{literal}</Text>;
renderCodeBlock = ({ literal }) => <Text style={styles.codeBlock}>{literal}</Text>;
renderBreak = () => {
const { tmid } = this.props;
return <Text>{tmid ? ' ' : '\n'}</Text>;
}
renderParagraph = ({ children }) => {
const { numberOfLines } = this.props;
if (!children || children.length === 0) {
return null;
}
return (
<View style={styles.block}>
<Text numberOfLines={numberOfLines}>
{children}
</Text>
</View>
);
};
renderLink = ({ children, href }) => (
<MarkdownLink link={href}>
{children}
</MarkdownLink>
);
renderHashtag = ({ hashtag }) => {
const { channels, navToRoomInfo } = this.props;
return (
<MarkdownHashtag
hashtag={hashtag}
channels={channels}
navToRoomInfo={navToRoomInfo}
/>
);
}
renderAtMention = ({ mentionName }) => {
const { username, mentions, navToRoomInfo } = this.props;
return (
<MarkdownAtMention
mentions={mentions}
mention={mentionName}
username={username}
navToRoomInfo={navToRoomInfo}
/>
);
}
renderEmoji = ({ emojiName, literal }) => {
const { getCustomEmoji, baseUrl } = this.props;
return (
<MarkdownEmoji
emojiName={emojiName}
literal={literal}
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji}
getCustomEmoji={getCustomEmoji}
baseUrl={baseUrl}
/>
);
}
renderImage = ({ src }) => <Image style={styles.inlineImage} source={{ uri: src }} />;
renderEditedIndicator = () => <Text style={styles.edited}> ({I18n.t('edited')})</Text>;
renderHeading = ({ children, level }) => {
const textStyle = styles[`heading${ level }Text`];
return (
<Text style={textStyle}>
{children}
</Text>
);
};
renderList = ({
children, start, tight, type
}) => (
<MarkdownList
ordered={type !== 'bullet'}
start={start}
tight={tight}
>
{children}
</MarkdownList>
);
renderListItem = ({
children, context, ...otherProps
}) => {
const level = context.filter(type => type === 'list').length;
return (
<MarkdownListItem
level={level}
{...otherProps}
>
{children}
</MarkdownListItem>
);
};
renderBlockQuote = ({ children }) => (
<MarkdownBlockQuote>
{children}
</MarkdownBlockQuote>
);
renderTable = ({ children, numColumns }) => (
<MarkdownTable numColumns={numColumns}>
{children}
</MarkdownTable>
);
renderTableRow = args => <MarkdownTableRow {...args} />;
renderTableCell = args => <MarkdownTableCell {...args} />;
render() {
const {
msg, useMarkdown = true, numberOfLines
} = this.props;
if (!msg) {
return null;
}
let m = formatText(msg);
// Ex: '[ ](https://open.rocket.chat/group/test?msg=abcdef) Test'
// Return: 'Test'
m = m.replace(/^\[([\s]]*)\]\(([^)]*)\)\s/, '').trim();
if (!useMarkdown) {
return <Text style={styles.text} numberOfLines={numberOfLines}>{m}</Text>;
}
const ast = this.parser.parse(m);
this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;
this.editedMessage(ast);
return this.renderer.render(ast);
}
}

View File

@ -0,0 +1,183 @@
import { StyleSheet, Platform } from 'react-native';
import sharedStyles from '../../views/Styles';
import {
COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER
} from '../../constants/colors';
const codeFontFamily = Platform.select({
ios: { fontFamily: 'Courier New' },
android: { fontFamily: 'monospace' }
});
export default StyleSheet.create({
container: {
alignItems: 'flex-start',
flexDirection: 'row'
},
childContainer: {
flex: 1
},
block: {
alignItems: 'flex-start',
flexDirection: 'row',
flexWrap: 'wrap'
},
emph: {
fontStyle: 'italic'
},
strong: {
fontWeight: 'bold'
},
del: {
textDecorationLine: 'line-through'
},
text: {
fontSize: 16,
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
},
textInfo: {
fontStyle: 'italic',
fontSize: 16,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
textBig: {
fontSize: 30,
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
},
customEmoji: {
width: 20,
height: 20
},
customEmojiBig: {
width: 30,
height: 30
},
temp: { opacity: 0.3 },
mention: {
fontSize: 16,
color: '#0072FE',
padding: 5,
...sharedStyles.textMedium,
backgroundColor: '#E8F2FF'
},
mentionLoggedUser: {
color: COLOR_WHITE,
backgroundColor: COLOR_PRIMARY
},
mentionAll: {
color: COLOR_WHITE,
backgroundColor: '#FF5B5A'
},
paragraph: {
marginTop: 0,
marginBottom: 0,
flexWrap: 'wrap',
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'flex-start'
},
inlineImage: {
width: 300,
height: 300,
resizeMode: 'contain'
},
codeInline: {
...sharedStyles.textRegular,
...codeFontFamily,
borderWidth: 1,
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderRadius: 4
},
codeBlock: {
...sharedStyles.textRegular,
...codeFontFamily,
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderColor: COLOR_BORDER,
borderWidth: 1,
borderRadius: 4,
padding: 4
},
link: {
fontSize: 16,
color: COLOR_PRIMARY,
...sharedStyles.textRegular
},
edited: {
fontSize: 14,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
heading1: {
...sharedStyles.textBold,
fontSize: 24
},
heading2: {
...sharedStyles.textBold,
fontSize: 22
},
heading3: {
...sharedStyles.textSemibold,
fontSize: 20
},
heading4: {
...sharedStyles.textSemibold,
fontSize: 18
},
heading5: {
...sharedStyles.textMedium,
fontSize: 16
},
heading6: {
...sharedStyles.textMedium,
fontSize: 14
},
quote: {
height: '100%',
width: 2,
backgroundColor: COLOR_BORDER,
marginRight: 5
},
touchableTable: {
justifyContent: 'center'
},
containerTable: {
borderBottomWidth: 1,
borderColor: COLOR_BORDER,
borderRightWidth: 1
},
table: {
borderColor: COLOR_BORDER,
borderLeftWidth: 1,
borderTopWidth: 1
},
tableExtraBorders: {
borderBottomWidth: 1,
borderRightWidth: 1
},
row: {
flexDirection: 'row'
},
rowBottomBorder: {
borderColor: COLOR_BORDER,
borderBottomWidth: 1
},
cell: {
borderColor: COLOR_BORDER,
justifyContent: 'flex-start',
paddingHorizontal: 13,
paddingVertical: 6
},
cellRightBorder: {
borderRightWidth: 1
},
alignCenter: {
textAlign: 'center'
},
alignRight: {
textAlign: 'right'
}
});

View File

@ -9,7 +9,7 @@ import moment from 'moment';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import Markdown from './Markdown'; import Markdown from '../markdown';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY } from '../../constants/colors'; import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY } from '../../constants/colors';

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import I18n from '../../i18n'; import I18n from '../../i18n';
import styles from './styles'; import styles from './styles';
import Markdown from './Markdown'; import Markdown from '../markdown';
import { getInfoMessage } from './utils'; import { getInfoMessage } from './utils';
const Content = React.memo((props) => { const Content = React.memo((props) => {
@ -21,13 +21,15 @@ const Content = React.memo((props) => {
<Markdown <Markdown
msg={props.msg} msg={props.msg}
baseUrl={props.baseUrl} baseUrl={props.baseUrl}
getCustomEmoji={props.getCustomEmoji}
username={props.user.username} username={props.user.username}
isEdited={props.isEdited} isEdited={props.isEdited}
mentions={props.mentions}
channels={props.channels}
numberOfLines={props.tmid ? 1 : 0} numberOfLines={props.tmid ? 1 : 0}
getCustomEmoji={props.getCustomEmoji} channels={props.channels}
useMarkdown={props.useMarkdown} mentions={props.mentions}
useMarkdown={props.useMarkdown && !props.tmid}
navToRoomInfo={props.navToRoomInfo}
tmid={props.tmid}
/> />
); );
} }
@ -42,15 +44,16 @@ const Content = React.memo((props) => {
Content.propTypes = { Content.propTypes = {
isTemp: PropTypes.bool, isTemp: PropTypes.bool,
isInfo: PropTypes.bool, isInfo: PropTypes.bool,
isEdited: PropTypes.bool,
useMarkdown: PropTypes.bool,
tmid: PropTypes.string, tmid: PropTypes.string,
msg: PropTypes.string, msg: PropTypes.string,
isEdited: PropTypes.bool,
useMarkdown: PropTypes.bool,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
user: PropTypes.object, user: PropTypes.object,
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), getCustomEmoji: PropTypes.func,
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
getCustomEmoji: PropTypes.func mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
navToRoomInfo: PropTypes.func
}; };
Content.displayName = 'MessageContent'; Content.displayName = 'MessageContent';

View File

@ -5,7 +5,7 @@ import FastImage from 'react-native-fast-image';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import Markdown from './Markdown'; import Markdown from '../markdown';
import styles from './styles'; import styles from './styles';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';

View File

@ -1,168 +0,0 @@
import React from 'react';
import { Text, Image } from 'react-native';
import PropTypes from 'prop-types';
import { emojify } from 'react-emojione';
import MarkdownRenderer, { PluginContainer } from 'react-native-markdown-renderer';
import MarkdownFlowdock from 'markdown-it-flowdock';
import styles from './styles';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import MarkdownEmojiPlugin from './MarkdownEmojiPlugin';
import I18n from '../../i18n';
const EmojiPlugin = new PluginContainer(MarkdownEmojiPlugin);
const MentionsPlugin = new PluginContainer(MarkdownFlowdock);
const plugins = [EmojiPlugin, MentionsPlugin];
// Support <http://link|Text>
const formatText = text => text.replace(
new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'),
(match, url, title) => `[${ title }](${ url })`
);
const emojiRanges = [
'\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]', // unicode emoji from https://www.regextester.com/106421
':.{1,40}:', // custom emoji
' |\n' // allow spaces and line breaks
].join('|');
const removeAllEmoji = str => str.replace(new RegExp(emojiRanges, 'g'), '');
const isOnlyEmoji = str => !removeAllEmoji(str).length;
const removeOneEmoji = str => str.replace(new RegExp(emojiRanges), '');
const emojiCount = (str) => {
let oldLength = 0;
let counter = 0;
while (oldLength !== str.length) {
oldLength = str.length;
str = removeOneEmoji(str);
if (oldLength !== str.length) {
counter += 1;
}
}
return counter;
};
const Markdown = React.memo(({
msg, style, rules, baseUrl, username, isEdited, numberOfLines, mentions, channels, getCustomEmoji, useMarkdown = true
}) => {
if (!msg) {
return null;
}
let m = formatText(msg);
if (m) {
m = emojify(m, { output: 'unicode' });
}
m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)\s/, '').trim();
if (numberOfLines > 0) {
m = m.replace(/[\n]+/g, '\n').trim();
}
if (!useMarkdown) {
return <Text style={styles.text} numberOfLines={numberOfLines}>{m}</Text>;
}
const isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;
return (
<MarkdownRenderer
rules={{
paragraph: (node, children) => (
<Text key={node.key} style={styles.paragraph} numberOfLines={numberOfLines}>
{children}
{isEdited ? <Text style={styles.edited}> ({I18n.t('edited')})</Text> : null}
</Text>
),
mention: (node) => {
const { content, key } = node;
let mentionStyle = styles.mention;
if (content === 'all' || content === 'here') {
mentionStyle = {
...mentionStyle,
...styles.mentionAll
};
} else if (content === username) {
mentionStyle = {
...mentionStyle,
...styles.mentionLoggedUser
};
}
if (mentions && mentions.length && mentions.findIndex(mention => mention.username === content) !== -1) {
return (
<Text style={mentionStyle} key={key}>
&nbsp;{content}&nbsp;
</Text>
);
}
return `@${ content }`;
},
hashtag: (node) => {
const { content, key } = node;
if (channels && channels.length && channels.findIndex(channel => channel.name === content) !== -1) {
return (
<Text key={key} style={styles.mention}>
&nbsp;#{content}&nbsp;
</Text>
);
}
return `#${ content }`;
},
emoji: (node) => {
if (node.children && node.children.length && node.children[0].content) {
const { content } = node.children[0];
const emoji = getCustomEmoji && getCustomEmoji(content);
if (emoji) {
return (
<CustomEmoji
key={node.key}
baseUrl={baseUrl}
style={isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji}
emoji={emoji}
/>
);
}
return <Text key={node.key}>:{content}:</Text>;
}
return null;
},
hardbreak: () => null,
blocklink: () => null,
image: node => (
<Image key={node.key} style={styles.inlineImage} source={{ uri: node.attributes.src }} />
),
...rules
}}
style={{
paragraph: styles.paragraph,
text: isMessageContainsOnlyEmoji ? styles.textBig : styles.text,
codeInline: styles.codeInline,
codeBlock: styles.codeBlock,
link: styles.link,
...style
}}
plugins={plugins}
>{m}
</MarkdownRenderer>
);
});
Markdown.propTypes = {
msg: PropTypes.string,
username: PropTypes.string,
baseUrl: PropTypes.string,
style: PropTypes.any,
rules: PropTypes.object,
isEdited: PropTypes.bool,
numberOfLines: PropTypes.number,
useMarkdown: PropTypes.bool,
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
getCustomEmoji: PropTypes.func
};
Markdown.displayName = 'MessageMarkdown';
export default Markdown;

View File

@ -1,78 +0,0 @@
export default function(md) {
function tokenize(state, silent) {
let token;
const start = state.pos;
const marker = state.src.charCodeAt(start);
if (silent) {
return false;
}
// :
if (marker !== 58) {
return false;
}
const scanned = state.scanDelims(state.pos, true);
const len = scanned.length;
const ch = String.fromCharCode(marker);
for (let i = 0; i < len; i += 1) {
token = state.push('text', '', 0);
token.content = ch;
state.delimiters.push({
marker,
jump: i,
token: state.tokens.length - 1,
level: state.level,
end: -1,
open: scanned.can_open,
close: scanned.can_close
});
}
state.pos += scanned.length;
return true;
}
function postProcess(state) {
let startDelim;
let endDelim;
let token;
const { delimiters } = state;
const max = delimiters.length;
for (let i = 0; i < max; i += 1) {
startDelim = delimiters[i];
// :
if (startDelim.marker !== 58) {
continue; // eslint-disable-line
}
if (startDelim.end === -1) {
continue; // eslint-disable-line
}
endDelim = delimiters[startDelim.end];
token = state.tokens[startDelim.token];
token.type = 'emoji_open';
token.tag = 'emoji';
token.nesting = 1;
token.markup = ':';
token.content = '';
token = state.tokens[endDelim.token];
token.type = 'emoji_close';
token.tag = 'emoji';
token.nesting = -1;
token.markup = ':';
token.content = '';
}
}
md.inline.ruler.before('emphasis', 'emoji', tokenize);
md.inline.ruler2.before('emphasis', 'emoji', postProcess);
}

View File

@ -1,24 +1,34 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { TouchableOpacity } from 'react-native';
import Avatar from '../Avatar'; import Avatar from '../Avatar';
import styles from './styles'; import styles from './styles';
const MessageAvatar = React.memo(({ const MessageAvatar = React.memo(({
isHeader, avatar, author, baseUrl, user, small isHeader, avatar, author, baseUrl, user, small, navToRoomInfo
}) => { }) => {
if (isHeader) { if (isHeader) {
const navParam = {
t: 'd',
rid: author._id
};
return ( return (
<Avatar <TouchableOpacity
style={small ? styles.avatarSmall : styles.avatar} onPress={() => navToRoomInfo(navParam)}
text={avatar ? '' : author.username} disabled={author._id === user.id}
size={small ? 20 : 36} >
borderRadius={small ? 2 : 4} <Avatar
avatar={avatar} style={small ? styles.avatarSmall : styles.avatar}
baseUrl={baseUrl} text={avatar ? '' : author.username}
userId={user.id} size={small ? 20 : 36}
token={user.token} borderRadius={small ? 2 : 4}
/> avatar={avatar}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/>
</TouchableOpacity>
); );
} }
return null; return null;
@ -30,7 +40,8 @@ MessageAvatar.propTypes = {
author: PropTypes.obj, author: PropTypes.obj,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
user: PropTypes.obj, user: PropTypes.obj,
small: PropTypes.bool small: PropTypes.bool,
navToRoomInfo: PropTypes.func
}; };
MessageAvatar.displayName = 'MessageAvatar'; MessageAvatar.displayName = 'MessageAvatar';

View File

@ -5,7 +5,7 @@ import moment from 'moment';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal'; import isEqual from 'deep-equal';
import Markdown from './Markdown'; import Markdown from '../markdown';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER } from '../../constants/colors'; import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER } from '../../constants/colors';

View File

@ -4,13 +4,13 @@ import { StyleSheet } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal'; import isEqual from 'deep-equal';
import Markdown from './Markdown'; import Markdown from '../markdown';
import openLink from '../../utils/openLink'; 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';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/webm', 'video/3gp', 'video/mkv'])]; const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1; const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -39,7 +39,8 @@ export default class MessageContainer extends React.Component {
toggleReactionPicker: PropTypes.func, toggleReactionPicker: PropTypes.func,
fetchThreadName: PropTypes.func, fetchThreadName: PropTypes.func,
onOpenFileModal: PropTypes.func, onOpenFileModal: PropTypes.func,
onReactionLongPress: PropTypes.func onReactionLongPress: PropTypes.func,
navToRoomInfo: PropTypes.func
} }
static defaultProps = { static defaultProps = {
@ -199,7 +200,7 @@ export default class MessageContainer extends React.Component {
render() { render() {
const { const {
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo
} = this.props; } = this.props;
const { const {
_id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, autoTranslate: autoTranslateMessage _id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, autoTranslate: autoTranslateMessage
@ -263,6 +264,7 @@ export default class MessageContainer extends React.Component {
onDiscussionPress={this.onDiscussionPress} onDiscussionPress={this.onDiscussionPress}
onOpenFileModal={onOpenFileModal} onOpenFileModal={onOpenFileModal}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo}
/> />
); );
} }

View File

@ -1,15 +1,10 @@
import { StyleSheet, Platform } from 'react-native'; import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { import {
COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE
} from '../../constants/colors'; } from '../../constants/colors';
const codeFontFamily = Platform.select({
ios: { fontFamily: 'Courier New' },
android: { fontFamily: 'monospace' }
});
export default StyleSheet.create({ export default StyleSheet.create({
root: { root: {
flexDirection: 'row' flexDirection: 'row'
@ -34,30 +29,6 @@ export default StyleSheet.create({
flexDirection: 'row' flexDirection: 'row'
// flex: 1 // flex: 1
}, },
text: {
fontSize: 16,
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
},
textBig: {
fontSize: 30,
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
},
textInfo: {
fontStyle: 'italic',
fontSize: 16,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
customEmoji: {
width: 20,
height: 20
},
customEmojiBig: {
width: 30,
height: 30
},
temp: { opacity: 0.3 }, temp: { opacity: 0.3 },
marginTop: { marginTop: {
marginTop: 6 marginTop: 6
@ -143,28 +114,6 @@ export default StyleSheet.create({
fontSize: 14, fontSize: 14,
...sharedStyles.textMedium ...sharedStyles.textMedium
}, },
mention: {
...sharedStyles.textMedium,
color: '#0072FE',
padding: 5,
backgroundColor: '#E8F2FF'
},
mentionLoggedUser: {
color: COLOR_WHITE,
backgroundColor: COLOR_PRIMARY
},
mentionAll: {
color: COLOR_WHITE,
backgroundColor: '#FF5B5A'
},
paragraph: {
marginTop: 0,
marginBottom: 0,
flexWrap: 'wrap',
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'flex-start'
},
imageContainer: { imageContainer: {
// flex: 1, // flex: 1,
flexDirection: 'column', flexDirection: 'column',
@ -186,29 +135,15 @@ export default StyleSheet.create({
height: 300, height: 300,
resizeMode: 'contain' resizeMode: 'contain'
}, },
edited: { text: {
fontSize: 14, fontSize: 16,
...sharedStyles.textColorDescription, ...sharedStyles.textColorNormal,
...sharedStyles.textRegular ...sharedStyles.textRegular
}, },
codeInline: { textInfo: {
...sharedStyles.textRegular, fontStyle: 'italic',
...codeFontFamily, fontSize: 16,
borderWidth: 1, ...sharedStyles.textColorDescription,
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderRadius: 4
},
codeBlock: {
...sharedStyles.textRegular,
...codeFontFamily,
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderColor: COLOR_BORDER,
borderWidth: 1,
borderRadius: 4,
padding: 4
},
link: {
color: COLOR_PRIMARY,
...sharedStyles.textRegular ...sharedStyles.textRegular
}, },
startedDiscussion: { startedDiscussion: {

View File

@ -87,17 +87,20 @@ export default {
alerts: 'alerts', alerts: 'alerts',
All_users_in_the_channel_can_write_new_messages: 'All users in the channel can write new messages', All_users_in_the_channel_can_write_new_messages: 'All users in the channel can write new messages',
All: 'All', All: 'All',
All_Messages: 'All Messages',
Allow_Reactions: 'Allow Reactions', Allow_Reactions: 'Allow Reactions',
Alphabetical: 'Alphabetical', Alphabetical: 'Alphabetical',
and_more: 'and more', and_more: 'and more',
and: 'and', and: 'and',
announcement: 'announcement', announcement: 'announcement',
Announcement: 'Announcement', Announcement: 'Announcement',
Apply_Your_Certificate: 'Apply Your Certificate',
ARCHIVE: 'ARCHIVE', ARCHIVE: 'ARCHIVE',
archive: 'archive', archive: 'archive',
are_typing: 'are typing', are_typing: 'are typing',
Are_you_sure_question_mark: 'Are you sure?', Are_you_sure_question_mark: 'Are you sure?',
Are_you_sure_you_want_to_leave_the_room: 'Are you sure you want to leave the room {{room}}?', Are_you_sure_you_want_to_leave_the_room: 'Are you sure you want to leave the room {{room}}?',
Audio: 'Audio',
Authenticating: 'Authenticating', Authenticating: 'Authenticating',
Auto_Translate: 'Auto-Translate', Auto_Translate: 'Auto-Translate',
Avatar_changed_successfully: 'Avatar changed successfully!', Avatar_changed_successfully: 'Avatar changed successfully!',
@ -135,28 +138,34 @@ export default {
Copied_to_clipboard: 'Copied to clipboard!', Copied_to_clipboard: 'Copied to clipboard!',
Copy: 'Copy', Copy: 'Copy',
Permalink: 'Permalink', Permalink: 'Permalink',
Certificate_password: 'Certificate Password',
Whats_the_password_for_your_certificate: 'What\'s the password for your certificate?',
Create_account: 'Create an account', Create_account: 'Create an account',
Create_Channel: 'Create Channel', Create_Channel: 'Create Channel',
Created_snippet: 'Created a snippet', Created_snippet: 'Created a snippet',
Create_a_new_workspace: 'Create a new workspace', Create_a_new_workspace: 'Create a new workspace',
Create: 'Create', Create: 'Create',
Default: 'Default',
Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.', Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.',
delete: 'delete', delete: 'delete',
Delete: 'Delete', Delete: 'Delete',
DELETE: 'DELETE', DELETE: 'DELETE',
description: 'description', description: 'description',
Description: 'Description', Description: 'Description',
DESKTOP_OPTIONS: 'DESKTOP OPTIONS',
Directory: 'Directory', Directory: 'Directory',
Direct_Messages: 'Direct Messages', Direct_Messages: 'Direct Messages',
Disable_notifications: 'Disable notifications', Disable_notifications: 'Disable notifications',
Discussions: 'Discussions', Discussions: 'Discussions',
Dont_Have_An_Account: 'Don\'t have an account?', Dont_Have_An_Account: 'Don\'t have an account?',
Do_you_have_a_certificate: 'Do you have a certificate?',
Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?', Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?',
edit: 'edit', edit: 'edit',
edited: 'edited', edited: 'edited',
Edit: 'Edit', Edit: 'Edit',
Email_or_password_field_is_empty: 'Email or password field is empty', Email_or_password_field_is_empty: 'Email or password field is empty',
Email: 'Email', Email: 'Email',
EMAIL: 'EMAIL',
email: 'e-mail', email: 'e-mail',
Enable_Auto_Translate: 'Enable Auto-Translate', Enable_Auto_Translate: 'Enable Auto-Translate',
Enable_markdown: 'Enable markdown', Enable_markdown: 'Enable markdown',
@ -176,12 +185,15 @@ export default {
Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.', Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.',
Forgot_password: 'Forgot password', Forgot_password: 'Forgot password',
Forgot_Password: 'Forgot Password', Forgot_Password: 'Forgot Password',
Full_table: 'Click to see full table',
Group_by_favorites: 'Group favorites', Group_by_favorites: 'Group favorites',
Group_by_type: 'Group by type', Group_by_type: 'Group by type',
Hide: 'Hide', Hide: 'Hide',
Has_joined_the_channel: 'Has joined the channel', Has_joined_the_channel: 'Has joined the channel',
Has_joined_the_conversation: 'Has joined the conversation', Has_joined_the_conversation: 'Has joined the conversation',
Has_left_the_channel: 'Has left the channel', Has_left_the_channel: 'Has left the channel',
IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP',
In_App_and_Desktop_Alert_info: 'Displays a banner at the top of the screen when app is open, and displays a notification on desktop',
Invisible: 'Invisible', Invisible: 'Invisible',
Invite: 'Invite', Invite: 'Invite',
is_a_valid_RocketChat_instance: 'is a valid Rocket.Chat instance', is_a_valid_RocketChat_instance: 'is a valid Rocket.Chat instance',
@ -243,9 +255,13 @@ export default {
No_Reactions: 'No Reactions', No_Reactions: 'No Reactions',
No_Read_Receipts: 'No Read Receipts', No_Read_Receipts: 'No Read Receipts',
Not_logged: 'Not logged', Not_logged: 'Not logged',
Nothing: 'Nothing',
Nothing_to_save: 'Nothing to save!', Nothing_to_save: 'Nothing to save!',
Notify_active_in_this_room: 'Notify active users in this room', Notify_active_in_this_room: 'Notify active users in this room',
Notify_all_in_this_room: 'Notify all in this room', Notify_all_in_this_room: 'Notify all in this room',
Notifications: 'Notifications',
Notification_Duration: 'Notification Duration',
Notification_Preferences: 'Notification Preferences',
Offline: 'Offline', Offline: 'Offline',
Oops: 'Oops!', Oops: 'Oops!',
Online: 'Online', Online: 'Online',
@ -269,6 +285,8 @@ export default {
Profile: 'Profile', Profile: 'Profile',
Public_Channel: 'Public Channel', Public_Channel: 'Public Channel',
Public: 'Public', Public: 'Public',
PUSH_NOTIFICATIONS: 'PUSH NOTIFICATIONS',
Push_Notifications_Alert_Info: 'These notifications are delivered to you when the app is not open',
Quote: 'Quote', Quote: 'Quote',
Reactions_are_disabled: 'Reactions are disabled', Reactions_are_disabled: 'Reactions are disabled',
Reactions_are_enabled: 'Reactions are enabled', Reactions_are_enabled: 'Reactions are enabled',
@ -277,6 +295,8 @@ export default {
Read_Only_Channel: 'Read Only Channel', Read_Only_Channel: 'Read Only Channel',
Read_Only: 'Read Only', Read_Only: 'Read Only',
Read_Receipt: 'Read Receipt', Read_Receipt: 'Read Receipt',
Receive_Group_Mentions: 'Receive Group Mentions',
Receive_Group_Mentions_Info: 'Receive @all and @here mentions',
Register: 'Register', Register: 'Register',
Repeat_Password: 'Repeat Password', Repeat_Password: 'Repeat Password',
Replied_on: 'Replied on:', Replied_on: 'Replied on:',
@ -284,6 +304,8 @@ export default {
reply: 'reply', reply: 'reply',
Reply: 'Reply', Reply: 'Reply',
Report: 'Report', Report: 'Report',
Receive_Notification: 'Receive Notification',
Receive_notifications_from: 'Receive notifications from {{name}}',
Resend: 'Resend', Resend: 'Resend',
Reset_password: 'Reset password', Reset_password: 'Reset password',
resetting_password: 'resetting password', resetting_password: 'resetting password',
@ -310,6 +332,7 @@ export default {
Search_by: 'Search by', Search_by: 'Search by',
Search_global_users: 'Search for global users', Search_global_users: 'Search for global users',
Search_global_users_description: 'If you turn-on, you can search for any user from others companies or servers.', Search_global_users_description: 'If you turn-on, you can search for any user from others companies or servers.',
Seconds: '{{second}} seconds',
Select_Avatar: 'Select Avatar', Select_Avatar: 'Select Avatar',
Select_Server: 'Select Server', Select_Server: 'Select Server',
Select_Users: 'Select Users', Select_Users: 'Select Users',
@ -327,10 +350,13 @@ export default {
Settings_succesfully_changed: 'Settings succesfully changed!', Settings_succesfully_changed: 'Settings succesfully changed!',
Share: 'Share', Share: 'Share',
Share_this_app: 'Share this app', Share_this_app: 'Share this app',
Show_Unread_Counter: 'Show Unread Counter',
Show_Unread_Counter_Info: 'Unread counter is displayed as a badge on the right of the channel, in the list',
Sign_in_your_server: 'Sign in your server', Sign_in_your_server: 'Sign in your server',
Sign_Up: 'Sign Up', Sign_Up: 'Sign Up',
Some_field_is_invalid_or_empty: 'Some field is invalid or empty', Some_field_is_invalid_or_empty: 'Some field is invalid or empty',
Sorting_by: 'Sorting by {{key}}', Sorting_by: 'Sorting by {{key}}',
Sound: 'Sound',
Star_room: 'Star room', Star_room: 'Star room',
Star: 'Star', Star: 'Star',
Starred_Messages: 'Starred Messages', Starred_Messages: 'Starred Messages',
@ -339,6 +365,7 @@ export default {
Start_of_conversation: 'Start of conversation', Start_of_conversation: 'Start of conversation',
Started_discussion: 'Started a discussion:', Started_discussion: 'Started a discussion:',
Submit: 'Submit', Submit: 'Submit',
Table: 'Table',
Take_a_photo: 'Take a photo', Take_a_photo: 'Take a photo',
Take_a_video: 'Take a video', Take_a_video: 'Take a video',
tap_to_change_status: 'tap to change status', tap_to_change_status: 'tap to change status',
@ -404,8 +431,9 @@ export default {
you: 'you', you: 'you',
You: 'You', You: 'You',
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'You need to access at least one Rocket.Chat server to share something.', You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'You need to access at least one Rocket.Chat server to share something.',
Your_certificate: 'Your Certificate',
Version_no: 'Version: {{version}}', Version_no: 'Version: {{version}}',
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!', You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
Change_Language: 'Change Language', Change_Language: 'Change Language',
Crash_report_disclaimer: 'We never track the content of your chats. The crash report only contains relevant information for us in order ' Crash_report_disclaimer: 'We never track the content of your chats. The crash report only contains relevant information for us in order to identify problems and fix it.'
}; };

View File

@ -178,6 +178,7 @@ export default {
Forgot_password_If_this_email_is_registered: 'Se este e-mail estiver cadastrado, enviaremos instruções sobre como redefinir sua senha. Se você não receber um e-mail em breve, volte e tente novamente.', Forgot_password_If_this_email_is_registered: 'Se este e-mail estiver cadastrado, enviaremos instruções sobre como redefinir sua senha. Se você não receber um e-mail em breve, volte e tente novamente.',
Forgot_password: 'Esqueci minha senha', Forgot_password: 'Esqueci minha senha',
Forgot_Password: 'Esqueci minha senha', Forgot_Password: 'Esqueci minha senha',
Full_table: 'Clique para ver a tabela completa',
Group_by_favorites: 'Agrupar favoritos', Group_by_favorites: 'Agrupar favoritos',
Group_by_type: 'Agrupar por tipo', Group_by_type: 'Agrupar por tipo',
Has_joined_the_channel: 'Entrou no canal', Has_joined_the_channel: 'Entrou no canal',
@ -326,6 +327,7 @@ export default {
Start_of_conversation: 'Início da conversa', Start_of_conversation: 'Início da conversa',
Started_discussion: 'Iniciou uma discussão:', Started_discussion: 'Iniciou uma discussão:',
Submit: 'Enviar', Submit: 'Enviar',
Table: 'Tabela',
Take_a_photo: 'Tirar uma foto', Take_a_photo: 'Tirar uma foto',
Take_a_video: 'Gravar um vídeo', Take_a_video: 'Gravar um vídeo',
Terms_of_Service: ' Termos de Serviço ', Terms_of_Service: ' Termos de Serviço ',

View File

@ -16,7 +16,9 @@ import { initializePushNotifications, onNotification } from './notifications/pus
import store from './lib/createStore'; import store from './lib/createStore';
import NotificationBadge from './notifications/inApp'; import NotificationBadge from './notifications/inApp';
import { defaultHeader, onNavigationStateChange } from './utils/navigation'; import { defaultHeader, onNavigationStateChange } from './utils/navigation';
import { loggerConfig, analytics } from './utils/log';
import Toast from './containers/Toast'; import Toast from './containers/Toast';
import RocketChat from './lib/rocketchat';
useScreens(); useScreens();
@ -119,6 +121,12 @@ const ChatsStack = createStackNavigator({
}, },
DirectoryView: { DirectoryView: {
getScreen: () => require('./views/DirectoryView').default getScreen: () => require('./views/DirectoryView').default
},
TableView: {
getScreen: () => require('./views/TableView').default
},
NotificationPrefView: {
getScreen: () => require('./views/NotificationPreferencesView').default
} }
}, { }, {
defaultNavigationOptions: defaultHeader defaultNavigationOptions: defaultHeader
@ -256,6 +264,7 @@ export default class Root extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.init(); this.init();
this.initCrashReport();
} }
componentDidMount() { componentDidMount() {
@ -285,6 +294,17 @@ export default class Root extends React.Component {
} }
} }
initCrashReport = () => {
RocketChat.getAllowCrashReport()
.then((allowCrashReport) => {
if (!allowCrashReport) {
loggerConfig.autoNotify = false;
loggerConfig.registerBeforeSendCallback(() => false);
analytics().setAnalyticsCollectionEnabled(false);
}
});
}
render() { render() {
return ( return (
<Provider store={store}> <Provider store={store}>

View File

@ -68,7 +68,7 @@ export default function() {
database.delete(emojiRecord); database.delete(emojiRecord);
} }
} catch (e) { } catch (e) {
log('err_get_emojis_delete', e); log(e);
} }
}); });
} }
@ -77,7 +77,7 @@ export default function() {
); );
} }
} catch (e) { } catch (e) {
log('err_get_custom_emojis', e); log(e);
return resolve(); return resolve();
} }
}); });

View File

@ -16,7 +16,7 @@ const create = (permissions) => {
try { try {
database.create('permissions', permission, true); database.create('permissions', permission, true);
} catch (e) { } catch (e) {
log('err_get_permissions_create', e); log(e);
} }
}); });
} }
@ -65,7 +65,7 @@ export default function() {
database.delete(permission); database.delete(permission);
} }
} catch (e) { } catch (e) {
log('err_get_permissions_delete', e); log(e);
} }
}); });
} }
@ -74,7 +74,7 @@ export default function() {
); );
} }
} catch (e) { } catch (e) {
log('err_get_permissions', e); log(e);
return resolve(); return resolve();
} }
}); });

View File

@ -21,14 +21,14 @@ export default function() {
try { try {
database.create('roles', role, true); database.create('roles', role, true);
} catch (e) { } catch (e) {
log('err_get_roles_create', e); log(e);
} }
})); }));
return resolve(); return resolve();
}); });
} }
} catch (e) { } catch (e) {
log('err_get_roles', e); log(e);
return resolve(); return resolve();
} }
}); });

View File

@ -11,7 +11,7 @@ function updateServer(param) {
try { try {
database.databases.serversDB.create('servers', { id: reduxStore.getState().server.server, ...param }, true); database.databases.serversDB.create('servers', { id: reduxStore.getState().server.server, ...param }, true);
} catch (e) { } catch (e) {
log('err_get_settings_update_server', e); log(e);
} }
}); });
} }
@ -34,7 +34,7 @@ export default async function() {
try { try {
database.create('settings', { ...setting, _updatedAt: new Date() }, true); database.create('settings', { ...setting, _updatedAt: new Date() }, true);
} catch (e) { } catch (e) {
log('err_get_settings_create', e); log(e);
} }
if (setting._id === 'Site_Name') { if (setting._id === 'Site_Name') {
@ -61,6 +61,6 @@ export default async function() {
updateServer.call(this, { iconURL }); updateServer.call(this, { iconURL });
} }
} catch (e) { } catch (e) {
log('err_get_settings', e); log(e);
} }
} }

View File

@ -10,7 +10,7 @@ export default function() {
const result = await this.sdk.get('commands.list'); const result = await this.sdk.get('commands.list');
if (!result.success) { if (!result.success) {
log('getSlashCommand fetch', result); console.log(result);
return resolve(); return resolve();
} }
@ -22,14 +22,14 @@ export default function() {
try { try {
database.create('slashCommand', command, true); database.create('slashCommand', command, true);
} catch (e) { } catch (e) {
log('get_slash_command', e); log(e);
} }
})); }));
return resolve(); return resolve();
}); });
} }
} catch (e) { } catch (e) {
log('err_get_slash_command', e); log(e);
return resolve(); return resolve();
} }
}); });

View File

@ -34,12 +34,6 @@ export const merge = (subscription, room) => {
} }
} }
if (subscription.mobilePushNotifications === 'nothing') {
subscription.notifications = true;
} else {
subscription.notifications = false;
}
if (!subscription.name) { if (!subscription.name) {
subscription.name = subscription.fname; subscription.name = subscription.fname;
} }

View File

@ -13,8 +13,8 @@ async function load({ rid: roomId, latest, t }) {
return []; return [];
} }
return data.messages; return data.messages;
} catch (error) { } catch (e) {
console.log(error); log(e);
return []; return [];
} }
} }
@ -52,7 +52,7 @@ export default function loadMessagesForRoom(...args) {
database.create('threadMessages', message, true); database.create('threadMessages', message, true);
} }
} catch (e) { } catch (e) {
log('err_load_messages_for_room_create', e); log(e);
} }
})); }));
return resolve(data); return resolve(data);
@ -61,7 +61,7 @@ export default function loadMessagesForRoom(...args) {
return resolve([]); return resolve([]);
} }
} catch (e) { } catch (e) {
log('err_load_messages_for_room', e); log(e);
reject(e); reject(e);
} }
}); });

View File

@ -45,7 +45,7 @@ export default function loadMissedMessages(...args) {
database.create('threadMessages', message, true); database.create('threadMessages', message, true);
} }
} catch (e) { } catch (e) {
log('err_load_missed_messages_create', e); log(e);
} }
})); }));
}); });
@ -65,14 +65,14 @@ export default function loadMissedMessages(...args) {
}); });
}); });
} catch (e) { } catch (e) {
log('err_load_missed_messages_delete', e); log(e);
} }
}); });
} }
} }
resolve(); resolve();
} catch (e) { } catch (e) {
log('err_load_missed_messages', e); log(e);
reject(e); reject(e);
} }
}); });

View File

@ -34,7 +34,7 @@ export default function loadThreadMessages({ tmid, offset = 0 }) {
message.rid = tmid; message.rid = tmid;
database.create('threadMessages', message, true); database.create('threadMessages', message, true);
} catch (e) { } catch (e) {
log('err_load_thread_messages_create', e); log(e);
} }
})); }));
return resolve(data); return resolve(data);
@ -43,7 +43,7 @@ export default function loadThreadMessages({ tmid, offset = 0 }) {
return resolve([]); return resolve([]);
} }
} catch (e) { } catch (e) {
log('err_load_thread_messages', e); log(e);
reject(e); reject(e);
} }
}); });

View File

@ -18,6 +18,6 @@ export default async function readMessages(rid) {
}); });
return data; return data;
} catch (e) { } catch (e) {
log('err_read_messages', e); log(e);
} }
} }

View File

@ -15,7 +15,7 @@ export function cancelUpload(path) {
try { try {
database.delete(upload); database.delete(upload);
} catch (e) { } catch (e) {
log('err_send_file_message_delete_upload', e); log(e);
} }
}); });
delete uploadQueue[path]; delete uploadQueue[path];
@ -45,7 +45,7 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
try { try {
database.create('uploads', fileInfo, true); database.create('uploads', fileInfo, true);
} catch (e) { } catch (e) {
return log('err_send_file_message_create_upload_1', e); return log(e);
} }
}); });
@ -75,7 +75,7 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
try { try {
database.create('uploads', fileInfo, true); database.create('uploads', fileInfo, true);
} catch (e) { } catch (e) {
return log('err_send_file_message_create_upload_2', e); return log(e);
} }
}); });
}; };
@ -90,7 +90,7 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
resolve(response); resolve(response);
} catch (e) { } catch (e) {
reject(e); reject(e);
log('err_send_file_message_delete_upload', e); log(e);
} }
}); });
} else { } else {
@ -100,30 +100,30 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
database.create('uploads', fileInfo, true); database.create('uploads', fileInfo, true);
const response = JSON.parse(xhr.response); const response = JSON.parse(xhr.response);
reject(response); reject(response);
} catch (err) { } catch (e) {
reject(err); reject(e);
log('err_send_file_message_create_upload_3', err); log(e);
} }
}); });
} }
}; };
xhr.onerror = (e) => { xhr.onerror = (error) => {
database.write(() => { database.write(() => {
fileInfo.error = true; fileInfo.error = true;
try { try {
database.create('uploads', fileInfo, true); database.create('uploads', fileInfo, true);
reject(error);
} catch (e) {
reject(e); reject(e);
} catch (err) { log(e);
reject(err);
log('err_send_file_message_create_upload_3', err);
} }
}); });
}; };
xhr.send(formData); xhr.send(formData);
} catch (err) { } catch (e) {
log('err_send_file_message_create_upload_4', err); log(e);
} }
}); });
} }

View File

@ -66,6 +66,6 @@ export default async function(rid, msg, tmid, user) {
}); });
} }
} catch (e) { } catch (e) {
log('err_send_message', e); log(e);
} }
} }

View File

@ -38,8 +38,8 @@ export default function subscribeRoom({ rid }) {
clearTimeout(typingTimeouts[username]); clearTimeout(typingTimeouts[username]);
typingTimeouts[username] = null; typingTimeouts[username] = null;
} }
} catch (error) { } catch (e) {
log('err_remove_user_typing', error); log(e);
} }
}; };
@ -60,8 +60,8 @@ export default function subscribeRoom({ rid }) {
typingTimeouts[username] = setTimeout(() => { typingTimeouts[username] = setTimeout(() => {
removeUserTyping(username); removeUserTyping(username);
}, 10000); }, 10000);
} catch (error) { } catch (e) {
log('err_add_user_typing', error); log(e);
} }
} }
}; };
@ -172,7 +172,7 @@ export default function subscribeRoom({ rid }) {
try { try {
promises = this.sdk.subscribeRoom(rid); promises = this.sdk.subscribeRoom(rid);
} catch (e) { } catch (e) {
log('err_subscribe_room', e); log(e);
} }
return { return {

View File

@ -44,7 +44,7 @@ export default function subscribeRooms() {
database.delete(subscription); database.delete(subscription);
}); });
} catch (e) { } catch (e) {
log('err_stream_msg_received_sub_removed', e); log(e);
} }
} else { } else {
const rooms = database.objects('rooms').filtered('_id == $0', data.rid); const rooms = database.objects('rooms').filtered('_id == $0', data.rid);
@ -55,7 +55,7 @@ export default function subscribeRooms() {
database.delete(rooms); database.delete(rooms);
}); });
} catch (e) { } catch (e) {
log('err_stream_msg_received_sub_updated', e); log(e);
} }
} }
} }
@ -68,7 +68,7 @@ export default function subscribeRooms() {
database.create('subscriptions', tmp, true); database.create('subscriptions', tmp, true);
}); });
} catch (e) { } catch (e) {
log('err_stream_msg_received_room_updated', e); log(e);
} }
} else if (type === 'inserted') { } else if (type === 'inserted') {
try { try {
@ -76,7 +76,7 @@ export default function subscribeRooms() {
database.create('rooms', data, true); database.create('rooms', data, true);
}); });
} catch (e) { } catch (e) {
log('err_stream_msg_received_room_inserted', e); log(e);
} }
} }
} }
@ -101,7 +101,7 @@ export default function subscribeRooms() {
database.create('messages', message, true); database.create('messages', message, true);
}); });
} catch (e) { } catch (e) {
log('err_stream_msg_received_message', e); log(e);
} }
}); });
} }
@ -139,7 +139,7 @@ export default function subscribeRooms() {
stop: () => stop() stop: () => stop()
}; };
} catch (e) { } catch (e) {
log('err_subscribe_rooms', e); log(e);
return Promise.reject(); return Promise.reject();
} }
} }

View File

@ -95,14 +95,23 @@ const subscriptionSchema = {
reactWhenReadOnly: { type: 'bool', optional: true }, reactWhenReadOnly: { type: 'bool', optional: true },
archived: { type: 'bool', optional: true }, archived: { type: 'bool', optional: true },
joinCodeRequired: { type: 'bool', optional: true }, joinCodeRequired: { type: 'bool', optional: true },
notifications: { type: 'bool', optional: true },
muted: 'string[]', muted: 'string[]',
broadcast: { type: 'bool', optional: true }, broadcast: { type: 'bool', optional: true },
prid: { type: 'string', optional: true }, prid: { type: 'string', optional: true },
draftMessage: { type: 'string', optional: true }, draftMessage: { type: 'string', optional: true },
lastThreadSync: 'date?', lastThreadSync: 'date?',
autoTranslate: 'bool?', autoTranslate: 'bool?',
autoTranslateLanguage: 'string?' autoTranslateLanguage: 'string?',
// Notifications
emailNotifications: { type: 'string', default: 'default' },
disableNotifications: { type: 'bool', default: false },
muteGroupMentions: { type: 'bool', default: false },
hideUnreadStatus: { type: 'bool', default: false },
audioNotifications: { type: 'string', default: 'default' },
desktopNotifications: { type: 'string', default: 'default' },
audioNotificationValue: { type: 'string', default: '0 Default' },
desktopNotificationDuration: { type: 'int', default: 0 },
mobilePushNotifications: { type: 'string', default: 'default' }
} }
}; };
@ -474,7 +483,7 @@ class DB {
return this.databases.activeDB = new Realm({ return this.databases.activeDB = new Realm({
path: `${ RNRealmPath.realmPath }${ path }.realm`, path: `${ RNRealmPath.realmPath }${ path }.realm`,
schema, schema,
schemaVersion: 13, schemaVersion: 14,
migration: (oldRealm, newRealm) => { migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 13) { if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 13) {
const newSubs = newRealm.objects('subscriptions'); const newSubs = newRealm.objects('subscriptions');

View File

@ -2,6 +2,7 @@ import { AsyncStorage, InteractionManager } from 'react-native';
import semver from 'semver'; import semver from 'semver';
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import RNUserDefaults from 'rn-user-defaults'; import RNUserDefaults from 'rn-user-defaults';
import * as FileSystem from 'expo-file-system';
import reduxStore from './createStore'; import reduxStore from './createStore';
import defaultSettings from '../constants/settings'; import defaultSettings from '../constants/settings';
@ -9,6 +10,7 @@ import messagesStatus from '../constants/messagesStatus';
import database from './realm'; import database from './realm';
import log from '../utils/log'; import log from '../utils/log';
import { isIOS, getBundleId } from '../utils/deviceInfo'; import { isIOS, getBundleId } from '../utils/deviceInfo';
import { extractHostname } from '../utils/server';
import { import {
setUser, setLoginServices, loginRequest, loginFailure, logout setUser, setLoginServices, loginRequest, loginFailure, logout
@ -45,6 +47,7 @@ import { SERVERS, SERVER_URL } from '../constants/userDefaults';
const TOKEN_KEY = 'reactnativemeteor_usertoken'; const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY'; const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
export const MARKDOWN_KEY = 'RC_MARKDOWN_KEY'; export const MARKDOWN_KEY = 'RC_MARKDOWN_KEY';
export const CRASH_REPORT_KEY = 'RC_CRASH_REPORT_KEY';
const returnAnArray = obj => obj || []; const returnAnArray = obj => obj || [];
const MIN_ROCKETCHAT_VERSION = '0.70.0'; const MIN_ROCKETCHAT_VERSION = '0.70.0';
@ -52,7 +55,12 @@ const STATUSES = ['offline', 'online', 'away', 'busy'];
const RocketChat = { const RocketChat = {
TOKEN_KEY, TOKEN_KEY,
subscribeRooms, async subscribeRooms() {
if (this.roomsSub) {
this.roomsSub.stop();
}
this.roomsSub = await subscribeRooms.call(this);
},
subscribeRoom, subscribeRoom,
canOpenRoom, canOpenRoom,
createChannel({ createChannel({
@ -85,7 +93,7 @@ const RocketChat = {
return result; return result;
} }
} catch (e) { } catch (e) {
log('err_get_server_info', e); log(e);
} }
return { return {
success: false, success: false,
@ -358,6 +366,12 @@ const RocketChat = {
try { try {
const servers = await RNUserDefaults.objectForKey(SERVERS); const servers = await RNUserDefaults.objectForKey(SERVERS);
await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server)); await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server));
// clear certificate for server - SSL Pinning
const certificate = await RNUserDefaults.objectForKey(extractHostname(server));
if (certificate && certificate.path) {
await RNUserDefaults.clear(extractHostname(server));
await FileSystem.deleteAsync(certificate.path);
}
} catch (error) { } catch (error) {
console.log('logout_rn_user_defaults', error); console.log('logout_rn_user_defaults', error);
} }
@ -433,7 +447,7 @@ const RocketChat = {
database.create('messages', message, true); database.create('messages', message, true);
}); });
} catch (e) { } catch (e) {
log('err_resend_message', e); log(e);
} }
} }
}, },
@ -564,7 +578,7 @@ const RocketChat = {
try { try {
room = await RocketChat.getRoom(message.rid); room = await RocketChat.getRoom(message.rid);
} catch (e) { } catch (e) {
log('err_get_permalink', e); log(e);
return null; return null;
} }
const { server } = reduxStore.getState().server; const { server } = reduxStore.getState().server;
@ -640,6 +654,10 @@ const RocketChat = {
// RC 0.48.0 // RC 0.48.0
return this.sdk.get('users.info', { userId }); return this.sdk.get('users.info', { userId });
}, },
getRoomInfo(roomId) {
// RC 0.72.0
return this.sdk.get('rooms.info', { roomId });
},
getRoomMemberId(rid, currentUserId) { getRoomMemberId(rid, currentUserId) {
if (rid === `${ currentUserId }${ currentUserId }`) { if (rid === `${ currentUserId }${ currentUserId }`) {
return currentUserId; return currentUserId;
@ -761,6 +779,13 @@ const RocketChat = {
} }
return JSON.parse(useMarkdown); return JSON.parse(useMarkdown);
}, },
async getAllowCrashReport() {
const allowCrashReport = await AsyncStorage.getItem(CRASH_REPORT_KEY);
if (allowCrashReport === null) {
return true;
}
return JSON.parse(allowCrashReport);
},
async getSortPreferences() { async getSortPreferences() {
const prefs = await RNUserDefaults.objectForKey(SORT_PREFS_KEY); const prefs = await RNUserDefaults.objectForKey(SORT_PREFS_KEY);
return prefs; return prefs;
@ -802,9 +827,11 @@ const RocketChat = {
} }
}, },
_determineAuthType(services) { _determineAuthType(services) {
const { name, custom, service } = services; const {
name, custom, showButton = true, service
} = services;
if (custom) { if (custom && showButton) {
return 'oauth_custom'; return 'oauth_custom';
} }
@ -957,8 +984,8 @@ const RocketChat = {
const autoTranslatePermission = database.objectForPrimaryKey('permissions', 'auto-translate'); const autoTranslatePermission = database.objectForPrimaryKey('permissions', 'auto-translate');
const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || []; const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || [];
return autoTranslatePermission.roles.some(role => userRoles.includes(role)); return autoTranslatePermission.roles.some(role => userRoles.includes(role));
} catch (error) { } catch (e) {
log('err_can_auto_translate', error); log(e);
return false; return false;
} }
}, },

View File

@ -13,7 +13,7 @@ const formatMsg = ({
if (!showLastMessage) { if (!showLastMessage) {
return ''; return '';
} }
if (!lastMessage) { if (!lastMessage || lastMessage.pinned) {
return I18n.t('No_Message'); return I18n.t('No_Message');
} }

View File

@ -4,7 +4,6 @@ import { View, Text } from 'react-native';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import { RectButton } from 'react-native-gesture-handler'; import { RectButton } from 'react-native-gesture-handler';
import log from '../../utils/log';
import Check from '../../containers/Check'; import Check from '../../containers/Check';
import styles, { ROW_HEIGHT } from './styles'; import styles, { ROW_HEIGHT } from './styles';
@ -24,7 +23,7 @@ const ServerItem = React.memo(({
}} }}
defaultSource={{ uri: 'logo' }} defaultSource={{ uri: 'logo' }}
style={styles.serverIcon} style={styles.serverIcon}
onError={() => log('err_loading_server_icon')} onError={() => console.log('err_loading_server_icon')}
/> />
) )
: ( : (

View File

@ -0,0 +1,17 @@
import { TOGGLE_CRASH_REPORT } from '../actions/actionsTypes';
const initialState = {
allowCrashReport: false
};
export default (state = initialState, action) => {
switch (action.type) {
case TOGGLE_CRASH_REPORT:
return {
allowCrashReport: action.payload
};
default:
return state;
}
};

View File

@ -12,6 +12,7 @@ import sortPreferences from './sortPreferences';
import notification from './notification'; import notification from './notification';
import markdown from './markdown'; import markdown from './markdown';
import share from './share'; import share from './share';
import crashReport from './crashReport';
export default combineReducers({ export default combineReducers({
settings, settings,
@ -26,5 +27,6 @@ export default combineReducers({
sortPreferences, sortPreferences,
notification, notification,
markdown, markdown,
share share,
crashReport
}); });

View File

@ -7,6 +7,7 @@ import * as actions from '../actions';
import { selectServerRequest } from '../actions/server'; import { selectServerRequest } from '../actions/server';
import { setAllPreferences } from '../actions/sortPreferences'; import { setAllPreferences } from '../actions/sortPreferences';
import { toggleMarkdown } from '../actions/markdown'; import { toggleMarkdown } from '../actions/markdown';
import { toggleCrashReport } from '../actions/crashReport';
import { APP } from '../actions/actionsTypes'; import { APP } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import log from '../utils/log'; import log from '../utils/log';
@ -46,7 +47,7 @@ const restore = function* restore() {
serversDB.create('servers', serverInfo, true); serversDB.create('servers', serverInfo, true);
await RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ serverInfo.id }`, serverItem[USER_ID]); await RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ serverInfo.id }`, serverItem[USER_ID]);
} catch (e) { } catch (e) {
log('err_create_servers', e); log(e);
} }
}); });
}); });
@ -66,6 +67,9 @@ const restore = function* restore() {
const useMarkdown = yield RocketChat.getUseMarkdown(); const useMarkdown = yield RocketChat.getUseMarkdown();
yield put(toggleMarkdown(useMarkdown)); yield put(toggleMarkdown(useMarkdown));
const allowCrashReport = yield RocketChat.getAllowCrashReport();
yield put(toggleCrashReport(allowCrashReport));
if (!token || !server) { if (!token || !server) {
yield all([ yield all([
RNUserDefaults.clear(RocketChat.TOKEN_KEY), RNUserDefaults.clear(RocketChat.TOKEN_KEY),
@ -79,7 +83,7 @@ const restore = function* restore() {
yield put(actions.appReady({})); yield put(actions.appReady({}));
} catch (e) { } catch (e) {
log('err_restore', e); log(e);
} }
}; };

View File

@ -82,7 +82,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
try { try {
serversDB.create('user', user, true); serversDB.create('user', user, true);
} catch (e) { } catch (e) {
log('err_set_user_token', e); log(e);
} }
}); });
@ -99,7 +99,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
yield put(appStart('inside')); yield put(appStart('inside'));
} }
} catch (e) { } catch (e) {
log('err_handle_login_success', e); log(e);
} }
}; };
@ -128,7 +128,7 @@ const handleLogout = function* handleLogout() {
yield put(appStart('outside')); yield put(appStart('outside'));
} catch (e) { } catch (e) {
yield put(appStart('outside')); yield put(appStart('outside'));
log('err_handle_logout', e); log(e);
} }
} }
}; };

View File

@ -78,7 +78,7 @@ const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
yield delay(500); yield delay(500);
yield put(replyInit(message, false)); yield put(replyInit(message, false));
} catch (e) { } catch (e) {
log('err_reply_broadcast', e); log(e);
} }
}; };

View File

@ -23,7 +23,7 @@ const watchUserTyping = function* watchUserTyping({ rid, status }) {
yield RocketChat.emitTyping(rid, false); yield RocketChat.emitTyping(rid, false);
} }
} catch (e) { } catch (e) {
log('err_watch_user_typing', e); log(e);
} }
}; };

View File

@ -1,5 +1,5 @@
import { import {
put, select, race, take, fork, cancel, takeLatest, delay put, select, race, take, fork, cancel, delay
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import { BACKGROUND, INACTIVE } from 'redux-enhancer-react-native-appstate'; import { BACKGROUND, INACTIVE } from 'redux-enhancer-react-native-appstate';
@ -10,18 +10,9 @@ import log from '../utils/log';
import mergeSubscriptionsRooms from '../lib/methods/helpers/mergeSubscriptionsRooms'; import mergeSubscriptionsRooms from '../lib/methods/helpers/mergeSubscriptionsRooms';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
let roomsSub;
const removeSub = function removeSub() {
if (roomsSub && roomsSub.stop) {
roomsSub.stop();
}
};
const handleRoomsRequest = function* handleRoomsRequest() { const handleRoomsRequest = function* handleRoomsRequest() {
try { try {
removeSub(); yield RocketChat.subscribeRooms();
roomsSub = yield RocketChat.subscribeRooms();
const newRoomsUpdatedAt = new Date(); const newRoomsUpdatedAt = new Date();
const server = yield select(state => state.server.server); const server = yield select(state => state.server.server);
const [serverRecord] = database.databases.serversDB.objects('servers').filtered('id = $0', server); const [serverRecord] = database.databases.serversDB.objects('servers').filtered('id = $0', server);
@ -33,8 +24,8 @@ const handleRoomsRequest = function* handleRoomsRequest() {
subscriptions.forEach((subscription) => { subscriptions.forEach((subscription) => {
try { try {
database.create('subscriptions', subscription, true); database.create('subscriptions', subscription, true);
} catch (error) { } catch (e) {
log('err_rooms_request_create_sub', error); log(e);
} }
}); });
}); });
@ -42,23 +33,18 @@ const handleRoomsRequest = function* handleRoomsRequest() {
try { try {
database.databases.serversDB.create('servers', { id: server, roomsUpdatedAt: newRoomsUpdatedAt }, true); database.databases.serversDB.create('servers', { id: server, roomsUpdatedAt: newRoomsUpdatedAt }, true);
} catch (e) { } catch (e) {
log('err_rooms_request_update', e); log(e);
} }
}); });
yield put(roomsSuccess()); yield put(roomsSuccess());
} catch (e) { } catch (e) {
yield put(roomsFailure(e)); yield put(roomsFailure(e));
log('err_rooms_request', e); log(e);
} }
}; };
const handleLogout = function handleLogout() {
removeSub();
};
const root = function* root() { const root = function* root() {
yield takeLatest(types.LOGOUT, handleLogout);
while (true) { while (true) {
const params = yield take(types.ROOMS.REQUEST); const params = yield take(types.ROOMS.REQUEST);
const isAuthenticated = yield select(state => state.login.isAuthenticated); const isAuthenticated = yield select(state => state.login.isAuthenticated);

View File

@ -14,6 +14,7 @@ import { setUser } from '../actions/login';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/realm';
import log from '../utils/log'; import log from '../utils/log';
import { extractHostname } from '../utils/server';
import I18n from '../i18n'; import I18n from '../i18n';
import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults'; import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults';
@ -34,7 +35,7 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
return serverInfo; return serverInfo;
} catch (e) { } catch (e) {
log('err_get_server_info', e); log(e);
} }
}; };
@ -73,25 +74,30 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
yield put(selectServerSuccess(server, (serverInfo && serverInfo.version) || version)); yield put(selectServerSuccess(server, (serverInfo && serverInfo.version) || version));
} catch (e) { } catch (e) {
yield put(selectServerFailure()); yield put(selectServerFailure());
log('err_select_server', e); log(e);
} }
}; };
const handleServerRequest = function* handleServerRequest({ server }) { const handleServerRequest = function* handleServerRequest({ server, certificate }) {
try { try {
const serverInfo = yield getServerInfo({ server }); if (certificate) {
yield RNUserDefaults.setObjectForKey(extractHostname(server), certificate);
const loginServicesLength = yield RocketChat.getLoginServices(server);
if (loginServicesLength === 0) {
Navigation.navigate('LoginView');
} else {
Navigation.navigate('LoginSignupView');
} }
yield put(selectServerRequest(server, serverInfo.version, false)); const serverInfo = yield getServerInfo({ server });
if (serverInfo) {
const loginServicesLength = yield RocketChat.getLoginServices(server);
if (loginServicesLength === 0) {
Navigation.navigate('LoginView');
} else {
Navigation.navigate('LoginSignupView');
}
yield put(selectServerRequest(server, serverInfo.version, false));
}
} catch (e) { } catch (e) {
yield put(serverFailure()); yield put(serverFailure());
log('err_server_request', e); log(e);
} }
}; };

View File

@ -18,7 +18,7 @@ const appHasComeBackToForeground = function* appHasComeBackToForeground() {
setBadgeCount(); setBadgeCount();
return yield RocketChat.setUserPresenceOnline(); return yield RocketChat.setUserPresenceOnline();
} catch (e) { } catch (e) {
log('err_app_has_come_back_to_foreground', e); log(e);
} }
}; };
@ -34,7 +34,7 @@ const appHasComeBackToBackground = function* appHasComeBackToBackground() {
try { try {
return yield RocketChat.setUserPresenceAway(); return yield RocketChat.setUserPresenceAway();
} catch (e) { } catch (e) {
log('err_app_has_come_back_to_background', e); log(e);
} }
}; };

View File

@ -31,7 +31,7 @@ class EventEmitter {
try { try {
listener.apply(this, args); listener.apply(this, args);
} catch (e) { } catch (e) {
log('err_emit', e); log(e);
} }
}); });
} }

View File

@ -1,11 +1,17 @@
import { Client } from 'bugsnag-react-native';
import firebase from 'react-native-firebase'; import firebase from 'react-native-firebase';
import config from '../../config';
export default (event, error) => { const bugsnag = new Client(config.BUGSNAG_API_KEY);
if (typeof error !== 'object') {
error = { error }; export const { analytics } = firebase;
} export const loggerConfig = bugsnag.config;
firebase.analytics().logEvent(event); export const { leaveBreadcrumb } = bugsnag;
if (__DEV__) {
console.warn(event, error); export default (e) => {
if (e instanceof Error) {
bugsnag.notify(e);
} else {
console.log(e);
} }
}; };

View File

@ -1,4 +1,4 @@
import firebase from 'react-native-firebase'; import { analytics, leaveBreadcrumb } from './log';
import { HEADER_BACKGROUND, HEADER_TITLE, HEADER_BACK } from '../constants/colors'; import { HEADER_BACKGROUND, HEADER_TITLE, HEADER_BACK } from '../constants/colors';
@ -31,6 +31,7 @@ export const onNavigationStateChange = (prevState, currentState) => {
const prevScreen = getActiveRouteName(prevState); const prevScreen = getActiveRouteName(prevState);
if (prevScreen !== currentScreen) { if (prevScreen !== currentScreen) {
firebase.analytics().setCurrentScreen(currentScreen); analytics().setCurrentScreen(currentScreen);
leaveBreadcrumb(currentScreen, { type: 'navigation' });
} }
}; };

18
app/utils/server.js Normal file
View File

@ -0,0 +1,18 @@
/*
Extract hostname from url
url = 'https://open.rocket.chat/method'
hostname = 'open.rocket.chat'
*/
export const extractHostname = (url) => {
let hostname;
if (url.indexOf('//') > -1) {
[,, hostname] = url.split('/');
} else {
[hostname] = url.split('/');
}
[hostname] = hostname.split(':');
[hostname] = hostname.split('?');
return hostname;
};

View File

@ -99,8 +99,8 @@ class DirectoryView extends React.Component {
} else { } else {
this.setState({ loading: false }); this.setState({ loading: false });
} }
} catch (error) { } catch (e) {
log('err_load_directory', error); log(e);
this.setState({ loading: false }); this.setState({ loading: false });
} }
}, 200) }, 200)

View File

@ -109,7 +109,7 @@ class LanguageView extends React.Component {
this.setState({ saving: false }); this.setState({ saving: false });
setTimeout(() => { setTimeout(() => {
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_preferences') })); showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_preferences') }));
log('err_save_user_preferences', e); log(e);
}, 300); }, 300);
} }
} }

View File

@ -6,7 +6,7 @@ import {
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal'; import equal from 'deep-equal';
import firebase from 'react-native-firebase'; import { analytics } from '../utils/log';
import KeyboardView from '../presentation/KeyboardView'; import KeyboardView from '../presentation/KeyboardView';
import TextInput from '../containers/TextInput'; import TextInput from '../containers/TextInput';
@ -156,7 +156,7 @@ class LoginView extends React.Component {
const { loginRequest } = this.props; const { loginRequest } = this.props;
Keyboard.dismiss(); Keyboard.dismiss();
loginRequest({ user, password, code }); loginRequest({ user, password, code });
firebase.analytics().logEvent('login'); analytics().logEvent('login');
} }
register = () => { register = () => {

View File

@ -35,7 +35,8 @@ class MessagesView extends React.Component {
loading: false, loading: false,
messages: [], messages: [],
selectedAttachment: {}, selectedAttachment: {},
photoModalVisible: false photoModalVisible: false,
fileLoading: true
}; };
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t'); this.t = props.navigation.getParam('t');
@ -47,7 +48,9 @@ class MessagesView extends React.Component {
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { loading, messages, photoModalVisible } = this.state; const {
loading, messages, photoModalVisible, fileLoading
} = this.state;
if (nextState.loading !== loading) { if (nextState.loading !== loading) {
return true; return true;
} }
@ -57,6 +60,10 @@ class MessagesView extends React.Component {
if (!equal(nextState.messages, messages)) { if (!equal(nextState.messages, messages)) {
return true; return true;
} }
if (fileLoading !== nextState.fileLoading) {
return true;
}
return false; return false;
} }
@ -225,6 +232,10 @@ class MessagesView extends React.Component {
} }
} }
setFileLoading = (fileLoading) => {
this.setState({ fileLoading });
}
renderEmpty = () => ( renderEmpty = () => (
<View style={styles.listEmptyContainer} testID={this.content.testID}> <View style={styles.listEmptyContainer} testID={this.content.testID}>
<Text style={styles.noDataFound}>{this.content.noDataMsg}</Text> <Text style={styles.noDataFound}>{this.content.noDataMsg}</Text>
@ -235,7 +246,7 @@ class MessagesView extends React.Component {
render() { render() {
const { const {
messages, loading, selectedAttachment, photoModalVisible messages, loading, selectedAttachment, photoModalVisible, fileLoading
} = this.state; } = this.state;
const { user, baseUrl } = this.props; const { user, baseUrl } = this.props;
@ -260,6 +271,8 @@ class MessagesView extends React.Component {
onClose={this.onCloseFileModal} onClose={this.onCloseFileModal}
user={user} user={user}
baseUrl={baseUrl} baseUrl={baseUrl}
loading={fileLoading}
setLoading={this.setFileLoading}
/> />
</SafeAreaView> </SafeAreaView>
); );

View File

@ -1,10 +1,14 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity, View, Alert, LayoutAnimation
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import * as FileSystem from 'expo-file-system';
import DocumentPicker from 'react-native-document-picker';
import ActionSheet from 'react-native-action-sheet';
import isEqual from 'deep-equal';
import { serverRequest } from '../actions/server'; import { serverRequest } from '../actions/server';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
@ -18,6 +22,7 @@ import { isIOS, isNotch } from '../utils/deviceInfo';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import StatusBar from '../containers/StatusBar'; import StatusBar from '../containers/StatusBar';
import { COLOR_PRIMARY } from '../constants/colors'; import { COLOR_PRIMARY } from '../constants/colors';
import log from '../utils/log';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
image: { image: {
@ -41,6 +46,22 @@ const styles = StyleSheet.create({
position: 'absolute', position: 'absolute',
paddingHorizontal: 9, paddingHorizontal: 9,
left: 15 left: 15
},
certificatePicker: {
flex: 1,
marginTop: 40,
alignItems: 'center',
justifyContent: 'center'
},
chooseCertificateTitle: {
fontSize: 15,
...sharedStyles.textRegular,
...sharedStyles.textColorDescription
},
chooseCertificate: {
fontSize: 15,
...sharedStyles.textSemibold,
...sharedStyles.textColorHeaderBack
} }
}); });
@ -61,9 +82,19 @@ class NewServerView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const server = props.navigation.getParam('server'); const server = props.navigation.getParam('server');
// Cancel
this.options = [I18n.t('Cancel')];
this.CANCEL_INDEX = 0;
// Delete
this.options.push(I18n.t('Delete'));
this.DELETE_INDEX = 1;
this.state = { this.state = {
text: server || '', text: server || '',
autoFocus: !server autoFocus: !server,
certificate: null
}; };
} }
@ -76,11 +107,14 @@ class NewServerView extends React.Component {
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { text } = this.state; const { text, certificate } = this.state;
const { connecting } = this.props; const { connecting } = this.props;
if (nextState.text !== text) { if (nextState.text !== text) {
return true; return true;
} }
if (!isEqual(nextState.certificate, certificate)) {
return true;
}
if (nextProps.connecting !== connecting) { if (nextProps.connecting !== connecting) {
return true; return true;
} }
@ -91,13 +125,51 @@ class NewServerView extends React.Component {
this.setState({ text }); this.setState({ text });
} }
submit = () => { submit = async() => {
const { text } = this.state; const { text, certificate } = this.state;
const { connectServer } = this.props; const { connectServer } = this.props;
let cert = null;
if (certificate) {
const certificatePath = `${ FileSystem.documentDirectory }/${ certificate.name }`;
try {
await FileSystem.copyAsync({ from: certificate.path, to: certificatePath });
} catch (e) {
log(e);
}
cert = {
path: this.uriToPath(certificatePath), // file:// isn't allowed by obj-C
password: certificate.password
};
}
if (text) { if (text) {
Keyboard.dismiss(); Keyboard.dismiss();
connectServer(this.completeUrl(text)); connectServer(this.completeUrl(text), cert);
}
}
chooseCertificate = async() => {
try {
const res = await DocumentPicker.pick({
type: ['com.rsa.pkcs-12']
});
const { uri: path, name } = res;
Alert.prompt(
I18n.t('Certificate_password'),
I18n.t('Whats_the_password_for_your_certificate'),
[
{
text: 'OK',
onPress: password => this.saveCertificate({ path, name, password })
}
],
'secure-text',
);
} catch (e) {
if (!DocumentPicker.isCancel(e)) {
log(e);
}
} }
} }
@ -120,6 +192,25 @@ class NewServerView extends React.Component {
return url.replace(/\/+$/, ''); return url.replace(/\/+$/, '');
} }
uriToPath = uri => uri.replace('file://', '');
saveCertificate = (certificate) => {
LayoutAnimation.easeInEaseOut();
this.setState({ certificate });
}
handleDelete = () => this.setState({ certificate: null }); // We not need delete file from DocumentPicker because it is a temp file
showActionSheet = () => {
ActionSheet.showActionSheetWithOptions({
options: this.options,
cancelButtonIndex: this.CANCEL_INDEX,
destructiveButtonIndex: this.DELETE_INDEX
}, (actionIndex) => {
if (actionIndex === this.DELETE_INDEX) { this.handleDelete(); }
});
}
renderBack = () => { renderBack = () => {
const { navigation } = this.props; const { navigation } = this.props;
@ -142,6 +233,18 @@ class NewServerView extends React.Component {
); );
} }
renderCertificatePicker = () => {
const { certificate } = this.state;
return (
<View style={styles.certificatePicker}>
<Text style={styles.chooseCertificateTitle}>{certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')}</Text>
<TouchableOpacity onPress={certificate ? this.showActionSheet : this.chooseCertificate} testID='new-server-choose-certificate'>
<Text style={styles.chooseCertificate}>{certificate ? certificate.name : I18n.t('Apply_Your_Certificate')}</Text>
</TouchableOpacity>
</View>
);
}
render() { render() {
const { connecting } = this.props; const { connecting } = this.props;
const { text, autoFocus } = this.state; const { text, autoFocus } = this.state;
@ -175,6 +278,7 @@ class NewServerView extends React.Component {
loading={connecting} loading={connecting}
testID='new-server-view-button' testID='new-server-view-button'
/> />
{ isIOS ? this.renderCertificatePicker() : null }
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>
{this.renderBack()} {this.renderBack()}
@ -188,7 +292,7 @@ const mapStateToProps = state => ({
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
connectServer: server => dispatch(serverRequest(server)) connectServer: (server, certificate) => dispatch(serverRequest(server, certificate))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(NewServerView); export default connect(mapStateToProps, mapDispatchToProps)(NewServerView);

View File

@ -0,0 +1,281 @@
import React from 'react';
import {
View, ScrollView, Switch, Text
} from 'react-native';
import PropTypes from 'prop-types';
import RNPickerSelect from 'react-native-picker-select';
import { SafeAreaView } from 'react-navigation';
import { SWITCH_TRACK_COLOR } from '../../constants/colors';
import StatusBar from '../../containers/StatusBar';
import ListItem from '../../containers/ListItem';
import Separator from '../../containers/Separator';
import I18n from '../../i18n';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import styles from './styles';
import sharedStyles from '../Styles';
import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log';
const SectionTitle = React.memo(({ title }) => <Text style={styles.sectionTitle}>{title}</Text>);
const SectionSeparator = React.memo(() => <View style={styles.sectionSeparatorBorder} />);
const Info = React.memo(({ info }) => <Text style={styles.infoText}>{info}</Text>);
SectionTitle.propTypes = {
title: PropTypes.string
};
Info.propTypes = {
info: PropTypes.string
};
const OPTIONS = {
desktopNotifications: [{
label: I18n.t('Default'), value: 'default'
}, {
label: I18n.t('All_Messages'), value: 'all'
}, {
label: I18n.t('Mentions'), value: 'mentions'
}, {
label: I18n.t('Nothing'), value: 'nothing'
}],
audioNotifications: [{
label: I18n.t('Default'), value: 'default'
}, {
label: I18n.t('All_Messages'), value: 'all'
}, {
label: I18n.t('Mentions'), value: 'mentions'
}, {
label: I18n.t('Nothing'), value: 'nothing'
}],
mobilePushNotifications: [{
label: I18n.t('Default'), value: 'default'
}, {
label: I18n.t('All_Messages'), value: 'all'
}, {
label: I18n.t('Mentions'), value: 'mentions'
}, {
label: I18n.t('Nothing'), value: 'nothing'
}],
emailNotifications: [{
label: I18n.t('Default'), value: 'default'
}, {
label: I18n.t('All_Messages'), value: 'all'
}, {
label: I18n.t('Mentions'), value: 'mentions'
}, {
label: I18n.t('Nothing'), value: 'nothing'
}],
desktopNotificationDuration: [{
label: I18n.t('Default'), value: 0
}, {
label: I18n.t('Seconds', { second: 1 }), value: 1
}, {
label: I18n.t('Seconds', { second: 2 }), value: 2
}, {
label: I18n.t('Seconds', { second: 3 }), value: 3
}, {
label: I18n.t('Seconds', { second: 4 }), value: 4
}, {
label: I18n.t('Seconds', { second: 5 }), value: 5
}],
audioNotificationValue: [{
label: 'None', value: 'none None'
}, {
label: I18n.t('Default'), value: '0 Default'
}, {
label: 'Beep', value: 'beep Beep'
}, {
label: 'Ding', value: 'ding Ding'
}, {
label: 'Chelle', value: 'chelle Chelle'
}, {
label: 'Droplet', value: 'droplet Droplet'
}, {
label: 'Highbell', value: 'highbell Highbell'
}, {
label: 'Seasons', value: 'seasons Seasons'
}]
};
export default class NotificationPreferencesView extends React.Component {
static navigationOptions = () => ({
title: I18n.t('Notification_Preferences')
})
static propTypes = {
navigation: PropTypes.object
}
constructor(props) {
super(props);
this.rid = props.navigation.getParam('rid');
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = {
room: JSON.parse(JSON.stringify(this.rooms[0] || {}))
};
}
onValueChangeSwitch = async(key, value) => {
const { room: newRoom } = this.state;
newRoom[key] = value;
this.setState({ room: newRoom });
const params = {
[key]: value ? '1' : '0'
};
try {
await RocketChat.saveNotificationSettings(this.rid, params);
} catch (e) {
log(e);
}
}
onValueChangePicker = async(key, value) => {
const { room: newRoom } = this.state;
newRoom[key] = value;
this.setState({ room: newRoom });
const params = {
[key]: value.toString()
};
try {
await RocketChat.saveNotificationSettings(this.rid, params);
} catch (e) {
log(e);
}
}
renderPicker = (key) => {
const { room } = this.state;
return (
<RNPickerSelect
testID={key}
style={{ viewContainer: styles.viewContainer }}
value={room[key]}
textInputProps={{ style: styles.pickerText }}
useNativeAndroidPickerStyle={false}
placeholder={{}}
onValueChange={value => this.onValueChangePicker(key, value)}
items={OPTIONS[key]}
/>
);
}
renderSwitch = (key) => {
const { room } = this.state;
return (
<Switch
value={!room[key]}
testID={key}
trackColor={SWITCH_TRACK_COLOR}
onValueChange={value => this.onValueChangeSwitch(key, !value)}
/>
);
}
render() {
const { room } = this.state;
return (
<SafeAreaView style={sharedStyles.listSafeArea} testID='notification-preference-view' forceInset={{ vertical: 'never' }}>
<StatusBar />
<ScrollView
{...scrollPersistTaps}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
testID='notification-preference-view-list'
>
<Separator />
<ListItem
title={I18n.t('Receive_Notification')}
testID='notification-preference-view-receive-notification'
right={() => this.renderSwitch('disableNotifications')}
/>
<Separator />
<Info info={I18n.t('Receive_notifications_from', { name: room.name })} />
<SectionSeparator />
<Separator />
<ListItem
title={I18n.t('Receive_Group_Mentions')}
testID='notification-preference-view-group-mentions'
right={() => this.renderSwitch('muteGroupMentions')}
/>
<Separator />
<Info info={I18n.t('Receive_Group_Mentions_Info')} />
<SectionSeparator />
<Separator />
<ListItem
title={I18n.t('Show_Unread_Counter')}
testID='notification-preference-view-unread-count'
right={() => this.renderSwitch('hideUnreadStatus')}
/>
<Separator />
<Info info={I18n.t('Show_Unread_Counter_Info')} />
<SectionSeparator />
<SectionTitle title={I18n.t('IN_APP_AND_DESKTOP')} />
<Separator />
<ListItem
title={I18n.t('Alert')}
testID='notification-preference-view-alert'
right={() => this.renderPicker('desktopNotifications')}
/>
<Separator />
<Info info={I18n.t('In_App_and_Desktop_Alert_info')} />
<SectionSeparator />
<SectionTitle title={I18n.t('PUSH_NOTIFICATIONS')} />
<Separator />
<ListItem
title={I18n.t('Alert')}
testID='notification-preference-view-push-notification'
right={() => this.renderPicker('mobilePushNotifications')}
/>
<Separator />
<Info info={I18n.t('Push_Notifications_Alert_Info')} />
<SectionSeparator />
<SectionTitle title={I18n.t('DESKTOP_OPTIONS')} />
<Separator />
<ListItem
title={I18n.t('Audio')}
testID='notification-preference-view-audio'
right={() => this.renderPicker('audioNotifications')}
/>
<Separator />
<ListItem
title={I18n.t('Sound')}
testID='notification-preference-view-sound'
right={() => this.renderPicker('audioNotificationValue')}
/>
<Separator />
<ListItem
title={I18n.t('Notification_Duration')}
testID='notification-preference-view-notification-duration'
right={() => this.renderPicker('desktopNotificationDuration')}
/>
<Separator />
<SectionSeparator />
<SectionTitle title={I18n.t('EMAIL')} />
<Separator />
<ListItem
title={I18n.t('Alert')}
testID='notification-preference-view-email-alert'
right={() => this.renderPicker('emailNotifications')}
/>
<Separator />
<View style={styles.marginBottom} />
</ScrollView>
</SafeAreaView>
);
}
}

View File

@ -0,0 +1,43 @@
import { StyleSheet } from 'react-native';
import { COLOR_BACKGROUND_CONTAINER, COLOR_PRIMARY, COLOR_WHITE } from '../../constants/colors';
import sharedStyles from '../Styles';
export default StyleSheet.create({
sectionSeparatorBorder: {
backgroundColor: COLOR_BACKGROUND_CONTAINER,
height: 10
},
marginBottom: {
height: 30,
backgroundColor: COLOR_BACKGROUND_CONTAINER
},
contentContainer: {
backgroundColor: COLOR_WHITE,
marginVertical: 10
},
infoText: {
...sharedStyles.textRegular,
...sharedStyles.textColorNormal,
fontSize: 13,
paddingHorizontal: 15,
paddingVertical: 10,
backgroundColor: COLOR_BACKGROUND_CONTAINER
},
sectionTitle: {
...sharedStyles.separatorBottom,
paddingHorizontal: 15,
backgroundColor: COLOR_BACKGROUND_CONTAINER,
paddingVertical: 10,
fontSize: 14,
...sharedStyles.textColorNormal
},
viewContainer: {
justifyContent: 'center'
},
pickerText: {
...sharedStyles.textRegular,
fontSize: 16,
color: COLOR_PRIMARY
}
});

View File

@ -63,7 +63,7 @@ class ProfileView extends React.Component {
const result = await RocketChat.getAvatarSuggestion(); const result = await RocketChat.getAvatarSuggestion();
this.setState({ avatarSuggestions: result }); this.setState({ avatarSuggestions: result });
} catch (e) { } catch (e) {
log('err_get_avatar_suggestion', e); log(e);
} }
} }

View File

@ -47,7 +47,7 @@ class RegisterView extends React.Component {
try { try {
this.parsedCustomFields = JSON.parse(props.Accounts_CustomFields); this.parsedCustomFields = JSON.parse(props.Accounts_CustomFields);
} catch (e) { } catch (e) {
log('err_parsing_account_custom_fields', e); log(e);
} }
} }
Object.keys(this.parsedCustomFields).forEach((key) => { Object.keys(this.parsedCustomFields).forEach((key) => {

View File

@ -64,8 +64,8 @@ class RoomActionsView extends React.Component {
if (result.success) { if (result.success) {
this.setState({ room: { ...result.channel, rid: result.channel._id } }); this.setState({ room: { ...result.channel, rid: result.channel._id } });
} }
} catch (error) { } catch (e) {
log('err_get_channel_info', error); log(e);
} }
} }
@ -75,8 +75,8 @@ class RoomActionsView extends React.Component {
if (counters.success) { if (counters.success) {
this.setState({ membersCount: counters.members, joined: counters.joined }); this.setState({ membersCount: counters.members, joined: counters.joined });
} }
} catch (error) { } catch (e) {
log('err_get_room_counters', error); log(e);
} }
} else if (room.t === 'd') { } else if (room.t === 'd') {
this.updateRoomMember(); this.updateRoomMember();
@ -168,13 +168,14 @@ class RoomActionsView extends React.Component {
room, membersCount, canViewMembers, joined, canAutoTranslate room, membersCount, canViewMembers, joined, canAutoTranslate
} = this.state; } = this.state;
const { const {
rid, t, blocker, notifications rid, t, blocker
} = room; } = room;
const notificationsAction = { const notificationsAction = {
icon: notifications ? 'bell' : 'Bell-off', icon: 'bell',
name: I18n.t(`${ notifications ? 'Enable' : 'Disable' }_notifications`), name: I18n.t('Notifications'),
event: this.toggleNotifications, route: 'NotificationPrefView',
params: { rid },
testID: 'room-actions-notifications' testID: 'room-actions-notifications'
}; };
@ -184,7 +185,7 @@ class RoomActionsView extends React.Component {
name: I18n.t('Room_Info'), name: I18n.t('Room_Info'),
route: 'RoomInfoView', route: 'RoomInfoView',
// forward room only if room isn't joined // forward room only if room isn't joined
params: { rid, t, room: joined ? null : room }, params: { rid, t },
testID: 'room-actions-info' testID: 'room-actions-info'
}], }],
renderItem: this.renderRoomInfo renderItem: this.renderRoomInfo
@ -341,7 +342,7 @@ class RoomActionsView extends React.Component {
this.setState({ member: result.user }); this.setState({ member: result.user });
} }
} catch (e) { } catch (e) {
log('err_update_room_member', e); log(e);
this.setState({ member: {} }); this.setState({ member: {} });
} }
} }
@ -353,7 +354,7 @@ class RoomActionsView extends React.Component {
try { try {
RocketChat.toggleBlockUser(rid, member._id, !blocker); RocketChat.toggleBlockUser(rid, member._id, !blocker);
} catch (e) { } catch (e) {
log('err_toggle_block_user', e); log(e);
} }
} }
@ -386,18 +387,6 @@ class RoomActionsView extends React.Component {
); );
} }
toggleNotifications = () => {
const { room } = this.state;
try {
const notifications = {
mobilePushNotifications: room.notifications ? 'default' : 'nothing'
};
RocketChat.saveNotificationSettings(room.rid, notifications);
} catch (e) {
log('err_toggle_notifications', e);
}
}
renderRoomInfo = ({ item }) => { renderRoomInfo = ({ item }) => {
const { room, member } = this.state; const { room, member } = this.state;
const { name, t, topic } = room; const { name, t, topic } = room;

View File

@ -206,7 +206,7 @@ class RoomInfoEditView extends React.Component {
this.setState({ nameError: e }); this.setState({ nameError: e });
} }
error = true; error = true;
log('err_save_room_settings', e); log(e);
} }
await this.setState({ saving: false }); await this.setState({ saving: false });
@ -261,7 +261,7 @@ class RoomInfoEditView extends React.Component {
try { try {
await RocketChat.toggleArchiveRoom(rid, t, !archived); await RocketChat.toggleArchiveRoom(rid, t, !archived);
} catch (e) { } catch (e) {
log('err_toggle_archive', e); log(e);
} }
} }
} }

View File

@ -20,8 +20,8 @@ import log from '../../utils/log';
const PERMISSION_EDIT_ROOM = 'edit-room'; const PERMISSION_EDIT_ROOM = 'edit-room';
const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase()); const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase());
const getRoomTitle = room => (room.t === 'd' const getRoomTitle = (room, type, name) => (type === 'd'
? <Text testID='room-info-view-name' style={styles.roomTitle}>{room.fname}</Text> ? <Text testID='room-info-view-name' style={styles.roomTitle}>{name}</Text>
: ( : (
<View style={styles.roomTitleRow}> <View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' /> <RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' />
@ -59,28 +59,18 @@ class RoomInfoView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
const room = props.navigation.getParam('room');
this.t = props.navigation.getParam('t'); this.t = props.navigation.getParam('t');
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.roles = database.objects('roles'); this.roles = database.objects('roles');
this.sub = { this.sub = {
unsubscribe: () => {} unsubscribe: () => {}
}; };
this.state = { this.state = {
room: this.rooms[0] || room || {}, room: {},
roomUser: {} roomUser: {}
}; };
} }
async componentDidMount() { async componentDidMount() {
safeAddListener(this.rooms, this.updateRoom);
const { room } = this.state;
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
if (permissions[PERMISSION_EDIT_ROOM] && !room.prid) {
const { navigation } = this.props;
navigation.setParams({ showEdit: true });
}
if (this.t === 'd') { if (this.t === 'd') {
const { user } = this.props; const { user } = this.props;
const roomUserId = RocketChat.getRoomMemberId(this.rid, user.id); const roomUserId = RocketChat.getRoomMemberId(this.rid, user.id);
@ -89,14 +79,34 @@ class RoomInfoView extends React.Component {
if (result.success) { if (result.success) {
this.setState({ roomUser: result.user }); this.setState({ roomUser: result.user });
} }
} catch (error) { } catch (e) {
log('err_get_user_info', error); log(e);
}
return;
}
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
safeAddListener(this.rooms, this.updateRoom);
let room = {};
if (this.rooms.length > 0) {
this.setState({ room: this.rooms[0] });
[room] = this.rooms;
} else {
try {
const result = await RocketChat.getRoomInfo(this.rid);
if (result.success) {
// eslint-disable-next-line prefer-destructuring
room = result.room;
this.setState({ room });
}
} catch (e) {
log(e);
} }
} }
} const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
if (permissions[PERMISSION_EDIT_ROOM] && !room.prid) {
componentWillUnmount() { const { navigation } = this.props;
this.rooms.removeAllListeners(); navigation.setParams({ showEdit: true });
}
} }
getRoleDescription = (id) => { getRoleDescription = (id) => {
@ -107,10 +117,7 @@ class RoomInfoView extends React.Component {
return null; return null;
} }
isDirect = () => { isDirect = () => this.t === 'd'
const { room: { t } } = this.state;
return t === 'd';
}
updateRoom = () => { updateRoom = () => {
if (this.rooms.length > 0) { if (this.rooms.length > 0) {
@ -181,15 +188,15 @@ class RoomInfoView extends React.Component {
return ( return (
<Avatar <Avatar
text={room.name} text={room.name || roomUser.username}
size={100} size={100}
style={styles.avatar} style={styles.avatar}
type={room.t} type={this.t}
baseUrl={baseUrl} baseUrl={baseUrl}
userId={user.id} userId={user.id}
token={user.token} token={user.token}
> >
{room.t === 'd' && roomUser._id ? <Status style={[sharedStyles.status, styles.status]} size={24} id={roomUser._id} /> : null} {this.t === 'd' && roomUser._id ? <Status style={[sharedStyles.status, styles.status]} size={24} id={roomUser._id} /> : null}
</Avatar> </Avatar>
); );
} }
@ -231,6 +238,29 @@ class RoomInfoView extends React.Component {
return null; return null;
} }
renderChannel = () => {
const { room } = this.state;
return (
<React.Fragment>
{this.renderItem('description', room)}
{this.renderItem('topic', room)}
{this.renderItem('announcement', room)}
{room.broadcast ? this.renderBroadcast() : null}
</React.Fragment>
);
}
renderDirect = () => {
const { roomUser } = this.state;
return (
<React.Fragment>
{this.renderRoles()}
{this.renderTimezone()}
{this.renderCustomFields(roomUser._id)}
</React.Fragment>
);
}
render() { render() {
const { room, roomUser } = this.state; const { room, roomUser } = this.state;
if (!room) { if (!room) {
@ -242,15 +272,9 @@ class RoomInfoView extends React.Component {
<SafeAreaView style={styles.container} testID='room-info-view' forceInset={{ vertical: 'never' }}> <SafeAreaView style={styles.container} testID='room-info-view' forceInset={{ vertical: 'never' }}>
<View style={styles.avatarContainer}> <View style={styles.avatarContainer}>
{this.renderAvatar(room, roomUser)} {this.renderAvatar(room, roomUser)}
<View style={styles.roomTitleContainer}>{ getRoomTitle(room) }</View> <View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser && roomUser.name) }</View>
</View> </View>
{!this.isDirect() ? this.renderItem('description', room) : null} {this.isDirect() ? this.renderDirect() : this.renderChannel()}
{!this.isDirect() ? this.renderItem('topic', room) : null}
{!this.isDirect() ? this.renderItem('announcement', room) : null}
{this.isDirect() ? this.renderRoles() : null}
{this.isDirect() ? this.renderTimezone() : null}
{this.isDirect() ? this.renderCustomFields(roomUser._id) : null}
{room.broadcast ? this.renderBroadcast() : null}
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>
); );

View File

@ -138,7 +138,7 @@ class RoomMembersView extends React.Component {
} }
} }
} catch (e) { } catch (e) {
log('err_on_press_user', e); log(e);
} }
} }
@ -169,7 +169,7 @@ class RoomMembersView extends React.Component {
this.fetchMembers(); this.fetchMembers();
}); });
} catch (e) { } catch (e) {
log('err_toggle_status', e); log(e);
} }
} }
@ -203,8 +203,8 @@ class RoomMembersView extends React.Component {
end: newMembers.length < PAGE_SIZE end: newMembers.length < PAGE_SIZE
}); });
navigation.setParams({ allUsers }); navigation.setParams({ allUsers });
} catch (error) { } catch (e) {
log('err_fetch_members, error'); log(e);
this.setState({ isLoading: false }); this.setState({ isLoading: false });
} }
} }
@ -228,7 +228,7 @@ class RoomMembersView extends React.Component {
await RocketChat.toggleMuteUserInRoom(rid, userLongPressed.username, !userLongPressed.muted); await RocketChat.toggleMuteUserInRoom(rid, userLongPressed.username, !userLongPressed.muted);
EventEmitter.emit(LISTENER, { message: I18n.t('User_has_been_key', { key: userLongPressed.muted ? I18n.t('unmuted') : I18n.t('muted') }) }); EventEmitter.emit(LISTENER, { message: I18n.t('User_has_been_key', { key: userLongPressed.muted ? I18n.t('unmuted') : I18n.t('muted') }) });
} catch (e) { } catch (e) {
log('err_handle_mute', e); log(e);
} }
} }

View File

@ -15,7 +15,10 @@ import { COLOR_TEXT_DESCRIPTION, HEADER_TITLE, COLOR_WHITE } from '../../../cons
const TITLE_SIZE = 16; const TITLE_SIZE = 16;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
height: '100%' flex: 1,
height: '100%',
marginRight: isAndroid ? 15 : 5,
marginLeft: isAndroid ? 10 : 0
}, },
titleContainer: { titleContainer: {
flex: 6, flex: 6,

View File

@ -105,7 +105,7 @@ export class List extends React.PureComponent {
this.setState({ end: result.length < 50, loading: false }); this.setState({ end: result.length < 50, loading: false });
} catch (e) { } catch (e) {
this.setState({ loading: false }); this.setState({ loading: false });
log('err_list_view_on_end_reached', e); log(e);
} }
}, 300) }, 300)

View File

@ -116,7 +116,7 @@ class UploadProgress extends Component {
try { try {
database.write(() => database.delete(uploadItem[0])); database.write(() => database.delete(uploadItem[0]));
} catch (e) { } catch (e) {
log('err_upload_progress_delete', e); log(e);
} }
} }
@ -124,7 +124,7 @@ class UploadProgress extends Component {
try { try {
await RocketChat.cancelUpload(item.path); await RocketChat.cancelUpload(item.path);
} catch (e) { } catch (e) {
log('err_upload_progress_cancel', e); log(e);
} }
} }
@ -137,7 +137,7 @@ class UploadProgress extends Component {
}); });
await RocketChat.sendFileMessage(rid, item, undefined, server, user); await RocketChat.sendFileMessage(rid, item, undefined, server, user);
} catch (e) { } catch (e) {
log('err_upload_progress_try_again', e); log(e);
} }
} }

View File

@ -5,7 +5,7 @@ import {
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RectButton } from 'react-native-gesture-handler'; import { RectButton } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView, HeaderBackButton } from 'react-navigation';
import equal from 'deep-equal'; import equal from 'deep-equal';
import moment from 'moment'; import moment from 'moment';
import EJSON from 'ejson'; import EJSON from 'ejson';
@ -36,7 +36,7 @@ import I18n from '../../i18n';
import RoomHeaderView, { RightButtons } from './Header'; import RoomHeaderView, { RightButtons } from './Header';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import Separator from './Separator'; import Separator from './Separator';
import { COLOR_WHITE } from '../../constants/colors'; import { COLOR_WHITE, HEADER_BACK } from '../../constants/colors';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import buildMessage from '../../lib/methods/helpers/buildMessage'; import buildMessage from '../../lib/methods/helpers/buildMessage';
import FileModal from '../../containers/FileModal'; import FileModal from '../../containers/FileModal';
@ -52,8 +52,8 @@ class RoomView extends React.Component {
const t = navigation.getParam('t'); const t = navigation.getParam('t');
const tmid = navigation.getParam('tmid'); const tmid = navigation.getParam('tmid');
const toggleFollowThread = navigation.getParam('toggleFollowThread', () => {}); const toggleFollowThread = navigation.getParam('toggleFollowThread', () => {});
const unreadsCount = navigation.getParam('unreadsCount', null);
return { return {
headerTitleContainerStyle: styles.headerTitleContainerStyle,
headerTitle: ( headerTitle: (
<RoomHeaderView <RoomHeaderView
rid={rid} rid={rid}
@ -72,6 +72,14 @@ class RoomView extends React.Component {
navigation={navigation} navigation={navigation}
toggleFollowThread={toggleFollowThread} toggleFollowThread={toggleFollowThread}
/> />
),
headerLeft: (
<HeaderBackButton
title={unreadsCount > 999 ? '+999' : unreadsCount || ' '}
backTitleVisible
onPress={() => navigation.goBack()}
tintColor={HEADER_BACK}
/>
) )
}; };
} }
@ -112,6 +120,7 @@ class RoomView extends React.Component {
this.t = props.navigation.getParam('t'); this.t = props.navigation.getParam('t');
this.tmid = props.navigation.getParam('tmid'); this.tmid = props.navigation.getParam('tmid');
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.chats = database.objects('subscriptions').filtered('rid != $0', this.rid);
const canAutoTranslate = RocketChat.canAutoTranslate(); const canAutoTranslate = RocketChat.canAutoTranslate();
this.state = { this.state = {
joined: this.rooms.length > 0, joined: this.rooms.length > 0,
@ -149,6 +158,7 @@ class RoomView extends React.Component {
EventEmitter.addEventListener('connected', this.handleConnected); EventEmitter.addEventListener('connected', this.handleConnected);
} }
safeAddListener(this.rooms, this.updateRoom); safeAddListener(this.rooms, this.updateRoom);
safeAddListener(this.chats, this.updateUnreadCount);
this.mounted = true; this.mounted = true;
}); });
console.timeEnd(`${ this.constructor.name } mount`); console.timeEnd(`${ this.constructor.name } mount`);
@ -222,6 +232,7 @@ class RoomView extends React.Component {
} }
} }
this.rooms.removeAllListeners(); this.rooms.removeAllListeners();
this.chats.removeAllListeners();
if (this.sub && this.sub.stop) { if (this.sub && this.sub.stop) {
this.sub.stop(); this.sub.stop();
} }
@ -283,7 +294,7 @@ class RoomView extends React.Component {
this.setState({ canAutoTranslate }); this.setState({ canAutoTranslate });
}); });
} catch (e) { } catch (e) {
log('err_room_init', e); log(e);
} }
} }
@ -309,7 +320,7 @@ class RoomView extends React.Component {
} }
RocketChat.setReaction(shortname, messageId); RocketChat.setReaction(shortname, messageId);
} catch (e) { } catch (e) {
log('err_room_on_reaction_press', e); log(e);
} }
}; };
@ -329,6 +340,17 @@ class RoomView extends React.Component {
}); });
}, 1000, true) }, 1000, true)
// eslint-disable-next-line react/sort-comp
updateUnreadCount = debounce(() => {
const { navigation } = this.props;
const unreadsCount = this.chats.filtered('archived != true && open == true && unread > 0').reduce((a, b) => a + (b.unread || 0), 0);
if (unreadsCount !== navigation.getParam('unreadsCount')) {
navigation.setParams({
unreadsCount
});
}
}, 300, false)
onThreadPress = debounce((item) => { onThreadPress = debounce((item) => {
const { navigation } = this.props; const { navigation } = this.props;
if (item.tmid) { if (item.tmid) {
@ -405,7 +427,7 @@ class RoomView extends React.Component {
} }
return Promise.resolve(); return Promise.resolve();
} catch (e) { } catch (e) {
log('err_get_messages', e); log(e);
} }
} }
@ -413,7 +435,7 @@ class RoomView extends React.Component {
try { try {
return RocketChat.loadThreadMessages({ tmid: this.tmid }); return RocketChat.loadThreadMessages({ tmid: this.tmid });
} catch (e) { } catch (e) {
log('err_get_thread_messages', e); log(e);
} }
} }
@ -426,7 +448,7 @@ class RoomView extends React.Component {
joined: true joined: true
}); });
} catch (e) { } catch (e) {
log('err_join_room', e); log(e);
} }
}; };
@ -438,8 +460,8 @@ class RoomView extends React.Component {
database.write(() => { database.write(() => {
database.create('threads', buildMessage(EJSON.fromJSONValue(thread)), true); database.create('threads', buildMessage(EJSON.fromJSONValue(thread)), true);
}); });
} catch (error) { } catch (e) {
log('err_fetch_thread_name', error); log(e);
} }
} }
@ -448,10 +470,18 @@ class RoomView extends React.Component {
await RocketChat.toggleFollowMessage(this.tmid, !isFollowingThread); await RocketChat.toggleFollowMessage(this.tmid, !isFollowingThread);
EventEmitter.emit(LISTENER, { message: isFollowingThread ? 'Unfollowed thread' : 'Following thread' }); EventEmitter.emit(LISTENER, { message: isFollowingThread ? 'Unfollowed thread' : 'Following thread' });
} catch (e) { } catch (e) {
log('err_toggle_follow_thread', e); log(e);
} }
} }
navToRoomInfo = (navParam) => {
const { navigation, user } = this.props;
if (navParam.rid === user.id) {
return;
}
navigation.navigate('RoomInfoView', navParam);
}
renderItem = (item, previousItem) => { renderItem = (item, previousItem) => {
const { room, lastOpen, canAutoTranslate } = this.state; const { room, lastOpen, canAutoTranslate } = this.state;
const { const {
@ -500,6 +530,7 @@ class RoomView extends React.Component {
isReadReceiptEnabled={Message_Read_Receipt_Enabled} isReadReceiptEnabled={Message_Read_Receipt_Enabled}
autoTranslateRoom={canAutoTranslate && room.autoTranslate} autoTranslateRoom={canAutoTranslate && room.autoTranslate}
autoTranslateLanguage={room.autoTranslateLanguage} autoTranslateLanguage={room.autoTranslateLanguage}
navToRoomInfo={this.navToRoomInfo}
/> />
); );

View File

@ -3,7 +3,6 @@ import { StyleSheet } from 'react-native';
import { import {
COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_TEXT_DESCRIPTION COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_TEXT_DESCRIPTION
} from '../../constants/colors'; } from '../../constants/colors';
import { isIOS } from '../../utils/deviceInfo';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
export default StyleSheet.create({ export default StyleSheet.create({
@ -65,9 +64,5 @@ export default StyleSheet.create({
fontSize: 16, fontSize: 16,
...sharedStyles.textMedium, ...sharedStyles.textMedium,
...sharedStyles.textColorNormal ...sharedStyles.textColorNormal
},
headerTitleContainerStyle: {
justifyContent: 'flex-start',
left: isIOS ? 40 : 50
} }
}); });

View File

@ -58,7 +58,7 @@ class Sort extends PureComponent {
setSortPreference(param); setSortPreference(param);
RocketChat.saveSortPreference(param); RocketChat.saveSortPreference(param);
} catch (e) { } catch (e) {
log('err_set_sort_preference', e); log(e);
} }
} }

View File

@ -350,7 +350,7 @@ class RoomsListView extends React.Component {
return this.goRoom({ rid: result.room._id, name: username, t: 'd' }); return this.goRoom({ rid: result.room._id, name: username, t: 'd' });
} }
} catch (e) { } catch (e) {
log('err_on_press_item', e); log(e);
} }
} else { } else {
return this.goRoom(item); return this.goRoom(item);
@ -383,7 +383,7 @@ class RoomsListView extends React.Component {
}); });
} }
} catch (e) { } catch (e) {
log('error_toggle_favorite', e); log(e);
} }
} }
@ -399,7 +399,7 @@ class RoomsListView extends React.Component {
}); });
} }
} catch (e) { } catch (e) {
log('error_toggle_read', e); log(e);
} }
} }
@ -413,7 +413,7 @@ class RoomsListView extends React.Component {
}); });
} }
} catch (e) { } catch (e) {
log('error_hide_channel', e); log(e);
} }
} }

View File

@ -8,7 +8,7 @@ import equal from 'deep-equal';
import RCTextInput from '../../containers/TextInput'; import RCTextInput from '../../containers/TextInput';
import RCActivityIndicator from '../../containers/ActivityIndicator'; import RCActivityIndicator from '../../containers/ActivityIndicator';
import styles from './styles'; import styles from './styles';
import Markdown from '../../containers/message/Markdown'; import Markdown from '../../containers/markdown';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import Message from '../../containers/message/Message'; import Message from '../../containers/message/Message';
@ -68,9 +68,9 @@ class SearchMessagesView extends React.Component {
loading: false loading: false
}); });
} }
} catch (error) { } catch (e) {
this.setState({ loading: false }); this.setState({ loading: false });
log('err_search_messages', error); log(e);
} }
}, 1000) }, 1000)

View File

@ -118,7 +118,7 @@ class SelectedUsersView extends React.Component {
await RocketChat.addUsersToRoom(rid); await RocketChat.addUsersToRoom(rid);
navigation.pop(); navigation.pop();
} catch (e) { } catch (e) {
log('err_add_user', e); log(e);
} finally { } finally {
setLoadingInvite(false); setLoadingInvite(false);
} }

View File

@ -83,7 +83,7 @@ class SetUsernameView extends React.Component {
await RocketChat.setUsername(username); await RocketChat.setUsername(username);
await loginRequest({ resume: token }); await loginRequest({ resume: token });
} catch (e) { } catch (e) {
log('err_submit_username', e); log(e);
} }
this.setState({ saving: false }); this.setState({ saving: false });
} }

View File

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { import {
View, Linking, ScrollView, AsyncStorage, SafeAreaView, Switch, Share View, Linking, ScrollView, AsyncStorage, SafeAreaView, Switch, Text, Share
} from 'react-native'; } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { toggleMarkdown as toggleMarkdownAction } from '../../actions/markdown'; import { toggleMarkdown as toggleMarkdownAction } from '../../actions/markdown';
import { toggleCrashReport as toggleCrashReportAction } from '../../actions/crashReport';
import { SWITCH_TRACK_COLOR } from '../../constants/colors'; import { SWITCH_TRACK_COLOR } from '../../constants/colors';
import { DrawerButton } from '../../containers/HeaderButton'; import { DrawerButton } from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
@ -13,16 +14,25 @@ import ListItem from '../../containers/ListItem';
import { DisclosureImage } from '../../containers/DisclosureIndicator'; import { DisclosureImage } from '../../containers/DisclosureIndicator';
import Separator from '../../containers/Separator'; import Separator from '../../containers/Separator';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { MARKDOWN_KEY } from '../../lib/rocketchat'; import { MARKDOWN_KEY, CRASH_REPORT_KEY } from '../../lib/rocketchat';
import { getReadableVersion, getDeviceModel, isAndroid } from '../../utils/deviceInfo'; import { getReadableVersion, getDeviceModel, isAndroid } from '../../utils/deviceInfo';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { showErrorAlert } from '../../utils/info'; import { showErrorAlert } from '../../utils/info';
import styles from './styles'; import styles from './styles';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import { loggerConfig, analytics } from '../../utils/log';
import { PLAY_MARKET_LINK, APP_STORE_LINK, LICENSE_LINK } from '../../constants/links'; import { PLAY_MARKET_LINK, APP_STORE_LINK, LICENSE_LINK } from '../../constants/links';
const SectionSeparator = React.memo(() => <View style={styles.sectionSeparatorBorder} />); const SectionSeparator = React.memo(() => <View style={styles.sectionSeparatorBorder} />);
const ItemInfo = React.memo(({ info }) => (
<View style={styles.infoContainer}>
<Text style={styles.infoText}>{info}</Text>
</View>
));
ItemInfo.propTypes = {
info: PropTypes.string
};
class SettingsView extends React.Component { class SettingsView extends React.Component {
static navigationOptions = ({ navigation }) => ({ static navigationOptions = ({ navigation }) => ({
@ -34,7 +44,9 @@ class SettingsView extends React.Component {
navigation: PropTypes.object, navigation: PropTypes.object,
server: PropTypes.object, server: PropTypes.object,
useMarkdown: PropTypes.bool, useMarkdown: PropTypes.bool,
toggleMarkdown: PropTypes.func allowCrashReport: PropTypes.bool,
toggleMarkdown: PropTypes.func,
toggleCrashReport: PropTypes.func
} }
toggleMarkdown = (value) => { toggleMarkdown = (value) => {
@ -43,6 +55,20 @@ class SettingsView extends React.Component {
toggleMarkdown(value); toggleMarkdown(value);
} }
toggleCrashReport = (value) => {
AsyncStorage.setItem(CRASH_REPORT_KEY, JSON.stringify(value));
const { toggleCrashReport } = this.props;
toggleCrashReport(value);
loggerConfig.autoNotify = value;
analytics().setAnalyticsCollectionEnabled(value);
if (value) {
loggerConfig.clearBeforeSendCallbacks();
} else {
loggerConfig.registerBeforeSendCallback(() => false);
}
}
navigateToRoom = (room) => { navigateToRoom = (room) => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate(room); navigation.navigate(room);
@ -81,6 +107,17 @@ class SettingsView extends React.Component {
); );
} }
renderCrashReportSwitch = () => {
const { allowCrashReport } = this.props;
return (
<Switch
value={allowCrashReport}
trackColor={SWITCH_TRACK_COLOR}
onValueChange={this.toggleCrashReport}
/>
);
}
render() { render() {
const { server } = this.props; const { server } = this.props;
return ( return (
@ -88,7 +125,7 @@ class SettingsView extends React.Component {
<StatusBar /> <StatusBar />
<ScrollView <ScrollView
{...scrollPersistTaps} {...scrollPersistTaps}
contentContainerStyle={sharedStyles.listContentContainer} contentContainerStyle={[sharedStyles.listContentContainer, styles.listWithoutBorderBottom]}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
testID='settings-view-list' testID='settings-view-list'
> >
@ -148,6 +185,18 @@ class SettingsView extends React.Component {
testID='settings-view-markdown' testID='settings-view-markdown'
right={() => this.renderMarkdownSwitch()} right={() => this.renderMarkdownSwitch()}
/> />
<SectionSeparator />
<ListItem
title={I18n.t('Send_crash_report')}
testID='settings-view-crash-report'
right={() => this.renderCrashReportSwitch()}
/>
<Separator />
<ItemInfo
info={I18n.t('Crash_report_disclaimer')}
/>
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>
); );
@ -156,11 +205,13 @@ class SettingsView extends React.Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
server: state.server, server: state.server,
useMarkdown: state.markdown.useMarkdown useMarkdown: state.markdown.useMarkdown,
allowCrashReport: state.crashReport.allowCrashReport
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
toggleMarkdown: params => dispatch(toggleMarkdownAction(params)) toggleMarkdown: params => dispatch(toggleMarkdownAction(params)),
toggleCrashReport: params => dispatch(toggleCrashReportAction(params))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(SettingsView); export default connect(mapStateToProps, mapDispatchToProps)(SettingsView);

View File

@ -8,5 +8,18 @@ export default StyleSheet.create({
...sharedStyles.separatorVertical, ...sharedStyles.separatorVertical,
backgroundColor: COLOR_BACKGROUND_CONTAINER, backgroundColor: COLOR_BACKGROUND_CONTAINER,
height: 10 height: 10
},
listWithoutBorderBottom: {
borderBottomWidth: 0
},
infoContainer: {
padding: 15,
paddingBottom: 40,
backgroundColor: COLOR_BACKGROUND_CONTAINER
},
infoText: {
fontSize: 14,
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
} }
}); });

View File

@ -132,7 +132,7 @@ class ShareListView extends React.Component {
value, fileInfo, isMedia, mediaLoading: false value, fileInfo, isMedia, mediaLoading: false
}); });
} catch (e) { } catch (e) {
log('err_process_media_share_extension', e); log(e);
this.setState({ mediaLoading: false }); this.setState({ mediaLoading: false });
} }

View File

@ -116,7 +116,7 @@ class ShareView extends React.Component {
try { try {
await RocketChat.sendFileMessage(rid, fileMessage, undefined, server, user); await RocketChat.sendFileMessage(rid, fileMessage, undefined, server, user);
} catch (e) { } catch (e) {
log('err_send_media_message', e); log(e);
} }
} }
} }
@ -127,8 +127,8 @@ class ShareView extends React.Component {
if (value !== '' && rid !== '') { if (value !== '' && rid !== '') {
try { try {
await RocketChat.sendMessage(rid, value, undefined, user); await RocketChat.sendMessage(rid, value, undefined, user);
} catch (error) { } catch (e) {
log('err_share_extension_send_message', error); log(e);
} }
} }
}; };

View File

@ -157,7 +157,7 @@ class Sidebar extends Component {
try { try {
RocketChat.setUserPresenceDefaultStatus(item.id); RocketChat.setUserPresenceDefaultStatus(item.id);
} catch (e) { } catch (e) {
log('err_set_user_presence_default_status', e); log(e);
} }
} }
}} }}

38
app/views/TableView.js Normal file
View File

@ -0,0 +1,38 @@
import React from 'react';
import { ScrollView } from 'react-native';
import PropTypes from 'prop-types';
import I18n from '../i18n';
import { isIOS } from '../utils/deviceInfo';
export default class TableView extends React.Component {
static navigationOptions = () => ({
title: I18n.t('Table')
});
static propTypes = {
navigation: PropTypes.object
}
render() {
const { navigation } = this.props;
const renderRows = navigation.getParam('renderRows');
const tableWidth = navigation.getParam('tableWidth');
if (isIOS) {
return (
<ScrollView contentContainerStyle={{ width: tableWidth }}>
{renderRows()}
</ScrollView>
);
}
return (
<ScrollView>
<ScrollView horizontal>
{renderRows()}
</ScrollView>
</ScrollView>
);
}
}

Some files were not shown because too many files have changed in this diff Show More