Merge beta into master (#1759)

This commit is contained in:
Diego Mello 2020-02-19 16:43:47 -03:00 committed by GitHub
parent 1cd5fa8625
commit 69aff7e56a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
855 changed files with 39759 additions and 20611 deletions

View File

@ -148,8 +148,6 @@ jobs:
- run:
name: Configure Gradle
command: |
cd android
echo -e "" > ./gradle.properties
# echo -e "android.enableAapt2=false" >> ./gradle.properties
echo -e "android.useAndroidX=true" >> ./gradle.properties
@ -165,6 +163,7 @@ jobs:
echo -e "VERSIONCODE=$CIRCLE_BUILD_NUM" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties
working_directory: android
- run:
name: Set Google Services
@ -172,20 +171,6 @@ jobs:
cp google-services.prod.json google-services.json
working_directory: android/app
- run:
name: Upload sourcemaps to Bugsnag
command: |
if [[ $BUGSNAG_KEY ]]; then
yarn generate-source-maps-android
curl https://upload.bugsnag.com/react-native-source-map \
-F apiKey=$BUGSNAG_KEY \
-F appVersionCode=$CIRCLE_BUILD_NUM \
-F dev=false \
-F platform=android \
-F sourceMap=@android-release.bundle.map \
-F bundle=@android-release.bundle
fi
- run:
name: Config variables
command: |
@ -194,8 +179,6 @@ jobs:
- run:
name: Build Android App
command: |
npx jetify
cd android
if [[ $KEYSTORE ]]; then
# TODO: enable app bundle again
./gradlew assembleRelease
@ -206,6 +189,20 @@ jobs:
mkdir -p /tmp/build
mv app/build/outputs /tmp/build/
working_directory: android
- run:
name: Upload sourcemaps to Bugsnag
command: |
if [[ $BUGSNAG_KEY ]]; then
yarn generate-source-maps-android upload \
--api-key=$BUGSNAG_KEY \
--app-version=$CIRCLE_BUILD_NUM \
--minifiedFile=android/app/build/generated/assets/react/release/app.bundle \
--source-map=android/app/build/generated/sourcemaps/react/release/app.bundle.map \
--minified-url=app.bundle \
--upload-sources
fi
- store_artifacts:
path: /tmp/build/outputs

View File

@ -103,7 +103,7 @@ Readme will guide you on how to config.
| Custom Fields on Signup | ✅ |
| Report message | ✅ |
| Theming | ✅ |
| Settings -> Review the App | |
| Settings -> Review the App | |
| Settings -> Default Browser | ❌ |
| Admin panel | ✅ |
| Reply message from notification | ✅ |

View File

@ -0,0 +1,11 @@
export default {
window: () => null
};
export const uiKitMessage = () => () => null;
export const uiKitModal = () => () => null;
export class UiKitParserMessage {}
export class UiKitParserModal {}

View File

@ -0,0 +1,8 @@
export class Client { }
export default {
bugsnag: () => '',
leaveBreadcrumb: () => '',
notify: () => '',
loggerConfig: () => ''
};

3
__mocks__/react-native-firebase.js vendored Normal file
View File

@ -0,0 +1,3 @@
export default {
analytics: null
};

File diff suppressed because it is too large Load Diff

View File

@ -138,7 +138,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "4.3.0"
versionName "4.4.0"
vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
}

View File

