diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3caf381e3..8e1745caa 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -15,7 +15,8 @@ jobs:
- checkout
- restore_cache:
- key: node-modules-{{ checksum ".circleci/config.yml" }}-{{ checksum "yarn.lock" }}
+ name: Restore NPM cache
+ key: node-modules-{{ checksum "yarn.lock" }}
- run:
name: Install NPM modules
@@ -38,7 +39,8 @@ jobs:
yarn codecov
- save_cache:
- key: node-modules-{{ checksum ".circleci/config.yml" }}-{{ checksum "yarn.lock" }}
+ key: node-modules-{{ checksum "yarn.lock" }}
+ name: Save NPM cache
paths:
- ./node_modules
@@ -52,6 +54,10 @@ jobs:
steps:
- checkout
+ - restore_cache:
+ name: Restore NPM cache
+ key: node-v1-mac-{{ checksum "yarn.lock" }}
+
- run:
name: Install Node 8
command: |
@@ -84,6 +90,12 @@ jobs:
command: |
detox test --configuration ios.sim.release --cleanup
+ - save_cache:
+ name: Save NPM cache
+ key: node-v1-mac-{{ checksum "yarn.lock" }}
+ paths:
+ - node_modules
+
- store_artifacts:
path: /tmp/screenshots
@@ -103,7 +115,8 @@ jobs:
- checkout
- restore_cache:
- key: node-modules-{{ checksum ".circleci/config.yml" }}-{{ checksum "yarn.lock" }}
+ name: Restore NPM cache
+ key: node-modules-{{ checksum "yarn.lock" }}
- run:
name: Install NPM modules
@@ -111,7 +124,8 @@ jobs:
yarn
- restore_cache:
- key: android-{{ checksum ".circleci/config.yml" }}-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
+ name: Restore gradle cache
+ key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
- run:
name: Configure Gradle
@@ -155,12 +169,14 @@ jobs:
path: /tmp/build/outputs
- save_cache:
- key: node-modules-{{ checksum ".circleci/config.yml" }}-{{ checksum "yarn.lock" }}
+ name: Save NPM cache
+ key: node-modules-{{ checksum "yarn.lock" }}
paths:
- ./node_modules
- save_cache:
- key: android-{{ checksum ".circleci/config.yml" }}-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
+ name: Save gradle cache
+ key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
paths:
- ~/.gradle
@@ -174,6 +190,14 @@ jobs:
steps:
- checkout
+ - restore_cache:
+ name: Restore gems cache
+ key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
+
+ - restore_cache:
+ name: Restore NPM cache
+ key: node-v1-mac-{{ checksum "yarn.lock" }}
+
- run:
name: Install Node 8
command: |
@@ -183,38 +207,49 @@ jobs:
set +e
nvm install 8
- - run:
- name: Update Fastlane
- command: |
- brew update
- brew install ruby
- sudo gem install fastlane
-
- run:
name: Install NPM modules
command: |
yarn
+ - run:
+ name: Update Fastlane
+ command: |
+ sudo bundle install
+ working_directory: ios
+
- run:
name: Set Google Services
command: |
- cd ios
cp GoogleService-Info.prod.plist GoogleService-Info.plist
+ working_directory: ios
- run:
name: Fastlane Build
no_output_timeout: 1200
command: |
- cd ios
agvtool new-version -all $CIRCLE_BUILD_NUM
if [[ $MATCH_KEYCHAIN_NAME ]]; then
- fastlane ios release
+ bundle exec fastlane ios release
else
export MATCH_KEYCHAIN_NAME="temp"
export MATCH_KEYCHAIN_PASSWORD="temp"
- fastlane ios build
+ bundle exec fastlane ios build
fi
+ working_directory: ios
+
+ - save_cache:
+ name: Save NPM cache
+ key: node-v1-mac-{{ checksum "yarn.lock" }}
+ paths:
+ - node_modules
+
+ - save_cache:
+ name: Save gems cache
+ key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
+ paths:
+ - vendor/bundle
- store_artifacts:
path: ios/RocketChatRN.ipa
@@ -235,18 +270,27 @@ jobs:
- attach_workspace:
at: ios
+ - restore_cache:
+ name: Restore gems cache
+ key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
+
- run:
name: Update Fastlane
command: |
- brew update
- brew install ruby
- sudo gem install fastlane
+ sudo bundle install
+ working_directory: ios
- run:
name: Fastlane Tesflight Upload
command: |
- cd ios
- fastlane pilot upload --ipa ios/RocketChatRN.ipa --changelog "$(sh ../.circleci/changelog.sh)"
+ bundle exec fastlane pilot upload --ipa ios/RocketChatRN.ipa --changelog "$(sh ../.circleci/changelog.sh)"
+ working_directory: ios
+
+ - save_cache:
+ name: Save gems cache
+ key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
+ paths:
+ - vendor/bundle
workflows:
version: 2
diff --git a/__mocks__/react-native-gesture-handler.js b/__mocks__/react-native-gesture-handler.js
index da7b586dd..2f9960f4a 100644
--- a/__mocks__/react-native-gesture-handler.js
+++ b/__mocks__/react-native-gesture-handler.js
@@ -2,3 +2,4 @@ export const RectButton = () => 'View';
export const State = () => 'View';
export const LongPressGestureHandler = () => 'View';
export const BorderlessButton = () => 'View';
+export const PanGestureHandler = () => 'View';
diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap
index 335bfb8b3..67f5c84e2 100644
--- a/__tests__/__snapshots__/Storyshots.test.js.snap
+++ b/__tests__/__snapshots__/Storyshots.test.js.snap
@@ -2780,7 +2780,7 @@ exports[`Storyshots Message list 1`] = `
"backgroundColor": "transparent",
"color": "#2F343D",
"fontFamily": "System",
- "fontSize": 16,
+ "fontSize": 30,
"fontWeight": "400",
}
}
@@ -2796,6 +2796,210 @@ exports[`Storyshots Message list 1`] = `
+
+ Single Emoji
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ 👏
+
+
+
+
+
+
+
+
+
@@ -3013,8 +3217,8 @@ exports[`Storyshots Message list 1`] = `
}
style={
Object {
- "height": 20,
- "width": 20,
+ "height": 30,
+ "width": 30,
}
}
/>
@@ -3027,6 +3231,654 @@ exports[`Storyshots Message list 1`] = `
"uri": "https://open.rocket.chat/emoji-custom/marioparty.gif",
}
}
+ style={
+ Object {
+ "height": 30,
+ "width": 30,
+ }
+ }
+ />
+
+
+
+
+
+
+
+
+
+ Single Custom Emojis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Normal Emoji + Custom Emojis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ 🤙
+
+
+
+
+
+
+
+
+
+
+
+ Four emoji
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ 🤙
+
+
+
+ 🤙🤙
+
diff --git a/android/app/build.gradle b/android/app/build.gradle
index e531214f1..148c2ad02 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -110,7 +110,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
- versionName "1.15.1"
+ versionName "1.16.1"
vectorDrawables.useSupportLibrary = true
}
@@ -190,7 +190,6 @@ dependencies {
implementation project(":reactnativekeyboardinput")
implementation project(':react-native-video')
implementation project(':react-native-vector-icons')
- implementation project(':rn-fetch-blob')
implementation project(':react-native-fast-image')
implementation project(':realm')
implementation project(':reactnativenotifications')
diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java
index a06fd4558..59a66db8b 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java
+++ b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java
@@ -17,7 +17,6 @@ import com.facebook.soloader.SoLoader;
import com.AlexanderZaytsev.RNI18n.RNI18nPackage;
import com.reactnative.ivpusic.imagepicker.PickerPackage;
-import com.RNFetchBlob.RNFetchBlobPackage;
import com.brentvatne.react.ReactVideoPackage;
import com.dylanvann.fastimage.FastImageViewPackage;
import com.oblador.vectoricons.VectorIconsPackage;
@@ -74,7 +73,6 @@ public class MainApplication extends Application implements ReactApplication, IN
new RNDeviceInfo(),
new PickerPackage(),
new VectorIconsPackage(),
- new RNFetchBlobPackage(),
new RealmReactPackage(),
new ReactVideoPackage(),
new ReactNativeAudioPackage(),
diff --git a/android/app/src/main/java/chat/rocket/reactnative/generated/BasePackageList.java b/android/app/src/main/java/chat/rocket/reactnative/generated/BasePackageList.java
index 5e9c77710..88c976465 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/generated/BasePackageList.java
+++ b/android/app/src/main/java/chat/rocket/reactnative/generated/BasePackageList.java
@@ -9,6 +9,7 @@ public class BasePackageList {
return Arrays.asList(
new expo.modules.constants.ConstantsPackage(),
new expo.modules.filesystem.FileSystemPackage(),
+ new expo.modules.haptics.HapticsPackage(),
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.webbrowser.WebBrowserPackage()
);
diff --git a/android/settings.gradle b/android/settings.gradle
index 6018bd529..e29a00fb7 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -18,8 +18,6 @@ include ':react-native-device-info'
project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-device-info/android')
include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
-include ':rn-fetch-blob'
-project(':rn-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/rn-fetch-blob/android')
include ':react-native-image-crop-picker'
project(':react-native-image-crop-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-crop-picker/android')
include ':react-native-i18n'
diff --git a/app/constants/colors.js b/app/constants/colors.js
index a1fa7cee7..b65cbc22a 100644
--- a/app/constants/colors.js
+++ b/app/constants/colors.js
@@ -1,4 +1,4 @@
-import { isIOS } from '../utils/deviceInfo';
+import { isIOS, isAndroid } from '../utils/deviceInfo';
export const COLOR_DANGER = '#f5455c';
export const COLOR_SUCCESS = '#2de0a5';
@@ -25,3 +25,8 @@ export const HEADER_BACKGROUND = isIOS ? '#f8f8f8' : '#2F343D';
export const HEADER_TITLE = isIOS ? COLOR_TITLE : COLOR_WHITE;
export const HEADER_BACK = isIOS ? COLOR_PRIMARY : COLOR_WHITE;
export const HEADER_TINT = isIOS ? COLOR_PRIMARY : COLOR_WHITE;
+
+export const SWITCH_TRACK_COLOR = {
+ false: isAndroid ? COLOR_DANGER : null,
+ true: COLOR_SUCCESS
+};
diff --git a/app/constants/settings.js b/app/constants/settings.js
index 1ae229a18..4e4d99395 100644
--- a/app/constants/settings.js
+++ b/app/constants/settings.js
@@ -70,5 +70,8 @@ export default {
},
API_Gitlab_URL: {
type: 'valueAsString'
+ },
+ AutoTranslate_Enabled: {
+ type: 'valueAsBoolean'
}
};
diff --git a/app/constants/userDefaults.js b/app/constants/userDefaults.js
new file mode 100644
index 000000000..e21b7b852
--- /dev/null
+++ b/app/constants/userDefaults.js
@@ -0,0 +1,6 @@
+export const SERVERS = 'kServers';
+export const TOKEN = 'kAuthToken';
+export const USER_ID = 'kUserId';
+export const SERVER_URL = 'kAuthServerURL';
+export const SERVER_NAME = 'kServerName';
+export const SERVER_ICON = 'kServerIconURL';
diff --git a/app/containers/Avatar.js b/app/containers/Avatar.js
index 41eea348c..c616788cf 100644
--- a/app/containers/Avatar.js
+++ b/app/containers/Avatar.js
@@ -3,6 +3,10 @@ import PropTypes from 'prop-types';
import { View } from 'react-native';
import FastImage from 'react-native-fast-image';
+const formatUrl = (url, baseUrl, uriSize, avatarAuthURLFragment) => (
+ `${ baseUrl }${ url }?format=png&width=${ uriSize }&height=${ uriSize }${ avatarAuthURLFragment }`
+);
+
const Avatar = React.memo(({
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token
}) => {
@@ -26,7 +30,14 @@ const Avatar = React.memo(({
avatarAuthURLFragment = `&rc_token=${ token }&rc_uid=${ userId }`;
}
- const uri = avatar || `${ baseUrl }/avatar/${ room }?format=png&width=${ uriSize }&height=${ uriSize }${ avatarAuthURLFragment }`;
+
+ let uri;
+ if (avatar) {
+ uri = avatar.includes('http') ? avatar : formatUrl(avatar, baseUrl, uriSize, avatarAuthURLFragment);
+ } else {
+ uri = formatUrl(`/avatar/${ room }`, baseUrl, uriSize, avatarAuthURLFragment);
+ }
+
const image = (
{
categories.tabs.map((tab, i) => (
-
- {this.renderCategory(tab.category, i)}
-
- ))
+ (i === 0 && frequentlyUsed.length === 0) ? null // when no frequentlyUsed don't show the tab
+ : (
+
+ {this.renderCategory(tab.category, i)}
+
+ )))
}
);
diff --git a/app/containers/MessageActions.js b/app/containers/MessageActions.js
index bc798c772..a3f7cbd14 100644
--- a/app/containers/MessageActions.js
+++ b/app/containers/MessageActions.js
@@ -4,6 +4,8 @@ import { Alert, Clipboard, Share } from 'react-native';
import { connect } from 'react-redux';
import ActionSheet from 'react-native-action-sheet';
import moment from 'moment';
+import * as Haptics from 'expo-haptics';
+
import {
actionsHide as actionsHideAction,
deleteRequest as deleteRequestAction,
@@ -13,11 +15,12 @@ import {
toggleReactionPicker as toggleReactionPickerAction,
toggleStarRequest as toggleStarRequestAction
} from '../actions/messages';
-import { vibrate } from '../utils/vibration';
import RocketChat from '../lib/rocketchat';
+import database from '../lib/realm';
import I18n from '../i18n';
import log from '../utils/log';
import Navigation from '../lib/Navigation';
+import { getMessageTranslation } from './message/utils';
@connect(
state => ({
@@ -46,7 +49,7 @@ export default class MessageActions extends React.Component {
room: PropTypes.object.isRequired,
actionMessage: PropTypes.object,
toast: PropTypes.element,
- // user: PropTypes.object.isRequired,
+ user: PropTypes.object,
deleteRequest: PropTypes.func.isRequired,
editInit: PropTypes.func.isRequired,
toggleStarRequest: PropTypes.func.isRequired,
@@ -127,6 +130,12 @@ export default class MessageActions extends React.Component {
this.READ_RECEIPT_INDEX = this.options.length - 1;
}
+ // Toggle Auto-translate
+ if (props.room.autoTranslate && props.actionMessage.u && props.actionMessage.u._id !== props.user.id) {
+ this.options.push(I18n.t(props.actionMessage.autoTranslate ? 'View_Original' : 'Translate'));
+ this.TOGGLE_TRANSLATION_INDEX = this.options.length - 1;
+ }
+
// Report
this.options.push(I18n.t('Report'));
this.REPORT_INDEX = this.options.length - 1;
@@ -138,7 +147,7 @@ export default class MessageActions extends React.Component {
}
setTimeout(() => {
this.showActionSheet();
- vibrate();
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
});
}
@@ -326,6 +335,23 @@ export default class MessageActions extends React.Component {
}
}
+ handleToggleTranslation = async() => {
+ const { actionMessage, room } = this.props;
+ try {
+ const message = database.objectForPrimaryKey('messages', actionMessage._id);
+ database.write(() => {
+ message.autoTranslate = !message.autoTranslate;
+ message._updatedAt = new Date();
+ });
+ const translatedMessage = getMessageTranslation(message, room.autoTranslateLanguage);
+ if (!translatedMessage) {
+ await RocketChat.translateMessage(actionMessage, room.autoTranslateLanguage);
+ }
+ } catch (err) {
+ log('err_toggle_translation', err);
+ }
+ }
+
handleActionPress = (actionIndex) => {
if (actionIndex) {
switch (actionIndex) {
@@ -365,6 +391,9 @@ export default class MessageActions extends React.Component {
case this.READ_RECEIPT_INDEX:
this.handleReadReceipt();
break;
+ case this.TOGGLE_TRANSLATION_INDEX:
+ this.handleToggleTranslation();
+ break;
default:
break;
}
diff --git a/app/containers/MessageBox/Recording.js b/app/containers/MessageBox/Recording.js
index d30a9cf19..96f45c9b7 100644
--- a/app/containers/MessageBox/Recording.js
+++ b/app/containers/MessageBox/Recording.js
@@ -44,13 +44,14 @@ export default class extends React.PureComponent {
this.recordingCanceled = false;
this.recording = true;
+ this.name = `${ Date.now() }.aac`;
this.state = {
currentTime: '00:00'
};
}
componentDidMount() {
- const audioPath = `${ AudioUtils.CachesDirectoryPath }/${ Date.now() }.aac`;
+ const audioPath = `${ AudioUtils.CachesDirectoryPath }/${ this.name }`;
AudioRecorder.prepareRecordingAtPath(audioPath, {
SampleRate: 22050,
@@ -84,12 +85,14 @@ export default class extends React.PureComponent {
if (!didSucceed) {
return onFinish && onFinish(didSucceed);
}
-
- const path = filePath.startsWith('file://') ? filePath.split('file://')[1] : filePath;
+ if (isAndroid) {
+ filePath = filePath.startsWith('file://') ? filePath : `file://${ filePath }`;
+ }
const fileInfo = {
+ name: this.name,
type: 'audio/aac',
store: 'Uploads',
- path
+ path: filePath
};
return onFinish && onFinish(fileInfo);
}
diff --git a/app/containers/message/Markdown.js b/app/containers/message/Markdown.js
index ae8cc77af..403af4872 100644
--- a/app/containers/message/Markdown.js
+++ b/app/containers/message/Markdown.js
@@ -20,6 +20,33 @@ const formatText = text => text.replace(
(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
}) => {
@@ -30,7 +57,7 @@ const Markdown = React.memo(({
if (m) {
m = emojify(m, { output: 'unicode' });
}
- m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim();
+ m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)\s/, '').trim();
if (numberOfLines > 0) {
m = m.replace(/[\n]+/g, '\n').trim();
}
@@ -39,6 +66,8 @@ const Markdown = React.memo(({
return {m};
}
+ const isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;
+
return (
;
+ return (
+
+ );
}
return :{content}:;
}
@@ -102,7 +138,7 @@ const Markdown = React.memo(({
}}
style={{
paragraph: styles.paragraph,
- text: styles.text,
+ text: isMessageContainsOnlyEmoji ? styles.textBig : styles.text,
codeInline: styles.codeInline,
codeBlock: styles.codeBlock,
link: styles.link,
diff --git a/app/containers/message/index.js b/app/containers/message/index.js
index 478055ad0..de62fe758 100644
--- a/app/containers/message/index.js
+++ b/app/containers/message/index.js
@@ -4,7 +4,7 @@ import { KeyboardUtils } from 'react-native-keyboard-input';
import Message from './Message';
import debounce from '../../utils/debounce';
-import { SYSTEM_MESSAGES, getCustomEmoji } from './utils';
+import { SYSTEM_MESSAGES, getCustomEmoji, getMessageTranslation } from './utils';
import messagesStatus from '../../constants/messagesStatus';
export default class MessageContainer extends React.Component {
@@ -27,6 +27,8 @@ export default class MessageContainer extends React.Component {
isReadReceiptEnabled: PropTypes.bool,
useRealName: PropTypes.bool,
useMarkdown: PropTypes.bool,
+ autoTranslateRoom: PropTypes.bool,
+ autoTranslateLanguage: PropTypes.string,
status: PropTypes.number,
onLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
@@ -49,12 +51,15 @@ export default class MessageContainer extends React.Component {
shouldComponentUpdate(nextProps) {
const {
- status, item, _updatedAt
+ status, item, _updatedAt, autoTranslateRoom
} = this.props;
if (status !== nextProps.status) {
return true;
}
+ if (autoTranslateRoom !== nextProps.autoTranslateRoom) {
+ return true;
+ }
if (item.tmsg !== nextProps.item.tmsg) {
return true;
}
@@ -191,16 +196,23 @@ export default class MessageContainer extends React.Component {
render() {
const {
- item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled
+ item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage
} = 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
+ _id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, autoTranslate: autoTranslateMessage
} = item;
+ let message = msg;
+ // "autoTranslateRoom" and "autoTranslateLanguage" are properties from the subscription
+ // "autoTranslateMessage" is a toggle between "View Original" and "Translate" state
+ if (autoTranslateRoom && autoTranslateMessage) {
+ message = getMessageTranslation(item, autoTranslateLanguage) || message;
+ }
+
return (
{
});
return findByAlias;
};
+
+export const getMessageTranslation = (message, autoTranslateLanguage) => {
+ if (!autoTranslateLanguage) {
+ return null;
+ }
+ const { translations } = message;
+ if (translations) {
+ const translation = translations.find(trans => trans.language === autoTranslateLanguage);
+ return translation && translation.value;
+ }
+ return null;
+};
diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js
index 08657afab..e390793f9 100644
--- a/app/i18n/locales/en.js
+++ b/app/i18n/locales/en.js
@@ -99,6 +99,7 @@ export default {
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}}?',
Authenticating: 'Authenticating',
+ Auto_Translate: 'Auto-Translate',
Avatar_changed_successfully: 'Avatar changed successfully!',
Avatar_Url: 'Avatar URL',
Away: 'Away',
@@ -155,11 +156,13 @@ export default {
Email_or_password_field_is_empty: 'Email or password field is empty',
Email: 'Email',
email: 'e-mail',
+ Enable_Auto_Translate: 'Enable Auto-Translate',
Enable_markdown: 'Enable markdown',
Enable_notifications: 'Enable notifications',
Everyone_can_access_this_channel: 'Everyone can access this channel',
erasing_room: 'erasing room',
Error_uploading: 'Error uploading',
+ Favorite: 'Favorite',
Favorites: 'Favorites',
Files: 'Files',
File_description: 'File description',
@@ -173,6 +176,7 @@ export default {
Forgot_Password: 'Forgot Password',
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',
@@ -266,6 +270,7 @@ export default {
Reactions_are_disabled: 'Reactions are disabled',
Reactions_are_enabled: 'Reactions are enabled',
Reactions: 'Reactions',
+ Read: 'Read',
Read_Only_Channel: 'Read Only Channel',
Read_Only: 'Read Only',
Read_Receipt: 'Read Receipt',
@@ -343,12 +348,14 @@ export default {
Timezone: 'Timezone',
topic: 'topic',
Topic: 'Topic',
+ Translate: 'Translate',
Try_again: 'Try again',
Two_Factor_Authentication: 'Two-factor Authentication',
Type_the_channel_name_here: 'Type the channel name here',
unarchive: 'unarchive',
UNARCHIVE: 'UNARCHIVE',
Unblock_user: 'Unblock user',
+ Unfavorite: 'Unfavorite',
Unfollowed_thread: 'Unfollowed thread',
Unmute: 'Unmute',
unmuted: 'unmuted',
@@ -374,6 +381,7 @@ export default {
Username_or_email: 'Username or email',
Validating: 'Validating',
Video_call: 'Video call',
+ View_Original: 'View Original',
Voice_call: 'Voice call',
Welcome: 'Welcome',
Welcome_to_RocketChat: 'Welcome to Rocket.Chat',
diff --git a/app/index.js b/app/index.js
index 04afb221a..0eb58053f 100644
--- a/app/index.js
+++ b/app/index.js
@@ -33,6 +33,7 @@ import SearchMessagesView from './views/SearchMessagesView';
import ReadReceiptsView from './views/ReadReceiptView';
import ThreadMessagesView from './views/ThreadMessagesView';
import MessagesView from './views/MessagesView';
+import AutoTranslateView from './views/AutoTranslateView';
import SelectedUsersView from './views/SelectedUsersView';
import CreateChannelView from './views/CreateChannelView';
import LegalView from './views/LegalView';
@@ -116,6 +117,7 @@ const ChatsStack = createStackNavigator({
SelectedUsersView,
ThreadMessagesView,
MessagesView,
+ AutoTranslateView,
ReadReceiptsView,
DirectoryView
}, {
diff --git a/app/lib/methods/helpers/normalizeMessage.js b/app/lib/methods/helpers/normalizeMessage.js
index 39fa9dae0..9005d145f 100644
--- a/app/lib/methods/helpers/normalizeMessage.js
+++ b/app/lib/methods/helpers/normalizeMessage.js
@@ -36,6 +36,10 @@ export default (msg) => {
if (!Array.isArray(msg.reactions)) {
msg.reactions = Object.keys(msg.reactions).map(key => ({ _id: `${ msg._id }${ key }`, emoji: key, usernames: msg.reactions[key].usernames }));
}
+ if (msg.translations && Object.keys(msg.translations).length) {
+ msg.translations = Object.keys(msg.translations).map(key => ({ _id: `${ msg._id }${ key }`, language: key, value: msg.translations[key] }));
+ msg.autoTranslate = true;
+ }
msg.urls = msg.urls ? parseUrls(msg.urls) : [];
msg._updatedAt = new Date();
// loadHistory returns msg.starred as object
diff --git a/app/lib/methods/sendFileMessage.js b/app/lib/methods/sendFileMessage.js
index 57d0f9a16..08da0b374 100644
--- a/app/lib/methods/sendFileMessage.js
+++ b/app/lib/methods/sendFileMessage.js
@@ -1,111 +1,129 @@
-import RNFetchBlob from 'rn-fetch-blob';
-
import reduxStore from '../createStore';
import database from '../realm';
import log from '../../utils/log';
-const promises = {};
-
-function _ufsCreate(fileInfo) {
- return this.sdk.methodCall('ufsCreate', fileInfo);
-}
-
-function _ufsComplete(fileId, store, token) {
- return this.sdk.methodCall('ufsComplete', fileId, store, token);
-}
-
-function _sendFileMessage(rid, data, msg = {}) {
- // RC 0.22.0
- return this.sdk.methodCall('sendFileMessage', rid, null, data, msg);
-}
+const uploadQueue = {};
export function isUploadActive(path) {
- return !!promises[path];
+ return !!uploadQueue[path];
}
-export async function cancelUpload(path) {
- if (promises[path]) {
- await promises[path].cancel();
- }
-}
-
-export async function sendFileMessage(rid, fileInfo, tmid) {
- try {
- const data = await RNFetchBlob.wrap(fileInfo.path);
- if (!fileInfo.size) {
- const fileStat = await RNFetchBlob.fs.stat(fileInfo.path);
- fileInfo.size = fileStat.size;
- fileInfo.name = fileStat.filename;
- }
-
- const { FileUpload_MaxFileSize } = reduxStore.getState().settings;
-
- // -1 maxFileSize means there is no limit
- if (FileUpload_MaxFileSize > -1 && fileInfo.size > FileUpload_MaxFileSize) {
- return Promise.reject({ error: 'error-file-too-large' }); // eslint-disable-line
- }
-
- fileInfo.rid = rid;
-
+export function cancelUpload(path) {
+ if (uploadQueue[path]) {
+ uploadQueue[path].abort();
database.write(() => {
- try {
- database.create('uploads', fileInfo, true);
- } catch (e) {
- return log('err_send_file_message_create_upload_1', e);
- }
- });
-
- const result = await _ufsCreate.call(this, fileInfo);
-
- promises[fileInfo.path] = RNFetchBlob.fetch('POST', result.url, {
- 'Content-Type': 'octet-stream'
- }, data);
- // Workaround for https://github.com/joltup/rn-fetch-blob/issues/96
- setTimeout(() => {
- if (promises[fileInfo.path] && promises[fileInfo.path].uploadProgress) {
- promises[fileInfo.path].uploadProgress((loaded, total) => {
- database.write(() => {
- fileInfo.progress = Math.floor((loaded / total) * 100);
- try {
- database.create('uploads', fileInfo, true);
- } catch (e) {
- return log('err_send_file_message_create_upload_2', e);
- }
- });
- });
- }
- });
- await promises[fileInfo.path];
-
- const completeResult = await _ufsComplete.call(this, result.fileId, fileInfo.store, result.token);
-
- await _sendFileMessage.call(this, completeResult.rid, {
- _id: completeResult._id,
- type: completeResult.type,
- size: completeResult.size,
- name: completeResult.name,
- description: completeResult.description,
- url: completeResult.path
- }, {
- tmid
- });
-
- database.write(() => {
- const upload = database.objects('uploads').filtered('path = $0', fileInfo.path);
+ const upload = database.objects('uploads').filtered('path = $0', path);
try {
database.delete(upload);
} catch (e) {
log('err_send_file_message_delete_upload', e);
}
});
- } catch (e) {
- database.write(() => {
- fileInfo.error = true;
- try {
- database.create('uploads', fileInfo, true);
- } catch (err) {
- log('err_send_file_message_create_upload_3', err);
- }
- });
+ delete uploadQueue[path];
}
}
+
+export function sendFileMessage(rid, fileInfo, tmid) {
+ return new Promise((resolve, reject) => {
+ try {
+ const { FileUpload_MaxFileSize, Site_Url } = reduxStore.getState().settings;
+ const { id, token } = reduxStore.getState().login.user;
+
+ // -1 maxFileSize means there is no limit
+ if (FileUpload_MaxFileSize > -1 && fileInfo.size > FileUpload_MaxFileSize) {
+ return reject({ error: 'error-file-too-large' }); // eslint-disable-line
+ }
+
+ const uploadUrl = `${ Site_Url }/api/v1/rooms.upload/${ rid }`;
+
+ const xhr = new XMLHttpRequest();
+ const formData = new FormData();
+
+ fileInfo.rid = rid;
+
+ database.write(() => {
+ try {
+ database.create('uploads', fileInfo, true);
+ } catch (e) {
+ return log('err_send_file_message_create_upload_1', e);
+ }
+ });
+
+ uploadQueue[fileInfo.path] = xhr;
+ xhr.open('POST', uploadUrl);
+
+ formData.append('file', {
+ uri: fileInfo.path,
+ type: fileInfo.type,
+ name: fileInfo.name || 'fileMessage'
+ });
+
+ if (fileInfo.description) {
+ formData.append('description', fileInfo.description);
+ }
+
+ if (tmid) {
+ formData.append('tmid', tmid);
+ }
+
+ xhr.setRequestHeader('X-Auth-Token', token);
+ xhr.setRequestHeader('X-User-Id', id);
+
+ xhr.upload.onprogress = ({ total, loaded }) => {
+ database.write(() => {
+ fileInfo.progress = Math.floor((loaded / total) * 100);
+ try {
+ database.create('uploads', fileInfo, true);
+ } catch (e) {
+ return log('err_send_file_message_create_upload_2', e);
+ }
+ });
+ };
+
+ xhr.onload = () => {
+ if (xhr.status >= 200 && xhr.status < 400) { // If response is all good...
+ database.write(() => {
+ const upload = database.objects('uploads').filtered('path = $0', fileInfo.path);
+ try {
+ database.delete(upload);
+ const response = JSON.parse(xhr.response);
+ resolve(response);
+ } catch (e) {
+ reject(e);
+ log('err_send_file_message_delete_upload', e);
+ }
+ });
+ } else {
+ database.write(() => {
+ fileInfo.error = true;
+ try {
+ 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);
+ }
+ });
+ }
+ };
+
+ xhr.onerror = (e) => {
+ database.write(() => {
+ fileInfo.error = true;
+ try {
+ database.create('uploads', fileInfo, true);
+ reject(e);
+ } catch (err) {
+ reject(err);
+ log('err_send_file_message_create_upload_3', err);
+ }
+ });
+ };
+
+ xhr.send(formData);
+ } catch (err) {
+ log('err_send_file_message_create_upload_4', err);
+ }
+ });
+}
diff --git a/app/lib/realm.js b/app/lib/realm.js
index f296752b8..7b4277882 100644
--- a/app/lib/realm.js
+++ b/app/lib/realm.js
@@ -4,6 +4,20 @@ import Realm from 'realm';
// Realm.clearTestState();
// AsyncStorage.clear();
+const userSchema = {
+ name: 'user',
+ primaryKey: 'id',
+ properties: {
+ id: 'string',
+ token: { type: 'string', optional: true },
+ username: { type: 'string', optional: true },
+ name: { type: 'string', optional: true },
+ language: { type: 'string', optional: true },
+ status: { type: 'string', optional: true },
+ roles: { type: 'string[]', optional: true }
+ }
+};
+
const serversSchema = {
name: 'servers',
primaryKey: 'id',
@@ -82,7 +96,9 @@ const subscriptionSchema = {
broadcast: { type: 'bool', optional: true },
prid: { type: 'string', optional: true },
draftMessage: { type: 'string', optional: true },
- lastThreadSync: 'date?'
+ lastThreadSync: 'date?',
+ autoTranslate: 'bool?',
+ autoTranslateLanguage: 'string?'
}
};
@@ -157,6 +173,16 @@ const messagesReactionsSchema = {
}
};
+const messagesTranslationsSchema = {
+ name: 'messagesTranslations',
+ primaryKey: '_id',
+ properties: {
+ _id: 'string',
+ language: 'string',
+ value: 'string'
+ }
+};
+
const messagesEditedBySchema = {
name: 'messagesEditedBy',
primaryKey: '_id',
@@ -198,7 +224,9 @@ const messagesSchema = {
replies: 'string[]',
mentions: { type: 'list', objectType: 'users' },
channels: { type: 'list', objectType: 'rooms' },
- unread: { type: 'bool', optional: true }
+ unread: { type: 'bool', optional: true },
+ autoTranslate: { type: 'bool', default: false },
+ translations: { type: 'list', objectType: 'messagesTranslations' }
}
};
@@ -232,6 +260,11 @@ const threadsSchema = {
tcount: { type: 'int', optional: true },
tlm: { type: 'date', optional: true },
replies: 'string[]',
+ mentions: { type: 'list', objectType: 'users' },
+ channels: { type: 'list', objectType: 'rooms' },
+ unread: { type: 'bool', optional: true },
+ autoTranslate: { type: 'bool', default: false },
+ translations: { type: 'list', objectType: 'messagesTranslations' },
draftMessage: 'string?'
}
};
@@ -258,7 +291,13 @@ const threadMessagesSchema = {
starred: { type: 'bool', optional: true },
editedBy: 'messagesEditedBy',
reactions: { type: 'list', objectType: 'messagesReactions' },
- role: { type: 'string', optional: true }
+ role: { type: 'string', optional: true },
+ replies: 'string[]',
+ mentions: { type: 'list', objectType: 'users' },
+ channels: { type: 'list', objectType: 'rooms' },
+ unread: { type: 'bool', optional: true },
+ autoTranslate: { type: 'bool', default: false },
+ translations: { type: 'list', objectType: 'messagesTranslations' }
}
};
@@ -360,7 +399,8 @@ const schema = [
messagesReactionsSchema,
rolesSchema,
uploadsSchema,
- slashCommandSchema
+ slashCommandSchema,
+ messagesTranslationsSchema
];
const inMemorySchema = [usersTypingSchema, activeUsersSchema];
@@ -370,11 +410,12 @@ class DB {
serversDB: new Realm({
path: 'default.realm',
schema: [
+ userSchema,
serversSchema
],
- schemaVersion: 8,
+ schemaVersion: 9,
migration: (oldRealm, newRealm) => {
- if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 8) {
+ if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 9) {
const newServers = newRealm.objects('servers');
// eslint-disable-next-line no-plusplus
@@ -429,9 +470,9 @@ class DB {
return this.databases.activeDB = new Realm({
path: `${ path }.realm`,
schema,
- schemaVersion: 12,
+ schemaVersion: 13,
migration: (oldRealm, newRealm) => {
- if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 11) {
+ if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 13) {
const newSubs = newRealm.objects('subscriptions');
newRealm.delete(newSubs);
const newMessages = newRealm.objects('messages');
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index a0118f5e7..364a43816 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -1,6 +1,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 reduxStore from './createStore';
import defaultSettings from '../constants/settings';
@@ -36,6 +37,7 @@ import sendMessage, { getMessage, sendMessageCall } from './methods/sendMessage'
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
import { getDeviceToken } from '../notifications/push';
+import { SERVERS, SERVER_URL } from '../constants/userDefaults';
const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
@@ -58,9 +60,9 @@ const RocketChat = {
},
async getUserToken() {
try {
- return await AsyncStorage.getItem(TOKEN_KEY);
+ return await RNUserDefaults.get(TOKEN_KEY);
} catch (error) {
- console.warn(`AsyncStorage error: ${ error.message }`);
+ console.warn(`RNUserDefaults error: ${ error.message }`);
}
},
async getServerInfo(server) {
@@ -321,10 +323,26 @@ const RocketChat = {
}
this.sdk = null;
+ try {
+ const servers = await RNUserDefaults.objectForKey(SERVERS);
+ await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server));
+ } catch (error) {
+ console.log('logout_rn_user_defaults', error);
+ }
+
+ const { serversDB } = database.databases;
+
+ const userId = await RNUserDefaults.get(`${ TOKEN_KEY }-${ server }`);
+
+ serversDB.write(() => {
+ const user = serversDB.objectForPrimaryKey('user', userId);
+ serversDB.delete(user);
+ });
+
Promise.all([
- AsyncStorage.removeItem('currentServer'),
- AsyncStorage.removeItem(TOKEN_KEY),
- AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`)
+ RNUserDefaults.clear('currentServer'),
+ RNUserDefaults.clear(TOKEN_KEY),
+ RNUserDefaults.clear(`${ TOKEN_KEY }-${ server }`)
]).catch(error => console.log(error));
try {
@@ -564,6 +582,12 @@ const RocketChat = {
// RC 0.64.0
return this.sdk.post('rooms.favorite', { roomId, favorite });
},
+ toggleRead(read, roomId) {
+ if (read) {
+ return this.sdk.post('subscriptions.unread', { roomId });
+ }
+ return this.sdk.post('subscriptions.read', { rid: roomId });
+ },
getRoomMembers(rid, allUsers, skip = 0, limit = 10) {
// RC 0.42.0
return this.sdk.methodCall('getUsersOfRoom', rid, allUsers, { skip, limit });
@@ -622,6 +646,9 @@ const RocketChat = {
// RC 0.48.0
return this.sdk.post(`${ this.roomTypeToApiType(t) }.unarchive`, { roomId });
},
+ hideRoom(roomId, t) {
+ return this.sdk.post(`${ this.roomTypeToApiType(t) }.close`, { roomId });
+ },
saveRoomSettings(rid, params) {
// RC 0.55.0
return this.sdk.methodCall('saveRoomSettings', rid, params);
@@ -863,6 +890,31 @@ const RocketChat = {
return this.sdk.get('directory', {
query, count, offset, sort
});
+ },
+ canAutoTranslate() {
+ try {
+ const AutoTranslate_Enabled = reduxStore.getState().settings && reduxStore.getState().settings.AutoTranslate_Enabled;
+ if (!AutoTranslate_Enabled) {
+ return false;
+ }
+ 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);
+ return false;
+ }
+ },
+ saveAutoTranslate({
+ rid, field, value, options
+ }) {
+ return this.sdk.methodCall('autoTranslate.saveSettings', rid, field, value, options);
+ },
+ getSupportedLanguagesAutoTranslate() {
+ return this.sdk.methodCall('autoTranslate.getSupportedLanguages', 'en');
+ },
+ translateMessage(message, targetLanguage) {
+ return this.sdk.methodCall('autoTranslate.translateMessage', message, targetLanguage);
}
};
diff --git a/app/presentation/RoomItem/Actions.js b/app/presentation/RoomItem/Actions.js
new file mode 100644
index 000000000..8793dc791
--- /dev/null
+++ b/app/presentation/RoomItem/Actions.js
@@ -0,0 +1,129 @@
+import React from 'react';
+import { Animated, View, Text } from 'react-native';
+import { RectButton } from 'react-native-gesture-handler';
+import PropTypes from 'prop-types';
+
+import I18n from '../../i18n';
+import styles, { ACTION_WIDTH, LONG_SWIPE } from './styles';
+import { CustomIcon } from '../../lib/Icons';
+
+export const LeftActions = React.memo(({
+ transX, isRead, width, onToggleReadPress
+}) => {
+ const translateX = transX.interpolate({
+ inputRange: [0, ACTION_WIDTH],
+ outputRange: [-ACTION_WIDTH, 0]
+ });
+ const translateXIcon = transX.interpolate({
+ inputRange: [0, ACTION_WIDTH, LONG_SWIPE - 2, LONG_SWIPE],
+ outputRange: [0, 0, -LONG_SWIPE + ACTION_WIDTH + 2, 0],
+ extrapolate: 'clamp'
+ });
+ return (
+
+
+
+
+
+
+ {I18n.t(isRead ? 'Unread' : 'Read')}
+
+
+
+
+
+ );
+});
+
+export const RightActions = React.memo(({
+ transX, favorite, width, toggleFav, onHidePress
+}) => {
+ const translateXFav = transX.interpolate({
+ inputRange: [-width / 2, -ACTION_WIDTH * 2, 0],
+ outputRange: [width / 2, width - ACTION_WIDTH * 2, width]
+ });
+ const translateXHide = transX.interpolate({
+ inputRange: [-width, -LONG_SWIPE, -ACTION_WIDTH * 2, 0],
+ outputRange: [0, width - LONG_SWIPE, width - ACTION_WIDTH, width]
+ });
+ return (
+
+
+
+
+
+ {I18n.t(favorite ? 'Unfavorite' : 'Favorite')}
+
+
+
+
+
+
+
+ {I18n.t('Hide')}
+
+
+
+
+ );
+});
+
+LeftActions.propTypes = {
+ transX: PropTypes.object,
+ isRead: PropTypes.bool,
+ width: PropTypes.number,
+ onToggleReadPress: PropTypes.func
+};
+
+RightActions.propTypes = {
+ transX: PropTypes.object,
+ favorite: PropTypes.bool,
+ width: PropTypes.number,
+ toggleFav: PropTypes.func,
+ onHidePress: PropTypes.func
+};
diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js
index 28a296dc7..1b5105f62 100644
--- a/app/presentation/RoomItem/index.js
+++ b/app/presentation/RoomItem/index.js
@@ -1,25 +1,23 @@
import React from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
-import { View, Text } from 'react-native';
-import { connect } from 'react-redux';
-import { RectButton } from 'react-native-gesture-handler';
+import { View, Text, Animated } from 'react-native';
+import { RectButton, PanGestureHandler, State } from 'react-native-gesture-handler';
import Avatar from '../../containers/Avatar';
import I18n from '../../i18n';
-import styles, { ROW_HEIGHT } from './styles';
+import styles, {
+ ROW_HEIGHT, ACTION_WIDTH, SMALL_SWIPE, LONG_SWIPE
+} from './styles';
import UnreadBadge from './UnreadBadge';
import TypeIcon from './TypeIcon';
import LastMessage from './LastMessage';
+import { LeftActions, RightActions } from './Actions';
export { ROW_HEIGHT };
-const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type'];
-@connect(state => ({
- userId: state.login.user && state.login.user.id,
- username: state.login.user && state.login.user.username,
- token: state.login.user && state.login.user.token
-}))
+const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type', 'width', 'isRead', 'favorite'];
+
export default class RoomItem extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
@@ -39,7 +37,13 @@ export default class RoomItem extends React.Component {
token: PropTypes.string,
avatarSize: PropTypes.number,
testID: PropTypes.string,
- height: PropTypes.number
+ width: PropTypes.number,
+ favorite: PropTypes.bool,
+ isRead: PropTypes.bool,
+ rid: PropTypes.string,
+ toggleFav: PropTypes.func,
+ toggleRead: PropTypes.func,
+ hideChannel: PropTypes.func
}
static defaultProps = {
@@ -50,6 +54,19 @@ export default class RoomItem extends React.Component {
// eslint-disable-next-line no-useless-constructor
constructor(props) {
super(props);
+ this.dragX = new Animated.Value(0);
+ this.rowOffSet = new Animated.Value(0);
+ this.transX = Animated.add(
+ this.rowOffSet,
+ this.dragX
+ );
+ this.state = {
+ rowState: 0 // 0: closed, 1: right opened, -1: left opened
+ };
+ this._onGestureEvent = Animated.event(
+ [{ nativeEvent: { translationX: this.dragX } }]
+ );
+ this._value = 0;
}
shouldComponentUpdate(nextProps) {
@@ -60,13 +77,132 @@ export default class RoomItem extends React.Component {
if (oldlastMessage && newLastmessage && oldlastMessage.ts !== newLastmessage.ts) {
return true;
}
- if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt !== _updatedAt) {
+ if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt.toISOString() !== _updatedAt.toISOString()) {
return true;
}
// eslint-disable-next-line react/destructuring-assignment
return attrs.some(key => nextProps[key] !== this.props[key]);
}
+ _onHandlerStateChange = ({ nativeEvent }) => {
+ if (nativeEvent.oldState === State.ACTIVE) {
+ this._handleRelease(nativeEvent);
+ }
+ };
+
+ _handleRelease = (nativeEvent) => {
+ const { translationX } = nativeEvent;
+ const { rowState } = this.state;
+ this._value = this._value + translationX;
+
+ let toValue = 0;
+ if (rowState === 0) { // if no option is opened
+ if (translationX > 0 && translationX < LONG_SWIPE) {
+ toValue = ACTION_WIDTH; // open left option if he swipe right but not enough to trigger action
+ this.setState({ rowState: -1 });
+ } else if (translationX >= LONG_SWIPE) {
+ toValue = 0;
+ this.toggleRead();
+ } else if (translationX < 0 && translationX > -LONG_SWIPE) {
+ toValue = -2 * ACTION_WIDTH; // open right option if he swipe left
+ this.setState({ rowState: 1 });
+ } else if (translationX <= -LONG_SWIPE) {
+ toValue = 0;
+ this.setState({ rowState: 0 });
+ this.hideChannel();
+ } else {
+ toValue = 0;
+ }
+ }
+
+ if (rowState === -1) { // if left option is opened
+ if (this._value < SMALL_SWIPE) {
+ toValue = 0;
+ this.setState({ rowState: 0 });
+ } else if (this._value > LONG_SWIPE) {
+ toValue = 0;
+ this.setState({ rowState: 0 });
+ this.toggleRead();
+ } else {
+ toValue = ACTION_WIDTH;
+ }
+ }
+
+ if (rowState === 1) { // if right option is opened
+ if (this._value > -2 * SMALL_SWIPE) {
+ toValue = 0;
+ this.setState({ rowState: 0 });
+ } else if (this._value < -LONG_SWIPE) {
+ toValue = 0;
+ this.setState({ rowState: 0 });
+ this.hideChannel();
+ } else {
+ toValue = -2 * ACTION_WIDTH;
+ }
+ }
+ this._animateRow(toValue);
+ }
+
+ _animateRow = (toValue) => {
+ this.rowOffSet.setValue(this._value);
+ this._value = toValue;
+ this.dragX.setValue(0);
+ Animated.spring(this.rowOffSet, {
+ toValue,
+ bounciness: 0,
+ useNativeDriver: true
+ }).start();
+ }
+
+ close = () => {
+ this.setState({ rowState: 0 });
+ this._animateRow(0);
+ }
+
+ toggleFav = () => {
+ const { toggleFav, rid, favorite } = this.props;
+ if (toggleFav) {
+ toggleFav(rid, favorite);
+ }
+ this.close();
+ }
+
+ toggleRead = () => {
+ const { toggleRead, rid, isRead } = this.props;
+ if (toggleRead) {
+ toggleRead(rid, isRead);
+ }
+ }
+
+ hideChannel = () => {
+ const { hideChannel, rid, type } = this.props;
+ if (hideChannel) {
+ hideChannel(rid, type);
+ }
+ }
+
+ onToggleReadPress = () => {
+ this.toggleRead();
+ this.close();
+ }
+
+ onHidePress = () => {
+ this.hideChannel();
+ this.close();
+ }
+
+ onPress = () => {
+ const { rowState } = this.state;
+ if (rowState !== 0) {
+ this.close();
+ return;
+ }
+ const { onPress } = this.props;
+ if (onPress) {
+ onPress();
+ }
+ }
+
formatDate = date => moment(date).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
@@ -76,7 +212,7 @@ export default class RoomItem extends React.Component {
render() {
const {
- unread, userMentions, name, _updatedAt, alert, testID, height, type, avatarSize, baseUrl, userId, username, token, onPress, id, prid, showLastMessage, lastMessage
+ unread, userMentions, name, _updatedAt, alert, testID, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, lastMessage, isRead, width, favorite
} = this.props;
const date = this.formatDate(_updatedAt);
@@ -97,30 +233,60 @@ export default class RoomItem extends React.Component {
}
return (
-
-
-
-
-
-
- { name }
- {_updatedAt ? { date } : null}
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ { name }
+ {_updatedAt ? { date } : null}
+
+
+
+
+
+
+
+
+
+
+
);
}
}
diff --git a/app/presentation/RoomItem/styles.js b/app/presentation/RoomItem/styles.js
index 87fb92c94..6ad29fd6d 100644
--- a/app/presentation/RoomItem/styles.js
+++ b/app/presentation/RoomItem/styles.js
@@ -6,14 +6,20 @@ import {
} from '../../constants/colors';
export const ROW_HEIGHT = 75 * PixelRatio.getFontScale();
+export const ACTION_WIDTH = 80;
+export const SMALL_SWIPE = ACTION_WIDTH / 2;
+export const LONG_SWIPE = ACTION_WIDTH * 3;
export default StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
- marginLeft: 14,
+ paddingLeft: 14,
height: ROW_HEIGHT
},
+ button: {
+ backgroundColor: COLOR_WHITE
+ },
centerContainer: {
flex: 1,
paddingVertical: 10,
@@ -93,5 +99,42 @@ export default StyleSheet.create({
},
avatar: {
marginRight: 10
+ },
+ upperContainer: {
+ overflow: 'hidden'
+ },
+ actionsContainer: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ height: ROW_HEIGHT
+ },
+ actionText: {
+ color: COLOR_WHITE,
+ fontSize: 15,
+ backgroundColor: 'transparent',
+ justifyContent: 'center',
+ marginTop: 4,
+ ...sharedStyles.textSemibold
+ },
+ actionLeftButtonContainer: {
+ position: 'absolute',
+ height: ROW_HEIGHT,
+ backgroundColor: COLOR_PRIMARY,
+ justifyContent: 'center',
+ top: 0
+ },
+ actionRightButtonContainer: {
+ position: 'absolute',
+ height: ROW_HEIGHT,
+ justifyContent: 'center',
+ top: 0,
+ backgroundColor: '#54585e'
+ },
+ actionButton: {
+ width: ACTION_WIDTH,
+ height: '100%',
+ alignItems: 'center',
+ justifyContent: 'center'
}
});
diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js
index de7bed594..892731602 100644
--- a/app/sagas/deepLinking.js
+++ b/app/sagas/deepLinking.js
@@ -1,8 +1,8 @@
-import { AsyncStorage } from 'react-native';
import { delay } from 'redux-saga';
import {
takeLatest, take, select, put, all
} from 'redux-saga/effects';
+import RNUserDefaults from 'rn-user-defaults';
import Navigation from '../lib/Navigation';
import * as types from '../actions/actionsTypes';
@@ -11,6 +11,7 @@ import database from '../lib/realm';
import RocketChat from '../lib/rocketchat';
import EventEmitter from '../utils/events';
import { appStart } from '../actions';
+import { isIOS } from '../utils/deviceInfo';
const roomTypes = {
channel: 'c', direct: 'd', group: 'p'
@@ -33,6 +34,10 @@ const handleOpen = function* handleOpen({ params }) {
return;
}
+ if (isIOS) {
+ yield RNUserDefaults.setName('group.ios.chat.rocket');
+ }
+
let { host } = params;
if (!/^(http|https)/.test(host)) {
host = `https://${ params.host }`;
@@ -43,8 +48,8 @@ const handleOpen = function* handleOpen({ params }) {
}
const [server, user] = yield all([
- AsyncStorage.getItem('currentServer'),
- AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ host }`)
+ RNUserDefaults.get('currentServer'),
+ RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ host }`)
]);
// TODO: needs better test
diff --git a/app/sagas/init.js b/app/sagas/init.js
index 4d92b3fa2..e333cee6d 100644
--- a/app/sagas/init.js
+++ b/app/sagas/init.js
@@ -1,6 +1,7 @@
import { AsyncStorage } from 'react-native';
import { put, takeLatest, all } from 'redux-saga/effects';
import SplashScreen from 'react-native-splash-screen';
+import RNUserDefaults from 'rn-user-defaults';
import * as actions from '../actions';
import { selectServerRequest } from '../actions/server';
@@ -11,14 +12,54 @@ import RocketChat from '../lib/rocketchat';
import log from '../utils/log';
import Navigation from '../lib/Navigation';
import database from '../lib/realm';
+import {
+ SERVERS, SERVER_ICON, SERVER_NAME, SERVER_URL, TOKEN, USER_ID
+} from '../constants/userDefaults';
+import { isIOS } from '../utils/deviceInfo';
const restore = function* restore() {
try {
- const { token, server } = yield all({
- token: AsyncStorage.getItem(RocketChat.TOKEN_KEY),
- server: AsyncStorage.getItem('currentServer')
+ let hasMigration;
+ if (isIOS) {
+ yield RNUserDefaults.setName('group.ios.chat.rocket');
+ hasMigration = yield AsyncStorage.getItem('hasMigration');
+ }
+
+ let { token, server } = yield all({
+ token: RNUserDefaults.get(RocketChat.TOKEN_KEY),
+ server: RNUserDefaults.get('currentServer')
});
+ // get native credentials
+ if (isIOS && !hasMigration) {
+ const { serversDB } = database.databases;
+ const servers = yield RNUserDefaults.objectForKey(SERVERS);
+ if (servers) {
+ serversDB.write(() => {
+ servers.forEach(async(serverItem) => {
+ const serverInfo = {
+ id: serverItem[SERVER_URL],
+ name: serverItem[SERVER_NAME],
+ iconURL: serverItem[SERVER_ICON]
+ };
+ try {
+ serversDB.create('servers', serverInfo, true);
+ await RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ serverInfo.id }`, serverItem[USER_ID]);
+ } catch (e) {
+ log('err_create_servers', e);
+ }
+ });
+ });
+ yield AsyncStorage.setItem('hasMigration', '1');
+ }
+
+ // if not have current
+ if (servers && servers.length !== 0 && (!token || !server)) {
+ server = servers[0][SERVER_URL];
+ token = servers[0][TOKEN];
+ }
+ }
+
const sortPreferences = yield RocketChat.getSortPreferences();
yield put(setAllPreferences(sortPreferences));
@@ -27,8 +68,8 @@ const restore = function* restore() {
if (!token || !server) {
yield all([
- AsyncStorage.removeItem(RocketChat.TOKEN_KEY),
- AsyncStorage.removeItem('currentServer')
+ RNUserDefaults.clear(RocketChat.TOKEN_KEY),
+ RNUserDefaults.clear('currentServer')
]);
yield put(actions.appStart('outside'));
} else if (server) {
diff --git a/app/sagas/login.js b/app/sagas/login.js
index 66b4183ad..369f636ff 100644
--- a/app/sagas/login.js
+++ b/app/sagas/login.js
@@ -1,7 +1,7 @@
-import { AsyncStorage } from 'react-native';
import {
put, call, takeLatest, select, take, fork, cancel
} from 'redux-saga/effects';
+import RNUserDefaults from 'rn-user-defaults';
import * as types from '../actions/actionsTypes';
import { appStart } from '../actions';
@@ -60,7 +60,7 @@ const fetchUserPresence = function* fetchUserPresence() {
const handleLoginSuccess = function* handleLoginSuccess({ user }) {
try {
const adding = yield select(state => state.server.adding);
- yield AsyncStorage.setItem(RocketChat.TOKEN_KEY, user.token);
+ yield RNUserDefaults.set(RocketChat.TOKEN_KEY, user.token);
const server = yield select(getServer);
yield put(roomsRequest());
@@ -72,7 +72,17 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
yield fork(fetchUserPresence);
I18n.locale = user.language;
- yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user));
+
+ const { serversDB } = database.databases;
+ serversDB.write(() => {
+ try {
+ serversDB.create('user', user, true);
+ } catch (e) {
+ log('err_set_user_token', e);
+ }
+ });
+
+ yield RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ server }`, user.id);
yield put(setUser(user));
EventEmitter.emit('connected');
@@ -105,7 +115,7 @@ const handleLogout = function* handleLogout() {
// see if there's other logged in servers and selects first one
if (servers.length > 0) {
const newServer = servers[0].id;
- const token = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ newServer }`);
+ const token = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ newServer }`);
if (token) {
return yield put(selectServerRequest(newServer));
}
diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js
index b6f1efec5..f6793f998 100644
--- a/app/sagas/selectServer.js
+++ b/app/sagas/selectServer.js
@@ -1,7 +1,8 @@
import {
put, take, takeLatest, fork, cancel, race
} from 'redux-saga/effects';
-import { AsyncStorage, Alert } from 'react-native';
+import { Alert } from 'react-native';
+import RNUserDefaults from 'rn-user-defaults';
import Navigation from '../lib/Navigation';
import { SERVER } from '../actions/actionsTypes';
@@ -14,6 +15,7 @@ import RocketChat from '../lib/rocketchat';
import database from '../lib/realm';
import log from '../utils/log';
import I18n from '../i18n';
+import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults';
const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
try {
@@ -38,13 +40,21 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) {
try {
- yield AsyncStorage.setItem('currentServer', server);
- const userStringified = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`);
+ const { serversDB } = database.databases;
- if (userStringified) {
- const user = JSON.parse(userStringified);
- yield RocketChat.connect({ server, user });
- yield put(setUser(user));
+ yield RNUserDefaults.set('currentServer', server);
+ const userId = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`);
+ const user = userId && serversDB.objectForPrimaryKey('user', userId);
+
+ const servers = yield RNUserDefaults.objectForKey(SERVERS);
+ const userCredentials = servers && servers.find(srv => srv[SERVER_URL] === server);
+ const userLogin = userCredentials && {
+ token: userCredentials[TOKEN]
+ };
+
+ if (user || userLogin) {
+ yield RocketChat.connect({ server, user: user || userLogin });
+ yield put(setUser(user || userLogin));
yield put(actions.appStart('inside'));
} else {
yield RocketChat.connect({ server });
diff --git a/app/utils/vibration.js b/app/utils/vibration.js
deleted file mode 100644
index a91c3e13c..000000000
--- a/app/utils/vibration.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Vibration } from 'react-native';
-
-import { isAndroid } from './deviceInfo';
-
-const vibrate = () => {
- if (isAndroid) {
- Vibration.vibrate(30);
- }
-};
-
-export { vibrate };
diff --git a/app/views/AutoTranslateView/index.js b/app/views/AutoTranslateView/index.js
new file mode 100644
index 000000000..d2691632d
--- /dev/null
+++ b/app/views/AutoTranslateView/index.js
@@ -0,0 +1,156 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ FlatList, Switch, View, StyleSheet
+} from 'react-native';
+import { SafeAreaView, ScrollView } from 'react-navigation';
+
+import RocketChat from '../../lib/rocketchat';
+import I18n from '../../i18n';
+// import log from '../../utils/log';
+import StatusBar from '../../containers/StatusBar';
+import { CustomIcon } from '../../lib/Icons';
+import sharedStyles from '../Styles';
+import ListItem from '../../containers/ListItem';
+import Separator from '../../containers/Separator';
+import {
+ SWITCH_TRACK_COLOR, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE, COLOR_SEPARATOR
+} from '../../constants/colors';
+import scrollPersistTaps from '../../utils/scrollPersistTaps';
+import database from '../../lib/realm';
+
+const styles = StyleSheet.create({
+ contentContainerStyle: {
+ borderColor: COLOR_SEPARATOR,
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ backgroundColor: COLOR_WHITE,
+ marginTop: 10,
+ paddingBottom: 30
+ },
+ sectionSeparator: {
+ ...sharedStyles.separatorVertical,
+ backgroundColor: COLOR_BACKGROUND_CONTAINER,
+ height: 10
+ }
+});
+
+const SectionSeparator = React.memo(() => );
+
+export default class AutoTranslateView extends React.Component {
+ static navigationOptions = () => ({
+ title: I18n.t('Auto_Translate')
+ })
+
+ 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 = {
+ languages: [],
+ selectedLanguage: this.rooms[0].autoTranslateLanguage,
+ enableAutoTranslate: this.rooms[0].autoTranslate
+ };
+ }
+
+ async componentDidMount() {
+ try {
+ const languages = await RocketChat.getSupportedLanguagesAutoTranslate();
+ this.setState({ languages });
+ } catch (error) {
+ console.log(error);
+ }
+ }
+
+ toggleAutoTranslate = async() => {
+ const { enableAutoTranslate } = this.state;
+ try {
+ await RocketChat.saveAutoTranslate({
+ rid: this.rid,
+ field: 'autoTranslate',
+ value: enableAutoTranslate ? '0' : '1',
+ options: { defaultLanguage: 'en' }
+ });
+ this.setState({ enableAutoTranslate: !enableAutoTranslate });
+ } catch (error) {
+ console.log(error);
+ }
+ }
+
+ saveAutoTranslateLanguage = async(language) => {
+ try {
+ await RocketChat.saveAutoTranslate({
+ rid: this.rid,
+ field: 'autoTranslateLanguage',
+ value: language
+ });
+ this.setState({ selectedLanguage: language });
+ } catch (error) {
+ console.log(error);
+ }
+ }
+
+ renderSeparator = () =>
+
+ renderIcon = () =>
+
+ renderSwitch = () => {
+ const { enableAutoTranslate } = this.state;
+ return (
+
+ );
+ }
+
+ renderItem = ({ item }) => {
+ const { selectedLanguage } = this.state;
+ const { language, name } = item;
+ const isSelected = selectedLanguage === language;
+
+ return (
+ this.saveAutoTranslateLanguage(language)}
+ testID={`auto-translate-view-${ language }`}
+ right={isSelected ? this.renderIcon : null}
+ />
+ );
+ }
+
+ render() {
+ const { languages } = this.state;
+ return (
+
+
+
+ this.renderSwitch()}
+ />
+
+ item.language}
+ renderItem={this.renderItem}
+ ItemSeparatorComponent={this.renderSeparator}
+ />
+
+
+ );
+ }
+}
+
+console.disableYellowBox = true;
diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js
index 2cf346fcc..b29cedbda 100644
--- a/app/views/CreateChannelView.js
+++ b/app/views/CreateChannelView.js
@@ -16,10 +16,9 @@ import scrollPersistTaps from '../utils/scrollPersistTaps';
import I18n from '../i18n';
import UserItem from '../presentation/UserItem';
import { showErrorAlert } from '../utils/info';
-import { isAndroid } from '../utils/deviceInfo';
import { CustomHeaderButtons, Item } from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar';
-import { COLOR_TEXT_DESCRIPTION, COLOR_WHITE } from '../constants/colors';
+import { COLOR_TEXT_DESCRIPTION, COLOR_WHITE, SWITCH_TRACK_COLOR } from '../constants/colors';
const styles = StyleSheet.create({
container: {
@@ -245,8 +244,7 @@ export default class CreateChannelView extends React.Component {
value={value}
onValueChange={onValueChange}
testID={`create-channel-${ id }`}
- onTintColor='#2de0a5'
- tintColor={isAndroid ? '#f5455c' : null}
+ trackColor={SWITCH_TRACK_COLOR}
disabled={disabled}
/>
diff --git a/app/views/DirectoryView/Options.js b/app/views/DirectoryView/Options.js
index 841484152..31725c118 100644
--- a/app/views/DirectoryView/Options.js
+++ b/app/views/DirectoryView/Options.js
@@ -9,6 +9,7 @@ import styles from './styles';
import { CustomIcon } from '../../lib/Icons';
import Check from '../../containers/Check';
import I18n from '../../i18n';
+import { SWITCH_TRACK_COLOR } from '../../constants/colors';
const ANIMATION_DURATION = 200;
const ANIMATION_PROPS = {
@@ -109,7 +110,7 @@ export default class DirectoryOptions extends PureComponent {
{I18n.t('Search_global_users')}
{I18n.t('Search_global_users_description')}
-
+
)
diff --git a/app/views/LanguageView/index.js b/app/views/LanguageView/index.js
index 045d2e9c5..5c7522fbb 100644
--- a/app/views/LanguageView/index.js
+++ b/app/views/LanguageView/index.js
@@ -46,7 +46,6 @@ const LANGUAGES = [
}), dispatch => ({
setUser: params => dispatch(setUserAction(params))
}))
-/** @extends React.Component */
export default class LanguageView extends React.Component {
static navigationOptions = () => ({
title: I18n.t('Change_Language')
diff --git a/app/views/RegisterView.js b/app/views/RegisterView.js
index 6c199a370..abb8f52b3 100644
--- a/app/views/RegisterView.js
+++ b/app/views/RegisterView.js
@@ -5,6 +5,8 @@ import {
} from 'react-native';
import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
+import RNPickerSelect from 'react-native-picker-select';
+import equal from 'deep-equal';
import TextInput from '../containers/TextInput';
import Button from '../containers/Button';
@@ -17,10 +19,13 @@ import { loginRequest as loginRequestAction } from '../actions/login';
import isValidEmail from '../utils/isValidEmail';
import { LegalButton } from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar';
+import log from '../utils/log';
const shouldUpdateState = ['name', 'email', 'password', 'username', 'saving'];
-@connect(null, dispatch => ({
+@connect(state => ({
+ Accounts_CustomFields: state.settings.Accounts_CustomFields
+}), dispatch => ({
loginRequest: params => dispatch(loginRequestAction(params))
}))
export default class RegisterView extends React.Component {
@@ -35,15 +40,34 @@ export default class RegisterView extends React.Component {
static propTypes = {
navigation: PropTypes.object,
loginRequest: PropTypes.func,
- Site_Name: PropTypes.string
+ Site_Name: PropTypes.string,
+ Accounts_CustomFields: PropTypes.string
}
- state = {
- name: '',
- email: '',
- password: '',
- username: '',
- saving: false
+ constructor(props) {
+ super(props);
+ const customFields = {};
+ this.parsedCustomFields = {};
+ if (props.Accounts_CustomFields) {
+ try {
+ this.parsedCustomFields = JSON.parse(props.Accounts_CustomFields);
+ } catch (e) {
+ log('err_parsing_account_custom_fields', e);
+ }
+ }
+ Object.keys(this.parsedCustomFields).forEach((key) => {
+ if (this.parsedCustomFields[key].defaultValue) {
+ customFields[key] = this.parsedCustomFields[key].defaultValue;
+ }
+ });
+ this.state = {
+ name: '',
+ email: '',
+ password: '',
+ username: '',
+ saving: false,
+ customFields
+ };
}
componentDidMount() {
@@ -53,6 +77,10 @@ export default class RegisterView extends React.Component {
}
shouldComponentUpdate(nextProps, nextState) {
+ const { customFields } = this.state;
+ if (!equal(nextState.customFields, customFields)) {
+ return true;
+ }
// eslint-disable-next-line react/destructuring-assignment
return shouldUpdateState.some(key => nextState[key] !== this.state[key]);
}
@@ -77,9 +105,15 @@ export default class RegisterView extends React.Component {
valid = () => {
const {
- name, email, password, username
+ name, email, password, username, customFields
} = this.state;
- return name.trim() && email.trim() && password.trim() && username.trim() && isValidEmail(email);
+ let requiredCheck = true;
+ Object.keys(this.parsedCustomFields).forEach((key) => {
+ if (this.parsedCustomFields[key].required) {
+ requiredCheck = requiredCheck && customFields[key] && Boolean(customFields[key].trim());
+ }
+ });
+ return name.trim() && email.trim() && password.trim() && username.trim() && isValidEmail(email) && requiredCheck;
}
submit = async() => {
@@ -90,13 +124,13 @@ export default class RegisterView extends React.Component {
Keyboard.dismiss();
const {
- name, email, password, username
+ name, email, password, username, customFields
} = this.state;
const { loginRequest } = this.props;
try {
await RocketChat.register({
- name, email, pass: password, username
+ name, email, pass: password, username, ...customFields
});
await loginRequest({ user: email, password });
} catch (e) {
@@ -105,6 +139,64 @@ export default class RegisterView extends React.Component {
this.setState({ saving: false });
}
+ renderCustomFields = () => {
+ const { customFields } = this.state;
+ const { Accounts_CustomFields } = this.props;
+ if (!Accounts_CustomFields) {
+ return null;
+ }
+ try {
+ return Object.keys(this.parsedCustomFields).map((key, index, array) => {
+ if (this.parsedCustomFields[key].type === 'select') {
+ const options = this.parsedCustomFields[key].options.map(option => ({ label: option, value: option }));
+ return (
+ {
+ const newValue = {};
+ newValue[key] = value;
+ this.setState({ customFields: { ...customFields, ...newValue } });
+ }}
+ value={customFields[key]}
+ >
+ { this[key] = e; }}
+ placeholder={key}
+ value={customFields[key]}
+ iconLeft='flag'
+ testID='register-view-custom-picker'
+ />
+
+ );
+ }
+
+ return (
+ { this[key] = e; }}
+ key={key}
+ placeholder={key}
+ value={customFields[key]}
+ iconLeft='flag'
+ onChangeText={(value) => {
+ const newValue = {};
+ newValue[key] = value;
+ this.setState({ customFields: { ...customFields, ...newValue } });
+ }}
+ onSubmitEditing={() => {
+ if (array.length - 1 > index) {
+ return this[array[index + 1]].focus();
+ }
+ this.avatarUrl.focus();
+ }}
+ />
+ );
+ });
+ } catch (error) {
+ return null;
+ }
+ }
+
render() {
const { saving } = this.state;
return (
@@ -153,6 +245,8 @@ export default class RegisterView extends React.Component {
containerStyle={sharedStyles.inputLastChild}
/>
+ {this.renderCustomFields()}
+