Merge beta into master (#1637)

This commit is contained in:
Diego Mello 2020-01-29 17:20:36 -03:00 committed by GitHub
parent 13da75ea44
commit 1cd5fa8625
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
216 changed files with 11956 additions and 9284 deletions

View File

@ -197,7 +197,8 @@ jobs:
npx jetify
cd android
if [[ $KEYSTORE ]]; then
./gradlew bundleRelease
# TODO: enable app bundle again
./gradlew assembleRelease
else
./gradlew assembleDebug
fi

View File

@ -1,4 +1,4 @@
# Rocket.Chat React Native Mobile
# Rocket.Chat Mobile
[![Project Dependencies](https://david-dm.org/RocketChat/Rocket.Chat.ReactNative.svg)](https://david-dm.org/RocketChat/Rocket.Chat.ReactNative)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/bb15e2392a71473ea59d3f634f35c54e)](https://www.codacy.com/app/RocketChat/Rocket.Chat.ReactNative?utm_source=github.com&utm_medium=referral&utm_content=RocketChat/Rocket.Chat.ReactNative&utm_campaign=badger)
@ -8,6 +8,16 @@
**Supported Server Versions:** 0.70.0+
## Download
### Official apps
<a href="https://play.google.com/store/apps/details?id=chat.rocket.android">
<img alt="Download on Google Play" src="https://play.google.com/intl/en_us/badges/images/badge_new.png" height=43>
</a>
<a href="https://apps.apple.com/us/app/rocket-chat/id1148741252">
<img alt="Download on App Store" src="https://user-images.githubusercontent.com/7317008/43209852-4ca39622-904b-11e8-8ce1-cdc3aee76ae9.png" height=43>
</a>
### Experimental apps
<a href="https://play.google.com/store/apps/details?id=chat.rocket.reactnative">
<img alt="Download on Google Play" src="https://play.google.com/intl/en_us/badges/images/badge_new.png" height=43>
</a>
@ -19,11 +29,17 @@
### TestFlight
You can signup to our TestFlight builds by acessing this link: https://testflight.apple.com/join/7I3dLCNT.
You can signup to our TestFlight builds by accessing these links:
- Official: https://testflight.apple.com/join/3gcYeoMr
- Experimental: https://testflight.apple.com/join/7I3dLCNT.
### Google Play beta
You can subscribe to Google Play Beta program and download latest versions: https://play.google.com/store/apps/details?id=chat.rocket.reactnative
You can subscribe to Google Play Beta program and download latest versions:
- Official: https://play.google.com/store/apps/details?id=chat.rocket.android
- Experimental: https://play.google.com/store/apps/details?id=chat.rocket.reactnative
## Reporting an Issue
@ -57,13 +73,8 @@ If you don't need multiple servers, there is a branch `single-server` just for t
Readme will guide you on how to config.
## Current priorities
1) Jitsi integration
2) Notification Preferences
3) Two-way authentication
4) Bugsnag
5) Optional Analytics
6) Typescript
7) Prettier
1) Omnichannel support
2) E2E encryption
## Features
| Feature | Status |
@ -71,6 +82,7 @@ Readme will guide you on how to config.
| Jitsi Integration | ✅ |
| Federation (Directory) | ✅ |
| Discussions | ❌ |
| Omnichannel | ❌ |
| Threads | ✅ |
| Record Audio | ✅ |
| Record Video | ✅ |
@ -83,18 +95,18 @@ Readme will guide you on how to config.
| Grouped messages | ✅ |
| Mark room as read | ✅ |
| Mark room as unread | ✅ |
| Tablet Support | |
| Tablet Support | |
| Read receipt | ✅ |
| Broadbast Channel | ✅ |
| Authentication via SAML | ✅ |
| Authentication via CAS | ✅ |
| Custom Fields on Signup | ✅ |
| Report message | ✅ |
| Theming | |
| Theming | |
| Settings -> Review the App | ❌ |
| Settings -> Default Browser | ❌ |
| Admin panel | ✅ |
| Reply message from notification | |
| Reply message from notification | |
| Unread counter banner on message list | ✅ |
| E2E Encryption | ❌ |
| Join a Protected Room | ❌ |
@ -106,7 +118,7 @@ Readme will guide you on how to config.
| Accessibility (Medium) | ❌ |
| Accessibility (Advanced) | ❌ |
| Authentication via Meteor | ❌ |
| Authentication via Wordpress | |
| Authentication via Wordpress | |
| Authentication via Custom OAuth | ✅ |
| Add user to the room | ✅ |
| Send message | ✅ |

File diff suppressed because it is too large Load Diff

View File

@ -1,55 +0,0 @@
# To learn about Buck see [Docs](https://buckbuild.com/).
# To run your application with Buck:
# - install Buck
# - `npm start` - to start the packager
# - `cd android`
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
# - `buck install -r android/app` - compile, install and run application
#
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
lib_deps = []
create_aar_targets(glob(["libs/*.aar"]))
create_jar_targets(glob(["libs/*.jar"]))
android_library(
name = "all-libs",
exported_deps = lib_deps,
)
android_library(
name = "app-code",
srcs = glob([
"src/main/java/**/*.java",
]),
deps = [
":all-libs",
":build_config",
":res",
],
)
android_build_config(
name = "build_config",
package = "chat.rocket.reactnative",
)
android_resource(
name = "res",
package = "chat.rocket.reactnative",
res = "src/main/res",
)
android_binary(
name = "app",
keystore = "//android/keystores:debug",
manifest = "src/main/AndroidManifest.xml",
package_type = "debug",
deps = [
":app-code",
],
)

View File

@ -83,7 +83,7 @@ project.ext.react = [
entryFile: "index.js",
bundleAssetName: "app.bundle",
iconFontNames: [ 'custom.ttf' ],
enableHermes: false, // clean and rebuild if changing
enableHermes: true, // clean and rebuild if changing
]
apply from: "../../node_modules/react-native/react.gradle"
@ -138,7 +138,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "1.25.0"
versionName "4.3.0"
vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
}
@ -217,6 +217,10 @@ dependencies {
} else {
implementation jscFlavor
}
implementation "com.google.code.gson:gson:2.8.5"
implementation "com.github.bumptech.glide:glide:4.9.0"
annotationProcessor "com.github.bumptech.glide:compiler:4.9.0"
}
// Run this once to be able to run the application with BUCK

View File

@ -20,14 +20,21 @@
android:networkSecurityConfig="@xml/network_security_config"
>
<activity
android:name="com.zoontek.rnbootsplash.RNBootSplashActivity"
android:theme="@style/BootTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
</intent-filter>
<intent-filter android:label="@string/app_name">
@ -40,6 +47,15 @@
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<receiver
android:name=".ReplyBroadcast"
android:enabled="true"
android:exported="false" />
<receiver
android:name=".DismissNotification"
android:enabled="true"
android:exported="false" >
</receiver>
<activity
android:noHistory="true"
android:name=".share.ShareActivity"

View File

@ -4,56 +4,139 @@ import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Intent;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings.System;
import android.media.RingtoneManager;
import com.google.gson.*;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
import java.util.concurrent.ExecutionException;
import java.lang.InterruptedException;
import com.facebook.react.bridge.ReactApplicationContext;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import com.wix.reactnativenotifications.core.notification.PushNotification;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
public class CustomPushNotification extends PushNotification {
public static ReactApplicationContext reactApplicationContext;
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
reactApplicationContext = new ReactApplicationContext(context);
}
private static Map<String, List<String>> notificationMessages = new HashMap<String, List<String>>();
public static String KEY_REPLY = "KEY_REPLY";
public static String NOTIFICATION_ID = "NOTIFICATION_ID";
public static void clearMessages(int notId) {
notificationMessages.remove(Integer.toString(notId));
}
@Override
public void onReceived() throws InvalidNotificationException {
final Bundle bundle = mNotificationProps.asBundle();
String notId = bundle.getString("notId");
String message = bundle.getString("message");
if (notificationMessages.get(notId) == null) {
notificationMessages.put(notId, new ArrayList<String>());
}
notificationMessages.get(notId).add(message);
super.postNotification(notId != null ? Integer.parseInt(notId) : 1);
notifyReceivedToJS();
}
@Override
public void onOpened() {
Bundle bundle = mNotificationProps.asBundle();
final String notId = bundle.getString("notId");
notificationMessages.remove(notId);
digestNotification();
}
@Override
protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
final Notification.Builder notification = new Notification.Builder(mContext);
Bundle bundle = mNotificationProps.asBundle();
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
int largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
String title = bundle.getString("title");
String message = bundle.getString("message");
String notId = bundle.getString("notId");
String CHANNEL_ID = "rocketchatrn_channel_01";
String CHANNEL_NAME = "All";
final Notification.Builder notification = new Notification.Builder(mContext)
.setSmallIcon(smallIconResId)
notification
.setContentIntent(intent)
.setContentTitle(title)
.setContentText(message)
.setStyle(new Notification.BigTextStyle().bigText(message))
.setPriority(Notification.PRIORITY_HIGH)
.setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
notification.setColor(mContext.getColor(R.color.notification_text));
}
Integer notificationId = notId != null ? Integer.parseInt(notId) : 1;
notificationChannel(notification);
notificationIcons(notification, bundle);
notificationStyle(notification, notificationId, bundle);
notificationReply(notification, notificationId, bundle);
notificationDismiss(notification, notificationId);
return notification;
}
private void notifyReceivedToJS() {
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
}
private Bitmap getAvatar(String uri) {
try {
return Glide.with(mContext)
.asBitmap()
.apply(RequestOptions.bitmapTransform(new RoundedCorners(10)))
.load(uri)
.submit(100, 100)
.get();
} catch (final ExecutionException | InterruptedException e) {
return null;
}
}
private void notificationIcons(Notification.Builder notification, Bundle bundle) {
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
Gson gson = new Gson();
Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class);
notification
.setSmallIcon(smallIconResId)
.setLargeIcon(getAvatar(ejson.getAvatarUri()));
}
private void notificationChannel(Notification.Builder notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
String CHANNEL_ID = "rocketchatrn_channel_01";
String CHANNEL_NAME = "All";
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT);
@ -63,10 +146,65 @@ public class CustomPushNotification extends PushNotification {
notification.setChannelId(CHANNEL_ID);
}
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
notification.setLargeIcon(largeIconBitmap);
return notification;
}
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());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
notification.setColor(mContext.getColor(R.color.notification_text));
}
notification.setStyle(messageStyle);
}
private void notificationReply(Notification.Builder notification, int notificationId, Bundle bundle) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return;
}
String label = "Reply";
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
Intent replyIntent = new Intent(mContext, ReplyBroadcast.class);
replyIntent.setAction(KEY_REPLY);
replyIntent.putExtra("pushNotification", bundle);
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY)
.setLabel(label)
.build();
CharSequence title = label;
Notification.Action replyAction = new Notification.Action.Builder(smallIconResId, title, replyPendingIntent)
.addRemoteInput(remoteInput)
.setAllowGeneratedReplies(true)
.build();
notification
.setShowWhen(true)
.addAction(replyAction);
}
private void notificationDismiss(Notification.Builder notification, int notificationId) {
Intent intent = new Intent(mContext, DismissNotification.class);
intent.putExtra(NOTIFICATION_ID, notificationId);
PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, intent, 0);
notification.setDeleteIntent(dismissPendingIntent);
}
}

View File

@ -0,0 +1,13 @@
package chat.rocket.reactnative;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class DismissNotification extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int notId = intent.getExtras().getInt(CustomPushNotification.NOTIFICATION_ID);
CustomPushNotification.clearMessages(notId);
}
}

View File

