Switch push notification lib (#346)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #342 

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
This commit is contained in:
Diego Mello 2018-07-10 10:40:32 -03:00 committed by Guilherme Gazzo
parent 8af34a705a
commit 224c421b69
138 changed files with 8329 additions and 7068 deletions

View File

@ -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

22
.snyk
View File

@ -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'

View File

@ -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,
},
]
}

View File

@ -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,
},
]
}

View File

@ -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;
}
}

View File

@ -1,76 +1,56 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.rocket.reactnative"
android:versionCode="1"
android:versionName="1.0">
package="chat.rocket.reactnative"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
android:resizeableActivity="true"
android:largeHeap="true">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:label="RocketChatRN">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="go.rocket.chat" />
<data android:scheme="rocketchat" android:host="room" />
<data android:scheme="rocketchat" android:host="auth" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
android:resizeableActivity="true"
android:largeHeap="true">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:label="RocketChatRN">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="go.rocket.chat" />
<data android:scheme="rocketchat" android:host="room" />
<data android:scheme="rocketchat" android:host="auth" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="${applicationId}" />
</intent-filter>
</receiver>
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
<service android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationRegistrationService"/>
<service
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
</application>
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="673693445664\0"/>
</application>
</manifest>

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<ReactPackage> getPackages() {
return Arrays.<ReactPackage>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.<ReactPackage>asList(
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public List<ReactPackage> createAdditionalReactPackages() {
return Arrays.<ReactPackage>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()
);
}
}

View File

@ -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<AppVisibilityListener> 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();
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

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

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/launch_screen">
</LinearLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

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

View File

@ -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 {

View File

@ -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'

28
app/Icons.js Normal file
View File

@ -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 };

13
app/Navigation.js Normal file
View File

@ -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();

View File

@ -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())

View File

@ -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',

View File

@ -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,

View File

@ -1,6 +1,6 @@
import { SERVER } from './actionsTypes';
export function setServer(server) {
export function selectServer(server) {
return {
type: SERVER.SELECT,
server

0
app/app.js Normal file
View File

View File

@ -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 (
<TouchableOpacity
onPress={() => this.props.navigation.dispatch(NavigationActions.back())}
style={styles.button}
testID='close-modal-button'
>
<Icon
style={styles.icon}
name='close'
size={25}
/>
</TouchableOpacity>
);
}
}

View File

@ -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 (
<SafeAreaView forceInset={{ bottom: 'never' }} style={styles.container}>
<View style={styles.appBar}>
{this.props.subview}
</View>
</SafeAreaView>
);
}
}

View File

@ -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,

View File

@ -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 (<PublicRoutes ref={nav => this.navigator = nav} />);
}
return (<AuthRoutes ref={nav => this.navigator = nav} />);
}
}

View File

@ -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 => <View key={key} style={styles.separator} />;
renderItem = ({
text, left, selected, onPress, testID
text, left, onPress, testID
}) => (
<Touch
key={text}
@ -184,8 +188,8 @@ export default class Sidebar extends Component {
activeOpacity={0.3}
testID={testID}
>
<View style={[styles.item, selected && styles.selected]}>
<View style={[styles.itemLeft, !selected && styles.itemLeftOpacity]}>
<View style={styles.item}>
<View style={styles.itemLeft}>
{left}
</View>
<Text style={styles.itemText}>
@ -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: <Icon name='chat-bubble' size={20} />,
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: <Icon name='person' size={20} />,
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: <Icon name='settings' size={20} />,
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: <Icon
name='exit-to-app'
size={20}
/>,
left: <Icon name='exit-to-app' size={20} />,
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 (
<ScrollView>
<SafeAreaView
style={styles.container}
forceInset={{ top: 'always', horizontal: 'never' }}
testID='sidebar'
>
<ScrollView style={{ backgroundColor: '#fff' }}>
<SafeAreaView testID='sidebar'>
<Touch
onPress={() => this.toggleServers()}
underlayColor='rgba(255, 255, 255, 0.5)'
@ -335,6 +338,7 @@ export default class Sidebar extends Component {
<Icon
name={this.state.showServers ? 'keyboard-arrow-up' : 'keyboard-arrow-down'}
size={30}
style={{ paddingHorizontal: 10 }}
/>
</View>
</Touch>

View File

@ -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: {}

View File

@ -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;

View File

@ -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 };

View File

@ -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 = {

View File

@ -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 }) => (
<TouchableOpacity
style={sharedStyles.headerButton}
onPress={navigation.toggleDrawer}
accessibilityLabel={I18n.t('Toggle_Drawer')}
accessibilityTraits='button'
testID={testID}
>
<Icon name='menu' size={30} color='#292E35' />
</TouchableOpacity>
);
const Routes = createDrawerNavigator(
{
Chats: {
screen: AuthRoutes,
navigationOptions: {
drawerLabel: I18n.t('Chats'),
drawerIcon: () => <Icon name='chat-bubble' size={20} />
}
},
ProfileView: {
screen: createStackNavigator({
ProfileView: {
screen: ProfileView,
navigationOptions: ({ navigation }) => ({
title: I18n.t('Profile'),
headerTintColor: '#292E35',
headerLeft: <MenuButton navigation={navigation} testID='profile-view-sidebar' />
})
}
})
},
SettingsView: {
screen: createStackNavigator({
SettingsView: {
screen: SettingsView,
navigationOptions: ({ navigation }) => ({
title: I18n.t('Settings'),
headerTintColor: '#292E35',
headerLeft: <MenuButton navigation={navigation} testID='settings-view-sidebar' />
})
}
})
}
},
{
contentComponent: Sidebar,
drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked',
initialRouteName: 'Chats',
backBehavior: 'initialRoute'
}
);
export default Routes;

View File

@ -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);
}
}

View File

@ -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: (
<TouchableOpacity
onPress={() => navigation.navigate({ key: 'AddServer', routeName: 'AddServer' })}
style={{ width: 50, alignItems: 'center' }}
accessibilityLabel={I18n.t('Add_Server')}
accessibilityTraits='button'
>
<Icon name='plus' size={16} />
</TouchableOpacity>
)
};
}
},
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;

View File

@ -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() {

View File

@ -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!',

View File

@ -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 = () => (
<Provider store={store}>
<Routes />
</Provider>
);
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;
}
}

View File

@ -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(

View File

@ -54,5 +54,6 @@ export default async function canOpenRoom({ rid, path }) {
return data;
} catch (e) {
log('canOpenRoom', e);
return false;
}
}

View File

@ -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 },

View File

@ -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,

View File

@ -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 }: `;

View File

@ -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({

42
app/push/index.js Normal file
View File

@ -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 };

34
app/push/push.android.js Normal file
View File

@ -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();

31
app/push/push.ios.js Normal file
View File

@ -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();

View File

@ -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,

View File

@ -19,6 +19,7 @@ export default function login(state = initialState, action) {
...state,
isFetching: true,
isAuthenticated: false,
isRegistering: false,
failure: false,
error: ''
};

View File

@ -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);

View File

@ -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));

View File

@ -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));
}

View File

@ -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) {

View File

@ -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);

View File

@ -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 }));

View File

@ -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(() => {

View File

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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) {

View File

@ -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'));
});

View File

@ -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 (
<SafeAreaView style={styles.view} testID='list-server-view'>
<View style={styles.view} testID='list-server-view'>
<SectionList
style={styles.list}
sections={this.state.sections}
@ -219,8 +202,7 @@ class ListServerView extends LoggedView {
keyExtractor={item => item.id}
ItemSeparatorComponent={this.renderSeparator}
/>
</SafeAreaView>
</View>
);
}
}
export default withNavigationFocus(ListServerView);

View File

@ -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 (
[
<ScrollView
key='login-view'
style={[sharedStyles.container, sharedStyles.containerScrollView]}
{...scrollPersistTaps}
>
<SafeAreaView testID='welcome-view'>
<View style={styles.container}>
<Image
source={require('../static/images/logo.png')}
style={sharedStyles.loginLogo}
resizeMode='center'
/>
<Text style={[sharedStyles.loginText, styles.header, { color: '#81848A' }]}>{I18n.t('Welcome_title_pt_1')}</Text>
<Text style={[sharedStyles.loginText, styles.header]}>{I18n.t('Welcome_title_pt_2')}</Text>
<Image
style={styles.planetImage}
source={require('../static/images/planet.png')}
/>
<Button
title={I18n.t('I_have_an_account')}
type='primary'
onPress={() => this.props.navigation.navigate({ key: 'Login', routeName: 'Login' })}
testID='welcome-view-login'
/>
<Button
title={I18n.t('Create_account')}
type='secondary'
onPress={() => this.props.navigation.navigate({ key: 'Register', routeName: 'Register' })}
testID='welcome-view-register'
/>
{this.renderServices()}
</View>
<Loading visible={this.props.login.isFetching} />
</SafeAreaView>
</ScrollView>,
<Modal
key='modal-oauth'
visible={this.state.modalVisible}
animationType='slide'
style={sharedStyles.oAuthModal}
onBackButtonPress={this.closeOAuth}
useNativeDriver
>
<WebView
source={{ uri: this.state.oAuthUrl }}
userAgent={userAgent}
onNavigationStateChange={(webViewState) => {
const url = decodeURIComponent(webViewState.url);
if (this.redirectRegex.test(url)) {
const parts = url.split('#');
const credentials = JSON.parse(parts[1]);
this.props.loginOAuth({ oauth: { ...credentials } });
this.setState({ modalVisible: false });
}
}}
/>
<Icon name='close' size={30} style={sharedStyles.closeOAuth} onPress={this.closeOAuth} />
</Modal>
]
<ScrollView
style={[sharedStyles.container, sharedStyles.containerScrollView]}
{...scrollPersistTaps}
>
<View testID='welcome-view'>
<View style={styles.container}>
<Image
source={require('../static/images/logo.png')}
style={sharedStyles.loginLogo}
resizeMode='center'
/>
<Text style={[sharedStyles.loginText, styles.header, { color: '#81848A' }]}>{I18n.t('Welcome_title_pt_1')}</Text>
<Text style={[sharedStyles.loginText, styles.header]}>{I18n.t('Welcome_title_pt_2')}</Text>
<Image
style={styles.planetImage}
source={require('../static/images/planet.png')}
/>
<Button
title={I18n.t('I_have_an_account')}
type='primary'
onPress={() => this.login()}
testID='welcome-view-login'
/>
<Button
title={I18n.t('Create_account')}
type='secondary'
onPress={() => this.register()}
testID='welcome-view-register'
/>
{this.renderServices()}
</View>
<Loading visible={this.props.isFetching} />
</View>
</ScrollView>
);
}
}

View File

@ -1,13 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Keyboard, Text, ScrollView, SafeAreaView, View } from 'react-native';
import { Keyboard, Text, ScrollView, View } from 'react-native';
import { connect } from 'react-redux';
import { Answers } from 'react-native-fabric';
import RocketChat from '../lib/rocketchat';
import KeyboardView from '../presentation/KeyboardView';
import TextInput from '../containers/TextInput';
import CloseModalButton from '../containers/CloseModalButton';
import Button from '../containers/Button';
import Loading from '../containers/Loading';
import styles from './Styles';
@ -26,12 +25,19 @@ import I18n from '../i18n';
}), () => ({
loginSubmit: params => RocketChat.loginWithPassword(params)
}))
/** @extends React.Component */
export default class LoginView extends LoggedView {
static propTypes = {
navigator: PropTypes.object,
loginSubmit: PropTypes.func.isRequired,
navigation: PropTypes.object.isRequired,
login: PropTypes.object,
server: PropTypes.string
server: PropTypes.string,
error: PropTypes.string,
Accounts_EmailOrUsernamePlaceholder: PropTypes.string,
Accounts_PasswordPlaceholder: PropTypes.string,
failure: PropTypes.bool,
isFetching: PropTypes.bool,
reason: PropTypes.string
}
constructor(props) {
@ -58,6 +64,22 @@ export default class LoginView extends LoggedView {
}
}
register = () => {
this.props.navigator.push({
screen: 'RegisterView',
title: this.props.server,
backButtonTitle: I18n.t('Login')
});
}
forgotPassword = () => {
this.props.navigator.push({
screen: 'ForgotPasswordView',
title: I18n.t('Forgot_Password'),
backButtonTitle: I18n.t('Login')
});
}
renderTOTP = () => {
if (/totp/ig.test(this.props.error)) {
return (
@ -84,8 +106,7 @@ export default class LoginView extends LoggedView {
key='login-view'
>
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<SafeAreaView testID='login-view'>
<CloseModalButton navigation={this.props.navigation} />
<View testID='login-view'>
<Text style={[styles.loginText, styles.loginTitle]}>Login</Text>
<TextInput
label={I18n.t('Username')}
@ -122,14 +143,14 @@ export default class LoginView extends LoggedView {
<Text
style={[styles.loginText, { marginTop: 10 }]}
testID='login-view-register'
onPress={() => this.props.navigation.navigate('Register')}
onPress={() => this.register()}
>{I18n.t('New_in_RocketChat_question_mark')} &nbsp;
<Text style={{ color: COLOR_BUTTON_PRIMARY }}>{I18n.t('Sign_Up')}
</Text>
</Text>
<Text
style={[styles.loginText, { marginTop: 20, fontSize: 13 }]}
onPress={() => this.props.navigation.navigate('ForgotPassword')}
onPress={() => this.forgotPassword()}
testID='login-view-forgot-password'
>{I18n.t('Forgot_password')}
</Text>
@ -137,7 +158,7 @@ export default class LoginView extends LoggedView {
{this.props.failure ? <Text style={styles.error}>{this.props.reason}</Text> : null}
<Loading visible={this.props.isFetching} />
</SafeAreaView>
</View>
</ScrollView>
</KeyboardView>
);

View File

@ -10,21 +10,23 @@ import Message from '../../containers/message';
import RCActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n';
@connect(
state => ({
messages: state.mentionedMessages.messages,
ready: state.mentionedMessages.ready,
user: state.login.user,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}),
dispatch => ({
openMentionedMessages: (rid, limit) => dispatch(openMentionedMessages(rid, limit)),
closeMentionedMessages: () => dispatch(closeMentionedMessages())
})
)
@connect(state => ({
messages: state.mentionedMessages.messages,
ready: state.mentionedMessages.ready,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
},
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}), dispatch => ({
openMentionedMessages: (rid, limit) => dispatch(openMentionedMessages(rid, limit)),
closeMentionedMessages: () => dispatch(closeMentionedMessages())
}))
/** @extends React.Component */
export default class MentionedMessagesView extends LoggedView {
static propTypes = {
navigation: PropTypes.object,
rid: PropTypes.string,
messages: PropTypes.array,
ready: PropTypes.bool,
user: PropTypes.object,
@ -57,7 +59,7 @@ export default class MentionedMessagesView extends LoggedView {
}
load = () => {
this.props.openMentionedMessages(this.props.navigation.state.params.rid, this.limit);
this.props.openMentionedMessages(this.props.rid, this.limit);
}
moreData = () => {

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, ScrollView, View, SafeAreaView, Keyboard } from 'react-native';
import { Text, ScrollView, View, Keyboard } from 'react-native';
import { connect } from 'react-redux';
import { serverRequest, addServer } from '../actions/server';
@ -21,14 +21,15 @@ import I18n from '../i18n';
validateServer: url => dispatch(serverRequest(url)),
addServer: url => dispatch(addServer(url))
}))
/** @extends React.Component */
export default class NewServerView extends LoggedView {
static propTypes = {
navigator: PropTypes.object,
validateServer: PropTypes.func.isRequired,
addServer: PropTypes.func.isRequired,
validating: PropTypes.bool.isRequired,
validInstance: PropTypes.bool.isRequired,
addingServer: PropTypes.bool.isRequired,
navigation: PropTypes.object.isRequired
addingServer: PropTypes.bool.isRequired
}
constructor(props) {
@ -36,11 +37,7 @@ export default class NewServerView extends LoggedView {
this.state = {
defaultServer: 'https://open.rocket.chat'
};
this.props.validateServer(this.state.defaultServer); // Need to call because in case of submit with empty field
}
componentDidMount() {
this.input.focus();
props.validateServer(this.state.defaultServer); // Need to call because in case of submit with empty field
}
onChangeText = (text) => {
@ -109,7 +106,7 @@ export default class NewServerView extends LoggedView {
keyboardVerticalOffset={128}
>
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<SafeAreaView testID='new-server-view'>
<View testID='new-server-view'>
<Text style={[styles.loginText, styles.loginTitle]}>{I18n.t('Sign_in_your_server')}</Text>
<TextInput
inputRef={e => this.input = e}
@ -132,7 +129,7 @@ export default class NewServerView extends LoggedView {
/>
</View>
<Loading visible={this.props.addingServer} />
</SafeAreaView>
</View>
</ScrollView>
</KeyboardView>
);

69
app/views/OAuthView.js Normal file
View File

@ -0,0 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import { WebView, Platform } from 'react-native';
import { connect } from 'react-redux';
import RocketChat from '../lib/rocketchat';
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;
@connect(state => ({
server: state.server.server
}))
export default class TermsServiceView extends React.PureComponent {
static navigatorButtons = {
leftButtons: [{
id: 'close',
title: I18n.t('Close')
}]
}
static propTypes = {
navigator: PropTypes.object,
oAuthUrl: PropTypes.string,
server: PropTypes.string
}
constructor(props) {
super(props);
this.redirectRegex = new RegExp(`(?=.*(${ props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g');
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
onNavigatorEvent(event) {
const { navigator } = this.props;
if (event.type === 'NavBarButtonPress') {
if (event.id === 'close') {
navigator.dismissModal();
}
}
}
login = async(params) => {
try {
await RocketChat.login(params);
} catch (e) {
console.warn(e);
}
}
render() {
return (
<WebView
source={{ uri: this.props.oAuthUrl }}
userAgent={userAgent}
onNavigationStateChange={(webViewState) => {
const url = decodeURIComponent(webViewState.url);
if (this.redirectRegex.test(url)) {
const parts = url.split('#');
const credentials = JSON.parse(parts[1]);
this.login({ oauth: { ...credentials } });
this.props.navigator.dismissModal();
}
}}
/>
);
}
}

View File

@ -16,22 +16,24 @@ const PIN_INDEX = 0;
const CANCEL_INDEX = 1;
const options = [I18n.t('Unpin'), I18n.t('Cancel')];
@connect(
state => ({
messages: state.pinnedMessages.messages,
ready: state.pinnedMessages.ready,
user: state.login.user,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}),
dispatch => ({
openPinnedMessages: (rid, limit) => dispatch(openPinnedMessages(rid, limit)),
closePinnedMessages: () => dispatch(closePinnedMessages()),
togglePinRequest: message => dispatch(togglePinRequest(message))
})
)
@connect(state => ({
messages: state.pinnedMessages.messages,
ready: state.pinnedMessages.ready,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
},
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}), dispatch => ({
openPinnedMessages: (rid, limit) => dispatch(openPinnedMessages(rid, limit)),
closePinnedMessages: () => dispatch(closePinnedMessages()),
togglePinRequest: message => dispatch(togglePinRequest(message))
}))
/** @extends React.Component */
export default class PinnedMessagesView extends LoggedView {
static propTypes = {
navigation: PropTypes.object,
rid: PropTypes.string,
messages: PropTypes.array,
ready: PropTypes.bool,
user: PropTypes.object,
@ -81,7 +83,7 @@ export default class PinnedMessagesView extends LoggedView {
}
load = () => {
this.props.openPinnedMessages(this.props.navigation.state.params.rid, this.limit);
this.props.openPinnedMessages(this.props.rid, this.limit);
}
moreData = () => {

View File

@ -3,7 +3,10 @@ import PropTypes from 'prop-types';
import { WebView } from 'react-native';
import { connect } from 'react-redux';
class PrivacyPolicyView extends React.PureComponent {
@connect(state => ({
privacyPolicy: state.settings.Layout_Privacy_Policy
}))
export default class PrivacyPolicyView extends React.PureComponent {
static propTypes = {
privacyPolicy: PropTypes.string
}
@ -14,11 +17,3 @@ class PrivacyPolicyView extends React.PureComponent {
);
}
}
function mapStateToProps(state) {
return {
privacyPolicy: state.settings.Layout_Privacy_Policy
};
}
export default connect(mapStateToProps)(PrivacyPolicyView);

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, ScrollView, SafeAreaView, Keyboard } from 'react-native';
import { View, ScrollView, SafeAreaView, Keyboard, Platform } from 'react-native';
import { connect } from 'react-redux';
import Dialog from 'react-native-dialog';
import SHA256 from 'js-sha256';
@ -22,17 +22,24 @@ import I18n from '../../i18n';
import Button from '../../containers/Button';
import Avatar from '../../containers/Avatar';
import Touch from '../../utils/touch';
import { iconsMap } from '../../Icons';
@connect(state => ({
user: state.login.user,
user: {
name: state.login.user && state.login.user.name,
username: state.login.user && state.login.user.username,
customFields: state.login.user && state.login.user.customFields,
emails: state.login.user && state.login.user.emails
},
Accounts_CustomFields: state.settings.Accounts_CustomFields
}))
/** @extends React.Component */
export default class ProfileView extends LoggedView {
static propTypes = {
navigation: PropTypes.object,
navigator: PropTypes.object,
user: PropTypes.object,
Accounts_CustomFields: PropTypes.string
};
}
constructor(props) {
super('ProfileView', props);
@ -49,11 +56,26 @@ export default class ProfileView extends LoggedView {
avatarSuggestions: {},
customFields: {}
};
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
componentWillMount() {
this.props.navigator.setButtons({
leftButtons: [{
id: 'sideMenu',
icon: Platform.OS === 'ios' ? iconsMap.menu : undefined
}]
});
}
async componentDidMount() {
this.init();
this.props.navigator.setDrawerEnabled({
side: 'left',
enabled: true
});
try {
const result = await RocketChat.getAvatarSuggestion();
this.setState({ avatarSuggestions: result });
@ -68,6 +90,22 @@ export default class ProfileView extends LoggedView {
}
}
onNavigatorEvent(event) {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'sideMenu' && Platform.OS === 'ios') {
this.props.navigator.toggleDrawer({
side: 'left',
animated: true,
to: 'missing'
});
}
}
}
setAvatar = (avatar) => {
this.setState({ avatar });
}
init = (user) => {
const {
name, username, emails, customFields
@ -195,10 +233,6 @@ export default class ProfileView extends LoggedView {
}
}
setAvatar = (avatar) => {
this.setState({ avatar });
}
resetAvatar = async() => {
try {
await RocketChat.resetAvatar();

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Keyboard, Text, View, SafeAreaView, ScrollView } from 'react-native';
import { Keyboard, Text, View, ScrollView } from 'react-native';
import { connect } from 'react-redux';
import { registerSubmit, setUsernameSubmit } from '../actions/login';
@ -10,7 +10,6 @@ import Loading from '../containers/Loading';
import KeyboardView from '../presentation/KeyboardView';
import styles from './Styles';
import { showToast } from '../utils/info';
import CloseModalButton from '../containers/CloseModalButton';
import scrollPersistTaps from '../utils/scrollPersistTaps';
import LoggedView from './View';
import I18n from '../i18n';
@ -26,8 +25,11 @@ import I18n from '../i18n';
registerSubmit: params => dispatch(registerSubmit(params)),
setUsernameSubmit: params => dispatch(setUsernameSubmit(params))
}))
/** @extends React.Component */
export default class RegisterView extends LoggedView {
static propTypes = {
navigator: PropTypes.object,
server: PropTypes.string,
registerSubmit: PropTypes.func.isRequired,
setUsernameSubmit: PropTypes.func,
Accounts_UsernamePlaceholder: PropTypes.string,
@ -88,11 +90,19 @@ export default class RegisterView extends LoggedView {
}
termsService = () => {
this.props.navigation.navigate({ key: 'TermsService', routeName: 'TermsService' });
this.props.navigator.push({
screen: 'TermsServiceView',
title: I18n.t('Terms_of_Service'),
backButtonTitle: I18n.t('Sign_Up')
});
}
privacyPolicy = () => {
this.props.navigation.navigate({ key: 'PrivacyPolicy', routeName: 'PrivacyPolicy' });
this.props.navigator.push({
screen: 'PrivacyPolicyView',
title: I18n.t('Privacy_Policy'),
backButtonTitle: I18n.t('Sign_Up')
});
}
_renderRegister() {
@ -202,8 +212,7 @@ export default class RegisterView extends LoggedView {
return (
<KeyboardView contentContainerStyle={styles.container}>
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<SafeAreaView testID='register-view'>
<CloseModalButton navigation={this.props.navigation} />
<View testID='register-view'>
<Text style={[styles.loginText, styles.loginTitle]}>{I18n.t('Sign_Up')}</Text>
{this._renderRegister()}
{this._renderUsername()}
@ -214,7 +223,7 @@ export default class RegisterView extends LoggedView {
: null
}
<Loading visible={this.props.login.isFetching} />
</SafeAreaView>
</View>
</ScrollView>
</KeyboardView>
);

View File

@ -14,7 +14,6 @@ import Touch from '../../utils/touch';
import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat';
import { leaveRoom } from '../../actions/room';
import { setLoading } from '../../actions/selectedUsers';
import log from '../../utils/log';
import RoomTypeIcon from '../../containers/RoomTypeIcon';
import I18n from '../../i18n';
@ -23,24 +22,24 @@ const renderSeparator = () => <View style={styles.separator} />;
const getRoomTitle = room => (room.t === 'd' ? <Text>{room.fname}</Text> : <Text><RoomTypeIcon type={room.t} />&nbsp;{room.name}</Text>);
@connect(state => ({
user_id: state.login.user.id,
user_username: state.login.user.username
userId: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username
}), dispatch => ({
leaveRoom: rid => dispatch(leaveRoom(rid)),
setLoadingInvite: loading => dispatch(setLoading(loading))
leaveRoom: rid => dispatch(leaveRoom(rid))
}))
/** @extends React.Component */
export default class RoomActionsView extends LoggedView {
static propTypes = {
baseUrl: PropTypes.string,
user: PropTypes.object,
navigation: PropTypes.object,
rid: PropTypes.string,
navigator: PropTypes.object,
userId: PropTypes.string,
username: PropTypes.string,
leaveRoom: PropTypes.func
}
constructor(props) {
super('RoomActionsView', props);
const { rid } = props.navigation.state.params;
const { rid } = props;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
[this.room] = this.rooms;
this.state = {
@ -63,59 +62,24 @@ export default class RoomActionsView extends LoggedView {
onPressTouchable = (item) => {
if (item.route) {
return this.props.navigation.navigate({ key: item.route, routeName: item.route, params: item.params });
this.props.navigator.push({
screen: item.route,
title: item.name,
passProps: item.params
});
}
if (item.event) {
return item.event();
}
}
updateRoomMembers = async() => {
const { t } = this.state.room;
if (!this.canViewMembers) {
return {};
}
if (t === 'c' || t === 'p') {
let onlineMembers = [];
let allMembers = [];
try {
const onlineMembersCall = RocketChat.getRoomMembers(this.state.room.rid, false);
const allMembersCall = RocketChat.getRoomMembers(this.state.room.rid, true);
const [onlineMembersResult, allMembersResult] = await Promise.all([onlineMembersCall, allMembersCall]);
onlineMembers = onlineMembersResult.records;
allMembers = allMembersResult.records;
return { onlineMembers, allMembers };
} catch (error) {
return {};
}
}
}
updateRoomMember = async() => {
if (this.state.room.t !== 'd') {
return {};
}
try {
const member = await RocketChat.getRoomMember(this.state.room.rid, this.props.user_id);
return { member };
} catch (e) {
log('RoomActions updateRoomMember', e);
return {};
}
}
updateRoom = () => {
this.setState({ room: this.room });
}
get canAddUser() { // Invite user
const {
rid, t
} = this.room;
const { allMembers } = this.state;
// TODO: same test joined
const userInRoom = !!allMembers.find(m => m.username === this.props.user_username);
const userInRoom = !!allMembers.find(m => m.username === this.props.username);
const permissions = RocketChat.hasPermission(['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], rid);
if (userInRoom && permissions['add-user-to-joined-room']) {
@ -149,8 +113,8 @@ export default class RoomActionsView extends LoggedView {
const sections = [{
data: [{
icon: 'ios-star',
name: 'USER',
route: 'RoomInfo',
name: I18n.t('Room_Info'),
route: 'RoomInfoView',
params: { rid },
testID: 'room-actions-info'
}],
@ -176,28 +140,28 @@ export default class RoomActionsView extends LoggedView {
{
icon: 'ios-attach',
name: I18n.t('Files'),
route: 'RoomFiles',
route: 'RoomFilesView',
params: { rid },
testID: 'room-actions-files'
},
{
icon: 'ios-at-outline',
name: I18n.t('Mentions'),
route: 'MentionedMessages',
route: 'MentionedMessagesView',
params: { rid },
testID: 'room-actions-mentioned'
},
{
icon: 'ios-star-outline',
name: I18n.t('Starred'),
route: 'StarredMessages',
route: 'StarredMessagesView',
params: { rid },
testID: 'room-actions-starred'
},
{
icon: 'ios-search',
name: I18n.t('Search'),
route: 'SearchMessages',
route: 'SearchMessagesView',
params: { rid },
testID: 'room-actions-search'
},
@ -210,14 +174,14 @@ export default class RoomActionsView extends LoggedView {
{
icon: 'ios-pin',
name: I18n.t('Pinned'),
route: 'PinnedMessages',
route: 'PinnedMessagesView',
params: { rid },
testID: 'room-actions-pinned'
},
{
icon: 'ios-code',
name: I18n.t('Snippets'),
route: 'SnippetedMessages',
route: 'SnippetedMessagesView',
params: { rid },
testID: 'room-actions-snippeted'
},
@ -254,7 +218,7 @@ export default class RoomActionsView extends LoggedView {
description: (onlineMembers.length === 1 ?
I18n.t('1_online_member') :
I18n.t('N_online_members', { n: onlineMembers.length })),
route: 'RoomMembers',
route: 'RoomMembersView',
params: { rid, members: onlineMembers },
testID: 'room-actions-members'
});
@ -264,19 +228,10 @@ export default class RoomActionsView extends LoggedView {
actions.push({
icon: 'ios-person-add',
name: I18n.t('Add_user'),
route: 'SelectedUsers',
route: 'SelectedUsersView',
params: {
nextAction: async() => {
try {
this.props.setLoadingInvite(true);
await RocketChat.addUsersToRoom(rid);
this.props.navigation.goBack();
} catch (e) {
log('RoomActions Add User', e);
} finally {
this.props.setLoadingInvite(false);
}
}
nextAction: 'ADD_USER',
rid
},
testID: 'room-actions-add-user'
});
@ -298,6 +253,46 @@ export default class RoomActionsView extends LoggedView {
return sections;
}
updateRoomMembers = async() => {
const { t } = this.state.room;
if (!this.canViewMembers) {
return {};
}
if (t === 'c' || t === 'p') {
let onlineMembers = [];
let allMembers = [];
try {
const onlineMembersCall = RocketChat.getRoomMembers(this.state.room.rid, false);
const allMembersCall = RocketChat.getRoomMembers(this.state.room.rid, true);
const [onlineMembersResult, allMembersResult] = await Promise.all([onlineMembersCall, allMembersCall]);
onlineMembers = onlineMembersResult.records;
allMembers = allMembersResult.records;
return { onlineMembers, allMembers };
} catch (error) {
return {};
}
}
}
updateRoomMember = async() => {
if (this.state.room.t !== 'd') {
return {};
}
try {
const member = await RocketChat.getRoomMember(this.state.room.rid, this.props.userId);
return { member };
} catch (e) {
log('RoomActions updateRoomMember', e);
return {};
}
}
updateRoom = () => {
this.setState({ room: this.room });
}
toggleBlockUser = async() => {
const { rid, blocker } = this.state.room;
const { member } = this.state;

View File

@ -10,25 +10,25 @@ import Message from '../../containers/message';
import RCActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n';
@connect(
state => ({
messages: state.roomFiles.messages,
ready: state.roomFiles.ready,
user: state.login.user
// baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}),
dispatch => ({
openRoomFiles: (rid, limit) => dispatch(openRoomFiles(rid, limit)),
closeRoomFiles: () => dispatch(closeRoomFiles())
})
)
@connect(state => ({
messages: state.roomFiles.messages,
ready: state.roomFiles.ready,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
}
}), dispatch => ({
openRoomFiles: (rid, limit) => dispatch(openRoomFiles(rid, limit)),
closeRoomFiles: () => dispatch(closeRoomFiles())
}))
/** @extends React.Component */
export default class RoomFilesView extends LoggedView {
static propTypes = {
navigation: PropTypes.object,
rid: PropTypes.string,
messages: PropTypes.array,
ready: PropTypes.bool,
user: PropTypes.object,
baseUrl: PropTypes.string,
openRoomFiles: PropTypes.func,
closeRoomFiles: PropTypes.func
}
@ -57,7 +57,7 @@ export default class RoomFilesView extends LoggedView {
}
load = () => {
this.props.openRoomFiles(this.props.navigation.state.params.rid, this.limit);
this.props.openRoomFiles(this.props.rid, this.limit);
}
moreData = () => {

View File

@ -34,18 +34,16 @@ const PERMISSIONS_ARRAY = [
PERMISSION_DELETE_P
];
@connect(null, dispatch => ({
eraseRoom: rid => dispatch(eraseRoom(rid))
}))
export default class RoomInfoEditView extends LoggedView {
/** @extends React.Component */
class RoomInfoEditView extends LoggedView {
static propTypes = {
navigation: PropTypes.object,
rid: PropTypes.string,
eraseRoom: PropTypes.func
};
constructor(props) {
super('RoomInfoEditView', props);
const { rid } = props.navigation.state.params;
const { rid } = props;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.permissions = {};
this.state = {
@ -400,3 +398,9 @@ export default class RoomInfoEditView extends LoggedView {
);
}
}
const mapDispatchToProps = dispatch => ({
eraseRoom: rid => dispatch(eraseRoom(rid))
});
export default connect(null, mapDispatchToProps)(RoomInfoEditView);

View File

@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, ScrollView } from 'react-native';
import { connect } from 'react-redux';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import moment from 'moment';
import LoggedView from '../View';
@ -12,11 +11,11 @@ import styles from './styles';
import sharedStyles from '../Styles';
import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat';
import Touch from '../../utils/touch';
import log from '../../utils/log';
import RoomTypeIcon from '../../containers/RoomTypeIcon';
import I18n from '../../i18n';
import { iconsMap } from '../../Icons';
const PERMISSION_EDIT_ROOM = 'edit-room';
@ -28,49 +27,28 @@ const getRoomTitle = room => (room.t === 'd' ?
<Text testID='room-info-view-name' style={styles.roomTitle} key='room-info-name'>{room.name}</Text>
]
);
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
user: state.login.user,
userId: state.login.user && state.login.user.id,
activeUsers: state.activeUsers,
Message_TimeFormat: state.settings.Message_TimeFormat,
roles: state.roles
}))
/** @extends React.Component */
export default class RoomInfoView extends LoggedView {
static propTypes = {
baseUrl: PropTypes.string,
user: PropTypes.object,
navigation: PropTypes.object,
navigator: PropTypes.object,
rid: PropTypes.string,
userId: PropTypes.string,
activeUsers: PropTypes.object,
Message_TimeFormat: PropTypes.string,
roles: PropTypes.object
}
static navigationOptions = ({ navigation }) => {
const params = navigation.state.params || {};
if (!params.hasEditPermission) {
return;
}
return {
headerRight: (
<Touch
onPress={() => navigation.navigate({ key: 'RoomInfoEdit', routeName: 'RoomInfoEdit', params: { rid: navigation.state.params.rid } })}
underlayColor='#ffffff'
activeOpacity={0.5}
accessibilityLabel={I18n.t('edit')}
accessibilityTraits='button'
testID='room-info-view-edit-button'
>
<View style={sharedStyles.headerButton}>
<MaterialIcon name='edit' size={20} />
</View>
</Touch>
)
};
};
constructor(props) {
super('RoomInfoView', props);
const { rid } = props.navigation.state.params;
const { rid } = props;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.sub = {
unsubscribe: () => {}
@ -80,6 +58,7 @@ export default class RoomInfoView extends LoggedView {
roomUser: {},
roles: []
};
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
async componentDidMount() {
@ -87,32 +66,42 @@ export default class RoomInfoView extends LoggedView {
this.rooms.addListener(this.updateRoom);
// get user of room
if (this.state.room.t === 'd') {
try {
const roomUser = await RocketChat.getRoomMember(this.state.room.rid, this.props.user.id);
this.setState({ roomUser });
const username = this.state.room.name;
if (this.state.room) {
if (this.state.room.t === 'd') {
try {
const roomUser = await RocketChat.getRoomMember(this.state.room.rid, this.props.userId);
this.setState({ roomUser });
const username = this.state.room.name;
const activeUser = this.props.activeUsers[roomUser._id];
if (!activeUser || !activeUser.utcOffset) {
// get full user data looking for utcOffset
// will be catched by .on('users) and saved on activeUsers reducer
this.getFullUserData(username);
}
const activeUser = this.props.activeUsers[roomUser._id];
if (!activeUser || !activeUser.utcOffset) {
// get full user data looking for utcOffset
// will be catched by .on('users) and saved on activeUsers reducer
this.getFullUserData(username);
}
// get all users roles
// needs to be changed by a better method
const allUsersRoles = await RocketChat.getUserRoles();
const userRoles = allUsersRoles.find(user => user.username === username);
if (userRoles) {
this.setState({ roles: userRoles.roles || [] });
// get all users roles
// needs to be changed by a better method
const allUsersRoles = await RocketChat.getUserRoles();
const userRoles = allUsersRoles.find(user => user.username === username);
if (userRoles) {
this.setState({ roles: userRoles.roles || [] });
}
} catch (e) {
log('RoomInfoView.componentDidMount', e);
}
} else {
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], this.state.room.rid);
if (permissions[PERMISSION_EDIT_ROOM]) {
this.props.navigator.setButtons({
rightButtons: [{
id: 'edit',
icon: iconsMap.create,
testID: 'room-info-view-edit-button'
}]
});
}
} catch (e) {
log('RoomInfoView.componentDidMount', e);
}
} else {
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], this.state.room.rid);
this.props.navigation.setParams({ hasEditPermission: permissions[PERMISSION_EDIT_ROOM] });
}
}
@ -121,6 +110,20 @@ export default class RoomInfoView extends LoggedView {
this.sub.unsubscribe();
}
onNavigatorEvent(event) {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'edit') {
this.props.navigator.push({
screen: 'RoomInfoEditView',
title: I18n.t('Room_Info_Edit'),
passProps: {
rid: this.props.rid
}
});
}
}
}
getFullUserData = async(username) => {
try {
const result = await RocketChat.subscribe('fullUserData', username);
@ -170,7 +173,6 @@ export default class RoomInfoView extends LoggedView {
if (!utcOffset) {
return null;
}
// TODO: translate
return (
<View style={styles.item}>
<Text style={styles.itemLabel}>{I18n.t('Timezone')}</Text>

View File

@ -1,57 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FlatList, Text, View, TextInput, Vibration, TouchableOpacity } from 'react-native';
import { FlatList, View, TextInput, Vibration } from 'react-native';
import { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet';
import LoggedView from '../View';
import styles from './styles';
import sharedStyles from '../Styles';
import RoomItem from '../../presentation/RoomItem';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import RocketChat from '../../lib/rocketchat';
import { goRoom } from '../../containers/routes/NavigationService';
import database from '../../lib/realm';
import { showToast } from '../../utils/info';
import log from '../../utils/log';
import I18n from '../../i18n';
@connect(state => ({
user: state.login.user,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class MentionedMessagesView extends LoggedView {
static propTypes = {
navigation: PropTypes.object
}
static navigationOptions = ({ navigation }) => {
const params = navigation.state.params || {};
const label = params.allUsers ? I18n.t('All') : I18n.t('Online');
if (params.allUsers === undefined) {
return;
}
return {
headerRight: (
<TouchableOpacity
onPress={params.onPressToogleStatus}
accessibilityLabel={label}
accessibilityTraits='button'
style={[sharedStyles.headerButton, styles.headerButton]}
testID='room-members-view-toggle-status'
>
<Text>{label}</Text>
</TouchableOpacity>
)
};
/** @extends React.Component */
export default class RoomMembersView extends LoggedView {
static navigatorButtons = {
rightButtons: [{
title: 'All',
id: 'toggleOnline',
testID: 'room-members-view-toggle-status'
}]
};
static propTypes = {
navigator: PropTypes.object,
rid: PropTypes.string,
members: PropTypes.array,
baseUrl: PropTypes.string
}
constructor(props) {
super('MentionedMessagesView', props);
this.CANCEL_INDEX = 0;
this.MUTE_INDEX = 1;
this.actionSheetOptions = [''];
const { rid, members } = props.navigation.state.params;
const { rid, members } = props;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.permissions = RocketChat.hasPermission(['mute-user'], rid);
this.state = {
@ -63,13 +52,10 @@ export default class MentionedMessagesView extends LoggedView {
userLongPressed: {},
room: {}
};
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
componentDidMount() {
this.props.navigation.setParams({
onPressToogleStatus: this.onPressToogleStatus,
allUsers: this.state.allUsers
});
this.rooms.addListener(this.updateRoom);
}
@ -77,9 +63,26 @@ export default class MentionedMessagesView extends LoggedView {
this.rooms.removeAllListeners();
}
updateRoom = async() => {
const [room] = this.rooms;
await this.setState({ room });
async onNavigatorEvent(event) {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'toggleOnline') {
try {
const allUsers = !this.state.allUsers;
const membersResult = await RocketChat.getRoomMembers(this.state.rid, allUsers);
const members = membersResult.records;
this.setState({ allUsers, members });
this.props.navigator.setButtons({
rightButtons: [{
title: this.state.allUsers ? 'Online' : 'All',
id: 'toggleOnline',
testID: 'room-members-view-toggle-status'
}]
});
} catch (e) {
log('RoomMembers.onNavigationButtonPressed', e);
}
}
}
}
onSearchChangeText = (text) => {
@ -90,26 +93,14 @@ export default class MentionedMessagesView extends LoggedView {
this.setState({ filtering: !!text, membersFiltered });
}
onPressToogleStatus = async() => {
try {
const allUsers = !this.state.allUsers;
this.props.navigation.setParams({ allUsers });
const membersResult = await RocketChat.getRoomMembers(this.state.rid, allUsers);
const members = membersResult.records;
this.setState({ allUsers, members });
} catch (e) {
log('onPressToogleStatus', e);
}
}
onPressUser = async(item) => {
try {
const subscriptions = database.objects('subscriptions').filtered('name = $0', item.username);
if (subscriptions.length) {
goRoom({ rid: subscriptions[0].rid, name: subscriptions[0].name });
this.goRoom({ rid: subscriptions[0].rid, name: subscriptions[0].name });
} else {
const room = await RocketChat.createDirectMessage(item.username);
goRoom({ rid: room.rid, name: item.username });
this.goRoom({ rid: room.rid, name: item.username });
}
} catch (e) {
log('onPressUser', e);
@ -134,6 +125,26 @@ export default class MentionedMessagesView extends LoggedView {
this.ActionSheet.show();
}
updateRoom = async() => {
const [room] = this.rooms;
await this.setState({ room });
}
goRoom = ({ rid, name }) => {
this.props.navigator.popToRoot();
setTimeout(() => {
this.props.navigator.push({
screen: 'RoomView',
title: name,
passProps: {
room: { rid, name },
rid,
name
}
});
}, 1000);
}
handleMute = async() => {
const { rid, userLongPressed } = this.state;
try {

View File

@ -1,218 +0,0 @@
import React from 'react';
import { Text, View, Platform, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { HeaderBackButton } from 'react-navigation';
import RocketChat from '../../../lib/rocketchat';
import realm from '../../../lib/realm';
import Avatar from '../../../containers/Avatar';
import { STATUS_COLORS } from '../../../constants/colors';
import styles from './styles';
import { closeRoom } from '../../../actions/room';
import log from '../../../utils/log';
import RoomTypeIcon from '../../../containers/RoomTypeIcon';
import I18n from '../../../i18n';
import sharedStyles from '../../Styles';
const title = (offline, connecting, authenticating, logged) => {
if (offline) {
return `${ I18n.t('You_are_offline') }...`;
}
if (connecting) {
return `${ I18n.t('Connecting') }...`;
}
if (authenticating) {
return `${ I18n.t('Authenticating') }...`;
}
if (logged) {
return null;
}
return `${ I18n.t('Not_logged') }...`;
};
@connect(state => ({
user: state.login.user,
activeUsers: state.activeUsers,
loading: state.messages.isFetching,
connecting: state.meteor.connecting,
authenticating: state.login.isFetching,
offline: !state.meteor.connected,
logged: !!state.login.token
}), dispatch => ({
close: () => dispatch(closeRoom())
}))
export default class RoomHeaderView extends React.PureComponent {
static propTypes = {
close: PropTypes.func.isRequired,
navigation: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
activeUsers: PropTypes.object
}
constructor(props) {
super(props);
this.state = {
room: props.navigation.state.params.room
};
this.room = realm.objects('subscriptions').filtered('rid = $0', this.state.room.rid);
}
componentDidMount() {
this.updateState();
this.room.addListener(this.updateState);
}
componentWillReceiveProps(nextProps) {
if (nextProps.navigation.state.params.room !== this.props.navigation.state.params.room) {
this.room.removeAllListeners();
this.room = realm.objects('subscriptions').filtered('rid = $0', nextProps.navigation.state.params.room.rid);
this.room.addListener(this.updateState);
}
}
componentWillUnmount() {
this.room.removeAllListeners();
}
getUserStatus() {
const userId = this.state.room.rid.replace(this.props.user.id, '').trim();
const userInfo = this.props.activeUsers[userId];
return (userInfo && userInfo.status) || 'offline';
}
getUserStatusLabel() {
const status = this.getUserStatus();
return I18n.t(status.charAt(0).toUpperCase() + status.slice(1));
}
updateState = () => {
if (this.room.length > 0) {
this.setState({ room: this.room[0] });
}
};
isDirect = () => this.state.room && this.state.room.t === 'd';
renderLeft = () => (<HeaderBackButton
onPress={() => {
this.props.navigation.goBack(null);
requestAnimationFrame(() => this.props.close());
}}
tintColor='#292E35'
title={I18n.t('Back')}
titleStyle={{ display: 'none' }}
/>);
renderCenter() {
if (!this.state.room.name) {
return <View style={styles.titleContainer} />;
}
let accessibilityLabel = this.state.room.name;
if (this.isDirect()) {
accessibilityLabel += `, ${ this.getUserStatusLabel() }`;
}
const {
offline, connecting, authenticating, logged, loading
} = this.props;
let t = '';
if (!title(offline, connecting, authenticating, logged) && loading) {
t = I18n.t('Loading_messages_ellipsis');
} else if (this.isDirect()) {
t = this.getUserStatusLabel();
} else {
t = this.state.room.topic || ' ';
}
return (
<TouchableOpacity
style={styles.titleContainer}
accessibilityLabel={accessibilityLabel}
accessibilityTraits='header'
onPress={() => this.props.navigation.navigate({ key: 'RoomInfo', routeName: 'RoomInfo', params: { rid: this.state.room.rid } })}
testID='room-view-header-title'
>
<Avatar
text={this.state.room.name}
size={24}
style={styles.avatar}
type={this.state.room.t}
>
{this.isDirect() ?
<View style={[styles.status, { backgroundColor: STATUS_COLORS[this.getUserStatus()] }]} />
: null
}
</Avatar>
<View style={styles.titleTextContainer}>
<View style={{ flexDirection: 'row' }}>
<RoomTypeIcon type={this.state.room.t} size={13} />
<Text style={styles.title} allowFontScaling={false} testID='room-view-title'>
{this.state.room.name}
</Text>
</View>
{ t ? <Text style={styles.userStatus} allowFontScaling={false} numberOfLines={1}>{t}</Text> : null}
</View>
</TouchableOpacity>
);
}
renderRight = () => (
<View style={styles.right}>
<TouchableOpacity
style={sharedStyles.headerButton}
onPress={() => {
try {
RocketChat.toggleFavorite(this.state.room.rid, this.state.room.f);
} catch (e) {
log('toggleFavorite', e);
}
}}
accessibilityLabel={I18n.t('Star_room')}
accessibilityTraits='button'
testID='room-view-header-star'
>
<Icon
name={`${ Platform.OS === 'ios' ? 'ios' : 'md' }-star${ this.state.room.f ? '' : '-outline' }`}
color='#f6c502'
size={24}
backgroundColor='transparent'
/>
</TouchableOpacity>
<TouchableOpacity
style={sharedStyles.headerButton}
onPress={() => this.props.navigation.navigate({ key: 'RoomActions', routeName: 'RoomActions', params: { rid: this.state.room.rid } })}
accessibilityLabel={I18n.t('Room_actions')}
accessibilityTraits='button'
testID='room-view-header-actions'
>
<Icon
name={Platform.OS === 'ios' ? 'ios-more' : 'md-more'}
color='#292E35'
size={24}
backgroundColor='transparent'
/>
</TouchableOpacity>
</View>
);
render() {
return (
<View style={styles.header} testID='room-view-header'>
{this.renderLeft()}
{this.renderCenter()}
{this.renderRight()}
</View>
);
}
}

View File

@ -1,46 +0,0 @@
import { StyleSheet, Platform } from 'react-native';
export default StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
flex: 1
},
titleContainer: {
alignItems: 'center',
justifyContent: 'flex-start',
flexDirection: 'row',
flex: 1,
marginLeft: Platform.OS === 'ios' ? 18 : 0,
height: 44
},
titleTextContainer: {
flexDirection: 'column',
justifyContent: 'flex-start',
flex: 1
},
status: {
borderRadius: 10,
width: 10,
height: 10,
position: 'absolute',
borderWidth: 2,
borderColor: '#fff',
bottom: -2,
right: -2
},
userStatus: {
fontSize: 10,
color: '#888'
},
title: {
fontWeight: '500',
color: '#292E35'
},
right: {
flexDirection: 'row'
},
avatar: {
marginRight: 5
}
});

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