[NEW] Auto-translate (#1012)

* Update realm

* View original and translate working

* Read AutoTranslate_Enabled setting

* RocketChat.canAutoTranslate()

* AutoTranslateView

* Save language

* Auto-translate switch

* Translate message
This commit is contained in:
Diego Mello 2019-06-28 14:02:30 -03:00 committed by GitHub
parent cfa126914c
commit b3986b98b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 330 additions and 32 deletions

View File

@ -1,4 +1,4 @@
import { isIOS } from '../utils/deviceInfo'; import { isIOS, isAndroid } from '../utils/deviceInfo';
export const COLOR_DANGER = '#f5455c'; export const COLOR_DANGER = '#f5455c';
export const COLOR_SUCCESS = '#2de0a5'; 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_TITLE = isIOS ? COLOR_TITLE : COLOR_WHITE;
export const HEADER_BACK = isIOS ? COLOR_PRIMARY : COLOR_WHITE; export const HEADER_BACK = isIOS ? COLOR_PRIMARY : COLOR_WHITE;
export const HEADER_TINT = 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
};

View File

@ -70,5 +70,8 @@ export default {
}, },
API_Gitlab_URL: { API_Gitlab_URL: {
type: 'valueAsString' type: 'valueAsString'
},
AutoTranslate_Enabled: {
type: 'valueAsBoolean'
} }
}; };

View File

@ -15,9 +15,11 @@ import {
} from '../actions/messages'; } from '../actions/messages';
import { vibrate } from '../utils/vibration'; import { vibrate } from '../utils/vibration';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm';
import I18n from '../i18n'; import I18n from '../i18n';
import log from '../utils/log'; import log from '../utils/log';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import { getMessageTranslation } from './message/utils';
@connect( @connect(
state => ({ state => ({
@ -46,7 +48,7 @@ export default class MessageActions extends React.Component {
room: PropTypes.object.isRequired, room: PropTypes.object.isRequired,
actionMessage: PropTypes.object, actionMessage: PropTypes.object,
toast: PropTypes.element, toast: PropTypes.element,
// user: PropTypes.object.isRequired, user: PropTypes.object,
deleteRequest: PropTypes.func.isRequired, deleteRequest: PropTypes.func.isRequired,
editInit: PropTypes.func.isRequired, editInit: PropTypes.func.isRequired,
toggleStarRequest: PropTypes.func.isRequired, toggleStarRequest: PropTypes.func.isRequired,
@ -127,6 +129,12 @@ export default class MessageActions extends React.Component {
this.READ_RECEIPT_INDEX = this.options.length - 1; 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 // Report
this.options.push(I18n.t('Report')); this.options.push(I18n.t('Report'));
this.REPORT_INDEX = this.options.length - 1; this.REPORT_INDEX = this.options.length - 1;
@ -326,6 +334,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) => { handleActionPress = (actionIndex) => {
if (actionIndex) { if (actionIndex) {
switch (actionIndex) { switch (actionIndex) {
@ -365,6 +390,9 @@ export default class MessageActions extends React.Component {
case this.READ_RECEIPT_INDEX: case this.READ_RECEIPT_INDEX:
this.handleReadReceipt(); this.handleReadReceipt();
break; break;
case this.TOGGLE_TRANSLATION_INDEX:
this.handleToggleTranslation();
break;
default: default:
break; break;
} }

View File

@ -4,7 +4,7 @@ import { KeyboardUtils } from 'react-native-keyboard-input';
import Message from './Message'; import Message from './Message';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import { SYSTEM_MESSAGES, getCustomEmoji } from './utils'; import { SYSTEM_MESSAGES, getCustomEmoji, getMessageTranslation } from './utils';
import messagesStatus from '../../constants/messagesStatus'; import messagesStatus from '../../constants/messagesStatus';
export default class MessageContainer extends React.Component { export default class MessageContainer extends React.Component {
@ -27,6 +27,8 @@ export default class MessageContainer extends React.Component {
isReadReceiptEnabled: PropTypes.bool, isReadReceiptEnabled: PropTypes.bool,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
useMarkdown: PropTypes.bool, useMarkdown: PropTypes.bool,
autoTranslateRoom: PropTypes.bool,
autoTranslateLanguage: PropTypes.string,
status: PropTypes.number, status: PropTypes.number,
onLongPress: PropTypes.func, onLongPress: PropTypes.func,
onReactionPress: PropTypes.func, onReactionPress: PropTypes.func,
@ -49,12 +51,15 @@ export default class MessageContainer extends React.Component {
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
const { const {
status, item, _updatedAt status, item, _updatedAt, autoTranslateRoom
} = this.props; } = this.props;
if (status !== nextProps.status) { if (status !== nextProps.status) {
return true; return true;
} }
if (autoTranslateRoom !== nextProps.autoTranslateRoom) {
return true;
}
if (item.tmsg !== nextProps.item.tmsg) { if (item.tmsg !== nextProps.item.tmsg) {
return true; return true;
} }
@ -191,16 +196,23 @@ export default class MessageContainer extends React.Component {
render() { render() {
const { 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; } = this.props;
const { const {
_id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread _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; } = 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 (
<Message <Message
id={_id} id={_id}
msg={msg} msg={message}
author={u} author={u}
ts={ts} ts={ts}
type={t} type={t}

View File

@ -114,3 +114,15 @@ export const getCustomEmoji = (content) => {
}); });
return findByAlias; 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;
};

View File

@ -99,6 +99,7 @@ export default {
Are_you_sure_question_mark: 'Are you sure?', Are_you_sure_question_mark: 'Are you sure?',
Are_you_sure_you_want_to_leave_the_room: 'Are you sure you want to leave the room {{room}}?', Are_you_sure_you_want_to_leave_the_room: 'Are you sure you want to leave the room {{room}}?',
Authenticating: 'Authenticating', Authenticating: 'Authenticating',
Auto_Translate: 'Auto-Translate',
Avatar_changed_successfully: 'Avatar changed successfully!', Avatar_changed_successfully: 'Avatar changed successfully!',
Avatar_Url: 'Avatar URL', Avatar_Url: 'Avatar URL',
Away: 'Away', Away: 'Away',
@ -155,6 +156,7 @@ export default {
Email_or_password_field_is_empty: 'Email or password field is empty', Email_or_password_field_is_empty: 'Email or password field is empty',
Email: 'Email', Email: 'Email',
email: 'e-mail', email: 'e-mail',
Enable_Auto_Translate: 'Enable Auto-Translate',
Enable_markdown: 'Enable markdown', Enable_markdown: 'Enable markdown',
Enable_notifications: 'Enable notifications', Enable_notifications: 'Enable notifications',
Everyone_can_access_this_channel: 'Everyone can access this channel', Everyone_can_access_this_channel: 'Everyone can access this channel',
@ -343,6 +345,7 @@ export default {
Timezone: 'Timezone', Timezone: 'Timezone',
topic: 'topic', topic: 'topic',
Topic: 'Topic', Topic: 'Topic',
Translate: 'Translate',
Try_again: 'Try again', Try_again: 'Try again',
Two_Factor_Authentication: 'Two-factor Authentication', Two_Factor_Authentication: 'Two-factor Authentication',
Type_the_channel_name_here: 'Type the channel name here', Type_the_channel_name_here: 'Type the channel name here',
@ -374,6 +377,7 @@ export default {
Username_or_email: 'Username or email', Username_or_email: 'Username or email',
Validating: 'Validating', Validating: 'Validating',
Video_call: 'Video call', Video_call: 'Video call',
View_Original: 'View Original',
Voice_call: 'Voice call', Voice_call: 'Voice call',
Welcome: 'Welcome', Welcome: 'Welcome',
Welcome_to_RocketChat: 'Welcome to Rocket.Chat', Welcome_to_RocketChat: 'Welcome to Rocket.Chat',

View File

@ -33,6 +33,7 @@ import SearchMessagesView from './views/SearchMessagesView';
import ReadReceiptsView from './views/ReadReceiptView'; import ReadReceiptsView from './views/ReadReceiptView';
import ThreadMessagesView from './views/ThreadMessagesView'; import ThreadMessagesView from './views/ThreadMessagesView';
import MessagesView from './views/MessagesView'; import MessagesView from './views/MessagesView';
import AutoTranslateView from './views/AutoTranslateView';
import SelectedUsersView from './views/SelectedUsersView'; import SelectedUsersView from './views/SelectedUsersView';
import CreateChannelView from './views/CreateChannelView'; import CreateChannelView from './views/CreateChannelView';
import LegalView from './views/LegalView'; import LegalView from './views/LegalView';
@ -116,6 +117,7 @@ const ChatsStack = createStackNavigator({
SelectedUsersView, SelectedUsersView,
ThreadMessagesView, ThreadMessagesView,
MessagesView, MessagesView,
AutoTranslateView,
ReadReceiptsView, ReadReceiptsView,
DirectoryView DirectoryView
}, { }, {

View File

@ -36,6 +36,9 @@ export default (msg) => {
if (!Array.isArray(msg.reactions)) { if (!Array.isArray(msg.reactions)) {
msg.reactions = Object.keys(msg.reactions).map(key => ({ _id: `${ msg._id }${ key }`, emoji: key, usernames: msg.reactions[key].usernames })); 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.urls = msg.urls ? parseUrls(msg.urls) : []; msg.urls = msg.urls ? parseUrls(msg.urls) : [];
msg._updatedAt = new Date(); msg._updatedAt = new Date();
// loadHistory returns msg.starred as object // loadHistory returns msg.starred as object

View File

@ -96,7 +96,9 @@ const subscriptionSchema = {
broadcast: { type: 'bool', optional: true }, broadcast: { type: 'bool', optional: true },
prid: { type: 'string', optional: true }, prid: { type: 'string', optional: true },
draftMessage: { type: 'string', optional: true }, draftMessage: { type: 'string', optional: true },
lastThreadSync: 'date?' lastThreadSync: 'date?',
autoTranslate: 'bool?',
autoTranslateLanguage: 'string?'
} }
}; };
@ -171,6 +173,16 @@ const messagesReactionsSchema = {
} }
}; };
const messagesTranslationsSchema = {
name: 'messagesTranslations',
primaryKey: '_id',
properties: {
_id: 'string',
language: 'string',
value: 'string'
}
};
const messagesEditedBySchema = { const messagesEditedBySchema = {
name: 'messagesEditedBy', name: 'messagesEditedBy',
primaryKey: '_id', primaryKey: '_id',
@ -212,7 +224,9 @@ const messagesSchema = {
replies: 'string[]', replies: 'string[]',
mentions: { type: 'list', objectType: 'users' }, mentions: { type: 'list', objectType: 'users' },
channels: { type: 'list', objectType: 'rooms' }, 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' }
} }
}; };
@ -246,6 +260,11 @@ const threadsSchema = {
tcount: { type: 'int', optional: true }, tcount: { type: 'int', optional: true },
tlm: { type: 'date', optional: true }, tlm: { type: 'date', optional: true },
replies: 'string[]', 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?' draftMessage: 'string?'
} }
}; };
@ -272,7 +291,13 @@ const threadMessagesSchema = {
starred: { type: 'bool', optional: true }, starred: { type: 'bool', optional: true },
editedBy: 'messagesEditedBy', editedBy: 'messagesEditedBy',
reactions: { type: 'list', objectType: 'messagesReactions' }, 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' }
} }
}; };
@ -374,7 +399,8 @@ const schema = [
messagesReactionsSchema, messagesReactionsSchema,
rolesSchema, rolesSchema,
uploadsSchema, uploadsSchema,
slashCommandSchema slashCommandSchema,
messagesTranslationsSchema
]; ];
const inMemorySchema = [usersTypingSchema, activeUsersSchema]; const inMemorySchema = [usersTypingSchema, activeUsersSchema];
@ -387,9 +413,9 @@ class DB {
userSchema, userSchema,
serversSchema serversSchema
], ],
schemaVersion: 8, schemaVersion: 9,
migration: (oldRealm, newRealm) => { migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 8) { if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 9) {
const newServers = newRealm.objects('servers'); const newServers = newRealm.objects('servers');
// eslint-disable-next-line no-plusplus // eslint-disable-next-line no-plusplus
@ -444,9 +470,9 @@ class DB {
return this.databases.activeDB = new Realm({ return this.databases.activeDB = new Realm({
path: `${ path }.realm`, path: `${ path }.realm`,
schema, schema,
schemaVersion: 12, schemaVersion: 13,
migration: (oldRealm, newRealm) => { migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 11) { if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 13) {
const newSubs = newRealm.objects('subscriptions'); const newSubs = newRealm.objects('subscriptions');
newRealm.delete(newSubs); newRealm.delete(newSubs);
const newMessages = newRealm.objects('messages'); const newMessages = newRealm.objects('messages');

View File

@ -881,6 +881,31 @@ const RocketChat = {
return this.sdk.get('directory', { return this.sdk.get('directory', {
query, count, offset, sort 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);
} }
}; };

View File

@ -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(() => <View style={styles.sectionSeparator} />);
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 = () => <Separator />
renderIcon = () => <CustomIcon name='check' size={20} style={sharedStyles.colorPrimary} />
renderSwitch = () => {
const { enableAutoTranslate } = this.state;
return (
<Switch
value={enableAutoTranslate}
trackColor={SWITCH_TRACK_COLOR}
onValueChange={this.toggleAutoTranslate}
/>
);
}
renderItem = ({ item }) => {
const { selectedLanguage } = this.state;
const { language, name } = item;
const isSelected = selectedLanguage === language;
return (
<ListItem
title={name || language}
onPress={() => this.saveAutoTranslateLanguage(language)}
testID={`auto-translate-view-${ language }`}
right={isSelected ? this.renderIcon : null}
/>
);
}
render() {
const { languages } = this.state;
return (
<SafeAreaView style={sharedStyles.listSafeArea} testID='auto-translate-view' forceInset={{ bottom: 'never' }}>
<StatusBar />
<ScrollView
{...scrollPersistTaps}
contentContainerStyle={styles.contentContainerStyle}
testID='auto-translate-view-list'
>
<ListItem
title={I18n.t('Enable_Auto_Translate')}
testID='auto-translate-view-switch'
right={() => this.renderSwitch()}
/>
<SectionSeparator />
<FlatList
data={languages}
extraData={this.state}
keyExtractor={item => item.language}
renderItem={this.renderItem}
ItemSeparatorComponent={this.renderSeparator}
/>
</ScrollView>
</SafeAreaView>
);
}
}
console.disableYellowBox = true;

View File

@ -16,10 +16,9 @@ import scrollPersistTaps from '../utils/scrollPersistTaps';
import I18n from '../i18n'; import I18n from '../i18n';
import UserItem from '../presentation/UserItem'; import UserItem from '../presentation/UserItem';
import { showErrorAlert } from '../utils/info'; import { showErrorAlert } from '../utils/info';
import { isAndroid } from '../utils/deviceInfo';
import { CustomHeaderButtons, Item } from '../containers/HeaderButton'; import { CustomHeaderButtons, Item } from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar'; 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({ const styles = StyleSheet.create({
container: { container: {
@ -245,8 +244,7 @@ export default class CreateChannelView extends React.Component {
value={value} value={value}
onValueChange={onValueChange} onValueChange={onValueChange}
testID={`create-channel-${ id }`} testID={`create-channel-${ id }`}
onTintColor='#2de0a5' trackColor={SWITCH_TRACK_COLOR}
tintColor={isAndroid ? '#f5455c' : null}
disabled={disabled} disabled={disabled}
/> />
</View> </View>

View File

@ -9,6 +9,7 @@ import styles from './styles';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import Check from '../../containers/Check'; import Check from '../../containers/Check';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { SWITCH_TRACK_COLOR } from '../../constants/colors';
const ANIMATION_DURATION = 200; const ANIMATION_DURATION = 200;
const ANIMATION_PROPS = { const ANIMATION_PROPS = {
@ -109,7 +110,7 @@ export default class DirectoryOptions extends PureComponent {
<Text style={styles.dropdownItemText}>{I18n.t('Search_global_users')}</Text> <Text style={styles.dropdownItemText}>{I18n.t('Search_global_users')}</Text>
<Text style={styles.dropdownItemDescription}>{I18n.t('Search_global_users_description')}</Text> <Text style={styles.dropdownItemDescription}>{I18n.t('Search_global_users_description')}</Text>
</View> </View>
<Switch value={globalUsers} onValueChange={toggleWorkspace} /> <Switch value={globalUsers} onValueChange={toggleWorkspace} trackColor={SWITCH_TRACK_COLOR} />
</View> </View>
</React.Fragment> </React.Fragment>
) )

View File

@ -46,7 +46,6 @@ const LANGUAGES = [
}), dispatch => ({ }), dispatch => ({
setUser: params => dispatch(setUserAction(params)) setUser: params => dispatch(setUserAction(params))
})) }))
/** @extends React.Component */
export default class LanguageView extends React.Component { export default class LanguageView extends React.Component {
static navigationOptions = () => ({ static navigationOptions = () => ({
title: I18n.t('Change_Language') title: I18n.t('Change_Language')

View File

@ -60,7 +60,8 @@ export default class RoomActionsView extends React.Component {
membersCount: 0, membersCount: 0,
member: {}, member: {},
joined: this.rooms.length > 0, joined: this.rooms.length > 0,
canViewMembers: false canViewMembers: false,
canAutoTranslate: false
}; };
} }
@ -89,6 +90,10 @@ export default class RoomActionsView extends React.Component {
} else if (room.t === 'd') { } else if (room.t === 'd') {
this.updateRoomMember(); this.updateRoomMember();
} }
const canAutoTranslate = RocketChat.canAutoTranslate();
this.setState({ canAutoTranslate });
safeAddListener(this.rooms, this.updateRoom); safeAddListener(this.rooms, this.updateRoom);
} }
@ -169,7 +174,7 @@ export default class RoomActionsView extends React.Component {
get sections() { get sections() {
const { const {
room, membersCount, canViewMembers, joined room, membersCount, canViewMembers, joined, canAutoTranslate
} = this.state; } = this.state;
const { const {
rid, t, blocker, notifications rid, t, blocker, notifications
@ -255,6 +260,16 @@ export default class RoomActionsView extends React.Component {
renderItem: this.renderItem renderItem: this.renderItem
}]; }];
if (canAutoTranslate) {
sections[2].data.push({
icon: 'language',
name: I18n.t('Auto_Translate'),
route: 'AutoTranslateView',
params: { rid },
testID: 'room-actions-auto-translate'
});
}
if (t === 'd') { if (t === 'd') {
sections.push({ sections.push({
data: [ data: [

View File

@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import styles from './styles'; import styles from './styles';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import { SWITCH_TRACK_COLOR } from '../../constants/colors';
export default class SwitchContainer extends React.PureComponent { export default class SwitchContainer extends React.PureComponent {
static propTypes = { static propTypes = {
@ -33,6 +34,7 @@ export default class SwitchContainer extends React.PureComponent {
onValueChange={onValueChange} onValueChange={onValueChange}
value={value} value={value}
disabled={disabled} disabled={disabled}
trackColor={SWITCH_TRACK_COLOR}
testID={testID} testID={testID}
/> />
<View style={styles.switchLabelContainer}> <View style={styles.switchLabelContainer}>

View File

@ -138,6 +138,7 @@ export default class RoomView extends React.Component {
this.t = props.navigation.getParam('t'); this.t = props.navigation.getParam('t');
this.tmid = props.navigation.getParam('tmid'); this.tmid = props.navigation.getParam('tmid');
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
const canAutoTranslate = RocketChat.canAutoTranslate();
this.state = { this.state = {
joined: this.rooms.length > 0, joined: this.rooms.length > 0,
room: this.rooms[0] || { rid: this.rid, t: this.t }, room: this.rooms[0] || { rid: this.rid, t: this.t },
@ -145,7 +146,8 @@ export default class RoomView extends React.Component {
photoModalVisible: false, photoModalVisible: false,
reactionsModalVisible: false, reactionsModalVisible: false,
selectedAttachment: {}, selectedAttachment: {},
selectedMessage: {} selectedMessage: {},
canAutoTranslate
}; };
this.beginAnimating = false; this.beginAnimating = false;
this.beginAnimatingTimeout = setTimeout(() => this.beginAnimating = true, 300); this.beginAnimatingTimeout = setTimeout(() => this.beginAnimating = true, 300);
@ -180,7 +182,7 @@ export default class RoomView extends React.Component {
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { const {
room, joined, lastOpen, photoModalVisible, reactionsModalVisible room, joined, lastOpen, photoModalVisible, reactionsModalVisible, canAutoTranslate
} = this.state; } = this.state;
const { showActions, showErrorActions, appState } = this.props; const { showActions, showErrorActions, appState } = this.props;
@ -202,6 +204,8 @@ export default class RoomView extends React.Component {
return true; return true;
} else if (joined !== nextState.joined) { } else if (joined !== nextState.joined) {
return true; return true;
} else if (canAutoTranslate !== nextState.canAutoTranslate) {
return true;
} else if (showActions !== nextProps.showActions) { } else if (showActions !== nextProps.showActions) {
return true; return true;
} else if (showErrorActions !== nextProps.showErrorActions) { } else if (showErrorActions !== nextProps.showErrorActions) {
@ -298,6 +302,11 @@ export default class RoomView extends React.Component {
this.sub = await RocketChat.subscribeRoom(room); this.sub = await RocketChat.subscribeRoom(room);
} }
} }
// We run `canAutoTranslate` again in order to refetch auto translate permission
// in case of a missing connection or poor connection on room open
const canAutoTranslate = RocketChat.canAutoTranslate();
this.setState({ canAutoTranslate });
}); });
} catch (e) { } catch (e) {
log('err_room_init', e); log('err_room_init', e);
@ -500,7 +509,7 @@ export default class RoomView extends React.Component {
} }
renderItem = (item, previousItem) => { renderItem = (item, previousItem) => {
const { room, lastOpen } = this.state; const { room, lastOpen, canAutoTranslate } = this.state;
const { const {
user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, useMarkdown, Message_Read_Receipt_Enabled user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, useMarkdown, Message_Read_Receipt_Enabled
} = this.props; } = this.props;
@ -545,6 +554,8 @@ export default class RoomView extends React.Component {
useRealName={useRealName} useRealName={useRealName}
useMarkdown={useMarkdown} useMarkdown={useMarkdown}
isReadReceiptEnabled={Message_Read_Receipt_Enabled} isReadReceiptEnabled={Message_Read_Receipt_Enabled}
autoTranslateRoom={canAutoTranslate && room.autoTranslate}
autoTranslateLanguage={room.autoTranslateLanguage}
/> />
); );

View File

@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { toggleMarkdown as toggleMarkdownAction } from '../../actions/markdown'; import { toggleMarkdown as toggleMarkdownAction } from '../../actions/markdown';
import { COLOR_DANGER, COLOR_SUCCESS } from '../../constants/colors'; import { SWITCH_TRACK_COLOR } from '../../constants/colors';
import { DrawerButton } from '../../containers/HeaderButton'; import { DrawerButton } from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import ListItem from '../../containers/ListItem'; import ListItem from '../../containers/ListItem';
@ -14,7 +14,7 @@ import { DisclosureImage } from '../../containers/DisclosureIndicator';
import Separator from '../../containers/Separator'; import Separator from '../../containers/Separator';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { MARKDOWN_KEY } from '../../lib/rocketchat'; import { MARKDOWN_KEY } from '../../lib/rocketchat';
import { getReadableVersion, getDeviceModel, isAndroid } from '../../utils/deviceInfo'; import { getReadableVersion, getDeviceModel } from '../../utils/deviceInfo';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { showErrorAlert } from '../../utils/info'; import { showErrorAlert } from '../../utils/info';
@ -23,10 +23,6 @@ import sharedStyles from '../Styles';
const LICENSE_LINK = 'https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/LICENSE'; const LICENSE_LINK = 'https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/LICENSE';
const SectionSeparator = React.memo(() => <View style={styles.sectionSeparatorBorder} />); const SectionSeparator = React.memo(() => <View style={styles.sectionSeparatorBorder} />);
const SWITCH_TRACK_COLOR = {
false: isAndroid ? COLOR_DANGER : null,
true: COLOR_SUCCESS
};
@connect(state => ({ @connect(state => ({
server: state.server, server: state.server,