Merge beta into master (#1759)
This commit is contained in:
parent
1cd5fa8625
commit
69aff7e56a
|
@ -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
|
||||
|
|
|
@ -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 | ✅ |
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
export default {
|
||||
window: () => null
|
||||
};
|
||||
|
||||
export const uiKitMessage = () => () => null;
|
||||
|
||||
export const uiKitModal = () => () => null;
|
||||
|
||||
export class UiKitParserMessage {}
|
||||
|
||||
export class UiKitParserModal {}
|
|
@ -0,0 +1,8 @@
|
|||
export class Client { }
|
||||
|
||||
export default {
|
||||
bugsnag: () => '',
|
||||
leaveBreadcrumb: () => '',
|
||||
notify: () => '',
|
||||
loggerConfig: () => ''
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
analytics: null
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 |
|
@ -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',
|
||||
|
|
|
@ -22,9 +22,10 @@ export function loginFailure(err) {
|
|||
};
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
export function logout(forcedByServer = false) {
|
||||
return {
|
||||
type: types.LOGOUT
|
||||
type: types.LOGOUT,
|
||||
forcedByServer
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }`;
|
||||
|
|
|
@ -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'
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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 }]} />;
|
||||
});
|
||||
|
|
|
@ -37,7 +37,7 @@ const styles = StyleSheet.create({
|
|||
...sharedStyles.textRegular
|
||||
},
|
||||
cancel: {
|
||||
marginRight: 10
|
||||
marginRight: 15
|
||||
},
|
||||
cancelText: {
|
||||
...sharedStyles.textRegular,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 }]}
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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);
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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);
|
|
@ -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);
|
||||
}];
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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.'
|
||||
};
|
||||
|
|
|
@ -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.'
|
||||
};
|
||||
|
|
|
@ -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'
|
||||
};
|
|
@ -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'
|
||||
};
|
|
@ -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'
|
||||
};
|
|
@ -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.'
|
||||
};
|
||||
|
|
55
app/index.js
55
app/index.js
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -73,4 +73,6 @@ export default class Message extends Model {
|
|||
@json('translations', sanitizer) translations;
|
||||
|
||||
@field('tmsg') tmsg;
|
||||
|
||||
@json('blocks', sanitizer) blocks;
|
||||
}
|
||||
|
|
|
@ -11,4 +11,6 @@ export default class SlashCommand extends Model {
|
|||
@field('client_only') clientOnly;
|
||||
|
||||
@field('provides_preview') providesPreview;
|
||||
|
||||
@field('app_id') appId;
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
]
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -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 }
|
||||
]
|
||||
})
|
||||
]
|
||||
|
|
|
@ -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 });
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }) {
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}));
|
||||
})
|
||||
];
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
const getUser = state => state.login.user || {};
|
||||
|
||||
export const getUserSelector = createSelector(
|
||||
[getUser],
|
||||
user => user
|
||||
);
|
|
@ -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({
|
||||
|
|
|
@ -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 => (
|
||||
|
|
|
@ -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 }
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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();
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue