diff --git a/.eslintrc.js b/.eslintrc.js index 94d3a23e1..933fb9646 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -124,8 +124,7 @@ module.exports = { "prefer-const": 2, "object-shorthand": 2, "consistent-return": 0, - "global-require": "off", - "react/prop-types": [0, { skipUndeclared: true }] + "global-require": "off" }, "globals": { "__DEV__": true diff --git a/.snyk b/.snyk index 1c6af145e..e50db5b83 100644 --- a/.snyk +++ b/.snyk @@ -1,5 +1,5 @@ # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. -version: v1.7.1 +version: v1.12.0 ignore: {} # patches apply the minimum changes required to fix a vulnerability patch: @@ -24,3 +24,23 @@ patch: patched: '2017-09-29T23:29:20.238Z' - realm > extract-zip > debug: patched: '2017-09-29T23:29:20.238Z' + 'npm:lodash:20180130': + - react-native > plist > xmlbuilder > lodash: + patched: '2018-07-02T23:20:39.933Z' + 'npm:hoek:20180212': + - realm > node-pre-gyp > hawk > hoek: + patched: '2018-06-22T03:39:40.096Z' + - realm > node-pre-gyp > hawk > boom > hoek: + patched: '2018-06-22T03:39:40.096Z' + - realm > node-pre-gyp > hawk > sntp > hoek: + patched: '2018-06-22T03:39:40.096Z' + - realm > node-pre-gyp > hawk > cryptiles > boom > hoek: + patched: '2018-06-22T03:39:40.096Z' + - realm > node-pre-gyp > request > hawk > hoek: + patched: '2018-06-22T03:39:40.096Z' + - realm > node-pre-gyp > request > hawk > boom > hoek: + patched: '2018-06-22T03:39:40.096Z' + - realm > node-pre-gyp > request > hawk > sntp > hoek: + patched: '2018-06-22T03:39:40.096Z' + - realm > node-pre-gyp > request > hawk > cryptiles > boom > hoek: + patched: '2018-06-22T03:39:40.096Z' diff --git a/__tests__/__snapshots__/RoomItem.js.snap b/__tests__/__snapshots__/RoomItem.js.snap index 8b1bf18ed..0ed9a5785 100644 --- a/__tests__/__snapshots__/RoomItem.js.snap +++ b/__tests__/__snapshots__/RoomItem.js.snap @@ -618,7 +618,7 @@ exports[`render unread +999 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } @@ -868,7 +868,7 @@ exports[`render unread 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } @@ -1118,7 +1118,7 @@ exports[`renders correctly 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 7dc94d89e..e028f8da6 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -426,7 +426,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } @@ -648,7 +648,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } @@ -874,7 +874,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } @@ -1119,7 +1119,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } @@ -1368,7 +1368,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } @@ -1613,7 +1613,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } @@ -1858,7 +1858,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } @@ -2103,7 +2103,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } @@ -2348,7 +2348,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } @@ -2570,7 +2570,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } @@ -2792,7 +2792,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` undefined, ], Object { - "backgroundColor": "#cbced1", + "backgroundColor": undefined, }, ] } diff --git a/android/app/build.gradle b/android/app/build.gradle index f2b8268d6..d224d8bbe 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -95,13 +95,19 @@ android { defaultConfig { applicationId "chat.rocket.reactnative" - minSdkVersion 16 + minSdkVersion 19 targetSdkVersion 27 versionCode VERSIONCODE as Integer versionName "1" ndk { abiFilters "armeabi-v7a", "x86" } + missingDimensionStrategy "RNN.reactNativeVersion", "reactNative55" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } signingConfigs { @@ -172,29 +178,30 @@ repositories { } dependencies { - compile project(':react-native-i18n') - compile project(':react-native-fabric') - compile project(':react-native-audio') - compile project(":reactnativekeyboardinput") - compile project(':react-native-splash-screen') - compile project(':react-native-video') - compile project(':react-native-push-notification') - compile project(':react-native-svg') - compile project(':react-native-image-picker') - compile project(':react-native-vector-icons') - compile project(':react-native-fetch-blob') - compile project(':react-native-zeroconf') - compile project(':react-native-toast') - compile project(':react-native-fast-image') - compile project(':realm') - compile fileTree(dir: "libs", include: ["*.jar"]) - compile "com.android.support:appcompat-v7:27.1.0" - compile "com.android.support:support-v4:27.1.0" - compile 'com.android.support:customtabs:27.1.0' - compile "com.facebook.react:react-native:+" // From node_modules - compile 'com.facebook.fresco:fresco:1.7.1' - compile 'com.facebook.fresco:animated-gif:1.7.1' - compile('com.crashlytics.sdk.android:crashlytics:2.9.2@aar') { + implementation project(':react-native-i18n') + implementation project(':react-native-fabric') + implementation project(':react-native-audio') + implementation project(":reactnativekeyboardinput") + implementation project(':react-native-video') + implementation project(':react-native-svg') + implementation project(':react-native-image-picker') + implementation project(':react-native-vector-icons') + implementation project(':react-native-fetch-blob') + implementation project(':react-native-zeroconf') + implementation project(':react-native-toast') + implementation project(':react-native-fast-image') + implementation project(':realm') + implementation project(':react-native-navigation') + implementation project(':reactnativenotifications') + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation "com.android.support:appcompat-v7:27.1.0" + implementation "com.android.support:support-v4:27.1.0" + implementation 'com.android.support:customtabs:27.1.0' + implementation 'com.android.support:design:27.1.0' + implementation "com.facebook.react:react-native:+" // From node_modules + implementation 'com.facebook.fresco:fresco:1.7.1' + implementation 'com.facebook.fresco:animated-gif:1.7.1' + implementation('com.crashlytics.sdk.android:crashlytics:2.9.2@aar') { transitive = true; } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f00c03278..9d13f8947 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,76 +1,56 @@ + package="chat.rocket.reactnative" + android:versionCode="1" + android:versionName="1.0"> - - - - - - - - + + + + + + + + - - + + - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + diff --git a/android/app/src/main/java/chat/rocket/reactnative/CustomPushNotification.java b/android/app/src/main/java/chat/rocket/reactnative/CustomPushNotification.java new file mode 100644 index 000000000..5ada8975b --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/CustomPushNotification.java @@ -0,0 +1,53 @@ +package chat.rocket.reactnative; + +import android.app.Notification; +import android.app.PendingIntent; +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.Bundle; +import android.provider.Settings.System; +import android.media.RingtoneManager; + +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; + +public class CustomPushNotification extends PushNotification { + public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) { + super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper); + } + + @Override + protected Notification.Builder getNotificationBuilder(PendingIntent intent) { + final Resources res = mContext.getResources(); + String packageName = mContext.getPackageName(); + + 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"); + + final Notification.Builder notification = new Notification.Builder(mContext); + notification + .setSmallIcon(smallIconResId) + .setContentIntent(intent) + .setContentTitle(title) + .setContentText(message) + .setStyle(new Notification.BigTextStyle().bigText(message)) + .setPriority(Notification.PRIORITY_HIGH) + .setColor(mContext.getColor(R.color.notification_text)) + .setDefaults(Notification.DEFAULT_ALL) + .setAutoCancel(true); + + Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId); + notification.setLargeIcon(largeIconBitmap); + + return notification; + } +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.java b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.java index a539ec30f..169d7a9c3 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.java +++ b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.java @@ -1,26 +1,17 @@ package chat.rocket.reactnative; -import android.os.Bundle; -import com.facebook.react.ReactActivity; -import org.devio.rn.splashscreen.SplashScreen; -import com.crashlytics.android.Crashlytics; -import io.fabric.sdk.android.Fabric; +import android.graphics.drawable.Drawable; +import android.support.v4.content.ContextCompat; +import android.widget.LinearLayout; +import com.reactnativenavigation.controllers.SplashActivity; -public class MainActivity extends ReactActivity { - - /** - * Returns the name of the main component registered from JavaScript. - * This is used to schedule rendering of the component. - */ +public class MainActivity extends SplashActivity { @Override - protected String getMainComponentName() { - return "RocketChatRN"; - } + public LinearLayout createSplashLayout() { + LinearLayout splash = new LinearLayout(this); + Drawable launch_screen_bitmap = ContextCompat.getDrawable(getApplicationContext(),R.drawable.launch_screen_bitmap); + splash.setBackground(launch_screen_bitmap); - @Override - protected void onCreate(Bundle savedInstanceState) { - SplashScreen.show(this); - super.onCreate(savedInstanceState); - Fabric.with(this, new Crashlytics()); + return splash; } } diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java index c6fbea41c..6f4059c82 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java +++ b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java @@ -1,71 +1,99 @@ package chat.rocket.reactnative; -import android.app.Application; +import android.content.Context; +import android.os.Bundle; -import com.facebook.react.ReactApplication; +import com.AlexanderZaytsev.RNI18n.RNI18nPackage; +import com.RNFetchBlob.RNFetchBlobPackage; +import com.balthazargronon.RCTZeroconf.ZeroconfReactPackage; +import com.brentvatne.react.ReactVideoPackage; +import com.crashlytics.android.Crashlytics; +import com.dylanvann.fastimage.FastImageViewPackage; +import com.facebook.react.ReactPackage; +import com.facebook.react.shell.MainReactPackage; import com.horcrux.svg.SvgPackage; import com.imagepicker.ImagePickerPackage; import com.oblador.vectoricons.VectorIconsPackage; -import com.RNFetchBlob.RNFetchBlobPackage; -import com.balthazargronon.RCTZeroconf.ZeroconfReactPackage; -import io.realm.react.RealmReactPackage; -import com.facebook.react.ReactNativeHost; -import com.facebook.react.ReactPackage; -import com.facebook.react.shell.MainReactPackage; -import com.facebook.soloader.SoLoader; -import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage; -import com.brentvatne.react.ReactVideoPackage; +import com.reactnativenavigation.NavigationApplication; import com.remobile.toast.RCTToastPackage; -import com.wix.reactnativekeyboardinput.KeyboardInputPackage; import com.rnim.rn.audio.ReactNativeAudioPackage; import com.smixx.fabric.FabricPackage; -import com.dylanvann.fastimage.FastImageViewPackage; -import com.AlexanderZaytsev.RNI18n.RNI18nPackage; +import com.wix.reactnativekeyboardinput.KeyboardInputPackage; +import com.wix.reactnativenotifications.RNNotificationsPackage; +import com.wix.reactnativenotifications.core.AppLaunchHelper; +import com.wix.reactnativenotifications.core.AppLifecycleFacade; +import com.wix.reactnativenotifications.core.JsIOHelper; +import com.wix.reactnativenotifications.core.notification.INotificationsApplication; +import com.wix.reactnativenotifications.core.notification.IPushNotification; import java.util.Arrays; import java.util.List; -import org.devio.rn.splashscreen.SplashScreenReactPackage; -public class MainApplication extends Application implements ReactApplication { +import io.fabric.sdk.android.Fabric; +import io.realm.react.RealmReactPackage; + +public class MainApplication extends NavigationApplication implements INotificationsApplication { + + private NotificationsLifecycleFacade notificationsLifecycleFacade; - private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override - public boolean getUseDeveloperSupport() { - return BuildConfig.DEBUG; + public boolean isDebug() { + return BuildConfig.DEBUG; } @Override + public String getJSMainModuleName() { + return "index"; + } + protected List getPackages() { - return Arrays.asList( - new MainReactPackage(), - new SvgPackage(), - new ImagePickerPackage(), - new VectorIconsPackage(), - new RNFetchBlobPackage(), - new ZeroconfReactPackage(), - new RealmReactPackage(), - new ReactNativePushNotificationPackage(), - new ReactVideoPackage(), - new SplashScreenReactPackage(), - new RCTToastPackage(), - new ReactNativeAudioPackage(), - new KeyboardInputPackage(MainApplication.this), - new RocketChatNativePackage(), - new FabricPackage(), - new FastImageViewPackage(), - new RNI18nPackage() - ); + // Add additional packages you require here + // No need to add RnnPackage and MainReactPackage + return Arrays.asList( + ); } - }; - @Override - public ReactNativeHost getReactNativeHost() { - return mReactNativeHost; - } + @Override + public List createAdditionalReactPackages() { + return Arrays.asList( + new MainReactPackage(), + new SvgPackage(), + new ImagePickerPackage(), + new VectorIconsPackage(), + new RNFetchBlobPackage(), + new ZeroconfReactPackage(), + new RealmReactPackage(), + new ReactVideoPackage(), + new RCTToastPackage(), + new ReactNativeAudioPackage(), + new KeyboardInputPackage(MainApplication.this), + new RocketChatNativePackage(), + new FabricPackage(), + new FastImageViewPackage(), + new RNI18nPackage(), + new RNNotificationsPackage(MainApplication.this) + ); + } - @Override - public void onCreate() { - super.onCreate(); - SoLoader.init(this, /* native exopackage */ false); - } + @Override + public void onCreate() { + super.onCreate(); + Fabric.with(this, new Crashlytics()); + + // Create an object of the custom facade impl + notificationsLifecycleFacade = new NotificationsLifecycleFacade(); + // Attach it to react-native-navigation + setActivityCallbacks(notificationsLifecycleFacade); + } + + @Override + public IPushNotification getPushNotification(Context context, Bundle bundle, AppLifecycleFacade defaultFacade, AppLaunchHelper defaultAppLaunchHelper) { + return new CustomPushNotification( + context, + bundle, + notificationsLifecycleFacade, // Instead of defaultFacade!!! + defaultAppLaunchHelper, + new JsIOHelper() + ); + } } diff --git a/android/app/src/main/java/chat/rocket/reactnative/NotificationsLifecycleFacade.java b/android/app/src/main/java/chat/rocket/reactnative/NotificationsLifecycleFacade.java new file mode 100644 index 000000000..343710fce --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/NotificationsLifecycleFacade.java @@ -0,0 +1,91 @@ +package chat.rocket.reactnative; + +import android.app.Activity; +import android.util.Log; + +import com.facebook.react.bridge.ReactContext; +import com.reactnativenavigation.NavigationApplication; +import com.reactnativenavigation.controllers.ActivityCallbacks; +import com.reactnativenavigation.react.ReactGateway; +import com.wix.reactnativenotifications.core.AppLifecycleFacade; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +public class NotificationsLifecycleFacade extends ActivityCallbacks implements AppLifecycleFacade { + + private static final String TAG = NotificationsLifecycleFacade.class.getSimpleName(); + + private Activity mVisibleActivity; + private Set mListeners = new CopyOnWriteArraySet<>(); + + @Override + public void onActivityResumed(Activity activity) { + switchToVisible(activity); + } + + @Override + public void onActivityPaused(Activity activity) { + switchToInvisible(activity); + } + + @Override + public void onActivityStopped(Activity activity) { + switchToInvisible(activity); + } + + @Override + public void onActivityDestroyed(Activity activity) { + switchToInvisible(activity); + } + + @Override + public boolean isReactInitialized() { + return NavigationApplication.instance.isReactContextInitialized(); + } + + @Override + public ReactContext getRunningReactContext() { + final ReactGateway reactGateway = NavigationApplication.instance.getReactGateway(); + if (reactGateway == null || !reactGateway.isInitialized()) { + return null; + } + + return reactGateway.getReactContext(); + } + + @Override + public boolean isAppVisible() { + return mVisibleActivity != null; + } + + @Override + public synchronized void addVisibilityListener(AppVisibilityListener listener) { + mListeners.add(listener); + } + + @Override + public synchronized void removeVisibilityListener(AppVisibilityListener listener) { + mListeners.remove(listener); + } + + private synchronized void switchToVisible(Activity activity) { + if (mVisibleActivity == null) { + mVisibleActivity = activity; + Log.d(TAG, "Activity is now visible ("+activity+")"); + for (AppVisibilityListener listener : mListeners) { + listener.onAppVisible(); + } + } + } + + private synchronized void switchToInvisible(Activity activity) { + if (mVisibleActivity == activity) { + mVisibleActivity = null; + Log.d(TAG, "Activity is now NOT visible ("+activity+")"); + for (AppVisibilityListener listener : mListeners) { + listener.onAppNotVisible(); + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable-hdpi/node_modules_reactnavigation_src_views_assets_backicon.png b/android/app/src/main/res/drawable-hdpi/node_modules_reactnavigation_src_views_assets_backicon.png deleted file mode 100644 index ad03a63bf..000000000 Binary files a/android/app/src/main/res/drawable-hdpi/node_modules_reactnavigation_src_views_assets_backicon.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-hdpi/launch_screen.png b/android/app/src/main/res/drawable-land-hdpi/launch_screen.png new file mode 100644 index 000000000..3db2b1f4e Binary files /dev/null and b/android/app/src/main/res/drawable-land-hdpi/launch_screen.png differ diff --git a/android/app/src/main/res/drawable-land-ldpi/launch_screen.png b/android/app/src/main/res/drawable-land-ldpi/launch_screen.png new file mode 100644 index 000000000..732902f05 Binary files /dev/null and b/android/app/src/main/res/drawable-land-ldpi/launch_screen.png differ diff --git a/android/app/src/main/res/drawable-land-mdpi/launch_screen.png b/android/app/src/main/res/drawable-land-mdpi/launch_screen.png new file mode 100644 index 000000000..129338ce2 Binary files /dev/null and b/android/app/src/main/res/drawable-land-mdpi/launch_screen.png differ diff --git a/android/app/src/main/res/drawable-land-xhdpi/launch_screen.png b/android/app/src/main/res/drawable-land-xhdpi/launch_screen.png new file mode 100644 index 000000000..770d9d0bd Binary files /dev/null and b/android/app/src/main/res/drawable-land-xhdpi/launch_screen.png differ diff --git a/android/app/src/main/res/drawable-land-xxhdpi/launch_screen.png b/android/app/src/main/res/drawable-land-xxhdpi/launch_screen.png new file mode 100644 index 000000000..2fa8e2693 Binary files /dev/null and b/android/app/src/main/res/drawable-land-xxhdpi/launch_screen.png differ diff --git a/android/app/src/main/res/drawable-land-xxxhdpi/launch_screen.png b/android/app/src/main/res/drawable-land-xxxhdpi/launch_screen.png new file mode 100644 index 000000000..d7ed59bf8 Binary files /dev/null and b/android/app/src/main/res/drawable-land-xxxhdpi/launch_screen.png differ diff --git a/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigation_src_views_assets_backicon.png b/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigation_src_views_assets_backicon.png deleted file mode 100644 index 083db295f..000000000 Binary files a/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigation_src_views_assets_backicon.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-hdpi/launch_screen.png b/android/app/src/main/res/drawable-port-hdpi/launch_screen.png new file mode 100644 index 000000000..2f5ef9bd0 Binary files /dev/null and b/android/app/src/main/res/drawable-port-hdpi/launch_screen.png differ diff --git a/android/app/src/main/res/drawable-port-ldpi/launch_screen.png b/android/app/src/main/res/drawable-port-ldpi/launch_screen.png new file mode 100644 index 000000000..2606d272b Binary files /dev/null and b/android/app/src/main/res/drawable-port-ldpi/launch_screen.png differ diff --git a/android/app/src/main/res/drawable-port-mdpi/launch_screen.png b/android/app/src/main/res/drawable-port-mdpi/launch_screen.png new file mode 100644 index 000000000..d9e82e470 Binary files /dev/null and b/android/app/src/main/res/drawable-port-mdpi/launch_screen.png differ diff --git a/android/app/src/main/res/drawable-port-xhdpi/launch_screen.png b/android/app/src/main/res/drawable-port-xhdpi/launch_screen.png new file mode 100644 index 000000000..21f01604b Binary files /dev/null and b/android/app/src/main/res/drawable-port-xhdpi/launch_screen.png differ diff --git a/android/app/src/main/res/drawable-port-xxhdpi/launch_screen.png b/android/app/src/main/res/drawable-port-xxhdpi/launch_screen.png new file mode 100644 index 000000000..d31670117 Binary files /dev/null and b/android/app/src/main/res/drawable-port-xxhdpi/launch_screen.png differ diff --git a/android/app/src/main/res/drawable-port-xxxhdpi/launch_screen.png b/android/app/src/main/res/drawable-port-xxxhdpi/launch_screen.png new file mode 100644 index 000000000..9458c38b9 Binary files /dev/null and b/android/app/src/main/res/drawable-port-xxxhdpi/launch_screen.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/node_modules_reactnavigation_src_views_assets_backicon.png b/android/app/src/main/res/drawable-xhdpi/node_modules_reactnavigation_src_views_assets_backicon.png deleted file mode 100644 index 6de0a1cbb..000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/node_modules_reactnavigation_src_views_assets_backicon.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnavigation_src_views_assets_backicon.png b/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnavigation_src_views_assets_backicon.png deleted file mode 100644 index 15a983a67..000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnavigation_src_views_assets_backicon.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/node_modules_reactnavigation_src_views_assets_backicon.png b/android/app/src/main/res/drawable-xxxhdpi/node_modules_reactnavigation_src_views_assets_backicon.png deleted file mode 100644 index 17e52e855..000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/node_modules_reactnavigation_src_views_assets_backicon.png and /dev/null differ diff --git a/android/app/src/main/res/drawable/launch_screen_bitmap.xml b/android/app/src/main/res/drawable/launch_screen_bitmap.xml new file mode 100644 index 000000000..de474fb1f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_screen_bitmap.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/android/app/src/main/res/layout/launch_screen.xml b/android/app/src/main/res/layout/launch_screen.xml deleted file mode 100644 index 86d6a72e7..000000000 --- a/android/app/src/main/res/layout/launch_screen.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_notification.png b/android/app/src/main/res/mipmap-hdpi/ic_notification.png index 912112d2c..08b68046a 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_notification.png and b/android/app/src/main/res/mipmap-hdpi/ic_notification.png differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_notification.png b/android/app/src/main/res/mipmap-ldpi/ic_notification.png new file mode 100644 index 000000000..6dfec747b Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/ic_notification.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_notification.png b/android/app/src/main/res/mipmap-mdpi/ic_notification.png new file mode 100644 index 000000000..6dfec747b Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_notification.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_notification.png b/android/app/src/main/res/mipmap-xhdpi/ic_notification.png new file mode 100644 index 000000000..31ebe6059 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_notification.png b/android/app/src/main/res/mipmap-xxhdpi/ic_notification.png new file mode 100644 index 000000000..556047f2e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png new file mode 100644 index 000000000..a252c5dfd Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 56779d1e1..effdcbed1 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1 +1,6 @@ - #660B0B0B \ No newline at end of file + + + #660B0B0B + #FFFFFF + #CC3333 + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 50a29364b..d6cd4cf1b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,8 +2,10 @@ buildscript { repositories { - jcenter() google() + mavenLocal() + mavenCentral() + jcenter() } dependencies { // classpath 'com.android.tools.build:gradle:2.2.3' @@ -17,6 +19,7 @@ buildscript { allprojects { repositories { mavenLocal() + mavenCentral() jcenter() google() maven { diff --git a/android/settings.gradle b/android/settings.gradle index ffb155c5c..0cc5136b6 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -9,8 +9,6 @@ include ':react-native-audio' project(':react-native-audio').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-audio/android') include ':reactnativekeyboardinput' project(':reactnativekeyboardinput').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keyboard-input/lib/android') -include ':react-native-splash-screen' -project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android') include ':react-native-video' project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android') include ':react-native-svg' @@ -25,8 +23,10 @@ include ':react-native-zeroconf' project(':react-native-zeroconf').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-zeroconf/android') include ':realm' project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android') -include ':react-native-push-notification' -project(':react-native-push-notification').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-push-notification/android') include ':react-native-toast' project(':react-native-toast').projectDir = new File(settingsDir, '../node_modules/@remobile/react-native-toast/android') +include ':react-native-navigation' +project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/') +include ':reactnativenotifications' +project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android') include ':app' diff --git a/app/Icons.js b/app/Icons.js new file mode 100644 index 000000000..d9f947972 --- /dev/null +++ b/app/Icons.js @@ -0,0 +1,28 @@ +import { Platform } from 'react-native'; +import Ionicons from 'react-native-vector-icons/Ionicons'; + +const isIOS = Platform.OS === 'ios'; +const prefix = isIOS ? 'ios' : 'md'; + +// icon name from provider: [ size of the uri, icon provider, name to be used later ] +const icons = { + [`${ prefix }-search`]: [30, Ionicons, 'search'], + [`${ prefix }-menu`]: [30, Ionicons, 'menu'], + [`${ prefix }-star`]: [30, Ionicons, 'star'], + [`${ prefix }-star-outline`]: [30, Ionicons, 'starOutline'], + [isIOS ? 'ios-create-outline' : 'md-create']: [30, Ionicons, 'create'], + [`${ prefix }-more`]: [30, Ionicons, 'more'], + [`${ prefix }-add`]: [30, Ionicons, 'add'] +}; + +const iconsMap = {}; +const iconsLoaded = async() => { + const promises = Object.keys(icons).map((icon) => { + const Provider = icons[icon][1]; + return Provider.getImageSource(icon, icons[icon][0]); + }); + const sources = await Promise.all(promises); + Object.keys(icons).forEach((icon, i) => (iconsMap[icons[icon][2]] = sources[i])); +}; + +export { iconsLoaded, iconsMap }; diff --git a/app/Navigation.js b/app/Navigation.js new file mode 100644 index 000000000..f192d2a80 --- /dev/null +++ b/app/Navigation.js @@ -0,0 +1,13 @@ +class NavigationActionsClass { + setNavigator(navigator) { + this.navigator = navigator; + } + + push = params => this.navigator && this.navigator.push(params); + pop = params => this.navigator && this.navigator.pop(params); + popToRoot = params => this.navigator && this.navigator.popToRoot(params); + resetTo = params => this.navigator && this.navigator.resetTo(params); + toggleDrawer = params => this.navigator && this.navigator.toggleDrawer(params); +} + +export const NavigationActions = new NavigationActionsClass(); diff --git a/app/ReactotronConfig.js b/app/ReactotronConfig.js index 3630fe5d7..4f2db6146 100644 --- a/app/ReactotronConfig.js +++ b/app/ReactotronConfig.js @@ -1,11 +1,14 @@ /* eslint-disable */ +import { NativeModules } from 'react-native'; import Reactotron from 'reactotron-react-native'; import { reactotronRedux } from 'reactotron-redux'; import sagaPlugin from 'reactotron-redux-saga' if (__DEV__) { + const scriptURL = NativeModules.SourceCode.scriptURL; + const scriptHostname = scriptURL.split('://')[1].split(':')[0]; Reactotron - .configure() + .configure({ host: scriptHostname }) .useReactNative() .use(reactotronRedux()) .use(sagaPlugin()) diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 2b2429a38..56fb4f21b 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -44,7 +44,7 @@ export const ROOM = createRequestTypes('ROOM', [ 'MESSAGE_RECEIVED', 'SET_LAST_OPEN' ]); -export const APP = createRequestTypes('APP', ['READY', 'INIT']); +export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT']); export const MESSAGES = createRequestTypes('MESSAGES', [ ...defaultTypes, 'ACTIONS_SHOW', diff --git a/app/actions/index.js b/app/actions/index.js index 8d117c711..634d049cb 100644 --- a/app/actions/index.js +++ b/app/actions/index.js @@ -1,6 +1,13 @@ import * as types from '../constants/types'; import { APP } from './actionsTypes'; +export function appStart(root) { + return { + type: APP.START, + root + }; +} + export function appReady() { return { type: APP.READY @@ -12,6 +19,7 @@ export function appInit() { type: APP.INIT }; } + export function setCurrentServer(server) { return { type: types.SET_CURRENT_SERVER, diff --git a/app/actions/server.js b/app/actions/server.js index cfc46ad89..887f60a92 100644 --- a/app/actions/server.js +++ b/app/actions/server.js @@ -1,6 +1,6 @@ import { SERVER } from './actionsTypes'; -export function setServer(server) { +export function selectServer(server) { return { type: SERVER.SELECT, server diff --git a/app/app.js b/app/app.js new file mode 100644 index 000000000..e69de29bb diff --git a/app/containers/CloseModalButton.js b/app/containers/CloseModalButton.js deleted file mode 100644 index 2a2958ab1..000000000 --- a/app/containers/CloseModalButton.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { TouchableOpacity, StyleSheet } from 'react-native'; -import Icon from 'react-native-vector-icons/MaterialIcons'; -import { NavigationActions } from 'react-navigation'; -import { COLOR_TEXT } from '../constants/colors'; - -const styles = StyleSheet.create({ - button: { - width: 25, - height: 25, - marginTop: 5 - }, - icon: { - color: COLOR_TEXT, - left: -5 - } -}); - -export default class CloseModalButton extends React.PureComponent { - static propTypes = { - navigation: PropTypes.object.isRequired - } - - render() { - return ( - this.props.navigation.dispatch(NavigationActions.back())} - style={styles.button} - testID='close-modal-button' - > - - - ); - } -} diff --git a/app/containers/Header.js b/app/containers/Header.js deleted file mode 100644 index 31065e706..000000000 --- a/app/containers/Header.js +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import { View, StyleSheet, Platform } from 'react-native'; -import PropTypes from 'prop-types'; -import { SafeAreaView } from 'react-navigation'; - -let platformContainerStyles; -if (Platform.OS === 'ios') { - platformContainerStyles = { - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: 'rgba(0, 0, 0, .3)' - }; -} else { - platformContainerStyles = { - shadowColor: 'black', - shadowOpacity: 0.1, - shadowRadius: StyleSheet.hairlineWidth, - shadowOffset: { - height: StyleSheet.hairlineWidth - }, - elevation: 4 - }; -} - -const height = Platform.OS === 'ios' ? 44 : 56; -const backgroundColor = Platform.OS === 'ios' ? '#F7F7F7' : '#FFF'; -const styles = StyleSheet.create({ - container: { - backgroundColor, - ...platformContainerStyles - }, - appBar: { - height, - backgroundColor - } -}); - -export default class Header extends React.PureComponent { - static propTypes = { - subview: PropTypes.object.isRequired - } - - render() { - return ( - - - {this.props.subview} - - - ); - } -} diff --git a/app/containers/MessageActions.js b/app/containers/MessageActions.js index 7dc91f028..ccb25a79d 100644 --- a/app/containers/MessageActions.js +++ b/app/containers/MessageActions.js @@ -21,7 +21,6 @@ import I18n from '../i18n'; @connect( state => ({ actionMessage: state.messages.actionMessage, - user: state.login.user, Message_AllowDeleting: state.settings.Message_AllowDeleting, Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes, Message_AllowEditing: state.settings.Message_AllowEditing, @@ -42,9 +41,9 @@ import I18n from '../i18n'; export default class MessageActions extends React.Component { static propTypes = { actionsHide: PropTypes.func.isRequired, - room: PropTypes.object, + room: PropTypes.object.isRequired, actionMessage: PropTypes.object, - user: PropTypes.object, + user: PropTypes.object.isRequired, deleteRequest: PropTypes.func.isRequired, editInit: PropTypes.func.isRequired, toggleStarRequest: PropTypes.func.isRequired, diff --git a/app/containers/Routes.js b/app/containers/Routes.js deleted file mode 100644 index cfb85be1c..000000000 --- a/app/containers/Routes.js +++ /dev/null @@ -1,88 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { Linking } from 'react-native'; -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import SplashScreen from 'react-native-splash-screen'; - -import { appInit } from '../actions'; -import { deepLinkingOpen } from '../actions/deepLinking'; -import AuthRoutes from './routes/AuthRoutes'; -import PublicRoutes from './routes/PublicRoutes'; -import * as NavigationService from './routes/NavigationService'; -import parseQuery from '../lib/methods/helpers/parseQuery'; - -@connect( - state => ({ - login: state.login, - app: state.app, - background: state.app.background - }), - dispatch => bindActionCreators({ - appInit, deepLinkingOpen - }, dispatch) -) -export default class Routes extends React.Component { - static propTypes = { - login: PropTypes.object.isRequired, - app: PropTypes.object.isRequired, - appInit: PropTypes.func.isRequired - } - - constructor(props) { - super(props); - this.handleOpenURL = this.handleOpenURL.bind(this); - } - - componentDidMount() { - if (this.props.app.ready) { - return SplashScreen.hide(); - } - this.props.appInit(); - - Linking - .getInitialURL() - .then(url => this.handleOpenURL({ url })) - .catch(console.error); - Linking.addEventListener('url', this.handleOpenURL); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.app.ready && this.props.app.ready !== nextProps.app.ready) { - SplashScreen.hide(); - } - } - - componentDidUpdate() { - NavigationService.setNavigator(this.navigator); - } - - componentWillUnmount() { - Linking.removeEventListener('url', this.handleOpenURL); - } - - handleOpenURL({ url }) { - if (url) { - url = url.replace(/rocketchat:\/\/|https:\/\/go.rocket.chat\//, ''); - const regex = /^(room|auth)\?/; - if (url.match(regex)) { - url = url.replace(regex, ''); - const params = parseQuery(url); - this.props.deepLinkingOpen(params); - } - } - } - - render() { - const { login } = this.props; - - if (this.props.app.starting) { - return null; - } - - if (!login.token || login.isRegistering) { - return ( this.navigator = nav} />); - } - return ( this.navigator = nav} />); - } -} diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js index ea6cc2f9d..d8e4895ab 100644 --- a/app/containers/Sidebar.js +++ b/app/containers/Sidebar.js @@ -1,14 +1,14 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { ScrollView, Text, View, StyleSheet, FlatList, LayoutAnimation } from 'react-native'; +import { ScrollView, Text, View, StyleSheet, FlatList, LayoutAnimation, AsyncStorage, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; -import { DrawerActions, SafeAreaView } from 'react-navigation'; import FastImage from 'react-native-fast-image'; import Icon from 'react-native-vector-icons/MaterialIcons'; import database from '../lib/realm'; -import { setServer } from '../actions/server'; +import { selectServer } from '../actions/server'; import { logout } from '../actions/login'; +import { appStart } from '../actions'; import Avatar from '../containers/Avatar'; import Status from '../containers/status'; import Touch from '../utils/touch'; @@ -16,6 +16,7 @@ import { STATUS_COLORS } from '../constants/colors'; import RocketChat from '../lib/rocketchat'; import log from '../utils/log'; import I18n from '../i18n'; +import { NavigationActions } from '../Navigation'; const styles = StyleSheet.create({ selected: { @@ -76,19 +77,29 @@ const styles = StyleSheet.create({ } }); const keyExtractor = item => item.id; + @connect(state => ({ server: state.server.server, - user: state.login.user + user: { + id: state.login.user && state.login.user.id, + language: state.login.user && state.login.user.language, + server: state.login.user && state.login.user.server, + status: state.login.user && state.login.user.status, + username: state.login.user && state.login.user.username + } }), dispatch => ({ - selectServer: server => dispatch(setServer(server)), - logout: () => dispatch(logout()) + selectServer: server => dispatch(selectServer(server)), + logout: () => dispatch(logout()), + appStart: () => dispatch(appStart('outside')) })) export default class Sidebar extends Component { static propTypes = { + navigator: PropTypes.object, server: PropTypes.string.isRequired, selectServer: PropTypes.func.isRequired, - navigation: PropTypes.object.isRequired, - logout: PropTypes.func.isRequired + user: PropTypes.object, + logout: PropTypes.func.isRequired, + appStart: PropTypes.func } constructor(props) { @@ -117,7 +128,6 @@ export default class Sidebar extends Component { onPressItem = (item) => { this.props.selectServer(item.id); - this.closeDrawer(); } setStatus = () => { @@ -149,7 +159,11 @@ export default class Sidebar extends Component { } closeDrawer = () => { - this.props.navigation.dispatch(DrawerActions.closeDrawer()); + this.props.navigator.toggleDrawer({ + side: 'left', + animated: true, + to: 'close' + }); } toggleServers = () => { @@ -157,25 +171,15 @@ export default class Sidebar extends Component { this.setState({ showServers: !this.state.showServers }); } - isRouteFocused = (route) => { - const { state } = this.props.navigation; - const activeItemKey = state.routes[state.index] ? state.routes[state.index].key : null; - return activeItemKey === route; - } - - sidebarNavigate = (route) => { - const { navigate } = this.props.navigation; - if (!this.isRouteFocused(route)) { - navigate(route); - } else { - this.closeDrawer(); - } + sidebarNavigate = (screen, title) => { + this.closeDrawer(); + NavigationActions.resetTo({ screen, title }); } renderSeparator = key => ; renderItem = ({ - text, left, selected, onPress, testID + text, left, onPress, testID }) => ( - - + + {left} @@ -222,12 +226,15 @@ export default class Sidebar extends Component { source={{ uri: encodeURI(`${ item.id }/assets/favicon_32.png`) }} />, selected: this.props.server === item.id, - onPress: () => { + onPress: async() => { this.closeDrawer(); this.toggleServers(); if (this.props.server !== item.id) { this.props.selectServer(item.id); - this.props.navigation.navigate('RoomsList'); + const token = await AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ item.id }`); + if (!token) { + this.props.appStart(); + } } }, testID: `sidebar-${ item.id }` @@ -239,31 +246,25 @@ export default class Sidebar extends Component { this.renderItem({ text: I18n.t('Chats'), left: , - onPress: () => this.sidebarNavigate('Chats'), - selected: this.isRouteFocused('Chats'), + onPress: () => this.sidebarNavigate('RoomsListView', I18n.t('Messages')), testID: 'sidebar-chats' }), this.renderItem({ text: I18n.t('Profile'), left: , - onPress: () => this.sidebarNavigate('ProfileView'), - selected: this.isRouteFocused('ProfileView'), + onPress: () => this.sidebarNavigate('ProfileView', I18n.t('Profile')), testID: 'sidebar-profile' }), this.renderItem({ text: I18n.t('Settings'), left: , - onPress: () => this.sidebarNavigate('SettingsView'), - selected: this.isRouteFocused('SettingsView'), + onPress: () => this.sidebarNavigate('SettingsView', I18n.t('Settings')), testID: 'sidebar-settings' }), this.renderSeparator('separator-logout'), this.renderItem({ text: I18n.t('Logout'), - left: , + left: , onPress: () => this.props.logout(), testID: 'sidebar-logout' }) @@ -297,7 +298,10 @@ export default class Sidebar extends Component { onPress: () => { this.closeDrawer(); this.toggleServers(); - this.props.navigation.navigate('AddServer'); + NavigationActions.push({ + screen: 'NewServerView', + title: I18n.t('Add_Server') + }); }, testID: 'sidebar-add-server' }) @@ -306,13 +310,12 @@ export default class Sidebar extends Component { render() { const { user, server } = this.props; + if (!user) { + return null; + } return ( - - + + this.toggleServers()} underlayColor='rgba(255, 255, 255, 0.5)' @@ -335,6 +338,7 @@ export default class Sidebar extends Component { diff --git a/app/containers/TextInput.js b/app/containers/TextInput.js index 7e85e553c..fb8504ad8 100644 --- a/app/containers/TextInput.js +++ b/app/containers/TextInput.js @@ -66,7 +66,10 @@ export default class RCTextInput extends React.PureComponent { secureTextEntry: PropTypes.bool, containerStyle: ViewPropTypes.style, inputStyle: PropTypes.object, - inputRef: PropTypes.func + inputRef: PropTypes.func, + testID: PropTypes.string, + iconLeft: PropTypes.string, + placeholder: PropTypes.string } static defaultProps = { error: {} diff --git a/app/containers/Typing.js b/app/containers/Typing.js index 162d462f0..3d3cf933f 100644 --- a/app/containers/Typing.js +++ b/app/containers/Typing.js @@ -32,7 +32,7 @@ export default class Typing extends React.Component { } get usersTyping() { const users = this.props.usersTyping.filter(_username => this.props.username !== _username); - return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }` : ''; + return users.length ? `${ users.join(', ') } ${ users.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }` : ''; } render() { const { usersTyping } = this; diff --git a/app/containers/message/Image.js b/app/containers/message/Image.js index 7724edbef..1cbdf3a1f 100644 --- a/app/containers/message/Image.js +++ b/app/containers/message/Image.js @@ -28,7 +28,8 @@ export default class extends React.PureComponent { static propTypes = { file: PropTypes.object.isRequired, baseUrl: PropTypes.string.isRequired, - user: PropTypes.object.isRequired + user: PropTypes.object.isRequired, + customEmojis: PropTypes.object } state = { modalVisible: false }; diff --git a/app/containers/message/index.js b/app/containers/message/index.js index 46152b204..5e20b9337 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -99,7 +99,11 @@ export default class Message extends React.Component { Message_GroupingPeriod: PropTypes.number.isRequired, customTimeFormat: PropTypes.string, message: PropTypes.object.isRequired, - user: PropTypes.object.isRequired, + user: PropTypes.shape({ + id: PropTypes.string.isRequired, + username: PropTypes.string.isRequired, + token: PropTypes.string.isRequired + }), editing: PropTypes.bool, errorActionsShow: PropTypes.func, toggleReactionPicker: PropTypes.func, @@ -109,7 +113,8 @@ export default class Message extends React.Component { onLongPress: PropTypes.func, _updatedAt: PropTypes.instanceOf(Date), archived: PropTypes.bool, - broadcast: PropTypes.bool + broadcast: PropTypes.bool, + previousItem: PropTypes.object } static defaultProps = { diff --git a/app/containers/routes/AuthRoutes.js b/app/containers/routes/AuthRoutes.js deleted file mode 100644 index 5361dd2e3..000000000 --- a/app/containers/routes/AuthRoutes.js +++ /dev/null @@ -1,190 +0,0 @@ -import React from 'react'; -import { Platform, TouchableOpacity } from 'react-native'; -import { createStackNavigator, createDrawerNavigator } from 'react-navigation'; -import Icon from 'react-native-vector-icons/MaterialIcons'; - -import Sidebar from '../../containers/Sidebar'; -import RoomsListView from '../../views/RoomsListView'; -import RoomView from '../../views/RoomView'; -import RoomActionsView from '../../views/RoomActionsView'; -import CreateChannelView from '../../views/CreateChannelView'; -import SelectedUsersView from '../../views/SelectedUsersView'; -import NewServerView from '../../views/NewServerView'; -import StarredMessagesView from '../../views/StarredMessagesView'; -import PinnedMessagesView from '../../views/PinnedMessagesView'; -import MentionedMessagesView from '../../views/MentionedMessagesView'; -import SnippetedMessagesView from '../../views/SnippetedMessagesView'; -import SearchMessagesView from '../../views/SearchMessagesView'; -import RoomFilesView from '../../views/RoomFilesView'; -import RoomMembersView from '../../views/RoomMembersView'; -import RoomInfoView from '../../views/RoomInfoView'; -import RoomInfoEditView from '../../views/RoomInfoEditView'; -import ProfileView from '../../views/ProfileView'; -import SettingsView from '../../views/SettingsView'; -import I18n from '../../i18n'; -import sharedStyles from '../../views/Styles'; - -const headerTintColor = '#292E35'; - -const AuthRoutes = createStackNavigator( - { - RoomsList: { - screen: RoomsListView - }, - Room: { - screen: RoomView - }, - CreateChannel: { - screen: CreateChannelView, - navigationOptions: { - title: I18n.t('Create_Channel'), - headerTintColor - } - }, - SelectedUsers: { - screen: SelectedUsersView, - navigationOptions: { - title: I18n.t('Select_Users'), - headerTintColor - } - }, - AddServer: { - screen: NewServerView, - navigationOptions: { - title: I18n.t('New_Server'), - headerTintColor - } - }, - RoomActions: { - screen: RoomActionsView, - navigationOptions: { - title: I18n.t('Actions'), - headerTintColor - } - }, - StarredMessages: { - screen: StarredMessagesView, - navigationOptions: { - title: I18n.t('Starred_Messages'), - headerTintColor - } - }, - PinnedMessages: { - screen: PinnedMessagesView, - navigationOptions: { - title: I18n.t('Pinned_Messages'), - headerTintColor - } - }, - MentionedMessages: { - screen: MentionedMessagesView, - navigationOptions: { - title: I18n.t('Mentioned_Messages'), - headerTintColor - } - }, - SnippetedMessages: { - screen: SnippetedMessagesView, - navigationOptions: { - title: I18n.t('Snippet_Messages'), - headerTintColor - } - }, - SearchMessages: { - screen: SearchMessagesView, - navigationOptions: { - title: I18n.t('Search_Messages'), - headerTintColor - } - }, - RoomFiles: { - screen: RoomFilesView, - navigationOptions: { - title: I18n.t('Room_Files'), - headerTintColor - } - }, - RoomMembers: { - screen: RoomMembersView, - navigationOptions: { - title: I18n.t('Room_Members'), - headerTintColor - } - }, - RoomInfo: { - screen: RoomInfoView, - navigationOptions: { - title: I18n.t('Room_Info'), - headerTintColor - } - }, - RoomInfoEdit: { - screen: RoomInfoEditView, - navigationOptions: { - title: I18n.t('Room_Info_Edit'), - headerTintColor - } - } - }, - { - navigationOptions: { - headerTitleAllowFontScaling: false - } - } -); - -const MenuButton = ({ navigation, testID }) => ( - - - -); - -const Routes = createDrawerNavigator( - { - Chats: { - screen: AuthRoutes, - navigationOptions: { - drawerLabel: I18n.t('Chats'), - drawerIcon: () => - } - }, - ProfileView: { - screen: createStackNavigator({ - ProfileView: { - screen: ProfileView, - navigationOptions: ({ navigation }) => ({ - title: I18n.t('Profile'), - headerTintColor: '#292E35', - headerLeft: - }) - } - }) - }, - SettingsView: { - screen: createStackNavigator({ - SettingsView: { - screen: SettingsView, - navigationOptions: ({ navigation }) => ({ - title: I18n.t('Settings'), - headerTintColor: '#292E35', - headerLeft: - }) - } - }) - } - }, - { - contentComponent: Sidebar, - drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked', - initialRouteName: 'Chats', - backBehavior: 'initialRoute' - } -); - -export default Routes; diff --git a/app/containers/routes/NavigationService.js b/app/containers/routes/NavigationService.js deleted file mode 100644 index a57d72150..000000000 --- a/app/containers/routes/NavigationService.js +++ /dev/null @@ -1,58 +0,0 @@ -import { NavigationActions, StackActions } from 'react-navigation'; - -const config = {}; - -export function setNavigator(nav) { - if (nav) { - config.navigator = nav; - } -} - -export function navigate(routeName, params) { - if (config.navigator && routeName) { - const action = NavigationActions.navigate({ key: routeName, routeName, params }); - config.navigator.dispatch(action); - } -} - -export function goBack() { - if (config.navigator) { - const action = NavigationActions.back({}); - config.navigator.dispatch(action); - } -} - -export function goRoomsList() { - if (config.navigator) { - const action = StackActions.reset({ - index: 0, - actions: [NavigationActions.navigate({ key: 'RoomsList', routeName: 'RoomsList' })] - }); - config.navigator.dispatch(action); - } -} - -export function goRoom({ rid, name }, counter = 0) { - // about counter: we can call this method before navigator be set. so we have to wait, if we tried a lot, we give up ... - if (!rid || counter > 10) { - return; - } - if (!config.navigator) { - return setTimeout(() => goRoom({ rid, name }, counter + 1), 100); - } - - const action = StackActions.reset({ - index: 1, - actions: [ - NavigationActions.navigate({ key: 'RoomsList', routeName: 'RoomsList' }), - NavigationActions.navigate({ key: `Room-${ rid }`, routeName: 'Room', params: { room: { rid, name }, rid, name } }) - ] - }); - config.navigator.dispatch(action); -} - -export function dispatch(action) { - if (config.navigator) { - config.navigator.dispatch(action); - } -} diff --git a/app/containers/routes/PublicRoutes.js b/app/containers/routes/PublicRoutes.js deleted file mode 100644 index bff89edfc..000000000 --- a/app/containers/routes/PublicRoutes.js +++ /dev/null @@ -1,120 +0,0 @@ -import React from 'react'; -import { TouchableOpacity } from 'react-native'; -import { createStackNavigator } from 'react-navigation'; -import Icon from 'react-native-vector-icons/FontAwesome'; - -import ListServerView from '../../views/ListServerView'; -import NewServerView from '../../views/NewServerView'; -import LoginSignupView from '../../views/LoginSignupView'; -import LoginView from '../../views/LoginView'; -import RegisterView from '../../views/RegisterView'; - -import TermsServiceView from '../../views/TermsServiceView'; -import PrivacyPolicyView from '../../views/PrivacyPolicyView'; -import ForgotPasswordView from '../../views/ForgotPasswordView'; -import database from '../../lib/realm'; -import I18n from '../../i18n'; - -const hasServers = () => { - const db = database.databases.serversDB.objects('servers'); - return db.length > 0; -}; - -const ServerStack = createStackNavigator({ - ListServer: { - screen: ListServerView, - navigationOptions({ navigation }) { - return { - title: I18n.t('Servers'), - headerRight: ( - navigation.navigate({ key: 'AddServer', routeName: 'AddServer' })} - style={{ width: 50, alignItems: 'center' }} - accessibilityLabel={I18n.t('Add_Server')} - accessibilityTraits='button' - > - - - ) - }; - } - }, - AddServer: { - screen: NewServerView, - navigationOptions: { - header: null - } - }, - LoginSignup: { - screen: LoginSignupView, - navigationOptions: { - header: null - } - } -}, { - headerMode: 'screen', - initialRouteName: hasServers() ? 'ListServer' : 'AddServer' -}); - -const LoginStack = createStackNavigator({ - Login: { - screen: LoginView, - navigationOptions: { - header: null - } - }, - ForgotPassword: { - screen: ForgotPasswordView, - navigationOptions: { - title: I18n.t('Forgot_my_password'), - headerTintColor: '#292E35' - } - } -}, { - headerMode: 'screen' -}); - -const RegisterStack = createStackNavigator({ - Register: { - screen: RegisterView, - navigationOptions: { - header: null - } - }, - TermsService: { - screen: TermsServiceView, - navigationOptions: { - title: I18n.t('Terms_of_Service'), - headerTintColor: '#292E35' - } - }, - PrivacyPolicy: { - screen: PrivacyPolicyView, - navigationOptions: { - title: I18n.t('Privacy_Policy'), - headerTintColor: '#292E35' - } - } -}, { - headerMode: 'screen' -}); - -const PublicRoutes = createStackNavigator( - { - Server: { - screen: ServerStack - }, - Login: { - screen: LoginStack - }, - Register: { - screen: RegisterStack - } - }, - { - mode: 'modal', - headerMode: 'none' - } -); - -export default PublicRoutes; diff --git a/app/containers/status.js b/app/containers/status.js index f318b91b5..26bafabb0 100644 --- a/app/containers/status.js +++ b/app/containers/status.js @@ -12,41 +12,33 @@ const styles = StyleSheet.create({ } }); -@connect(state => ({ - activeUsers: state.activeUsers, - user: state.login.user, - offline: !state.meteor.connected -})) +@connect((state, ownProps) => { + if (state.login.user && ownProps.id === state.login.user.id) { + return { + status: state.login.user && state.login.user.status, + offline: !state.meteor.connected + }; + } -export default class Status extends React.Component { + const user = state.activeUsers[ownProps.id]; + return { + status: (user && user.status) || 'offline' + }; +}) + +export default class Status extends React.PureComponent { static propTypes = { style: ViewPropTypes.style, - id: PropTypes.string, - activeUsers: PropTypes.object, - user: PropTypes.object, + status: PropTypes.string, offline: PropTypes.bool }; - shouldComponentUpdate(nextProps) { - const { id: userId, user } = this.props; - if (user.id === userId) { - if (nextProps.offline !== this.props.offline) { - return true; - } - return (nextProps.user && nextProps.user.status !== user.status); - } - return (nextProps.activeUsers[userId] && nextProps.activeUsers[userId].status) !== this.status; - } - get status() { - const { id: userId, user, offline } = this.props; - if (user.id === userId) { - if (offline) { - return 'offline'; - } - return user.status || 'offline'; + const { offline, status } = this.props; + if (offline) { + return 'offline'; } - return (this.props.activeUsers && this.props.activeUsers[userId] && this.props.activeUsers[userId].status) || 'offline'; + return status; } render() { diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index a54dcdceb..79702e574 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -109,6 +109,7 @@ export default { changing_avatar: 'changing avatar', Channel_Name: 'Channel Name', Chats: 'Chats', + Close: 'Close', Close_emoji_selector: 'Close emoji selector', Code: 'Code', Colaborative: 'Colaborative', @@ -141,6 +142,7 @@ export default { Forgot_my_password: 'Forgot my password', Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.', Forgot_password: 'Forgot password', + Forgot_Password: 'Forgot Password', Has_joined_the_channel: 'Has joined the channel', Has_left_the_channel: 'Has left the channel', I_have_an_account: 'I have an account', @@ -164,6 +166,7 @@ export default { Message_actions: 'Message actions', Message_pinned: 'Message pinned', Message_removed: 'Message removed', + Messages: 'Messages', Microphone_Permission_Message: 'Rocket Chat needs access to your microphone so you can send audio message.', Microphone_Permission: 'Microphone Permission', Mute: 'Mute', @@ -294,6 +297,7 @@ export default { Validating: 'Validating', Video_call: 'Video call', Voice_call: 'Voice call', + Welcome: 'Welcome', Welcome_title_pt_1: 'Prepare to take off with', Welcome_title_pt_2: 'the ultimate chat platform', Yes_action_it: 'Yes, {{action}} it!', diff --git a/app/index.js b/app/index.js index cf9d88777..d0ceb4ef9 100644 --- a/app/index.js +++ b/app/index.js @@ -1,14 +1,94 @@ -import React from 'react'; -import { Provider } from 'react-redux'; +import { Component } from 'react'; +import { Linking } from 'react-native'; +import { Navigation } from 'react-native-navigation'; import store from './lib/createStore'; +import { appInit } from './actions'; +import database from './lib/realm'; +import { iconsLoaded } from './Icons'; +import { registerScreens } from './views'; +import { deepLinkingOpen } from './actions/deepLinking'; +import parseQuery from './lib/methods/helpers/parseQuery'; +import I18n from './i18n'; +import { initializePushNotifications } from './push'; -import Routes from './containers/Routes'; +const startLogged = () => { + Navigation.startSingleScreenApp({ + screen: { + screen: 'RoomsListView', + title: I18n.t('Messages') + }, + drawer: { + left: { + screen: 'Sidebar' + } + }, + animationType: 'fade' + }); +}; -const RocketChat = () => ( - - - -); +const startNotLogged = (route) => { + Navigation.startSingleScreenApp({ + screen: { + screen: route, + title: route === 'NewServerView' ? I18n.t('New_Server') : I18n.t('Servers') + }, + animationType: 'fade' + }); +}; -export default RocketChat; +const hasServers = () => { + const db = database.databases.serversDB.objects('servers'); + return db.length > 0; +}; + +const handleOpenURL = ({ url }) => { + if (url) { + url = url.replace(/rocketchat:\/\/|https:\/\/go.rocket.chat\//, ''); + const regex = /^(room|auth)\?/; + if (url.match(regex)) { + url = url.replace(regex, ''); + const params = parseQuery(url); + store.dispatch(deepLinkingOpen(params)); + } + } +}; + +registerScreens(store); +iconsLoaded(); + +export default class App extends Component { + constructor(props) { + super(props); + store.dispatch(appInit()); + store.subscribe(this.onStoreUpdate.bind(this)); + initializePushNotifications(); + + Linking + .getInitialURL() + .then(url => handleOpenURL({ url })) + .catch(e => console.warn(e)); + Linking.addEventListener('url', handleOpenURL); + } + + onStoreUpdate = () => { + const { root } = store.getState().app; + + if (this.currentRoot !== root) { + this.currentRoot = root; + if (root === 'outside') { + if (hasServers()) { + startNotLogged('ListServerView'); + } else { + startNotLogged('NewServerView'); + } + } else if (root === 'inside') { + startLogged(); + } + } + } + + setDeviceToken(deviceToken) { + this.deviceToken = deviceToken; + } +} diff --git a/app/lib/createStore.js b/app/lib/createStore.js index c9188f082..24106225d 100644 --- a/app/lib/createStore.js +++ b/app/lib/createStore.js @@ -14,7 +14,7 @@ if (__DEV__) { /* eslint-disable global-require */ const reduxImmutableStateInvariant = require('redux-immutable-state-invariant').default(); sagaMiddleware = createSagaMiddleware({ - sagaMonitor: Reactotron.createSagaMonitor() + // sagaMonitor: Reactotron.createSagaMonitor() }); enhancers = compose( diff --git a/app/lib/methods/canOpenRoom.js b/app/lib/methods/canOpenRoom.js index 30dc0809f..b642ac779 100644 --- a/app/lib/methods/canOpenRoom.js +++ b/app/lib/methods/canOpenRoom.js @@ -54,5 +54,6 @@ export default async function canOpenRoom({ rid, path }) { return data; } catch (e) { log('canOpenRoom', e); + return false; } } diff --git a/app/lib/realm.js b/app/lib/realm.js index 260b160aa..081643e57 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -139,7 +139,7 @@ const attachment = { video_url: { type: 'string', optional: true }, title: { type: 'string', optional: true }, title_link: { type: 'string', optional: true }, - title_link_download: { type: 'bool', optional: true }, + // title_link_download: { type: 'bool', optional: true }, type: { type: 'string', optional: true }, author_icon: { type: 'string', optional: true }, author_name: { type: 'string', optional: true }, diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 1e196474a..a20ef73f7 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -40,6 +40,8 @@ import loadMissedMessages from './methods/loadMissedMessages'; import sendMessage, { getMessage, _sendMessageCall } from './methods/sendMessage'; +import { getDeviceToken } from '../push'; + const TOKEN_KEY = 'reactnativemeteor_usertoken'; const call = (method, ...params) => RocketChat.ddp.call(method, ...params); // eslint-disable-line const returnAnArray = obj => obj || []; @@ -119,7 +121,7 @@ const RocketChat = { reduxStore.dispatch(setActiveUser(this.activeUsers)); this._setUserTimer = null; return this.activeUsers = {}; - }, 5000); + }, 2000); const activeUser = reduxStore.getState().activeUsers[ddpMessage.id]; if (!ddpMessage.fields) { @@ -175,8 +177,8 @@ const RocketChat = { this.ddp.on('disconnected', () => console.log('disconnected')); this.ddp.on('logged', protectedFunction((user) => { - this.getRooms().catch(e => log('logged getRooms', e)); this.loginSuccess(user); + this.getRooms().catch(e => log('logged getRooms', e)); })); this.ddp.once('logged', protectedFunction(({ id }) => { this.subscribeRooms(id); @@ -556,21 +558,24 @@ const RocketChat = { AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`); }, - registerPushToken(id, token) { - const key = Platform.OS === 'ios' ? 'apn' : 'gcm'; - const data = { - id: `RocketChatRN${ id }`, - token: { [key]: token }, - appName: 'chat.rocket.reactnative', // TODO: try to get from config file - userId: id, - metadata: {} - }; - return call('raix:push-update', data); + registerPushToken(userId) { + const deviceToken = getDeviceToken(); + if (deviceToken) { + const key = Platform.OS === 'ios' ? 'apn' : 'gcm'; + const data = { + id: `RocketChatRN${ userId }`, + token: { [key]: deviceToken }, + appName: 'chat.rocket.reactnative', // TODO: try to get from config file + userId, + metadata: {} + }; + return call('raix:push-update', data); + } }, - updatePushToken(pushId) { - return call('raix:push-setuser', pushId); - }, + // updatePushToken(pushId) { + // return call('raix:push-setuser', pushId); + // }, loadMissedMessages, loadMessagesForRoom, getMessage, diff --git a/app/presentation/RoomItem.js b/app/presentation/RoomItem.js index 243d4e3be..4797370b4 100644 --- a/app/presentation/RoomItem.js +++ b/app/presentation/RoomItem.js @@ -121,7 +121,7 @@ const renderNumber = (unread, userMentions) => { const attrs = ['name', 'unread', 'userMentions', 'alert', 'showLastMessage', 'type']; @connect(state => ({ - user: state.login && state.login.user, + username: state.login.user && state.login.user.username, StoreLastMessage: state.settings.Store_Last_Message })) export default class RoomItem extends React.Component { @@ -139,7 +139,7 @@ export default class RoomItem extends React.Component { id: PropTypes.string, onPress: PropTypes.func, onLongPress: PropTypes.func, - user: PropTypes.object, + username: PropTypes.string, avatarSize: PropTypes.number, statusStyle: ViewPropTypes.style, testID: PropTypes.string @@ -182,7 +182,7 @@ export default class RoomItem extends React.Component { let prefix = ''; - if (lastMessage.u.username === this.props.user.username) { + if (lastMessage.u.username === this.props.username) { prefix = I18n.t('You_colon'); } else if (type !== 'd') { prefix = `${ lastMessage.u.username }: `; diff --git a/app/push.js b/app/push.js_ similarity index 78% rename from app/push.js rename to app/push.js_ index 2a8de1ff6..2b898a22d 100644 --- a/app/push.js +++ b/app/push.js_ @@ -1,16 +1,19 @@ import PushNotification from 'react-native-push-notification'; import { AsyncStorage } from 'react-native'; import EJSON from 'ejson'; -import { goRoom } from './containers/routes/NavigationService'; + +import { NavigationActions } from './Navigation'; const handleNotification = (notification) => { - if (!notification.userInteraction) { - return; + if (notification.userInteraction) { + const { + rid, name, sender, type + } = EJSON.parse(notification.ejson || notification.data.ejson); + NavigationActions.push({ + screen: 'RoomView', + passProps: { rid, name: type === 'd' ? sender.username : name } + }); } - const { - rid, name, sender, type - } = EJSON.parse(notification.ejson || notification.data.ejson); - return rid && goRoom({ rid, name: type === 'd' ? sender.username : name }); }; PushNotification.configure({ diff --git a/app/push/index.js b/app/push/index.js new file mode 100644 index 000000000..10ab113a0 --- /dev/null +++ b/app/push/index.js @@ -0,0 +1,42 @@ +import EJSON from 'ejson'; + +import PushNotification from './push'; +import store from '../lib/createStore'; +import { deepLinkingOpen } from '../actions/deepLinking'; + +const onNotification = (notification) => { + if (notification) { + const data = notification.getData(); + if (data) { + try { + const { + rid, name, sender, type, host + } = EJSON.parse(data.ejson); + + const types = { + c: 'channel', d: 'direct', p: 'group' + }; + const roomName = type === 'd' ? sender.username : name; + + const params = { + host, + rid, + path: `${ types[type] }/${ roomName }` + }; + store.dispatch(deepLinkingOpen(params)); + } catch (e) { + console.warn(e); + } + } + } +}; + +const initializePushNotifications = () => { + PushNotification.configure({ + onNotification + }); +}; + +const getDeviceToken = () => PushNotification.getDeviceToken(); + +export { initializePushNotifications, getDeviceToken }; diff --git a/app/push/push.android.js b/app/push/push.android.js new file mode 100644 index 000000000..6e7ab0248 --- /dev/null +++ b/app/push/push.android.js @@ -0,0 +1,34 @@ +import { NotificationsAndroid, PendingNotifications } from 'react-native-notifications'; + +class PushNotification { + constructor() { + this.onRegister = null; + this.onNotification = null; + this.deviceToken = null; + + NotificationsAndroid.setRegistrationTokenUpdateListener((deviceToken) => { + this.deviceToken = deviceToken; + }); + + NotificationsAndroid.setNotificationOpenedListener((notification) => { + this.onNotification(notification); + }); + } + + getDeviceToken() { + return this.deviceToken; + } + + configure(params) { + this.onRegister = params.onRegister; + this.onNotification = params.onNotification; + + PendingNotifications.getInitialNotification() + .then((notification) => { + this.onNotification(notification); + }) + .catch(e => console.warn(e)); + } +} + +export default new PushNotification(); diff --git a/app/push/push.ios.js b/app/push/push.ios.js new file mode 100644 index 000000000..0636ea8c4 --- /dev/null +++ b/app/push/push.ios.js @@ -0,0 +1,31 @@ +import NotificationsIOS from 'react-native-notifications'; + +class PushNotification { + constructor() { + this.onRegister = null; + this.onNotification = null; + this.deviceToken = null; + + NotificationsIOS.addEventListener('remoteNotificationsRegistered', (deviceToken) => { + this.deviceToken = deviceToken; + }); + + NotificationsIOS.addEventListener('notificationOpened', (notification) => { + this.onNotification(notification); + }); + + NotificationsIOS.requestPermissions(); + } + + getDeviceToken() { + return this.deviceToken; + } + + configure(params) { + this.onRegister = params.onRegister; + this.onNotification = params.onNotification; + + NotificationsIOS.consumeBackgroundQueue(); + } +} +export default new PushNotification(); diff --git a/app/reducers/app.js b/app/reducers/app.js index 486e54a95..53d978e38 100644 --- a/app/reducers/app.js +++ b/app/reducers/app.js @@ -2,6 +2,7 @@ import { FOREGROUND, BACKGROUND, INACTIVE } from 'redux-enhancer-react-native-ap import { APP } from '../actions/actionsTypes'; const initialState = { + root: null, starting: true, ready: false, inactive: false, @@ -31,6 +32,11 @@ export default function app(state = initialState, action) { foreground: false, background: false }; + case APP.START: + return { + ...state, + root: action.root + }; case APP.INIT: return { ...state, diff --git a/app/reducers/login.js b/app/reducers/login.js index 2fbb295b3..7f735a0d1 100644 --- a/app/reducers/login.js +++ b/app/reducers/login.js @@ -19,6 +19,7 @@ export default function login(state = initialState, action) { ...state, isFetching: true, isAuthenticated: false, + isRegistering: false, failure: false, error: '' }; diff --git a/app/sagas/connect.js b/app/sagas/connect.js index e7495efd6..9312e7129 100644 --- a/app/sagas/connect.js +++ b/app/sagas/connect.js @@ -1,4 +1,4 @@ -import { call, takeLatest, select, put, all } from 'redux-saga/effects'; +import { call, takeLatest, select, put } from 'redux-saga/effects'; import { AsyncStorage } from 'react-native'; import { METEOR } from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; @@ -31,7 +31,8 @@ const test = function* test() { const server = yield select(getServer); const user = yield call(getToken); // const response = - yield all([call(connect, server, user && user.token ? { resume: user.token, ...user.user } : undefined)]);// , put(loginRequest({ resume: user.token }))]); + // yield all([call(connect, server, user && user.token ? { resume: user.token, ...user.user } : undefined)]);// , put(loginRequest({ resume: user.token }))]); + yield call(connect, server, user && user.token ? { resume: user.token, ...user.user } : undefined); // yield put(connectSuccess(response)); } catch (err) { console.warn('test', err); diff --git a/app/sagas/createChannel.js b/app/sagas/createChannel.js index b538c3fb9..f9b285616 100644 --- a/app/sagas/createChannel.js +++ b/app/sagas/createChannel.js @@ -1,9 +1,10 @@ import { delay } from 'redux-saga'; import { select, put, call, take, takeLatest } from 'redux-saga/effects'; +import { NavigationActions } from '../Navigation'; + import { CREATE_CHANNEL, LOGIN } from '../actions/actionsTypes'; import { createChannelSuccess, createChannelFailure } from '../actions/createChannel'; import RocketChat from '../lib/rocketchat'; -import { goRoom } from '../containers/routes/NavigationService'; const create = function* create(data) { return yield RocketChat.createChannel(data); @@ -18,7 +19,17 @@ const handleRequest = function* handleRequest({ data }) { } const result = yield call(create, data); const { rid, name } = result; - goRoom({ rid, name }); + NavigationActions.popToRoot(); + yield delay(1000); + NavigationActions.push({ + screen: 'RoomView', + title: name, + passProps: { + room: { rid, name }, + rid, + name + } + }); yield put(createChannelSuccess(result)); } catch (err) { yield put(createChannelFailure(err)); diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index 9cd2d57f5..e50fa23f5 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -1,24 +1,26 @@ import { AsyncStorage } from 'react-native'; -import { delay } from 'redux-saga'; -import { takeLatest, take, select, call, put } from 'redux-saga/effects'; +import { takeLatest, take, select, put } from 'redux-saga/effects'; + import * as types from '../actions/actionsTypes'; -import { setServer, addServer } from '../actions/server'; -import * as NavigationService from '../containers/routes/NavigationService'; +import { appStart } from '../actions'; +import { selectServer, addServer } from '../actions/server'; import database from '../lib/realm'; import RocketChat from '../lib/rocketchat'; +import { NavigationActions } from '../Navigation'; -const navigate = function* go({ server, params, sameServer = true }) { - const user = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`); - if (user) { - const { rid, path } = params; - if (rid) { - const canOpenRoom = yield RocketChat.canOpenRoom({ rid, path }); - if (canOpenRoom) { - return yield call(NavigationService.goRoom, { rid: params.rid }); - } - } - if (!sameServer) { - yield call(NavigationService.goRoomsList); +const navigate = function* go({ params, sameServer = true }) { + if (!sameServer) { + yield put(appStart('inside')); + } + if (params.rid) { + const canOpenRoom = yield RocketChat.canOpenRoom(params); + if (canOpenRoom) { + return NavigationActions.push({ + screen: 'RoomView', + passProps: { + rid: params.rid + } + }); } } }; @@ -35,7 +37,14 @@ const handleOpen = function* handleOpen({ params }) { return; } - const host = `https://${ params.host }`; + let { host } = params; + if (!/^(http|https)/.test(host)) { + host = `https://${ params.host }`; + } + // remove last "/" from host + if (host.slice(-1) === '/') { + host = host.slice(0, host.length - 1); + } try { yield RocketChat.testServer(host); @@ -43,18 +52,26 @@ const handleOpen = function* handleOpen({ params }) { return; } + const token = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ host }`); + // TODO: needs better test // if deep link is from same server if (server === host) { - yield navigate({ server, params }); + if (token) { + yield navigate({ params }); + } } else { // if deep link is from a different server // search if deep link's server already exists const servers = yield database.databases.serversDB.objects('servers').filtered('id = $0', host); // TODO: need better test if (servers.length) { - // if server exists, select it - yield put(setServer(servers[0].id)); - yield delay(2000); - yield navigate({ server: servers[0].id, params, sameServer: false }); + const deepLinkServer = servers[0].id; + if (!token) { + yield put(appStart('outside')); + } else { + yield put(selectServer(deepLinkServer)); + yield take(types.METEOR.REQUEST); + yield navigate({ params, sameServer: false }); + } } else { yield put(addServer(host)); } diff --git a/app/sagas/init.js b/app/sagas/init.js index d911923af..1f419d2f5 100644 --- a/app/sagas/init.js +++ b/app/sagas/init.js @@ -2,7 +2,7 @@ import { AsyncStorage } from 'react-native'; import { call, put, takeLatest } from 'redux-saga/effects'; import * as actions from '../actions'; -import { setServer } from '../actions/server'; +import { selectServer } from '../actions/server'; import { restoreToken, setUser } from '../actions/login'; import { APP } from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; @@ -13,11 +13,16 @@ const restore = function* restore() { const token = yield call([AsyncStorage, 'getItem'], RocketChat.TOKEN_KEY); if (token) { yield put(restoreToken(token)); + } else { + yield put(actions.appStart('outside')); } const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer'); if (currentServer) { - yield put(setServer(currentServer)); + yield put(selectServer(currentServer)); + if (token) { + yield put(actions.appStart('inside')); + } const login = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); if (login) { diff --git a/app/sagas/login.js b/app/sagas/login.js index 495b4ed5b..1e584d6c7 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -1,7 +1,9 @@ import { AsyncStorage } from 'react-native'; +import { delay } from 'redux-saga'; import { put, call, take, takeLatest, select, all } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; +import { appStart } from '../actions'; import { // loginRequest, // loginSubmit, @@ -18,7 +20,6 @@ import { forgotPasswordFailure } from '../actions/login'; import RocketChat from '../lib/rocketchat'; -import * as NavigationService from '../containers/routes/NavigationService'; import log from '../utils/log'; import I18n from '../i18n'; @@ -26,7 +27,6 @@ const getUser = state => state.login; const getServer = state => state.server.server; const getIsConnected = state => state.meteor.connected; -// const loginCall = args => ((args.resume || args.oauth) ? RocketChat.login(args) : RocketChat.loginWithPassword(args)); const loginCall = args => RocketChat.loginWithPassword(args); const registerCall = args => RocketChat.register(args); const setUsernameCall = args => RocketChat.setUsername(args); @@ -34,67 +34,27 @@ const loginSuccessCall = () => RocketChat.loginSuccess(); const logoutCall = args => RocketChat.logout(args); const forgotPasswordCall = args => RocketChat.forgotPassword(args); -// const getToken = function* getToken() { -// const currentServer = yield select(getServer); -// const user = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); -// if (user) { -// try { -// yield put(setToken(JSON.parse(user))); -// yield call([AsyncStorage, 'setItem'], RocketChat.TOKEN_KEY, JSON.parse(user).token || ''); -// return JSON.parse(user); -// } catch (e) { -// console.log('getTokenerr', e); -// } -// } else { -// return yield put(setToken()); -// } -// }; - -// const handleLoginWhenServerChanges = function* handleLoginWhenServerChanges() { -// try { -// const user = yield call(getToken); -// if (user.token) { -// yield put(loginRequest({ resume: user.token })); -// } -// } catch (e) { -// console.log(e); -// } -// }; - -const saveToken = function* saveToken() { +const handleLoginSuccess = function* handleLoginSuccess() { try { const [server, user] = yield all([select(getServer), select(getUser)]); yield AsyncStorage.setItem(RocketChat.TOKEN_KEY, user.token); yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user)); - const token = yield AsyncStorage.getItem('pushId'); - if (token) { - yield RocketChat.registerPushToken(user.user.id, token); - } - if (!user.user.username && !user.isRegistering) { + // const token = yield AsyncStorage.getItem('pushId'); + // if (token) { + // yield RocketChat.registerPushToken(user.user.id, token); + // } + yield RocketChat.registerPushToken(user.user.id); + if (!user.user.username || user.isRegistering) { yield put(registerIncomplete()); + } else { + yield delay(300); + yield put(appStart('inside')); } } catch (e) { - log('saveToken', e); + log('handleLoginSuccess', e); } }; -// const handleLoginRequest = function* handleLoginRequest({ credentials }) { -// try { -// // const server = yield select(getServer); -// const user = yield call(loginCall, credentials); -// yield put(loginSuccess(user)); -// } catch (err) { -// if (err.error === 403) { -// return yield put(logout()); -// } -// yield put(loginFailure(err)); -// } -// }; - -// const handleLoginSubmit = function* handleLoginSubmit({ credentials }) { -// yield put(loginRequest(credentials)); -// }; - const handleRegisterSubmit = function* handleRegisterSubmit({ credentials }) { yield put(registerRequest(credentials)); }; @@ -137,6 +97,8 @@ const handleLogout = function* handleLogout() { const server = yield select(getServer); if (server) { try { + yield put(appStart('outside')); + yield delay(300); yield call(logoutCall, { server }); } catch (e) { log('handleLogout', e); @@ -145,7 +107,7 @@ const handleLogout = function* handleLogout() { }; const handleRegisterIncomplete = function* handleRegisterIncomplete() { - yield call(NavigationService.navigate, 'Register'); + yield put(appStart('outside')); }; const handleForgotPasswordRequest = function* handleForgotPasswordRequest({ email }) { @@ -183,7 +145,7 @@ const handleSetUser = function* handleSetUser(params) { const root = function* root() { // yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges); // yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest); - yield takeLatest(types.LOGIN.SUCCESS, saveToken); + yield takeLatest(types.LOGIN.SUCCESS, handleLoginSuccess); // yield takeLatest(types.LOGIN.SUBMIT, handleLoginSubmit); yield takeLatest(types.LOGIN.REGISTER_REQUEST, handleRegisterRequest); yield takeLatest(types.LOGIN.REGISTER_SUBMIT, handleRegisterSubmit); diff --git a/app/sagas/messages.js b/app/sagas/messages.js index 5792ba324..01e004acd 100644 --- a/app/sagas/messages.js +++ b/app/sagas/messages.js @@ -1,5 +1,6 @@ import { delay } from 'redux-saga'; import { takeLatest, put, call, select } from 'redux-saga/effects'; + import { MESSAGES } from '../actions/actionsTypes'; import { messagesSuccess, @@ -16,8 +17,8 @@ import { } from '../actions/messages'; import RocketChat from '../lib/rocketchat'; import database from '../lib/realm'; -import { goRoom } from '../containers/routes/NavigationService'; import log from '../utils/log'; +import { NavigationActions } from '../Navigation'; const deleteMessage = message => RocketChat.deleteMessage(message); const editMessage = message => RocketChat.editMessage(message); @@ -74,17 +75,30 @@ const handleTogglePinRequest = function* handleTogglePinRequest({ message }) { } }; +const goRoom = function* goRoom({ rid, name }) { + NavigationActions.popToRoot(); + yield delay(1000); + NavigationActions.push({ + screen: 'RoomView', + passProps: { + room: { rid, name }, + rid, + name + } + }); +}; + const handleReplyBroadcast = function* handleReplyBroadcast({ message }) { try { const { username } = message.u; const subscriptions = database.objects('subscriptions').filtered('name = $0', username); if (subscriptions.length) { - goRoom({ rid: subscriptions[0].rid, name: subscriptions[0].name }); + yield goRoom({ rid: subscriptions[0].rid, name: subscriptions[0].name }); } else { const room = yield RocketChat.createDirectMessage(username); - goRoom({ rid: room.rid, name: username }); + yield goRoom({ rid: room.rid, name: username }); } - yield delay(100); + yield delay(500); const server = yield select(state => state.server.server); const msg = `[ ](${ server }/direct/${ username }?msg=${ message._id }) `; yield put(setInput({ msg })); diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js index 0f9ceaf17..bf63f52ac 100644 --- a/app/sagas/rooms.js +++ b/app/sagas/rooms.js @@ -9,8 +9,8 @@ import { addUserTyping, removeUserTyping, setLastOpen } from '../actions/room'; import { messagesRequest, editCancel } from '../actions/messages'; import RocketChat from '../lib/rocketchat'; import database from '../lib/realm'; -import * as NavigationService from '../containers/routes/NavigationService'; import log from '../utils/log'; +import { NavigationActions } from '../Navigation'; const leaveRoom = rid => RocketChat.leaveRoom(rid); const eraseRoom = rid => RocketChat.eraseRoom(rid); @@ -139,7 +139,7 @@ const updateLastOpen = function* updateLastOpen() { }; const goRoomsListAndDelete = function* goRoomsListAndDelete(rid) { - NavigationService.goRoomsList(); + NavigationActions.popToRoot(); yield delay(1000); try { database.write(() => { diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index 973677f22..1de9f453d 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -1,22 +1,23 @@ -import { put, call, takeLatest, take } from 'redux-saga/effects'; +import { put, call, takeLatest } from 'redux-saga/effects'; import { delay } from 'redux-saga'; import { AsyncStorage } from 'react-native'; -import { SERVER, LOGIN } from '../actions/actionsTypes'; +import { NavigationActions } from '../Navigation'; +import { SERVER } from '../actions/actionsTypes'; import * as actions from '../actions'; import { connectRequest } from '../actions/connect'; -import { serverSuccess, serverFailure, setServer } from '../actions/server'; +import { serverSuccess, serverFailure, selectServer } from '../actions/server'; import { setRoles } from '../actions/roles'; import RocketChat from '../lib/rocketchat'; import database from '../lib/realm'; -import { navigate } from '../containers/routes/NavigationService'; import log from '../utils/log'; +import I18n from '../i18n'; const validate = function* validate(server) { return yield RocketChat.testServer(server); }; -const selectServer = function* selectServer({ server }) { +const handleSelectServer = function* handleSelectServer({ server }) { try { yield database.setActiveDB(server); @@ -36,7 +37,7 @@ const selectServer = function* selectServer({ server }) { yield put(connectRequest()); } catch (e) { - log('selectServer', e); + log('handleSelectServer', e); } }; @@ -53,12 +54,12 @@ const validateServer = function* validateServer({ server }) { const addServer = function* addServer({ server }) { try { + yield put(actions.appStart('outside')); + yield call(NavigationActions.resetTo, { screen: 'ListServerView', title: I18n.t('Servers') }); database.databases.serversDB.write(() => { database.databases.serversDB.create('servers', { id: server, current: false }, true); }); - yield put(setServer(server)); - yield take(LOGIN.SET_TOKEN); - navigate('LoginSignup'); + yield put(selectServer(server)); } catch (e) { log('addServer', e); } @@ -66,7 +67,7 @@ const addServer = function* addServer({ server }) { const root = function* root() { yield takeLatest(SERVER.REQUEST, validateServer); - yield takeLatest(SERVER.SELECT, selectServer); + yield takeLatest(SERVER.SELECT, handleSelectServer); yield takeLatest(SERVER.ADD, addServer); }; export default root; diff --git a/app/static/images/navicon_add@2x.png b/app/static/images/navicon_add@2x.png new file mode 100644 index 000000000..4df23afcc Binary files /dev/null and b/app/static/images/navicon_add@2x.png differ diff --git a/app/static/images/navicon_menu@2x.png b/app/static/images/navicon_menu@2x.png new file mode 100644 index 000000000..12c54031f Binary files /dev/null and b/app/static/images/navicon_menu@2x.png differ diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index bad0fcfad..addda0446 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -13,21 +13,19 @@ import scrollPersistTaps from '../utils/scrollPersistTaps'; import Button from '../containers/Button'; import I18n from '../i18n'; -@connect( - state => ({ - createChannel: state.createChannel, - users: state.selectedUsers.users - }), - dispatch => ({ - create: data => dispatch(createChannelRequest(data)) - }) -) +@connect(state => ({ + createChannel: state.createChannel, + users: state.selectedUsers.users +}), dispatch => ({ + create: data => dispatch(createChannelRequest(data)) +})) +/** @extends React.Component */ export default class CreateChannelView extends LoggedView { static propTypes = { + navigator: PropTypes.object, create: PropTypes.func.isRequired, createChannel: PropTypes.object.isRequired, - users: PropTypes.array.isRequired, - navigation: PropTypes.object.isRequired + users: PropTypes.array.isRequired }; constructor(props) { diff --git a/app/views/ForgotPasswordView.js b/app/views/ForgotPasswordView.js index 77d4666d4..f17e325c8 100644 --- a/app/views/ForgotPasswordView.js +++ b/app/views/ForgotPasswordView.js @@ -20,12 +20,13 @@ import I18n from '../i18n'; forgotPasswordInit: () => dispatch(forgotPasswordInit()), forgotPasswordRequest: email => dispatch(forgotPasswordRequest(email)) })) +/** @extends React.Component */ export default class ForgotPasswordView extends LoggedView { static propTypes = { + navigator: PropTypes.object, forgotPasswordInit: PropTypes.func.isRequired, forgotPasswordRequest: PropTypes.func.isRequired, - login: PropTypes.object, - navigation: PropTypes.object.isRequired + login: PropTypes.object } constructor(props) { @@ -44,7 +45,7 @@ export default class ForgotPasswordView extends LoggedView { componentDidUpdate() { const { login } = this.props; if (login.success) { - this.props.navigation.goBack(); + this.props.navigator.pop(); setTimeout(() => { showErrorAlert(I18n.t('Forgot_password_If_this_email_is_registered'), I18n.t('Alert')); }); diff --git a/app/views/ListServerView.js b/app/views/ListServerView.js index aee2ea6e6..d67c3c303 100644 --- a/app/views/ListServerView.js +++ b/app/views/ListServerView.js @@ -2,17 +2,16 @@ import React from 'react'; import Icon from 'react-native-vector-icons/Ionicons'; import PropTypes from 'prop-types'; -// import Zeroconf from 'react-native-zeroconf'; -import { View, Text, SectionList, StyleSheet, SafeAreaView } from 'react-native'; +import { View, Text, SectionList, StyleSheet } from 'react-native'; import { connect } from 'react-redux'; -import { withNavigationFocus } from 'react-navigation'; import LoggedView from './View'; -import { setServer } from '../actions/server'; +import { selectServer } from '../actions/server'; import database from '../lib/realm'; import Fade from '../animations/fade'; import Touch from '../utils/touch'; import I18n from '../i18n'; +import { iconsMap } from '../Icons'; const styles = StyleSheet.create({ view: { @@ -63,22 +62,19 @@ const styles = StyleSheet.create({ } }); -// const zeroconf = new Zeroconf(); - - @connect(state => ({ server: state.server.server, login: state.login, connected: state.meteor.connected }), dispatch => ({ - selectServer: server => dispatch(setServer(server)) + selectServer: server => dispatch(selectServer(server)) })) -class ListServerView extends LoggedView { +/** @extends React.Component */ +export default class ListServerView extends LoggedView { static propTypes = { - navigation: PropTypes.object.isRequired, + navigator: PropTypes.object, login: PropTypes.object.isRequired, selectServer: PropTypes.func.isRequired, - connected: PropTypes.bool.isRequired, server: PropTypes.string } @@ -88,51 +84,36 @@ class ListServerView extends LoggedView { sections: [] }; this.data = database.databases.serversDB.objects('servers'); - // this.redirected = false; this.data.addListener(this.updateState); + props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); + } + + async componentWillMount() { + this.props.navigator.setButtons({ + rightButtons: [{ + id: 'addServer', + icon: iconsMap.add + }] + }); } componentDidMount() { - // zeroconf.on('update', this.updateState); - // zeroconf.scan('http', 'tcp', 'local.'); this.updateState(); this.jumpToSelectedServer(); } - // componentDidUpdate() { - // if (this.props.connected && - // this.props.server && - // !this.props.login.token && - // !this.redirected) { - // this.redirected = true; - // this.props.navigation.navigate({ key: 'LoginSignup', routeName: 'LoginSignup' }); - // } else if (!this.props.connected) { - // this.redirected = false; - // } - // } - componentWillUnmount() { - // zeroconf.stop(); this.data.removeAllListeners(); - // zeroconf.removeListener('update', this.updateState); } - openLogin = () => { - this.props.navigation.navigate({ key: 'LoginSignup', routeName: 'LoginSignup' }); - } - - selectAndNavigateTo = (server) => { - this.props.selectServer(server); - this.openLogin(); - } - - jumpToSelectedServer() { - if (this.props.server && !this.props.login.isRegistering) { - setTimeout(() => { - if (this.props.isFocused) { - this.openLogin(); - } - }, 500); + onNavigatorEvent(event) { + if (event.type === 'NavBarButtonPress') { + if (event.id === 'addServer') { + this.props.navigator.push({ + screen: 'NewServerView', + title: I18n.t('New_Server') + }); + } } } @@ -145,24 +126,6 @@ class ListServerView extends LoggedView { title: I18n.t('My_servers'), data: this.data }]; - // - // this.state.nearBy = zeroconf.getServices(); - // if (this.state.nearBy) { - // const nearBy = Object.keys(this.state.nearBy) - // .filter(key => this.state.nearBy[key].addresses); - // if (nearBy.length) { - // sections.push({ - // title: 'Nearby', - // data: nearBy.map((key) => { - // const server = this.state.nearBy[key]; - // const address = `http://${ server.addresses[0] }:${ server.port }`; - // return { - // id: address - // }; - // }) - // }); - // } - // } return { ...this.state, @@ -170,6 +133,26 @@ class ListServerView extends LoggedView { }; }; + openLogin = (server) => { + this.props.navigator.push({ + screen: 'LoginSignupView', + title: server + }); + } + + selectAndNavigateTo = (server) => { + this.props.selectServer(server); + this.openLogin(server); + } + + jumpToSelectedServer() { + if (this.props.server && !this.props.login.isRegistering) { + setTimeout(() => { + this.openLogin(this.props.server); + }, 1000); + } + } + updateState = () => { this.setState(this.getState()); } @@ -210,7 +193,7 @@ class ListServerView extends LoggedView { render() { return ( - + item.id} ItemSeparatorComponent={this.renderSeparator} /> - + ); } } -export default withNavigationFocus(ListServerView); diff --git a/app/views/LoginSignupView.js b/app/views/LoginSignupView.js index c9a55546d..df9869a73 100644 --- a/app/views/LoginSignupView.js +++ b/app/views/LoginSignupView.js @@ -1,13 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, View, ScrollView, TouchableOpacity, SafeAreaView, WebView, Platform, LayoutAnimation, Image, StyleSheet } from 'react-native'; +import { Text, View, ScrollView, TouchableOpacity, LayoutAnimation, Image, StyleSheet } from 'react-native'; import { connect } from 'react-redux'; import Icon from 'react-native-vector-icons/FontAwesome'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import { Base64 } from 'js-base64'; -import Modal from 'react-native-modal'; -import RocketChat from '../lib/rocketchat'; import { open, close } from '../actions/login'; import LoggedView from './View'; import sharedStyles from './Styles'; @@ -17,9 +15,6 @@ import Button from '../containers/Button'; import Loading from '../containers/Loading'; import I18n from '../i18n'; -const userAgentAndroid = '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'; -const userAgent = Platform.OS === 'ios' ? 'UserAgent' : userAgentAndroid; - const styles = StyleSheet.create({ container: { alignItems: 'center', @@ -44,14 +39,13 @@ const styles = StyleSheet.create({ planetImage: { width: 200, height: 162, - marginVertical: 20, - opacity: 0.6 + marginVertical: 20 } }); @connect(state => ({ server: state.server.server, - login: state.login, + isFetching: state.login.isFetching, Accounts_EmailOrUsernamePlaceholder: state.settings.Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder: state.settings.Accounts_PasswordPlaceholder, Accounts_OAuth_Facebook: state.settings.Accounts_OAuth_Facebook, @@ -63,17 +57,16 @@ const styles = StyleSheet.create({ Accounts_OAuth_Twitter: state.settings.Accounts_OAuth_Twitter, services: state.login.services }), dispatch => ({ - loginOAuth: params => RocketChat.login(params), open: () => dispatch(open()), close: () => dispatch(close()) })) +/** @extends React.Component */ export default class LoginSignupView extends LoggedView { static propTypes = { - loginOAuth: PropTypes.func.isRequired, + navigator: PropTypes.object, open: PropTypes.func.isRequired, close: PropTypes.func.isRequired, - navigation: PropTypes.object.isRequired, - login: PropTypes.object, + isFetching: PropTypes.bool, server: PropTypes.string, Accounts_EmailOrUsernamePlaceholder: PropTypes.bool, Accounts_PasswordPlaceholder: PropTypes.string, @@ -89,13 +82,6 @@ export default class LoginSignupView extends LoggedView { constructor(props) { super('LoginSignupView', props); - - this.state = { - modalVisible: false, - oAuthUrl: '', - showSocialButtons: false - }; - this.redirectRegex = new RegExp(`(?=.*(${ this.props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g'); } componentDidMount() { @@ -183,19 +169,29 @@ export default class LoginSignupView extends LoggedView { } openOAuth = (oAuthUrl) => { - this.setState({ oAuthUrl, modalVisible: true }); + this.props.navigator.showModal({ + screen: 'OAuthView', + title: 'OAuth', + passProps: { + oAuthUrl + } + }); + } + + login = () => { + this.props.navigator.push({ + screen: 'LoginView', + title: this.props.server, + backButtonTitle: I18n.t('Welcome') + }); } register = () => { - this.props.navigation.navigate({ key: 'Register', routeName: 'Register' }); - } - - closeOAuth = () => { - this.setState({ modalVisible: false }); - } - - toggleSocialButtons = () => { - this.setState({ showSocialButtons: !this.state.showSocialButtons }); + this.props.navigator.push({ + screen: 'RegisterView', + title: this.props.server, + backButtonTitle: I18n.t('Welcome') + }); } renderServices = () => { @@ -279,66 +275,40 @@ export default class LoginSignupView extends LoggedView { render() { return ( - [ - - - - - {I18n.t('Welcome_title_pt_1')} - {I18n.t('Welcome_title_pt_2')} - -