@ -0,0 +1,42 @@
package chat.rocket.reactnative;
import android.content.SharedPreferences;
import chat.rocket.userdefaults.RNUserDefaultsModule;
public class Ejson {
String host;
String rid;
String type;
Sender sender;
private String TOKEN_KEY = "reactnativemeteor_usertoken-";
private SharedPreferences sharedPreferences = RNUserDefaultsModule.getPreferences(CustomPushNotification.reactApplicationContext);
public String getAvatarUri() {
if (type == null || !type.equals("d")) {
return null;
}
return serverURL() + "/avatar/" + this.sender.username + "?rc_token=" + token() + "&rc_uid=" + userId();
}
public String token() {
return sharedPreferences.getString(TOKEN_KEY.concat(userId()), "");
}
public String userId() {
return sharedPreferences.getString(TOKEN_KEY.concat(serverURL()), "");
}
public String serverURL() {
String url = this.host;
if (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
return url;
}
private class Sender {
String username;
}
}

View File

@ -5,16 +5,16 @@ import com.facebook.react.ReactRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import android.os.Bundle;
import com.facebook.react.ReactFragmentActivity;
import org.devio.rn.splashscreen.SplashScreen;
import android.content.Intent;
import android.content.res.Configuration;
import com.zoontek.rnbootsplash.RNBootSplash;
public class MainActivity extends ReactFragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.show(this);
super.onCreate(null);
super.onCreate(savedInstanceState);
RNBootSplash.show(R.drawable.launch_screen, MainActivity.this);
}
/**

View File

@ -0,0 +1,157 @@
package chat.rocket.reactnative;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import java.io.IOException;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.HashMap;
import java.util.Map;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import chat.rocket.userdefaults.RNUserDefaultsModule;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class ReplyBroadcast extends BroadcastReceiver {
private Context mContext;
private Bundle bundle;
private NotificationManager notificationManager;
@Override
public void onReceive(Context context, Intent intent) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
final CharSequence message = getReplyMessage(intent);
if (message == null) {
return;
}
mContext = context;
bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
String notId = bundle.getString("notId");
Gson gson = new Gson();
Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class);
replyToMessage(ejson, Integer.parseInt(notId), message);
}
}
protected void replyToMessage(final Ejson ejson, final int notId, final CharSequence message) {
String serverURL = ejson.serverURL();
String rid = ejson.rid;
if (serverURL == null || rid == null) {
return;
}
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
String json = buildMessage(rid, message.toString());
CustomPushNotification.clearMessages(notId);
RequestBody body = RequestBody.create(JSON, json);
Request request = new Request.Builder()
.header("x-auth-token", ejson.token())
.header("x-user-id", ejson.userId())
.url(String.format("%s/api/v1/chat.sendMessage", serverURL))
.post(body)
.build();
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.i("RCNotification", String.format("Reply FAILED exception %s", e.getMessage()));
onReplyFailed(notificationManager, notId);
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
if (response.isSuccessful()) {
Log.d("RCNotification", "Reply SUCCESS");
onReplySuccess(notificationManager, notId);
} else {
Log.i("RCNotification", String.format("Reply FAILED status %s BODY %s", response.code(), response.body().string()));
onReplyFailed(notificationManager, notId);
}
}
});
}
private String getMessageId() {
final String ALPHA_NUMERIC_STRING = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
int count = 17;
StringBuilder builder = new StringBuilder();
while (count-- != 0) {
int character = (int)(Math.random()*ALPHA_NUMERIC_STRING.length());
builder.append(ALPHA_NUMERIC_STRING.charAt(character));
}
return builder.toString();
}
protected String buildMessage(String rid, String message) {
Gson gsonBuilder = new GsonBuilder().create();
Map msgMap = new HashMap();
msgMap.put("_id", getMessageId());
msgMap.put("rid", rid);
msgMap.put("msg", message);
msgMap.put("tmid", null);
Map msg = new HashMap();
msg.put("message", msgMap);
String json = gsonBuilder.toJson(msg);
return json;
}
protected void onReplyFailed(NotificationManager notificationManager, int notId) {
String CHANNEL_ID = "CHANNEL_ID_REPLY_FAILED";
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(channel);
Notification notification =
new Notification.Builder(mContext, CHANNEL_ID)
.setContentTitle("Failed to reply message.")
.setSmallIcon(smallIconResId)
.build();
notificationManager.notify(notId, notification);
}
protected void onReplySuccess(NotificationManager notificationManager, int notId) {
notificationManager.cancel(notId);
}
private CharSequence getReplyMessage(Intent intent) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(CustomPushNotification.KEY_REPLY);
}
return null;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- the background color. it can be a system color or a custom one defined in colors.xml -->
<item android:drawable="@color/splashBackground" />
<item>
<!-- the app logo, centered horizontally and vertically -->
<bitmap
android:src="@drawable/splash"
android:gravity="center" />
</item>
</layer-list>

View File

@ -1,8 +0,0 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<item android:drawable="@color/splashBackground"/>
<item>
<bitmap
android:src="@drawable/launch_screen"
android:gravity="center"/>
</item>
</layer-list>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="@color/splashBackground"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/launch_screen"
android:scaleType="fitCenter"
/>
</RelativeLayout>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="splashBackground" type="color">#000000</item>
</resources>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary_dark">#660B0B0B</color>
<item name="splashBackground" type="color">#FFFFFF</item>
<item name="splashBackground" type="color">#eeeff1</item>
<item name="notification_text" type="color">#CC3333</item>
</resources>

View File

@ -1,7 +1,8 @@
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:colorEdgeEffect">#aaaaaa</item>
<item name="android:textColor">#000000</item>
<item name="colorPrimaryDark">@color/splashBackground</item>
<item name="android:navigationBarColor">@color/splashBackground</item>
</style>
<style name="Share.Window" parent="android:Theme">
@ -18,4 +19,10 @@
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowAnimationStyle">@style/Share.Window</item>
</style>
<style name="BootTheme" parent="AppTheme">
<item name="android:background">@drawable/launch_screen</item>
<item name="colorPrimaryDark">@color/splashBackground</item>
<item name="android:navigationBarColor">@color/splashBackground</item>
</style>
</resources>

View File

@ -55,16 +55,16 @@ allprojects {
}
}
// subprojects { subproject ->
// afterEvaluate {
// if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) {
// android {
// compileSdkVersion 28
// buildToolsVersion "28.0.3"
// defaultConfig {
// targetSdkVersion 28
// }
// }
// }
// }
// }
subprojects { subproject ->
afterEvaluate {
if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) {
android {
compileSdkVersion 28
buildToolsVersion "28.0.3"
defaultConfig {
targetSdkVersion 28
}
}
}
}
}

View File

@ -54,3 +54,11 @@ export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT';
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS';
export const USERS_TYPING = createRequestTypes('USERS_TYPING', ['ADD', 'REMOVE', 'CLEAR']);
export const INVITE_LINKS = createRequestTypes('INVITE_LINKS', [
'SET_TOKEN',
'SET_PARAMS',
'SET_INVITE',
'CREATE',
'CLEAR',
...defaultTypes
]);

View File

@ -0,0 +1,55 @@
import * as types from './actionsTypes';
export function inviteLinksSetToken(token) {
return {
type: types.INVITE_LINKS.SET_TOKEN,
token
};
}
export function inviteLinksRequest(token) {
return {
type: types.INVITE_LINKS.REQUEST,
token
};
}
export function inviteLinksSuccess() {
return {
type: types.INVITE_LINKS.SUCCESS
};
}
export function inviteLinksFailure() {
return {
type: types.INVITE_LINKS.FAILURE
};
}
export function inviteLinksClear() {
return {
type: types.INVITE_LINKS.CLEAR
};
}
export function inviteLinksCreate(rid) {
return {
type: types.INVITE_LINKS.CREATE,
rid
};
}
export function inviteLinksSetParams(params) {
return {
type: types.INVITE_LINKS.SET_PARAMS,
params
};
}
export function inviteLinksSetInvite(invite) {
return {
type: types.INVITE_LINKS.SET_INVITE,
invite
};
}

View File

@ -59,6 +59,9 @@ export default {
Message_TimeFormat: {
type: 'valueAsString'
},
Message_TimeAndDateFormat: {
type: 'valueAsString'
},
Site_Name: {
type: 'valueAsString'
},

View File

@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, TouchableOpacity, FlatList } from 'react-native';
import { shortnameToUnicode } from 'emoji-toolkit';
import { responsive } from 'react-native-responsive-ui';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../utils/scrollPersistTaps';

View File

@ -2,7 +2,6 @@ import React, { Component } from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import { shortnameToUnicode } from 'emoji-toolkit';
import equal from 'deep-equal';
import { connect } from 'react-redux';
import orderBy from 'lodash/orderBy';
@ -15,6 +14,7 @@ import categories from './categories';
import database from '../../lib/database';
import { emojisByCategory } from '../../emojis';
import protectedFunction from '../../lib/methods/helpers/protectedFunction';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import log from '../../utils/log';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';

View File

@ -1,132 +0,0 @@
import React, { useState } from 'react';
import {
View, Text, TouchableWithoutFeedback, StyleSheet, SafeAreaView
} from 'react-native';
import FastImage from 'react-native-fast-image';
import PropTypes from 'prop-types';
import Modal from 'react-native-modal';
import ImageViewer from 'react-native-image-zoom-viewer';
import { Video } from 'expo-av';
import sharedStyles from '../views/Styles';
import { formatAttachmentUrl } from '../lib/utils';
import ActivityIndicator from './ActivityIndicator';
import { themes } from '../constants/colors';
import { withTheme } from '../theme';
const styles = StyleSheet.create({
safeArea: {
flex: 1
},
modal: {
margin: 0
},
titleContainer: {
width: '100%',
alignItems: 'center',
marginVertical: 10
},
title: {
textAlign: 'center',
fontSize: 16,
...sharedStyles.textSemibold
},
description: {
textAlign: 'center',
fontSize: 14,
...sharedStyles.textMedium
},
video: {
flex: 1
}
});
const ModalContent = React.memo(({
attachment, onClose, user, baseUrl, theme
}) => {
if (attachment && attachment.image_url) {
const url = formatAttachmentUrl(attachment.image_url, user.id, user.token, baseUrl);
return (
<SafeAreaView style={styles.safeArea}>
<TouchableWithoutFeedback onPress={onClose}>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: themes[theme].buttonText }]}>{attachment.title}</Text>
{attachment.description ? <Text style={[styles.description, { color: themes[theme].buttonText }]}>{attachment.description}</Text> : null}
</View>
</TouchableWithoutFeedback>
<ImageViewer
imageUrls={[{ url }]}
onClick={onClose}
backgroundColor='transparent'
enableSwipeDown
onSwipeDown={onClose}
renderIndicator={() => null}
renderImage={props => <FastImage {...props} />}
loadingRender={() => <ActivityIndicator size='large' theme={theme} />}
/>
</SafeAreaView>
);
}
if (attachment && attachment.video_url) {
const [loading, setLoading] = useState(true);
const uri = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl);
return (
<>
<Video
source={{ uri }}
rate={1.0}
volume={1.0}
isMuted={false}
resizeMode={Video.RESIZE_MODE_CONTAIN}
shouldPlay
isLooping={false}
style={styles.video}
useNativeControls
onReadyForDisplay={() => setLoading(false)}
onLoadStart={() => setLoading(true)}
onError={console.log}
/>
{ loading ? <ActivityIndicator size='large' theme={theme} absolute /> : null }
</>
);
}
return null;
});
const FileModal = React.memo(({
isVisible, onClose, attachment, user, baseUrl, theme
}) => (
<Modal
style={styles.modal}
isVisible={isVisible}
onBackdropPress={onClose}
onBackButtonPress={onClose}
onSwipeComplete={onClose}
swipeDirection={['up', 'down']}
>
<ModalContent attachment={attachment} onClose={onClose} user={user} baseUrl={baseUrl} theme={theme} />
</Modal>
), (prevProps, nextProps) => (
prevProps.isVisible === nextProps.isVisible && prevProps.loading === nextProps.loading && prevProps.theme === nextProps.theme
));
FileModal.propTypes = {
isVisible: PropTypes.bool,
attachment: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string,
onClose: PropTypes.func
};
FileModal.displayName = 'FileModal';
ModalContent.propTypes = {
attachment: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string,
onClose: PropTypes.func
};
ModalContent.displayName = 'FileModalContent';
export default withTheme(FileModal);

View File

@ -57,6 +57,12 @@ export const MoreButton = React.memo(({ onPress, testID }) => (
</CustomHeaderButtons>
));
export const SaveButton = React.memo(({ onPress, testID }) => (
<CustomHeaderButtons>
<Item title='save' iconName='Download' onPress={onPress} testID={testID} />
</CustomHeaderButtons>
));
export const LegalButton = React.memo(({ navigation, testID }) => (
<MoreButton onPress={() => navigation.navigate('LegalView')} testID={testID} />
));
@ -80,6 +86,10 @@ MoreButton.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired
};
SaveButton.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired
};
LegalButton.propTypes = {
navigation: PropTypes.object.isRequired,
testID: PropTypes.string.isRequired

View File

@ -1,8 +1,8 @@
import React, { useContext } from 'react';
import { Text } from 'react-native';
import PropTypes from 'prop-types';
import { shortnameToUnicode } from 'emoji-toolkit';
import shortnameToUnicode from '../../../utils/shortnameToUnicode';
import styles from '../styles';
import MessageboxContext from '../Context';
import CustomEmoji from '../../EmojiPicker/CustomEmoji';

View File

@ -42,7 +42,6 @@ import {
MENTIONS_TRACKING_TYPE_USERS
} from './constants';
import CommandsPreview from './CommandsPreview';
import { withTheme } from '../../theme';
const imagePickerConfig = {
cropping: true,
@ -566,7 +565,6 @@ class MessageBox extends Component {
}
}
showUploadModal = (file) => {
this.setState({ file: { ...file, isVisible: true } });
}
@ -889,4 +887,4 @@ const dispatchToProps = ({
typing: (rid, status) => userTypingAction(rid, status)
});
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withTheme(MessageBox));
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(MessageBox);

View File

@ -1,9 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import { STATUS_COLORS } from '../../constants/colors';
import { STATUS_COLORS, themes } from '../../constants/colors';
const Status = React.memo(({ status, size, style }) => (
const Status = React.memo(({
status, size, style, theme
}) => (
<View
style={
[
@ -12,7 +14,8 @@ const Status = React.memo(({ status, size, style }) => (
borderRadius: size,
width: size,
height: size,
backgroundColor: STATUS_COLORS[status]
backgroundColor: STATUS_COLORS[status],
borderColor: themes[theme].backgroundColor
}
]}
/>
@ -20,11 +23,13 @@ const Status = React.memo(({ status, size, style }) => (
Status.propTypes = {
status: PropTypes.string,
size: PropTypes.number,
style: PropTypes.any
style: PropTypes.any,
theme: PropTypes.string
};
Status.defaultProps = {
status: 'offline',
size: 16
size: 16,
theme: 'light'
};
export default Status;

View File

@ -3,12 +3,14 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Status from './Status';
import { withTheme } from '../../theme';
class StatusContainer extends React.PureComponent {
static propTypes = {
style: PropTypes.any,
size: PropTypes.number,
status: PropTypes.string
status: PropTypes.string,
theme: PropTypes.string
};
static defaultProps = {
@ -16,8 +18,10 @@ class StatusContainer extends React.PureComponent {
}
render() {
const { style, size, status } = this.props;
return <Status size={size} style={style} status={status} />;
const {
style, size, status, theme
} = this.props;
return <Status size={size} style={style} status={status} theme={theme} />;
}
}
@ -25,4 +29,4 @@ const mapStateToProps = (state, ownProps) => ({
status: state.meteor.connected ? state.activeUsers[ownProps.id] : 'offline'
});
export default connect(mapStateToProps)(StatusContainer);
export default connect(mapStateToProps)(withTheme(StatusContainer));

View File

@ -40,7 +40,7 @@ const AtMention = React.memo(({
return (
<Text
style={[preview ? { ...styles.text, color: themes[theme].titleText } : mentionStyle, ...style]}
style={[preview ? { ...styles.text, color: themes[theme].bodyText } : mentionStyle, ...style]}
onPress={preview ? undefined : handlePress}
>
{`@${ mention }`}

View File

@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text } from 'react-native';
import { shortnameToUnicode } from 'emoji-toolkit';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { themes } from '../../constants/colors';
@ -25,7 +25,7 @@ const Emoji = React.memo(({
return (
<Text
style={[
{ color: themes[theme].titleText },
{ color: themes[theme].bodyText },
isMessageContainsOnlyEmoji ? styles.textBig : styles.text,
...style
]}

View File

@ -21,7 +21,7 @@ const Hashtag = React.memo(({
if (channels && channels.length && channels.findIndex(channel => channel.name === hashtag) !== -1) {
return (
<Text
style={[preview ? { ...styles.text, color: themes[theme].titleText } : styles.mention, ...style]}
style={[preview ? { ...styles.text, color: themes[theme].bodyText } : styles.mention, ...style]}
onPress={preview ? undefined : handlePress}
>
{`#${ hashtag }`}
@ -29,7 +29,7 @@ const Hashtag = React.memo(({
);
}
return (
<Text style={[preview ? { ...styles.text, color: themes[theme].titleText } : styles.mention, ...style]}>
<Text style={[preview ? { ...styles.text, color: themes[theme].bodyText } : styles.mention, ...style]}>
{`#${ hashtag }`}
</Text>
);

View File

@ -25,7 +25,7 @@ const Link = React.memo(({
style={
!preview
? { ...styles.link, color: themes[theme].actionTintColor }
: { color: themes[theme].titleText }
: { color: themes[theme].bodyText }
}
>
{ childLength !== 0 ? children : link }

View File

@ -39,7 +39,7 @@ const ListItem = React.memo(({
return (
<View style={style.container}>
<View style={[{ width: bulletWidth }, style.bullet]}>
<Text style={{ color: themes[theme].titleText }}>
<Text style={{ color: themes[theme].bodyText }}>
{bullet}
</Text>
</View>

View File

@ -25,7 +25,7 @@ const TableCell = React.memo(({
return (
<View style={[...cellStyle, { width: CELL_WIDTH }]}>
<Text style={[textStyle, { color: themes[theme].titleText }]}>
<Text style={[textStyle, { color: themes[theme].bodyText }]}>
{children}
</Text>
</View>

View File

@ -3,8 +3,8 @@ import { Text, Image } from 'react-native';
import { Parser, Node } from 'commonmark';
import Renderer from 'commonmark-react-renderer';
import PropTypes from 'prop-types';
import { shortnameToUnicode } from 'emoji-toolkit';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import I18n from '../../i18n';
import { themes } from '../../constants/colors';
@ -171,11 +171,11 @@ class Markdown extends PureComponent {
!preview
? {
...styles.codeInline,
color: themes[theme].titleText,
color: themes[theme].bodyText,
backgroundColor: themes[theme].bannerBackground,
borderColor: themes[theme].bannerBackground
}
: { ...styles.text, color: themes[theme].titleText },
: { ...styles.text, color: themes[theme].bodyText },
...style
]}
>
@ -192,11 +192,11 @@ class Markdown extends PureComponent {
!preview
? {
...styles.codeBlock,
color: themes[theme].titleText,
color: themes[theme].bodyText,
backgroundColor: themes[theme].bannerBackground,
borderColor: themes[theme].bannerBackground
}
: { ...styles.text, color: themes[theme].titleText },
: { ...styles.text, color: themes[theme].bodyText },
...style
]}
>
@ -216,7 +216,7 @@ class Markdown extends PureComponent {
return null;
}
return (
<Text style={[style, { color: themes[theme].titleText }]} numberOfLines={numberOfLines}>
<Text style={[style, { color: themes[theme].bodyText }]} numberOfLines={numberOfLines}>
{children}
</Text>
);
@ -297,7 +297,7 @@ class Markdown extends PureComponent {
const { numberOfLines, theme } = this.props;
const textStyle = styles[`heading${ level }Text`];
return (
<Text numberOfLines={numberOfLines} style={[textStyle, { color: themes[theme].titleText }]}>
<Text numberOfLines={numberOfLines} style={[textStyle, { color: themes[theme].bodyText }]}>
{children}
</Text>
);
@ -390,7 +390,7 @@ class Markdown extends PureComponent {
}
if (!useMarkdown && !preview) {
return <Text style={[styles.text, { color: themes[theme].titleText }]} numberOfLines={numberOfLines}>{m}</Text>;
return <Text style={[styles.text, { color: themes[theme].bodyText }]} numberOfLines={numberOfLines}>{m}</Text>;
}
const ast = this.parser.parse(m);

View File

@ -8,7 +8,7 @@ import Video from './Video';
import Reply from './Reply';
const Attachments = React.memo(({
attachments, timeFormat, user, baseUrl, useMarkdown, onOpenFileModal, getCustomEmoji, theme
attachments, timeFormat, user, baseUrl, useMarkdown, showAttachment, getCustomEmoji, theme
}) => {
if (!attachments || attachments.length === 0) {
return null;
@ -16,13 +16,13 @@ const Attachments = React.memo(({
return attachments.map((file, index) => {
if (file.image_url) {
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />;
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />;
}
if (file.audio_url) {
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />;
}
if (file.video_url) {
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />;
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />;
}
// eslint-disable-next-line react/no-array-index-key
@ -36,7 +36,7 @@ Attachments.propTypes = {
user: PropTypes.object,
baseUrl: PropTypes.string,
useMarkdown: PropTypes.bool,
onOpenFileModal: PropTypes.func,
showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string
};

View File

@ -16,7 +16,7 @@ const Content = React.memo((props) => {
let content = null;
if (props.tmid && !props.msg) {
content = <Text style={[styles.text, { color: themes[props.theme].titleText }]}>{I18n.t('Sent_an_attachment')}</Text>;
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
} else {
content = (
<Markdown

View File

@ -18,7 +18,7 @@ const Discussion = React.memo(({
return (
<>
<Text style={[styles.startedDiscussion, { color: themes[theme].auxiliaryText }]}>{I18n.t('Started_discussion')}</Text>
<Text style={[styles.text, { color: themes[theme].titleText }]}>{msg}</Text>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>{msg}</Text>
<View style={styles.buttonContainer}>
<Touchable
onPress={onDiscussionPress}
@ -28,7 +28,7 @@ const Discussion = React.memo(({
>
<>
<CustomIcon name='chat' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
<Text style={[styles.buttonText, { color: themes[theme].titleText }]}>{buttonText}</Text>
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text>
</>
</Touchable>
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>

View File

@ -1,8 +1,8 @@
import React from 'react';
import { Text } from 'react-native';
import PropTypes from 'prop-types';
import { shortnameToUnicode } from 'emoji-toolkit';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
const Emoji = React.memo(({

View File

@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import { createImageProgress } from 'react-native-image-progress';
import * as Progress from 'react-native-progress';
import Markdown from '../markdown';
import styles from './styles';
@ -12,6 +14,8 @@ import { withSplit } from '../../split';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
const ImageProgress = createImageProgress(FastImage);
const Button = React.memo(({
children, onPress, split, theme
}) => (
@ -25,22 +29,26 @@ const Button = React.memo(({
));
const Image = React.memo(({ img, theme }) => (
<FastImage
<ImageProgress
style={[styles.image, { borderColor: themes[theme].borderColor }]}
source={{ uri: encodeURI(img) }}
resizeMode={FastImage.resizeMode.cover}
indicator={Progress.Pie}
indicatorProps={{
color: themes[theme].actionTintColor
}}
/>
));
const ImageContainer = React.memo(({
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji, split, theme
file, baseUrl, user, useMarkdown, showAttachment, getCustomEmoji, split, theme
}) => {
const img = formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
if (!img) {
return null;
}
const onPress = () => onOpenFileModal(file);
const onPress = () => showAttachment(file);
if (file.description) {
return (
@ -65,7 +73,7 @@ ImageContainer.propTypes = {
baseUrl: PropTypes.string,
user: PropTypes.object,
useMarkdown: PropTypes.bool,
onOpenFileModal: PropTypes.func,
showAttachment: PropTypes.func,
theme: PropTypes.string,
getCustomEmoji: PropTypes.func,
split: PropTypes.bool

View File

@ -1,9 +1,9 @@
import React from 'react';
import { View, Text } from 'react-native';
import removeMarkdown from 'remove-markdown';
import { shortnameToUnicode } from 'emoji-toolkit';
import PropTypes from 'prop-types';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import { CustomIcon } from '../../lib/Icons';
import DisclosureIndicator from '../DisclosureIndicator';
import styles from './styles';

View File

@ -72,7 +72,7 @@ const Title = React.memo(({ attachment, timeFormat, theme }) => {
const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
return (
<View style={styles.authorContainer}>
{attachment.author_name ? <Text style={[styles.author, { color: themes[theme].titleText }]}>{attachment.author_name}</Text> : null}
{attachment.author_name ? <Text style={[styles.author, { color: themes[theme].bodyText }]}>{attachment.author_name}</Text> : null}
{time ? <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{ time }</Text> : null}
</View>
);
@ -116,8 +116,8 @@ const Fields = React.memo(({ attachment, theme }) => {
<View style={styles.fieldsContainer}>
{attachment.fields.map(field => (
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
<Text style={[styles.fieldTitle, { color: themes[theme].titleText }]}>{field.title}</Text>
<Text style={[styles.fieldValue, { color: themes[theme].titleText }]}>{field.value}</Text>
<Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text>
<Text style={[styles.fieldValue, { color: themes[theme].bodyText }]}>{field.value}</Text>
</View>
))}
</View>

View File

@ -27,7 +27,7 @@ const styles = StyleSheet.create({
});
const Video = React.memo(({
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji, theme
file, baseUrl, user, useMarkdown, showAttachment, getCustomEmoji, theme
}) => {
if (!baseUrl) {
return null;
@ -35,7 +35,7 @@ const Video = React.memo(({
const onPress = () => {
if (isTypeSupported(file.video_type)) {
return onOpenFileModal(file);
return showAttachment(file);
}
const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
openLink(uri, theme);
@ -64,7 +64,7 @@ Video.propTypes = {
baseUrl: PropTypes.string,
user: PropTypes.object,
useMarkdown: PropTypes.bool,
onOpenFileModal: PropTypes.func,
showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string
};

View File

@ -40,7 +40,7 @@ class MessageContainer extends React.Component {
replyBroadcast: PropTypes.func,
reactionInit: PropTypes.func,
fetchThreadName: PropTypes.func,
onOpenFileModal: PropTypes.func,
showAttachment: PropTypes.func,
onReactionLongPress: PropTypes.func,
navToRoomInfo: PropTypes.func,
callJitsi: PropTypes.func,
@ -212,7 +212,7 @@ class MessageContainer extends React.Component {
render() {
const {
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, 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, 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
@ -275,7 +275,7 @@ class MessageContainer extends React.Component {
replyBroadcast={this.replyBroadcast}
reactionInit={this.reactionInit}
onDiscussionPress={this.onDiscussionPress}
onOpenFileModal={onOpenFileModal}
showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo}
callJitsi={callJitsi}

View File

@ -16,6 +16,7 @@ export default {
'error-duplicate-channel-name': 'Ein Kanal mit dem Namen {{channel_name}} ist bereits vorhanden',
'error-email-domain-blacklisted': 'Die E-Mail-Domain wird auf die schwarze Liste gesetzt',
'error-email-send-failed': 'Fehler beim Versuch, eine E-Mail zu senden: {{message}}',
'error-save-image': 'Fehler beim Speichern des Bildes',
'error-field-unavailable': '{{field}} wird bereits verwendet :(',
'error-file-too-large': 'Datei ist zu groß',
'error-importer-not-defined': 'Der Import wurde nicht korrekt definiert, es fehlt die Importklasse.',
@ -33,7 +34,7 @@ export default {
'error-invalid-email': 'Ungültige E-Mail {{emai}}',
'error-invalid-email-address': 'Ungültige E-Mail-Adresse',
'error-invalid-file-height': 'Ungültige Dateihöhe',
'error-invalid-file-type': 'ungültiger Dateityp',
'error-invalid-file-type': 'Ungültiger Dateityp',
'error-invalid-file-width': 'Ungültige Dateibreite',
'error-invalid-from-address': 'Sie haben eine ungültige FROM-Adresse mitgeteilt.',
'error-invalid-integration': 'Ungültige Integration',
@ -80,29 +81,36 @@ export default {
Activity: 'Aktivität',
Add_Reaction: 'Reaktion hinzufügen',
Add_Server: 'Server hinzufügen',
Add_user: 'Nutzer hinzufügen',
Add_users: 'Nutzer hinzufügen',
Admin_Panel: 'Admin Panel',
Alert: 'Warnen',
alert: 'warnen',
alerts: 'Warnungen',
All_users_in_the_channel_can_write_new_messages: 'Alle Benutzer im Kanal können neue Nachrichten schreiben',
All: 'Alles',
All_Messages: 'Alle Nachrichten',
Allow_Reactions: 'Reaktionen zulassen',
Alphabetical: 'Alphabetisch',
and_more: 'und mehr',
and: 'und',
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.',
ARCHIVE: 'ARCHIV',
archive: 'Archiv',
are_typing: 'tippen',
Are_you_sure_question_mark: 'Sind Sie sicher?',
Are_you_sure_you_want_to_leave_the_room: 'Möchten Sie den Raum wirklich verlassen {{room}}?',
Audio: 'Audio',
Authenticating: 'Authentifizierung',
Automatic: 'Automatisch',
Auto_Translate: 'Automatische Übersetzung',
Avatar_changed_successfully: 'Avatar erfolgreich geändert!',
Avatar_Url: 'Avatar-URL',
Away: 'Abwesend',
Back: 'Zurück',
Black: 'Schwarz',
Block_user: 'Benutzer blockieren',
Broadcast_channel_Description: 'Nur autorisierte Benutzer können neue Nachrichten schreiben, die anderen Benutzer können jedoch antworten',
Broadcast_Channel: 'Broadcastkanal',
@ -116,10 +124,13 @@ export default {
Channel_Name: 'Kanal Name',
Channels: 'Kanäle',
Chats: 'Chats',
Call_already_ended: 'Anruf bereits beendet!',
Click_to_join: 'Klicken um teilzunehmen!',
Close: 'Schließen',
Close_emoji_selector: 'Schließen Sie die Emoji-Auswahl',
Choose: 'Wählen',
Choose_from_library: 'Aus der Bibliothek auswählen',
Choose_file: 'Datei auswählen',
Code: 'Code',
Collaborative: 'Kollaborativ',
Confirm: 'Bestätigen',
@ -129,32 +140,41 @@ export default {
connecting_server: 'verbinde zum Server',
Connecting: 'Verbinden ...',
Contact_us: 'Kontaktiere uns',
Contact_your_server_admin: 'Kontaktieren Sie Ihren Server-Administrator.',
Continue_with: 'Weitermachen mit',
Copied_to_clipboard: 'In die Zwischenablage kopiert!',
Copy: 'Kopieren',
Permalink: 'Permalink',
Certificate_password: 'Zertifikats-Passwort',
Whats_the_password_for_your_certificate: 'Wie lautet das Passwort für Ihr Zertifikat?',
Create_account: 'Ein Konto erstellen',
Create_Channel: 'Kanal erstellen',
Created_snippet: 'Erstellt ein Snippet',
Create_a_new_workspace: 'Erstellen Sie einen neuen Arbeitsbereich',
Create: 'Erstellen',
Dark: 'Dunkel',
Dark_level: 'Dunkelstufe',
Default: 'Standard',
Delete_Room_Warning: 'Durch das Löschen eines Raums werden alle Nachrichten gelöscht, die im Raum gepostet wurden. Das kann nicht rückgängig gemacht werden.',
delete: 'löschen',
Delete: 'Löschen',
DELETE: 'LÖSCHEN',
description: 'Beschreibung',
Description: 'Beschreibung',
DESKTOP_OPTIONS: 'Desktop-Einstellungen',
Directory: 'Verzeichnis',
Direct_Messages: 'Direkte Nachrichten',
Disable_notifications: 'Benachrichtigungen deaktiveren',
Discussions: 'Diskussionen',
Dont_Have_An_Account: 'Sie haben noch kein Konto?',
Do_you_have_a_certificate: 'Haben Sie ein Zertifikat?',
Do_you_really_want_to_key_this_room_question_mark: 'Möchten Sie diesen Raum wirklich {{key}}?',
edit: 'bearbeiten',
edited: 'bearbeitet',
Edit: 'Bearbeiten',
Email_or_password_field_is_empty: 'Das E-Mail- oder Passwortfeld ist leer',
Email: 'Email',
EMAIL: 'EMAIL',
email: 'Email',
Enable_Auto_Translate: 'Automatische Übersetzung aktivieren',
Enable_markdown: 'Markdown aktivieren',
@ -174,12 +194,15 @@ export default {
Forgot_password_If_this_email_is_registered: 'Wenn diese E-Mail registriert ist, senden wir Anweisungen zum Zurücksetzen Ihres Passworts. Wenn Sie in Kürze keine E-Mail erhalten, kommen Sie bitte zurück und versuchen Sie es erneut.',
Forgot_password: 'Passwort vergessen',
Forgot_Password: 'Passwort vergessen',
Full_table: 'Klicken um die ganze Tabelle anzuzeigen',
Group_by_favorites: 'Nach Favoriten gruppieren',
Group_by_type: 'Gruppieren nach Typ',
Hide: 'Ausblenden',
Has_joined_the_channel: 'Ist dem Kanal beigetreten',
Has_joined_the_conversation: 'Hat sich dem Gespräch angeschlossen',
Has_left_the_channel: 'Hat den Kanal verlassen',
IN_APP_AND_DESKTOP: 'IN-APP UND DESKTOP',
In_App_and_Desktop_Alert_info: 'Zeigt ein Banner oben am Bildschirm, wenn die App geöffnet ist und eine Benachrichtigung auf dem Desktop.',
Invisible: 'Unsichtbar',
Invite: 'Einladen',
is_a_valid_RocketChat_instance: 'ist eine gültige Rocket.Chat-Instanz',
@ -195,10 +218,11 @@ export default {
leaving_room: 'Raum verlassen',
leave: 'verlassen',
Legal: 'Rechtliches',
Light: 'Hell',
License: 'Lizenz',
Livechat: 'Live-Chat',
Login: 'Anmeldung',
Login_error: 'Ihre Referenzen wurden abgelehnt! Bitte versuche es erneut.',
Login_error: 'Ihre Zugangsdaten wurden abgelehnt! Bitte versuchen Sie es erneut.',
Login_with: 'Einloggen mit',
Logout: 'Ausloggen',
members: 'Mitglieder',
@ -214,7 +238,7 @@ export default {
messages: 'Nachrichten',
Messages: 'Mitteilungen',
Message_Reported: 'Nachricht gemeldet',
Microphone_Permission_Message: 'Rocket Chat benötigt Zugriff auf Ihr Mikrofon, damit Sie eine Audionachricht senden können.',
Microphone_Permission_Message: 'Rocket.Chat benötigt Zugriff auf Ihr Mikrofon, damit Sie eine Audionachricht senden können.',
Microphone_Permission: 'Mikrofonberechtigung',
Mute: 'Stumm',
muted: 'stummgeschaltet',
@ -230,7 +254,7 @@ export default {
No_files: 'Keine Dateien',
No_mentioned_messages: 'Keine erwähnten Nachrichten',
No_pinned_messages: 'Keine angehefteten Nachrichten',
No_results_found: 'keine Ergebnisse gefunden',
No_results_found: 'Keine Ergebnisse gefunden',
No_starred_messages: 'Keine markierten Nachrichten',
No_thread_messages: 'Keine Threadnachrichten',
No_announcement_provided: 'Keine Ankündigung erfolgt.',
@ -241,15 +265,20 @@ export default {
No_Reactions: 'Keine Reaktionen',
No_Read_Receipts: 'Keine Lesebestätigungen',
Not_logged: 'Nicht protokolliert',
Not_RC_Server: 'Dies ist kein Rocket.Chat-Server.\n{{contact}}',
Nothing: 'Nichts',
Nothing_to_save: 'Nichts zu speichern!',
Notify_active_in_this_room: 'Aktive Benutzer in diesem Raum benachrichtigen',
Notify_all_in_this_room: 'Benachrichtigen Sie alle in diesem Raum',
Notifications: 'Benachrichtigungen',
Notification_Duration: 'Benachrichtigungsdauer',
Notification_Preferences: 'Benachrichtigungseinstellungen',
Offline: 'Offline',
Oops: 'Hoppla!',
Online: 'Online',
Only_authorized_users_can_write_new_messages: 'Nur autorisierte Benutzer können neue Nachrichten schreiben',
Open_emoji_selector: 'Öffne die Emoji-Auswahl',
Open_Source_Communication: 'Open Source-Kommunikation',
Open_Source_Communication: 'Open-Source-Kommunikation',
Password: 'Passwort',
Permalink_copied_to_clipboard: 'Permalink in die Zwischenablage kopiert!',
Pin: 'Anheften',
@ -257,15 +286,19 @@ export default {
pinned: 'angeheftet',
Pinned: 'Angeheftet',
Please_enter_your_password: 'Bitte geben Sie Ihr Passwort ein',
Preferences: 'Einstellungen',
Preferences_saved: 'Einstellungen gespeichert!',
Privacy_Policy: ' Datenschutzbestimmungen',
Private_Channel: 'Privater Kanal',
Private_Groups: 'Private Gruppen',
Private: 'Privat',
Processing: 'Bearbeite …',
Profile_saved_successfully: 'Profil erfolgreich gespeichert!',
Profile: 'Profil',
Public_Channel: 'Öffentlicher Kanal',
Public: 'Öffentlich',
PUSH_NOTIFICATIONS: 'Push-Benachrichtigungen',
Push_Notifications_Alert_Info: 'Diese Benachrichtigungen werden Ihnen zugestellt, wenn die App nicht geöffnet ist.',
Quote: 'Zitat',
Reactions_are_disabled: 'Reaktionen sind deaktiviert',
Reactions_are_enabled: 'Reaktionen sind aktiviert',
@ -274,6 +307,8 @@ export default {
Read_Only_Channel: 'Nur-Lese-Kanal',
Read_Only: 'Schreibgeschützt',
Read_Receipt: 'Lesebestätigung',
Receive_Group_Mentions: 'Erhalte Gruppen-Benachrichtigungen',
Receive_Group_Mentions_Info: 'Empfange @all und @here Erwähnungen',
Register: 'Registrieren',
Repeat_Password: 'Wiederhole das Passwort',
Replied_on: 'Antwortete am:',
@ -281,6 +316,8 @@ export default {
reply: 'Antworten',
Reply: 'Antworten',
Report: 'Bericht',
Receive_Notification: 'Erhalte Benachrichtigungen',
Receive_notifications_from: 'Erhalte Benachrichtigungen von {{name}}',
Resend: 'Erneut senden',
Reset_password: 'Passwort zurücksetzen',
resetting_password: 'Passwort zurücksetzen',
@ -294,7 +331,7 @@ export default {
Room_Files: 'Raumdateien',
Room_Info_Edit: 'Rauminfo bearbeiten',
Room_Info: 'Rauminfo',
Room_Members: 'Raum Mitglieder',
Room_Members: 'Raum-Mitglieder',
Room_name_changed: 'Raumname geändert in {{name}} von {{userBy}}',
SAVE: 'SPEICHERN',
Save_Changes: 'Änderungen speichern',
@ -302,17 +339,21 @@ export default {
saving_preferences: 'Präferenzen speichern',
saving_profile: 'Profil speichern',
saving_settings: 'Einstellungen speichern',
saved_to_gallery: 'Gespeichert in der Galerie',
Search_Messages: 'Nachrichten suchen',
Search: 'Suche',
Search_by: 'Suche nach',
Search_global_users: 'Suche nach globalen Benutzern',
Search_global_users_description: 'Beim Einschalten können Sie nach Benutzern von anderen Unternehmen oder Servern suchen.',
Seconds: '{{second}} Sekunden',
Select_Avatar: 'Wählen Sie einen Avatar aus',
Select_Server: 'Server auswählen',
Select_Users: 'Wählen Sie einen Benutzer aus',
Send: 'Senden',
Send_audio_message: 'Audio-Nachricht senden',
Send_crash_report: 'Absturzbericht senden',
Send_message: 'Nachricht senden',
Send_to: 'Senden an …',
Sent_an_attachment: 'Sende einen Anhang',
Server: 'Server',
Servers: 'Server',
@ -322,10 +363,13 @@ export default {
Settings_succesfully_changed: 'Einstellungen erfolgreich geändert!',
Share: 'Teilen',
Share_this_app: 'Teile diese App',
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',
Sign_Up: 'Anmelden',
Some_field_is_invalid_or_empty: 'Ein Feld ist ungültig oder leer',
Sorting_by: 'Sortierung nach {{key}}',
Sound: 'Ton',
Star_room: 'Favorisierter Raum',
Star: 'Favoriten',
Starred_Messages: 'Favorisierte Nachrichten',
@ -333,19 +377,23 @@ export default {
Starred: 'Favorisiert',
Start_of_conversation: 'Beginn des Gesprächs',
Started_discussion: 'Hat eine Diskussion gestartet:',
Started_call: 'Anruf gestartet von {{userBy}}',
Submit: 'einreichen',
Table: 'Tabelle',
Take_a_photo: 'Foto aufnehmen',
Take_a_video: 'Video aufnehmen',
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',
The_URL_is_invalid: 'Die eingegebene URL ist ungültig. Überprüfen Sie es und versuchen Sie es erneut, bitte!',
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',
This_room_is_read_only: 'Dieser Raum kann nur gelesen werden',
Thread: 'Thread',
Threads: 'Threads',
Timezone: 'Zeitzone',
To: 'An',
topic: 'Thema',
Topic: 'Thema',
Translate: 'Übersetzen',
@ -354,7 +402,7 @@ export default {
Type_the_channel_name_here: 'Geben Sie hier den Kanalnamen ein',
unarchive: 'wiederherstellen',
UNARCHIVE: 'WIEDERHERSTELLEN',
Unblock_user: 'Nutzer entblockieren',
Unblock_user: 'Nutzer entsperren',
Unfavorite: 'Nicht mehr favorisieren',
Unfollowed_thread: 'Thread nicht mehr folgen',
Unmute: 'Stummschaltung aufheben',
@ -364,7 +412,7 @@ export default {
Unread: 'Ungelesen',
Unread_on_top: 'Ungelesen an der Spitze',
Unstar: 'von Favoriten entfernen',
Updating: 'Aktualisierung...',
Updating: 'Aktualisierung',
Uploading: 'Hochladen',
Upload_file_question_mark: 'Datei hochladen?',
Users: 'Benutzer',
@ -383,20 +431,40 @@ export default {
Video_call: 'Videoanruf',
View_Original: 'Original anzeigen',
Voice_call: 'Sprachanruf',
Websocket_disabled: 'Websockets sind auf diesem Server nicht aktiviert.\n{{contact}}',
Welcome: 'Herzlich willkommen',
Welcome_to_RocketChat: 'Willkommen bei Rocket.Chat',
Whats_your_2fa: 'Wie ist dein 2FA-Code?',
Whats_your_2fa: 'Wie lautet Ihr 2FA-Code?',
Without_Servers: 'Ohne Server',
Write_External_Permission_Message: 'Rocket.Chat benötigt Zugriff auf Ihre Galerie um Bilder speichern zu können.',
Write_External_Permission: 'Galerie-Zugriff',
Yes_action_it: 'Ja, {{action}}!',
Yesterday: 'Gestern',
You_are_in_preview_mode: 'Sie befinden sich im Vorschaumodus',
You_are_offline: 'Sie sind offline',
You_can_search_using_RegExp_eg: 'Sie können mit RegExp suchen. z.B. `/ ^ text $ / i`',
You_colon: 'Sie:',
You_colon: 'Sie: ',
you_were_mentioned: 'Sie wurden erwähnt',
you: 'sie',
you: 'Sie',
You: 'Sie',
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',
Version_no: 'Version: {{version}}',
You_will_not_be_able_to_recover_this_message: 'Sie können diese Nachricht nicht wiederherstellen!',
Change_Language: 'Sprache ändern',
Crash_report_disclaimer: 'Wir verfolgen niemals den Inhalt Ihrer Chats. Der Crash-Report enthält nur für uns relevante Informationen in der Reihenfolge '
Crash_report_disclaimer: 'Wir verfolgen niemals den Inhalt Ihrer Chats. Der Crash-Report enthält nur für uns relevante Informationen um das Problem zu erkennen und zu beheben.',
Type_message: 'Type message',
Room_search: 'Raum-Suche',
Room_selection: 'Raum-Auswahl 1...9',
Next_room: 'Nächster Raum',
Previous_room: 'Voriger Raum',
New_room: 'Neuer Raum',
Upload_room: 'Zu einem Raum hochladen',
Search_messages: 'Nachrichten durchsuchen',
Scroll_messages: 'Nachrichten durchblättern',
Reply_latest: 'Auf die letzte Nachricht antworten',
Server_selection: 'Server-Auswahl',
Server_selection_numbers: 'Server-Auswahl 1...9',
Add_server: 'Server hinufügen',
New_line: 'Zeilenumbruch'
};

View File

@ -16,6 +16,7 @@ export default {
'error-duplicate-channel-name': 'A channel with name {{channel_name}} exists',
'error-email-domain-blacklisted': 'The email domain is blacklisted',
'error-email-send-failed': 'Error trying to send email: {{message}}',
'error-save-image': 'Error while saving image',
'error-field-unavailable': '{{field}} is already in use :(',
'error-file-too-large': 'File is too large',
'error-importer-not-defined': 'The importer was not defined correctly, it is missing the Import class.',
@ -80,7 +81,7 @@ export default {
Activity: 'Activity',
Add_Reaction: 'Add Reaction',
Add_Server: 'Add Server',
Add_user: 'Add user',
Add_users: 'Add users',
Admin_Panel: 'Admin Panel',
Alert: 'Alert',
alert: 'alert',
@ -120,6 +121,7 @@ export default {
Cancel: 'Cancel',
changing_avatar: 'changing avatar',
creating_channel: 'creating channel',
creating_invite: 'creating invite',
Channel_Name: 'Channel Name',
Channels: 'Channels',
Chats: 'Chats',
@ -171,6 +173,7 @@ export default {
edit: 'edit',
edited: 'edited',
Edit: 'Edit',
Edit_Invite: 'Edit Invite',
Email_or_password_field_is_empty: 'Email or password field is empty',
Email: 'Email',
EMAIL: 'EMAIL',
@ -181,6 +184,7 @@ export default {
Everyone_can_access_this_channel: 'Everyone can access this channel',
erasing_room: 'erasing room',
Error_uploading: 'Error uploading',
Expiration_Days: 'Expiration (Days)',
Favorite: 'Favorite',
Favorites: 'Favorites',
Files: 'Files',
@ -194,6 +198,7 @@ export default {
Forgot_password: 'Forgot password',
Forgot_Password: 'Forgot Password',
Full_table: 'Click to see full table',
Generate_New_Link: 'Generate New Link',
Group_by_favorites: 'Group favorites',
Group_by_type: 'Group by type',
Hide: 'Hide',
@ -207,7 +212,10 @@ export default {
is_a_valid_RocketChat_instance: 'is a valid Rocket.Chat instance',
is_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
is_typing: 'is typing',
Invalid_or_expired_invite_token: 'Invalid or expired invite token',
Invalid_server_version: 'The server you\'re trying to connect is using a version that\'s not supported by the app anymore: {{currentVersion}}.\n\nWe require version {{minVersion}}',
Invite_Link: 'Invite Link',
Invite_users: 'Invite users',
Join_the_community: 'Join the community',
Join: 'Join',
Just_invited_people_can_access_this_channel: 'Just invited people can access this channel',
@ -224,6 +232,7 @@ export default {
Login_error: 'Your credentials were rejected! Please try again.',
Login_with: 'Login with',
Logout: 'Logout',
Max_number_of_uses: 'Max number of uses',
members: 'members',
Members: 'Members',
Mentioned_Messages: 'Mentioned Messages',
@ -246,11 +255,13 @@ export default {
N_users: '{{n}} users',
name: 'name',
Name: 'Name',
Never: 'Never',
New_Message: 'New Message',
New_Password: 'New Password',
New_Server: 'New Server',
Next: 'Next',
No_files: 'No files',
No_limit: 'No limit',
No_mentioned_messages: 'No mentioned messages',
No_pinned_messages: 'No pinned messages',
No_results_found: 'No results found',
@ -338,6 +349,7 @@ export default {
saving_preferences: 'saving preferences',
saving_profile: 'saving profile',
saving_settings: 'saving settings',
saved_to_gallery: 'Saved to gallery',
Search_Messages: 'Search Messages',
Search: 'Search',
Search_by: 'Search by',
@ -360,6 +372,7 @@ export default {
Settings: 'Settings',
Settings_succesfully_changed: 'Settings succesfully changed!',
Share: 'Share',
Share_Link: 'Share Link',
Share_this_app: 'Share this app',
Show_Unread_Counter: 'Show Unread Counter',
Show_Unread_Counter_Info: 'Unread counter is displayed as a badge on the right of the channel, in the list',
@ -415,6 +428,7 @@ export default {
Upload_file_question_mark: 'Upload file?',
Users: 'Users',
User_added_by: 'User {{userAdded}} added by {{userBy}}',
User_Info: 'User Info',
User_has_been_key: 'User has been {{key}}!',
User_is_no_longer_role_by_: '{{user}} is no longer {{role}} by {{userBy}}',
User_muted_by: 'User {{userMuted}} muted by {{userBy}}',
@ -434,6 +448,8 @@ export default {
Welcome_to_RocketChat: 'Welcome to Rocket.Chat',
Whats_your_2fa: 'What\'s your 2FA code?',
Without_Servers: 'Without Servers',
Write_External_Permission_Message: 'Rocket Chat needs access to your gallery so you can save images.',
Write_External_Permission: 'Gallery Permission',
Yes_action_it: 'Yes, {{action}} it!',
Yesterday: 'Yesterday',
You_are_in_preview_mode: 'You are in preview mode',
@ -445,6 +461,10 @@ export default {
You: 'You',
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'You need to access at least one Rocket.Chat server to share something.',
Your_certificate: 'Your Certificate',
Your_invite_link_will_expire_after__usesLeft__uses: 'Your invite link will expire after {{usesLeft}} uses.',
Your_invite_link_will_expire_on__date__or_after__usesLeft__uses: 'Your invite link will expire on {{date}} or after {{usesLeft}} uses.',
Your_invite_link_will_expire_on__date__: 'Your invite link will expire on {{date}}.',
Your_invite_link_will_never_expire: 'Your invite link will never expire.',
Version_no: 'Version: {{version}}',
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
Change_Language: 'Change Language',

View File

@ -80,7 +80,7 @@ export default {
Activity: 'Activité',
Add_Reaction: 'Ajouter une réaction',
Add_Server: 'Ajouter un serveur',
Add_user: 'Ajouter un utilisateur',
Add_users: 'Ajouter des utilisateurs',
Alert: 'Alerte',
alert: 'alerte',
alerts: 'alertes',

View File

@ -16,6 +16,7 @@ export default {
'error-duplicate-channel-name': 'Já existe um canal com nome {{channel_name}}',
'error-email-domain-blacklisted': 'O domínio de e-mail está na lista negra',
'error-email-send-failed': 'Erro ao tentar enviar e-mail: {{message}}',
'error-save-image': 'Erro ao salvar imagem',
'error-field-unavailable': '{{field}} já está sendo usado :(',
'error-file-too-large': 'Arquivo é muito grande',
'error-importer-not-defined': 'O importador não foi definido corretamente; está faltando a classe Import.',
@ -87,7 +88,7 @@ export default {
Activity: 'Atividade',
Add_Reaction: 'Reagir',
Add_Server: 'Adicionar servidor',
Add_user: 'Adicionar usuário',
Add_users: 'Adicionar usuário',
Alert: 'Alerta',
alert: 'alerta',
alerts: 'alertas',
@ -122,6 +123,7 @@ export default {
Cancel: 'Cancelar',
changing_avatar: 'trocando avatar',
creating_channel: 'criando canal',
creating_invite: 'criando convite',
Channel_Name: 'Nome do Canal',
Channels: 'Canais',
Chats: 'Conversas',
@ -168,6 +170,7 @@ export default {
edited: 'editado',
erasing_room: 'apagando sala',
Edit: 'Editar',
Edit_Invite: 'Editar convite',
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
Email: 'Email',
email: 'e-mail',
@ -175,6 +178,7 @@ export default {
Enable_notifications: 'Habilitar notificações',
Everyone_can_access_this_channel: 'Todos podem acessar este canal',
Error_uploading: 'Erro subindo',
Expiration_Days: 'Expira em (dias)',
Favorites: 'Favoritos',
Files: 'Arquivos',
File_description: 'Descrição do arquivo',
@ -187,6 +191,7 @@ export default {
Forgot_password: 'Esqueci minha senha',
Forgot_Password: 'Esqueci minha senha',
Full_table: 'Clique para ver a tabela completa',
Generate_New_Link: 'Gerar novo convite',
Group_by_favorites: 'Agrupar favoritos',
Group_by_type: 'Agrupar por tipo',
Has_joined_the_channel: 'Entrou no canal',
@ -195,7 +200,10 @@ export default {
Invisible: 'Invisível',
Invite: 'Convidar',
is_typing: 'está digitando',
Invalid_or_expired_invite_token: 'Token de convite inválido ou vencido',
Invalid_server_version: 'O servidor que você está conectando não é suportado mais por esta versão do aplicativo: {{currentVersion}}.\n\nEsta versão do aplicativo requer a versão {{minVersion}} do servidor para funcionar corretamente.',
Invite_Link: 'Link de Convite',
Invite_users: 'Convidar usuários',
Join_the_community: 'Junte-se à comunidade',
Join: 'Entrar',
Just_invited_people_can_access_this_channel: 'Apenas as pessoas convidadas podem acessar este canal',
@ -211,6 +219,7 @@ export default {
Login_error: 'Suas credenciais foram rejeitadas. Tente novamente por favor!',
Login_with: 'Login with',
Logout: 'Sair',
Max_number_of_uses: 'Número máximo de usos',
Members: 'Membros',
Mentioned_Messages: 'Mensagens mencionadas',
mentioned: 'mencionado',
@ -230,11 +239,13 @@ export default {
N_users: '{{n}} usuários',
name: 'nome',
Name: 'Nome',
Never: 'Nunca',
New_in_RocketChat_question_mark: 'Novo no Rocket.Chat?',
New_Message: 'Nova Mensagem',
New_Password: 'Nova Senha',
Next: 'Próximo',
No_files: 'Não há arquivos',
No_limit: 'Sem limite',
No_mentioned_messages: 'Não há menções',
No_pinned_messages: 'Não há mensagens fixadas',
No_results_found: 'Nenhum resultado encontrado',
@ -308,6 +319,7 @@ export default {
saving_preferences: 'salvando preferências',
saving_profile: 'salvando perfil',
saving_settings: 'salvando configurações',
saved_to_gallery: 'Salvo na galeria',
Search_Messages: 'Buscar Mensagens',
Search: 'Buscar',
Search_by: 'Buscar por',
@ -326,6 +338,7 @@ export default {
Settings: 'Configurações',
Settings_succesfully_changed: 'Configurações salvas com sucesso!',
Share: 'Compartilhar',
Share_Link: 'Share Link',
Sign_in_your_server: 'Entrar no seu servidor',
Sign_Up: 'Registrar',
Some_field_is_invalid_or_empty: 'Algum campo está inválido ou vazio',
@ -399,8 +412,13 @@ export default {
you_were_mentioned: 'você foi mencionado',
you: 'você',
You: 'Você',
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Você precisa acessar ao menos um servidor Rocket.Chat para compartilhar.',
Your_invite_link_will_expire_after__usesLeft__uses: 'Seu link de convite irá vencer depois de {{usesLeft}} usos.',
Your_invite_link_will_expire_on__date__or_after__usesLeft__uses: 'Seu link de convite irá vencer em {{date}} ou depois de {{usesLeft}} usos.',
Your_invite_link_will_expire_on__date__: 'Seu link de convite irá vencer em {{date}}.',
Your_invite_link_will_never_expire: 'Seu link de convite nunca irá vencer.',
You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!',
Write_External_Permission_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens',
Write_External_Permission: 'Acesso à Galeria',
Crash_report_disclaimer: 'Nós não rastreamos o conteúdo das suas conversas. O relatório de erros apenas contém informações relevantes para identificarmos problemas e corrigí-los.',
Type_message: 'Digitar mensagem',
Room_search: 'Busca de sala',

View File

@ -80,7 +80,7 @@ export default {
Activity: 'Actividade',
Add_Reaction: 'Adicionar Reacção',
Add_Server: 'Adicionar Servidor',
Add_user: 'Adicionar utilizador',
Add_users: 'Adicionar utilizadores',
Alert: 'Alerta',
alert: 'alerta',
alerts: 'alertas',

View File

@ -80,7 +80,7 @@ export default {
Activity: 'Активность',
Add_Reaction: 'Добавить реакцию',
Add_Server: 'Добавить сервер',
Add_user: 'Добавить пользователя',
Add_users: 'Добавить пользователей',
Admin_Panel: 'Панель админа',
Alert: 'Оповещение',
alert: 'оповещение',

View File

@ -80,7 +80,7 @@ export default {
Activity: '按活动排序',
Add_Reaction: '增加回复',
Add_Server: '添加服务器',
Add_user: '添加用户',
Add_users: '添加用户',
Alert: '警告',
alert: '警告',
alerts: '警告',

View File

@ -25,7 +25,9 @@ import parseQuery from './lib/methods/helpers/parseQuery';
import { initializePushNotifications, onNotification } from './notifications/push';
import store from './lib/createStore';
import NotificationBadge from './notifications/inApp';
import { defaultHeader, onNavigationStateChange, cardStyle } from './utils/navigation';
import {
defaultHeader, onNavigationStateChange, cardStyle, getActiveRouteName
} from './utils/navigation';
import { loggerConfig, analytics } from './utils/log';
import Toast from './containers/Toast';
import { ThemeContext } from './theme';
@ -47,7 +49,7 @@ if (isIOS) {
const parseDeepLinking = (url) => {
if (url) {
url = url.replace(/rocketchat:\/\/|https:\/\/go.rocket.chat\//, '');
const regex = /^(room|auth)\?/;
const regex = /^(room|auth|invite)\?/;
if (url.match(regex)) {
url = url.replace(regex, '').trim();
if (url) {
@ -144,6 +146,12 @@ const ChatsStack = createStackNavigator({
SelectedUsersView: {
getScreen: () => require('./views/SelectedUsersView').default
},
InviteUsersView: {
getScreen: () => require('./views/InviteUsersView').default
},
InviteUsersEditView: {
getScreen: () => require('./views/InviteUsersEditView').default
},
MessagesView: {
getScreen: () => require('./views/MessagesView').default
},
@ -258,9 +266,19 @@ const NewMessageStack = createStackNavigator({
cardStyle
});
const AttachmentStack = createStackNavigator({
AttachmentView: {
getScreen: () => require('./views/AttachmentView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const InsideStackModal = createStackNavigator({
Main: ChatsDrawer,
NewMessageStack,
AttachmentStack,
JitsiMeetView: {
getScreen: () => require('./views/JitsiMeetView').default
}
@ -388,6 +406,9 @@ const RoomActionsStack = createStackNavigator({
},
NotificationPrefView: {
getScreen: () => require('./views/NotificationPreferencesView').default
},
AttachmentView: {
getScreen: () => require('./views/AttachmentView').default
}
}, {
defaultNavigationOptions: defaultHeader,
@ -439,6 +460,10 @@ class CustomModalStack extends React.Component {
const {
navigation, showModal, closeModal, screenProps
} = this.props;
const pageSheetViews = ['AttachmentView'];
const pageSheet = pageSheetViews.includes(getActiveRouteName(navigation.state));
return (
<Modal
useNativeDriver
@ -448,7 +473,7 @@ class CustomModalStack extends React.Component {
hideModalContentWhileAnimating
avoidKeyboard
>
<View style={sharedStyles.modal}>
<View style={[sharedStyles.modal, pageSheet ? sharedStyles.modalPageSheet : sharedStyles.modalFormSheet]}>
<ModalSwitch navigation={navigation} screenProps={screenProps} />
</View>
</Modal>

View File

@ -18,12 +18,9 @@ function normalizeAttachments(msg) {
}
export default (msg) => {
/**
* 2019-03-29: Realm object properties are *always* optional, but `u.username` is required
* https://realm.io/docs/javascript/latest/#to-one-relationships
*/
if (!msg || !msg.u || !msg.u.username) { return; }
if (!msg) {
return null;
}
msg = normalizeAttachments(msg);
msg.reactions = msg.reactions || [];
msg.unread = msg.unread || false;

View File

@ -5,6 +5,36 @@ import database from '../database';
import log from '../../utils/log';
import random from '../../utils/random';
const changeMessageStatus = async(id, tmid, status) => {
const db = database.active;
const msgCollection = db.collections.get('messages');
const threadMessagesCollection = db.collections.get('thread_messages');
const successBatch = [];
const messageRecord = await msgCollection.find(id);
successBatch.push(
messageRecord.prepareUpdate((m) => {
m.status = status;
})
);
if (tmid) {
const threadMessageRecord = await threadMessagesCollection.find(id);
successBatch.push(
threadMessageRecord.prepareUpdate((tm) => {
tm.status = status;
})
);
}
try {
await db.action(async() => {
await db.batch(...successBatch);
});
} catch (error) {
// Do nothing
}
};
export async function sendMessageCall(message) {
const {
id: _id, subscription: { id: rid }, msg, tmid
@ -17,30 +47,9 @@ export async function sendMessageCall(message) {
_id, rid, msg, tmid
}
});
await changeMessageStatus(_id, tmid, messagesStatus.SENT);
} catch (e) {
const db = database.active;
const msgCollection = db.collections.get('messages');
const threadMessagesCollection = db.collections.get('thread_messages');
const errorBatch = [];
const messageRecord = await msgCollection.find(_id);
errorBatch.push(
messageRecord.prepareUpdate((m) => {
m.status = messagesStatus.ERROR;
})
);
if (tmid) {
const threadMessageRecord = await threadMessagesCollection.find(_id);
errorBatch.push(
threadMessageRecord.prepareUpdate((tm) => {
tm.status = messagesStatus.ERROR;
})
);
}
await db.action(async() => {
await db.batch(...errorBatch);
});
await changeMessageStatus(_id, tmid, messagesStatus.ERROR);
}
}

View File

@ -10,16 +10,17 @@ import reduxStore from '../../createStore';
import { addUserTyping, removeUserTyping, clearUserTyping } from '../../../actions/usersTyping';
import debounce from '../../../utils/debounce';
const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch(() => console.log('unsubscribeRoom')));
const unsubscribe = (subscriptions = []) => Promise.all(subscriptions.map(sub => sub.unsubscribe));
const removeListener = listener => listener.stop();
let promises;
let connectedListener;
let disconnectedListener;
let notifyRoomListener;
let messageReceivedListener;
export default function subscribeRoom({ rid }) {
console.log(`[RCRN] Subscribed to room ${ rid }`);
let promises;
let connectedListener;
let disconnectedListener;
let notifyRoomListener;
let messageReceivedListener;
const handleConnection = () => {
this.loadMissedMessages({ rid }).catch(e => console.log(e));
@ -197,25 +198,35 @@ export default function subscribeRoom({ rid }) {
});
});
const stop = () => {
const stop = async() => {
let params;
if (promises) {
promises.then(unsubscribe);
try {
params = await promises;
await unsubscribe(params);
} catch (error) {
// Do nothing
}
promises = false;
}
if (connectedListener) {
connectedListener.then(removeListener);
params = await connectedListener;
removeListener(params);
connectedListener = false;
}
if (disconnectedListener) {
disconnectedListener.then(removeListener);
params = await disconnectedListener;
removeListener(params);
disconnectedListener = false;
}
if (notifyRoomListener) {
notifyRoomListener.then(removeListener);
params = await notifyRoomListener;
removeListener(params);
notifyRoomListener = false;
}
if (messageReceivedListener) {
messageReceivedListener.then(removeListener);
params = await messageReceivedListener;
removeListener(params);
messageReceivedListener = false;
}
reduxStore.dispatch(clearUserTyping());
@ -226,11 +237,7 @@ export default function subscribeRoom({ rid }) {
notifyRoomListener = this.sdk.onStreamData('stream-notify-room', handleNotifyRoomReceived);
messageReceivedListener = this.sdk.onStreamData('stream-room-messages', handleMessageReceived);
try {
promises = this.sdk.subscribeRoom(rid);
} catch (e) {
log(e);
}
promises = this.sdk.subscribeRoom(rid);
return {
stop: () => stop()

View File

@ -903,6 +903,8 @@ const RocketChat = {
name, custom, showButton = true, service
} = services;
const authName = name || service;
if (custom && showButton) {
return 'oauth_custom';
}
@ -916,8 +918,8 @@ const RocketChat = {
}
// TODO: remove this after other oauth providers are implemented. e.g. Drupal, github_enterprise
const availableOAuth = ['facebook', 'github', 'gitlab', 'google', 'linkedin', 'meteor-developer', 'twitter'];
return availableOAuth.includes(name) ? 'oauth' : 'not_supported';
const availableOAuth = ['facebook', 'github', 'gitlab', 'google', 'linkedin', 'meteor-developer', 'twitter', 'wordpress'];
return availableOAuth.includes(authName) ? 'oauth' : 'not_supported';
},
getUsernameSuggestion() {
// RC 0.65.0
@ -1098,6 +1100,23 @@ const RocketChat = {
},
translateMessage(message, targetLanguage) {
return this.sdk.methodCall('autoTranslate.translateMessage', message, targetLanguage);
},
getRoomTitle(room) {
const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings;
return ((room.prid || useRealName) && room.fname) || room.name;
},
findOrCreateInvite({ rid, days, maxUses }) {
// RC 2.4.0
return this.sdk.post('findOrCreateInvite', { rid, days, maxUses });
},
validateInviteToken(token) {
// RC 2.4.0
return this.sdk.post('validateInviteToken', { token });
},
useInviteToken(token) {
// RC 2.4.0
return this.sdk.post('useInviteToken', { token });
}
};

View File

@ -1,3 +1,9 @@
export const formatAttachmentUrl = (attachmentUrl, userId, token, server) => (
encodeURI(attachmentUrl.includes('http') ? attachmentUrl : `${ server }${ attachmentUrl }?rc_uid=${ userId }&rc_token=${ token }`)
);
export const formatAttachmentUrl = (attachmentUrl, userId, token, server) => {
if (attachmentUrl.startsWith('http')) {
if (attachmentUrl.includes('rc_token')) {
return encodeURI(attachmentUrl);
}
return encodeURI(`${ attachmentUrl }?rc_uid=${ userId }&rc_token=${ token }`);
}
return encodeURI(`${ server }${ attachmentUrl }?rc_uid=${ userId }&rc_token=${ token }`);
};

View File

@ -1,6 +1,17 @@
import NotificationsIOS from 'react-native-notifications';
import NotificationsIOS, { NotificationAction, NotificationCategory } from 'react-native-notifications';
import reduxStore from '../../lib/createStore';
import I18n from '../../i18n';
const replyAction = new NotificationAction({
activationMode: 'background',
title: I18n.t('Reply'),
textInput: {
buttonTitle: I18n.t('Reply'),
placeholder: I18n.t('Type_message')
},
identifier: 'REPLY_ACTION'
});
class PushNotification {
constructor() {
@ -20,7 +31,12 @@ class PushNotification {
completion();
});
NotificationsIOS.requestPermissions();
const actions = [];
actions.push(new NotificationCategory({
identifier: 'MESSAGE',
actions: [replyAction]
}));
NotificationsIOS.requestPermissions(actions);
}
getDeviceToken() {

View File

@ -0,0 +1,442 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import {
PanGestureHandler,
State,
PinchGestureHandler
} from 'react-native-gesture-handler';
import FastImage from 'react-native-fast-image';
import Animated, { Easing } from 'react-native-reanimated';
import { ResponsiveComponent } from 'react-native-responsive-ui';
const AnimatedFastImage = Animated.createAnimatedComponent(FastImage);
const styles = StyleSheet.create({
wrapper: {
flex: 1
},
image: {
backgroundColor: 'transparent'
}
});
const {
set,
cond,
eq,
or,
add,
sub,
min,
max,
multiply,
divide,
lessThan,
decay,
timing,
diff,
not,
abs,
startClock,
stopClock,
clockRunning,
Value,
Clock,
event
} = Animated;
function scaleDiff(value) {
const tmp = new Value(1);
const prev = new Value(1);
return [set(tmp, divide(value, prev)), set(prev, value), tmp];
}
function dragDiff(value, updating) {
const tmp = new Value(0);
const prev = new Value(0);
return cond(
updating,
[set(tmp, sub(value, prev)), set(prev, value), tmp],
set(prev, 0)
);
}
// returns linear friction coeff. When `value` is 0 coeff is 1 (no friction), then
// it grows linearly until it reaches `MAX_FRICTION` when `value` is equal
// to `MAX_VALUE`
function friction(value) {
const MAX_FRICTION = 5;
const MAX_VALUE = 100;
return max(
1,
min(MAX_FRICTION, add(1, multiply(value, (MAX_FRICTION - 1) / MAX_VALUE)))
);
}
function speed(value) {
const clock = new Clock();
const dt = diff(clock);
return cond(lessThan(dt, 1), 0, multiply(1000, divide(diff(value), dt)));
}
const MIN_SCALE = 1;
const MAX_SCALE = 2;
function scaleRest(value) {
return cond(
lessThan(value, MIN_SCALE),
MIN_SCALE,
cond(lessThan(MAX_SCALE, value), MAX_SCALE, value)
);
}
function scaleFriction(value, rest, delta) {
const MAX_FRICTION = 20;
const MAX_VALUE = 0.5;
const res = multiply(value, delta);
const howFar = abs(sub(rest, value));
const f = max(
1,
min(MAX_FRICTION, add(1, multiply(howFar, (MAX_FRICTION - 1) / MAX_VALUE)))
);
return cond(
lessThan(0, howFar),
multiply(value, add(1, divide(add(delta, -1), f))),
res
);
}
function runTiming(clock, value, dest, startStopClock = true) {
const state = {
finished: new Value(0),
position: new Value(0),
frameTime: new Value(0),
time: new Value(0)
};
const config = {
toValue: new Value(0),
duration: 300,
easing: Easing.inOut(Easing.cubic)
};
return [
cond(clockRunning(clock), 0, [
set(state.finished, 0),
set(state.frameTime, 0),
set(state.time, 0),
set(state.position, value),
set(config.toValue, dest),
startStopClock && startClock(clock)
]),
timing(clock, state, config),
cond(state.finished, startStopClock && stopClock(clock)),
state.position
];
}
function runDecay(clock, value, velocity) {
const state = {
finished: new Value(0),
velocity: new Value(0),
position: new Value(0),
time: new Value(0)
};
const config = { deceleration: 0.99 };
return [
cond(clockRunning(clock), 0, [
set(state.finished, 0),
set(state.velocity, velocity),
set(state.position, value),
set(state.time, 0),
startClock(clock)
]),
set(state.position, value),
decay(clock, state, config),
cond(state.finished, stopClock(clock)),
state.position
];
}
function bouncyPinch(
value,
gesture,
gestureActive,
focalX,
displacementX,
focalY,
displacementY
) {
const clock = new Clock();
const delta = scaleDiff(gesture);
const rest = scaleRest(value);
const focalXRest = cond(
lessThan(value, 1),
0,
sub(displacementX, multiply(focalX, add(-1, divide(rest, value))))
);
const focalYRest = cond(
lessThan(value, 1),
0,
sub(displacementY, multiply(focalY, add(-1, divide(rest, value))))
);
const nextScale = new Value(1);
return cond(
[delta, gestureActive],
[
stopClock(clock),
set(nextScale, scaleFriction(value, rest, delta)),
set(
displacementX,
sub(displacementX, multiply(focalX, add(-1, divide(nextScale, value))))
),
set(
displacementY,
sub(displacementY, multiply(focalY, add(-1, divide(nextScale, value))))
),
nextScale
],
cond(
or(clockRunning(clock), not(eq(rest, value))),
[
set(displacementX, runTiming(clock, displacementX, focalXRest, false)),
set(displacementY, runTiming(clock, displacementY, focalYRest, false)),
runTiming(clock, value, rest)
],
value
)
);
}
function bouncy(
value,
gestureDiv,
gestureActive,
lowerBound,
upperBound,
f
) {
const timingClock = new Clock();
const decayClock = new Clock();
const velocity = speed(value);
// did value go beyond the limits (lower, upper)
const isOutOfBounds = or(
lessThan(value, lowerBound),
lessThan(upperBound, value)
);
// position to snap to (upper or lower is beyond or the current value elsewhere)
const rest = cond(
lessThan(value, lowerBound),
lowerBound,
cond(lessThan(upperBound, value), upperBound, value)
);
// how much the value exceeds the bounds, this is used to calculate friction
const outOfBounds = abs(sub(rest, value));
return cond(
[gestureDiv, velocity, gestureActive],
[
stopClock(timingClock),
stopClock(decayClock),
add(value, divide(gestureDiv, f(outOfBounds)))
],
cond(
or(clockRunning(timingClock), isOutOfBounds),
[stopClock(decayClock), runTiming(timingClock, value, rest)],
cond(
or(clockRunning(decayClock), lessThan(5, abs(velocity))),
runDecay(decayClock, value, velocity),
value
)
)
);
}
const WIDTH = 300;
const HEIGHT = 300;
// it was picked from https://github.com/software-mansion/react-native-reanimated/tree/master/Example/imageViewer
// and changed to use FastImage animated component
class ImageViewer extends ResponsiveComponent {
pinchRef = React.createRef();
panRef = React.createRef();
static propTypes = {
uri: PropTypes.string
}
constructor(props) {
super(props);
// DECLARE TRANSX
const panTransX = new Value(0);
const panTransY = new Value(0);
// PINCH
const pinchScale = new Value(1);
const pinchFocalX = new Value(0);
const pinchFocalY = new Value(0);
const pinchState = new Value(-1);
this._onPinchEvent = event([
{
nativeEvent: {
state: pinchState,
scale: pinchScale,
focalX: pinchFocalX,
focalY: pinchFocalY
}
}
]);
// SCALE
const scale = new Value(1);
const pinchActive = eq(pinchState, State.ACTIVE);
this._focalDisplacementX = new Value(0);
const relativeFocalX = sub(
pinchFocalX,
add(panTransX, this._focalDisplacementX)
);
this._focalDisplacementY = new Value(0);
const relativeFocalY = sub(
pinchFocalY,
add(panTransY, this._focalDisplacementY)
);
this._scale = set(
scale,
bouncyPinch(
scale,
pinchScale,
pinchActive,
relativeFocalX,
this._focalDisplacementX,
relativeFocalY,
this._focalDisplacementY
)
);
// PAN
const dragX = new Value(0);
const dragY = new Value(0);
const panState = new Value(-1);
this._onPanEvent = event([
{
nativeEvent: {
translationX: dragX,
translationY: dragY,
state: panState
}
}
]);
const panActive = eq(panState, State.ACTIVE);
const panFriction = value => friction(value);
// X
const panUpX = cond(
lessThan(this._scale, 1),
0,
multiply(-1, this._focalDisplacementX)
);
const panLowX = add(panUpX, multiply(-WIDTH, add(max(1, this._scale), -1)));
this._panTransX = set(
panTransX,
bouncy(
panTransX,
dragDiff(dragX, panActive),
or(panActive, pinchActive),
panLowX,
panUpX,
panFriction
)
);
// Y
const panUpY = cond(
lessThan(this._scale, 1),
0,
multiply(-1, this._focalDisplacementY)
);
const panLowY = add(
panUpY,
multiply(-HEIGHT, add(max(1, this._scale), -1))
);
this._panTransY = set(
panTransY,
bouncy(
panTransY,
dragDiff(dragY, panActive),
or(panActive, pinchActive),
panLowY,
panUpY,
panFriction
)
);
}
render() {
const { uri, ...props } = this.props;
const { width } = this.state.window;
// The below two animated values makes it so that scale appears to be done
// from the top left corner of the image view instead of its center. This
// is required for the "scale focal point" math to work correctly
const scaleTopLeftFixX = divide(multiply(WIDTH, add(this._scale, -1)), 2);
const scaleTopLeftFixY = divide(multiply(HEIGHT, add(this._scale, -1)), 2);
return (
<View style={styles.wrapper}>
<PinchGestureHandler
ref={this.pinchRef}
simultaneousHandlers={this.panRef}
onGestureEvent={this._onPinchEvent}
onHandlerStateChange={this._onPinchEvent}
>
<Animated.View>
<PanGestureHandler
ref={this.panRef}
minDist={10}
avgTouches
simultaneousHandlers={this.pinchRef}
onGestureEvent={this._onPanEvent}
onHandlerStateChange={this._onPanEvent}
>
<AnimatedFastImage
style={[
styles.image,
{
width,
height: '100%'
},
{
transform: [
{ translateX: this._panTransX },
{ translateY: this._panTransY },
{ translateX: this._focalDisplacementX },
{ translateY: this._focalDisplacementY },
{ translateX: scaleTopLeftFixX },
{ translateY: scaleTopLeftFixY },
{ scale: this._scale }
]
}
]}
resizeMode='contain'
source={{ uri }}
{...props}
/>
</PanGestureHandler>
</Animated.View>
</PinchGestureHandler>
</View>
);
}
}
export default ImageViewer;

View File

@ -0,0 +1,38 @@
import React from 'react';
import { ScrollView, StyleSheet } from 'react-native';
import FastImage from 'react-native-fast-image';
import PropTypes from 'prop-types';
const styles = StyleSheet.create({
scrollContent: {
width: '100%',
height: '100%'
},
image: {
flex: 1
}
});
const ImageViewer = ({
uri, ...props
}) => (
<ScrollView
contentContainerStyle={styles.scrollContent}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
maximumZoomScale={2}
>
<FastImage
style={styles.image}
resizeMode='contain'
source={{ uri }}
{...props}
/>
</ScrollView>
);
ImageViewer.propTypes = {
uri: PropTypes.string
};
export default ImageViewer;

View File

@ -1,5 +1,4 @@
import React from 'react';
import { shortnameToUnicode } from 'emoji-toolkit';
import PropTypes from 'prop-types';
import _ from 'lodash';
@ -7,6 +6,7 @@ import I18n from '../../i18n';
import styles from './styles';
import Markdown from '../../containers/markdown';
import { themes } from '../../constants/colors';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
const formatMsg = ({
lastMessage, type, showLastMessage, username
@ -14,7 +14,7 @@ const formatMsg = ({
if (!showLastMessage) {
return '';
}
if (!lastMessage || lastMessage.pinned) {
if (!lastMessage || !lastMessage.u || lastMessage.pinned) {
return I18n.t('No_Message');
}
if (lastMessage.t === 'jitsi_call_started') {

View File

@ -5,9 +5,7 @@ import { connect } from 'react-redux';
import Avatar from '../../containers/Avatar';
import I18n from '../../i18n';
import styles, {
ROW_HEIGHT
} from './styles';
import styles, { ROW_HEIGHT } from './styles';
import UnreadBadge from './UnreadBadge';
import TypeIcon from './TypeIcon';
import LastMessage from './LastMessage';

View File

@ -15,6 +15,7 @@ import crashReport from './crashReport';
import customEmojis from './customEmojis';
import activeUsers from './activeUsers';
import usersTyping from './usersTyping';
import inviteLinks from './inviteLinks';
export default combineReducers({
settings,
@ -32,5 +33,6 @@ export default combineReducers({
crashReport,
customEmojis,
activeUsers,
usersTyping
usersTyping,
inviteLinks
});

View File

@ -0,0 +1,37 @@
import { INVITE_LINKS } from '../actions/actionsTypes';
const initialState = {
token: '',
days: 1,
maxUses: 0,
invite: {}
};
export default (state = initialState, action) => {
switch (action.type) {
case INVITE_LINKS.SET_TOKEN:
return {
token: action.token
};
case INVITE_LINKS.SET_PARAMS:
return {
...state,
...action.params
};
case INVITE_LINKS.SET_INVITE:
return {
...state,
invite: action.invite
};
case INVITE_LINKS.REQUEST:
return state;
case INVITE_LINKS.SUCCESS:
return initialState;
case INVITE_LINKS.FAILURE:
return initialState;
case INVITE_LINKS.CLEAR:
return initialState;
default:
return state;
}
};

View File

@ -6,16 +6,27 @@ import RNUserDefaults from 'rn-user-defaults';
import Navigation from '../lib/Navigation';
import * as types from '../actions/actionsTypes';
import { selectServerRequest } from '../actions/server';
import { inviteLinksSetToken, inviteLinksRequest } from '../actions/inviteLinks';
import database from '../lib/database';
import RocketChat from '../lib/rocketchat';
import EventEmitter from '../utils/events';
import { appStart } from '../actions';
import { isIOS } from '../utils/deviceInfo';
const roomTypes = {
channel: 'c', direct: 'd', group: 'p'
};
const handleInviteLink = function* handleInviteLink({ params, requireLogin = false }) {
if (params.path && params.path.startsWith('invite/')) {
const token = params.path.replace('invite/', '');
if (requireLogin) {
yield put(inviteLinksSetToken(token));
} else {
yield put(inviteLinksRequest(token));
}
}
};
const navigate = function* navigate({ params }) {
yield put(appStart('inside'));
if (params.rid) {
@ -25,6 +36,8 @@ const navigate = function* navigate({ params }) {
yield Navigation.navigate('RoomsListView');
Navigation.navigate('RoomView', { rid: params.rid, name, t: roomTypes[type] });
}
} else {
yield handleInviteLink({ params });
}
};
@ -33,10 +46,6 @@ const handleOpen = function* handleOpen({ params }) {
return;
}
if (isIOS) {
yield RNUserDefaults.setName('group.ios.chat.rocket');
}
let { host } = params;
if (!/^(http|https)/.test(host)) {
host = `https://${ params.host }`;
@ -68,7 +77,7 @@ const handleOpen = function* handleOpen({ params }) {
const servers = yield serversCollection.find(host);
if (servers && user) {
yield put(selectServerRequest(host));
yield take(types.SERVER.SELECT_SUCCESS);
yield take(types.LOGIN.SUCCESS);
yield navigate({ params });
return;
}
@ -87,10 +96,9 @@ const handleOpen = function* handleOpen({ params }) {
if (params.token) {
yield take(types.SERVER.SELECT_SUCCESS);
yield RocketChat.connect({ server: host, user: { token: params.token } });
} else {
yield handleInviteLink({ params, requireLogin: true });
}
Navigation.navigate('OnboardingView', { previousServer: server });
yield delay(1000);
EventEmitter.emit('NewServer', { server: host });
}
};

View File

@ -8,6 +8,7 @@ import createChannel from './createChannel';
import init from './init';
import state from './state';
import deepLinking from './deepLinking';
import inviteLinks from './inviteLinks';
const root = function* root() {
yield all([
@ -19,7 +20,8 @@ const root = function* root() {
messages(),
selectServer(),
state(),
deepLinking()
deepLinking(),
inviteLinks()
]);
};

View File

@ -1,8 +1,8 @@
import { AsyncStorage } from 'react-native';
import { put, takeLatest, all } from 'redux-saga/effects';
import SplashScreen from 'react-native-splash-screen';
import RNUserDefaults from 'rn-user-defaults';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import RNBootSplash from 'react-native-bootsplash';
import * as actions from '../actions';
import { selectServerRequest } from '../actions/server';
@ -94,9 +94,6 @@ const restore = function* restore() {
}
}
const allowCrashReport = yield RocketChat.getAllowCrashReport();
yield put(toggleCrashReport(allowCrashReport));
if (!token || !server) {
yield all([
RNUserDefaults.clear(RocketChat.TOKEN_KEY),
@ -125,7 +122,7 @@ const start = function* start({ root }) {
} else if (root === 'outside') {
yield Navigation.navigate('OutsideStack');
}
SplashScreen.hide();
RNBootSplash.hide();
};
const root = function* root() {

72
app/sagas/inviteLinks.js Normal file
View File

@ -0,0 +1,72 @@
import {
put, takeLatest, delay, select
} from 'redux-saga/effects';
import { Alert } from 'react-native';
import { INVITE_LINKS } from '../actions/actionsTypes';
import { inviteLinksSuccess, inviteLinksFailure, inviteLinksSetInvite } from '../actions/inviteLinks';
import RocketChat from '../lib/rocketchat';
import log from '../utils/log';
import Navigation from '../lib/Navigation';
import I18n from '../i18n';
const handleRequest = function* handleRequest({ token }) {
try {
const validateResult = yield RocketChat.validateInviteToken(token);
if (!validateResult.valid) {
yield put(inviteLinksFailure());
return;
}
const result = yield RocketChat.useInviteToken(token);
if (!result.success) {
yield put(inviteLinksFailure());
return;
}
if (result.room && result.room.rid) {
yield delay(1000);
yield Navigation.navigate('RoomsListView');
const { room } = result;
Navigation.navigate('RoomView', {
rid: room.rid,
name: RocketChat.getRoomTitle(room),
t: room.t
});
}
yield put(inviteLinksSuccess());
} catch (e) {
yield put(inviteLinksFailure());
log(e);
}
};
const handleFailure = function handleFailure() {
Alert.alert(I18n.t('Oops'), I18n.t('Invalid_or_expired_invite_token'));
};
const handleCreateInviteLink = function* handleCreateInviteLink({ rid }) {
try {
const inviteLinks = yield select(state => state.inviteLinks);
const result = yield RocketChat.findOrCreateInvite({
rid, days: inviteLinks.days, maxUses: inviteLinks.maxUses
});
if (!result.success) {
Alert.alert(I18n.t('Oops'), I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_invite') }));
return;
}
yield put(inviteLinksSetInvite(result));
} catch (e) {
log(e);
}
};
const root = function* root() {
yield takeLatest(INVITE_LINKS.REQUEST, handleRequest);
yield takeLatest(INVITE_LINKS.FAILURE, handleFailure);
yield takeLatest(INVITE_LINKS.CREATE, handleCreateInviteLink);
};
export default root;

View File

@ -20,6 +20,7 @@ import I18n from '../i18n';
import database from '../lib/database';
import EventEmitter from '../utils/events';
import Navigation from '../lib/Navigation';
import { inviteLinksRequest } from '../actions/inviteLinks';
const getServer = state => state.server.server;
const loginWithPasswordCall = args => RocketChat.loginWithPassword(args);
@ -111,20 +112,31 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
});
yield RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ server }`, user.id);
yield RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ user.id }`, user.token);
yield put(setUser(user));
EventEmitter.emit('connected');
let currentRoot;
if (!user.username) {
yield put(appStart('setUsername'));
} else if (adding) {
yield put(serverFinishAdd());
yield put(appStart('inside'));
} else {
const currentRoot = yield select(state => state.app.root);
currentRoot = yield select(state => state.app.root);
if (currentRoot !== 'inside') {
yield put(appStart('inside'));
}
}
// after a successful login, check if it's been invited via invite link
currentRoot = yield select(state => state.app.root);
if (currentRoot === 'inside') {
const inviteLinkToken = yield select(state => state.inviteLinks.token);
if (inviteLinkToken) {
yield put(inviteLinksRequest(inviteLinkToken));
}
}
} catch (e) {
log(e);
}

View File

@ -11,7 +11,18 @@ 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';
const updateRooms = function* updateRooms({ server, newRoomsUpdatedAt }) {
const serversDB = database.servers;
const serversCollection = serversDB.collections.get('servers');
const serverRecord = yield serversCollection.find(server);
return serversDB.action(async() => {
await serverRecord.update((record) => {
record.roomsUpdatedAt = newRoomsUpdatedAt;
});
});
};
const handleRoomsRequest = function* handleRoomsRequest() {
try {
@ -26,28 +37,15 @@ const handleRoomsRequest = function* handleRoomsRequest() {
const { subscriptions } = mergeSubscriptionsRooms(subscriptionsResult, roomsResult);
const db = database.active;
yield db.action(async() => {
if (!subscriptions.length) {
return;
}
const subCollection = db.collections.get('subscriptions');
// const messagesCollection = db.collections.get('messages');
const subCollection = db.collections.get('subscriptions');
if (subscriptions.length) {
const subsIds = subscriptions.map(sub => sub.rid);
const existingSubs = await subCollection.query(Q.where('id', Q.oneOf(subsIds))).fetch();
const existingSubs = yield subCollection.query(Q.where('id', Q.oneOf(subsIds))).fetch();
const subsToUpdate = existingSubs.filter(i1 => subscriptions.find(i2 => i1._id === i2._id));
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 = await 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);
@ -59,37 +57,14 @@ const handleRoomsRequest = function* handleRoomsRequest() {
Object.assign(subscription, newSub);
});
})
// ...messagesToCreate.map(message => messagesCollection.prepareCreate((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(() => {
// Object.assign(message, newMessage);
// });
// })
];
try {
yield db.action(async() => {
await db.batch(...allRecords);
} catch (e) {
log(e);
}
return allRecords.length;
});
yield serversDB.action(async() => {
try {
await serverRecord.update((record) => {
record.roomsUpdatedAt = newRoomsUpdatedAt;
});
} catch (e) {
log(e);
}
});
});
}
yield updateRooms({ server, newRoomsUpdatedAt });
yield put(roomsSuccess());
} catch (e) {
yield put(roomsFailure(e));

View File

@ -4,6 +4,7 @@ import {
import { Alert } from 'react-native';
import RNUserDefaults from 'rn-user-defaults';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import semver from 'semver';
import Navigation from '../lib/Navigation';
import { SERVER } from '../actions/actionsTypes';
@ -35,18 +36,20 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
return;
}
const validVersion = semver.coerce(serverInfo.version);
const serversDB = database.servers;
const serversCollection = serversDB.collections.get('servers');
yield serversDB.action(async() => {
try {
const serverRecord = await serversCollection.find(server);
await serverRecord.update((record) => {
record.version = serverInfo.version;
record.version = validVersion;
});
} catch (e) {
await serversCollection.create((record) => {
record._raw = sanitizedRaw({ id: server }, serversCollection.schema);
record.version = serverInfo.version;
record.version = validVersion;
});
}
});
@ -100,8 +103,6 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
RocketChat.setSettings();
RocketChat.setCustomEmojis();
yield RocketChat.setCustomEmojis();
let serverInfo;
if (fetchVersion) {
serverInfo = yield getServerInfo({ server, raiseError: false });

View File

@ -50,6 +50,11 @@ export const initTabletNav = (setState) => {
setState({ showModal: true });
return null;
}
if (routeName === 'AttachmentView') {
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
setState({ showModal: true });
return null;
}
}
if (action.type === 'Navigation/RESET' && isSplited()) {
const { params } = action.actions[action.index];

View File

@ -1,7 +1,8 @@
import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';
export const headers = { 'User-Agent': `RC-RN Mobile/${ DeviceInfo.getVersion() } (build: ${ DeviceInfo.getBuildNumber() }; os: ${ Platform.OS } ${ DeviceInfo.getSystemVersion() })` };
// this form is required by Rocket.Chat's parser in "app/statistics/server/lib/UAParserCustom.js"
export const headers = { 'User-Agent': `RC Mobile; ${ Platform.OS } ${ DeviceInfo.getSystemVersion() }; v${ DeviceInfo.getVersion() } (${ DeviceInfo.getBuildNumber() })` };
export default (url, options = {}) => {
let customOptions = { ...options, headers };

View File

@ -0,0 +1,10 @@
/* eslint-disable quote-props */
/* eslint-disable object-curly-newline */
/* eslint-disable object-curly-spacing */
/* eslint-disable comma-spacing */
/* eslint-disable key-spacing */
const ascii = {'*\\0/*':'🙆','*\\O/*':'🙆','-___-':'😑',':\'-)':'😂','\':-)':'😅','\':-D':'😅','>:-)':'😆','\':-(':'😓','>:-(':'😠',':\'-(':'😢','O:-)':'😇','0:-3':'😇','0:-)':'😇','0;^)':'😇','O;-)':'😇','0;-)':'😇','O:-3':'😇','-__-':'😑',':-Þ':'😛','</3':'💔',':\')':'😂',':-D':'😃','\':)':'😅','\'=)':'😅','\':D':'😅','\'=D':'😅','>:)':'😆','>;)':'😆','>=)':'😆',';-)':'😉','*-)':'😉',';-]':'😉',';^)':'😉','\':(':'😓','\'=(':'😓',':-*':'😘',':^*':'😘','>:P':'😜','X-P':'😜','>:[':'😞',':-(':'😞',':-[':'😞','>:(':'😠',':\'(':'😢',';-(':'😢','>.<':'😣','#-)':'😵','%-)':'😵','X-)':'😵','\\0/':'🙆','\\O/':'🙆','0:3':'😇','0:)':'😇','O:)':'😇','O=)':'😇','O:3':'😇','B-)':'😎','8-)':'😎','B-D':'😎','8-D':'😎','-_-':'😑','>:\\':'😕','>:/':'😕',':-/':'😕',':-.':'😕',':-P':'😛',':Þ':'😛',':-b':'😛',':-O':'😮','O_O':'😮','>:O':'😮',':-X':'😶',':-#':'😶',':-)':'🙂','(y)':'👍','<3':'❤','=D':'😃',';)':'😉','*)':'😉',';]':'😉',';D':'😉',':*':'😘','=*':'😘',':(':'😞',':[':'😞','=(':'😞',':@':'😠',';(':'😢','D:':'😨',':$':'😳','=$':'😳','#)':'😵','%)':'😵','X)':'😵','B)':'😎','8)':'😎',':/':'😕',':\\':'😕','=/':'😕','=\\':'😕',':L':'😕','=L':'😕',':P':'😛','=P':'😛',':b':'😛',':O':'😮',':X':'😶',':#':'😶','=X':'😶','=#':'😶',':)':'🙂','=]':'🙂','=)':'🙂',':]':'🙂',':D':'😄'};
export const asciiRegexp = '(\\*\\\\0\\/\\*|\\*\\\\O\\/\\*|\\-___\\-|\\:\'\\-\\)|\'\\:\\-\\)|\'\\:\\-D|\\>\\:\\-\\)|>\\:\\-\\)|\'\\:\\-\\(|\\>\\:\\-\\(|>\\:\\-\\(|\\:\'\\-\\(|O\\:\\-\\)|0\\:\\-3|0\\:\\-\\)|0;\\^\\)|O;\\-\\)|0;\\-\\)|O\\:\\-3|\\-__\\-|\\:\\-Þ|\\:\\-Þ|\\<\\/3|<\\/3|\\:\'\\)|\\:\\-D|\'\\:\\)|\'\\=\\)|\'\\:D|\'\\=D|\\>\\:\\)|>\\:\\)|\\>;\\)|>;\\)|\\>\\=\\)|>\\=\\)|;\\-\\)|\\*\\-\\)|;\\-\\]|;\\^\\)|\'\\:\\(|\'\\=\\(|\\:\\-\\*|\\:\\^\\*|\\>\\:P|>\\:P|X\\-P|\\>\\:\\[|>\\:\\[|\\:\\-\\(|\\:\\-\\[|\\>\\:\\(|>\\:\\(|\\:\'\\(|;\\-\\(|\\>\\.\\<|>\\.<|#\\-\\)|%\\-\\)|X\\-\\)|\\\\0\\/|\\\\O\\/|0\\:3|0\\:\\)|O\\:\\)|O\\=\\)|O\\:3|B\\-\\)|8\\-\\)|B\\-D|8\\-D|\\-_\\-|\\>\\:\\\\|>\\:\\\\|\\>\\:\\/|>\\:\\/|\\:\\-\\/|\\:\\-\\.|\\:\\-P|\\:Þ|\\:Þ|\\:\\-b|\\:\\-O|O_O|\\>\\:O|>\\:O|\\:\\-X|\\:\\-#|\\:\\-\\)|\\(y\\)|\\<3|<3|\\=D|;\\)|\\*\\)|;\\]|;D|\\:\\*|\\=\\*|\\:\\(|\\:\\[|\\=\\(|\\:@|;\\(|D\\:|\\:\\$|\\=\\$|#\\)|%\\)|X\\)|B\\)|8\\)|\\:\\/|\\:\\\\|\\=\\/|\\=\\\\|\\:L|\\=L|\\:P|\\=P|\\:b|\\:O|\\:X|\\:#|\\=X|\\=#|\\:\\)|\\=\\]|\\=\\)|\\:\\]|\\:D)';
export default ascii;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,46 @@
import emojis from './emojis';
import ascii, { asciiRegexp } from './ascii';
const shortnamePattern = new RegExp(/:[-+_a-z0-9]+:/, 'gi');
const replaceShortNameWithUnicode = shortname => emojis[shortname] || shortname;
const regAscii = new RegExp(`((\\s|^)${ asciiRegexp }(?=\\s|$|[!,.?]))`, 'gi');
const unescapeHTML = (string) => {
const unescaped = {
'&amp;': '&',
'&#38;': '&',
'&#x26;': '&',
'&lt;': '<',
'&#60;': '<',
'&#x3C;': '<',
'&gt;': '>',
'&#62;': '>',
'&#x3E;': '>',
'&quot;': '"',
'&#34;': '"',
'&#x22;': '"',
'&apos;': '\'',
'&#39;': '\'',
'&#x27;': '\''
};
return string.replace(/&(?:amp|#38|#x26|lt|#60|#x3C|gt|#62|#x3E|apos|#39|#x27|quot|#34|#x22);/ig, match => unescaped[match]);
};
const shortnameToUnicode = (str) => {
str = str.replace(shortnamePattern, replaceShortNameWithUnicode);
str = str.replace(regAscii, (entire, m1, m2, m3) => {
if (!m3 || (!(unescapeHTML(m3) in ascii))) {
// if the ascii doesnt exist just return the entire match
return entire;
}
m3 = unescapeHTML(m3);
return ascii[m3];
});
return str;
};
export default shortnameToUnicode;

View File

@ -0,0 +1,38 @@
/* eslint-disable no-undef */
import shortnameToUnicode from './index';
test('render joy', () => {
expect(shortnameToUnicode(':joy:')).toBe('😂');
});
test('render several emojis', () => {
expect(shortnameToUnicode(':dog::cat::hamburger::icecream::rocket:')).toBe('🐶🐱🍔🍦🚀');
});
test('render unknown emoji', () => {
expect(shortnameToUnicode(':unknown:')).toBe(':unknown:');
});
test('render empty', () => {
expect(shortnameToUnicode('')).toBe('');
});
test('render text with emoji', () => {
expect(shortnameToUnicode('Hello there! :hugging:')).toBe('Hello there! 🤗');
});
test('render ascii smile', () => {
expect(shortnameToUnicode(':)')).toBe('🙂');
});
test('render several ascii emojis', () => {
expect(shortnameToUnicode(':) :( -_- \':-D')).toBe('🙂😞😑😅');
});
test('render text with ascii emoji', () => {
expect(shortnameToUnicode('Hello there! :)')).toBe('Hello there!🙂');
});
test('render emoji and ascii emoji', () => {
expect(shortnameToUnicode('\':-D :joy:')).toBe('😅 😂');
});

152
app/views/AttachmentView.js Normal file
View File

@ -0,0 +1,152 @@
import React from 'react';
import { StyleSheet, View, PermissionsAndroid } from 'react-native';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import CameraRoll from '@react-native-community/cameraroll';
import * as mime from 'react-native-mime-types';
import { FileSystem } from 'react-native-unimodules';
import { Video } from 'expo-av';
import SHA256 from 'js-sha256';
import { LISTENER } from '../containers/Toast';
import EventEmitter from '../utils/events';
import I18n from '../i18n';
import { withTheme } from '../theme';
import ImageViewer from '../presentation/ImageViewer';
import { themedHeader } from '../utils/navigation';
import { themes } from '../constants/colors';
import { formatAttachmentUrl } from '../lib/utils';
import RCActivityIndicator from '../containers/ActivityIndicator';
import { SaveButton, CloseModalButton } from '../containers/HeaderButton';
import { isAndroid } from '../utils/deviceInfo';
const styles = StyleSheet.create({
container: {
flex: 1
}
});
class AttachmentView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => {
const { theme } = screenProps;
const attachment = navigation.getParam('attachment');
const from = navigation.getParam('from');
const handleSave = navigation.getParam('handleSave', () => {});
const { title, video_url } = attachment;
const options = {
title,
...themedHeader(theme),
headerRight: !video_url ? <SaveButton testID='save-image' onPress={handleSave} /> : null
};
if (from !== 'MessagesView') {
options.gesturesEnabled = false;
options.headerLeft = <CloseModalButton testID='close-attachment-view' navigation={navigation} />;
}
return options;
}
static propTypes = {
navigation: PropTypes.object,
theme: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
})
}
constructor(props) {
super(props);
const attachment = props.navigation.getParam('attachment');
this.state = { attachment, loading: true };
}
componentDidMount() {
const { navigation } = this.props;
navigation.setParams({ handleSave: this.handleSave });
}
handleSave = async() => {
const { attachment } = this.state;
const { user, baseUrl } = this.props;
const { image_url, image_type } = attachment;
const img = formatAttachmentUrl(image_url, user.id, user.token, baseUrl);
if (isAndroid) {
const rationale = {
title: I18n.t('Write_External_Permission'),
message: I18n.t('Write_External_Permission_Message')
};
const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, rationale);
if (!(result || result === PermissionsAndroid.RESULTS.GRANTED)) {
return;
}
}
this.setState({ loading: true });
try {
const extension = `.${ mime.extension(image_type) || 'jpg' }`;
const file = `${ FileSystem.documentDirectory + SHA256(image_url) + extension }`;
const { uri } = await FileSystem.downloadAsync(img, file);
await CameraRoll.save(uri, { album: 'Rocket.Chat' });
EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') });
} catch (e) {
EventEmitter.emit(LISTENER, { message: I18n.t('error-save-image') });
}
this.setState({ loading: false });
};
renderImage = uri => (
<ImageViewer
uri={uri}
onLoadEnd={() => this.setState({ loading: false })}
/>
);
renderVideo = uri => (
<Video
source={{ uri }}
rate={1.0}
volume={1.0}
isMuted={false}
resizeMode={Video.RESIZE_MODE_CONTAIN}
shouldPlay
isLooping={false}
style={styles.container}
useNativeControls
onLoad={() => this.setState({ loading: false })}
onError={console.log}
/>
);
render() {
const { loading, attachment } = this.state;
const { theme, user, baseUrl } = this.props;
let content = null;
if (attachment && attachment.image_url) {
const uri = formatAttachmentUrl(attachment.image_url, user.id, user.token, baseUrl);
content = this.renderImage(encodeURI(uri));
} else if (attachment && attachment.video_url) {
const uri = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl);
content = this.renderVideo(encodeURI(uri));
}
return (
<View style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}>
{content}
{loading ? <RCActivityIndicator absolute size='large' theme={theme} /> : null}
</View>
);
}
}
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
}
});
export default connect(mapStateToProps)(withTheme(AttachmentView));

View File

@ -1,30 +1,10 @@
import React from 'react';
import { StyleSheet, Image } from 'react-native';
import StatusBar from '../containers/StatusBar';
import { isAndroid } from '../utils/deviceInfo';
import { withTheme } from '../theme';
const styles = StyleSheet.create({
image: {
width: '100%',
height: '100%',
backgroundColor: 'white'
}
});
export default React.memo(withTheme(({ theme }) => (
<>
<StatusBar theme={theme} />
{isAndroid
? (
<Image
source={{ uri: 'launch_screen' }}
style={styles.image}
resizeMode='contain'
/>
)
: null
}
</>
)));

View File

@ -2,6 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { WebView } from 'react-native-webview';
import { connect } from 'react-redux';
import parse from 'url-parse';
import RocketChat from '../lib/rocketchat';
import { isIOS } from '../utils/deviceInfo';
import { CloseModalButton } from '../containers/HeaderButton';
@ -9,6 +11,7 @@ import StatusBar from '../containers/StatusBar';
import ActivityIndicator from '../containers/ActivityIndicator';
import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation';
import debounce from '../utils/debounce';
const userAgent = isIOS
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
@ -40,6 +43,12 @@ class AuthenticationWebView extends React.PureComponent {
this.redirectRegex = new RegExp(`(?=.*(${ props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g');
}
componentWillUnmount() {
if (this.debouncedLogin && this.debouncedLogin.stop) {
this.debouncedLogin.stop();
}
}
dismiss = () => {
const { navigation } = this.props;
navigation.pop();
@ -62,24 +71,25 @@ class AuthenticationWebView extends React.PureComponent {
this.dismiss();
}
// eslint-disable-next-line react/sort-comp
debouncedLogin = debounce(params => this.login(params), 3000);
onNavigationStateChange = (webViewState) => {
const url = decodeURIComponent(webViewState.url);
if (this.authType === 'saml' || this.authType === 'cas') {
const { navigation } = this.props;
const ssoToken = navigation.getParam('ssoToken');
if (url.includes('ticket') || url.includes('validate')) {
if (url.includes('ticket') || url.includes('validate') || url.includes('saml_idp_credentialToken')) {
let payload;
const credentialToken = { credentialToken: ssoToken };
if (this.authType === 'saml') {
const parsedUrl = parse(url, true);
const token = (parsedUrl.query && parsedUrl.query.saml_idp_credentialToken) || ssoToken;
const credentialToken = { credentialToken: token };
payload = { ...credentialToken, saml: true };
} else {
payload = { cas: credentialToken };
payload = { cas: { credentialToken: ssoToken } };
}
// We need to set a timeout when the login is done with SSO in order to make it work on our side.
// It is actually due to the SSO server processing the response.
setTimeout(() => {
this.login(payload);
}, 3000);
this.debouncedLogin(payload);
}
}

View File

@ -0,0 +1,160 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ScrollView, View } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import { connect } from 'react-redux';
import RNPickerSelect from 'react-native-picker-select';
import {
inviteLinksSetParams as inviteLinksSetParamsAction,
inviteLinksCreate as inviteLinksCreateAction
} from '../../actions/inviteLinks';
import ListItem from '../../containers/ListItem';
import styles from './styles';
import Button from '../../containers/Button';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import I18n from '../../i18n';
import StatusBar from '../../containers/StatusBar';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
import Separator from '../../containers/Separator';
const OPTIONS = {
days: [{
label: I18n.t('Never'), value: 0
},
{
label: '1', value: 1
},
{
label: '7', value: 7
},
{
label: '15', value: 15
},
{
label: '30', value: 30
}],
maxUses: [{
label: I18n.t('No_limit'), value: 0
},
{
label: '1', value: 1
},
{
label: '5', value: 5
},
{
label: '10', value: 10
},
{
label: '25', value: 25
},
{
label: '50', value: 50
},
{
label: '100', value: 100
}]
};
class InviteUsersView extends React.Component {
static navigationOptions = ({ screenProps }) => ({
title: I18n.t('Invite_users'),
...themedHeader(screenProps.theme)
})
static propTypes = {
navigation: PropTypes.object,
theme: PropTypes.string,
timeDateFormat: PropTypes.string,
createInviteLink: PropTypes.func,
inviteLinksSetParams: PropTypes.func
}
constructor(props) {
super(props);
this.rid = props.navigation.getParam('rid');
}
onValueChangePicker = (key, value) => {
const { inviteLinksSetParams } = this.props;
const params = {
[key]: value
};
inviteLinksSetParams(params);
}
createInviteLink = () => {
const { createInviteLink, navigation } = this.props;
createInviteLink(this.rid);
navigation.pop();
}
renderPicker = (key) => {
const { props } = this;
const { theme } = props;
return (
<RNPickerSelect
style={{ viewContainer: styles.viewContainer }}
value={props[key]}
textInputProps={{ style: { ...styles.pickerText, color: themes[theme].actionTintColor } }}
useNativeAndroidPickerStyle={false}
placeholder={{}}
onValueChange={value => this.onValueChangePicker(key, value)}
items={OPTIONS[key]}
/>
);
}
render() {
const { theme } = this.props;
return (
<SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} forceInset={{ vertical: 'never' }}>
<ScrollView
{...scrollPersistTaps}
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
>
<StatusBar theme={theme} />
<Separator theme={theme} />
<ListItem
title={I18n.t('Expiration_Days')}
right={() => this.renderPicker('days')}
theme={theme}
/>
<Separator theme={theme} />
<ListItem
title={I18n.t('Max_number_of_uses')}
right={() => this.renderPicker('maxUses')}
theme={theme}
/>
<Separator theme={theme} />
<View style={styles.innerContainer}>
<View style={[styles.divider, { backgroundColor: themes[theme].separatorColor }]} />
<Button
title={I18n.t('Generate_New_Link')}
type='primary'
onPress={this.createInviteLink}
theme={theme}
/>
</View>
</ScrollView>
</SafeAreaView>
);
}
}
const mapStateToProps = state => ({
days: state.inviteLinks.days,
maxUses: state.inviteLinks.maxUses
});
const mapDispatchToProps = dispatch => ({
inviteLinksSetParams: params => dispatch(inviteLinksSetParamsAction(params)),
createInviteLink: rid => dispatch(inviteLinksCreateAction(rid))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersView));

View File

@ -0,0 +1,45 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../Styles';
export default StyleSheet.create({
container: {
flex: 1
},
innerContainer: {
paddingHorizontal: 20
},
divider: {
width: '100%',
height: StyleSheet.hairlineWidth,
marginVertical: 20
},
sectionSeparatorBorder: {
height: 10
},
marginBottom: {
height: 30
},
contentContainer: {
marginVertical: 10
},
infoText: {
...sharedStyles.textRegular,
fontSize: 13,
paddingHorizontal: 15,
paddingVertical: 10
},
sectionTitle: {
...sharedStyles.separatorBottom,
paddingHorizontal: 15,
paddingVertical: 10,
fontSize: 14
},
viewContainer: {
justifyContent: 'center'
},
pickerText: {
...sharedStyles.textRegular,
fontSize: 16
}
});

View File

@ -0,0 +1,151 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Share, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import moment from 'moment';
import { connect } from 'react-redux';
import {
inviteLinksCreate as inviteLinksCreateAction,
inviteLinksClear as inviteLinksClearAction
} from '../../actions/inviteLinks';
import RCTextInput from '../../containers/TextInput';
import styles from './styles';
import Markdown from '../../containers/markdown';
import Button from '../../containers/Button';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import I18n from '../../i18n';
import StatusBar from '../../containers/StatusBar';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
class InviteUsersView extends React.Component {
static navigationOptions = ({ screenProps }) => ({
title: I18n.t('Invite_users'),
...themedHeader(screenProps.theme)
})
static propTypes = {
navigation: PropTypes.object,
theme: PropTypes.string,
timeDateFormat: PropTypes.string,
invite: PropTypes.object,
createInviteLink: PropTypes.func,
clearInviteLink: PropTypes.func
}
constructor(props) {
super(props);
this.rid = props.navigation.getParam('rid');
}
componentDidMount() {
const { createInviteLink } = this.props;
createInviteLink(this.rid);
}
componentWillUnmount() {
const { clearInviteLink } = this.props;
clearInviteLink();
}
share = () => {
const { invite } = this.props;
if (!invite) {
return;
}
Share.share({ message: invite.url });
}
edit = () => {
const { navigation } = this.props;
navigation.navigate('InviteUsersEditView', { rid: this.rid });
}
linkExpirationText = () => {
const { timeDateFormat, invite } = this.props;
if (!invite || !invite.url) {
return null;
}
if (invite.expires) {
const expiration = new Date(invite.expires);
if (invite.maxUses) {
const usesLeft = invite.maxUses - invite.uses;
return I18n.t('Your_invite_link_will_expire_on__date__or_after__usesLeft__uses', { date: moment(expiration).format(timeDateFormat), usesLeft });
}
return I18n.t('Your_invite_link_will_expire_on__date__', { date: moment(expiration).format(timeDateFormat) });
}
if (invite.maxUses) {
const usesLeft = invite.maxUses - invite.uses;
return I18n.t('Your_invite_link_will_expire_after__usesLeft__uses', { usesLeft });
}
return I18n.t('Your_invite_link_will_never_expire');
}
renderExpiration = () => {
const { theme } = this.props;
const expirationMessage = this.linkExpirationText();
return <Markdown msg={expirationMessage} username='' baseUrl='' theme={theme} />;
}
render() {
const {
theme, invite
} = this.props;
return (
<SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} forceInset={{ vertical: 'never' }}>
<ScrollView
{...scrollPersistTaps}
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
>
<StatusBar theme={theme} />
<View style={styles.innerContainer}>
<RCTextInput
label={I18n.t('Invite_Link')}
theme={theme}
value={invite && invite.url}
editable={false}
/>
{this.renderExpiration()}
<View style={[styles.divider, { backgroundColor: themes[theme].separatorColor }]} />
<Button
title={I18n.t('Share_Link')}
type='primary'
onPress={this.share}
theme={theme}
/>
<Button
title={I18n.t('Edit_Invite')}
type='secondary'
onPress={this.edit}
theme={theme}
/>
</View>
</ScrollView>
</SafeAreaView>
);
}
}
const mapStateToProps = state => ({
timeDateFormat: state.settings.Message_TimeAndDateFormat,
days: state.inviteLinks.days,
maxUses: state.inviteLinks.maxUses,
invite: state.inviteLinks.invite
});
const mapDispatchToProps = dispatch => ({
createInviteLink: rid => dispatch(inviteLinksCreateAction(rid)),
clearInviteLink: () => dispatch(inviteLinksClearAction())
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersView));

View File

@ -0,0 +1,16 @@
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
container: {
flex: 1
},
innerContainer: {
padding: 20,
paddingBottom: 0
},
divider: {
width: '100%',
height: StyleSheet.hairlineWidth,
marginVertical: 20
}
});

View File

@ -27,7 +27,8 @@ const styles = StyleSheet.create({
paddingVertical: 30
},
safeArea: {
paddingBottom: 30
paddingBottom: 30,
flex: 1
},
serviceButton: {
borderRadius: 2,
@ -231,6 +232,17 @@ class LoginSignupView extends React.Component {
this.openOAuth({ url });
}
onPressWordpress = () => {
const { services, server } = this.props;
const { clientId, serverURL } = services.wordpress;
const endpoint = `${ serverURL }/oauth/authorize`;
const redirect_uri = `${ server }/_oauth/wordpress?close`;
const scope = 'openid';
const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` });
}
onPressCustomOAuth = (loginService) => {
const { server } = this.props;
const {
@ -313,7 +325,8 @@ class LoginSignupView extends React.Component {
google: this.onPressGoogle,
linkedin: this.onPressLinkedin,
'meteor-developer': this.onPressMeteor,
twitter: this.onPressTwitter
twitter: this.onPressTwitter,
wordpress: this.onPressWordpress
};
return oauthProviders[name];
}
@ -421,18 +434,22 @@ class LoginSignupView extends React.Component {
render() {
const { theme } = this.props;
return (
<ScrollView
style={[
sharedStyles.containerScrollView,
sharedStyles.container,
styles.container,
{ backgroundColor: themes[theme].backgroundColor },
isTablet && sharedStyles.tabletScreenContent
]}
{...scrollPersistTaps}
<SafeAreaView
testID='welcome-view'
forceInset={{ vertical: 'never' }}
style={[styles.safeArea, { backgroundColor: themes[theme].backgroundColor }]}
>
<StatusBar theme={theme} />
<SafeAreaView testID='welcome-view' forceInset={{ vertical: 'never' }} style={styles.safeArea}>
<ScrollView
style={[
sharedStyles.containerScrollView,
sharedStyles.container,
styles.container,
{ backgroundColor: themes[theme].backgroundColor },
isTablet && sharedStyles.tabletScreenContent
]}
{...scrollPersistTaps}
>
<StatusBar theme={theme} />
{this.renderServices()}
{this.renderServicesSeparator()}
<Button
@ -449,8 +466,8 @@ class LoginSignupView extends React.Component {
theme={theme}
testID='welcome-view-register'
/>
</SafeAreaView>
</ScrollView>
</ScrollView>
</SafeAreaView>
);
}
}

View File

@ -70,10 +70,6 @@ class LoginView extends React.Component {
Accounts_PasswordReset: true
}
static defaultProps = {
Accounts_PasswordReset: true
}
constructor(props) {
super(props);
this.state = {

View File

@ -13,9 +13,9 @@ import I18n from '../../i18n';
import RocketChat from '../../lib/rocketchat';
import StatusBar from '../../containers/StatusBar';
import getFileUrlFromMessage from '../../lib/methods/helpers/getFileUrlFromMessage';
import FileModal from '../../containers/FileModal';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import { withSplit } from '../../split';
import { themedHeader } from '../../utils/navigation';
const ACTION_INDEX = 0;
@ -32,7 +32,8 @@ class MessagesView extends React.Component {
baseUrl: PropTypes.string,
navigation: PropTypes.object,
customEmojis: PropTypes.object,
theme: PropTypes.string
theme: PropTypes.string,
split: PropTypes.bool
}
constructor(props) {
@ -40,8 +41,6 @@ class MessagesView extends React.Component {
this.state = {
loading: false,
messages: [],
selectedAttachment: {},
photoModalVisible: false,
fileLoading: true
};
this.rid = props.navigation.getParam('rid');
@ -55,7 +54,7 @@ class MessagesView extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
const {
loading, messages, photoModalVisible, fileLoading
loading, messages, fileLoading
} = this.state;
const { theme } = this.props;
if (nextProps.theme !== theme) {
@ -64,9 +63,6 @@ class MessagesView extends React.Component {
if (nextState.loading !== loading) {
return true;
}
if (nextState.photoModalVisible !== photoModalVisible) {
return true;
}
if (!equal(nextState.messages, messages)) {
return true;
}
@ -90,7 +86,7 @@ class MessagesView extends React.Component {
isEdited: !!item.editedAt,
isHeader: true,
attachments: item.attachments || [],
onOpenFileModal: this.onOpenFileModal,
showAttachment: this.showAttachment,
getCustomEmoji: this.getCustomEmoji
});
@ -215,12 +211,13 @@ class MessagesView extends React.Component {
return null;
}
onOpenFileModal = (attachment) => {
this.setState({ selectedAttachment: attachment, photoModalVisible: true });
}
onCloseFileModal = () => {
this.setState({ selectedAttachment: {}, photoModalVisible: false });
showAttachment = (attachment) => {
const { navigation, split } = this.props;
let params = { attachment };
if (split) {
params = { ...params, from: 'MessagesView' };
}
navigation.navigate('AttachmentView', params);
}
onLongPress = (message) => {
@ -278,10 +275,8 @@ class MessagesView extends React.Component {
renderItem = ({ item }) => this.content.renderItem(item)
render() {
const {
messages, loading, selectedAttachment, photoModalVisible, fileLoading
} = this.state;
const { user, baseUrl, theme } = this.props;
const { messages, loading } = this.state;
const { theme } = this.props;
if (!loading && messages.length === 0) {
return this.renderEmpty();
@ -305,15 +300,6 @@ class MessagesView extends React.Component {
onEndReached={this.load}
ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null}
/>
<FileModal
attachment={selectedAttachment}
isVisible={photoModalVisible}
onClose={this.onCloseFileModal}
user={user}
baseUrl={baseUrl}
loading={fileLoading}
setLoading={this.setFileLoading}
/>
</SafeAreaView>
);
}
@ -329,4 +315,4 @@ const mapStateToProps = state => ({
customEmojis: state.customEmojis
});
export default connect(mapStateToProps)(withTheme(MessagesView));
export default connect(mapStateToProps)(withSplit(withTheme(MessagesView)));

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