@ -10,8 +10,10 @@ import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.app.Person;
import com.google.gson.*;
import com.bumptech.glide.Glide;
@ -30,6 +32,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.Date;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
@ -41,7 +44,7 @@ public class CustomPushNotification extends PushNotification {
reactApplicationContext = new ReactApplicationContext(context);
}
private static Map<String, List<String>> notificationMessages = new HashMap<String, List<String>>();
private static Map<String, List<Bundle>> notificationMessages = new HashMap<String, List<Bundle>>();
public static String KEY_REPLY = "KEY_REPLY";
public static String NOTIFICATION_ID = "NOTIFICATION_ID";
@ -53,15 +56,26 @@ public class CustomPushNotification extends PushNotification {
public void onReceived() throws InvalidNotificationException {
final Bundle bundle = mNotificationProps.asBundle();
String notId = bundle.getString("notId");
String message = bundle.getString("message");
String notId = bundle.getString("notId", "1");
String title = bundle.getString("title");
if (notificationMessages.get(notId) == null) {
notificationMessages.put(notId, new ArrayList<String>());
notificationMessages.put(notId, new ArrayList<Bundle>());
}
notificationMessages.get(notId).add(message);
super.postNotification(notId != null ? Integer.parseInt(notId) : 1);
Gson gson = new Gson();
Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class);
boolean hasSender = ejson.sender != null;
bundle.putLong("time", new Date().getTime());
bundle.putString("username", hasSender ? ejson.sender.username : title);
bundle.putString("senderId", hasSender ? ejson.sender._id : "1");
bundle.putString("avatarUri", ejson.getAvatarUri());
notificationMessages.get(notId).add(bundle);
super.postNotification(Integer.parseInt(notId));
notifyReceivedToJS();
}
@ -69,7 +83,7 @@ public class CustomPushNotification extends PushNotification {
@Override
public void onOpened() {
Bundle bundle = mNotificationProps.asBundle();
final String notId = bundle.getString("notId");
final String notId = bundle.getString("notId", "1");
notificationMessages.remove(notId);
digestNotification();
}
@ -79,19 +93,20 @@ public class CustomPushNotification extends PushNotification {
final Notification.Builder notification = new Notification.Builder(mContext);
Bundle bundle = mNotificationProps.asBundle();
String notId = bundle.getString("notId", "1");
String title = bundle.getString("title");
String message = bundle.getString("message");
String notId = bundle.getString("notId");
notification
.setContentIntent(intent)
.setContentTitle(title)
.setContentText(message)
.setContentIntent(intent)
.setPriority(Notification.PRIORITY_HIGH)
.setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true);
Integer notificationId = notId != null ? Integer.parseInt(notId) : 1;
Integer notificationId = Integer.parseInt(notId);
notificationColor(notification);
notificationChannel(notification);
notificationIcons(notification, bundle);
notificationStyle(notification, notificationId, bundle);
@ -114,10 +129,18 @@ public class CustomPushNotification extends PushNotification {
.submit(100, 100)
.get();
} catch (final ExecutionException | InterruptedException e) {
return null;
return largeIcon();
}
}
private Bitmap largeIcon() {
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
int largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
return largeIconBitmap;
}
private void notificationIcons(Notification.Builder notification, Bundle bundle) {
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
@ -127,9 +150,11 @@ public class CustomPushNotification extends PushNotification {
Gson gson = new Gson();
Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class);
notification
.setSmallIcon(smallIconResId)
.setLargeIcon(getAvatar(ejson.getAvatarUri()));
notification.setSmallIcon(smallIconResId);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
notification.setLargeIcon(getAvatar(ejson.getAvatarUri()));
}
}
private void notificationChannel(Notification.Builder notification) {
@ -148,23 +173,82 @@ public class CustomPushNotification extends PushNotification {
}
}
private void notificationStyle(Notification.Builder notification, int notId, Bundle bundle) {
Notification.InboxStyle messageStyle = new Notification.InboxStyle();
List<String> messages = notificationMessages.get(Integer.toString(notId));
if (messages != null) {
for (int i = 0; i < messages.size(); i++) {
messageStyle.addLine(messages.get(i));
}
String summary = bundle.getString("summaryText");
messageStyle.setSummaryText(summary.replace("%n%", Integer.toString(messages.size())));
notification.setNumber(messages.size());
private String extractMessage(String message, Ejson ejson) {
if (ejson.type != null && !ejson.type.equals("d")) {
int pos = message.indexOf(":");
int start = pos == -1 ? 0 : pos + 2;
return message.substring(start, message.length());
}
return message;
}
private void notificationColor(Notification.Builder notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
notification.setColor(mContext.getColor(R.color.notification_text));
}
}
notification.setStyle(messageStyle);
private void notificationStyle(Notification.Builder notification, int notId, Bundle bundle) {
List<Bundle> bundles = notificationMessages.get(Integer.toString(notId));
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Notification.InboxStyle messageStyle = new Notification.InboxStyle();
if (bundles != null) {
for (int i = 0; i < bundles.size(); i++) {
Bundle data = bundles.get(i);
String message = data.getString("message");
messageStyle.addLine(message);
}
}
notification.setStyle(messageStyle);
} else {
Notification.MessagingStyle messageStyle;
Gson gson = new Gson();
Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
messageStyle = new Notification.MessagingStyle("");
} else {
Person sender = new Person.Builder()
.setKey("")
.setName("")
.build();
messageStyle = new Notification.MessagingStyle(sender);
}
String title = bundle.getString("title");
messageStyle.setConversationTitle(title);
if (bundles != null) {
for (int i = 0; i < bundles.size(); i++) {
Bundle data = bundles.get(i);
long timestamp = data.getLong("time");
String message = data.getString("message");
String username = data.getString("username");
String senderId = data.getString("senderId");
String avatarUri = data.getString("avatarUri");
String m = extractMessage(message, ejson);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
messageStyle.addMessage(m, timestamp, username);
} else {
Person sender = new Person.Builder()
.setKey(senderId)
.setName(username)
.setIcon(Icon.createWithBitmap(getAvatar(avatarUri)))
.build();
messageStyle.addMessage(m, timestamp, sender);
}
}
}
notification.setStyle(messageStyle);
}
}
private void notificationReply(Notification.Builder notification, int notificationId, Bundle bundle) {

View File

@ -14,7 +14,7 @@ public class Ejson {
private SharedPreferences sharedPreferences = RNUserDefaultsModule.getPreferences(CustomPushNotification.reactApplicationContext);
public String getAvatarUri() {
if (type == null || !type.equals("d")) {
if (type == null) {
return null;
}
return serverURL() + "/avatar/" + this.sender.username + "?rc_token=" + token() + "&rc_uid=" + userId();
@ -36,7 +36,8 @@ public class Ejson {
return url;
}
private class Sender {
public class Sender {
String username;
String _id;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -22,6 +22,7 @@ export const SHARE = createRequestTypes('SHARE', [
export const USER = createRequestTypes('USER', ['SET']);
export const ROOMS = createRequestTypes('ROOMS', [
...defaultTypes,
'REFRESH',
'SET_SEARCH',
'CLOSE_SERVER_DROPDOWN',
'TOGGLE_SERVER_DROPDOWN',

View File

@ -22,9 +22,10 @@ export function loginFailure(err) {
};
}
export function logout() {
export function logout(forcedByServer = false) {
return {
type: types.LOGOUT
type: types.LOGOUT,
forcedByServer
};
}

View File

@ -4,6 +4,8 @@ export function notificationReceived(params) {
return {
type: NOTIFICATION.RECEIVED,
payload: {
title: params.title,
avatar: params.avatar,
message: params.text,
payload: params.payload
}

View File

@ -1,9 +1,10 @@
import * as types from './actionsTypes';
export function roomsRequest() {
export function roomsRequest(params = { allData: false }) {
return {
type: types.ROOMS.REQUEST
type: types.ROOMS.REQUEST,
params
};
}
@ -20,6 +21,12 @@ export function roomsFailure(err) {
};
}
export function roomsRefresh() {
return {
type: types.ROOMS.REFRESH
};
}
export function setSearch(searchText) {
return {
type: types.ROOMS.SET_SEARCH,

View File

@ -1,3 +1,8 @@
export const PLAY_MARKET_LINK = 'https://play.google.com/store/apps/details?id=chat.rocket.reactnative';
export const APP_STORE_LINK = 'https://itunes.apple.com/app/rocket-chat-experimental/id1272915472?ls=1&mt=8';
import { getBundleId, isIOS } from '../utils/deviceInfo';
const APP_STORE_ID = '1272915472';
export const PLAY_MARKET_LINK = `https://play.google.com/store/apps/details?id=${ getBundleId }`;
export const APP_STORE_LINK = `https://itunes.apple.com/app/id${ APP_STORE_ID }`;
export const LICENSE_LINK = 'https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/LICENSE';
export const STORE_REVIEW_LINK = isIOS ? `itms-apps://itunes.apple.com/app/id${ APP_STORE_ID }?action=write-review` : `market://details?id=${ getBundleId }`;

View File

@ -5,6 +5,9 @@ export default {
Accounts_EmailOrUsernamePlaceholder: {
type: 'valueAsString'
},
Accounts_EmailVerification: {
type: 'valueAsBoolean'
},
Accounts_NamePlaceholder: {
type: 'valueAsString'
},
@ -32,6 +35,9 @@ export default {
Jitsi_Domain: {
type: 'valueAsString'
},
Jitsi_Enabled_TokenAuth: {
type: 'valueAsBoolean'
},
Jitsi_URL_Room_Prefix: {
type: 'valueAsString'
},

View File

@ -1,13 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, Text } from 'react-native';
import { RectButton } from 'react-native-gesture-handler';
import Touchable from 'react-native-platform-touchable';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import ActivityIndicator from '../ActivityIndicator';
/* eslint-disable react-native/no-unused-styles */
const styles = StyleSheet.create({
container: {
paddingHorizontal: 15,
@ -48,9 +47,9 @@ export default class Button extends React.PureComponent {
} = this.props;
const isPrimary = type === 'primary';
return (
<RectButton
<Touchable
onPress={onPress}
enabled={!(disabled || loading)}
disabled={disabled || loading}
style={[
styles.container,
backgroundColor
@ -76,7 +75,7 @@ export default class Button extends React.PureComponent {
</Text>
)
}
</RectButton>
</Touchable>
);
}
}

View File

@ -14,6 +14,7 @@ import Navigation from '../lib/Navigation';
import { getMessageTranslation } from './message/utils';
import { LISTENER } from './Toast';
import EventEmitter from '../utils/events';
import { showConfirmationAlert } from '../utils/info';
class MessageActions extends React.Component {
static propTypes = {
@ -223,29 +224,18 @@ class MessageActions extends React.Component {
}
handleDelete = () => {
const { message } = this.props;
Alert.alert(
I18n.t('Are_you_sure_question_mark'),
I18n.t('You_will_not_be_able_to_recover_this_message'),
[
{
text: I18n.t('Cancel'),
style: 'cancel'
},
{
text: I18n.t('Yes_action_it', { action: 'delete' }),
style: 'destructive',
onPress: async() => {
try {
await RocketChat.deleteMessage(message.id, message.subscription.id);
} catch (e) {
log(e);
}
}
showConfirmationAlert({
message: I18n.t('You_will_not_be_able_to_recover_this_message'),
callToAction: I18n.t('Delete'),
onPress: async() => {
const { message } = this.props;
try {
await RocketChat.deleteMessage(message.id, message.subscription.id);
} catch (e) {
log(e);
}
],
{ cancelable: false }
);
}
});
}
handleEdit = () => {
@ -262,6 +252,9 @@ class MessageActions extends React.Component {
handleShare = async() => {
const { message } = this.props;
const permalink = await this.getPermalink(message);
if (!permalink) {
return;
}
Share.share({
message: permalink
});

View File

@ -17,7 +17,7 @@ export default class EmojiKeyboard extends React.PureComponent {
constructor(props) {
super(props);
const state = store.getState();
this.baseUrl = state.settings.Site_Url || state.server ? state.server.server : '';
this.baseUrl = state.server.server;
}
onEmojiSelected = (emoji) => {

View File

@ -92,7 +92,7 @@ ReplyPreview.propTypes = {
const mapStateToProps = state => ({
useMarkdown: state.markdown.useMarkdown,
Message_TimeFormat: state.settings.Message_TimeFormat,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
baseUrl: state.server.server
});
export default connect(mapStateToProps)(ReplyPreview);

View File

@ -9,6 +9,7 @@ import DocumentPicker from 'react-native-document-picker';
import ActionSheet from 'react-native-action-sheet';
import { Q } from '@nozbe/watermelondb';
import { generateTriggerId } from '../../lib/methods/actions';
import TextInput from '../../presentation/TextInput';
import { userTyping as userTypingAction } from '../../actions/room';
import RocketChat from '../../lib/rocketchat';
@ -42,6 +43,8 @@ import {
MENTIONS_TRACKING_TYPE_USERS
} from './constants';
import CommandsPreview from './CommandsPreview';
import { Review } from '../../utils/review';
import { getUserSelector } from '../../selectors/login';
const imagePickerConfig = {
cropping: true,
@ -103,7 +106,8 @@ class MessageBox extends Component {
isVisible: false
},
commandPreview: [],
showCommandPreview: false
showCommandPreview: false,
command: {}
};
this.text = '';
this.focused = false;
@ -279,7 +283,7 @@ class MessageBox extends Component {
try {
const command = await commandsCollection.find(name);
if (command.providesPreview) {
return this.setCommandPreview(name, params);
return this.setCommandPreview(command, name, params);
}
} catch (e) {
console.log('Slash command not found');
@ -338,16 +342,22 @@ class MessageBox extends Component {
}
onPressCommandPreview = (item) => {
const { rid } = this.props;
const { command } = this.state;
const {
rid, tmid, message: { id: messageTmid }, replyCancel
} = this.props;
const { text } = this;
const command = text.substr(0, text.indexOf(' ')).slice(1);
const name = text.substr(0, text.indexOf(' ')).slice(1);
const params = text.substr(text.indexOf(' ') + 1) || 'params';
this.setState({ commandPreview: [], showCommandPreview: false });
this.setState({ commandPreview: [], showCommandPreview: false, command: {} });
this.stopTrackingMention();
this.clearInput();
this.handleTyping(false);
try {
RocketChat.executeCommandPreview(command, params, rid, item);
const { appId } = command;
const triggerId = generateTriggerId(appId);
RocketChat.executeCommandPreview(name, params, rid, item, triggerId, tmid || messageTmid);
replyCancel();
} catch (e) {
log(e);
}
@ -451,13 +461,13 @@ class MessageBox extends Component {
}, 1000);
}
setCommandPreview = async(command, params) => {
setCommandPreview = async(command, name, params) => {
const { rid } = this.props;
try {
const { preview } = await RocketChat.getCommandPreview(command, rid, params);
this.setState({ commandPreview: preview.items, showCommandPreview: true });
const { preview } = await RocketChat.getCommandPreview(name, rid, params);
this.setState({ commandPreview: preview.items, showCommandPreview: true, command });
} catch (e) {
this.setState({ commandPreview: [], showCommandPreview: true });
this.setState({ commandPreview: [], showCommandPreview: true, command: {} });
log(e);
}
}
@ -493,7 +503,7 @@ class MessageBox extends Component {
sendMediaMessage = async(file) => {
const {
rid, tmid, baseUrl: server, user
rid, tmid, baseUrl: server, user, message: { id: messageTmid }, replyCancel
} = this.props;
this.setState({ file: { isVisible: false } });
const fileInfo = {
@ -505,7 +515,9 @@ class MessageBox extends Component {
path: file.path
};
try {
await RocketChat.sendFileMessage(rid, fileInfo, tmid, server, user);
replyCancel();
await RocketChat.sendFileMessage(rid, fileInfo, tmid || messageTmid, server, user);
Review.pushPositiveEvent();
} catch (e) {
log(e);
}
@ -518,7 +530,7 @@ class MessageBox extends Component {
this.showUploadModal(image);
}
} catch (e) {
log(e);
// Do nothing
}
}
@ -529,7 +541,7 @@ class MessageBox extends Component {
this.showUploadModal(video);
}
} catch (e) {
log(e);
// Do nothing
}
}
@ -540,7 +552,7 @@ class MessageBox extends Component {
this.showUploadModal(image);
}
} catch (e) {
log(e);
// Do nothing
}
}
@ -639,7 +651,7 @@ class MessageBox extends Component {
submit = async() => {
const {
onSubmit, rid: roomId
onSubmit, rid: roomId, tmid
} = this.props;
const message = this.text;
@ -653,7 +665,7 @@ class MessageBox extends Component {
}
const {
editing, replying
editing, replying, message: { id: messageTmid }, replyCancel
} = this.props;
// Slash command
@ -667,7 +679,10 @@ class MessageBox extends Component {
if (slashCommand.length > 0) {
try {
const messageWithoutCommand = message.replace(/([^\s]+)/, '').trim();
RocketChat.runSlashCommand(command, roomId, messageWithoutCommand);
const [{ appId }] = slashCommand;
const triggerId = generateTriggerId(appId);
RocketChat.runSlashCommand(command, roomId, messageWithoutCommand, triggerId, tmid || messageTmid);
replyCancel();
} catch (e) {
log(e);
}
@ -684,7 +699,7 @@ class MessageBox extends Component {
// Reply
} else if (replying) {
const {
message: replyingMessage, replyCancel, threadsEnabled, replyWithMention
message: replyingMessage, threadsEnabled, replyWithMention
} = this.props;
// Thread
@ -872,13 +887,9 @@ class MessageBox extends Component {
}
const mapStateToProps = state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
baseUrl: state.server.server,
threadsEnabled: state.settings.Threads_enabled,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
},
user: getUserSelector(state),
FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList,
FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize
});

View File

@ -32,6 +32,8 @@ const RoomTypeIcon = React.memo(({
return <Image source={{ uri: 'hashtag' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
} if (type === 'd') {
return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />;
} if (type === 'l') {
return <CustomIcon name='livechat' size={13} style={[styles.style, styles.discussion, { color }]} />;
}
return <Image source={{ uri: 'lock' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
});

View File

@ -37,7 +37,7 @@ const styles = StyleSheet.create({
...sharedStyles.textRegular
},
cancel: {
marginRight: 10
marginRight: 15
},
cancelText: {
...sharedStyles.textRegular,

View File

@ -7,6 +7,7 @@ import sharedStyles from '../views/Styles';
import TextInput from '../presentation/TextInput';
import { themes } from '../constants/colors';
import { CustomIcon } from '../lib/Icons';
import ActivityIndicator from './ActivityIndicator';
const styles = StyleSheet.create({
error: {
@ -56,6 +57,7 @@ export default class RCTextInput extends React.PureComponent {
static propTypes = {
label: PropTypes.string,
error: PropTypes.object,
loading: PropTypes.bool,
secureTextEntry: PropTypes.bool,
containerStyle: PropTypes.any,
inputStyle: PropTypes.object,
@ -102,6 +104,11 @@ export default class RCTextInput extends React.PureComponent {
);
}
get loading() {
const { theme } = this.props;
return <ActivityIndicator style={[styles.iconContainer, styles.iconRight, { color: themes[theme].bodyText }]} />;
}
tooglePassword = () => {
this.setState(prevState => ({ showPassword: !prevState.showPassword }));
}
@ -109,7 +116,7 @@ export default class RCTextInput extends React.PureComponent {
render() {
const { showPassword } = this.state;
const {
label, error, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps
label, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps
} = this.props;
const { dangerColor } = themes[theme];
return (
@ -131,10 +138,6 @@ export default class RCTextInput extends React.PureComponent {
<TextInput
style={[
styles.input,
error.error && {
color: dangerColor,
borderColor: dangerColor
},
iconLeft && styles.inputIconLeft,
secureTextEntry && styles.inputIconRight,
{
@ -142,6 +145,10 @@ export default class RCTextInput extends React.PureComponent {
borderColor: themes[theme].separatorColor,
color: themes[theme].titleText
},
error.error && {
color: dangerColor,
borderColor: dangerColor
},
inputStyle
]}
ref={inputRef}
@ -158,8 +165,9 @@ export default class RCTextInput extends React.PureComponent {
/>
{iconLeft ? this.iconLeft : null}
{secureTextEntry ? this.iconPassword : null}
{loading ? this.loading : null}
</View>
{error.error ? <Text style={[styles.error, { color: dangerColor }]}>{error.reason}</Text> : null}
{error && error.reason ? <Text style={[styles.error, { color: dangerColor }]}>{error.reason}</Text> : null}
</View>
);
}

View File

@ -43,15 +43,19 @@ class Toast extends React.Component {
EventEmitter.removeListener(LISTENER);
}
getToastRef = toast => this.toast = toast;
showToast = ({ message }) => {
this.toast.show(message, 1000);
if (this.toast && this.toast.show) {
this.toast.show(message, 1000);
}
}
render() {
const { theme } = this.props;
return (
<EasyToast
ref={toast => this.toast = toast}
ref={this.getToastRef}
position='center'
style={[styles.toast, { backgroundColor: themes[theme].toastBackground }]}
textStyle={[styles.text, { color: themes[theme].buttonText }]}

View File

@ -0,0 +1,31 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import Button from '../Button';
import I18n from '../../i18n';
export const Actions = ({
blockId, appId, elements, parser, theme
}) => {
const [showMoreVisible, setShowMoreVisible] = useState(() => elements.length > 5);
const renderedElements = showMoreVisible ? elements.slice(0, 5) : elements;
const Elements = () => renderedElements
.map(element => parser.renderActions({ blockId, appId, ...element }, BLOCK_CONTEXT.ACTION, parser));
return (
<>
<Elements />
{showMoreVisible && (<Button theme={theme} title={I18n.t('Show_more')} onPress={() => setShowMoreVisible(false)} />)}
</>
);
};
Actions.propTypes = {
blockId: PropTypes.string,
appId: PropTypes.string,
elements: PropTypes.array,
parser: PropTypes.object,
theme: PropTypes.string
};

View File

@ -0,0 +1,22 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
const styles = StyleSheet.create({
container: {
minHeight: 36,
alignItems: 'center',
flexDirection: 'row'
}
});
export const Context = ({ elements, parser }) => (
<View style={styles.container}>
{elements.map(element => parser.renderContext(element, BLOCK_CONTEXT.CONTEXT, parser))}
</View>
);
Context.propTypes = {
elements: PropTypes.array,
parser: PropTypes.object
};

View File

@ -0,0 +1,117 @@
import React, { useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import PropTypes from 'prop-types';
import DateTimePicker from '@react-native-community/datetimepicker';
import Touchable from 'react-native-platform-touchable';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import moment from 'moment';
import Button from '../Button';
import { textParser } from './utils';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import { CustomIcon } from '../../lib/Icons';
import { isAndroid } from '../../utils/deviceInfo';
import ActivityIndicator from '../ActivityIndicator';
const styles = StyleSheet.create({
input: {
height: 48,
paddingLeft: 16,
borderWidth: StyleSheet.hairlineWidth,
borderRadius: 2,
alignItems: 'center',
flexDirection: 'row'
},
inputText: {
...sharedStyles.textRegular,
fontSize: 14
},
icon: {
right: 16,
position: 'absolute'
},
loading: {
padding: 0
}
});
export const DatePicker = ({
element, language, action, context, theme, loading, value, error
}) => {
const [show, onShow] = useState(false);
const { initial_date, placeholder } = element;
const [currentDate, onChangeDate] = useState(new Date(initial_date || value));
const onChange = ({ nativeEvent: { timestamp } }, date) => {
const newDate = date || new Date(timestamp);
onChangeDate(newDate);
action({ value: moment(newDate).format('YYYY-MM-DD') });
if (isAndroid) {
onShow(false);
}
};
let button = (
<Button
title={textParser([placeholder])}
onPress={() => onShow(!show)}
loading={loading}
theme={theme}
/>
);
if (context === BLOCK_CONTEXT.FORM) {
button = (
<Touchable
onPress={() => onShow(!show)}
style={{ backgroundColor: themes[theme].backgroundColor }}
background={Touchable.Ripple(themes[theme].bannerBackground)}
>
<View style={[styles.input, { borderColor: error ? themes[theme].dangerColor : themes[theme].separatorColor }]}>
<Text
style={[
styles.inputText,
{ color: error ? themes[theme].dangerColor : themes[theme].titleText }
]}
>
{currentDate.toLocaleDateString(language)}
</Text>
{
loading
? <ActivityIndicator style={[styles.loading, styles.icon]} />
: <CustomIcon name='calendar' size={20} color={error ? themes[theme].dangerColor : themes[theme].auxiliaryText} style={styles.icon} />
}
</View>
</Touchable>
);
}
const content = show ? (
<DateTimePicker
mode='date'
display='default'
value={currentDate}
onChange={onChange}
textColor={themes[theme].titleText}
/>
) : null;
return (
<>
{button}
{content}
</>
);
};
DatePicker.propTypes = {
element: PropTypes.object,
language: PropTypes.string,
action: PropTypes.func,
context: PropTypes.number,
loading: PropTypes.bool,
theme: PropTypes.string,
value: PropTypes.string,
error: PropTypes.string
};

View File

@ -0,0 +1,18 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import Separator from '../Separator';
const styles = StyleSheet.create({
separator: {
width: '100%',
alignSelf: 'center',
marginBottom: 16
}
});
export const Divider = ({ theme }) => <Separator style={styles.separator} theme={theme} />;
Divider.propTypes = {
theme: PropTypes.string
};

View File

@ -0,0 +1,61 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import FastImage from 'react-native-fast-image';
import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import ImageContainer from '../message/Image';
import Navigation from '../../lib/Navigation';
const styles = StyleSheet.create({
image: {
borderRadius: 2
},
mediaContext: {
marginRight: 8
}
});
const ThumbContext = args => <View style={styles.mediaContext}><Thumb size={20} {...args} /></View>;
export const Thumb = ({ element, size = 88 }) => (
<FastImage
style={[{ width: size, height: size }, styles.image]}
source={{ uri: element.imageUrl }}
/>
);
Thumb.propTypes = {
element: PropTypes.object,
size: PropTypes.number
};
export const Media = ({ element, theme }) => {
const showAttachment = attachment => Navigation.navigate('AttachmentView', { attachment });
const { imageUrl } = element;
return (
<ImageContainer
file={{ image_url: imageUrl }}
imageUrl={imageUrl}
showAttachment={showAttachment}
theme={theme}
/>
);
};
Media.propTypes = {
element: PropTypes.object,
theme: PropTypes.string
};
const genericImage = (element, context, theme) => {
switch (context) {
case BLOCK_CONTEXT.SECTION:
return <Thumb element={element} />;
case BLOCK_CONTEXT.CONTEXT:
return <ThumbContext element={element} />;
default:
return <Media element={element} theme={theme} />;
}
};
export const Image = ({ element, context, theme }) => genericImage(element, context, theme);

View File

@ -0,0 +1,55 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
const styles = StyleSheet.create({
container: {
marginBottom: 16
},
label: {
fontSize: 14,
marginVertical: 10,
...sharedStyles.textSemibold
},
description: {
marginBottom: 10,
fontSize: 15,
...sharedStyles.textRegular
},
error: {
marginTop: 8,
fontSize: 14,
...sharedStyles.textRegular,
...sharedStyles.textAlignCenter
},
hint: {
fontSize: 14,
...sharedStyles.textRegular
}
});
export const Input = ({
element, parser, label, description, error, hint, theme
}) => (
<View style={styles.container}>
{label ? <Text style={[styles.label, { color: error ? themes[theme].dangerColor : themes[theme].titleText }]}>{label}</Text> : null}
{description ? <Text style={[styles.description, { color: themes[theme].auxiliaryText }]}>{description}</Text> : null}
{parser.renderInputs({ ...element }, BLOCK_CONTEXT.FORM, parser)}
{error ? <Text style={[styles.error, { color: themes[theme].dangerColor }]}>{error}</Text> : null}
{hint ? <Text style={[styles.hint, { color: themes[theme].auxiliaryText }]}>{hint}</Text> : null}
</View>
);
Input.propTypes = {
element: PropTypes.object,
parser: PropTypes.object,
label: PropTypes.string,
description: PropTypes.string,
error: PropTypes.string,
hint: PropTypes.string,
theme: PropTypes.string
};

View File

@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { UiKitMessage, UiKitModal } from './index';
import { KitContext } from './utils';
export const messageBlockWithContext = context => props => (
<KitContext.Provider value={context}>
<MessageBlock {...props} />
</KitContext.Provider>
);
const MessageBlock = ({ blocks }) => UiKitMessage(blocks);
MessageBlock.propTypes = {
blocks: PropTypes.any
};
export const modalBlockWithContext = context => data => (
<KitContext.Provider value={{ ...context, ...data }}>
<ModalBlock {...data} />
</KitContext.Provider>
);
const ModalBlock = ({ blocks }) => UiKitModal(blocks);
ModalBlock.propTypes = {
blocks: PropTypes.any
};

View File

@ -0,0 +1,45 @@
import React from 'react';
import { Text, View, Image } from 'react-native';
import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable';
import { themes } from '../../../constants/colors';
import { textParser } from '../utils';
import { CustomIcon } from '../../../lib/Icons';
import styles from './styles';
const keyExtractor = item => item.value.toString();
const Chip = ({ item, onSelect, theme }) => (
<Touchable
key={item.value}
onPress={() => onSelect(item)}
style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }]}
background={Touchable.Ripple(themes[theme].bannerBackground)}
>
<>
{item.imageUrl ? <Image style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
<Text style={[styles.chipText, { color: themes[theme].titleText }]}>{textParser([item.text])}</Text>
<CustomIcon name='cross' size={16} color={themes[theme].auxiliaryText} />
</>
</Touchable>
);
Chip.propTypes = {
item: PropTypes.object,
onSelect: PropTypes.func,
theme: PropTypes.string
};
const Chips = ({ items, onSelect, theme }) => (
<View style={styles.chips}>
{items.map(item => <Chip key={keyExtractor(item)} item={item} onSelect={onSelect} theme={theme} />)}
</View>
);
Chips.propTypes = {
items: PropTypes.array,
onSelect: PropTypes.func,
theme: PropTypes.string
};
export default Chips;

View File

@ -0,0 +1,36 @@
import React from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable';
import { CustomIcon } from '../../../lib/Icons';
import { themes } from '../../../constants/colors';
import ActivityIndicator from '../../ActivityIndicator';
import styles from './styles';
const Input = ({
children, open, theme, loading
}) => (
<Touchable
onPress={() => open(true)}
style={{ backgroundColor: themes[theme].backgroundColor }}
background={Touchable.Ripple(themes[theme].bannerBackground)}
>
<View style={[styles.input, { borderColor: themes[theme].separatorColor }]}>
{children}
{
loading
? <ActivityIndicator style={[styles.loading, styles.icon]} />
: <CustomIcon name='arrow-down' size={22} color={themes[theme].auxiliaryText} style={styles.icon} />
}
</View>
</Touchable>
);
Input.propTypes = {
children: PropTypes.node,
open: PropTypes.func,
theme: PropTypes.string,
loading: PropTypes.bool
};
export default Input;

View File

@ -0,0 +1,61 @@
import React from 'react';
import { Text, FlatList } from 'react-native';
import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable';
import Separator from '../../Separator';
import Check from '../../Check';
import { textParser } from '../utils';
import { themes } from '../../../constants/colors';
import styles from './styles';
const keyExtractor = item => item.value.toString();
// RectButton doesn't work on modal (Android)
const Item = ({
item, selected, onSelect, theme
}) => (
<Touchable
key={item}
onPress={() => onSelect(item)}
style={[
styles.item,
{ backgroundColor: themes[theme].backgroundColor }
]}
>
<>
<Text style={{ color: themes[theme].titleText }}>{textParser([item.text])}</Text>
{selected ? <Check theme={theme} /> : null}
</>
</Touchable>
);
Item.propTypes = {
item: PropTypes.object,
selected: PropTypes.number,
onSelect: PropTypes.func,
theme: PropTypes.string
};
const Items = ({
items, selected, onSelect, theme
}) => (
<FlatList
data={items}
style={[styles.items, { backgroundColor: themes[theme].backgroundColor }]}
contentContainerStyle={[styles.itemContent, { backgroundColor: themes[theme].backgroundColor }]}
keyboardShouldPersistTaps='always'
ItemSeparatorComponent={() => <Separator theme={theme} />}
keyExtractor={keyExtractor}
renderItem={({ item }) => <Item item={item} onSelect={onSelect} theme={theme} selected={selected.find(s => s === item.value)} />}
/>
);
Items.propTypes = {
items: PropTypes.array,
selected: PropTypes.array,
onSelect: PropTypes.func,
theme: PropTypes.string
};
export default Items;

View File

@ -0,0 +1,171 @@
import React, { useState, useEffect } from 'react';
import {
View, Text, TouchableWithoutFeedback, Modal, KeyboardAvoidingView, Animated, Easing
} from 'react-native';
import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import Button from '../../Button';
import TextInput from '../../TextInput';
import { textParser } from '../utils';
import { themes } from '../../../constants/colors';
import Chips from './Chips';
import Items from './Items';
import Input from './Input';
import styles from './styles';
const ANIMATION_DURATION = 200;
const ANIMATION_PROPS = {
duration: ANIMATION_DURATION,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true
};
const animatedValue = new Animated.Value(0);
export const MultiSelect = React.memo(({
options = [],
onChange,
placeholder = { text: 'Search' },
context,
loading,
value: values,
multiselect = false,
theme
}) => {
const [selected, select] = useState(values || []);
const [open, setOpen] = useState(false);
const [search, onSearchChange] = useState('');
const [currentValue, setCurrentValue] = useState('');
const [showContent, setShowContent] = useState(false);
useEffect(() => {
setOpen(showContent);
}, [showContent]);
const onShow = () => {
Animated.timing(
animatedValue,
{
toValue: 1,
...ANIMATION_PROPS
}
).start();
setShowContent(true);
};
const onHide = () => {
Animated.timing(
animatedValue,
{
toValue: 0,
...ANIMATION_PROPS
}
).start(() => setShowContent(false));
};
const onSelect = (item) => {
const { value } = item;
if (multiselect) {
let newSelect = [];
if (!selected.includes(value)) {
newSelect = [...selected, value];
} else {
newSelect = selected.filter(s => s !== value);
}
select(newSelect);
onChange({ value: newSelect });
} else {
onChange({ value });
setCurrentValue(value);
setOpen(false);
}
};
const renderContent = () => {
const items = options.filter(option => textParser([option.text]).toLowerCase().includes(search.toLowerCase()));
return (
<View style={[styles.modal, { backgroundColor: themes[theme].backgroundColor }]}>
<View style={[styles.content, { backgroundColor: themes[theme].backgroundColor }]}>
<TextInput
onChangeText={onSearchChange}
placeholder={placeholder.text}
theme={theme}
/>
<Items items={items} selected={selected} onSelect={onSelect} theme={theme} />
</View>
</View>
);
};
const translateY = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [600, 0]
});
let button = multiselect ? (
<Button
title={`${ selected.length } selecteds`}
onPress={onShow}
loading={loading}
theme={theme}
/>
) : (
<Input
open={onShow}
theme={theme}
loading={loading}
>
<Text style={[styles.pickerText, { color: themes[theme].auxiliaryText }]}>{currentValue}</Text>
</Input>
);
if (context === BLOCK_CONTEXT.FORM) {
button = (
<Input
open={onShow}
theme={theme}
loading={loading}
>
<Chips items={options.filter(option => selected.includes(option.value))} onSelect={onSelect} theme={theme} />
</Input>
);
}
return (
<>
<Modal
animationType='fade'
transparent
visible={open}
onRequestClose={onHide}
onShow={onShow}
>
<TouchableWithoutFeedback onPress={onHide}>
<View style={styles.container}>
<View style={[styles.backdrop, { backgroundColor: themes[theme].backdropColor }]} />
<KeyboardAvoidingView style={styles.keyboardView} behavior='padding'>
<Animated.View style={[styles.animatedContent, { transform: [{ translateY }] }]}>
{showContent ? renderContent() : null}
</Animated.View>
</KeyboardAvoidingView>
</View>
</TouchableWithoutFeedback>
</Modal>
{button}
</>
);
});
MultiSelect.propTypes = {
options: PropTypes.array,
onChange: PropTypes.func,
placeholder: PropTypes.object,
context: PropTypes.number,
loading: PropTypes.bool,
multiselect: PropTypes.bool,
value: PropTypes.array,
theme: PropTypes.string
};

View File

@ -0,0 +1,84 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../../../views/Styles';
export default StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'flex-end'
},
backdrop: {
...StyleSheet.absoluteFill,
opacity: 0.3
},
modal: {
height: 300,
width: '100%',
borderTopRightRadius: 16,
borderTopLeftRadius: 16,
overflow: 'hidden'
},
content: {
padding: 16
},
animatedContent: {
width: '100%'
},
keyboardView: {
width: '100%'
},
pickerText: {
...sharedStyles.textRegular,
fontSize: 16
},
item: {
height: 48,
alignItems: 'center',
flexDirection: 'row'
},
input: {
minHeight: 48,
padding: 8,
paddingBottom: 0,
borderWidth: StyleSheet.hairlineWidth,
borderRadius: 2,
alignItems: 'center',
flexDirection: 'row'
},
icon: {
position: 'absolute',
right: 16
},
itemContent: {
paddingBottom: 36
},
items: {
height: 226
},
chips: {
flexDirection: 'row',
flexWrap: 'wrap',
marginRight: 16
},
chip: {
flexDirection: 'row',
borderRadius: 2,
height: 28,
alignItems: 'center',
paddingHorizontal: 4,
marginBottom: 8,
marginRight: 8
},
chipText: {
paddingHorizontal: 8,
...sharedStyles.textMedium,
fontSize: 14
},
chipImage: {
marginLeft: 4,
borderRadius: 2,
width: 20,
height: 20
}
});

View File

@ -0,0 +1,103 @@
import React, { useState } from 'react';
import { Text, FlatList, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import Popover from 'react-native-popover-view';
import Touchable from 'react-native-platform-touchable';
import { CustomIcon } from '../../lib/Icons';
import Separator from '../Separator';
import ActivityIndicator from '../ActivityIndicator';
import { themes } from '../../constants/colors';
import { BUTTON_HIT_SLOP } from '../message/utils';
const keyExtractor = item => item.value;
const styles = StyleSheet.create({
menu: {
justifyContent: 'center'
},
option: {
padding: 8,
minHeight: 32
},
loading: {
padding: 0
}
});
const Option = ({
option: { text, value }, onOptionPress, parser, theme
}) => (
<Touchable
onPress={() => onOptionPress({ value })}
background={Touchable.Ripple(themes[theme].bannerBackground)}
style={styles.option}
>
<Text>{parser.text(text)}</Text>
</Touchable>
);
Option.propTypes = {
option: PropTypes.object,
onOptionPress: PropTypes.func,
parser: PropTypes.object,
theme: PropTypes.string
};
const Options = ({
options, onOptionPress, parser, theme
}) => (
<FlatList
data={options}
renderItem={({ item }) => <Option option={item} onOptionPress={onOptionPress} parser={parser} theme={theme} />}
keyExtractor={keyExtractor}
ItemSeparatorComponent={() => <Separator theme={theme} />}
/>
);
Options.propTypes = {
options: PropTypes.array,
onOptionPress: PropTypes.func,
parser: PropTypes.object,
theme: PropTypes.string
};
const touchable = {};
export const Overflow = ({
element, loading, action, parser, theme
}) => {
const { options, blockId } = element;
const [show, onShow] = useState(false);
const onOptionPress = ({ value }) => {
onShow(false);
action({ value });
};
return (
<>
<Touchable
ref={ref => touchable[blockId] = ref}
background={Touchable.Ripple(themes[theme].bannerBackground)}
onPress={() => onShow(!show)}
hitSlop={BUTTON_HIT_SLOP}
style={styles.menu}
>
{!loading ? <CustomIcon size={18} name='menu' color={themes[theme].bodyText} /> : <ActivityIndicator style={styles.loading} theme={theme} />}
</Touchable>
<Popover
isVisible={show}
fromView={touchable[blockId]}
onRequestClose={() => onShow(false)}
>
<Options options={options} onOptionPress={onOptionPress} parser={parser} theme={theme} />
</Popover>
</>
);
};
Overflow.propTypes = {
element: PropTypes.any,
action: PropTypes.func,
loading: PropTypes.bool,
parser: PropTypes.object,
theme: PropTypes.string
};

View File

@ -0,0 +1,70 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
const styles = StyleSheet.create({
content: {
marginBottom: 8
},
row: {
flexDirection: 'row'
},
column: {
justifyContent: 'center'
},
text: {
flex: 1,
padding: 4,
fontSize: 16,
lineHeight: 22,
textAlignVertical: 'center',
...sharedStyles.textRegular
},
field: {
marginVertical: 6
}
});
const Accessory = ({
blockId, appId, element, parser
}) => parser.renderAccessories(
{ blockId, appId, ...element },
BLOCK_CONTEXT.SECTION,
parser
);
const Fields = ({ fields, parser, theme }) => fields.map(field => (
<Text style={[styles.text, styles.field, { color: themes[theme].bodyText }]}>
{parser.text(field)}
</Text>
));
const accessoriesRight = ['image', 'overflow'];
export const Section = ({
blockId, appId, text, fields, accessory, parser, theme
}) => (
<View
style={[
styles.content,
accessory && accessoriesRight.includes(accessory.type) ? styles.row : styles.column
]}
>
{text ? <Text style={[styles.text, { color: themes[theme].bodyText }]}>{parser.text(text)}</Text> : null}
{fields ? <Fields fields={fields} theme={theme} parser={parser} /> : null}
{accessory ? <Accessory element={{ blockId, appId, ...accessory }} parser={parser} /> : null}
</View>
);
Section.propTypes = {
blockId: PropTypes.string,
appId: PropTypes.string,
text: PropTypes.object,
fields: PropTypes.array,
accessory: PropTypes.any,
theme: PropTypes.string,
parser: PropTypes.object
};

View File

@ -0,0 +1,89 @@
import React, { useState } from 'react';
import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import RNPickerSelect from 'react-native-picker-select';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import { textParser } from './utils';
import { isAndroid, isIOS } from '../../utils/deviceInfo';
import ActivityIndicator from '../ActivityIndicator';
const styles = StyleSheet.create({
iosPadding: {
height: 48,
justifyContent: 'center'
},
viewContainer: {
marginBottom: 16,
paddingHorizontal: 16,
borderWidth: StyleSheet.hairlineWidth,
borderRadius: 2,
justifyContent: 'center'
},
pickerText: {
...sharedStyles.textRegular,
fontSize: 16
},
icon: {
right: 16
},
loading: {
padding: 0
}
});
export const Select = ({
options = [],
placeholder,
onChange,
loading,
disabled,
value: initialValue,
theme
}) => {
const [selected, setSelected] = useState(!Array.isArray(initialValue) && initialValue);
const items = options.map(option => ({ label: textParser([option.text]), value: option.value }));
const pickerStyle = {
...styles.viewContainer,
...(isIOS ? styles.iosPadding : {}),
borderColor: themes[theme].separatorColor,
backgroundColor: themes[theme].backgroundColor
};
const Icon = () => (
loading
? <ActivityIndicator style={styles.loading} />
: <CustomIcon size={22} name='arrow-down' style={isAndroid && styles.icon} color={themes[theme].auxiliaryText} />
);
return (
<RNPickerSelect
items={items}
placeholder={placeholder ? { label: textParser([placeholder]), value: null } : {}}
useNativeAndroidPickerStyle={false}
value={selected}
disabled={disabled}
onValueChange={(value) => {
onChange({ value });
setSelected(value);
}}
style={{
viewContainer: pickerStyle,
inputAndroidContainer: pickerStyle
}}
Icon={Icon}
textInputProps={{ style: { ...styles.pickerText, color: selected ? themes[theme].titleText : themes[theme].auxiliaryText } }}
/>
);
};
Select.propTypes = {
options: PropTypes.array,
placeholder: PropTypes.string,
onChange: PropTypes.func,
loading: PropTypes.bool,
disabled: PropTypes.bool,
value: PropTypes.array,
theme: PropTypes.string
};

View File

@ -0,0 +1,246 @@
/* eslint-disable class-methods-use-this */
import React, { useContext } from 'react';
import { StyleSheet } from 'react-native';
import {
uiKitMessage,
UiKitParserMessage,
uiKitModal,
UiKitParserModal,
BLOCK_CONTEXT
} from '@rocket.chat/ui-kit';
import Markdown from '../markdown';
import Button from '../Button';
import TextInput from '../TextInput';
import { useBlockContext } from './utils';
import { themes } from '../../constants/colors';
import { Divider } from './Divider';
import { Section } from './Section';
import { Actions } from './Actions';
import { Image } from './Image';
import { Select } from './Select';
import { Context } from './Context';
import { MultiSelect } from './MultiSelect';
import { Input } from './Input';
import { DatePicker } from './DatePicker';
import { Overflow } from './Overflow';
import { ThemeContext } from '../../theme';
const styles = StyleSheet.create({
input: {
marginBottom: 0
},
multiline: {
height: 130
},
button: {
marginBottom: 16
}
});
const plainText = ({ text } = { text: '' }) => text;
class MessageParser extends UiKitParserMessage {
text({ text, type } = { text: '' }, context) {
const { theme } = useContext(ThemeContext);
if (type !== 'mrkdwn') {
return text;
}
const isContext = context === BLOCK_CONTEXT.CONTEXT;
return (
<Markdown
msg={text}
theme={theme}
style={[isContext && { color: themes[theme].auxiliaryText }]}
preview={isContext}
/>
);
}
button(element, context) {
const {
text, value, actionId, style
} = element;
const [{ loading }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext);
return (
<Button
key={actionId}
type={style}
title={this.text(text)}
loading={loading}
onPress={() => action({ value })}
style={styles.button}
theme={theme}
/>
);
}
divider() {
const { theme } = useContext(ThemeContext);
return <Divider theme={theme} />;
}
section(args) {
const { theme } = useContext(ThemeContext);
return <Section {...args} theme={theme} parser={this} />;
}
actions(args) {
const { theme } = useContext(ThemeContext);
return <Actions {...args} theme={theme} parser={this} />;
}
overflow(element, context) {
const [{ loading }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext);
return (
<Overflow
element={element}
context={context}
loading={loading}
action={action}
theme={theme}
parser={this}
/>
);
}
datePicker(element, context) {
const [{
loading, value, error, language
}, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext);
return (
<DatePicker
element={element}
language={language}
theme={theme}
value={value}
action={action}
context={context}
loading={loading}
error={error}
/>
);
}
image(element, context) {
const { theme } = useContext(ThemeContext);
return <Image element={element} theme={theme} context={context} />;
}
context(args) {
const { theme } = useContext(ThemeContext);
return <Context {...args} theme={theme} parser={this} />;
}
multiStaticSelect(element, context) {
const [{ loading, value }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext);
return (
<MultiSelect
{...element}
theme={theme}
value={value}
onChange={action}
context={context}
loading={loading}
multiselect
/>
);
}
staticSelect(element, context) {
const [{ loading, value }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext);
return (
<Select
{...element}
theme={theme}
value={value}
onChange={action}
loading={loading}
/>
);
}
selectInput(element, context) {
const [{ loading, value }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext);
return (
<MultiSelect
{...element}
theme={theme}
value={value}
onChange={action}
context={context}
loading={loading}
/>
);
}
}
class ModalParser extends UiKitParserModal {
constructor() {
super();
Object.getOwnPropertyNames(MessageParser.prototype).forEach((method) => {
ModalParser.prototype[method] = ModalParser.prototype[method] || MessageParser.prototype[method];
});
}
input({
element, blockId, appId, label, description, hint
}, context) {
const [{ error }] = useBlockContext({ ...element, appId, blockId }, context);
const { theme } = useContext(ThemeContext);
return (
<Input
parser={this}
element={{ ...element, appId, blockId }}
label={plainText(label)}
description={plainText(description)}
hint={plainText(hint)}
error={error}
theme={theme}
/>
);
}
image(element, context) {
const { theme } = useContext(ThemeContext);
return <Image element={element} theme={theme} context={context} />;
}
plainInput(element, context) {
const [{ loading, value, error }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext);
const { multiline, actionId, placeholder } = element;
return (
<TextInput
id={actionId}
placeholder={plainText(placeholder)}
onInput={action}
multiline={multiline}
loading={loading}
onChangeText={text => action({ value: text })}
inputStyle={multiline && styles.multiline}
containerStyle={styles.input}
value={value}
error={{ error }}
theme={theme}
/>
);
}
}
export const messageParser = new MessageParser();
export const modalParser = new ModalParser();
export const UiKitMessage = uiKitMessage(messageParser);
export const UiKitModal = uiKitModal(modalParser);
export const UiKitComponent = ({ render, blocks }) => render(blocks);

View File

@ -0,0 +1,63 @@
/* eslint-disable no-shadow */
import React, { useContext, useState } from 'react';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
export const textParser = ([{ text }]) => text;
export const defaultContext = {
action: (...args) => console.log(args),
state: console.log,
appId: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
errors: {}
};
export const KitContext = React.createContext(defaultContext);
export const useBlockContext = ({
blockId, actionId, appId, initialValue
}, context) => {
const {
action, appId: appIdFromContext, viewId, state, language, errors, values = {}
} = useContext(KitContext);
const { value = initialValue } = values[actionId] || {};
const [loading, setLoading] = useState(false);
const error = errors && actionId && errors[actionId];
if ([BLOCK_CONTEXT.SECTION, BLOCK_CONTEXT.ACTION].includes(context)) {
return [{
loading, setLoading, error, value, language
}, async({ value }) => {
setLoading(true);
try {
await action({
blockId,
appId: appId || appIdFromContext,
actionId,
value,
viewId
});
} catch (e) {
// do nothing
}
setLoading(false);
}];
}
return [{
loading, setLoading, value, error, language
}, async({ value }) => {
setLoading(true);
try {
await state({
blockId,
appId,
actionId,
value
});
} catch (e) {
// do nothing
}
setLoading(false);
}];
};

View File

@ -28,21 +28,27 @@ const AtMention = React.memo(({
}
const handlePress = () => {
if (mentions && mentions.length && mentions.findIndex(m => m.username === mention) !== -1) {
const index = mentions.findIndex(m => m.username === mention);
const navParam = {
t: 'd',
rid: mentions[index]._id
};
navToRoomInfo(navParam);
}
const index = mentions.findIndex(m => m.username === mention);
const navParam = {
t: 'd',
rid: mentions[index]._id
};
navToRoomInfo(navParam);
};
if (mentions && mentions.length && mentions.findIndex(m => m.username === mention) !== -1) {
return (
<Text
style={[preview ? { ...styles.text, color: themes[theme].bodyText } : mentionStyle, ...style]}
onPress={preview ? undefined : handlePress}
>
{mention}
</Text>
);
}
return (
<Text
style={[preview ? { ...styles.text, color: themes[theme].bodyText } : mentionStyle, ...style]}
onPress={preview ? undefined : handlePress}
>
<Text style={[styles.text, { color: themes[theme].bodyText }, ...style]}>
{`@${ mention }`}
</Text>
);

View File

@ -24,12 +24,12 @@ const Hashtag = React.memo(({
style={[preview ? { ...styles.text, color: themes[theme].bodyText } : styles.mention, ...style]}
onPress={preview ? undefined : handlePress}
>
{`#${ hashtag }`}
{hashtag}
</Text>
);
}
return (
<Text style={[preview ? { ...styles.text, color: themes[theme].bodyText } : styles.mention, ...style]}>
<Text style={[styles.text, { color: themes[theme].bodyText }, ...style]}>
{`#${ hashtag }`}
</Text>
);

View File

@ -1,10 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text } from 'react-native';
import { Text, Clipboard } from 'react-native';
import styles from './styles';
import { themes } from '../../constants/colors';
import openLink from '../../utils/openLink';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import I18n from '../../i18n';
const Link = React.memo(({
children, link, preview, theme
@ -17,11 +20,16 @@ const Link = React.memo(({
};
const childLength = React.Children.toArray(children).filter(o => o).length;
const onLongPress = () => {
Clipboard.setString(link);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
};
// if you have a [](https://rocket.chat) render https://rocket.chat
return (
<Text
onPress={preview ? undefined : handlePress}
onLongPress={preview ? undefined : onLongPress}
style={
!preview
? { ...styles.link, color: themes[theme].actionTintColor }

View File

@ -38,7 +38,7 @@ const Table = React.memo(({
);
};
const onPress = () => Navigation.navigate('TableView', { renderRows, tableWidth: getTableWidth() });
const onPress = () => Navigation.navigate('MarkdownTableView', { renderRows, tableWidth: getTableWidth() });
return (
<TouchableOpacity onPress={onPress}>

View File

@ -60,6 +60,8 @@ const emojiCount = (str) => {
return counter;
};
const parser = new Parser();
class Markdown extends PureComponent {
static propTypes = {
msg: PropTypes.string,
@ -81,13 +83,9 @@ class Markdown extends PureComponent {
constructor(props) {
super(props);
this.parser = this.createParser();
this.renderer = this.createRenderer(props.preview);
}
createParser = () => new Parser();
createRenderer = (preview = false) => new Renderer({
renderers: {
text: this.renderText,
@ -385,7 +383,7 @@ class Markdown extends PureComponent {
if (preview) {
m = m.split('\n').reduce((lines, line) => `${ lines } ${ line }`, '');
const ast = this.parser.parse(m);
const ast = parser.parse(m);
return this.renderer.render(ast);
}
@ -393,7 +391,7 @@ class Markdown extends PureComponent {
return <Text style={[styles.text, { color: themes[theme].bodyText }]} numberOfLines={numberOfLines}>{m}</Text>;
}
const ast = this.parser.parse(m);
const ast = parser.parse(m);
this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;
this.editedMessage(ast);

View File

@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import { messageBlockWithContext } from '../UIKit/MessageBlock';
const Blocks = React.memo(({
blocks, id: mid, rid, blockAction
}) => {
if (blocks && blocks.length > 0) {
const [, secondBlock] = blocks;
const { appId = '' } = secondBlock;
return React.createElement(
messageBlockWithContext({
action: async({ actionId, value, blockId }) => {
await blockAction({
actionId,
appId,
value,
blockId,
rid,
mid
});
},
appId,
rid
}), { blocks }
);
}
return null;
});
Blocks.propTypes = {
blocks: PropTypes.array,
id: PropTypes.string,
rid: PropTypes.string,
blockAction: PropTypes.func
};
Blocks.displayName = 'MessageBlocks';
export default Blocks;

View File

@ -28,7 +28,7 @@ const Button = React.memo(({
</Touchable>
));
const Image = React.memo(({ img, theme }) => (
export const MessageImage = React.memo(({ img, theme }) => (
<ImageProgress
style={[styles.image, { borderColor: themes[theme].borderColor }]}
source={{ uri: encodeURI(img) }}
@ -41,9 +41,9 @@ const Image = React.memo(({ img, theme }) => (
));
const ImageContainer = React.memo(({
file, baseUrl, user, useMarkdown, showAttachment, getCustomEmoji, split, theme
file, imageUrl, baseUrl, user, useMarkdown, showAttachment, getCustomEmoji, split, theme
}) => {
const img = formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
if (!img) {
return null;
}
@ -54,7 +54,7 @@ const ImageContainer = React.memo(({
return (
<Button split={split} theme={theme} onPress={onPress}>
<View>
<Image img={img} theme={theme} />
<MessageImage img={img} theme={theme} />
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />
</View>
</Button>
@ -63,13 +63,14 @@ const ImageContainer = React.memo(({
return (
<Button split={split} theme={theme} onPress={onPress}>
<Image img={img} theme={theme} />
<MessageImage img={img} theme={theme} />
</Button>
);
}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file) && prevProps.split === nextProps.split && prevProps.theme === nextProps.theme);
ImageContainer.propTypes = {
file: PropTypes.object,
imageUrl: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
useMarkdown: PropTypes.bool,
@ -80,7 +81,7 @@ ImageContainer.propTypes = {
};
ImageContainer.displayName = 'MessageImageContainer';
Image.propTypes = {
MessageImage.propTypes = {
img: PropTypes.string,
theme: PropTypes.string
};

View File

@ -10,6 +10,7 @@ import MessageAvatar from './MessageAvatar';
import Attachments from './Attachments';
import Urls from './Urls';
import Thread from './Thread';
import Blocks from './Blocks';
import Reactions from './Reactions';
import Broadcast from './Broadcast';
import Discussion from './Discussion';
@ -35,6 +36,16 @@ const MessageInner = React.memo((props) => {
</>
);
}
if (props.blocks && props.blocks.length) {
return (
<>
<User {...props} />
<Blocks {...props} />
<Thread {...props} />
<Reactions {...props} />
</>
);
}
return (
<>
<User {...props} />
@ -139,7 +150,8 @@ Message.propTypes = {
};
MessageInner.propTypes = {
type: PropTypes.string
type: PropTypes.string,
blocks: PropTypes.array
};
export default MessageTouchable;

View File

@ -8,7 +8,7 @@ import styles from './styles';
const MessageAvatar = React.memo(({
isHeader, avatar, author, baseUrl, user, small, navToRoomInfo
}) => {
if (isHeader) {
if (isHeader && author) {
const navParam = {
t: 'd',
rid: author._id

View File

@ -56,7 +56,7 @@ const Reaction = React.memo(({
const Reactions = React.memo(({
reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji, theme
}) => {
if (!reactions || reactions.length === 0) {
if (!Array.isArray(reactions) || reactions.length === 0) {
return null;
}
return (

View File

@ -1,5 +1,7 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import {
View, Text, StyleSheet, Clipboard
} from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
@ -10,6 +12,9 @@ import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import { withSplit } from '../../split';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import I18n from '../../i18n';
const styles = StyleSheet.create({
button: {
@ -82,9 +87,15 @@ const Url = React.memo(({
const onPress = () => openLink(url.url, theme);
const onLongPress = () => {
Clipboard.setString(url.url);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
};
return (
<Touchable
onPress={onPress}
onLongPress={onLongPress}
style={[
styles.button,
index > 0 && styles.marginTop,

View File

@ -16,6 +16,7 @@ class MessageContainer extends React.Component {
username: PropTypes.string.isRequired,
token: PropTypes.string.isRequired
}),
rid: PropTypes.string,
timeFormat: PropTypes.string,
customThreadTimeFormat: PropTypes.string,
style: PropTypes.any,
@ -44,11 +45,25 @@ class MessageContainer extends React.Component {
onReactionLongPress: PropTypes.func,
navToRoomInfo: PropTypes.func,
callJitsi: PropTypes.func,
blockAction: PropTypes.func,
theme: PropTypes.string
}
static defaultProps = {
getCustomEmoji: () => {},
onLongPress: () => {},
onReactionPress: () => {},
onDiscussionPress: () => {},
onThreadPress: () => {},
errorActionsShow: () => {},
replyBroadcast: () => {},
reactionInit: () => {},
fetchThreadName: () => {},
showAttachment: () => {},
onReactionLongPress: () => {},
navToRoomInfo: () => {},
callJitsi: () => {},
blockAction: () => {},
archived: false,
broadcast: false,
theme: 'light'
@ -212,10 +227,10 @@ class MessageContainer extends React.Component {
render() {
const {
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, theme
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme
} = this.props;
const {
id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, autoTranslate: autoTranslateMessage
id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage
} = item;
let message = msg;
@ -229,10 +244,12 @@ class MessageContainer extends React.Component {
<Message
id={id}
msg={message}
rid={rid}
author={u}
ts={ts}
type={t}
attachments={attachments}
blocks={blocks}
urls={urls}
reactions={reactions}
alias={alias}
@ -279,6 +296,7 @@ class MessageContainer extends React.Component {
getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo}
callJitsi={callJitsi}
blockAction={blockAction}
theme={theme}
/>
);

View File

@ -6,9 +6,12 @@ import en from './locales/en';
import ru from './locales/ru';
import fr from './locales/fr';
import de from './locales/de';
import nl from './locales/nl';
import ptBR from './locales/pt-BR';
import zhCN from './locales/zh-CN';
import ptPT from './locales/pt-PT';
import esES from './locales/es-ES';
import it from './locales/it';
i18n.translations = {
en,
@ -17,7 +20,10 @@ i18n.translations = {
'zh-CN': zhCN,
fr,
de,
'pt-PT': ptPT
'pt-PT': ptPT,
'es-ES': esES,
nl,
it
};
i18n.fallbacks = true;

View File

@ -82,10 +82,10 @@ export default {
Add_Reaction: 'Reaktion hinzufügen',
Add_Server: 'Server hinzufügen',
Add_users: 'Nutzer hinzufügen',
Admin_Panel: 'Admin Panel',
Alert: 'Warnen',
alert: 'warnen',
alerts: 'Warnungen',
Admin_Panel: 'Admin-Panel',
Alert: 'Benachrichtigung',
alert: 'Benachrichtigung',
alerts: 'Benachrichtigungen',
All_users_in_the_channel_can_write_new_messages: 'Alle Benutzer im Kanal können neue Nachrichten schreiben',
All: 'Alles',
All_Messages: 'Alle Nachrichten',
@ -96,11 +96,11 @@ export default {
announcement: 'Ankündigung',
Announcement: 'Ankündigung',
Apply_Your_Certificate: 'Wenden Sie Ihr Zertifikat an',
Applying_a_theme_will_change_how_the_app_looks: 'Ein Theme zu setzen wird das Aussehen der Anwendung ändern.',
Applying_a_theme_will_change_how_the_app_looks: 'Das Erscheinungsbild festzulegen wird das Aussehen der Anwendung ändern.',
ARCHIVE: 'ARCHIV',
archive: 'Archiv',
are_typing: 'tippen',
Are_you_sure_question_mark: 'Sind Sie sicher?',
Are_you_sure_question_mark: 'Bist du sicher?',
Are_you_sure_you_want_to_leave_the_room: 'Möchten Sie den Raum wirklich verlassen {{room}}?',
Audio: 'Audio',
Authenticating: 'Authentifizierung',
@ -121,6 +121,7 @@ export default {
Cancel: 'Abbrechen',
changing_avatar: 'Avatar wechseln',
creating_channel: 'Kanal erstellen',
creating_invite: 'Einladung erstellen',
Channel_Name: 'Kanal Name',
Channels: 'Kanäle',
Chats: 'Chats',
@ -146,6 +147,7 @@ export default {
Copy: 'Kopieren',
Permalink: 'Permalink',
Certificate_password: 'Zertifikats-Passwort',
Clear_cache: 'Lokalen Server-Cache leeren',
Whats_the_password_for_your_certificate: 'Wie lautet das Passwort für Ihr Zertifikat?',
Create_account: 'Ein Konto erstellen',
Create_Channel: 'Kanal erstellen',
@ -172,6 +174,7 @@ export default {
edit: 'bearbeiten',
edited: 'bearbeitet',
Edit: 'Bearbeiten',
Edit_Invite: 'Einladung bearbeiten',
Email_or_password_field_is_empty: 'Das E-Mail- oder Passwortfeld ist leer',
Email: 'Email',
EMAIL: 'EMAIL',
@ -182,6 +185,7 @@ export default {
Everyone_can_access_this_channel: 'Jeder kann auf diesen Kanal zugreifen',
erasing_room: 'lösche Raum',
Error_uploading: 'Fehler beim Hochladen',
Expiration_Days: 'läuft ab (Tage)',
Favorite: 'Favorisieren',
Favorites: 'Favoriten',
Files: 'Dateien',
@ -195,6 +199,7 @@ export default {
Forgot_password: 'Passwort vergessen',
Forgot_Password: 'Passwort vergessen',
Full_table: 'Klicken um die ganze Tabelle anzuzeigen',
Generate_New_Link: 'Neuen Link erstellen',
Group_by_favorites: 'Nach Favoriten gruppieren',
Group_by_type: 'Gruppieren nach Typ',
Hide: 'Ausblenden',
@ -208,7 +213,10 @@ export default {
is_a_valid_RocketChat_instance: 'ist eine gültige Rocket.Chat-Instanz',
is_not_a_valid_RocketChat_instance: 'ist keine gültige Rocket.Chat-Instanz',
is_typing: 'tippt',
Invalid_or_expired_invite_token: 'Ungültiger oder abgelaufener Einladungscode',
Invalid_server_version: 'Der Server, zu dem Sie eine Verbindung herstellen möchten, verwendet eine Version, die von der App nicht mehr unterstützt wird: {{currentVersion}}.\n\nWir benötigen Version {{MinVersion}}.',
Invite_Link: 'Einladungs-Link',
Invite_users: 'Benutzer einladen',
Join_the_community: 'Trete der Community bei',
Join: 'Beitreten',
Just_invited_people_can_access_this_channel: 'Nur eingeladene Personen können auf diesen Kanal zugreifen',
@ -224,7 +232,8 @@ export default {
Login: 'Anmeldung',
Login_error: 'Ihre Zugangsdaten wurden abgelehnt! Bitte versuchen Sie es erneut.',
Login_with: 'Einloggen mit',
Logout: 'Ausloggen',
Logout: 'Abmelden',
Max_number_of_uses: 'Maximale Anzahl der Benutzungen',
members: 'Mitglieder',
Members: 'Mitglieder',
Mentioned_Messages: 'Erwähnte Nachrichten',
@ -236,6 +245,7 @@ export default {
Message_removed: 'Nachricht entfernt',
message: 'Nachricht',
messages: 'Nachrichten',
Message: 'Nachricht',
Messages: 'Mitteilungen',
Message_Reported: 'Nachricht gemeldet',
Microphone_Permission_Message: 'Rocket.Chat benötigt Zugriff auf Ihr Mikrofon, damit Sie eine Audionachricht senden können.',
@ -247,12 +257,14 @@ export default {
N_users: '{{n}} Benutzer',
name: 'Name',
Name: 'Name',
Never: 'Niemals',
New_Message: 'Neue Nachricht',
New_Password: 'Neues Kennwort',
New_Server: 'Neuer Server',
Next: 'Nächster',
No_files: 'Keine Dateien',
No_mentioned_messages: 'Keine erwähnten Nachrichten',
No_limit: 'Kein Limit',
No_mentioned_messages: 'Keine Nachrichten mit Erwähnungen',
No_pinned_messages: 'Keine angehefteten Nachrichten',
No_results_found: 'Keine Ergebnisse gefunden',
No_starred_messages: 'Keine markierten Nachrichten',
@ -322,6 +334,13 @@ export default {
Reset_password: 'Passwort zurücksetzen',
resetting_password: 'Passwort zurücksetzen',
RESET: 'ZURÜCKSETZEN',
Review_app_title: 'Gefällt dir diese App?',
Review_app_desc: 'Gib uns 5 Sterne im {{store}}',
Review_app_yes: 'Sicher!',
Review_app_no: 'Nein',
Review_app_later: 'Vielleicht später',
Review_app_unable_store: 'Kann {{store}} nicht öffnen',
Review_this_app: 'App bewerten',
Roles: 'Rollen',
Room_actions: 'Raumaktionen',
Room_changed_announcement: 'Raumansage geändert in: {{announcement}} von {{userBy}}',
@ -362,7 +381,9 @@ export default {
Settings: 'Einstellungen',
Settings_succesfully_changed: 'Einstellungen erfolgreich geändert!',
Share: 'Teilen',
Share_this_app: 'Teile diese App',
Share_Link: 'Link teilen',
Share_this_app: 'App teilen',
Show_more: 'Mehr anzeigen …',
Show_Unread_Counter: 'Zähler anzeigen',
Show_Unread_Counter_Info: 'Anzahl der ungelesenen Nachrichten anzeigen',
Sign_in_your_server: 'Melden Sie sich bei Ihrem Server an',
@ -385,7 +406,7 @@ export default {
tap_to_change_status: 'Tippen um den Status zu ändern',
Tap_to_view_servers_list: 'Tippen Sie hier, um die Serverliste anzuzeigen',
Terms_of_Service: ' Nutzungsbedingungen',
Theme: 'Theme',
Theme: 'Erscheinungsbild',
The_URL_is_invalid: 'Die eingegebene URL ist ungültig. Überprüfen Sie es und versuchen Sie es bitte erneut!',
There_was_an_error_while_action: 'Während {{action}} ist ein Fehler aufgetreten!',
This_room_is_blocked: 'Dieser Raum ist gesperrt',
@ -410,18 +431,19 @@ export default {
Unpin: 'Nachricht nicht mehr anheften',
unread_messages: 'ungelesene',
Unread: 'Ungelesen',
Unread_on_top: 'Ungelesen an der Spitze',
Unread_on_top: 'Ungelesene oben',
Unstar: 'von Favoriten entfernen',
Updating: 'Aktualisierung …',
Uploading: 'Hochladen',
Upload_file_question_mark: 'Datei hochladen?',
Users: 'Benutzer',
User_added_by: 'Benutzer {{userAdded}} hinzugefügt von {{userBy}}',
User_Info: 'Nutzerinfo',
User_has_been_key: 'Benutzer wurde {{key}}!',
User_is_no_longer_role_by_: '{{user}} ist nicht länger {{role}} von {{userBy}}',
User_muted_by: 'Benutzer {{userMuted}} von {{userBy}} stummgeschaltet',
User_removed_by: 'Benutzer {{userRemoved}} wurde von {{userBy}} entfernt',
User_sent_an_attachment: '{{user}} hat eine Anlage gesendet',
User_sent_an_attachment: '{{user}}: eine Datei gesendet',
User_unmuted_by: 'Benutzer {{userUnmuted}} nicht stummgeschaltet von {{userBy}}',
User_was_set_role_by_: '{{user}} wurde von {{userBy}} {{role}} festgelegt.',
Username_is_empty: 'Der Benutzername ist leer',
@ -447,8 +469,13 @@ export default {
you_were_mentioned: 'Sie wurden erwähnt',
you: 'Sie',
You: 'Sie',
Logged_out_by_server: 'Du bist vom Server abgemeldet worden. Bitte melde dich wieder an.',
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Sie benötigen Zugang zu mindestens einem Rocket.Chat-Server um etwas zu teilen.',
Your_certificate: 'Ihr Zertifikat',
Your_invite_link_will_expire_after__usesLeft__uses: 'Dein Einladungs-Link wird nach {{usesLeft}} Benutzungen ablaufen.',
Your_invite_link_will_expire_on__date__or_after__usesLeft__uses: 'Dein Einladungs-Link wird am {{date}} oder nach {{usesLeft}} Benutzungen ablaufen.',
Your_invite_link_will_expire_on__date__: 'Dein Einladungs-Link wird am {{date}} ablaufen.',
Your_invite_link_will_never_expire: 'Dein Einladungs-Link wird niemals ablaufen.',
Version_no: 'Version: {{version}}',
You_will_not_be_able_to_recover_this_message: 'Sie können diese Nachricht nicht wiederherstellen!',
Change_Language: 'Sprache ändern',
@ -466,5 +493,8 @@ export default {
Server_selection: 'Server-Auswahl',
Server_selection_numbers: 'Server-Auswahl 1...9',
Add_server: 'Server hinufügen',
New_line: 'Zeilenumbruch'
New_line: 'Zeilenumbruch',
You_will_be_logged_out_of_this_application: 'Du wirst in dieser Anwendung vom Server abgemeldet.',
Clear: 'Löschen',
This_will_clear_all_your_offline_data: 'Dies wird deine Offline-Daten löschen.'
};

View File

@ -147,6 +147,7 @@ export default {
Copy: 'Copy',
Permalink: 'Permalink',
Certificate_password: 'Certificate Password',
Clear_cache: 'Clear local server cache',
Whats_the_password_for_your_certificate: 'What\'s the password for your certificate?',
Create_account: 'Create an account',
Create_Channel: 'Create Channel',
@ -244,6 +245,7 @@ export default {
Message_removed: 'Message removed',
message: 'message',
messages: 'messages',
Message: 'Message',
Messages: 'Messages',
Message_Reported: 'Message reported',
Microphone_Permission_Message: 'Rocket Chat needs access to your microphone so you can send audio message.',
@ -332,6 +334,13 @@ export default {
Reset_password: 'Reset password',
resetting_password: 'resetting password',
RESET: 'RESET',
Review_app_title: 'Are you enjoying this app?',
Review_app_desc: 'Give us 5 stars on {{store}}',
Review_app_yes: 'Sure!',
Review_app_no: 'No',
Review_app_later: 'Maybe later',
Review_app_unable_store: 'Unable to open {{store}}',
Review_this_app: 'Review this app',
Roles: 'Roles',
Room_actions: 'Room actions',
Room_changed_announcement: 'Room announcement changed to: {{announcement}} by {{userBy}}',
@ -374,6 +383,7 @@ export default {
Share: 'Share',
Share_Link: 'Share Link',
Share_this_app: 'Share this app',
Show_more: 'Show more..',
Show_Unread_Counter: 'Show Unread Counter',
Show_Unread_Counter_Info: 'Unread counter is displayed as a badge on the right of the channel, in the list',
Sign_in_your_server: 'Sign in your server',
@ -440,6 +450,8 @@ export default {
Username: 'Username',
Username_or_email: 'Username or email',
Validating: 'Validating',
Verify_email_title: 'Registration Succeeded!',
Verify_email_desc: 'We have sent you an email to confirm your registration. If you do not receive an email shortly, please come back and try again.',
Video_call: 'Video call',
View_Original: 'View Original',
Voice_call: 'Voice call',
@ -459,6 +471,7 @@ export default {
you_were_mentioned: 'you were mentioned',
you: 'you',
You: 'You',
Logged_out_by_server: 'You\'ve been logged out by the server. Please log in again.',
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'You need to access at least one Rocket.Chat server to share something.',
Your_certificate: 'Your Certificate',
Your_invite_link_will_expire_after__usesLeft__uses: 'Your invite link will expire after {{usesLeft}} uses.',
@ -482,5 +495,8 @@ export default {
Server_selection: 'Server selection',
Server_selection_numbers: 'Server selection 1...9',
Add_server: 'Add server',
New_line: 'New line'
New_line: 'New line',
You_will_be_logged_out_of_this_application: 'You will be logged out of this application.',
Clear: 'Clear',
This_will_clear_all_your_offline_data: 'This will clear all your offline data.'
};

466
app/i18n/locales/es-ES.js Normal file
View File

@ -0,0 +1,466 @@
export default {
'1_person_reacted': '1 persona reaccionó',
'1_user': '1 usuario',
'error-action-not-allowed': '{{action}} no permitida',
'error-application-not-found': 'Aplicación no encontrada',
'error-archived-duplicate-name': 'Hay un canal archivado con nombre {{room_name}}',
'error-avatar-invalid-url': 'URL de avatar inválida: {{url}}',
'error-avatar-url-handling': 'Error durante el procesamiento de ajuste de imagen de usuario desde una dirección URL ({{url}}) para {{username}}',
'error-cant-invite-for-direct-room': 'No se puede invitar a los usuarios a salas de chat directas',
'error-could-not-change-email': 'No es posible cambiar la dirección de correo electrónico',
'error-could-not-change-name': 'No es posible cambiar el nombre',
'error-could-not-change-username': 'No es posible cambiar el nombre de usuario',
'error-delete-protected-role': 'No se puede eliminar un rol protegido',
'error-department-not-found': 'Departamento no encontrado',
'error-direct-message-file-upload-not-allowed': 'No se permite compartir archivos en mensajes directos',
'error-duplicate-channel-name': 'Ya existe un canal con nombre {{channel_name}}',
'error-email-domain-blacklisted': 'El dominio del correo electrónico está en la lista negra',
'error-email-send-failed': 'Error al enviar el correo electrónico: {{message}}',
'error-field-unavailable': '{{field}} ya está en uso :(',
'error-file-too-large': 'El archivo es demasiado grande',
'error-importer-not-defined': 'El importador no se configuró correctamente. Falta la clase de importación',
'error-input-is-not-a-valid-field': '{{input}} no es válido {{field}}',
'error-invalid-actionlink': 'Enlace de acción inválido',
'error-invalid-arguments': 'Los argumentos no son correctos',
'error-invalid-asset': 'El archivo archivo no es correcto',
'error-invalid-channel': 'El canal no es correcto.',
'error-invalid-channel-start-with-chars': 'Canal incorrecto. Debe comenzar con @ o #',
'error-invalid-custom-field': 'Invalid custom field',
'error-invalid-custom-field-name': 'Nombre inválido para el campo personalizado. Utilice sólo letras, números, guiones o guión bajo',
'error-invalid-date': 'La fecha proporcionada no es correcta.',
'error-invalid-description': 'La descipción no es correcta',
'error-invalid-domain': 'El dominio no es correcto',
'error-invalid-email': 'El email {{emai}} no es correcto',
'error-invalid-email-address': 'La dirección de correo no es correcta',
'error-invalid-file-height': 'La altura de la imagen no es correcta',
'error-invalid-file-type': 'El formato del archivo no es correcto',
'error-invalid-file-width': 'El ancho de la imagen o es correcto',
'error-invalid-from-address': 'La dirección del remitente (FROM) no es correcta.',
'error-invalid-integration': 'La integración no es correcta',
'error-invalid-message': 'El mensaje no es correcto',
'error-invalid-method': 'El método no es correcto',
'error-invalid-name': 'El nombre no es correcto',
'error-invalid-password': 'La contraseña no es correcta',
'error-invalid-redirectUri': 'La URL de redirección no es correcta.',
'error-invalid-role': 'El rol no es correcto',
'error-invalid-room': 'La sala no es correcta',
'error-invalid-room-name': 'No se puede asignar el nombre {{name}} a una sala.',
'error-invalid-room-type': 'No se puede asginar el tipo {{type}} a una sala.',
'error-invalid-settings': 'La configuración proporcionada no es correcta',
'error-invalid-subscription': 'La subscripción no es correcta',
'error-invalid-token': 'El token no es correcto',
'error-invalid-triggerWords': 'El triggerWords no es correcto',
'error-invalid-urls': 'Las URLs no son correctas',
'error-invalid-user': 'El usuario no es correcto',
'error-invalid-username': 'El nombre de usuario no es correcto',
'error-invalid-webhook-response': 'El webhook no ha respondido con código de estado HTTP 200.',
'error-message-deleting-blocked': 'No está permitido eliminar mensajes',
'error-message-editing-blocked': 'No está permitido editar mensajes',
'error-message-size-exceeded': 'El mensaje supera el tamaño máximo permitido',
'error-missing-unsubscribe-link': 'Debes proporcionar el enlace para cancelar la suscripción [unsubscribe].',
'error-no-tokens-for-this-user': 'No hay tokens asignados para el usuario',
'error-not-allowed': 'No permitido',
'error-not-authorized': 'No autorizado',
'error-push-disabled': 'El Push está desactivado',
'error-remove-last-owner': 'El usuario el único propietario existente. Debes establecer un nuevo propietario antes de eliminarlo.',
'error-role-in-use': 'No puedes eliminar el rol dado que está en uso',
'error-role-name-required': 'Debes indicar el nombre del rol',
'error-the-field-is-required': 'El campo {{field}} es obligatorio.',
'error-too-many-requests': 'Hemos recibido demasiadas peticiones. Debes esperar {{seconds}} segundos antes de continuar. Por favor, sé paciente.',
'error-user-is-not-activated': 'El usuario no está activo',
'error-user-has-no-roles': 'El usuario no tiene roles',
'error-user-limit-exceeded': 'El número de usuarios que quieres invitiar al canal #channel_name supera el límite establecido por el adminitrador.',
'error-user-not-in-room': 'El usuario no está en la sala',
'error-user-registration-custom-field': 'error-user-registration-custom-field',
'error-user-registration-disabled': 'El registro de usuario está deshabilitador',
'error-user-registration-secret': 'El registro de usuarios sólo está permitido por URL secretas',
'error-you-are-last-owner': 'El usuario el único propietario existente. Debes establecer un nuevo propietario antes de abandonar la sala.',
Actions: 'Acciones',
activity: 'actividad',
Activity: 'Actividad',
Add_Reaction: 'Reaccionar',
Add_Server: 'Añadir servidor',
Add_user: 'Añadir usuario',
Admin_Panel: 'Panel de Control',
Alert: 'Alerta',
alert: 'alerta',
alerts: 'alertas',
All_users_in_the_channel_can_write_new_messages: 'Todos los usuarios en el canal pueden escribir mensajes',
All: 'Todos',
All_Messages: 'Todos los mensajes',
Allow_Reactions: 'Permitir reacciones',
Alphabetical: 'Alfabético',
and_more: 'más',
and: 'y',
announcement: 'anuncio',
Announcement: 'Anuncio',
Apply_Your_Certificate: 'Applica tu Certificación',
Applying_a_theme_will_change_how_the_app_looks: 'Aplicando un tema modificará el aspecto de la App.',
ARCHIVE: 'FICHERO',
archive: 'Fichero',
are_typing: 'escribiendo',
Are_you_sure_question_mark: '¿Estás seguro?',
Are_you_sure_you_want_to_leave_the_room: '¿Deseas salir de la sala {{room}}?',
Audio: 'Audio',
Authenticating: 'Autenticando',
Automatic: 'Automático',
Auto_Translate: 'Auto-Translate',
Avatar_changed_successfully: 'Has cambiado tu Avatar!',
Avatar_Url: 'URL del Avatar',
Away: 'Ausente',
Back: 'Volver',
Black: 'Black',
Block_user: 'Bloquear usuario',
Broadcast_channel_Description: 'Sólo los usuario permitidos pueden escribir nuevos mensajes, el resto podrán responder sobre los mismos.',
Broadcast_Channel: 'Canal de Transmisión',
Busy: 'Ocupado',
By_proceeding_you_are_agreeing: 'Al proceder estarás de acuerdo',
Cancel_editing: 'Cancelar edición',
Cancel_recording: 'Cancelar grabación',
Cancel: 'Cancelar',
changing_avatar: 'cambiando avatar',
creating_channel: 'creando channel',
Channel_Name: 'Nombre sala',
Channels: 'Salas',
Chats: 'Chats',
Call_already_ended: 'La llamada ya ha finalizado!',
Click_to_join: 'Unirme!',
Close: 'Cerrar',
Close_emoji_selector: 'Cerrar selector de emojis',
Choose: 'Seleccionar',
Choose_from_library: 'Seleccionar desde Galería',
Choose_file: 'Seleccionar Archivo',
Code: 'Código',
Collaborative: 'Colaborativo',
Confirm: 'Confirmar',
Connect: 'Conectar',
Connect_to_a_server: 'Conectar a servidor',
Connected: 'Conectado',
connecting_server: 'conectando a servidor',
Connecting: 'Conectando...',
Contact_us: 'Contactar',
Contact_your_server_admin: 'Contacta con el administrador.',
Continue_with: 'Continuar con',
Copied_to_clipboard: 'Copiado al portapapeles!',
Copy: 'Copiar',
Permalink: 'Enlace permanente',
Certificate_password: 'Contraseña del certificado',
Whats_the_password_for_your_certificate: '¿Cuál es la contraseña de tu cerficiado?',
Create_account: 'Crear una cuenta',
Create_Channel: 'Crear Sala',
Created_snippet: 'Crear snippet',
Create_a_new_workspace: 'Crear un Workspace',
Create: 'Crear',
Dark: 'Óscuro',
Dark_level: 'Nivel',
Default: 'Por defecto',
Delete_Room_Warning: 'Eliminar a un usuario causará la eliminación de todos los mensajes creados por dicho usuario. Esta operación no se puede deshacer.',
delete: 'eliminar',
Delete: 'Eliminar',
DELETE: 'ELIMINAR',
description: 'descripción',
Description: 'Descripción',
DESKTOP_OPTIONS: 'OPCIONES DE ESCRITORIO',
Directory: 'Directorio',
Direct_Messages: 'Mensajes directo',
Disable_notifications: 'Desactivar notificaciones',
Discussions: 'Conversaciones',
Dont_Have_An_Account: '¿Todavía no tienes una cuenta?',
Do_you_have_a_certificate: '¿Tienes un certificado?',
Do_you_really_want_to_key_this_room_question_mark: '¿Deseas {{key}} de esta sala?',
edit: 'editar',
edited: 'editado',
Edit: 'Editar',
Email_or_password_field_is_empty: 'El email o la contraseña están vacios',
Email: 'Email',
EMAIL: 'EMAIL',
email: 'e-mail',
Enable_Auto_Translate: 'Permitir Auto-Translate',
Enable_markdown: 'Permitir markdown',
Enable_notifications: 'Permitir notificaciones',
Everyone_can_access_this_channel: 'Todos los usuarios pueden acceder a este canal',
erasing_room: 'eliminando sala',
Error_uploading: 'Error en la subida',
Favorite: 'Favorito',
Favorites: 'Favoritos',
Files: 'Archivos',
File_description: 'Descripción del archivo',
File_name: 'Nombre del archivo',
Finish_recording: 'Finalizar grabación',
Following_thread: 'Siguiendo hilo',
For_your_security_you_must_enter_your_current_password_to_continue: 'Por seguridad, debes introducir tu contraseña para continuar',
Forgot_my_password: 'He olvidado mi contraseña',
Forgot_password_If_this_email_is_registered: 'Si este email está registrado, te enviaremos las instrucciones para resetear tu contraseña.Si no recibes un email en un rato, vuelve aquí e inténtalo de nuevo.',
Forgot_password: 'Restablecer mi contraseña',
Forgot_Password: 'Restabler mi Contraseña',
Full_table: 'Click para ver la tabla completa',
Group_by_favorites: 'Agrupar por favoritos',
Group_by_type: 'Agrupar por tipo',
Hide: 'Ocultar',
Has_joined_the_channel: 'Se ha unido al canal',
Has_joined_the_conversation: 'Se ha unido a la conversación',
Has_left_the_channel: 'Ha dejado el canal',
IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP',
In_App_and_Desktop_Alert_info: 'Muestra un banner en la parte superior de la pantalla cuando la aplicación está abierta y muestra una notificación en el escritorio',
Invisible: 'Invisible',
Invite: 'Invitar',
is_a_valid_RocketChat_instance: 'es una instancia válida Rocket.Chat',
is_not_a_valid_RocketChat_instance: 'no es una instancia válida Rocket.Chat',
is_typing: 'escribiendo',
Invalid_server_version: 'El servidor que intentas conectar está usando una versión que ya no es soportada por la aplicación : {{currentVersion}}. Requerimos una versión {{minVersion}}.',
Join_the_community: 'Conectar con la comunidad',
Join: 'Conectar',
Just_invited_people_can_access_this_channel: 'Sólo gente invitada puede acceder a este canal.',
Language: 'Idioma',
last_message: 'último mensaje',
Leave_channel: 'Abandonar canal',
leaving_room: 'abandonando sala',
leave: 'abandonar',
Legal: 'Legal',
Light: 'Claro',
License: 'Licencia',
Livechat: 'Livechat',
Login: 'Acceder',
Login_error: '¡Sus credenciales fueron rechazadas! Por favor, inténtelo de nuevo.',
Login_with: 'Acceder con',
Logout: 'Salir',
members: 'miembros',
Members: 'Miembros',
Mentioned_Messages: 'Mensajes mencionados',
mentioned: 'mencionado',
Mentions: 'Menciones',
Message_accessibility: 'Mensaje de {{user}} a las {{time}}: {{message}}',
Message_actions: 'Acciones de mensaje',
Message_pinned: 'Mensaje fijado',
Message_removed: 'Mensaje eliminado',
message: 'mensaje',
messages: 'mensajes',
Messages: 'Mensajes',
Message_Reported: 'Mensaje notificado',
Microphone_Permission_Message: 'Rocket Chat necesita acceso a su micrófono para que pueda enviar un mensaje de audio.',
Microphone_Permission: 'Permiso de micrófono',
Mute: 'Mutear',
muted: 'muteado',
My_servers: 'Mis servidores',
N_people_reacted: 'Han reaccionado {{n}} personas',
N_users: '{{n}} usuarios',
name: 'nombre',
Name: 'Nombre',
New_Message: 'Nuevo mensaje',
New_Password: 'Nueva contraseña',
New_Server: 'Nuevo servidor',
Next: 'Siguiente',
No_files: 'No hay archivos',
No_mentioned_messages: 'No hay mensajes mencionados',
No_pinned_messages: 'No hay mensajes fijados',
No_results_found: 'No hay resultados',
No_starred_messages: 'No hay mensajes destacados',
No_thread_messages: 'No hay hilots',
No_announcement_provided: 'No se ha indicado un anuncio',
No_description_provided: 'No se ha indicado descripción',
No_topic_provided: 'No se ha indicado asunto.',
No_Message: 'Sin mensajes',
No_messages_yet: 'No hay todavía mensajes',
No_Reactions: 'No hay reacciones',
No_Read_Receipts: 'No hay confirmaciones de lectura',
Not_logged: 'No logueado',
Not_RC_Server: 'Esto no es un servidor de Rocket.Chat.\n{{contact}}',
Nothing: 'Nada',
Nothing_to_save: 'No hay nada para guardar!',
Notify_active_in_this_room: 'Notificar usuarios activos en esta sala',
Notify_all_in_this_room: 'Notificar a todos en esta sala',
Notifications: 'Notificaciones',
Notification_Duration: 'Duración notificación',
Notification_Preferences: 'Configuración de notificaciones',
Offline: 'Sin conexión',
Oops: 'Oops!',
Online: 'Conectado',
Only_authorized_users_can_write_new_messages: 'Sólo pueden escribir mensajes usuarios autorizados',
Open_emoji_selector: 'Abrir selector de emojis',
Open_Source_Communication: 'Comunicación Open Source',
Password: 'Contraseña',
Permalink_copied_to_clipboard: 'Enlace permanente copiado al portapapeles!',
Pin: 'Fijar',
Pinned_Messages: 'Mensajes fijados',
pinned: 'fijado',
Pinned: 'Fijado',
Please_enter_your_password: 'Por favor introduce tu contraseña',
Preferences: 'Configuración',
Preferences_saved: 'Configuración guardada!',
Privacy_Policy: 'Política de Privacidad',
Private_Channel: 'Canal privado',
Private_Groups: 'Grupos privados',
Private: 'Privado',
Processing: 'Procesando...',
Profile_saved_successfully: 'Perfil guardado correctamente!',
Profile: 'Perfil',
Public_Channel: 'Canal público',
Public: 'Público',
PUSH_NOTIFICATIONS: 'PUSH NOTIFICATIONS',
Push_Notifications_Alert_Info: 'Estas notificaciones se le entregan cuando la aplicación no está abierta',
Quote: 'Citar',
Reactions_are_disabled: 'Las reacciones están desactivadas',
Reactions_are_enabled: 'Las reacciones están habilitadas',
Reactions: 'Reacciones',
Read: 'Leer',
Read_Only_Channel: 'Canal de sólo lectura',
Read_Only: 'Sólo lectura ',
Read_Receipt: 'Comprobante de lectura',
Receive_Group_Mentions: 'Recibir menciones de grupo',
Receive_Group_Mentions_Info: 'Recibir menciones @all y @here',
Register: 'Registrar',
Repeat_Password: 'Repetir contraseña',
Replied_on: 'Respondido el:',
replies: 'respuestas',
reply: 'respuesta',
Reply: 'Respuesta',
Report: 'Informe',
Receive_Notification: 'Recibir notificación',
Receive_notifications_from: 'Recibir notificación de {{name}}',
Resend: 'Reenviar',
Reset_password: 'Resetear contraseña',
resetting_password: 'reseteando contraseña',
RESET: 'RESET',
Roles: 'Roles',
Room_actions: 'Acciones de sala',
Room_changed_announcement: 'El anuncio de la sala cambió a: {{announcement}} por {{userBy}}',
Room_changed_description: 'La descripción de la sala cambió a: {{description}} por {{userBy}}',
Room_changed_privacy: 'El tipo de la sala cambió a: {{type}} por {{userBy}}',
Room_changed_topic: 'El asunto de la sala cambió a: {{topic}} por {{userBy}}',
Room_Files: 'Archivos',
Room_Info_Edit: 'Editar información de la sala',
Room_Info: 'Información de la sala',
Room_Members: 'Miembros de la sala',
Room_name_changed: 'El nombre de la sala cambió a: {{name}} por {{userBy}}',
SAVE: 'SAVE',
Save_Changes: 'Guardar cambios',
Save: 'Guardar',
saving_preferences: 'guardando preferencias',
saving_profile: 'guardando perfil',
saving_settings: 'guardando confiración',
Search_Messages: 'Buscar mensajes',
Search: 'Buscar',
Search_by: 'Buscar por',
Search_global_users: 'Buscar por usuarios globales',
Search_global_users_description: 'Si lo activas, puedes buscar cualquier usuario de otras empresas o servidores.',
Seconds: '{{second}} segundos',
Select_Avatar: 'Selecciona avatar',
Select_Server: 'Selecciona servidor',
Select_Users: 'Selecciona usuarios',
Send: 'Enviar',
Send_audio_message: 'Enviar nota de audio',
Send_crash_report: 'Enviar informe errores',
Send_message: 'Enviar mensaje',
Send_to: 'Enviar a..',
Sent_an_attachment: 'Enviar un adjunto',
Server: 'Servidor',
Servers: 'Servidores',
Server_version: 'Versión servidor: {{version}}',
Set_username_subtitle: 'El nombre de usuario se utiliza para permitir que otros le mencionen en los mensajes',
Settings: 'Configuración',
Settings_succesfully_changed: 'Configuración cambiada correctamente!',
Share: 'Compartir',
Share_this_app: 'Compartir esta App',
Show_Unread_Counter: 'Mostrar contador No leídos',
Show_Unread_Counter_Info: 'El contador de no leídos se muestra como una insignia a la derecha del canal, en la lista',
Sign_in_your_server: 'Accede a tu servidor',
Sign_Up: 'Acceder',
Some_field_is_invalid_or_empty: 'Algún campo es incorrecto o vacío',
Sorting_by: 'Ordenado por {{key}}',
Sound: 'Sonido',
Star_room: 'Destacar sala',
Star: 'Destacar',
Starred_Messages: 'Mensajes destacados',
starred: 'destacado',
Starred: 'Destacado',
Start_of_conversation: 'Comiezo de la conversación',
Started_discussion: 'Comenzar una conversación:',
Started_call: 'Llamada iniciada por {{userBy}}',
Submit: 'Enviar',
Table: 'Tabla',
Take_a_photo: 'Enviar Foto',
Take_a_video: 'Enviar Vídeo',
tap_to_change_status: 'pulsa para cambiar el estado',
Tap_to_view_servers_list: 'Pulsa para ver la lista de servidores',
Terms_of_Service: 'Términos de servicio',
Theme: 'Tema',
The_URL_is_invalid: 'URL inválida o no se puede establecer una conexión segura.\n{{contact}}',
There_was_an_error_while_action: 'Ha habido un error mientras {{action}}!',
This_room_is_blocked: 'La sala está bloqueada',
This_room_is_read_only: 'Esta sala es de sólo lectura',
Thread: 'Hilo',
Threads: 'Hilos',
Timezone: 'Zona horaria',
To: 'Para',
topic: 'asunto',
Topic: 'Asunto',
Translate: 'Traducir',
Try_again: 'Intentar de nuevo',
Two_Factor_Authentication: 'Autenticación de doble factor',
Type_the_channel_name_here: 'Escribe el nombre del canal aquí',
unarchive: 'reactivar',
UNARCHIVE: 'UNARCHIVE',
Unblock_user: 'Desbloquear usuario',
Unfavorite: 'Quitar Favorito',
Unfollowed_thread: 'Dejar de seguir el Hilo',
Unmute: 'Desmutear',
unmuted: 'Desmuteado',
Unpin: 'Quitar estado Fijado',
unread_messages: 'marcar como No leído',
Unread: 'Marcar como No leído',
Unread_on_top: 'Mensajes No leídos en la parte superior',
Unstar: 'Quitar Destacado',
Updating: 'Actualizando...',
Uploading: 'Subiendo',
Upload_file_question_mark: 'Subir fichero?',
Users: 'Usuarios',
User_added_by: 'Usuario {{userAdded}} añadido por {{userBy}}',
User_has_been_key: 'El usuario ha sido {{key}}!',
User_is_no_longer_role_by_: '{{user}} ha dejado de ser {{role}} por {{userBy}}',
User_muted_by: 'Usuario {{userMuted}} muteado por {{userBy}}',
User_removed_by: 'Usuario {{userRemoved}} eliminado por {{userBy}}',
User_sent_an_attachment: '{{user}} envío un adjunto',
User_unmuted_by: 'Usuario {{userUnmuted}} desmuteado por {{userBy}}',
User_was_set_role_by_: '{{user}} ha comenzado a ser {{role}} por {{userBy}}',
Username_is_empty: 'Nombre de usuario está vacío',
Username: 'Nombre de usuario',
Username_or_email: 'Nombre de usuario o email',
Validating: 'Validando',
Video_call: 'Vídeo llamada',
View_Original: 'Ver original',
Voice_call: 'Llamada de voz',
Websocket_disabled: 'Websocket está deshabilitado para este servidor.\n{{contact}}',
Welcome: 'Bienvenido',
Welcome_to_RocketChat: 'Bienvenido a Rocket.Chat',
Whats_your_2fa: '¿Cuál es tu código 2FA?',
Without_Servers: 'Sin servidores',
Yes_action_it: 'Sí, {{action}}!',
Yesterday: 'Ayer',
You_are_in_preview_mode: 'Estás en modo Vista Previa',
You_are_offline: 'Estás desconectado',
You_can_search_using_RegExp_eg: 'Puedes usar expresiones regulares. Por ejemplo, `/^text$/i`',
You_colon: 'Tú: ',
you_were_mentioned: 'has sido mencionado',
you: 'tú',
You: 'Tú',
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Necesita acceder al menos a un servidor Rocket.Chat para compartir algo.',
Your_certificate: 'Tu certificado',
Version_no: 'Versión: {{version}}',
You_will_not_be_able_to_recover_this_message: 'No podrás recuperar este mensaje!',
Change_Language: 'Cambiar idioma',
Crash_report_disclaimer: 'Nunca rastreamos el contenido de sus conversaciones. El informe del error sólo contiene información relevante para nosotros con el fin de identificar los problemas y solucionarlos.',
Type_message: 'Escribir mensaje',
Room_search: 'Búsqueda de salas',
Room_selection: 'Selecciona sala 1...9',
Next_room: 'Siguiente sala',
Previous_room: 'Sala anterior',
New_room: 'Nueva sala',
Upload_room: 'Subir a sala',
Search_messages: 'Buscar mensajes',
Scroll_messages: 'Scroll mensajes',
Reply_latest: 'Responder al último',
Server_selection: 'Seleccionar servidor',
Server_selection_numbers: 'Seleccionar servidor 1...9',
Add_server: 'Añadir servidor',
New_line: 'Nueva línea'
};

486
app/i18n/locales/it.js Normal file
View File

@ -0,0 +1,486 @@
export default {
'1_person_reacted': '1 persona ha reagito',
'1_user': '1 utente',
'error-action-not-allowed': '{{action}} non autorizzata',
'error-application-not-found': 'Applicazione non trovata',
'error-archived-duplicate-name': 'Esiste già un canale archiviato con nome {{room_name}}',
'error-avatar-invalid-url': 'URL avatar non valido: {{url}}',
'error-avatar-url-handling': 'Errore nella gestione dell\'impostazione avatar dall\'URL ({{url}}) per {{username}}',
'error-cant-invite-for-direct-room': 'Impossibile invitare l\'utente alle stanze dirette',
'error-could-not-change-email': 'Impossibile cambiare l\'indirizzo e-mail',
'error-could-not-change-name': 'Impossibile cambiare nome',
'error-could-not-change-username': 'Impossibile cambiare username',
'error-delete-protected-role': 'Impossibile eliminare un ruolo protetto',
'error-department-not-found': 'Reparto non trovato',
'error-direct-message-file-upload-not-allowed': 'La condivisione di file non è autorizzata nei messaggi diretti',
'error-duplicate-channel-name': 'Esiste già un canale con nome {{channel_name}}',
'error-email-domain-blacklisted': 'Il dominio e-mail è nella lista nera',
'error-email-send-failed': 'Errore nel tentativo di invio e-mail: {{message}}',
'error-save-image': 'Errore nel salvataggio dell\'immagine',
'error-field-unavailable': '{{field}} è già in uso :(',
'error-file-too-large': 'File troppo grande',
'error-importer-not-defined': 'L\'importatore non è stato definito correttamente: classe Import mancante.',
'error-input-is-not-a-valid-field': '{{input}} non è valido come {{field}}',
'error-invalid-actionlink': 'Link azione non valido',
'error-invalid-arguments': 'Parametri non validi',
'error-invalid-asset': 'Asset non valido',
'error-invalid-channel': 'Canale non valido.',
'error-invalid-channel-start-with-chars': 'Canale non valido. Inizia con @ o #',
'error-invalid-custom-field': 'Campo personalizzato non valido',
'error-invalid-custom-field-name': 'Nome campo personalizzato non valido. Usa solo lettere, numeri, trattini e underscore.',
'error-invalid-date': 'Data fornita non valida.',
'error-invalid-description': 'Descrizione non valida',
'error-invalid-domain': 'Dominio non valido',
'error-invalid-email': 'E-mail {{emai}} non valida',
'error-invalid-email-address': 'Indirizzo e-mail non valido',
'error-invalid-file-height': 'Altezza del file non valida',
'error-invalid-file-type': 'Tipo di file non valido',
'error-invalid-file-width': 'Larghezza del file non valida',
'error-invalid-from-address': 'Hai informato un indirizzo FROM non valido.',
'error-invalid-integration': 'Integrazione non valida',
'error-invalid-message': 'Messaggio non valido',
'error-invalid-method': 'Metodo o funzione non valida',
'error-invalid-name': 'Nome non corretto',
'error-invalid-password': 'Password non corretta',
'error-invalid-redirectUri': 'redirectUri non valido',
'error-invalid-role': 'Ruolo non valido',
'error-invalid-room': 'Stanza non valida',
'error-invalid-room-name': '{{room_name}} non è un nome di stanza valido',
'error-invalid-room-type': '{{type}} non è un tipo di stanza valido',
'error-invalid-settings': 'Impostazioni fornite non valide',
'error-invalid-subscription': 'Iscrizione non valida',
'error-invalid-token': 'Token non valido',
'error-invalid-triggerWords': 'triggerWords non valide',
'error-invalid-urls': 'URL non validi',
'error-invalid-user': 'Utente non valido',
'error-invalid-username': 'Nome utente non valido',
'error-invalid-webhook-response': 'L\'URL del webhook ha risposto con uno stato diverso da 200',
'error-message-deleting-blocked': 'Cancellazione di messaggi bloccata',
'error-message-editing-blocked': 'Modifica di messaggi bloccata',
'error-message-size-exceeded': 'La dimensione del messaggio supera Message_MaxAllowedSize',
'error-missing-unsubscribe-link': 'Devi fornire il link [unsubscribe].',
'error-no-tokens-for-this-user': 'Non ci sono token per questo utente',
'error-not-allowed': 'Non permesso',
'error-not-authorized': 'Non autorizzato',
'error-push-disabled': 'Push è disabilitato',
'error-remove-last-owner': 'Questo è l\'ultimo proprietario rimasto. Imposta un nuovo proprietario prima di rimuoverlo.',
'error-role-in-use': 'Impossibile eliminare il ruolo perchè ancora in uso',
'error-role-name-required': 'Il nome del ruolo è obbligatorio',
'error-the-field-is-required': 'Il campo {{field}} è obbligatorio.',
'error-too-many-requests': 'Errore, troppe richieste effettuate. Rallenta. Devi attendere {{seconds}} secondi prima di riprovare.',
'error-user-is-not-activated': 'L\'utente non è attivato',
'error-user-has-no-roles': 'L\'utente non ha ruoli',
'error-user-limit-exceeded': 'Il numero di utenti che stai invitando in #channel_name supera il limite imposto dall\'amministratore',
'error-user-not-in-room': 'L\'utente non è in questa stanza',
'error-user-registration-custom-field': 'error-user-registration-custom-field',
'error-user-registration-disabled': 'Registrazione utente disabilitata',
'error-user-registration-secret': 'Registrazione utente permessa solo via URL segreto',
'error-you-are-last-owner': 'Sei l\'ultimo proprietario rimasto. Imposta un nuovo proprietario prima di lasciare la stanza.',
Actions: 'Azioni',
activity: 'attività',
Activity: 'Attività',
Add_Reaction: 'Aggiungi reazione',
Add_Server: 'Aggiungi server',
Add_users: 'Aggiungi utenti',
Admin_Panel: 'Amministrazione',
Alert: 'Avviso',
alert: 'avviso',
alerts: 'avvisi',
All_users_in_the_channel_can_write_new_messages: 'Tutti gli utenti nel canale possono scrivere messaggi',
All: 'Tutti',
All_Messages: 'Tutti i messaggi',
Allow_Reactions: 'Permetti reazioni',
Alphabetical: 'Alfabetico',
and_more: 'e altro',
and: 'e',
announcement: 'annuncio',
Announcement: 'Annuncio',
Apply_Your_Certificate: 'Applica il tuo certificato',
Applying_a_theme_will_change_how_the_app_looks: 'Applicare un tema cambierà l\'aspetto dell\'app.',
ARCHIVE: 'ARCHIVIO',
archive: 'archivio',
are_typing: 'stanno scrivendo',
Are_you_sure_question_mark: 'Sei sicuro?',
Are_you_sure_you_want_to_leave_the_room: 'Sei sicuro di voler lasciare la stanza {{room}}?',
Audio: 'Audio',
Authenticating: 'Autenticazione',
Automatic: 'Automatico',
Auto_Translate: 'Traduzione automatica',
Avatar_changed_successfully: 'Avatar aggiornato correttamente!',
Avatar_Url: 'URL avatar',
Away: 'Assente',
Back: 'Indietro',
Black: 'Nero',
Block_user: 'Blocca utente',
Broadcast_channel_Description: 'Solo gli utenti autorizzati possono scrivere messaggi, ma gli altri utenti saranno in grado di rispondere',
Broadcast_Channel: 'Canale broadcast',
Busy: 'Occupato',
By_proceeding_you_are_agreeing: 'Procedendo accetti i nostri',
Cancel_editing: 'Annulla modifica',
Cancel_recording: 'Annulla registrazione',
Cancel: 'Annulla',
changing_avatar: 'cambio avatar',
creating_channel: 'creo canale',
creating_invite: 'creo invito',
Channel_Name: 'Nome canale',
Channels: 'Canali',
Chats: 'Chat',
Call_already_ended: 'Chiamata già terminata!',
Click_to_join: 'Clicca per unirti!',
Close: 'Chiudi',
Close_emoji_selector: 'Chiudi selettore emoji',
Choose: 'Scegli',
Choose_from_library: 'Scegli dalla libreria',
Choose_file: 'Scegli file',
Code: 'Codice',
Collaborative: 'Collaborativo',
Confirm: 'Conferma',
Connect: 'Connetti',
Connect_to_a_server: 'Connetti ad un server',
Connected: 'Connesso',
connecting_server: 'connessione al server',
Connecting: 'Connessione...',
Contact_us: 'Contattaci',
Contact_your_server_admin: 'Contatta l\'amministratore.',
Continue_with: 'Continua con',
Copied_to_clipboard: 'Copiato in Appunti!',
Copy: 'Copia',
Permalink: 'Permalink',
Certificate_password: 'Password certificato',
Whats_the_password_for_your_certificate: 'Qual\'è la password del tuo certificato?',
Create_account: 'Crea un account',
Create_Channel: 'Crea canale',
Created_snippet: 'Snippet creato',
Create_a_new_workspace: 'Crea un nuovo workspace',
Create: 'Crea',
Dark: 'Scuro',
Dark_level: 'Contrasto',
Default: 'Predefinito',
Delete_Room_Warning: 'Eliminare una stanza cancellerà tutti i messaggi in essa contenuti. Questa azione non può essere annullata.',
delete: 'elimina',
Delete: 'Elimina',
DELETE: 'ELIMINA',
description: 'descrizione',
Description: 'Descrizione',
DESKTOP_OPTIONS: 'OPZIONI DESKTOP',
Directory: 'Rubrica',
Direct_Messages: 'Messaggi diretti',
Disable_notifications: 'Disabilita notifiche',
Discussions: 'Discussioni',
Dont_Have_An_Account: 'Non hai un account?',
Do_you_have_a_certificate: 'Hai un certificato?',
Do_you_really_want_to_key_this_room_question_mark: 'Sei sicuro di voler {{key}} questa stanza?',
edit: 'modifica',
edited: 'modificato',
Edit: 'Modifica',
Edit_Invite: 'Modifica invito',
Email_or_password_field_is_empty: 'Il campo e-mail o password sono vuoti',
Email: 'E-mail',
EMAIL: 'E-MAIL',
email: 'e-mail',
Enable_Auto_Translate: 'Abilita traduzione automatica',
Enable_markdown: 'Abilita Markdown',
Enable_notifications: 'Abilita notifiche',
Everyone_can_access_this_channel: 'Tutti hanno accesso a questo canale',
erasing_room: 'cancellazione stanza',
Error_uploading: 'Errore nel caricamento di',
Expiration_Days: 'Scadenza (giorni)',
Favorite: 'Preferito',
Favorites: 'Preferiti',
Files: 'File',
File_description: 'Descrizione file',
File_name: 'Nome file',
Finish_recording: 'Termina registrazione',
Following_thread: 'Thread seguito',
For_your_security_you_must_enter_your_current_password_to_continue: 'Per garantire la sicurezza del tuo account, inserisci la password per continuare.',
Forgot_my_password: 'Ho dimenticato la password',
Forgot_password_If_this_email_is_registered: 'Se questa e-mail è registrata, manderemo istruzioni su come resettare la tua password. Se non ricevi nulla, torna qui e riprova di nuovo.',
Forgot_password: 'Password dimenticata',
Forgot_Password: 'Password dimenticata',
Full_table: 'Clicca per la tabella completa',
Generate_New_Link: 'Genera nuovo link',
Group_by_favorites: 'Raggruppa per preferiti',
Group_by_type: 'Raggruppa per tipo',
Hide: 'Nascondi',
Has_joined_the_channel: 'si è unito al canale',
Has_joined_the_conversation: 'si è unito alla conversazione',
Has_left_the_channel: 'ha lasciato il canale',
IN_APP_AND_DESKTOP: 'IN-APP E DESKTOP',
In_App_and_Desktop_Alert_info: 'Mostra una notifica in cima allo schermo quando l\'app è aperta, e mostra una notifica sul desktop',
Invisible: 'Invisibile',
Invite: 'Invita',
is_a_valid_RocketChat_instance: 'è un\'istanza di Rocket.Chat valida',
is_not_a_valid_RocketChat_instance: 'non è una valida istanza di Rocket.Chat',
is_typing: 'sta scrivendo',
Invalid_or_expired_invite_token: 'Token di invito non valido o scaduto',
Invalid_server_version: 'Il server a cui stai cercando di connetterti sta utilizzando una versione non più supportata dall\'app: {{currentVersion}}.\n\nVersione minima richiesta: {{minVersion}}',
Invite_Link: 'Link di invito',
Invite_users: 'Invita utenti',
Join_the_community: 'Unisciti alla community',
Join: 'Entra',
Just_invited_people_can_access_this_channel: 'Solo le persone invitate possono accedere a questo canale',
Language: 'Lingua',
last_message: 'ultimo messaggio',
Leave_channel: 'Abbandona canale',
leaving_room: 'abbandonando stanza',
leave: 'abbandona',
Legal: 'Informazioni',
Light: 'Chiaro',
License: 'Licenza',
Livechat: 'Livechat',
Login: 'Accedi',
Login_error: 'Le tue credenziali sono state rifiutate! Prova di nuovo.',
Login_with: 'Accedi con',
Logout: 'Disconnetti',
Max_number_of_uses: 'Max numero di utilizzi',
members: 'membri',
Members: 'Membri',
Mentioned_Messages: 'Messaggi menzionati',
mentioned: 'menzionato',
Mentions: 'Menzioni',
Message_accessibility: 'Messaggio da {{user}} alle {{time}}: {{message}}',
Message_actions: 'Azioni messaggio',
Message_pinned: 'Messaggio attaccato',
Message_removed: 'Messaggio rimosso',
message: 'messaggio',
messages: 'messaggi',
Messages: 'Messaggi',
Message_Reported: 'Messaggio segnalato',
Microphone_Permission_Message: 'Rocket.Chat richiede l\'accesso al microfono per inviare messaggi audio.',
Microphone_Permission: 'Permesso microfono',
Mute: 'Silenzia',
muted: 'silenziato',
My_servers: 'I miei server',
N_people_reacted: '{{n}} persone hanno reagito',
N_users: '{{n}} utenti',
name: 'nome',
Name: 'Nome',
Never: 'Mai',
New_Message: 'Nuovo messaggio',
New_Password: 'Nuova password',
New_Server: 'Nuovo server',
Next: 'Successivo',
No_files: 'Nessun file',
No_limit: 'Nessun limite',
No_mentioned_messages: 'Nessun messaggio menzionato',
No_pinned_messages: 'Nessun messaggio attaccato',
No_results_found: 'Nessun risultato',
No_starred_messages: 'Nessun messaggio preferito',
No_thread_messages: 'Nessun messaggio thread',
No_announcement_provided: 'Nessun annuncio inserito.',
No_description_provided: 'Nessuna descrizione inserita.',
No_topic_provided: 'Nessun argomento inserito.',
No_Message: 'Nessun messaggio',
No_messages_yet: 'Non ci sono ancora messaggi',
No_Reactions: 'Nessuna reazione',
No_Read_Receipts: 'Nessuna conferma di lettura',
Not_logged: 'Non loggato',
Not_RC_Server: 'Questo non è un server di Rocket.Chat.\n{{contact}}',
Nothing: 'Niente',
Nothing_to_save: 'Niente da salvare!',
Notify_active_in_this_room: 'Notifica solo gli utenti attivi in questa stanza',
Notify_all_in_this_room: 'Notifica tutti gli utenti in questa stanza',
Notifications: 'Notifiche',
Notification_Duration: 'Durata notifiche',
Notification_Preferences: 'Impostazioni notifiche',
Offline: 'Offline',
Oops: 'Oops!',
Online: 'Online',
Only_authorized_users_can_write_new_messages: 'Solo gli utenti autorizzati possono scrivere nuovi messaggi',
Open_emoji_selector: 'Apri selettore emoji',
Open_Source_Communication: 'Comunicazione open-source',
Password: 'Password',
Permalink_copied_to_clipboard: 'Permalink copiato negli appunti!',
Pin: 'Attacca',
Pinned_Messages: 'Messaggi attaccati',
pinned: 'attaccato',
Pinned: 'Attaccati',
Please_enter_your_password: 'Per favore, inserisci la tua password',
Preferences: 'Impostazioni',
Preferences_saved: 'Impostazioni salvate!',
Privacy_Policy: ' Privacy Policy',
Private_Channel: 'Canale privato',
Private_Groups: 'Gruppi privati',
Private: 'Privato',
Processing: 'Elaborazione...',
Profile_saved_successfully: 'Profilo salvato correttamente!',
Profile: 'Profilo',
Public_Channel: 'Canale pubblico',
Public: 'Pubblico',
PUSH_NOTIFICATIONS: 'NOTIFICHE PUSH',
Push_Notifications_Alert_Info: 'Queste notifiche ti vengono recapitate quando l\'app non è aperta',
Quote: 'Cita',
Reactions_are_disabled: 'Le reazioni sono disabilitate',
Reactions_are_enabled: 'Le reazioni sono abilitate',
Reactions: 'Reazioni',
Read: 'Letto',
Read_Only_Channel: 'Canale in sola lettura',
Read_Only: 'Sola lettura',
Read_Receipt: 'Conferma di lettura',
Receive_Group_Mentions: 'Ricevi menzioni di gruppo',
Receive_Group_Mentions_Info: 'Ricevi menzioni @all e @here',
Register: 'Registrati',
Repeat_Password: 'Conferma password',
Replied_on: 'Risposto il:',
replies: 'risposte',
reply: 'risposta',
Reply: 'Rispondi',
Report: 'Segnala',
Receive_Notification: 'Ricevi notifiche',
Receive_notifications_from: 'Ricevi notifiche da {{name}}',
Resend: 'Invia di nuovo',
Reset_password: 'Ripristina password',
resetting_password: 'ripristinando password',
RESET: 'RIPRISTINA',
Roles: 'Ruoli',
Room_actions: 'Azioni stanza',
Room_changed_announcement: 'Annuncio stanza cambiato in: {{announcement}} da {{userBy}}',
Room_changed_description: 'Descrizione stanza cambiata in: {{description}} da {{userBy}}',
Room_changed_privacy: 'Tipo stanza cambiato in: {{type}} da {{userBy}}',
Room_changed_topic: 'Argomento stanza cambiato in: {{topic}} da {{userBy}}',
Room_Files: 'File stanza',
Room_Info_Edit: 'Modifica informazioni stanza',
Room_Info: 'Informazioni stanza',
Room_Members: 'Membri stanza',
Room_name_changed: 'Nome stanza cambiato in: {{name}} da {{userBy}}',
SAVE: 'SALVA',
Save_Changes: 'Salva cambiamenti',
Save: 'Salva',
saving_preferences: 'salvataggio impostazioni',
saving_profile: 'salvataggio profilo',
saving_settings: 'salvataggio impostazioni',
saved_to_gallery: 'Salvato in Galleria',
Search_Messages: 'Cerca messaggi',
Search: 'Cerca',
Search_by: 'Cerca per',
Search_global_users: 'Cerca utenti globali',
Search_global_users_description: 'Se attivi questa opzione, puoi cercare qualsiasi utente da altre aziende o server.',
Seconds: '{{second}} secondi',
Select_Avatar: 'Seleziona avatar',
Select_Server: 'Seleziona server',
Select_Users: 'Seleziona utenti',
Send: 'Invia',
Send_audio_message: 'Invia messaggio audio',
Send_crash_report: 'Invia crash report',
Send_message: 'Invia messaggio',
Send_to: 'Invia a...',
Sent_an_attachment: 'Inviato un allegato',
Server: 'Server',
Servers: 'Servers',
Server_version: 'Versione server: {{version}}',
Set_username_subtitle: 'Il nome utente viene utilizzato per permettere ad altri di menzionarti nei messaggi',
Settings: 'Impostazioni',
Settings_succesfully_changed: 'Impostazioni modificate correttamente!',
Share: 'Condividi',
Share_Link: 'Condividi link',
Share_this_app: 'Condividi questa app',
Show_Unread_Counter: 'Mostra contatore messaggi non letti',
Show_Unread_Counter_Info: 'Il contatore viene mostrato come un\'etichetta alla destra del canale, nella lista',
Sign_in_your_server: 'Accedi al tuo server',
Sign_Up: 'Registrati',
Some_field_is_invalid_or_empty: 'Un campo non è valido o è vuoto',
Sorting_by: 'Ordina per {{key}}',
Sound: 'Suono',
Star_room: 'Aggiungi stanza ai preferiti',
Star: 'Aggiungi ai preferiti',
Starred_Messages: 'Messaggi preferiti',
starred: 'preferiti',
Starred: 'Preferiti',
Start_of_conversation: 'Inizio della conversazione',
Started_discussion: 'Discussione iniziata:',
Started_call: 'Chiamata iniziata da {{userBy}}',
Submit: 'Invia',
Table: 'Tabella',
Take_a_photo: 'Scatta una foto',
Take_a_video: 'Registra un video',
tap_to_change_status: 'tocca per cambiare stato',
Tap_to_view_servers_list: 'Tocca per vedere la lista server',
Terms_of_Service: ' Termini di servizio ',
Theme: 'Tema',
The_URL_is_invalid: 'URL non valido o errore nello stabilimento di una connessione sicura.\n{{contact}}',
There_was_an_error_while_action: 'Si è verificato un errore nel {{action}}!',
This_room_is_blocked: 'Questa stanza è bloccata',
This_room_is_read_only: 'Questa stanza è in sola lettura',
Thread: 'Thread',
Threads: 'Threads',
Timezone: 'Fuso orario',
To: 'A',
topic: 'argomento',
Topic: 'Argomento',
Translate: 'Traduci',
Try_again: 'Riprova',
Two_Factor_Authentication: 'Autenticazione a due fattori',
Type_the_channel_name_here: 'Scrivi il nome del canale qui',
unarchive: 'rimuovi dall\'archivio',
UNARCHIVE: 'RIMUOVI DALL\'ARCHIVIO',
Unblock_user: 'Sblocca utente',
Unfavorite: 'Rimuovi dai preferiti',
Unfollowed_thread: 'Non segui più il thread',
Unmute: 'Attiva notifiche',
unmuted: 'notifiche attivate',
Unpin: 'Stacca',
unread_messages: 'non letti',
Unread: 'Non letto',
Unread_on_top: 'Non letti sopra',
Unstar: 'Rimuovi dai preferiti',
Updating: 'Aggiornamento...',
Uploading: 'Caricamento',
Upload_file_question_mark: 'Carica file?',
Users: 'Utenti',
User_added_by: 'Utente {{userAdded}} aggiunto da {{userBy}}',
User_Info: 'Informazioni utente',
User_has_been_key: 'Utente {{key}}!',
User_is_no_longer_role_by_: '{{user}} non è più {{role}} da {{userBy}}',
User_muted_by: 'Utente {{userMuted}} silenziato da {{userBy}}',
User_removed_by: 'Utente {{userRemoved}} rimosso da {{userBy}}',
User_sent_an_attachment: '{{user}} ha inviato un allegato',
User_unmuted_by: 'Utente {{userUnmuted}} de-silenziato da {{userBy}}',
User_was_set_role_by_: '{{user}} è stato impostato come {{role}} da {{userBy}}',
Username_is_empty: 'Username vuoto',
Username: 'Username',
Username_or_email: 'Username o email',
Validating: 'Validazione',
Video_call: 'Videochiamata',
View_Original: 'Mostra originale',
Voice_call: 'Chiamata vocale',
Websocket_disabled: 'Websocket è disabilitato per questo server.\n{{contact}}',
Welcome: 'Benvenuto',
Welcome_to_RocketChat: 'Benvenuto in Rocket.Chat',
Whats_your_2fa: 'Qual\'è il tuo codice 2FA?',
Without_Servers: 'Senza server',
Write_External_Permission_Message: 'Rocket.Chat ha bisogno dell\'accesso alla galleria per salvare le immagini.',
Write_External_Permission: 'Permesso galleria',
Yes_action_it: 'Sì, {{action}}!',
Yesterday: 'Ieri',
You_are_in_preview_mode: 'Sei in modalità anteprima',
You_are_offline: 'Sei offline',
You_can_search_using_RegExp_eg: 'Puoi usare espressioni regolari. es. `/^testo$/i`',
You_colon: 'Tu: ',
you_were_mentioned: 'sei stato menzionato',
you: 'tu',
You: 'Tu',
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Devi accedere ad almeno un server Rocket.Chat prima di condividere qualcosa.',
Your_certificate: 'Il tuo certificato',
Your_invite_link_will_expire_after__usesLeft__uses: 'Il tuo link di invito scadrà dopo {{usesLeft}} utilizzi.',
Your_invite_link_will_expire_on__date__or_after__usesLeft__uses: 'Il tuo link di invito scadrà il {{date}} oppure dopo {{usesLeft}} utilizzi.',
Your_invite_link_will_expire_on__date__: 'Il tuo link di invito scadrà il {{date}}.',
Your_invite_link_will_never_expire: 'Il tuo link di invito non scadrà mai.',
Version_no: 'Versione: {{version}}',
You_will_not_be_able_to_recover_this_message: 'Non sarai in grado di ripristinare questo messaggio!',
Change_Language: 'Cambia lingua',
Crash_report_disclaimer: 'Non registreremo mai il contenuto delle tue chat. Il crash report contiene solo informazioni necessarie per l\'identificazione e la risoluzione dei problemi.',
Type_message: 'Scrivi messaggio',
Room_search: 'Ricerca stanze',
Room_selection: 'Selezione stanza 1...9',
Next_room: 'Prossima stanza',
Previous_room: 'Stanza precedente',
New_room: 'Nuova stanza',
Upload_room: 'Carica nella stanza',
Search_messages: 'Cerca messaggi',
Scroll_messages: 'Scroll messaggi',
Reply_latest: 'Rispondi all\'ultimo',
Server_selection: 'Selezione server',
Server_selection_numbers: 'Selezione server 1...9',
Add_server: 'Aggiungi server',
New_line: 'Nuova linea'
};

493
app/i18n/locales/nl.js Normal file
View File

@ -0,0 +1,493 @@
export default {
'1_person_reacted': '1 persoon heeft gereageerd',
'1_user': '1 gebruiker',
'error-action-not-allowed': '{{actie}} is niet toegestaan',
'error-application-not-found': 'Applicatie niet gevonden',
'error-archived-duplicate-name': 'Er is een gearchiveerd kanaal met de naam {{room_name}}',
'error-avatar-invalid-url': 'Foutieve avatar URL: {{url}}',
'error-avatar-url-handling': 'Fout tijdens verwerken avatar instellingen vanaf een URL({{url}}) for {{username}}',
'error-cant-invite-for-direct-room': 'Kan gebruikers niet in directe kamers toevoegen',
'error-could-not-change-email': 'Kon email niet veranderen',
'error-could-not-change-name': 'Kon naam niet veranderen',
'error-could-not-change-username': 'Kon gebruikersnaam niet veranderen',
'error-delete-protected-role': 'Beveiligde rollen kunnen niet verwijderd worden.',
'error-department-not-found': 'Afdeling niet gevonden',
'error-direct-message-file-upload-not-allowed': 'Delen van bestanden niet toegestaan in directe berichten',
'error-duplicate-channel-name': 'Een kanaal met de naam {{channel_name}} bestaat',
'error-email-domain-blacklisted': 'Het email domein is blacklisted',
'error-email-send-failed': 'Fout tijdens verzenden van email: {{message}}',
'error-save-image': 'Fout tijdens opslaan afbeelding',
'error-field-unavailable': '{{field}} is alr in gebruik :(',
'error-file-too-large': 'Bestand is te groot',
'error-importer-not-defined': 'De importer is niet goed gedefinieerd, het mist de Import class.',
'error-input-is-not-a-valid-field': '{{input}} is geen geldig {{field}}',
'error-invalid-actionlink': 'Ongeldige action link',
'error-invalid-arguments': 'Ongeldige argumenten',
'error-invalid-asset': 'Ongeldig asset',
'error-invalid-channel': 'Ongeldig channel.',
'error-invalid-channel-start-with-chars': 'Ongeldig channel. Begin met @ of #',
'error-invalid-custom-field': 'Ongeldig custom veld',
'error-invalid-custom-field-name': 'Ongeldige custom veld naam. Gebruik alleen letters, cijfers, - of _.',
'error-invalid-date': 'Ongeldige datum opgegeven.',
'error-invalid-description': 'Ongeldige beschrijving',
'error-invalid-domain': 'Ongeldig domein',
'error-invalid-email': 'Ongeldige email {{emai}}',
'error-invalid-email-address': 'Ongeldig emailadres',
'error-invalid-file-height': 'Ongeldige file height',
'error-invalid-file-type': 'Ongeldig bestandstype',
'error-invalid-file-width': 'Ongeldige file width',
'error-invalid-from-address': 'Een ongeldig FROM adres is ingevuld.',
'error-invalid-integration': 'Ongeldige integration',
'error-invalid-message': 'Ongeldige message',
'error-invalid-method': 'Ongeldige method',
'error-invalid-name': 'Ongeldige naam',
'error-invalid-password': 'Ongeldig password',
'error-invalid-redirectUri': 'Ongeldige redirectUri',
'error-invalid-role': 'Ongeldige role',
'error-invalid-room': 'Ongeldige kamer',
'error-invalid-room-name': '{{room_name}} is geen geldige kamernaam',
'error-invalid-room-type': '{{type}} is geen geldig kamertype.',
'error-invalid-settings': 'Ongeldige instellingen ingevuld',
'error-invalid-subscription': 'Ongeldige subscription',
'error-invalid-token': 'Ongeldig token',
'error-invalid-triggerWords': 'Ongeldige triggerWords',
'error-invalid-urls': 'Ongeldige URLs',
'error-invalid-user': 'Ongeldige user',
'error-invalid-username': 'Ongeldige username',
'error-invalid-webhook-response': 'De webhook URL antwoorde met een andere status dan 200',
'error-message-deleting-blocked': 'Berichten verwijderen is geblokkeerd.',
'error-message-editing-blocked': 'Berichten aanpassen is geblokkeerd.',
'error-message-size-exceeded': 'Berichtgrootte is meer dan Message_MaxAllowedSize',
'error-missing-unsubscribe-link': 'De [unsubscribe] link moet gegeven worden.',
'error-no-tokens-for-this-user': 'Er zijn geen tokens voor deze user',
'error-not-allowed': 'Niet toegestaan',
'error-not-authorized': 'Niet gemachtigd',
'error-push-disabled': 'Push staat uit',
'error-remove-last-owner': 'Dit is de laatste eigenaar. Kies een nieuwe eigenaar voor je deze verwijderd.',
'error-role-in-use': 'Kan rol niet verwijderen omdat hij in gebruik is',
'error-role-name-required': 'Rol naam verplicht',
'error-the-field-is-required': 'Het veld {{field}} is verplicht.',
'error-too-many-requests': 'Error, te veel requests. Doe alsjeblieft rustig aan. Je moet {{seconds}} wachten voor je het opnieuw kan proberen.',
'error-user-is-not-activated': 'Gebruiker is niet geactiveerd',
'error-user-has-no-roles': 'Gebruiker heeft geen rollen',
'error-user-limit-exceeded': 'De hoeveelheid gebruikers die je probeert uit te nodigen voor #channel_name is meer dan het limiet wat de admin gekozen heeft',
'error-user-not-in-room': 'Gebruiker is niet in deze kamer',
'error-user-registration-custom-field': 'error-user-registration-custom-field',
'error-user-registration-disabled': 'Registratie van gebruikers staat uit',
'error-user-registration-secret': 'Registratie van gebruikers kan alleen via Secret URL',
'error-you-are-last-owner': 'Je bent de laatste eigenaar. Kies eerst een nieuwe voor je de kamer verlaat.',
Actions: 'Acties',
activity: 'activiteit',
Activity: 'Activiteit',
Add_Reaction: 'Voeg reactie toe',
Add_Server: 'Voeg server toe',
Add_users: 'Voeg gebruikers toe',
Admin_Panel: 'Admin Paneel',
Alert: 'Alert',
alert: 'alert',
alerts: 'alerts',
All_users_in_the_channel_can_write_new_messages: 'Alle gebruikers in het kanaal kunnen nieuwe berichten sturen',
All: 'Alle',
All_Messages: 'Alle Berichten',
Allow_Reactions: 'Sta reacties toe',
Alphabetical: 'Alfabetisch',
and_more: 'en meer',
and: 'en',
announcement: 'aankondiging',
Announcement: 'Aankondiging',
Apply_Your_Certificate: 'Gebruik je certificaat',
Applying_a_theme_will_change_how_the_app_looks: 'Een thema toepassen verandert de looks van de app.',
ARCHIVE: 'ARCHIVEER',
archive: 'archiveer',
are_typing: 'zijn aan het typen',
Are_you_sure_question_mark: 'Weet je het zeker?',
Are_you_sure_you_want_to_leave_the_room: 'Weet je zeker dat je de kamer {{room}} wil verlaten?',
Audio: 'Audio',
Authenticating: 'Authenticating',
Automatic: 'Automatisch',
Auto_Translate: 'Auto-Vertalen',
Avatar_changed_successfully: 'Avatar succesvol aangepast!',
Avatar_Url: 'Avatar URL',
Away: 'Weg',
Back: 'Terug',
Black: 'Zwart',
Block_user: 'Blokkeer gebruiker',
Broadcast_channel_Description: 'Alleen toegestane gebruikers kunnen nieuwe berichten sturen, maar iedereen kan reageren',
Broadcast_Channel: 'Broadcast Kanaal',
Busy: 'Bezig',
By_proceeding_you_are_agreeing: 'Door verder te gaan ga je akkoord met onze',
Cancel_editing: 'Stop bewerken',
Cancel_recording: 'Stop opnemen',
Cancel: 'Stop',
changing_avatar: 'avatar aan het veranderen',
creating_channel: 'kanaal aan het maken',
creating_invite: 'uitnodiging maken',
Channel_Name: 'Kanaal Name',
Channels: 'Kanalen',
Chats: 'Chats',
Call_already_ended: 'Gesprek al beeïndigd!',
Click_to_join: 'Klik om lid te worden!',
Close: 'Sluiten',
Close_emoji_selector: 'Sluit emoji selector',
Choose: 'Kies',
Choose_from_library: 'Kies uit bibliotheek',
Choose_file: 'Kies bestand',
Code: 'Code',
Collaborative: 'Samenwerkend',
Confirm: 'Bevestig',
Connect: 'Verbind',
Connect_to_a_server: 'Verbind met een server',
Connected: 'Verbonden',
connecting_server: 'Verbonden met een server',
Connecting: 'Aan het verbinden...',
Contact_us: 'Contact opnemen',
Contact_your_server_admin: 'Neem contact op met je server admin.',
Continue_with: 'Ga verder met',
Copied_to_clipboard: 'Gekopïeerd naar klembord!',
Copy: 'Kopïeer',
Permalink: 'Permalink',
Certificate_password: 'Certificate Password',
Whats_the_password_for_your_certificate: 'Wat is het wachtwoord voor je certificate?',
Create_account: 'Maak een account',
Create_Channel: 'Maak een kanaal',
Created_snippet: 'Snippet gemaakt',
Create_a_new_workspace: 'Een nieuwe workspace maken',
Create: 'Maken',
Dark: 'Donker',
Dark_level: 'Donker niveau',
Default: 'Standaard',
Delete_Room_Warning: 'Een kamer verwijderen verwijdert alle berichten erin. Dit kan niet ongedaan gemaakt worden.',
delete: 'delete',
Delete: 'Delete',
DELETE: 'DELETE',
description: 'beschrijving',
Description: 'Beschrijving',
DESKTOP_OPTIONS: 'DESKTOP OPTIES',
Directory: 'Map',
Direct_Messages: 'Directe berichten',
Disable_notifications: 'Zet notificaties uit',
Discussions: 'Discussies',
Dont_Have_An_Account: 'Heb je geen account?',
Do_you_have_a_certificate: 'Heb je een certificate?',
Do_you_really_want_to_key_this_room_question_mark: 'Wil je deze kamer echt {{key}}?',
edit: 'bewerk',
edited: 'bewerkt',
Edit: 'Bewerk',
Edit_Invite: 'Bewerk uitnodiging',
Email_or_password_field_is_empty: 'Email of wachtwoord veld is leeg',
Email: 'Email',
EMAIL: 'EMAIL',
email: 'e-mail',
Enable_Auto_Translate: 'Zet Auto-Translate aan',
Enable_markdown: 'Zet markdown aan',
Enable_notifications: 'Zet notifications aan',
Everyone_can_access_this_channel: 'Iedereen kan bij dit kanaal',
erasing_room: 'kamer legen',
Error_uploading: 'Error tijdens uploaden',
Expiration_Days: 'Vervalt in (Dagen)',
Favorite: 'Favoriet',
Favorites: 'Favorieten',
Files: 'Bestanden',
File_description: 'Bestandsbeschrijving',
File_name: 'Bestandsnaam',
Finish_recording: 'Beëindig opname',
Following_thread: 'Volg thread',
For_your_security_you_must_enter_your_current_password_to_continue: 'Voor je veiligheid moet je je wachtwoord invullen om door te gaan',
Forgot_my_password: 'Wachtwoord vergeten',
Forgot_password_If_this_email_is_registered: 'Als dit email adres bij ons bekend is, sturen we je instructies op om je wachtwoord te resetten. Als je geen email krijgt, probeer het dan nogmaals.',
Forgot_password: 'Wachtwoord vergeten',
Forgot_Password: 'Wachtwoord Vergeten',
Full_table: 'Klik om de hele tabel te zien',
Generate_New_Link: 'Genereer Nieuwe Link',
Group_by_favorites: 'Sorteer op favorieten',
Group_by_type: 'Sorteer op type',
Hide: 'Verberg',
Has_joined_the_channel: 'Is bij het kanaal gekomen',
Has_joined_the_conversation: 'Neemt deel aan het gesprek',
Has_left_the_channel: 'Heeft het kanaal verlaten',
IN_APP_AND_DESKTOP: 'IN-APP EN DESKTOP',
In_App_and_Desktop_Alert_info: 'Laat een banner bovenaan het scherm zien als de app open is en geeft een notificatie op de desktop',
Invisible: 'Onzichtbaar',
Invite: 'Nodig uit',
is_a_valid_RocketChat_instance: 'is een geldige Rocket.Chat instantie',
is_not_a_valid_RocketChat_instance: 'is geen geldige Rocket.Chat instantie',
is_typing: 'is aan het typen',
Invalid_or_expired_invite_token: 'Ongeldig of verlopen uitnodigingstoken',
Invalid_server_version: 'De server die je probeert te bereiken gebruikt een versie die niet meer door de app ondersteunt wordt: {{currentVersion}}.\n\nMinimale versienummer {{minVersion}}',
Invite_Link: 'Uitnodigingslink',
Invite_users: 'Nodig gebruikers uit',
Join_the_community: 'Word lid van de community',
Join: 'Word lid',
Just_invited_people_can_access_this_channel: 'Alleen genodigden kunnen bij dit kanaal',
Language: 'Taal',
last_message: 'laatste bericht',
Leave_channel: 'Verlaat kanaal',
leaving_room: 'ruimte verlaten',
leave: 'verlaten',
Legal: 'Legaal',
Light: 'Light',
License: 'License',
Livechat: 'Livechat',
Login: 'Login',
Login_error: 'Je inloggegevens zijn fout! Probeer het opnieuw.',
Login_with: 'Login met',
Logout: 'Logout',
Max_number_of_uses: 'Maximaal aantal gebruiksmogelijkheden ',
members: 'leden',
Members: 'Leden',
Mentioned_Messages: 'Vermelde Berichten',
mentioned: 'vermeld',
Mentions: 'Vermeldingen',
Message_accessibility: 'Bericht van {{user}} om {{time}}: {{message}}',
Message_actions: 'Berichtacties',
Message_pinned: 'Bericht vastgezet',
Message_removed: 'Bericht verwijderd',
message: 'bericht',
messages: 'berichten',
Messages: 'Berichten',
Message_Reported: 'Bericht gerapporteerd',
Microphone_Permission_Message: 'Rocket Chat heeft toegang tot je microfoon nodig voor geluidsberichten.',
Microphone_Permission: 'Microfoon toestemming',
Mute: 'Dempen',
muted: 'gedempt',
My_servers: 'Mijn servers',
N_people_reacted: '{{n}} mensen reageerden',
N_users: '{{n}} gebruikers',
name: 'naam',
Name: 'Naam',
Never: 'Nooit',
New_Message: 'Nieuw Bericht',
New_Password: 'Nieuw Wachtwoord',
New_Server: 'Nieuwe Server',
Next: 'Volgende',
No_files: 'Geen bestanden',
No_limit: 'Geen limiet',
No_mentioned_messages: 'Geen vermelde berichten',
No_pinned_messages: 'Geen vastgezette berichten',
No_results_found: 'Geen resultaten gevonden',
No_starred_messages: 'Geen berichten met ster gemarkeerd',
No_thread_messages: 'Geen thread berichten',
No_announcement_provided: 'Geen announcement opgegeven.',
No_description_provided: 'Geen beschrijving opgegeven.',
No_topic_provided: 'Geen onderwerp opgegeven.',
No_Message: 'Geen bericht',
No_messages_yet: 'Nog geen berichten',
No_Reactions: 'Geen reacties',
No_Read_Receipts: 'Geen leesbevestiging',
Not_logged: 'Niet gelogged',
Not_RC_Server: 'Dit is geen Rocket.Chat server.\n{{contact}}',
Nothing: 'Niets',
Nothing_to_save: 'Niets om op te slaan!',
Notify_active_in_this_room: 'Bericht de actieve gebruikers in deze kamer',
Notify_all_in_this_room: 'Bericht iedereen in deze kamer',
Notifications: 'Notificaties',
Notification_Duration: 'Notificatie Duur',
Notification_Preferences: 'Notificatievoorkeuren',
Offline: 'Offline',
Oops: 'Oeps!',
Online: 'Online',
Only_authorized_users_can_write_new_messages: 'Alleen gebruikers met toestemming mogen nieuwe berichten maken',
Open_emoji_selector: 'Open de emoji selector',
Open_Source_Communication: 'Open de Source Communication',
Password: 'Wachtwoord',
Permalink_copied_to_clipboard: 'Permalink gekopiëerd naar klembord!',
Pin: 'Vastzetten',
Pinned_Messages: 'Vastgezette berichten',
pinned: 'vastgezet',
Pinned: 'Vastgezet',
Please_enter_your_password: 'Vul je wachtwoord in',
Preferences: 'Voorkeuren',
Preferences_saved: 'Voorkeuren opgeslagen!',
Privacy_Policy: ' Privacy Policy',
Private_Channel: 'Prive Kanaal',
Private_Groups: 'Prive Groepen',
Private: 'Prive',
Processing: 'Verwerken...',
Profile_saved_successfully: 'Profiel succesvol opgeslagen!',
Profile: 'Profiel',
Public_Channel: 'Publiek kanaal',
Public: 'Publiek',
PUSH_NOTIFICATIONS: 'PUSHNOTIFICATIES',
Push_Notifications_Alert_Info: 'Deze notificaties krijg je als de app niet geopend is',
Quote: 'Quote',
Reactions_are_disabled: 'Reacties zijn uitgeschakeld',
Reactions_are_enabled: 'Reacties zijn ingeschakeld',
Reactions: 'Reacties',
Read: 'Lezen',
Read_Only_Channel: 'Alleen-lezen Kanaal',
Read_Only: 'Alleen Lezen',
Read_Receipt: 'Leesbevestiging',
Receive_Group_Mentions: 'Ontvang Groepsvermeldingen',
Receive_Group_Mentions_Info: 'Ontvang @all en @here vermeldingen',
Register: 'Aanmelden',
Repeat_Password: 'Wachtwoord herhalen',
Replied_on: 'Gereageerd op:',
replies: 'reacties',
reply: 'reactie',
Reply: 'Reacties',
Report: 'Rapporteren',
Receive_Notification: 'Ontvang notificatie',
Receive_notifications_from: 'Ontvang notificaties van {{name}}',
Resend: 'Opnieuw verzenden',
Reset_password: 'Wachtwoord reset',
resetting_password: 'wachtwoord aan het resetten',
RESET: 'RESET',
Review_app_title: 'Vind je dit een TOP app?',
Review_app_desc: 'Geef ons 5 sterren op {{store}}',
Review_app_yes: 'Doe ik!',
Review_app_no: 'Nee',
Review_app_later: 'Misschien later',
Review_app_unable_store: 'Kon {{store}} niet openen',
Review_this_app: 'Review deze app',
Roles: 'Rollen',
Room_actions: 'Kamer acties',
Room_changed_announcement: 'Kamer announcement veranderd naar: {{announcement}} door {{userBy}}',
Room_changed_description: 'Kamer beschrijving veranderd naar: {{description}} door {{userBy}}',
Room_changed_privacy: 'Kamer type veranderd naar: {{type}} door {{userBy}}',
Room_changed_topic: 'Kamer onderwerp veranderd naar: {{topic}} door {{userBy}}',
Room_Files: 'Kamer Bestanden',
Room_Info_Edit: 'Kamer Info Aanpassen',
Room_Info: 'Kamer Info',
Room_Members: 'Kamer Leden',
Room_name_changed: 'Kamer naam veranderd naar: {{name}} door {{userBy}}',
SAVE: 'OPSLAAN',
Save_Changes: 'Sla wijzigingen op',
Save: 'Opslaan',
saving_preferences: 'voorkeuren opslaan',
saving_profile: 'profiel opslaan',
saving_settings: 'instellingen opslaan',
saved_to_gallery: 'Aan galerij toegevoegd',
Search_Messages: 'Zoek Berichten',
Search: 'Zoek',
Search_by: 'Zoek op',
Search_global_users: 'Zoek voor algemene gebruikers',
Search_global_users_description: 'Als je dit aan zet, kan je gebruikers van andere bedrijven en servers zoeken.',
Seconds: '{{second}} seconden',
Select_Avatar: 'Kies Avatar',
Select_Server: 'Kies Server',
Select_Users: 'Kies Gebruikers',
Send: 'Verstuur',
Send_audio_message: 'Verstuur geluidsbericht',
Send_crash_report: 'Verstuur crash report',
Send_message: 'Verstuur bericht',
Send_to: 'Verstuur naar...',
Sent_an_attachment: 'Verstuur een bijlage',
Server: 'Server',
Servers: 'Servers',
Server_version: 'Server versie: {{version}}',
Set_username_subtitle: 'De gebruikersnaam wordt gebruikt om anderen jou te vermelden in berichten',
Settings: 'Instellingen',
Settings_succesfully_changed: 'Instellingen succesvol veranderd!',
Share: 'Delen',
Share_Link: 'Deel Link',
Share_this_app: 'Deel deze app',
Show_Unread_Counter: 'Laat Ongelezen Teller Zien',
Show_Unread_Counter_Info: 'De Ongelezen Tller is een badge aan de rechterkant van het kanaal in de lijst',
Sign_in_your_server: 'Log in bij je server',
Sign_Up: 'Inschrijven',
Some_field_is_invalid_or_empty: 'Een veld is ongeldig of leeg',
Sorting_by: 'Sorteren op {{key}}',
Sound: 'Geluid',
Star_room: 'Sterrenkamer',
Star: 'Ster',
Starred_Messages: 'Berichten met ster gemarkeerd',
starred: 'met ster gemarkeerd',
Starred: 'Met ster gemarkeerd',
Start_of_conversation: 'Begin van een gesprek',
Started_discussion: 'Begin van een discussie:',
Started_call: 'Gesprek gestart door {{userBy}}',
Submit: 'Verstuur',
Table: 'Tabel',
Take_a_photo: 'Neem een foto',
Take_a_video: 'Neem een video',
tap_to_change_status: 'tik om je status te veranderen',
Tap_to_view_servers_list: 'Tik om een server lijst te weergeven',
Terms_of_Service: ' Servicevoorwaarden ',
Theme: 'Thema',
The_URL_is_invalid: 'Ongeldige URL of niet mogelijk een veilige verbinding op te zetten.\n{{contact}}',
There_was_an_error_while_action: 'Er was eer fout tijdens {{action}}!',
This_room_is_blocked: 'Deze kamer is geblokkeerd',
This_room_is_read_only: 'Deze kamer is alleen-lezen',
Thread: 'Thread',
Threads: 'Threads',
Timezone: 'Tijdzone',
To: 'Naar',
topic: 'onderwerp',
Topic: 'Onderwerp',
Translate: 'Vertalen',
Try_again: 'Probeer opnieuw',
Two_Factor_Authentication: 'Tweee-factor Authenticatie',
Type_the_channel_name_here: 'Typ hier de kanaalnaam',
unarchive: 'dearchiveren',
UNARCHIVE: 'DEARCHIVEREN',
Unblock_user: 'Gebruiker deblokkeren',
Unfavorite: 'Uit favorieten halen',
Unfollowed_thread: 'Thread ontvolgd',
Unmute: 'Dempen opheffen',
unmuted: 'ongedempt',
Unpin: 'Losmaken',
unread_messages: 'ongelezen',
Unread: 'Ongelezen',
Unread_on_top: 'Ongelezen bovenaan',
Unstar: 'Ster verwijderen',
Updating: 'Updaten...',
Uploading: 'Uploaden',
Upload_file_question_mark: 'Bestand uploaden?',
Users: 'Gebruikers',
User_added_by: 'Gebruiker {{userAdded}} toegevoegd door {{userBy}}',
User_Info: 'Gebruiker Info',
User_has_been_key: 'Gebruiker is {{key}}!',
User_is_no_longer_role_by_: '{{user}} is geen {{role}} meer door {{userBy}}',
User_muted_by: 'Gebruiker {{userMuted}} gedempt door {{userBy}}',
User_removed_by: 'Gebruiker {{userRemoved}} verwijderd door {{userBy}}',
User_sent_an_attachment: '{{user}} stuurde een bijlage',
User_unmuted_by: 'Dempen opgeheven voor {{userUnmuted}} door {{userBy}}',
User_was_set_role_by_: '{{user}} is nu {{role}} door {{userBy}}',
Username_is_empty: 'Gebruikersnaam is leeg',
Username: 'Gebruikersnaam',
Username_or_email: 'Gebruikersnaam of email',
Validating: 'Aan het valideren',
Video_call: 'Videogesprek',
View_Original: 'Bekijk origineel',
Voice_call: 'Audiogesprek',
Websocket_disabled: 'Websocket staat uit voor deze server.\n{{contact}}',
Welcome: 'Welkom',
Welcome_to_RocketChat: 'Welkom bij Rocket.Chat',
Whats_your_2fa: 'Wat is je 2FA code?',
Without_Servers: 'Zonder Servers',
Write_External_Permission_Message: 'Rocket Chat moet bij je galerij kunnen om afbeeldingen op te slaan.',
Write_External_Permission: 'Galerij Toestemming',
Yes_action_it: 'Ja, {{action}} het!',
Yesterday: 'Gisteren',
You_are_in_preview_mode: 'Je bent in preview mode',
You_are_offline: 'Je bent offline',
You_can_search_using_RegExp_eg: 'Je kan RegExp. gebruiken, bijv. `/^text$/i`',
You_colon: 'Jij: ',
you_were_mentioned: 'je bent vermeld',
you: 'jij',
You: 'Jij',
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Je moet minimaal toegang hebben tot 1 Rocket.Chat server om iets te delen.',
Your_certificate: 'Jouw Certificaat',
Your_invite_link_will_expire_after__usesLeft__uses: 'Je uitnodigingslink wordt ongeldig over {{usesLeft}} keer.',
Your_invite_link_will_expire_on__date__or_after__usesLeft__uses: 'Je uitnodigingslink wordt ongeldig op {{date}} of na{{usesLeft}} keer.',
Your_invite_link_will_expire_on__date__: 'Je uitnodigingslink wordt ongeldig op {{date}}.',
Your_invite_link_will_never_expire: 'Je uitnodigingslink wordt nooit ongeldig.',
Version_no: 'Versie: {{version}}',
You_will_not_be_able_to_recover_this_message: 'Je kan dit bericht niet meer terugkrijgen!',
Change_Language: 'Verander taal',
Crash_report_disclaimer: 'We kijken nooit naar de content van je chats. Het crashrapport bevat alleen relevante informatie voor ons om problemen te isoleren en op te lossen.',
Type_message: 'Type bericht',
Room_search: 'Kamers zoeken',
Room_selection: 'Kamerselectie 1...9',
Next_room: 'Volgende kamer',
Previous_room: 'Vorige kamer',
New_room: 'Nieuwe Kamer',
Upload_room: 'Upload naar kamer',
Search_messages: 'Doorzoek messages',
Scroll_messages: 'Scroll door messages',
Reply_latest: 'Beantwoord de laatste',
Server_selection: 'Server selectie',
Server_selection_numbers: 'Server selectie 1...9',
Add_server: 'Voeg Server Toe',
New_line: 'Nieuwe Regel'
};

View File

@ -302,6 +302,13 @@ export default {
Reset_password: 'Resetar senha',
resetting_password: 'redefinindo senha',
RESET: 'RESETAR',
Review_app_title: 'Você está gostando do app?',
Review_app_desc: 'Nos dê 5 estrelas na {{store}}',
Review_app_yes: 'Claro!',
Review_app_no: 'Não',
Review_app_later: 'Talvez depois',
Review_app_unable_store: 'Não foi possível abrir {{store}}',
Review_this_app: 'Avaliar esse app',
Roles: 'Papéis',
Room_actions: 'Ações',
Room_changed_announcement: 'O anúncio da sala foi alterado para: {{announcement}} por {{userBy}}',
@ -339,6 +346,7 @@ export default {
Settings_succesfully_changed: 'Configurações salvas com sucesso!',
Share: 'Compartilhar',
Share_Link: 'Share Link',
Show_more: 'Mostrar mais..',
Sign_in_your_server: 'Entrar no seu servidor',
Sign_Up: 'Registrar',
Some_field_is_invalid_or_empty: 'Algum campo está inválido ou vazio',
@ -396,6 +404,8 @@ export default {
Username_is_empty: 'Usuário está vazio',
Username: 'Usuário',
Username_or_email: 'Usuário ou email',
Verify_email_title: 'Registrado com sucesso!',
Verify_email_desc: 'Nós lhe enviamos um e-mail para confirmar o seu registro. Se você não receber um e-mail em breve, por favor retorne e tente novamente.',
Video_call: 'Chamada de vídeo',
Voice_call: 'Chamada de voz',
Websocket_disabled: 'Websocket está desativado para esse servidor.\n{{contact}}',
@ -433,5 +443,8 @@ export default {
Server_selection: 'Seleção de servidor',
Server_selection_numbers: 'Selecionar servidor 1...9',
Add_server: 'Adicionar servidor',
New_line: 'Nova linha'
New_line: 'Nova linha',
You_will_be_logged_out_of_this_application: 'Você sairá deste aplicativo.',
Clear: 'Limpar',
This_will_clear_all_your_offline_data: 'Isto limpará todos os seus dados offline.'
};

View File

@ -1,5 +1,7 @@
import React from 'react';
import { View, Linking, BackHandler } from 'react-native';
import {
View, Linking, BackHandler, ScrollView
} from 'react-native';
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import { createDrawerNavigator } from 'react-navigation-drawer';
@ -34,13 +36,16 @@ import { ThemeContext } from './theme';
import RocketChat, { THEME_PREFERENCES_KEY } from './lib/rocketchat';
import { MIN_WIDTH_SPLIT_LAYOUT } from './constants/tablet';
import {
isTablet, isSplited, isIOS, setWidth, supportSystemTheme
isTablet, isSplited, isIOS, setWidth, supportSystemTheme, isAndroid
} from './utils/deviceInfo';
import { KEY_COMMAND } from './commands';
import Tablet, { initTabletNav } from './tablet';
import sharedStyles from './views/Styles';
import { SplitContext } from './split';
import RoomsListView from './views/RoomsListView';
import RoomView from './views/RoomView';
if (isIOS) {
const RNScreens = require('react-native-screens');
RNScreens.useScreens();
@ -109,9 +114,7 @@ const OutsideStackModal = createStackNavigator({
});
const RoomRoutes = {
RoomView: {
getScreen: () => require('./views/RoomView').default
},
RoomView,
ThreadMessagesView: {
getScreen: () => require('./views/ThreadMessagesView').default
},
@ -125,9 +128,7 @@ const RoomRoutes = {
// Inside
const ChatsStack = createStackNavigator({
RoomsListView: {
getScreen: () => require('./views/RoomsListView').default
},
RoomsListView,
RoomActionsView: {
getScreen: () => require('./views/RoomActionsView').default
},
@ -275,10 +276,21 @@ const AttachmentStack = createStackNavigator({
cardStyle
});
const ModalBlockStack = createStackNavigator({
ModalBlockView: {
getScreen: () => require('./views/ModalBlockView').default
}
}, {
mode: 'modal',
defaultNavigationOptions: defaultHeader,
cardStyle
});
const InsideStackModal = createStackNavigator({
Main: ChatsDrawer,
NewMessageStack,
AttachmentStack,
ModalBlockStack,
JitsiMeetView: {
getScreen: () => require('./views/JitsiMeetView').default
}
@ -422,6 +434,7 @@ const ModalSwitch = createSwitchNavigator({
SidebarStack,
RoomActionsStack,
SettingsStack,
ModalBlockStack,
AuthLoading: () => null
},
{
@ -453,6 +466,9 @@ class CustomModalStack extends React.Component {
closeModal();
return true;
}
if (state && state.routes[state.index] && state.routes[state.index].routes.length > 1) {
navigation.goBack();
}
return false;
}
@ -464,6 +480,24 @@ class CustomModalStack extends React.Component {
const pageSheetViews = ['AttachmentView'];
const pageSheet = pageSheetViews.includes(getActiveRouteName(navigation.state));
const androidProps = isAndroid && {
style: { marginBottom: 0 }
};
let content = (
<View style={[sharedStyles.modal, pageSheet ? sharedStyles.modalPageSheet : sharedStyles.modalFormSheet]}>
<ModalSwitch navigation={navigation} screenProps={{ ...screenProps, closeModal: this.closeModal }} />
</View>
);
if (isAndroid) {
content = (
<ScrollView overScrollMode='never'>
{content}
</ScrollView>
);
}
return (
<Modal
useNativeDriver
@ -472,10 +506,9 @@ class CustomModalStack extends React.Component {
onBackdropPress={closeModal}
hideModalContentWhileAnimating
avoidKeyboard
{...androidProps}
>
<View style={[sharedStyles.modal, pageSheet ? sharedStyles.modalPageSheet : sharedStyles.modalFormSheet]}>
<ModalSwitch navigation={navigation} screenProps={screenProps} />
</View>
{content}
</Modal>
);
}

View File

@ -6,6 +6,12 @@ function setTopLevelNavigator(navigatorRef) {
_navigator = navigatorRef;
}
function back() {
_navigator.dispatch(
NavigationActions.back()
);
}
function navigate(routeName, params) {
_navigator.dispatch(
NavigationActions.navigate({
@ -16,6 +22,7 @@ function navigate(routeName, params) {
}
export default {
back,
navigate,
setTopLevelNavigator
};

View File

@ -60,7 +60,7 @@ class DB {
}
setShareDB(database = '') {
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//, '.');
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.');
const dbName = `${ appGroupPath }${ path }.db`;
const adapter = new SQLiteAdapter({
@ -83,7 +83,7 @@ class DB {
}
setActiveDB(database = '') {
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//, '.');
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.');
const dbName = `${ appGroupPath }${ path }.db`;
const adapter = new SQLiteAdapter({

View File

@ -73,4 +73,6 @@ export default class Message extends Model {
@json('translations', sanitizer) translations;
@field('tmsg') tmsg;
@json('blocks', sanitizer) blocks;
}

View File

@ -11,4 +11,6 @@ export default class SlashCommand extends Model {
@field('client_only') clientOnly;
@field('provides_preview') providesPreview;
@field('app_id') appId;
}

View File

@ -23,6 +23,23 @@ export default schemaMigrations({
]
})
]
},
{
toVersion: 4,
steps: [
addColumns({
table: 'messages',
columns: [
{ name: 'blocks', type: 'string', isOptional: true }
]
}),
addColumns({
table: 'slash_commands',
columns: [
{ name: 'app_id', type: 'string', isOptional: true }
]
})
]
}
]
});

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 3,
version: 4,
tables: [
tableSchema({
name: 'subscriptions',
@ -84,7 +84,8 @@ export default appSchema({
{ name: 'unread', type: 'boolean', isOptional: true },
{ name: 'auto_translate', type: 'boolean', isOptional: true },
{ name: 'translations', type: 'string', isOptional: true },
{ name: 'tmsg', type: 'string', isOptional: true }
{ name: 'tmsg', type: 'string', isOptional: true },
{ name: 'blocks', type: 'string', isOptional: true }
]
}),
tableSchema({
@ -217,7 +218,8 @@ export default appSchema({
{ name: 'params', type: 'string', isOptional: true },
{ name: 'description', type: 'string', isOptional: true },
{ name: 'client_only', type: 'boolean', isOptional: true },
{ name: 'provides_preview', type: 'boolean', isOptional: true }
{ name: 'provides_preview', type: 'boolean', isOptional: true },
{ name: 'app_id', type: 'string', isOptional: true }
]
})
]

160
app/lib/methods/actions.js Normal file
View File

@ -0,0 +1,160 @@
import random from '../../utils/random';
import EventEmitter from '../../utils/events';
import Navigation from '../Navigation';
const ACTION_TYPES = {
ACTION: 'blockAction',
SUBMIT: 'viewSubmit',
CLOSED: 'viewClosed'
};
export const MODAL_ACTIONS = {
MODAL: 'modal',
OPEN: 'modal.open',
CLOSE: 'modal.close',
UPDATE: 'modal.update',
ERRORS: 'errors'
};
export const CONTAINER_TYPES = {
VIEW: 'view',
MESSAGE: 'message'
};
const triggersId = new Map();
const invalidateTriggerId = (id) => {
const appId = triggersId.get(id);
triggersId.delete(id);
return appId;
};
export const generateTriggerId = (appId) => {
const triggerId = random(17);
triggersId.set(triggerId, appId);
return triggerId;
};
export const handlePayloadUserInteraction = (type, { triggerId, ...data }) => {
if (!triggersId.has(triggerId)) {
return;
}
const appId = invalidateTriggerId(triggerId);
if (!appId) {
return;
}
const { view } = data;
let { viewId } = data;
if (view && view.id) {
viewId = view.id;
}
if (!viewId) {
return;
}
if ([MODAL_ACTIONS.ERRORS].includes(type)) {
EventEmitter.emit(viewId, {
type,
triggerId,
viewId,
appId,
...data
});
return MODAL_ACTIONS.ERRORS;
}
if ([MODAL_ACTIONS.UPDATE].includes(type)) {
EventEmitter.emit(viewId, {
type,
triggerId,
viewId,
appId,
...data
});
return MODAL_ACTIONS.UPDATE;
}
if ([MODAL_ACTIONS.OPEN].includes(type) || [MODAL_ACTIONS.MODAL].includes(type)) {
Navigation.navigate('ModalBlockView', {
data: {
triggerId,
viewId,
appId,
...data
}
});
return MODAL_ACTIONS.OPEN;
}
return MODAL_ACTIONS.CLOSE;
};
export function triggerAction({
type, actionId, appId, rid, mid, viewId, container, ...rest
}) {
return new Promise(async(resolve, reject) => {
const triggerId = generateTriggerId(appId);
const payload = rest.payload || rest;
try {
const { userId, authToken } = this.sdk.currentLogin;
const { host } = this.sdk.client;
// we need to use fetch because this.sdk.post add /v1 to url
const result = await fetch(`${ host }/api/apps/ui.interaction/${ appId }/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Token': authToken,
'X-User-Id': userId
},
body: JSON.stringify({
type,
actionId,
payload,
container,
mid,
rid,
triggerId,
viewId
})
});
try {
const { type: interactionType, ...data } = await result.json();
return resolve(handlePayloadUserInteraction(interactionType, data));
} catch (e) {
// modal.close has no body, so result.json will fail
// but it returns ok status
if (result.ok) {
return resolve();
}
}
} catch (e) {
// do nothing
}
return reject();
});
}
export default function triggerBlockAction(options) {
return triggerAction.call(this, { type: ACTION_TYPES.ACTION, ...options });
}
export async function triggerSubmitView({ viewId, ...options }) {
const result = await triggerAction.call(this, { type: ACTION_TYPES.SUBMIT, viewId, ...options });
if (!result || MODAL_ACTIONS.CLOSE === result) {
Navigation.back();
}
}
export function triggerCancel({ view, ...options }) {
return triggerAction.call(this, { type: ACTION_TYPES.CLOSED, view, ...options });
}

View File

@ -17,10 +17,25 @@ const jitsiBaseUrl = ({
return `${ urlProtocol }${ urlDomain }${ prefix }${ uniqueIdentifier }`;
};
function callJitsi(rid, onlyAudio = false) {
async function callJitsi(rid, onlyAudio = false) {
let accessToken;
let queryString = '';
const { settings } = reduxStore.getState();
const { Jitsi_Enabled_TokenAuth } = settings;
Navigation.navigate('JitsiMeetView', { url: `${ jitsiBaseUrl(settings) }${ rid }`, onlyAudio, rid });
if (Jitsi_Enabled_TokenAuth) {
try {
accessToken = await this.sdk.methodCall('jitsi:generateAccessToken', rid);
} catch (e) {
// do nothing
}
}
if (accessToken) {
queryString = `?jwt=${ accessToken }`;
}
Navigation.navigate('JitsiMeetView', { url: `${ jitsiBaseUrl(settings) }${ rid }${ queryString }`, onlyAudio, rid });
}
export default callJitsi;

View File

@ -28,6 +28,8 @@ export default function() {
// filter slash commands
let slashCommandsToCreate = commands.filter(i1 => !allSlashCommandsRecords.find(i2 => i1.command === i2.id));
let slashCommandsToUpdate = allSlashCommandsRecords.filter(i1 => commands.find(i2 => i1.id === i2.command));
let slashCommandsToDelete = allSlashCommandsRecords
.filter(i1 => !slashCommandsToCreate.find(i2 => i2.command === i1.id) && !slashCommandsToUpdate.find(i2 => i2.id === i1.id));
// Create
slashCommandsToCreate = slashCommandsToCreate.map(command => slashCommandsCollection.prepareCreate(protectedFunction((s) => {
@ -43,9 +45,13 @@ export default function() {
}));
});
// Delete
slashCommandsToDelete = slashCommandsToDelete.map(command => command.prepareDestroyPermanently());
const allRecords = [
...slashCommandsToCreate,
...slashCommandsToUpdate
...slashCommandsToUpdate,
...slashCommandsToDelete
];
try {

View File

@ -28,13 +28,9 @@ export async function cancelUpload(item) {
export function sendFileMessage(rid, fileInfo, tmid, server, user) {
return new Promise(async(resolve, reject) => {
try {
const serversDB = database.servers;
const serversCollection = serversDB.collections.get('servers');
const serverInfo = await serversCollection.find(server);
const { id: Site_Url } = serverInfo;
const { id, token } = user;
const uploadUrl = `${ Site_Url }/api/v1/rooms.upload/${ rid }`;
const uploadUrl = `${ server }/api/v1/rooms.upload/${ rid }`;
const xhr = new XMLHttpRequest();
const formData = new FormData();

View File

@ -9,26 +9,66 @@ import database from '../../database';
import reduxStore from '../../createStore';
import { addUserTyping, removeUserTyping, clearUserTyping } from '../../../actions/usersTyping';
import debounce from '../../../utils/debounce';
import RocketChat from '../../rocketchat';
const unsubscribe = (subscriptions = []) => Promise.all(subscriptions.map(sub => sub.unsubscribe));
const removeListener = listener => listener.stop();
export default class RoomSubscription {
constructor(rid) {
this.rid = rid;
this.isAlive = true;
}
let promises;
let connectedListener;
let disconnectedListener;
let notifyRoomListener;
let messageReceivedListener;
subscribe = async() => {
console.log(`[RCRN] Subscribing to room ${ this.rid }`);
if (this.promises) {
await this.unsubscribe();
}
this.promises = RocketChat.subscribeRoom(this.rid);
export default function subscribeRoom({ rid }) {
console.log(`[RCRN] Subscribed to room ${ rid }`);
this.connectedListener = RocketChat.onStreamData('connected', this.handleConnection);
this.disconnectedListener = RocketChat.onStreamData('close', this.handleConnection);
this.notifyRoomListener = RocketChat.onStreamData('stream-notify-room', this.handleNotifyRoomReceived);
this.messageReceivedListener = RocketChat.onStreamData('stream-room-messages', this.handleMessageReceived);
if (!this.isAlive) {
this.unsubscribe();
}
}
const handleConnection = () => {
this.loadMissedMessages({ rid }).catch(e => console.log(e));
unsubscribe = async() => {
console.log(`[RCRN] Unsubscribing from room ${ this.rid }`);
this.isAlive = false;
if (this.promises) {
try {
const subscriptions = await this.promises || [];
subscriptions.forEach(sub => sub.unsubscribe().catch(() => console.log('unsubscribeRoom')));
} catch (e) {
// do nothing
}
}
this.removeListener(this.connectedListener);
this.removeListener(this.disconnectedListener);
this.removeListener(this.notifyRoomListener);
this.removeListener(this.messageReceivedListener);
reduxStore.dispatch(clearUserTyping());
}
removeListener = async(promise) => {
if (promise) {
try {
const listener = await promise;
listener.stop();
} catch (e) {
// do nothing
}
}
};
const handleNotifyRoomReceived = protectedFunction((ddpMessage) => {
handleConnection = () => {
RocketChat.loadMissedMessages({ rid: this.rid }).catch(e => console.log(e));
};
handleNotifyRoomReceived = protectedFunction((ddpMessage) => {
const [_rid, ev] = ddpMessage.fields.eventName.split('/');
if (rid !== _rid) {
if (this.rid !== _rid) {
return;
}
if (ev === 'typing') {
@ -87,14 +127,14 @@ export default function subscribeRoom({ rid }) {
}
});
const read = debounce((lastOpen) => {
this.readMessages(rid, lastOpen);
read = debounce((lastOpen) => {
RocketChat.readMessages(this.rid, lastOpen);
}, 300);
const handleMessageReceived = protectedFunction((ddpMessage) => {
handleMessageReceived = protectedFunction((ddpMessage) => {
const message = buildMessage(EJSON.fromJSONValue(ddpMessage.fields.args[0]));
const lastOpen = new Date();
if (rid !== message.rid) {
if (this.rid !== message.rid) {
return;
}
InteractionManager.runAfterInteractions(async() => {
@ -126,7 +166,7 @@ export default function subscribeRoom({ rid }) {
batch.push(
msgCollection.prepareCreate(protectedFunction((m) => {
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
m.subscription.id = rid;
m.subscription.id = this.rid;
Object.assign(m, message);
}))
);
@ -150,7 +190,7 @@ export default function subscribeRoom({ rid }) {
batch.push(
threadsCollection.prepareCreate(protectedFunction((t) => {
t._raw = sanitizedRaw({ id: message._id }, threadsCollection.schema);
t.subscription.id = rid;
t.subscription.id = this.rid;
Object.assign(t, message);
}))
);
@ -178,7 +218,7 @@ export default function subscribeRoom({ rid }) {
threadMessagesCollection.prepareCreate(protectedFunction((tm) => {
tm._raw = sanitizedRaw({ id: message._id }, threadMessagesCollection.schema);
Object.assign(tm, message);
tm.subscription.id = rid;
tm.subscription.id = this.rid;
tm.rid = message.tmid;
delete tm.tmid;
}))
@ -186,7 +226,7 @@ export default function subscribeRoom({ rid }) {
}
}
read(lastOpen);
this.read(lastOpen);
try {
await db.action(async() => {
@ -197,49 +237,4 @@ export default function subscribeRoom({ rid }) {
}
});
});
const stop = async() => {
let params;
if (promises) {
try {
params = await promises;
await unsubscribe(params);
} catch (error) {
// Do nothing
}
promises = false;
}
if (connectedListener) {
params = await connectedListener;
removeListener(params);
connectedListener = false;
}
if (disconnectedListener) {
params = await disconnectedListener;
removeListener(params);
disconnectedListener = false;
}
if (notifyRoomListener) {
params = await notifyRoomListener;
removeListener(params);
notifyRoomListener = false;
}
if (messageReceivedListener) {
params = await messageReceivedListener;
removeListener(params);
messageReceivedListener = false;
}
reduxStore.dispatch(clearUserTyping());
};
connectedListener = this.sdk.onStreamData('connected', handleConnection);
disconnectedListener = this.sdk.onStreamData('close', handleConnection);
notifyRoomListener = this.sdk.onStreamData('stream-notify-room', handleNotifyRoomReceived);
messageReceivedListener = this.sdk.onStreamData('stream-room-messages', handleMessageReceived);
promises = this.sdk.subscribeRoom(rid);
return {
stop: () => stop()
};
}

View File

@ -1,4 +1,5 @@
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { InteractionManager } from 'react-native';
import database from '../../database';
import { merge } from '../helpers/mergeSubscriptionsRooms';
@ -9,6 +10,9 @@ import random from '../../../utils/random';
import store from '../../createStore';
import { roomsRequest } from '../../../actions/rooms';
import { notificationReceived } from '../../../actions/notification';
import { handlePayloadUserInteraction } from '../actions';
import buildMessage from '../helpers/buildMessage';
import RocketChat from '../../rocketchat';
const removeListener = listener => listener.stop();
@ -16,8 +20,12 @@ let connectedListener;
let disconnectedListener;
let streamListener;
let subServer;
let subQueue = {};
let subTimer = null;
let roomQueue = {};
let roomTimer = null;
const WINDOW_TIME = 500;
// TODO: batch execution
const createOrUpdateSubscription = async(subscription, room) => {
try {
const db = database.active;
@ -128,32 +136,32 @@ const createOrUpdateSubscription = async(subscription, room) => {
}
}
// if (tmp.lastMessage) {
// const lastMessage = buildMessage(tmp.lastMessage);
// const messagesCollection = db.collections.get('messages');
// let messageRecord;
// try {
// messageRecord = await messagesCollection.find(lastMessage._id);
// } catch (error) {
// // Do nothing
// }
if (tmp.lastMessage) {
const lastMessage = buildMessage(tmp.lastMessage);
const messagesCollection = db.collections.get('messages');
let messageRecord;
try {
messageRecord = await messagesCollection.find(lastMessage._id);
} catch (error) {
// Do nothing
}
// if (messageRecord) {
// batch.push(
// messageRecord.prepareUpdate(() => {
// Object.assign(messageRecord, lastMessage);
// })
// );
// } else {
// batch.push(
// messagesCollection.prepareCreate((m) => {
// m._raw = sanitizedRaw({ id: lastMessage._id }, messagesCollection.schema);
// m.subscription.id = lastMessage.rid;
// return Object.assign(m, lastMessage);
// })
// );
// }
// }
if (messageRecord) {
batch.push(
messageRecord.prepareUpdate(() => {
Object.assign(messageRecord, lastMessage);
})
);
} else {
batch.push(
messagesCollection.prepareCreate((m) => {
m._raw = sanitizedRaw({ id: lastMessage._id }, messagesCollection.schema);
m.subscription.id = lastMessage.rid;
return Object.assign(m, lastMessage);
})
);
}
}
await db.batch(...batch);
});
@ -162,6 +170,38 @@ const createOrUpdateSubscription = async(subscription, room) => {
}
};
const debouncedUpdateSub = (subscription) => {
if (!subTimer) {
subTimer = setTimeout(() => {
const subBatch = subQueue;
subQueue = {};
subTimer = null;
Object.keys(subBatch).forEach((key) => {
InteractionManager.runAfterInteractions(() => {
createOrUpdateSubscription(subBatch[key]);
});
});
}, WINDOW_TIME);
}
subQueue[subscription.rid] = subscription;
};
const debouncedUpdateRoom = (room) => {
if (!roomTimer) {
roomTimer = setTimeout(() => {
const roomBatch = roomQueue;
roomQueue = {};
roomTimer = null;
Object.keys(roomBatch).forEach((key) => {
InteractionManager.runAfterInteractions(() => {
createOrUpdateSubscription(null, roomBatch[key]);
});
});
}, WINDOW_TIME);
}
roomQueue[room._id] = room;
};
export default function subscribeRooms() {
const handleConnection = () => {
store.dispatch(roomsRequest());
@ -202,12 +242,12 @@ export default function subscribeRooms() {
log(e);
}
} else {
await createOrUpdateSubscription(data);
debouncedUpdateSub(data);
}
}
if (/rooms/.test(ev)) {
if (type === 'updated' || type === 'inserted') {
await createOrUpdateSubscription(null, data);
debouncedUpdateRoom(data);
}
}
if (/message/.test(ev)) {
@ -217,6 +257,7 @@ export default function subscribeRooms() {
_id,
rid: args.rid,
msg: args.msg,
blocks: args.blocks,
ts: new Date(),
_updatedAt: new Date(),
status: messagesStatus.SENT,
@ -240,8 +281,21 @@ export default function subscribeRooms() {
}
if (/notification/.test(ev)) {
const [notification] = ddpMessage.fields.args;
try {
const { payload: { rid } } = notification;
const subCollection = db.collections.get('subscriptions');
const sub = await subCollection.find(rid);
notification.title = RocketChat.getRoomTitle(sub);
notification.avatar = RocketChat.getRoomAvatar(sub);
} catch (e) {
// do nothing
}
store.dispatch(notificationReceived(notification));
}
if (/uiInteraction/.test(ev)) {
const { type: eventType, ...args } = type;
handlePayloadUserInteraction(eventType, args);
}
});
const stop = () => {
@ -257,6 +311,16 @@ export default function subscribeRooms() {
streamListener.then(removeListener);
streamListener = false;
}
subQueue = {};
roomQueue = {};
if (subTimer) {
clearTimeout(subTimer);
subTimer = false;
}
if (roomTimer) {
clearTimeout(roomTimer);
roomTimer = false;
}
};
connectedListener = this.sdk.onStreamData('connected', handleConnection);

View File

@ -21,7 +21,6 @@ import {
} from '../actions/share';
import subscribeRooms from './methods/subscriptions/rooms';
import subscribeRoom from './methods/subscriptions/room';
import protectedFunction from './methods/helpers/protectedFunction';
import readMessages from './methods/readMessages';
@ -33,6 +32,7 @@ import { getCustomEmojis, setCustomEmojis } from './methods/getCustomEmojis';
import getSlashCommands from './methods/getSlashCommands';
import getRoles from './methods/getRoles';
import canOpenRoom from './methods/canOpenRoom';
import triggerBlockAction, { triggerSubmitView, triggerCancel } from './methods/actions';
import loadMessagesForRoom from './methods/loadMessagesForRoom';
import loadMissedMessages from './methods/loadMissedMessages';
@ -73,7 +73,6 @@ const RocketChat = {
log(e);
}
},
subscribeRoom,
canOpenRoom,
createChannel({
name, users, type, readOnly, broadcast
@ -455,6 +454,27 @@ const RocketChat = {
console.log(error);
}
},
async clearCache({ server }) {
try {
const serversDB = database.servers;
await serversDB.action(async() => {
const serverCollection = serversDB.collections.get('servers');
const serverRecord = await serverCollection.find(server);
await serverRecord.update((s) => {
s.roomsUpdatedAt = null;
});
});
} catch (e) {
// Do nothing
}
try {
const db = database.active;
await db.action(() => db.unsafeResetDatabase());
} catch (e) {
// Do nothing
}
},
registerPushToken() {
return new Promise(async(resolve) => {
const token = getDeviceToken();
@ -545,18 +565,28 @@ const RocketChat = {
RocketChat.spotlight(searchText, usernames, { users: filterUsers, rooms: filterRooms }),
new Promise((resolve, reject) => this.oldPromise = reject)
]);
data = data.concat(users.map(user => ({
...user,
rid: user.username,
name: user.username,
t: 'd',
search: true
})), rooms.map(room => ({
rid: room._id,
...room,
search: true
})));
if (filterUsers) {
data = data.concat(users.map(user => ({
...user,
rid: user.username,
name: user.username,
t: 'd',
search: true
})));
}
if (filterRooms) {
rooms.forEach((room) => {
// Check if it exists on local database
const index = data.findIndex(item => item.rid === room._id);
if (index === -1) {
data.push({
rid: room._id,
...room,
search: true
});
}
});
}
}
delete this.oldPromise;
return data;
@ -584,6 +614,9 @@ const RocketChat = {
}
return this.sdk.post('channels.join', { roomId });
},
triggerBlockAction,
triggerSubmitView,
triggerCancel,
sendFileMessage,
cancelUpload,
isUploadActive,
@ -669,6 +702,9 @@ const RocketChat = {
subscribe(...args) {
return this.sdk.subscribe(...args);
},
subscribeRoom(...args) {
return this.sdk.subscribeRoom(...args);
},
unsubscribe(subscription) {
return this.sdk.unsubscribe(subscription);
},
@ -927,7 +963,7 @@ const RocketChat = {
},
roomTypeToApiType(t) {
const types = {
c: 'channels', d: 'im', p: 'groups'
c: 'channels', d: 'im', p: 'groups', l: 'channels'
};
return types[t];
},
@ -983,10 +1019,10 @@ const RocketChat = {
rid, updatedSince
});
},
runSlashCommand(command, roomId, params) {
runSlashCommand(command, roomId, params, triggerId, tmid) {
// RC 0.60.2
return this.sdk.post('commands.run', {
command, roomId, params
command, roomId, params, triggerId, tmid
});
},
getCommandPreview(command, roomId, params) {
@ -995,10 +1031,10 @@ const RocketChat = {
command, roomId, params
});
},
executeCommandPreview(command, params, roomId, previewItem) {
executeCommandPreview(command, params, roomId, previewItem, triggerId, tmid) {
// RC 0.65.0
return this.sdk.post('commands.preview', {
command, params, roomId, previewItem
command, params, roomId, previewItem, triggerId, tmid
});
},
_setUser(ddpMessage) {
@ -1105,6 +1141,9 @@ const RocketChat = {
const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings;
return ((room.prid || useRealName) && room.fname) || room.name;
},
getRoomAvatar(room) {
return room.prid ? room.fname : room.name;
},
findOrCreateInvite({ rid, days, maxUses }) {
// RC 2.4.0

View File

@ -16,6 +16,7 @@ import { removeNotification as removeNotificationAction } from '../../actions/no
import sharedStyles from '../../views/Styles';
import { ROW_HEIGHT } from '../../presentation/RoomItem';
import { withTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login';
const AVATAR_SIZE = 48;
const ANIMATION_DURATION = 300;
@ -72,8 +73,7 @@ class NotificationBadge extends React.Component {
static propTypes = {
navigation: PropTypes.object,
baseUrl: PropTypes.string,
token: PropTypes.string,
userId: PropTypes.string,
user: PropTypes.object,
notification: PropTypes.object,
window: PropTypes.object,
removeNotification: PropTypes.func,
@ -158,26 +158,31 @@ class NotificationBadge extends React.Component {
}
goToRoom = async() => {
const { notification: { payload }, navigation, baseUrl } = this.props;
const { notification, navigation, baseUrl } = this.props;
const { payload } = notification;
const { rid, type, prid } = payload;
if (!rid) {
return;
}
const name = type === 'd' ? payload.sender.username : payload.name;
// if sub is not on local database, title will be null, so we use payload from notification
const { title = name } = notification;
await navigation.navigate('RoomsListView');
navigation.navigate('RoomView', {
rid, name, t: type, prid, baseUrl
rid, name: title, t: type, prid, baseUrl
});
this.hide();
}
render() {
const {
baseUrl, token, userId, notification, window, theme
baseUrl, user: { id: userId, token }, notification, window, theme
} = this.props;
const { message, payload } = notification;
const { type } = payload;
const name = type === 'd' ? payload.sender.username : payload.name;
// if sub is not on local database, title and avatar will be null, so we use payload from notification
const { title = name, avatar = name } = notification;
let top = 0;
if (isIOS) {
@ -211,9 +216,9 @@ class NotificationBadge extends React.Component {
background={Touchable.SelectableBackgroundBorderless()}
>
<>
<Avatar text={name} size={AVATAR_SIZE} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} />
<Avatar text={avatar} size={AVATAR_SIZE} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} />
<View style={styles.inner}>
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{name}</Text>
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text>
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>{message}</Text>
</View>
</>
@ -227,9 +232,8 @@ class NotificationBadge extends React.Component {
}
const mapStateToProps = state => ({
userId: state.login.user && state.login.user.id,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
token: state.login.user && state.login.user.token,
user: getUserSelector(state),
baseUrl: state.server.server,
notification: state.notification
});

View File

@ -14,9 +14,12 @@ export const onNotification = (notification) => {
} = EJSON.parse(data.ejson);
const types = {
c: 'channel', d: 'direct', p: 'group'
c: 'channel', d: 'direct', p: 'group', l: 'channels'
};
const roomName = type === 'd' ? sender.username : name;
let roomName = type === 'd' ? sender.username : name;
if (type === 'l') {
roomName = sender.name;
}
const params = {
host,

View File

@ -2,6 +2,7 @@ import * as types from '../actions/actionsTypes';
const initialState = {
isFetching: false,
refreshing: false,
failure: false,
errorMessage: {},
searchText: '',
@ -23,15 +24,23 @@ export default function login(state = initialState, action) {
case types.ROOMS.SUCCESS:
return {
...state,
isFetching: false
isFetching: false,
refreshing: false
};
case types.ROOMS.FAILURE:
return {
...state,
isFetching: false,
refreshing: false,
failure: true,
errorMessage: action.err
};
case types.ROOMS.REFRESH:
return {
...state,
isFetching: true,
refreshing: true
};
case types.ROOMS.SET_SEARCH:
return {
...state,

View File

@ -13,7 +13,7 @@ import EventEmitter from '../utils/events';
import { appStart } from '../actions';
const roomTypes = {
channel: 'c', direct: 'd', group: 'p'
channel: 'c', direct: 'd', group: 'p', channels: 'l'
};
const handleInviteLink = function* handleInviteLink({ params, requireLogin = false }) {

View File

@ -121,6 +121,8 @@ const start = function* start({ root }) {
yield Navigation.navigate('SetUsernameView');
} else if (root === 'outside') {
yield Navigation.navigate('OutsideStack');
} else if (root === 'loading') {
yield Navigation.navigate('AuthLoading');
}
RNBootSplash.hide();
};

View File

@ -1,5 +1,5 @@
import {
put, call, takeLatest, select, take, fork, cancel
put, call, takeLatest, select, take, fork, cancel, race, delay
} from 'redux-saga/effects';
import RNUserDefaults from 'rn-user-defaults';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
@ -19,8 +19,8 @@ import log from '../utils/log';
import I18n from '../i18n';
import database from '../lib/database';
import EventEmitter from '../utils/events';
import Navigation from '../lib/Navigation';
import { inviteLinksRequest } from '../actions/inviteLinks';
import { showErrorAlert } from '../utils/info';
const getServer = state => state.server.server;
const loginWithPasswordCall = args => RocketChat.loginWithPassword(args);
@ -36,11 +36,11 @@ const handleLoginRequest = function* handleLoginRequest({ credentials, logoutOnE
result = yield call(loginWithPasswordCall, credentials);
}
return yield put(loginSuccess(result));
} catch (error) {
if (logoutOnError) {
yield put(logout());
} catch (e) {
if (logoutOnError && (e.data && e.data.message && /you've been logged out by the server/i.test(e.data.message))) {
yield put(logout(true));
} else {
yield put(loginFailure(error));
yield put(loginFailure(e));
}
}
};
@ -142,27 +142,35 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
}
};
const handleLogout = function* handleLogout() {
Navigation.navigate('AuthLoading');
const handleLogout = function* handleLogout({ forcedByServer }) {
yield put(appStart('loading'));
const server = yield select(getServer);
if (server) {
try {
yield call(logoutCall, { server });
const serversDB = database.servers;
// all servers
const serversCollection = serversDB.collections.get('servers');
const servers = yield serversCollection.query().fetch();
// see if there're other logged in servers and selects first one
if (servers.length > 0) {
const newServer = servers[0].id;
const token = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ newServer }`);
if (token) {
return yield put(selectServerRequest(newServer));
// if the user was logged out by the server
if (forcedByServer) {
yield put(appStart('outside'));
showErrorAlert(I18n.t('Logged_out_by_server'), I18n.t('Oops'));
EventEmitter.emit('NewServer', { server });
} else {
const serversDB = database.servers;
// all servers
const serversCollection = serversDB.collections.get('servers');
const servers = yield serversCollection.query().fetch();
// see if there're other logged in servers and selects first one
if (servers.length > 0) {
const newServer = servers[0].id;
const token = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ newServer }`);
if (token) {
return yield put(selectServerRequest(newServer));
}
}
// if there's no servers, go outside
yield put(appStart('outside'));
}
// if there's no servers, go outside
yield put(appStart('outside'));
} catch (e) {
yield put(appStart('outside'));
log(e);
@ -185,7 +193,11 @@ const root = function* root() {
while (true) {
const params = yield take(types.LOGIN.SUCCESS);
const loginSuccessTask = yield fork(handleLoginSuccess, params);
yield take(types.SERVER.SELECT_REQUEST);
// yield take(types.SERVER.SELECT_REQUEST);
yield race({
selectRequest: take(types.SERVER.SELECT_REQUEST),
timeout: delay(2000)
});
yield cancel(loginSuccessTask);
}
};

View File

@ -6,11 +6,13 @@ import { Q } from '@nozbe/watermelondb';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import * as types from '../actions/actionsTypes';
import { roomsSuccess, roomsFailure } from '../actions/rooms';
import { roomsSuccess, roomsFailure, roomsRefresh } from '../actions/rooms';
import database from '../lib/database';
import log from '../utils/log';
import mergeSubscriptionsRooms from '../lib/methods/helpers/mergeSubscriptionsRooms';
import RocketChat from '../lib/rocketchat';
import buildMessage from '../lib/methods/helpers/buildMessage';
import protectedFunction from '../lib/methods/helpers/protectedFunction';
const updateRooms = function* updateRooms({ server, newRoomsUpdatedAt }) {
const serversDB = database.servers;
@ -24,20 +26,26 @@ const updateRooms = function* updateRooms({ server, newRoomsUpdatedAt }) {
});
};
const handleRoomsRequest = function* handleRoomsRequest() {
const handleRoomsRequest = function* handleRoomsRequest({ params }) {
try {
const serversDB = database.servers;
yield RocketChat.subscribeRooms();
RocketChat.subscribeRooms();
const newRoomsUpdatedAt = new Date();
let roomsUpdatedAt;
const server = yield select(state => state.server.server);
const serversCollection = serversDB.collections.get('servers');
const serverRecord = yield serversCollection.find(server);
const { roomsUpdatedAt } = serverRecord;
if (params.allData) {
yield put(roomsRefresh());
} else {
const serversCollection = serversDB.collections.get('servers');
const serverRecord = yield serversCollection.find(server);
({ roomsUpdatedAt } = serverRecord);
}
const [subscriptionsResult, roomsResult] = yield RocketChat.getRooms(roomsUpdatedAt);
const { subscriptions } = mergeSubscriptionsRooms(subscriptionsResult, roomsResult);
const db = database.active;
const subCollection = db.collections.get('subscriptions');
const messagesCollection = db.collections.get('messages');
if (subscriptions.length) {
const subsIds = subscriptions.map(sub => sub.rid);
@ -46,6 +54,14 @@ const handleRoomsRequest = function* handleRoomsRequest() {
const subsToCreate = subscriptions.filter(i1 => !existingSubs.find(i2 => i1._id === i2._id));
// TODO: subsToDelete?
const lastMessages = subscriptions
.map(sub => sub.lastMessage && buildMessage(sub.lastMessage))
.filter(lm => lm);
const lastMessagesIds = lastMessages.map(lm => lm._id);
const existingMessages = yield messagesCollection.query(Q.where('id', Q.oneOf(lastMessagesIds))).fetch();
const messagesToUpdate = existingMessages.filter(i1 => lastMessages.find(i2 => i1.id === i2._id));
const messagesToCreate = lastMessages.filter(i1 => !existingMessages.find(i2 => i1._id === i2.id));
const allRecords = [
...subsToCreate.map(subscription => subCollection.prepareCreate((s) => {
s._raw = sanitizedRaw({ id: subscription.rid }, subCollection.schema);
@ -56,6 +72,17 @@ const handleRoomsRequest = function* handleRoomsRequest() {
return subscription.prepareUpdate(() => {
Object.assign(subscription, newSub);
});
}),
...messagesToCreate.map(message => messagesCollection.prepareCreate(protectedFunction((m) => {
m._raw = sanitizedRaw({ id: message._id }, messagesCollection.schema);
m.subscription.id = message.rid;
return Object.assign(m, message);
}))),
...messagesToUpdate.map((message) => {
const newMessage = lastMessages.find(m => m._id === message.id);
return message.prepareUpdate(protectedFunction(() => {
Object.assign(message, newMessage);
}));
})
];

8
app/selectors/login.js Normal file
View File

@ -0,0 +1,8 @@
import { createSelector } from 'reselect';
const getUser = state => state.login.user || {};
export const getUserSelector = createSelector(
[getUser],
user => user
);

View File

@ -112,6 +112,11 @@ export const initTabletNav = (setState) => {
KeyCommands.deleteKeyCommands([...defaultCommands, ...keyCommands]);
setState({ inside: false, showModal: false });
}
if (routeName === 'ModalBlockView') {
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
setState({ showModal: true });
return null;
}
if (routeName === 'RoomView') {
const resetAction = StackActions.reset({

View File

@ -1,7 +1,7 @@
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
export const ThemeContext = React.createContext(null);
export const ThemeContext = React.createContext({ theme: 'light' });
export function withTheme(Component) {
const ThemedComponent = props => (

View File

@ -1,3 +1,23 @@
import { Alert } from 'react-native';
import I18n from '../i18n';
export const showErrorAlert = (message, title, onPress = () => {}) => Alert.alert(title, message, [{ text: 'OK', onPress }], { cancelable: true });
export const showConfirmationAlert = ({ message, callToAction, onPress }) => (
Alert.alert(
I18n.t('Are_you_sure_question_mark'),
message,
[
{
text: I18n.t('Cancel'),
style: 'cancel'
},
{
text: callToAction,
style: 'destructive',
onPress
}
],
{ cancelable: false }
)
);

96
app/utils/review.js Normal file
View File

@ -0,0 +1,96 @@
import { Alert, Linking, AsyncStorage } from 'react-native';
import { isIOS } from './deviceInfo';
import I18n from '../i18n';
import { showErrorAlert } from './info';
import { STORE_REVIEW_LINK } from '../constants/links';
const store = isIOS ? 'App Store' : 'Play Store';
const reviewKey = 'reviewKey';
const reviewDelay = 2000;
const numberOfDays = 7;
const numberOfPositiveEvent = 5;
const daysBetween = (date1, date2) => {
const one_day = 1000 * 60 * 60 * 24;
const date1_ms = date1.getTime();
const date2_ms = date2.getTime();
const difference_ms = date2_ms - date1_ms;
return Math.round(difference_ms / one_day);
};
const onCancelPress = () => {
try {
const data = JSON.stringify({ doneReview: true });
return AsyncStorage.setItem(reviewKey, data);
} catch (e) {
// do nothing
}
};
export const onReviewPress = async() => {
await onCancelPress();
try {
const supported = await Linking.canOpenURL(STORE_REVIEW_LINK);
if (supported) {
Linking.openURL(STORE_REVIEW_LINK);
}
} catch (e) {
showErrorAlert(I18n.t('Review_app_unable_store', { store }));
}
};
const onAskMeLaterPress = () => {
try {
const data = JSON.stringify({ lastReview: new Date().getTime() });
return AsyncStorage.setItem(reviewKey, data);
} catch (e) {
// do nothing
}
};
const onReviewButton = { text: I18n.t('Review_app_yes'), onPress: onReviewPress };
const onAskMeLaterButton = { text: I18n.t('Review_app_later'), onPress: onAskMeLaterPress };
const onCancelButton = { text: I18n.t('Review_app_no'), onPress: onCancelPress };
const askReview = () => Alert.alert(
I18n.t('Review_app_title'),
I18n.t('Review_app_desc', { store }),
isIOS
? [onReviewButton, onAskMeLaterButton, onCancelButton]
: [onAskMeLaterButton, onCancelButton, onReviewButton],
{
cancelable: true,
onDismiss: onAskMeLaterPress
}
);
const tryReview = async() => {
const data = await AsyncStorage.getItem(reviewKey) || '{}';
const reviewData = JSON.parse(data);
const { lastReview = 0, doneReview = false } = reviewData;
const lastReviewDate = new Date(lastReview);
// if ask me later was pressed earlier, we can ask for review only after {{numberOfDays}} days
// if there's no review and it wasn't dismissed by the user
if (daysBetween(lastReviewDate, new Date()) >= numberOfDays && !doneReview) {
setTimeout(askReview, reviewDelay);
}
};
class ReviewApp {
positiveEventCount = 0;
pushPositiveEvent = () => {
if (this.positiveEventCount >= numberOfPositiveEvent) {
return;
}
this.positiveEventCount += 1;
if (this.positiveEventCount === numberOfPositiveEvent) {
tryReview();
}
}
}
export const Review = new ReviewApp();

View File

@ -11,6 +11,7 @@ import styles from '../Styles';
import { themedHeader } from '../../utils/navigation';
import { withTheme } from '../../theme';
import { themes } from '../../constants/colors';
import { getUserSelector } from '../../selectors/login';
class AdminPanelView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => ({
@ -21,12 +22,12 @@ class AdminPanelView extends React.Component {
static propTypes = {
baseUrl: PropTypes.string,
authToken: PropTypes.string,
token: PropTypes.string,
theme: PropTypes.string
}
render() {
const { baseUrl, authToken, theme } = this.props;
const { baseUrl, token, theme } = this.props;
if (!baseUrl) {
return null;
}
@ -35,7 +36,7 @@ class AdminPanelView extends React.Component {
<StatusBar theme={theme} />
<WebView
source={{ uri: `${ baseUrl }/admin/info?layout=embedded` }}
injectedJavaScript={`Meteor.loginWithToken('${ authToken }', function() { })`}
injectedJavaScript={`Meteor.loginWithToken('${ token }', function() { })`}
/>
</SafeAreaView>
);
@ -43,8 +44,8 @@ class AdminPanelView extends React.Component {
}
const mapStateToProps = state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
authToken: state.login.user && state.login.user.token
baseUrl: state.server.server,
token: getUserSelector(state).token
});
export default connect(mapStateToProps)(withTheme(AdminPanelView));

View File

@ -19,6 +19,7 @@ import { formatAttachmentUrl } from '../lib/utils';
import RCActivityIndicator from '../containers/ActivityIndicator';
import { SaveButton, CloseModalButton } from '../containers/HeaderButton';
import { isAndroid } from '../utils/deviceInfo';
import { getUserSelector } from '../selectors/login';
const styles = StyleSheet.create({
container: {
@ -142,11 +143,8 @@ class AttachmentView extends React.Component {
}
const mapStateToProps = state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
user: {
id: state.login.user && state.login.user.id,
token: state.login.user && state.login.user.token
}
baseUrl: state.server.server,
user: getUserSelector(state)
});
export default connect(mapStateToProps)(withTheme(AttachmentView));

View File

@ -22,6 +22,8 @@ import StatusBar from '../containers/StatusBar';
import { SWITCH_TRACK_COLOR, themes } from '../constants/colors';
import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation';
import { Review } from '../utils/review';
import { getUserSelector } from '../selectors/login';
const styles = StyleSheet.create({
container: {
@ -201,6 +203,8 @@ class CreateChannelView extends React.Component {
create({
name: channelName, users, type, readOnly, broadcast
});
Review.pushPositiveEvent();
}
removeUser = (user) => {
@ -362,16 +366,13 @@ class CreateChannelView extends React.Component {
}
const mapStateToProps = state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
baseUrl: state.server.server,
error: state.createChannel.error,
failure: state.createChannel.failure,
isFetching: state.createChannel.isFetching,
result: state.createChannel.result,
users: state.selectedUsers.users,
user: {
id: state.login.user && state.login.user.id,
token: state.login.user && state.login.user.token
}
user: getUserSelector(state)
});
const mapDispatchToProps = dispatch => ({

View File

@ -23,6 +23,7 @@ import { withTheme } from '../../theme';
import { themes } from '../../constants/colors';
import styles from './styles';
import { themedHeader } from '../../utils/navigation';
import { getUserSelector } from '../../selectors/login';
class DirectoryView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => {
@ -253,11 +254,8 @@ class DirectoryView extends React.Component {
}
const mapStateToProps = state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
user: {
id: state.login.user && state.login.user.id,
token: state.login.user && state.login.user.token
},
baseUrl: state.server.server,
user: getUserSelector(state),
isFederationEnabled: state.settings.FEDERATION_Enabled
});

View File

@ -52,7 +52,7 @@ class InviteUsersView extends React.Component {
share = () => {
const { invite } = this.props;
if (!invite) {
if (!invite || !invite.url) {
return;
}
Share.share({ message: invite.url });

View File

@ -2,14 +2,27 @@ import React from 'react';
import PropTypes from 'prop-types';
import JitsiMeet, { JitsiMeetView as RNJitsiMeetView } from 'react-native-jitsi-meet';
import BackgroundTimer from 'react-native-background-timer';
import { connect } from 'react-redux';
import RocketChat from '../lib/rocketchat';
import { getUserSelector } from '../selectors/login';
import sharedStyles from './Styles';
const formatUrl = (url, baseUrl, uriSize, avatarAuthURLFragment) => (
`${ baseUrl }/avatar/${ url }?format=png&width=${ uriSize }&height=${ uriSize }${ avatarAuthURLFragment }`
);
class JitsiMeetView extends React.Component {
static propTypes = {
navigation: PropTypes.object
navigation: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.shape({
id: PropTypes.string,
username: PropTypes.string,
name: PropTypes.string,
token: PropTypes.string
})
}
constructor(props) {
@ -21,14 +34,25 @@ class JitsiMeetView extends React.Component {
}
componentDidMount() {
const { navigation } = this.props;
const { navigation, user, baseUrl } = this.props;
const {
name: displayName, id: userId, token, username
} = user;
const avatarAuthURLFragment = `&rc_token=${ token }&rc_uid=${ userId }`;
const avatar = formatUrl(username, baseUrl, 100, avatarAuthURLFragment);
setTimeout(() => {
const userInfo = {
displayName,
avatar
};
const url = navigation.getParam('url');
const onlyAudio = navigation.getParam('onlyAudio', false);
if (onlyAudio) {
JitsiMeet.audioCall(url);
JitsiMeet.audioCall(url, userInfo);
} else {
JitsiMeet.call(url);
JitsiMeet.call(url, userInfo);
}
}, 1000);
}
@ -71,4 +95,9 @@ class JitsiMeetView extends React.Component {
}
}
export default JitsiMeetView;
const mapStateToProps = state => ({
user: getUserSelector(state),
baseUrl: state.server.server
});
export default connect(mapStateToProps)(JitsiMeetView);

View File

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FlatList } from 'react-native';
import { connect } from 'react-redux';
import { SafeAreaView, NavigationActions } from 'react-navigation';
import { SafeAreaView } from 'react-navigation';
import RocketChat from '../../lib/rocketchat';
import I18n from '../../i18n';
@ -18,6 +18,9 @@ import Separator from '../../containers/Separator';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
import { appStart as appStartAction } from '../../actions';
import { getUserSelector } from '../../selectors/login';
import database from '../../lib/database';
const LANGUAGES = [
{
@ -29,6 +32,9 @@ const LANGUAGES = [
}, {
label: 'English',
value: 'en'
}, {
label: 'Español (ES)',
value: 'es-ES'
}, {
label: 'Français',
value: 'fr'
@ -41,6 +47,12 @@ const LANGUAGES = [
}, {
label: 'Russian',
value: 'ru'
}, {
label: 'Nederlands',
value: 'nl'
}, {
label: 'Italiano',
value: 'it'
}
];
@ -51,23 +63,23 @@ class LanguageView extends React.Component {
})
static propTypes = {
userLanguage: PropTypes.string,
navigation: PropTypes.object,
user: PropTypes.object,
setUser: PropTypes.func,
appStart: PropTypes.func,
theme: PropTypes.string
}
constructor(props) {
super(props);
this.state = {
language: props.userLanguage ? props.userLanguage : 'en',
language: props.user ? props.user.language : 'en',
saving: false
};
}
shouldComponentUpdate(nextProps, nextState) {
const { language, saving } = this.state;
const { userLanguage, theme } = this.props;
const { user, theme } = this.props;
if (nextProps.theme !== theme) {
return true;
}
@ -77,15 +89,15 @@ class LanguageView extends React.Component {
if (nextState.saving !== saving) {
return true;
}
if (nextProps.userLanguage !== userLanguage) {
if (nextProps.user.language !== user.language) {
return true;
}
return false;
}
formIsChanged = (language) => {
const { userLanguage } = this.props;
return (userLanguage !== language);
const { user } = this.props;
return (user.language !== language);
}
submit = async(language) => {
@ -95,12 +107,12 @@ class LanguageView extends React.Component {
this.setState({ saving: true });
const { userLanguage, setUser, navigation } = this.props;
const { user, setUser, appStart } = this.props;
const params = {};
// language
if (userLanguage !== language) {
if (user.language !== language) {
params.language = language;
}
@ -108,18 +120,27 @@ class LanguageView extends React.Component {
await RocketChat.saveUserPreferences(params);
setUser({ language: params.language });
this.setState({ saving: false });
setTimeout(() => {
navigation.reset([NavigationActions.navigate({ routeName: 'SettingsView' })], 0);
navigation.navigate('RoomsListView');
}, 300);
const serversDB = database.servers;
const usersCollection = serversDB.collections.get('users');
await serversDB.action(async() => {
try {
const userRecord = await usersCollection.find(user.id);
await userRecord.update((record) => {
record.language = params.language;
});
} catch (e) {
// do nothing
}
});
await appStart('loading');
await appStart('inside');
} catch (e) {
this.setState({ saving: false });
setTimeout(() => {
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_preferences') }));
log(e);
}, 300);
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_preferences') }));
log(e);
}
this.setState({ saving: false });
}
renderSeparator = () => {
@ -179,11 +200,12 @@ class LanguageView extends React.Component {
}
const mapStateToProps = state => ({
userLanguage: state.login.user && state.login.user.language
user: getUserSelector(state)
});
const mapDispatchToProps = dispatch => ({
setUser: params => dispatch(setUserAction(params))
setUser: params => dispatch(setUserAction(params)),
appStart: params => dispatch(appStartAction(params))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LanguageView));

View File

@ -17,13 +17,14 @@ import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import { withSplit } from '../../split';
import { themedHeader } from '../../utils/navigation';
import { getUserSelector } from '../../selectors/login';
const ACTION_INDEX = 0;
const CANCEL_INDEX = 1;
class MessagesView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => ({
title: navigation.state.params.name,
title: I18n.t(navigation.state.params.name),
...themedHeader(screenProps.theme)
});
@ -73,6 +74,14 @@ class MessagesView extends React.Component {
return false;
}
navToRoomInfo = (navParam) => {
const { navigation, user } = this.props;
if (navParam.rid === user.id) {
return;
}
navigation.navigate('RoomInfoView', navParam);
}
defineMessagesViewContent = (name) => {
const { messages } = this.state;
const { user, baseUrl, theme } = this.props;
@ -87,7 +96,8 @@ class MessagesView extends React.Component {
isHeader: true,
attachments: item.attachments || [],
showAttachment: this.showAttachment,
getCustomEmoji: this.getCustomEmoji
getCustomEmoji: this.getCustomEmoji,
navToRoomInfo: this.navToRoomInfo
});
return ({
@ -306,12 +316,8 @@ class MessagesView extends React.Component {
}
const mapStateToProps = state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
},
baseUrl: state.server.server,
user: getUserSelector(state),
customEmojis: state.customEmojis
});

278
app/views/ModalBlockView.js Normal file
View File

@ -0,0 +1,278 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';
import { connect } from 'react-redux';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation';
import EventEmitter from '../utils/events';
import { themes } from '../constants/colors';
import { CustomHeaderButtons, Item } from '../containers/HeaderButton';
import { modalBlockWithContext } from '../containers/UIKit/MessageBlock';
import RocketChat from '../lib/rocketchat';
import ActivityIndicator from '../containers/ActivityIndicator';
import { MODAL_ACTIONS, CONTAINER_TYPES } from '../lib/methods/actions';
import sharedStyles from './Styles';
import { textParser } from '../containers/UIKit/utils';
import Navigation from '../lib/Navigation';
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 16
},
content: {
paddingVertical: 16
},
submit: {
...sharedStyles.textSemibold,
fontSize: 16
}
});
Object.fromEntries = Object.fromEntries || (arr => arr.reduce((acc, [k, v]) => ((acc[k] = v, acc)), {}));
const groupStateByBlockIdMap = (obj, [key, { blockId, value }]) => {
obj[blockId] = obj[blockId] || {};
obj[blockId][key] = value;
return obj;
};
const groupStateByBlockId = obj => Object.entries(obj).reduce(groupStateByBlockIdMap, {});
const filterInputFields = ({ element, elements = [] }) => {
if (element && element.initialValue) {
return true;
}
if (elements.length && elements.map(e => ({ element: e })).filter(filterInputFields).length) {
return true;
}
};
const mapElementToState = ({ element, blockId, elements = [] }) => {
if (elements.length) {
return elements.map(e => ({ element: e, blockId })).filter(filterInputFields).map(mapElementToState);
}
return [element.actionId, { value: element.initialValue, blockId }];
};
const reduceState = (obj, el) => (Array.isArray(el[0]) ? { ...obj, ...Object.fromEntries(el) } : { ...obj, [el[0]]: el[1] });
class ModalBlockView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => {
const { theme, closeModal } = screenProps;
const data = navigation.getParam('data');
const cancel = navigation.getParam('cancel', () => {});
const submitting = navigation.getParam('submitting', false);
const { view } = data;
const { title, submit, close } = view;
return {
title: textParser([title]),
...themedHeader(theme),
headerLeft: (
<CustomHeaderButtons>
<Item
title={textParser([close.text])}
style={styles.submit}
onPress={!submitting && (() => cancel({ closeModal }))}
testID='close-modal-uikit'
/>
</CustomHeaderButtons>
),
headerRight: (
<CustomHeaderButtons>
<Item
title={textParser([submit.text])}
style={styles.submit}
onPress={!submitting && (navigation.getParam('submit', () => {}))}
testID='submit-modal-uikit'
/>
</CustomHeaderButtons>
)
};
}
static propTypes = {
navigation: PropTypes.object,
theme: PropTypes.string,
language: PropTypes.string,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
})
}
constructor(props) {
super(props);
this.submitting = false;
const { navigation } = props;
const data = navigation.getParam('data');
this.values = data.view.blocks.filter(filterInputFields).map(mapElementToState).reduce(reduceState, {});
this.state = {
data,
loading: false
};
}
componentDidMount() {
const { data } = this.state;
const { navigation } = this.props;
const { viewId } = data;
navigation.setParams({ submit: this.submit, cancel: this.cancel });
EventEmitter.addEventListener(viewId, this.handleUpdate);
}
shouldComponentUpdate(nextProps, nextState) {
if (!isEqual(nextProps, this.props)) {
return true;
}
if (!isEqual(nextState, this.state)) {
return true;
}
return false;
}
componentDidUpdate(prevProps) {
const { navigation } = this.props;
const oldData = prevProps.navigation.getParam('data', {});
const newData = navigation.getParam('data', {});
if (!isEqual(oldData, newData)) {
navigation.push('ModalBlockView', { data: newData });
}
}
componentWillUnmount() {
const { data } = this.state;
const { viewId } = data;
EventEmitter.removeListener(viewId, this.handleUpdate);
}
handleUpdate = ({ type, ...data }) => {
if ([MODAL_ACTIONS.ERRORS].includes(type)) {
const { errors } = data;
this.setState({ errors });
} else {
this.setState({ data });
}
};
cancel = async({ closeModal }) => {
const { data } = this.state;
const { appId, viewId, view } = data;
// handle tablet case
if (closeModal) {
closeModal();
} else {
Navigation.back();
}
try {
await RocketChat.triggerCancel({
appId,
viewId,
view: {
...view,
id: viewId,
state: groupStateByBlockId(this.values)
},
isCleared: true
});
} catch (e) {
// do nothing
}
}
submit = async() => {
const { data } = this.state;
const { navigation } = this.props;
navigation.setParams({ submitting: true });
const { appId, viewId } = data;
this.setState({ loading: true });
try {
await RocketChat.triggerSubmitView({
viewId,
appId,
payload: {
view: {
id: viewId,
state: groupStateByBlockId(this.values)
}
}
});
} catch (e) {
// do nothing
}
navigation.setParams({ submitting: false });
this.setState({ loading: false });
};
action = async({ actionId, value, blockId }) => {
const { data } = this.state;
const { mid, appId, viewId } = data;
await RocketChat.triggerBlockAction({
container: {
type: CONTAINER_TYPES.VIEW,
id: viewId
},
actionId,
appId,
value,
blockId,
mid
});
this.changeState({ actionId, value, blockId });
}
changeState = ({ actionId, value, blockId = 'default' }) => {
this.values[actionId] = {
blockId,
value
};
};
render() {
const { data, loading, errors } = this.state;
const { theme, language } = this.props;
const { values } = this;
const { view } = data;
const { blocks } = view;
return (
<KeyboardAwareScrollView
style={[
styles.container,
{ backgroundColor: themes[theme].auxiliaryBackground }
]}
keyboardShouldPersistTaps='always'
>
<View style={styles.content}>
{
React.createElement(
modalBlockWithContext({
action: this.action,
state: this.changeState,
...data
}),
{
blocks,
errors,
language,
values
}
)
}
</View>
{loading ? <ActivityIndicator absolute size='large' theme={theme} /> : null}
</KeyboardAwareScrollView>
);
}
}
const mapStateToProps = state => ({
language: state.login.user && state.login.user.language
});
export default connect(mapStateToProps)(withTheme(ModalBlockView));

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