[RELEASE] Merge beta into master (#1174)
This commit is contained in:
parent
494890ad84
commit
d524ccdb72
|
@ -116,10 +116,26 @@ jobs:
|
|||
steps:
|
||||
- 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:
|
||||
name: Restore NPM cache
|
||||
key: node-modules-{{ checksum "yarn.lock" }}
|
||||
|
||||
- run:
|
||||
name: Install React Native CLI
|
||||
command: |
|
||||
npm i -g react-native-cli
|
||||
|
||||
- run:
|
||||
name: Install NPM modules
|
||||
command: |
|
||||
|
@ -148,12 +164,32 @@ jobs:
|
|||
fi
|
||||
|
||||
echo -e "VERSIONCODE=$CIRCLE_BUILD_NUM" >> ./gradle.properties
|
||||
echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties
|
||||
|
||||
- run:
|
||||
name: Set Google Services
|
||||
command: |
|
||||
cd android/app
|
||||
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:
|
||||
name: Build Android App
|
||||
|
@ -215,6 +251,7 @@ jobs:
|
|||
- run:
|
||||
name: Install NPM modules
|
||||
command: |
|
||||
yarn global add react-native react-native-cli
|
||||
yarn
|
||||
|
||||
- run:
|
||||
|
@ -229,11 +266,26 @@ jobs:
|
|||
cp GoogleService-Info.prod.plist GoogleService-Info.plist
|
||||
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:
|
||||
name: Fastlane Build
|
||||
no_output_timeout: 1200
|
||||
command: |
|
||||
agvtool new-version -all $CIRCLE_BUILD_NUM
|
||||
/usr/libexec/PlistBuddy -c "Set BugsnagAPIKey $BUGSNAG_KEY" ./RocketChatRN/Info.plist
|
||||
|
||||
if [[ $MATCH_KEYCHAIN_NAME ]]; then
|
||||
bundle exec fastlane ios release
|
||||
|
@ -288,7 +340,7 @@ jobs:
|
|||
- run:
|
||||
name: Fastlane Tesflight Upload
|
||||
command: |
|
||||
bundle exec fastlane pilot upload --ipa ios/RocketChatRN.ipa --changelog "$(sh ../.circleci/changelog.sh)"
|
||||
bundle exec fastlane ios beta
|
||||
working_directory: ios
|
||||
|
||||
- save_cache:
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,7 @@
|
|||
apply plugin: "com.android.application"
|
||||
apply plugin: "io.fabric"
|
||||
apply plugin: "com.google.firebase.firebase-perf"
|
||||
apply plugin: 'com.bugsnag.android.gradle'
|
||||
|
||||
import com.android.build.OutputFile
|
||||
|
||||
|
@ -135,8 +136,9 @@ android {
|
|||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode VERSIONCODE as Integer
|
||||
versionName "1.18.0"
|
||||
versionName "1.19.0"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
|
|
@ -52,6 +52,9 @@
|
|||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="com.bugsnag.android.API_KEY"
|
||||
android:value="${BugsnagAPIKey}" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -7,6 +7,7 @@ import org.unimodules.core.interfaces.Package;
|
|||
public class BasePackageList {
|
||||
public List<Package> getPackageList() {
|
||||
return Arrays.<Package>asList(
|
||||
new expo.modules.av.AVPackage(),
|
||||
new expo.modules.constants.ConstantsPackage(),
|
||||
new expo.modules.filesystem.FileSystemPackage(),
|
||||
new expo.modules.haptics.HapticsPackage(),
|
||||
|
|
|
@ -24,6 +24,7 @@ buildscript {
|
|||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath 'io.fabric.tools:gradle:1.28.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
|
||||
// in the individual module build.gradle files
|
||||
|
|
|
@ -22,3 +22,4 @@ org.gradle.jvmargs=-Xmx2048M -XX\:MaxHeapSize\=32g
|
|||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
VERSIONCODE=999999999
|
||||
BugsnagAPIKey=""
|
||||
|
|
|
@ -18,4 +18,5 @@ if (__DEV__) {
|
|||
Reactotron.clear();
|
||||
console.warn = Reactotron.log;
|
||||
console.log = Reactotron.log;
|
||||
console.disableYellowBox = true;
|
||||
}
|
||||
|
|
|
@ -74,3 +74,4 @@ export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
|
|||
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
|
||||
export const NOTIFICATION = createRequestTypes('NOTIFICATION', ['RECEIVED', 'REMOVE']);
|
||||
export const TOGGLE_MARKDOWN = 'TOGGLE_MARKDOWN';
|
||||
export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT';
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import * as types from './actionsTypes';
|
||||
|
||||
export function toggleCrashReport(value) {
|
||||
return {
|
||||
type: types.TOGGLE_CRASH_REPORT,
|
||||
payload: value
|
||||
};
|
||||
}
|
|
@ -23,10 +23,11 @@ export function selectServerFailure() {
|
|||
};
|
||||
}
|
||||
|
||||
export function serverRequest(server) {
|
||||
export function serverRequest(server, certificate = null) {
|
||||
return {
|
||||
type: SERVER.REQUEST,
|
||||
server
|
||||
server,
|
||||
certificate
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet, SafeAreaView
|
||||
} from 'react-native';
|
||||
|
@ -6,7 +6,7 @@ import FastImage from 'react-native-fast-image';
|
|||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-native-modal';
|
||||
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 { COLOR_WHITE } from '../constants/colors';
|
||||
|
@ -38,6 +38,18 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
indicator: {
|
||||
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) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const uri = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl);
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<VideoPlayer
|
||||
<>
|
||||
<Video
|
||||
source={{ uri }}
|
||||
onBack={onClose}
|
||||
disableVolume
|
||||
rate={1.0}
|
||||
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;
|
||||
|
@ -95,11 +118,11 @@ const FileModal = React.memo(({
|
|||
onBackdropPress={onClose}
|
||||
onBackButtonPress={onClose}
|
||||
onSwipeComplete={onClose}
|
||||
swipeDirection={['up', 'left', 'right', 'down']}
|
||||
swipeDirection={['up', 'down']}
|
||||
>
|
||||
<ModalContent attachment={attachment} onClose={onClose} user={user} baseUrl={baseUrl} />
|
||||
</Modal>
|
||||
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible);
|
||||
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible && prevProps.loading === nextProps.loading);
|
||||
|
||||
FileModal.propTypes = {
|
||||
isVisible: PropTypes.bool,
|
||||
|
|
|
@ -310,8 +310,8 @@ class MessageActions extends React.Component {
|
|||
try {
|
||||
await RocketChat.reportMessage(actionMessage._id);
|
||||
Alert.alert(I18n.t('Message_Reported'));
|
||||
} catch (err) {
|
||||
log('err_report_message', err);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -327,8 +327,8 @@ class MessageActions extends React.Component {
|
|||
if (!translatedMessage) {
|
||||
await RocketChat.translateMessage(actionMessage, room.autoTranslateLanguage);
|
||||
}
|
||||
} catch (err) {
|
||||
log('err_toggle_translation', err);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
|||
import moment from 'moment';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Markdown from '../message/Markdown';
|
||||
import Markdown from '../markdown';
|
||||
import { getCustomEmoji } from '../message/utils';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
@ -50,6 +50,7 @@ const styles = StyleSheet.create({
|
|||
|
||||
class ReplyPreview extends Component {
|
||||
static propTypes = {
|
||||
useMarkdown: PropTypes.bool,
|
||||
message: PropTypes.object.isRequired,
|
||||
Message_TimeFormat: PropTypes.string.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
|
@ -68,7 +69,7 @@ class ReplyPreview extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
message, Message_TimeFormat, baseUrl, username
|
||||
message, Message_TimeFormat, baseUrl, username, useMarkdown
|
||||
} = this.props;
|
||||
const time = moment(message.ts).format(Message_TimeFormat);
|
||||
return (
|
||||
|
@ -78,7 +79,7 @@ class ReplyPreview extends Component {
|
|||
<Text style={styles.username}>{message.u.username}</Text>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</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>
|
||||
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
|
||||
</View>
|
||||
|
@ -87,6 +88,7 @@ class ReplyPreview extends Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
useMarkdown: state.markdown.useMarkdown,
|
||||
Message_TimeFormat: state.settings.Message_TimeFormat,
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
|
||||
});
|
||||
|
|
|
@ -293,7 +293,7 @@ class MessageBox extends Component {
|
|||
try {
|
||||
RocketChat.executeCommandPreview(command, params, rid, item);
|
||||
} catch (e) {
|
||||
log('onPressCommandPreview', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -362,7 +362,7 @@ class MessageBox extends Component {
|
|||
try {
|
||||
database.create('users', user, true);
|
||||
} catch (e) {
|
||||
log('err_create_users', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -468,7 +468,7 @@ class MessageBox extends Component {
|
|||
this.setState({ commandPreview: preview.items });
|
||||
} catch (e) {
|
||||
this.showCommandPreview = false;
|
||||
log('command Preview', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -504,7 +504,7 @@ class MessageBox extends Component {
|
|||
try {
|
||||
await RocketChat.sendFileMessage(rid, fileInfo, tmid, server, user);
|
||||
} 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);
|
||||
this.showUploadModal(image);
|
||||
} catch (e) {
|
||||
log('err_take_photo', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -522,7 +522,7 @@ class MessageBox extends Component {
|
|||
const video = await ImagePicker.openCamera(this.videoPickerConfig);
|
||||
this.showUploadModal(video);
|
||||
} catch (e) {
|
||||
log('err_take_video', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -531,7 +531,7 @@ class MessageBox extends Component {
|
|||
const image = await ImagePicker.openPicker(this.libraryPickerConfig);
|
||||
this.showUploadModal(image);
|
||||
} catch (e) {
|
||||
log('err_choose_from_library', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -546,9 +546,9 @@ class MessageBox extends Component {
|
|||
mime: res.type,
|
||||
path: res.uri
|
||||
});
|
||||
} catch (error) {
|
||||
if (!DocumentPicker.isCancel(error)) {
|
||||
log('chooseFile', error);
|
||||
} catch (e) {
|
||||
if (!DocumentPicker.isCancel(e)) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -618,7 +618,7 @@ class MessageBox extends Component {
|
|||
if (e && e.error === 'error-file-too-large') {
|
||||
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);
|
||||
RocketChat.runSlashCommand(command, roomId, messageWithoutCommand);
|
||||
} catch (e) {
|
||||
log('slashCommand', e);
|
||||
log(e);
|
||||
}
|
||||
this.clearInput();
|
||||
return;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
});
|
|
@ -9,7 +9,7 @@ import moment from 'moment';
|
|||
import equal from 'deep-equal';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import Markdown from './Markdown';
|
||||
import Markdown from '../markdown';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY } from '../../constants/colors';
|
||||
|
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import I18n from '../../i18n';
|
||||
import styles from './styles';
|
||||
import Markdown from './Markdown';
|
||||
import Markdown from '../markdown';
|
||||
import { getInfoMessage } from './utils';
|
||||
|
||||
const Content = React.memo((props) => {
|
||||
|
@ -21,13 +21,15 @@ const Content = React.memo((props) => {
|
|||
<Markdown
|
||||
msg={props.msg}
|
||||
baseUrl={props.baseUrl}
|
||||
getCustomEmoji={props.getCustomEmoji}
|
||||
username={props.user.username}
|
||||
isEdited={props.isEdited}
|
||||
mentions={props.mentions}
|
||||
channels={props.channels}
|
||||
numberOfLines={props.tmid ? 1 : 0}
|
||||
getCustomEmoji={props.getCustomEmoji}
|
||||
useMarkdown={props.useMarkdown}
|
||||
channels={props.channels}
|
||||
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 = {
|
||||
isTemp: PropTypes.bool,
|
||||
isInfo: PropTypes.bool,
|
||||
isEdited: PropTypes.bool,
|
||||
useMarkdown: PropTypes.bool,
|
||||
tmid: PropTypes.string,
|
||||
msg: PropTypes.string,
|
||||
isEdited: PropTypes.bool,
|
||||
useMarkdown: PropTypes.bool,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
getCustomEmoji: PropTypes.func,
|
||||
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
getCustomEmoji: PropTypes.func
|
||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
navToRoomInfo: PropTypes.func
|
||||
};
|
||||
Content.displayName = 'MessageContent';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import FastImage from 'react-native-fast-image';
|
|||
import equal from 'deep-equal';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import Markdown from './Markdown';
|
||||
import Markdown from '../markdown';
|
||||
import styles from './styles';
|
||||
import { formatAttachmentUrl } from '../../lib/utils';
|
||||
|
||||
|
|
|
@ -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}>
|
||||
{content}
|
||||
</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}>
|
||||
#{content}
|
||||
</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;
|
|
@ -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);
|
||||
}
|
|
@ -1,14 +1,23 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
|
||||
import Avatar from '../Avatar';
|
||||
import styles from './styles';
|
||||
|
||||
const MessageAvatar = React.memo(({
|
||||
isHeader, avatar, author, baseUrl, user, small
|
||||
isHeader, avatar, author, baseUrl, user, small, navToRoomInfo
|
||||
}) => {
|
||||
if (isHeader) {
|
||||
const navParam = {
|
||||
t: 'd',
|
||||
rid: author._id
|
||||
};
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => navToRoomInfo(navParam)}
|
||||
disabled={author._id === user.id}
|
||||
>
|
||||
<Avatar
|
||||
style={small ? styles.avatarSmall : styles.avatar}
|
||||
text={avatar ? '' : author.username}
|
||||
|
@ -19,6 +28,7 @@ const MessageAvatar = React.memo(({
|
|||
userId={user.id}
|
||||
token={user.token}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
@ -30,7 +40,8 @@ MessageAvatar.propTypes = {
|
|||
author: PropTypes.obj,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.obj,
|
||||
small: PropTypes.bool
|
||||
small: PropTypes.bool,
|
||||
navToRoomInfo: PropTypes.func
|
||||
};
|
||||
MessageAvatar.displayName = 'MessageAvatar';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import moment from 'moment';
|
|||
import Touchable from 'react-native-platform-touchable';
|
||||
import isEqual from 'deep-equal';
|
||||
|
||||
import Markdown from './Markdown';
|
||||
import Markdown from '../markdown';
|
||||
import openLink from '../../utils/openLink';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER } from '../../constants/colors';
|
||||
|
|
|
@ -4,13 +4,13 @@ import { StyleSheet } from 'react-native';
|
|||
import Touchable from 'react-native-platform-touchable';
|
||||
import isEqual from 'deep-equal';
|
||||
|
||||
import Markdown from './Markdown';
|
||||
import Markdown from '../markdown';
|
||||
import openLink from '../../utils/openLink';
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
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 styles = StyleSheet.create({
|
||||
|
|
|
@ -39,7 +39,8 @@ export default class MessageContainer extends React.Component {
|
|||
toggleReactionPicker: PropTypes.func,
|
||||
fetchThreadName: PropTypes.func,
|
||||
onOpenFileModal: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func
|
||||
onReactionLongPress: PropTypes.func,
|
||||
navToRoomInfo: PropTypes.func
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -199,7 +200,7 @@ export default class MessageContainer extends React.Component {
|
|||
|
||||
render() {
|
||||
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;
|
||||
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
|
||||
|
@ -263,6 +264,7 @@ export default class MessageContainer extends React.Component {
|
|||
onDiscussionPress={this.onDiscussionPress}
|
||||
onOpenFileModal={onOpenFileModal}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
navToRoomInfo={navToRoomInfo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
import { StyleSheet, Platform } from 'react-native';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER
|
||||
COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE
|
||||
} from '../../constants/colors';
|
||||
|
||||
const codeFontFamily = Platform.select({
|
||||
ios: { fontFamily: 'Courier New' },
|
||||
android: { fontFamily: 'monospace' }
|
||||
});
|
||||
|
||||
export default StyleSheet.create({
|
||||
root: {
|
||||
flexDirection: 'row'
|
||||
|
@ -34,30 +29,6 @@ export default StyleSheet.create({
|
|||
flexDirection: 'row'
|
||||
// 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 },
|
||||
marginTop: {
|
||||
marginTop: 6
|
||||
|
@ -143,28 +114,6 @@ export default StyleSheet.create({
|
|||
fontSize: 14,
|
||||
...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: {
|
||||
// flex: 1,
|
||||
flexDirection: 'column',
|
||||
|
@ -186,29 +135,15 @@ export default StyleSheet.create({
|
|||
height: 300,
|
||||
resizeMode: 'contain'
|
||||
},
|
||||
edited: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorDescription,
|
||||
text: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
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: {
|
||||
color: COLOR_PRIMARY,
|
||||
textInfo: {
|
||||
fontStyle: 'italic',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
startedDiscussion: {
|
||||
|
|
|
@ -87,17 +87,20 @@ export default {
|
|||
alerts: 'alerts',
|
||||
All_users_in_the_channel_can_write_new_messages: 'All users in the channel can write new messages',
|
||||
All: 'All',
|
||||
All_Messages: 'All Messages',
|
||||
Allow_Reactions: 'Allow Reactions',
|
||||
Alphabetical: 'Alphabetical',
|
||||
and_more: 'and more',
|
||||
and: 'and',
|
||||
announcement: 'announcement',
|
||||
Announcement: 'Announcement',
|
||||
Apply_Your_Certificate: 'Apply Your Certificate',
|
||||
ARCHIVE: 'ARCHIVE',
|
||||
archive: 'archive',
|
||||
are_typing: 'are typing',
|
||||
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}}?',
|
||||
Audio: 'Audio',
|
||||
Authenticating: 'Authenticating',
|
||||
Auto_Translate: 'Auto-Translate',
|
||||
Avatar_changed_successfully: 'Avatar changed successfully!',
|
||||
|
@ -135,28 +138,34 @@ export default {
|
|||
Copied_to_clipboard: 'Copied to clipboard!',
|
||||
Copy: 'Copy',
|
||||
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_Channel: 'Create Channel',
|
||||
Created_snippet: 'Created a snippet',
|
||||
Create_a_new_workspace: 'Create a new workspace',
|
||||
Create: 'Create',
|
||||
Default: 'Default',
|
||||
Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.',
|
||||
delete: 'delete',
|
||||
Delete: 'Delete',
|
||||
DELETE: 'DELETE',
|
||||
description: 'description',
|
||||
Description: 'Description',
|
||||
DESKTOP_OPTIONS: 'DESKTOP OPTIONS',
|
||||
Directory: 'Directory',
|
||||
Direct_Messages: 'Direct Messages',
|
||||
Disable_notifications: 'Disable notifications',
|
||||
Discussions: 'Discussions',
|
||||
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?',
|
||||
edit: 'edit',
|
||||
edited: 'edited',
|
||||
Edit: 'Edit',
|
||||
Email_or_password_field_is_empty: 'Email or password field is empty',
|
||||
Email: 'Email',
|
||||
EMAIL: 'EMAIL',
|
||||
email: 'e-mail',
|
||||
Enable_Auto_Translate: 'Enable Auto-Translate',
|
||||
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: 'Forgot password',
|
||||
Forgot_Password: 'Forgot Password',
|
||||
Full_table: 'Click to see full table',
|
||||
Group_by_favorites: 'Group favorites',
|
||||
Group_by_type: 'Group by type',
|
||||
Hide: 'Hide',
|
||||
Has_joined_the_channel: 'Has joined the channel',
|
||||
Has_joined_the_conversation: 'Has joined the conversation',
|
||||
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',
|
||||
Invite: 'Invite',
|
||||
is_a_valid_RocketChat_instance: 'is a valid Rocket.Chat instance',
|
||||
|
@ -243,9 +255,13 @@ export default {
|
|||
No_Reactions: 'No Reactions',
|
||||
No_Read_Receipts: 'No Read Receipts',
|
||||
Not_logged: 'Not logged',
|
||||
Nothing: 'Nothing',
|
||||
Nothing_to_save: 'Nothing to save!',
|
||||
Notify_active_in_this_room: 'Notify active users 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',
|
||||
Oops: 'Oops!',
|
||||
Online: 'Online',
|
||||
|
@ -269,6 +285,8 @@ export default {
|
|||
Profile: 'Profile',
|
||||
Public_Channel: 'Public Channel',
|
||||
Public: 'Public',
|
||||
PUSH_NOTIFICATIONS: 'PUSH NOTIFICATIONS',
|
||||
Push_Notifications_Alert_Info: 'These notifications are delivered to you when the app is not open',
|
||||
Quote: 'Quote',
|
||||
Reactions_are_disabled: 'Reactions are disabled',
|
||||
Reactions_are_enabled: 'Reactions are enabled',
|
||||
|
@ -277,6 +295,8 @@ export default {
|
|||
Read_Only_Channel: 'Read Only Channel',
|
||||
Read_Only: 'Read Only',
|
||||
Read_Receipt: 'Read Receipt',
|
||||
Receive_Group_Mentions: 'Receive Group Mentions',
|
||||
Receive_Group_Mentions_Info: 'Receive @all and @here mentions',
|
||||
Register: 'Register',
|
||||
Repeat_Password: 'Repeat Password',
|
||||
Replied_on: 'Replied on:',
|
||||
|
@ -284,6 +304,8 @@ export default {
|
|||
reply: 'reply',
|
||||
Reply: 'Reply',
|
||||
Report: 'Report',
|
||||
Receive_Notification: 'Receive Notification',
|
||||
Receive_notifications_from: 'Receive notifications from {{name}}',
|
||||
Resend: 'Resend',
|
||||
Reset_password: 'Reset password',
|
||||
resetting_password: 'resetting password',
|
||||
|
@ -310,6 +332,7 @@ export default {
|
|||
Search_by: 'Search by',
|
||||
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.',
|
||||
Seconds: '{{second}} seconds',
|
||||
Select_Avatar: 'Select Avatar',
|
||||
Select_Server: 'Select Server',
|
||||
Select_Users: 'Select Users',
|
||||
|
@ -327,10 +350,13 @@ export default {
|
|||
Settings_succesfully_changed: 'Settings succesfully changed!',
|
||||
Share: 'Share',
|
||||
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_Up: 'Sign Up',
|
||||
Some_field_is_invalid_or_empty: 'Some field is invalid or empty',
|
||||
Sorting_by: 'Sorting by {{key}}',
|
||||
Sound: 'Sound',
|
||||
Star_room: 'Star room',
|
||||
Star: 'Star',
|
||||
Starred_Messages: 'Starred Messages',
|
||||
|
@ -339,6 +365,7 @@ export default {
|
|||
Start_of_conversation: 'Start of conversation',
|
||||
Started_discussion: 'Started a discussion:',
|
||||
Submit: 'Submit',
|
||||
Table: 'Table',
|
||||
Take_a_photo: 'Take a photo',
|
||||
Take_a_video: 'Take a video',
|
||||
tap_to_change_status: 'tap to change status',
|
||||
|
@ -404,8 +431,9 @@ export default {
|
|||
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.',
|
||||
Your_certificate: 'Your Certificate',
|
||||
Version_no: 'Version: {{version}}',
|
||||
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
|
||||
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.'
|
||||
};
|
||||
|
|
|
@ -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: 'Esqueci minha senha',
|
||||
Forgot_Password: 'Esqueci minha senha',
|
||||
Full_table: 'Clique para ver a tabela completa',
|
||||
Group_by_favorites: 'Agrupar favoritos',
|
||||
Group_by_type: 'Agrupar por tipo',
|
||||
Has_joined_the_channel: 'Entrou no canal',
|
||||
|
@ -326,6 +327,7 @@ export default {
|
|||
Start_of_conversation: 'Início da conversa',
|
||||
Started_discussion: 'Iniciou uma discussão:',
|
||||
Submit: 'Enviar',
|
||||
Table: 'Tabela',
|
||||
Take_a_photo: 'Tirar uma foto',
|
||||
Take_a_video: 'Gravar um vídeo',
|
||||
Terms_of_Service: ' Termos de Serviço ',
|
||||
|
|
20
app/index.js
20
app/index.js
|
@ -16,7 +16,9 @@ import { initializePushNotifications, onNotification } from './notifications/pus
|
|||
import store from './lib/createStore';
|
||||
import NotificationBadge from './notifications/inApp';
|
||||
import { defaultHeader, onNavigationStateChange } from './utils/navigation';
|
||||
import { loggerConfig, analytics } from './utils/log';
|
||||
import Toast from './containers/Toast';
|
||||
import RocketChat from './lib/rocketchat';
|
||||
|
||||
useScreens();
|
||||
|
||||
|
@ -119,6 +121,12 @@ const ChatsStack = createStackNavigator({
|
|||
},
|
||||
DirectoryView: {
|
||||
getScreen: () => require('./views/DirectoryView').default
|
||||
},
|
||||
TableView: {
|
||||
getScreen: () => require('./views/TableView').default
|
||||
},
|
||||
NotificationPrefView: {
|
||||
getScreen: () => require('./views/NotificationPreferencesView').default
|
||||
}
|
||||
}, {
|
||||
defaultNavigationOptions: defaultHeader
|
||||
|
@ -256,6 +264,7 @@ export default class Root extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.init();
|
||||
this.initCrashReport();
|
||||
}
|
||||
|
||||
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() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
|
|
|
@ -68,7 +68,7 @@ export default function() {
|
|||
database.delete(emojiRecord);
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_get_emojis_delete', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ export default function() {
|
|||
);
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_get_custom_emojis', e);
|
||||
log(e);
|
||||
return resolve();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ const create = (permissions) => {
|
|||
try {
|
||||
database.create('permissions', permission, true);
|
||||
} catch (e) {
|
||||
log('err_get_permissions_create', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ export default function() {
|
|||
database.delete(permission);
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_get_permissions_delete', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ export default function() {
|
|||
);
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_get_permissions', e);
|
||||
log(e);
|
||||
return resolve();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,14 +21,14 @@ export default function() {
|
|||
try {
|
||||
database.create('roles', role, true);
|
||||
} catch (e) {
|
||||
log('err_get_roles_create', e);
|
||||
log(e);
|
||||
}
|
||||
}));
|
||||
return resolve();
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_get_roles', e);
|
||||
log(e);
|
||||
return resolve();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ function updateServer(param) {
|
|||
try {
|
||||
database.databases.serversDB.create('servers', { id: reduxStore.getState().server.server, ...param }, true);
|
||||
} catch (e) {
|
||||
log('err_get_settings_update_server', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export default async function() {
|
|||
try {
|
||||
database.create('settings', { ...setting, _updatedAt: new Date() }, true);
|
||||
} catch (e) {
|
||||
log('err_get_settings_create', e);
|
||||
log(e);
|
||||
}
|
||||
|
||||
if (setting._id === 'Site_Name') {
|
||||
|
@ -61,6 +61,6 @@ export default async function() {
|
|||
updateServer.call(this, { iconURL });
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_get_settings', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export default function() {
|
|||
const result = await this.sdk.get('commands.list');
|
||||
|
||||
if (!result.success) {
|
||||
log('getSlashCommand fetch', result);
|
||||
console.log(result);
|
||||
return resolve();
|
||||
}
|
||||
|
||||
|
@ -22,14 +22,14 @@ export default function() {
|
|||
try {
|
||||
database.create('slashCommand', command, true);
|
||||
} catch (e) {
|
||||
log('get_slash_command', e);
|
||||
log(e);
|
||||
}
|
||||
}));
|
||||
return resolve();
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_get_slash_command', e);
|
||||
log(e);
|
||||
return resolve();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -34,12 +34,6 @@ export const merge = (subscription, room) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (subscription.mobilePushNotifications === 'nothing') {
|
||||
subscription.notifications = true;
|
||||
} else {
|
||||
subscription.notifications = false;
|
||||
}
|
||||
|
||||
if (!subscription.name) {
|
||||
subscription.name = subscription.fname;
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ async function load({ rid: roomId, latest, t }) {
|
|||
return [];
|
||||
}
|
||||
return data.messages;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ export default function loadMessagesForRoom(...args) {
|
|||
database.create('threadMessages', message, true);
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_load_messages_for_room_create', e);
|
||||
log(e);
|
||||
}
|
||||
}));
|
||||
return resolve(data);
|
||||
|
@ -61,7 +61,7 @@ export default function loadMessagesForRoom(...args) {
|
|||
return resolve([]);
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_load_messages_for_room', e);
|
||||
log(e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -45,7 +45,7 @@ export default function loadMissedMessages(...args) {
|
|||
database.create('threadMessages', message, true);
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_load_missed_messages_create', e);
|
||||
log(e);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
@ -65,14 +65,14 @@ export default function loadMissedMessages(...args) {
|
|||
});
|
||||
});
|
||||
} catch (e) {
|
||||
log('err_load_missed_messages_delete', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
} catch (e) {
|
||||
log('err_load_missed_messages', e);
|
||||
log(e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -34,7 +34,7 @@ export default function loadThreadMessages({ tmid, offset = 0 }) {
|
|||
message.rid = tmid;
|
||||
database.create('threadMessages', message, true);
|
||||
} catch (e) {
|
||||
log('err_load_thread_messages_create', e);
|
||||
log(e);
|
||||
}
|
||||
}));
|
||||
return resolve(data);
|
||||
|
@ -43,7 +43,7 @@ export default function loadThreadMessages({ tmid, offset = 0 }) {
|
|||
return resolve([]);
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_load_thread_messages', e);
|
||||
log(e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -18,6 +18,6 @@ export default async function readMessages(rid) {
|
|||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
log('err_read_messages', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export function cancelUpload(path) {
|
|||
try {
|
||||
database.delete(upload);
|
||||
} catch (e) {
|
||||
log('err_send_file_message_delete_upload', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
delete uploadQueue[path];
|
||||
|
@ -45,7 +45,7 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
|
|||
try {
|
||||
database.create('uploads', fileInfo, true);
|
||||
} 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 {
|
||||
database.create('uploads', fileInfo, true);
|
||||
} 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);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
log('err_send_file_message_delete_upload', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -100,30 +100,30 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
|
|||
database.create('uploads', fileInfo, true);
|
||||
const response = JSON.parse(xhr.response);
|
||||
reject(response);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
log('err_send_file_message_create_upload_3', err);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = (e) => {
|
||||
xhr.onerror = (error) => {
|
||||
database.write(() => {
|
||||
fileInfo.error = true;
|
||||
try {
|
||||
database.create('uploads', fileInfo, true);
|
||||
reject(error);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
log('err_send_file_message_create_upload_3', err);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
} catch (err) {
|
||||
log('err_send_file_message_create_upload_4', err);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -66,6 +66,6 @@ export default async function(rid, msg, tmid, user) {
|
|||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_send_message', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,8 +38,8 @@ export default function subscribeRoom({ rid }) {
|
|||
clearTimeout(typingTimeouts[username]);
|
||||
typingTimeouts[username] = null;
|
||||
}
|
||||
} catch (error) {
|
||||
log('err_remove_user_typing', error);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -60,8 +60,8 @@ export default function subscribeRoom({ rid }) {
|
|||
typingTimeouts[username] = setTimeout(() => {
|
||||
removeUserTyping(username);
|
||||
}, 10000);
|
||||
} catch (error) {
|
||||
log('err_add_user_typing', error);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -172,7 +172,7 @@ export default function subscribeRoom({ rid }) {
|
|||
try {
|
||||
promises = this.sdk.subscribeRoom(rid);
|
||||
} catch (e) {
|
||||
log('err_subscribe_room', e);
|
||||
log(e);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -44,7 +44,7 @@ export default function subscribeRooms() {
|
|||
database.delete(subscription);
|
||||
});
|
||||
} catch (e) {
|
||||
log('err_stream_msg_received_sub_removed', e);
|
||||
log(e);
|
||||
}
|
||||
} else {
|
||||
const rooms = database.objects('rooms').filtered('_id == $0', data.rid);
|
||||
|
@ -55,7 +55,7 @@ export default function subscribeRooms() {
|
|||
database.delete(rooms);
|
||||
});
|
||||
} 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);
|
||||
});
|
||||
} catch (e) {
|
||||
log('err_stream_msg_received_room_updated', e);
|
||||
log(e);
|
||||
}
|
||||
} else if (type === 'inserted') {
|
||||
try {
|
||||
|
@ -76,7 +76,7 @@ export default function subscribeRooms() {
|
|||
database.create('rooms', data, true);
|
||||
});
|
||||
} 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);
|
||||
});
|
||||
} catch (e) {
|
||||
log('err_stream_msg_received_message', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ export default function subscribeRooms() {
|
|||
stop: () => stop()
|
||||
};
|
||||
} catch (e) {
|
||||
log('err_subscribe_rooms', e);
|
||||
log(e);
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,14 +95,23 @@ const subscriptionSchema = {
|
|||
reactWhenReadOnly: { type: 'bool', optional: true },
|
||||
archived: { type: 'bool', optional: true },
|
||||
joinCodeRequired: { type: 'bool', optional: true },
|
||||
notifications: { type: 'bool', optional: true },
|
||||
muted: 'string[]',
|
||||
broadcast: { type: 'bool', optional: true },
|
||||
prid: { type: 'string', optional: true },
|
||||
draftMessage: { type: 'string', optional: true },
|
||||
lastThreadSync: 'date?',
|
||||
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({
|
||||
path: `${ RNRealmPath.realmPath }${ path }.realm`,
|
||||
schema,
|
||||
schemaVersion: 13,
|
||||
schemaVersion: 14,
|
||||
migration: (oldRealm, newRealm) => {
|
||||
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 13) {
|
||||
const newSubs = newRealm.objects('subscriptions');
|
||||
|
|
|
@ -2,6 +2,7 @@ import { AsyncStorage, InteractionManager } from 'react-native';
|
|||
import semver from 'semver';
|
||||
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
|
||||
import RNUserDefaults from 'rn-user-defaults';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
|
||||
import reduxStore from './createStore';
|
||||
import defaultSettings from '../constants/settings';
|
||||
|
@ -9,6 +10,7 @@ import messagesStatus from '../constants/messagesStatus';
|
|||
import database from './realm';
|
||||
import log from '../utils/log';
|
||||
import { isIOS, getBundleId } from '../utils/deviceInfo';
|
||||
import { extractHostname } from '../utils/server';
|
||||
|
||||
import {
|
||||
setUser, setLoginServices, loginRequest, loginFailure, logout
|
||||
|
@ -45,6 +47,7 @@ import { SERVERS, SERVER_URL } from '../constants/userDefaults';
|
|||
const TOKEN_KEY = 'reactnativemeteor_usertoken';
|
||||
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
|
||||
export const MARKDOWN_KEY = 'RC_MARKDOWN_KEY';
|
||||
export const CRASH_REPORT_KEY = 'RC_CRASH_REPORT_KEY';
|
||||
const returnAnArray = obj => obj || [];
|
||||
const MIN_ROCKETCHAT_VERSION = '0.70.0';
|
||||
|
||||
|
@ -52,7 +55,12 @@ const STATUSES = ['offline', 'online', 'away', 'busy'];
|
|||
|
||||
const RocketChat = {
|
||||
TOKEN_KEY,
|
||||
subscribeRooms,
|
||||
async subscribeRooms() {
|
||||
if (this.roomsSub) {
|
||||
this.roomsSub.stop();
|
||||
}
|
||||
this.roomsSub = await subscribeRooms.call(this);
|
||||
},
|
||||
subscribeRoom,
|
||||
canOpenRoom,
|
||||
createChannel({
|
||||
|
@ -85,7 +93,7 @@ const RocketChat = {
|
|||
return result;
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_get_server_info', e);
|
||||
log(e);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
|
@ -358,6 +366,12 @@ const RocketChat = {
|
|||
try {
|
||||
const servers = await RNUserDefaults.objectForKey(SERVERS);
|
||||
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) {
|
||||
console.log('logout_rn_user_defaults', error);
|
||||
}
|
||||
|
@ -433,7 +447,7 @@ const RocketChat = {
|
|||
database.create('messages', message, true);
|
||||
});
|
||||
} catch (e) {
|
||||
log('err_resend_message', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -564,7 +578,7 @@ const RocketChat = {
|
|||
try {
|
||||
room = await RocketChat.getRoom(message.rid);
|
||||
} catch (e) {
|
||||
log('err_get_permalink', e);
|
||||
log(e);
|
||||
return null;
|
||||
}
|
||||
const { server } = reduxStore.getState().server;
|
||||
|
@ -640,6 +654,10 @@ const RocketChat = {
|
|||
// RC 0.48.0
|
||||
return this.sdk.get('users.info', { userId });
|
||||
},
|
||||
getRoomInfo(roomId) {
|
||||
// RC 0.72.0
|
||||
return this.sdk.get('rooms.info', { roomId });
|
||||
},
|
||||
getRoomMemberId(rid, currentUserId) {
|
||||
if (rid === `${ currentUserId }${ currentUserId }`) {
|
||||
return currentUserId;
|
||||
|
@ -761,6 +779,13 @@ const RocketChat = {
|
|||
}
|
||||
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() {
|
||||
const prefs = await RNUserDefaults.objectForKey(SORT_PREFS_KEY);
|
||||
return prefs;
|
||||
|
@ -802,9 +827,11 @@ const RocketChat = {
|
|||
}
|
||||
},
|
||||
_determineAuthType(services) {
|
||||
const { name, custom, service } = services;
|
||||
const {
|
||||
name, custom, showButton = true, service
|
||||
} = services;
|
||||
|
||||
if (custom) {
|
||||
if (custom && showButton) {
|
||||
return 'oauth_custom';
|
||||
}
|
||||
|
||||
|
@ -957,8 +984,8 @@ const RocketChat = {
|
|||
const autoTranslatePermission = database.objectForPrimaryKey('permissions', 'auto-translate');
|
||||
const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || [];
|
||||
return autoTranslatePermission.roles.some(role => userRoles.includes(role));
|
||||
} catch (error) {
|
||||
log('err_can_auto_translate', error);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -13,7 +13,7 @@ const formatMsg = ({
|
|||
if (!showLastMessage) {
|
||||
return '';
|
||||
}
|
||||
if (!lastMessage) {
|
||||
if (!lastMessage || lastMessage.pinned) {
|
||||
return I18n.t('No_Message');
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import { View, Text } from 'react-native';
|
|||
import FastImage from 'react-native-fast-image';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
|
||||
import log from '../../utils/log';
|
||||
import Check from '../../containers/Check';
|
||||
import styles, { ROW_HEIGHT } from './styles';
|
||||
|
||||
|
@ -24,7 +23,7 @@ const ServerItem = React.memo(({
|
|||
}}
|
||||
defaultSource={{ uri: 'logo' }}
|
||||
style={styles.serverIcon}
|
||||
onError={() => log('err_loading_server_icon')}
|
||||
onError={() => console.log('err_loading_server_icon')}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -12,6 +12,7 @@ import sortPreferences from './sortPreferences';
|
|||
import notification from './notification';
|
||||
import markdown from './markdown';
|
||||
import share from './share';
|
||||
import crashReport from './crashReport';
|
||||
|
||||
export default combineReducers({
|
||||
settings,
|
||||
|
@ -26,5 +27,6 @@ export default combineReducers({
|
|||
sortPreferences,
|
||||
notification,
|
||||
markdown,
|
||||
share
|
||||
share,
|
||||
crashReport
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ import * as actions from '../actions';
|
|||
import { selectServerRequest } from '../actions/server';
|
||||
import { setAllPreferences } from '../actions/sortPreferences';
|
||||
import { toggleMarkdown } from '../actions/markdown';
|
||||
import { toggleCrashReport } from '../actions/crashReport';
|
||||
import { APP } from '../actions/actionsTypes';
|
||||
import RocketChat from '../lib/rocketchat';
|
||||
import log from '../utils/log';
|
||||
|
@ -46,7 +47,7 @@ const restore = function* restore() {
|
|||
serversDB.create('servers', serverInfo, true);
|
||||
await RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ serverInfo.id }`, serverItem[USER_ID]);
|
||||
} catch (e) {
|
||||
log('err_create_servers', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -66,6 +67,9 @@ const restore = function* restore() {
|
|||
const useMarkdown = yield RocketChat.getUseMarkdown();
|
||||
yield put(toggleMarkdown(useMarkdown));
|
||||
|
||||
const allowCrashReport = yield RocketChat.getAllowCrashReport();
|
||||
yield put(toggleCrashReport(allowCrashReport));
|
||||
|
||||
if (!token || !server) {
|
||||
yield all([
|
||||
RNUserDefaults.clear(RocketChat.TOKEN_KEY),
|
||||
|
@ -79,7 +83,7 @@ const restore = function* restore() {
|
|||
|
||||
yield put(actions.appReady({}));
|
||||
} catch (e) {
|
||||
log('err_restore', e);
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
|||
try {
|
||||
serversDB.create('user', user, true);
|
||||
} catch (e) {
|
||||
log('err_set_user_token', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -99,7 +99,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
|||
yield put(appStart('inside'));
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_handle_login_success', e);
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -128,7 +128,7 @@ const handleLogout = function* handleLogout() {
|
|||
yield put(appStart('outside'));
|
||||
} catch (e) {
|
||||
yield put(appStart('outside'));
|
||||
log('err_handle_logout', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -78,7 +78,7 @@ const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
|
|||
yield delay(500);
|
||||
yield put(replyInit(message, false));
|
||||
} catch (e) {
|
||||
log('err_reply_broadcast', e);
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ const watchUserTyping = function* watchUserTyping({ rid, status }) {
|
|||
yield RocketChat.emitTyping(rid, false);
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_watch_user_typing', e);
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {
|
||||
put, select, race, take, fork, cancel, takeLatest, delay
|
||||
put, select, race, take, fork, cancel, delay
|
||||
} from 'redux-saga/effects';
|
||||
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 RocketChat from '../lib/rocketchat';
|
||||
|
||||
let roomsSub;
|
||||
|
||||
const removeSub = function removeSub() {
|
||||
if (roomsSub && roomsSub.stop) {
|
||||
roomsSub.stop();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoomsRequest = function* handleRoomsRequest() {
|
||||
try {
|
||||
removeSub();
|
||||
roomsSub = yield RocketChat.subscribeRooms();
|
||||
yield RocketChat.subscribeRooms();
|
||||
const newRoomsUpdatedAt = new Date();
|
||||
const server = yield select(state => state.server.server);
|
||||
const [serverRecord] = database.databases.serversDB.objects('servers').filtered('id = $0', server);
|
||||
|
@ -33,8 +24,8 @@ const handleRoomsRequest = function* handleRoomsRequest() {
|
|||
subscriptions.forEach((subscription) => {
|
||||
try {
|
||||
database.create('subscriptions', subscription, true);
|
||||
} catch (error) {
|
||||
log('err_rooms_request_create_sub', error);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -42,23 +33,18 @@ const handleRoomsRequest = function* handleRoomsRequest() {
|
|||
try {
|
||||
database.databases.serversDB.create('servers', { id: server, roomsUpdatedAt: newRoomsUpdatedAt }, true);
|
||||
} catch (e) {
|
||||
log('err_rooms_request_update', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
|
||||
yield put(roomsSuccess());
|
||||
} catch (e) {
|
||||
yield put(roomsFailure(e));
|
||||
log('err_rooms_request', e);
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = function handleLogout() {
|
||||
removeSub();
|
||||
};
|
||||
|
||||
const root = function* root() {
|
||||
yield takeLatest(types.LOGOUT, handleLogout);
|
||||
while (true) {
|
||||
const params = yield take(types.ROOMS.REQUEST);
|
||||
const isAuthenticated = yield select(state => state.login.isAuthenticated);
|
||||
|
|
|
@ -14,6 +14,7 @@ import { setUser } from '../actions/login';
|
|||
import RocketChat from '../lib/rocketchat';
|
||||
import database from '../lib/realm';
|
||||
import log from '../utils/log';
|
||||
import { extractHostname } from '../utils/server';
|
||||
import I18n from '../i18n';
|
||||
import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults';
|
||||
|
||||
|
@ -34,7 +35,7 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
|
|||
|
||||
return serverInfo;
|
||||
} 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));
|
||||
} catch (e) {
|
||||
yield put(selectServerFailure());
|
||||
log('err_select_server', e);
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleServerRequest = function* handleServerRequest({ server }) {
|
||||
const handleServerRequest = function* handleServerRequest({ server, certificate }) {
|
||||
try {
|
||||
if (certificate) {
|
||||
yield RNUserDefaults.setObjectForKey(extractHostname(server), certificate);
|
||||
}
|
||||
|
||||
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) {
|
||||
yield put(serverFailure());
|
||||
log('err_server_request', e);
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ const appHasComeBackToForeground = function* appHasComeBackToForeground() {
|
|||
setBadgeCount();
|
||||
return yield RocketChat.setUserPresenceOnline();
|
||||
} catch (e) {
|
||||
log('err_app_has_come_back_to_foreground', e);
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -34,7 +34,7 @@ const appHasComeBackToBackground = function* appHasComeBackToBackground() {
|
|||
try {
|
||||
return yield RocketChat.setUserPresenceAway();
|
||||
} catch (e) {
|
||||
log('err_app_has_come_back_to_background', e);
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ class EventEmitter {
|
|||
try {
|
||||
listener.apply(this, args);
|
||||
} catch (e) {
|
||||
log('err_emit', e);
|
||||
log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import { Client } from 'bugsnag-react-native';
|
||||
import firebase from 'react-native-firebase';
|
||||
import config from '../../config';
|
||||
|
||||
export default (event, error) => {
|
||||
if (typeof error !== 'object') {
|
||||
error = { error };
|
||||
}
|
||||
firebase.analytics().logEvent(event);
|
||||
if (__DEV__) {
|
||||
console.warn(event, error);
|
||||
const bugsnag = new Client(config.BUGSNAG_API_KEY);
|
||||
|
||||
export const { analytics } = firebase;
|
||||
export const loggerConfig = bugsnag.config;
|
||||
export const { leaveBreadcrumb } = bugsnag;
|
||||
|
||||
export default (e) => {
|
||||
if (e instanceof Error) {
|
||||
bugsnag.notify(e);
|
||||
} else {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -31,6 +31,7 @@ export const onNavigationStateChange = (prevState, currentState) => {
|
|||
const prevScreen = getActiveRouteName(prevState);
|
||||
|
||||
if (prevScreen !== currentScreen) {
|
||||
firebase.analytics().setCurrentScreen(currentScreen);
|
||||
analytics().setCurrentScreen(currentScreen);
|
||||
leaveBreadcrumb(currentScreen, { type: 'navigation' });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -99,8 +99,8 @@ class DirectoryView extends React.Component {
|
|||
} else {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
} catch (error) {
|
||||
log('err_load_directory', error);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}, 200)
|
||||
|
|
|
@ -109,7 +109,7 @@ class LanguageView extends React.Component {
|
|||
this.setState({ saving: false });
|
||||
setTimeout(() => {
|
||||
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_preferences') }));
|
||||
log('err_save_user_preferences', e);
|
||||
log(e);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
import { connect } from 'react-redux';
|
||||
import { SafeAreaView } from 'react-navigation';
|
||||
import equal from 'deep-equal';
|
||||
import firebase from 'react-native-firebase';
|
||||
import { analytics } from '../utils/log';
|
||||
|
||||
import KeyboardView from '../presentation/KeyboardView';
|
||||
import TextInput from '../containers/TextInput';
|
||||
|
@ -156,7 +156,7 @@ class LoginView extends React.Component {
|
|||
const { loginRequest } = this.props;
|
||||
Keyboard.dismiss();
|
||||
loginRequest({ user, password, code });
|
||||
firebase.analytics().logEvent('login');
|
||||
analytics().logEvent('login');
|
||||
}
|
||||
|
||||
register = () => {
|
||||
|
|
|
@ -35,7 +35,8 @@ class MessagesView extends React.Component {
|
|||
loading: false,
|
||||
messages: [],
|
||||
selectedAttachment: {},
|
||||
photoModalVisible: false
|
||||
photoModalVisible: false,
|
||||
fileLoading: true
|
||||
};
|
||||
this.rid = props.navigation.getParam('rid');
|
||||
this.t = props.navigation.getParam('t');
|
||||
|
@ -47,7 +48,9 @@ class MessagesView extends React.Component {
|
|||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { loading, messages, photoModalVisible } = this.state;
|
||||
const {
|
||||
loading, messages, photoModalVisible, fileLoading
|
||||
} = this.state;
|
||||
if (nextState.loading !== loading) {
|
||||
return true;
|
||||
}
|
||||
|
@ -57,6 +60,10 @@ class MessagesView extends React.Component {
|
|||
if (!equal(nextState.messages, messages)) {
|
||||
return true;
|
||||
}
|
||||
if (fileLoading !== nextState.fileLoading) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -225,6 +232,10 @@ class MessagesView extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
setFileLoading = (fileLoading) => {
|
||||
this.setState({ fileLoading });
|
||||
}
|
||||
|
||||
renderEmpty = () => (
|
||||
<View style={styles.listEmptyContainer} testID={this.content.testID}>
|
||||
<Text style={styles.noDataFound}>{this.content.noDataMsg}</Text>
|
||||
|
@ -235,7 +246,7 @@ class MessagesView extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
messages, loading, selectedAttachment, photoModalVisible
|
||||
messages, loading, selectedAttachment, photoModalVisible, fileLoading
|
||||
} = this.state;
|
||||
const { user, baseUrl } = this.props;
|
||||
|
||||
|
@ -260,6 +271,8 @@ class MessagesView extends React.Component {
|
|||
onClose={this.onCloseFileModal}
|
||||
user={user}
|
||||
baseUrl={baseUrl}
|
||||
loading={fileLoading}
|
||||
setLoading={this.setFileLoading}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity
|
||||
Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity, View, Alert, LayoutAnimation
|
||||
} from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
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 sharedStyles from './Styles';
|
||||
|
@ -18,6 +22,7 @@ import { isIOS, isNotch } from '../utils/deviceInfo';
|
|||
import { CustomIcon } from '../lib/Icons';
|
||||
import StatusBar from '../containers/StatusBar';
|
||||
import { COLOR_PRIMARY } from '../constants/colors';
|
||||
import log from '../utils/log';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
|
@ -41,6 +46,22 @@ const styles = StyleSheet.create({
|
|||
position: 'absolute',
|
||||
paddingHorizontal: 9,
|
||||
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) {
|
||||
super(props);
|
||||
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 = {
|
||||
text: server || '',
|
||||
autoFocus: !server
|
||||
autoFocus: !server,
|
||||
certificate: null
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -76,11 +107,14 @@ class NewServerView extends React.Component {
|
|||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { text } = this.state;
|
||||
const { text, certificate } = this.state;
|
||||
const { connecting } = this.props;
|
||||
if (nextState.text !== text) {
|
||||
return true;
|
||||
}
|
||||
if (!isEqual(nextState.certificate, certificate)) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.connecting !== connecting) {
|
||||
return true;
|
||||
}
|
||||
|
@ -91,13 +125,51 @@ class NewServerView extends React.Component {
|
|||
this.setState({ text });
|
||||
}
|
||||
|
||||
submit = () => {
|
||||
const { text } = this.state;
|
||||
submit = async() => {
|
||||
const { text, certificate } = this.state;
|
||||
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) {
|
||||
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(/\/+$/, '');
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
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() {
|
||||
const { connecting } = this.props;
|
||||
const { text, autoFocus } = this.state;
|
||||
|
@ -175,6 +278,7 @@ class NewServerView extends React.Component {
|
|||
loading={connecting}
|
||||
testID='new-server-view-button'
|
||||
/>
|
||||
{ isIOS ? this.renderCertificatePicker() : null }
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
{this.renderBack()}
|
||||
|
@ -188,7 +292,7 @@ const mapStateToProps = state => ({
|
|||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
connectServer: server => dispatch(serverRequest(server))
|
||||
connectServer: (server, certificate) => dispatch(serverRequest(server, certificate))
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(NewServerView);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -63,7 +63,7 @@ class ProfileView extends React.Component {
|
|||
const result = await RocketChat.getAvatarSuggestion();
|
||||
this.setState({ avatarSuggestions: result });
|
||||
} catch (e) {
|
||||
log('err_get_avatar_suggestion', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ class RegisterView extends React.Component {
|
|||
try {
|
||||
this.parsedCustomFields = JSON.parse(props.Accounts_CustomFields);
|
||||
} catch (e) {
|
||||
log('err_parsing_account_custom_fields', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
Object.keys(this.parsedCustomFields).forEach((key) => {
|
||||
|
|
|
@ -64,8 +64,8 @@ class RoomActionsView extends React.Component {
|
|||
if (result.success) {
|
||||
this.setState({ room: { ...result.channel, rid: result.channel._id } });
|
||||
}
|
||||
} catch (error) {
|
||||
log('err_get_channel_info', error);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,8 +75,8 @@ class RoomActionsView extends React.Component {
|
|||
if (counters.success) {
|
||||
this.setState({ membersCount: counters.members, joined: counters.joined });
|
||||
}
|
||||
} catch (error) {
|
||||
log('err_get_room_counters', error);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
} else if (room.t === 'd') {
|
||||
this.updateRoomMember();
|
||||
|
@ -168,13 +168,14 @@ class RoomActionsView extends React.Component {
|
|||
room, membersCount, canViewMembers, joined, canAutoTranslate
|
||||
} = this.state;
|
||||
const {
|
||||
rid, t, blocker, notifications
|
||||
rid, t, blocker
|
||||
} = room;
|
||||
|
||||
const notificationsAction = {
|
||||
icon: notifications ? 'bell' : 'Bell-off',
|
||||
name: I18n.t(`${ notifications ? 'Enable' : 'Disable' }_notifications`),
|
||||
event: this.toggleNotifications,
|
||||
icon: 'bell',
|
||||
name: I18n.t('Notifications'),
|
||||
route: 'NotificationPrefView',
|
||||
params: { rid },
|
||||
testID: 'room-actions-notifications'
|
||||
};
|
||||
|
||||
|
@ -184,7 +185,7 @@ class RoomActionsView extends React.Component {
|
|||
name: I18n.t('Room_Info'),
|
||||
route: 'RoomInfoView',
|
||||
// forward room only if room isn't joined
|
||||
params: { rid, t, room: joined ? null : room },
|
||||
params: { rid, t },
|
||||
testID: 'room-actions-info'
|
||||
}],
|
||||
renderItem: this.renderRoomInfo
|
||||
|
@ -341,7 +342,7 @@ class RoomActionsView extends React.Component {
|
|||
this.setState({ member: result.user });
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_update_room_member', e);
|
||||
log(e);
|
||||
this.setState({ member: {} });
|
||||
}
|
||||
}
|
||||
|
@ -353,7 +354,7 @@ class RoomActionsView extends React.Component {
|
|||
try {
|
||||
RocketChat.toggleBlockUser(rid, member._id, !blocker);
|
||||
} 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 }) => {
|
||||
const { room, member } = this.state;
|
||||
const { name, t, topic } = room;
|
||||
|
|
|
@ -206,7 +206,7 @@ class RoomInfoEditView extends React.Component {
|
|||
this.setState({ nameError: e });
|
||||
}
|
||||
error = true;
|
||||
log('err_save_room_settings', e);
|
||||
log(e);
|
||||
}
|
||||
|
||||
await this.setState({ saving: false });
|
||||
|
@ -261,7 +261,7 @@ class RoomInfoEditView extends React.Component {
|
|||
try {
|
||||
await RocketChat.toggleArchiveRoom(rid, t, !archived);
|
||||
} catch (e) {
|
||||
log('err_toggle_archive', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ import log from '../../utils/log';
|
|||
const PERMISSION_EDIT_ROOM = 'edit-room';
|
||||
|
||||
const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase());
|
||||
const getRoomTitle = room => (room.t === 'd'
|
||||
? <Text testID='room-info-view-name' style={styles.roomTitle}>{room.fname}</Text>
|
||||
const getRoomTitle = (room, type, name) => (type === 'd'
|
||||
? <Text testID='room-info-view-name' style={styles.roomTitle}>{name}</Text>
|
||||
: (
|
||||
<View style={styles.roomTitleRow}>
|
||||
<RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' />
|
||||
|
@ -59,28 +59,18 @@ class RoomInfoView extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.rid = props.navigation.getParam('rid');
|
||||
const room = props.navigation.getParam('room');
|
||||
this.t = props.navigation.getParam('t');
|
||||
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
|
||||
this.roles = database.objects('roles');
|
||||
this.sub = {
|
||||
unsubscribe: () => {}
|
||||
};
|
||||
this.state = {
|
||||
room: this.rooms[0] || room || {},
|
||||
room: {},
|
||||
roomUser: {}
|
||||
};
|
||||
}
|
||||
|
||||
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') {
|
||||
const { user } = this.props;
|
||||
const roomUserId = RocketChat.getRoomMemberId(this.rid, user.id);
|
||||
|
@ -89,14 +79,34 @@ class RoomInfoView extends React.Component {
|
|||
if (result.success) {
|
||||
this.setState({ roomUser: result.user });
|
||||
}
|
||||
} catch (error) {
|
||||
log('err_get_user_info', error);
|
||||
} catch (e) {
|
||||
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) {
|
||||
const { navigation } = this.props;
|
||||
navigation.setParams({ showEdit: true });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.rooms.removeAllListeners();
|
||||
}
|
||||
|
||||
getRoleDescription = (id) => {
|
||||
|
@ -107,10 +117,7 @@ class RoomInfoView extends React.Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
isDirect = () => {
|
||||
const { room: { t } } = this.state;
|
||||
return t === 'd';
|
||||
}
|
||||
isDirect = () => this.t === 'd'
|
||||
|
||||
updateRoom = () => {
|
||||
if (this.rooms.length > 0) {
|
||||
|
@ -181,15 +188,15 @@ class RoomInfoView extends React.Component {
|
|||
|
||||
return (
|
||||
<Avatar
|
||||
text={room.name}
|
||||
text={room.name || roomUser.username}
|
||||
size={100}
|
||||
style={styles.avatar}
|
||||
type={room.t}
|
||||
type={this.t}
|
||||
baseUrl={baseUrl}
|
||||
userId={user.id}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@ -231,6 +238,29 @@ class RoomInfoView extends React.Component {
|
|||
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() {
|
||||
const { room, roomUser } = this.state;
|
||||
if (!room) {
|
||||
|
@ -242,15 +272,9 @@ class RoomInfoView extends React.Component {
|
|||
<SafeAreaView style={styles.container} testID='room-info-view' forceInset={{ vertical: 'never' }}>
|
||||
<View style={styles.avatarContainer}>
|
||||
{this.renderAvatar(room, roomUser)}
|
||||
<View style={styles.roomTitleContainer}>{ getRoomTitle(room) }</View>
|
||||
<View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser && roomUser.name) }</View>
|
||||
</View>
|
||||
{!this.isDirect() ? this.renderItem('description', room) : null}
|
||||
{!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}
|
||||
{this.isDirect() ? this.renderDirect() : this.renderChannel()}
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
);
|
||||
|
|
|
@ -138,7 +138,7 @@ class RoomMembersView extends React.Component {
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_on_press_user', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -169,7 +169,7 @@ class RoomMembersView extends React.Component {
|
|||
this.fetchMembers();
|
||||
});
|
||||
} catch (e) {
|
||||
log('err_toggle_status', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,8 +203,8 @@ class RoomMembersView extends React.Component {
|
|||
end: newMembers.length < PAGE_SIZE
|
||||
});
|
||||
navigation.setParams({ allUsers });
|
||||
} catch (error) {
|
||||
log('err_fetch_members, error');
|
||||
} catch (e) {
|
||||
log(e);
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
}
|
||||
|
@ -228,7 +228,7 @@ class RoomMembersView extends React.Component {
|
|||
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') }) });
|
||||
} catch (e) {
|
||||
log('err_handle_mute', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,10 @@ import { COLOR_TEXT_DESCRIPTION, HEADER_TITLE, COLOR_WHITE } from '../../../cons
|
|||
const TITLE_SIZE = 16;
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
height: '100%'
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
marginRight: isAndroid ? 15 : 5,
|
||||
marginLeft: isAndroid ? 10 : 0
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 6,
|
||||
|
|
|
@ -105,7 +105,7 @@ export class List extends React.PureComponent {
|
|||
this.setState({ end: result.length < 50, loading: false });
|
||||
} catch (e) {
|
||||
this.setState({ loading: false });
|
||||
log('err_list_view_on_end_reached', e);
|
||||
log(e);
|
||||
}
|
||||
}, 300)
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ class UploadProgress extends Component {
|
|||
try {
|
||||
database.write(() => database.delete(uploadItem[0]));
|
||||
} catch (e) {
|
||||
log('err_upload_progress_delete', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,7 @@ class UploadProgress extends Component {
|
|||
try {
|
||||
await RocketChat.cancelUpload(item.path);
|
||||
} 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);
|
||||
} catch (e) {
|
||||
log('err_upload_progress_try_again', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
} from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
import { SafeAreaView } from 'react-navigation';
|
||||
import { SafeAreaView, HeaderBackButton } from 'react-navigation';
|
||||
import equal from 'deep-equal';
|
||||
import moment from 'moment';
|
||||
import EJSON from 'ejson';
|
||||
|
@ -36,7 +36,7 @@ import I18n from '../../i18n';
|
|||
import RoomHeaderView, { RightButtons } from './Header';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
import Separator from './Separator';
|
||||
import { COLOR_WHITE } from '../../constants/colors';
|
||||
import { COLOR_WHITE, HEADER_BACK } from '../../constants/colors';
|
||||
import debounce from '../../utils/debounce';
|
||||
import buildMessage from '../../lib/methods/helpers/buildMessage';
|
||||
import FileModal from '../../containers/FileModal';
|
||||
|
@ -52,8 +52,8 @@ class RoomView extends React.Component {
|
|||
const t = navigation.getParam('t');
|
||||
const tmid = navigation.getParam('tmid');
|
||||
const toggleFollowThread = navigation.getParam('toggleFollowThread', () => {});
|
||||
const unreadsCount = navigation.getParam('unreadsCount', null);
|
||||
return {
|
||||
headerTitleContainerStyle: styles.headerTitleContainerStyle,
|
||||
headerTitle: (
|
||||
<RoomHeaderView
|
||||
rid={rid}
|
||||
|
@ -72,6 +72,14 @@ class RoomView extends React.Component {
|
|||
navigation={navigation}
|
||||
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.tmid = props.navigation.getParam('tmid');
|
||||
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
|
||||
this.chats = database.objects('subscriptions').filtered('rid != $0', this.rid);
|
||||
const canAutoTranslate = RocketChat.canAutoTranslate();
|
||||
this.state = {
|
||||
joined: this.rooms.length > 0,
|
||||
|
@ -149,6 +158,7 @@ class RoomView extends React.Component {
|
|||
EventEmitter.addEventListener('connected', this.handleConnected);
|
||||
}
|
||||
safeAddListener(this.rooms, this.updateRoom);
|
||||
safeAddListener(this.chats, this.updateUnreadCount);
|
||||
this.mounted = true;
|
||||
});
|
||||
console.timeEnd(`${ this.constructor.name } mount`);
|
||||
|
@ -222,6 +232,7 @@ class RoomView extends React.Component {
|
|||
}
|
||||
}
|
||||
this.rooms.removeAllListeners();
|
||||
this.chats.removeAllListeners();
|
||||
if (this.sub && this.sub.stop) {
|
||||
this.sub.stop();
|
||||
}
|
||||
|
@ -283,7 +294,7 @@ class RoomView extends React.Component {
|
|||
this.setState({ canAutoTranslate });
|
||||
});
|
||||
} catch (e) {
|
||||
log('err_room_init', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -309,7 +320,7 @@ class RoomView extends React.Component {
|
|||
}
|
||||
RocketChat.setReaction(shortname, messageId);
|
||||
} catch (e) {
|
||||
log('err_room_on_reaction_press', e);
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -329,6 +340,17 @@ class RoomView extends React.Component {
|
|||
});
|
||||
}, 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) => {
|
||||
const { navigation } = this.props;
|
||||
if (item.tmid) {
|
||||
|
@ -405,7 +427,7 @@ class RoomView extends React.Component {
|
|||
}
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
log('err_get_messages', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -413,7 +435,7 @@ class RoomView extends React.Component {
|
|||
try {
|
||||
return RocketChat.loadThreadMessages({ tmid: this.tmid });
|
||||
} catch (e) {
|
||||
log('err_get_thread_messages', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -426,7 +448,7 @@ class RoomView extends React.Component {
|
|||
joined: true
|
||||
});
|
||||
} catch (e) {
|
||||
log('err_join_room', e);
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -438,8 +460,8 @@ class RoomView extends React.Component {
|
|||
database.write(() => {
|
||||
database.create('threads', buildMessage(EJSON.fromJSONValue(thread)), true);
|
||||
});
|
||||
} catch (error) {
|
||||
log('err_fetch_thread_name', error);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -448,10 +470,18 @@ class RoomView extends React.Component {
|
|||
await RocketChat.toggleFollowMessage(this.tmid, !isFollowingThread);
|
||||
EventEmitter.emit(LISTENER, { message: isFollowingThread ? 'Unfollowed thread' : 'Following thread' });
|
||||
} 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) => {
|
||||
const { room, lastOpen, canAutoTranslate } = this.state;
|
||||
const {
|
||||
|
@ -500,6 +530,7 @@ class RoomView extends React.Component {
|
|||
isReadReceiptEnabled={Message_Read_Receipt_Enabled}
|
||||
autoTranslateRoom={canAutoTranslate && room.autoTranslate}
|
||||
autoTranslateLanguage={room.autoTranslateLanguage}
|
||||
navToRoomInfo={this.navToRoomInfo}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import { StyleSheet } from 'react-native';
|
|||
import {
|
||||
COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_TEXT_DESCRIPTION
|
||||
} from '../../constants/colors';
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import sharedStyles from '../Styles';
|
||||
|
||||
export default StyleSheet.create({
|
||||
|
@ -65,9 +64,5 @@ export default StyleSheet.create({
|
|||
fontSize: 16,
|
||||
...sharedStyles.textMedium,
|
||||
...sharedStyles.textColorNormal
|
||||
},
|
||||
headerTitleContainerStyle: {
|
||||
justifyContent: 'flex-start',
|
||||
left: isIOS ? 40 : 50
|
||||
}
|
||||
});
|
||||
|
|
|
@ -58,7 +58,7 @@ class Sort extends PureComponent {
|
|||
setSortPreference(param);
|
||||
RocketChat.saveSortPreference(param);
|
||||
} catch (e) {
|
||||
log('err_set_sort_preference', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -350,7 +350,7 @@ class RoomsListView extends React.Component {
|
|||
return this.goRoom({ rid: result.room._id, name: username, t: 'd' });
|
||||
}
|
||||
} catch (e) {
|
||||
log('err_on_press_item', e);
|
||||
log(e);
|
||||
}
|
||||
} else {
|
||||
return this.goRoom(item);
|
||||
|
@ -383,7 +383,7 @@ class RoomsListView extends React.Component {
|
|||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log('error_toggle_favorite', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -399,7 +399,7 @@ class RoomsListView extends React.Component {
|
|||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log('error_toggle_read', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -413,7 +413,7 @@ class RoomsListView extends React.Component {
|
|||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log('error_hide_channel', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import equal from 'deep-equal';
|
|||
import RCTextInput from '../../containers/TextInput';
|
||||
import RCActivityIndicator from '../../containers/ActivityIndicator';
|
||||
import styles from './styles';
|
||||
import Markdown from '../../containers/message/Markdown';
|
||||
import Markdown from '../../containers/markdown';
|
||||
import debounce from '../../utils/debounce';
|
||||
import RocketChat from '../../lib/rocketchat';
|
||||
import Message from '../../containers/message/Message';
|
||||
|
@ -68,9 +68,9 @@ class SearchMessagesView extends React.Component {
|
|||
loading: false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (e) {
|
||||
this.setState({ loading: false });
|
||||
log('err_search_messages', error);
|
||||
log(e);
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ class SelectedUsersView extends React.Component {
|
|||
await RocketChat.addUsersToRoom(rid);
|
||||
navigation.pop();
|
||||
} catch (e) {
|
||||
log('err_add_user', e);
|
||||
log(e);
|
||||
} finally {
|
||||
setLoadingInvite(false);
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ class SetUsernameView extends React.Component {
|
|||
await RocketChat.setUsername(username);
|
||||
await loginRequest({ resume: token });
|
||||
} catch (e) {
|
||||
log('err_submit_username', e);
|
||||
log(e);
|
||||
}
|
||||
this.setState({ saving: false });
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, Linking, ScrollView, AsyncStorage, SafeAreaView, Switch, Share
|
||||
View, Linking, ScrollView, AsyncStorage, SafeAreaView, Switch, Text, Share
|
||||
} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { toggleMarkdown as toggleMarkdownAction } from '../../actions/markdown';
|
||||
import { toggleCrashReport as toggleCrashReportAction } from '../../actions/crashReport';
|
||||
import { SWITCH_TRACK_COLOR } from '../../constants/colors';
|
||||
import { DrawerButton } from '../../containers/HeaderButton';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
|
@ -13,16 +14,25 @@ import ListItem from '../../containers/ListItem';
|
|||
import { DisclosureImage } from '../../containers/DisclosureIndicator';
|
||||
import Separator from '../../containers/Separator';
|
||||
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 openLink from '../../utils/openLink';
|
||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||
import { showErrorAlert } from '../../utils/info';
|
||||
import styles from './styles';
|
||||
import sharedStyles from '../Styles';
|
||||
import { loggerConfig, analytics } from '../../utils/log';
|
||||
import { PLAY_MARKET_LINK, APP_STORE_LINK, LICENSE_LINK } from '../../constants/links';
|
||||
|
||||
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 {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
|
@ -34,7 +44,9 @@ class SettingsView extends React.Component {
|
|||
navigation: PropTypes.object,
|
||||
server: PropTypes.object,
|
||||
useMarkdown: PropTypes.bool,
|
||||
toggleMarkdown: PropTypes.func
|
||||
allowCrashReport: PropTypes.bool,
|
||||
toggleMarkdown: PropTypes.func,
|
||||
toggleCrashReport: PropTypes.func
|
||||
}
|
||||
|
||||
toggleMarkdown = (value) => {
|
||||
|
@ -43,6 +55,20 @@ class SettingsView extends React.Component {
|
|||
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) => {
|
||||
const { navigation } = this.props;
|
||||
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() {
|
||||
const { server } = this.props;
|
||||
return (
|
||||
|
@ -88,7 +125,7 @@ class SettingsView extends React.Component {
|
|||
<StatusBar />
|
||||
<ScrollView
|
||||
{...scrollPersistTaps}
|
||||
contentContainerStyle={sharedStyles.listContentContainer}
|
||||
contentContainerStyle={[sharedStyles.listContentContainer, styles.listWithoutBorderBottom]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
testID='settings-view-list'
|
||||
>
|
||||
|
@ -148,6 +185,18 @@ class SettingsView extends React.Component {
|
|||
testID='settings-view-markdown'
|
||||
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>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
@ -156,11 +205,13 @@ class SettingsView extends React.Component {
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
server: state.server,
|
||||
useMarkdown: state.markdown.useMarkdown
|
||||
useMarkdown: state.markdown.useMarkdown,
|
||||
allowCrashReport: state.crashReport.allowCrashReport
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
toggleMarkdown: params => dispatch(toggleMarkdownAction(params))
|
||||
toggleMarkdown: params => dispatch(toggleMarkdownAction(params)),
|
||||
toggleCrashReport: params => dispatch(toggleCrashReportAction(params))
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SettingsView);
|
||||
|
|
|
@ -8,5 +8,18 @@ export default StyleSheet.create({
|
|||
...sharedStyles.separatorVertical,
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
height: 10
|
||||
},
|
||||
listWithoutBorderBottom: {
|
||||
borderBottomWidth: 0
|
||||
},
|
||||
infoContainer: {
|
||||
padding: 15,
|
||||
paddingBottom: 40,
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
}
|
||||
});
|
||||
|
|
|
@ -132,7 +132,7 @@ class ShareListView extends React.Component {
|
|||
value, fileInfo, isMedia, mediaLoading: false
|
||||
});
|
||||
} catch (e) {
|
||||
log('err_process_media_share_extension', e);
|
||||
log(e);
|
||||
this.setState({ mediaLoading: false });
|
||||
}
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ class ShareView extends React.Component {
|
|||
try {
|
||||
await RocketChat.sendFileMessage(rid, fileMessage, undefined, server, user);
|
||||
} catch (e) {
|
||||
log('err_send_media_message', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -127,8 +127,8 @@ class ShareView extends React.Component {
|
|||
if (value !== '' && rid !== '') {
|
||||
try {
|
||||
await RocketChat.sendMessage(rid, value, undefined, user);
|
||||
} catch (error) {
|
||||
log('err_share_extension_send_message', error);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -157,7 +157,7 @@ class Sidebar extends Component {
|
|||
try {
|
||||
RocketChat.setUserPresenceDefaultStatus(item.id);
|
||||
} catch (e) {
|
||||
log('err_set_user_presence_default_status', e);
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue