Merge beta into master (#1637)
|
@ -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
|
||||
|
|
40
README.md
|
@ -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 | ✅ |
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 30 KiB |
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="splashBackground" type="color">#000000</item>
|
||||
</resources>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
]);
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -59,6 +59,9 @@ export default {
|
|||
Message_TimeFormat: {
|
||||
type: 'valueAsString'
|
||||
},
|
||||
Message_TimeAndDateFormat: {
|
||||
type: 'valueAsString'
|
||||
},
|
||||
Site_Name: {
|
||||
type: 'valueAsString'
|
||||
},
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 }`}
|
||||
|
|
|
@ -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
|
||||
]}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -80,7 +80,7 @@ export default {
|
|||
Activity: 'Активность',
|
||||
Add_Reaction: 'Добавить реакцию',
|
||||
Add_Server: 'Добавить сервер',
|
||||
Add_user: 'Добавить пользователя',
|
||||
Add_users: 'Добавить пользователей',
|
||||
Admin_Panel: 'Панель админа',
|
||||
Alert: 'Оповещение',
|
||||
alert: 'оповещение',
|
||||
|
|
|
@ -80,7 +80,7 @@ export default {
|
|||
Activity: '按活动排序',
|
||||
Add_Reaction: '增加回复',
|
||||
Add_Server: '添加服务器',
|
||||
Add_user: '添加用户',
|
||||
Add_users: '添加用户',
|
||||
Alert: '警告',
|
||||
alert: '警告',
|
||||
alerts: '警告',
|
||||
|
|
31
app/index.js
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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 }`);
|
||||
};
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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') {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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()
|
||||
]);
|
||||
};
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
|
@ -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 = {
|
||||
'&': '&',
|
||||
'&': '&',
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'<': '<',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'>': '>',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
'"': '"',
|
||||
'"': '"',
|
||||
''': '\'',
|
||||
''': '\'',
|
||||
''': '\''
|
||||
};
|
||||
|
||||
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;
|
|
@ -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('😅 😂');
|
||||
});
|
|
@ -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));
|
|
@ -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
|
||||
}
|
||||
</>
|
||||
)));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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));
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,10 +70,6 @@ class LoginView extends React.Component {
|
|||
Accounts_PasswordReset: true
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
Accounts_PasswordReset: true
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
|
|
@ -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)));
|
||||
|
|