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, "prefer-const": 2,
"object-shorthand": 2, "object-shorthand": 2,
"consistent-return": 0, "consistent-return": 0,
"global-require": "off", "global-require": "off"
"react/prop-types": [0, { skipUndeclared: true }]
}, },
"globals": { "globals": {
"__DEV__": true "__DEV__": true

22
.snyk
View File

@ -1,5 +1,5 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.7.1 version: v1.12.0
ignore: {} ignore: {}
# patches apply the minimum changes required to fix a vulnerability # patches apply the minimum changes required to fix a vulnerability
patch: patch:
@ -24,3 +24,23 @@ patch:
patched: '2017-09-29T23:29:20.238Z' patched: '2017-09-29T23:29:20.238Z'
- realm > extract-zip > debug: - realm > extract-zip > debug:
patched: '2017-09-29T23:29:20.238Z' 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, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }
@ -868,7 +868,7 @@ exports[`render unread 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }
@ -1118,7 +1118,7 @@ exports[`renders correctly 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }

View File

@ -426,7 +426,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }
@ -648,7 +648,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }
@ -874,7 +874,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }
@ -1119,7 +1119,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }
@ -1368,7 +1368,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }
@ -1613,7 +1613,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }
@ -1858,7 +1858,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }
@ -2103,7 +2103,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }
@ -2348,7 +2348,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }
@ -2570,7 +2570,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }
@ -2792,7 +2792,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
undefined, undefined,
], ],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": undefined,
}, },
] ]
} }

View File

@ -95,13 +95,19 @@ android {
defaultConfig { defaultConfig {
applicationId "chat.rocket.reactnative" applicationId "chat.rocket.reactnative"
minSdkVersion 16 minSdkVersion 19
targetSdkVersion 27 targetSdkVersion 27
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "1" versionName "1"
ndk { ndk {
abiFilters "armeabi-v7a", "x86" abiFilters "armeabi-v7a", "x86"
} }
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative55"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
} }
signingConfigs { signingConfigs {
@ -172,29 +178,30 @@ repositories {
} }
dependencies { dependencies {
compile project(':react-native-i18n') implementation project(':react-native-i18n')
compile project(':react-native-fabric') implementation project(':react-native-fabric')
compile project(':react-native-audio') implementation project(':react-native-audio')
compile project(":reactnativekeyboardinput") implementation project(":reactnativekeyboardinput")
compile project(':react-native-splash-screen') implementation project(':react-native-video')
compile project(':react-native-video') implementation project(':react-native-svg')
compile project(':react-native-push-notification') implementation project(':react-native-image-picker')
compile project(':react-native-svg') implementation project(':react-native-vector-icons')
compile project(':react-native-image-picker') implementation project(':react-native-fetch-blob')
compile project(':react-native-vector-icons') implementation project(':react-native-zeroconf')
compile project(':react-native-fetch-blob') implementation project(':react-native-toast')
compile project(':react-native-zeroconf') implementation project(':react-native-fast-image')
compile project(':react-native-toast') implementation project(':realm')
compile project(':react-native-fast-image') implementation project(':react-native-navigation')
compile project(':realm') implementation project(':reactnativenotifications')
compile fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:27.1.0" implementation "com.android.support:appcompat-v7:27.1.0"
compile "com.android.support:support-v4:27.1.0" implementation "com.android.support:support-v4:27.1.0"
compile 'com.android.support:customtabs:27.1.0' implementation 'com.android.support:customtabs:27.1.0'
compile "com.facebook.react:react-native:+" // From node_modules implementation 'com.android.support:design:27.1.0'
compile 'com.facebook.fresco:fresco:1.7.1' implementation "com.facebook.react:react-native:+" // From node_modules
compile 'com.facebook.fresco:animated-gif:1.7.1' implementation 'com.facebook.fresco:fresco:1.7.1'
compile('com.crashlytics.sdk.android:crashlytics:2.9.2@aar') { implementation 'com.facebook.fresco:animated-gif:1.7.1'
implementation('com.crashlytics.sdk.android:crashlytics:2.9.2@aar') {
transitive = true; transitive = true;
} }
} }

View File

@ -10,7 +10,7 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> --> <!-- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> -->
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
<permission <permission
android:name="${applicationId}.permission.C2D_MESSAGE" android:name="${applicationId}.permission.C2D_MESSAGE"
@ -50,27 +50,7 @@
</activity> </activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" /> <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="673693445664\0"/>
<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> </application>
</manifest> </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; package chat.rocket.reactnative;
import android.os.Bundle; import android.graphics.drawable.Drawable;
import com.facebook.react.ReactActivity; import android.support.v4.content.ContextCompat;
import org.devio.rn.splashscreen.SplashScreen; import android.widget.LinearLayout;
import com.crashlytics.android.Crashlytics; import com.reactnativenavigation.controllers.SplashActivity;
import io.fabric.sdk.android.Fabric;
public class MainActivity extends ReactActivity { public class MainActivity extends SplashActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override @Override
protected String getMainComponentName() { public LinearLayout createSplashLayout() {
return "RocketChatRN"; LinearLayout splash = new LinearLayout(this);
} Drawable launch_screen_bitmap = ContextCompat.getDrawable(getApplicationContext(),R.drawable.launch_screen_bitmap);
splash.setBackground(launch_screen_bitmap);
@Override return splash;
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.show(this);
super.onCreate(savedInstanceState);
Fabric.with(this, new Crashlytics());
} }
} }

View File

@ -1,41 +1,60 @@
package chat.rocket.reactnative; 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.horcrux.svg.SvgPackage;
import com.imagepicker.ImagePickerPackage; import com.imagepicker.ImagePickerPackage;
import com.oblador.vectoricons.VectorIconsPackage; import com.oblador.vectoricons.VectorIconsPackage;
import com.RNFetchBlob.RNFetchBlobPackage; import com.reactnativenavigation.NavigationApplication;
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.remobile.toast.RCTToastPackage; import com.remobile.toast.RCTToastPackage;
import com.wix.reactnativekeyboardinput.KeyboardInputPackage;
import com.rnim.rn.audio.ReactNativeAudioPackage; import com.rnim.rn.audio.ReactNativeAudioPackage;
import com.smixx.fabric.FabricPackage; import com.smixx.fabric.FabricPackage;
import com.dylanvann.fastimage.FastImageViewPackage; import com.wix.reactnativekeyboardinput.KeyboardInputPackage;
import com.AlexanderZaytsev.RNI18n.RNI18nPackage; 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.Arrays;
import java.util.List; 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 @Override
public boolean getUseDeveloperSupport() { public boolean isDebug() {
return BuildConfig.DEBUG; return BuildConfig.DEBUG;
} }
@Override @Override
public String getJSMainModuleName() {
return "index";
}
protected List<ReactPackage> getPackages() { protected List<ReactPackage> getPackages() {
// Add additional packages you require here
// No need to add RnnPackage and MainReactPackage
return Arrays.<ReactPackage>asList(
);
}
@Override
public List<ReactPackage> createAdditionalReactPackages() {
return Arrays.<ReactPackage>asList( return Arrays.<ReactPackage>asList(
new MainReactPackage(), new MainReactPackage(),
new SvgPackage(), new SvgPackage(),
@ -44,28 +63,37 @@ public class MainApplication extends Application implements ReactApplication {
new RNFetchBlobPackage(), new RNFetchBlobPackage(),
new ZeroconfReactPackage(), new ZeroconfReactPackage(),
new RealmReactPackage(), new RealmReactPackage(),
new ReactNativePushNotificationPackage(),
new ReactVideoPackage(), new ReactVideoPackage(),
new SplashScreenReactPackage(),
new RCTToastPackage(), new RCTToastPackage(),
new ReactNativeAudioPackage(), new ReactNativeAudioPackage(),
new KeyboardInputPackage(MainApplication.this), new KeyboardInputPackage(MainApplication.this),
new RocketChatNativePackage(), new RocketChatNativePackage(),
new FabricPackage(), new FabricPackage(),
new FastImageViewPackage(), new FastImageViewPackage(),
new RNI18nPackage() new RNI18nPackage(),
new RNNotificationsPackage(MainApplication.this)
); );
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
} }
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
SoLoader.init(this, /* native exopackage */ false); 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 { buildscript {
repositories { repositories {
jcenter()
google() google()
mavenLocal()
mavenCentral()
jcenter()
} }
dependencies { dependencies {
// classpath 'com.android.tools.build:gradle:2.2.3' // classpath 'com.android.tools.build:gradle:2.2.3'
@ -17,6 +19,7 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
mavenLocal() mavenLocal()
mavenCentral()
jcenter() jcenter()
google() google()
maven { 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') project(':react-native-audio').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-audio/android')
include ':reactnativekeyboardinput' include ':reactnativekeyboardinput'
project(':reactnativekeyboardinput').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keyboard-input/lib/android') 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' include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android') project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
include ':react-native-svg' 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') project(':react-native-zeroconf').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-zeroconf/android')
include ':realm' include ':realm'
project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android') 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' include ':react-native-toast'
project(':react-native-toast').projectDir = new File(settingsDir, '../node_modules/@remobile/react-native-toast/android') 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' 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 */ /* eslint-disable */
import { NativeModules } from 'react-native';
import Reactotron from 'reactotron-react-native'; import Reactotron from 'reactotron-react-native';
import { reactotronRedux } from 'reactotron-redux'; import { reactotronRedux } from 'reactotron-redux';
import sagaPlugin from 'reactotron-redux-saga' import sagaPlugin from 'reactotron-redux-saga'
if (__DEV__) { if (__DEV__) {
const scriptURL = NativeModules.SourceCode.scriptURL;
const scriptHostname = scriptURL.split('://')[1].split(':')[0];
Reactotron Reactotron
.configure() .configure({ host: scriptHostname })
.useReactNative() .useReactNative()
.use(reactotronRedux()) .use(reactotronRedux())
.use(sagaPlugin()) .use(sagaPlugin())

View File

@ -44,7 +44,7 @@ export const ROOM = createRequestTypes('ROOM', [
'MESSAGE_RECEIVED', 'MESSAGE_RECEIVED',
'SET_LAST_OPEN' 'SET_LAST_OPEN'
]); ]);
export const APP = createRequestTypes('APP', ['READY', 'INIT']); export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT']);
export const MESSAGES = createRequestTypes('MESSAGES', [ export const MESSAGES = createRequestTypes('MESSAGES', [
...defaultTypes, ...defaultTypes,
'ACTIONS_SHOW', 'ACTIONS_SHOW',

View File

@ -1,6 +1,13 @@
import * as types from '../constants/types'; import * as types from '../constants/types';
import { APP } from './actionsTypes'; import { APP } from './actionsTypes';
export function appStart(root) {
return {
type: APP.START,
root
};
}
export function appReady() { export function appReady() {
return { return {
type: APP.READY type: APP.READY
@ -12,6 +19,7 @@ export function appInit() {
type: APP.INIT type: APP.INIT
}; };
} }
export function setCurrentServer(server) { export function setCurrentServer(server) {
return { return {
type: types.SET_CURRENT_SERVER, type: types.SET_CURRENT_SERVER,

View File

@ -1,6 +1,6 @@
import { SERVER } from './actionsTypes'; import { SERVER } from './actionsTypes';
export function setServer(server) { export function selectServer(server) {
return { return {
type: SERVER.SELECT, type: SERVER.SELECT,
server 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( @connect(
state => ({ state => ({
actionMessage: state.messages.actionMessage, actionMessage: state.messages.actionMessage,
user: state.login.user,
Message_AllowDeleting: state.settings.Message_AllowDeleting, Message_AllowDeleting: state.settings.Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes, Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing: state.settings.Message_AllowEditing, Message_AllowEditing: state.settings.Message_AllowEditing,
@ -42,9 +41,9 @@ import I18n from '../i18n';
export default class MessageActions extends React.Component { export default class MessageActions extends React.Component {
static propTypes = { static propTypes = {
actionsHide: PropTypes.func.isRequired, actionsHide: PropTypes.func.isRequired,
room: PropTypes.object, room: PropTypes.object.isRequired,
actionMessage: PropTypes.object, actionMessage: PropTypes.object,
user: PropTypes.object, user: PropTypes.object.isRequired,
deleteRequest: PropTypes.func.isRequired, deleteRequest: PropTypes.func.isRequired,
editInit: PropTypes.func.isRequired, editInit: PropTypes.func.isRequired,
toggleStarRequest: 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 React, { Component } from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import { DrawerActions, SafeAreaView } from 'react-navigation';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import database from '../lib/realm'; import database from '../lib/realm';
import { setServer } from '../actions/server'; import { selectServer } from '../actions/server';
import { logout } from '../actions/login'; import { logout } from '../actions/login';
import { appStart } from '../actions';
import Avatar from '../containers/Avatar'; import Avatar from '../containers/Avatar';
import Status from '../containers/status'; import Status from '../containers/status';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
@ -16,6 +16,7 @@ import { STATUS_COLORS } from '../constants/colors';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import log from '../utils/log'; import log from '../utils/log';
import I18n from '../i18n'; import I18n from '../i18n';
import { NavigationActions } from '../Navigation';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
selected: { selected: {
@ -76,19 +77,29 @@ const styles = StyleSheet.create({
} }
}); });
const keyExtractor = item => item.id; const keyExtractor = item => item.id;
@connect(state => ({ @connect(state => ({
server: state.server.server, 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 => ({ }), dispatch => ({
selectServer: server => dispatch(setServer(server)), selectServer: server => dispatch(selectServer(server)),
logout: () => dispatch(logout()) logout: () => dispatch(logout()),
appStart: () => dispatch(appStart('outside'))
})) }))
export default class Sidebar extends Component { export default class Sidebar extends Component {
static propTypes = { static propTypes = {
navigator: PropTypes.object,
server: PropTypes.string.isRequired, server: PropTypes.string.isRequired,
selectServer: PropTypes.func.isRequired, selectServer: PropTypes.func.isRequired,
navigation: PropTypes.object.isRequired, user: PropTypes.object,
logout: PropTypes.func.isRequired logout: PropTypes.func.isRequired,
appStart: PropTypes.func
} }
constructor(props) { constructor(props) {
@ -117,7 +128,6 @@ export default class Sidebar extends Component {
onPressItem = (item) => { onPressItem = (item) => {
this.props.selectServer(item.id); this.props.selectServer(item.id);
this.closeDrawer();
} }
setStatus = () => { setStatus = () => {
@ -149,7 +159,11 @@ export default class Sidebar extends Component {
} }
closeDrawer = () => { closeDrawer = () => {
this.props.navigation.dispatch(DrawerActions.closeDrawer()); this.props.navigator.toggleDrawer({
side: 'left',
animated: true,
to: 'close'
});
} }
toggleServers = () => { toggleServers = () => {
@ -157,25 +171,15 @@ export default class Sidebar extends Component {
this.setState({ showServers: !this.state.showServers }); this.setState({ showServers: !this.state.showServers });
} }
isRouteFocused = (route) => { sidebarNavigate = (screen, title) => {
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(); this.closeDrawer();
} NavigationActions.resetTo({ screen, title });
} }
renderSeparator = key => <View key={key} style={styles.separator} />; renderSeparator = key => <View key={key} style={styles.separator} />;
renderItem = ({ renderItem = ({
text, left, selected, onPress, testID text, left, onPress, testID
}) => ( }) => (
<Touch <Touch
key={text} key={text}
@ -184,8 +188,8 @@ export default class Sidebar extends Component {
activeOpacity={0.3} activeOpacity={0.3}
testID={testID} testID={testID}
> >
<View style={[styles.item, selected && styles.selected]}> <View style={styles.item}>
<View style={[styles.itemLeft, !selected && styles.itemLeftOpacity]}> <View style={styles.itemLeft}>
{left} {left}
</View> </View>
<Text style={styles.itemText}> <Text style={styles.itemText}>
@ -222,12 +226,15 @@ export default class Sidebar extends Component {
source={{ uri: encodeURI(`${ item.id }/assets/favicon_32.png`) }} source={{ uri: encodeURI(`${ item.id }/assets/favicon_32.png`) }}
/>, />,
selected: this.props.server === item.id, selected: this.props.server === item.id,
onPress: () => { onPress: async() => {
this.closeDrawer(); this.closeDrawer();
this.toggleServers(); this.toggleServers();
if (this.props.server !== item.id) { if (this.props.server !== item.id) {
this.props.selectServer(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 }` testID: `sidebar-${ item.id }`
@ -239,31 +246,25 @@ export default class Sidebar extends Component {
this.renderItem({ this.renderItem({
text: I18n.t('Chats'), text: I18n.t('Chats'),
left: <Icon name='chat-bubble' size={20} />, left: <Icon name='chat-bubble' size={20} />,
onPress: () => this.sidebarNavigate('Chats'), onPress: () => this.sidebarNavigate('RoomsListView', I18n.t('Messages')),
selected: this.isRouteFocused('Chats'),
testID: 'sidebar-chats' testID: 'sidebar-chats'
}), }),
this.renderItem({ this.renderItem({
text: I18n.t('Profile'), text: I18n.t('Profile'),
left: <Icon name='person' size={20} />, left: <Icon name='person' size={20} />,
onPress: () => this.sidebarNavigate('ProfileView'), onPress: () => this.sidebarNavigate('ProfileView', I18n.t('Profile')),
selected: this.isRouteFocused('ProfileView'),
testID: 'sidebar-profile' testID: 'sidebar-profile'
}), }),
this.renderItem({ this.renderItem({
text: I18n.t('Settings'), text: I18n.t('Settings'),
left: <Icon name='settings' size={20} />, left: <Icon name='settings' size={20} />,
onPress: () => this.sidebarNavigate('SettingsView'), onPress: () => this.sidebarNavigate('SettingsView', I18n.t('Settings')),
selected: this.isRouteFocused('SettingsView'),
testID: 'sidebar-settings' testID: 'sidebar-settings'
}), }),
this.renderSeparator('separator-logout'), this.renderSeparator('separator-logout'),
this.renderItem({ this.renderItem({
text: I18n.t('Logout'), text: I18n.t('Logout'),
left: <Icon left: <Icon name='exit-to-app' size={20} />,
name='exit-to-app'
size={20}
/>,
onPress: () => this.props.logout(), onPress: () => this.props.logout(),
testID: 'sidebar-logout' testID: 'sidebar-logout'
}) })
@ -297,7 +298,10 @@ export default class Sidebar extends Component {
onPress: () => { onPress: () => {
this.closeDrawer(); this.closeDrawer();
this.toggleServers(); this.toggleServers();
this.props.navigation.navigate('AddServer'); NavigationActions.push({
screen: 'NewServerView',
title: I18n.t('Add_Server')
});
}, },
testID: 'sidebar-add-server' testID: 'sidebar-add-server'
}) })
@ -306,13 +310,12 @@ export default class Sidebar extends Component {
render() { render() {
const { user, server } = this.props; const { user, server } = this.props;
if (!user) {
return null;
}
return ( return (
<ScrollView> <ScrollView style={{ backgroundColor: '#fff' }}>
<SafeAreaView <SafeAreaView testID='sidebar'>
style={styles.container}
forceInset={{ top: 'always', horizontal: 'never' }}
testID='sidebar'
>
<Touch <Touch
onPress={() => this.toggleServers()} onPress={() => this.toggleServers()}
underlayColor='rgba(255, 255, 255, 0.5)' underlayColor='rgba(255, 255, 255, 0.5)'
@ -335,6 +338,7 @@ export default class Sidebar extends Component {
<Icon <Icon
name={this.state.showServers ? 'keyboard-arrow-up' : 'keyboard-arrow-down'} name={this.state.showServers ? 'keyboard-arrow-up' : 'keyboard-arrow-down'}
size={30} size={30}
style={{ paddingHorizontal: 10 }}
/> />
</View> </View>
</Touch> </Touch>

View File

@ -66,7 +66,10 @@ export default class RCTextInput extends React.PureComponent {
secureTextEntry: PropTypes.bool, secureTextEntry: PropTypes.bool,
containerStyle: ViewPropTypes.style, containerStyle: ViewPropTypes.style,
inputStyle: PropTypes.object, inputStyle: PropTypes.object,
inputRef: PropTypes.func inputRef: PropTypes.func,
testID: PropTypes.string,
iconLeft: PropTypes.string,
placeholder: PropTypes.string
} }
static defaultProps = { static defaultProps = {
error: {} error: {}

View File

@ -28,7 +28,8 @@ export default class extends React.PureComponent {
static propTypes = { static propTypes = {
file: PropTypes.object.isRequired, file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired user: PropTypes.object.isRequired,
customEmojis: PropTypes.object
} }
state = { modalVisible: false }; state = { modalVisible: false };

View File

@ -99,7 +99,11 @@ export default class Message extends React.Component {
Message_GroupingPeriod: PropTypes.number.isRequired, Message_GroupingPeriod: PropTypes.number.isRequired,
customTimeFormat: PropTypes.string, customTimeFormat: PropTypes.string,
message: PropTypes.object.isRequired, 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, editing: PropTypes.bool,
errorActionsShow: PropTypes.func, errorActionsShow: PropTypes.func,
toggleReactionPicker: PropTypes.func, toggleReactionPicker: PropTypes.func,
@ -109,7 +113,8 @@ export default class Message extends React.Component {
onLongPress: PropTypes.func, onLongPress: PropTypes.func,
_updatedAt: PropTypes.instanceOf(Date), _updatedAt: PropTypes.instanceOf(Date),
archived: PropTypes.bool, archived: PropTypes.bool,
broadcast: PropTypes.bool broadcast: PropTypes.bool,
previousItem: PropTypes.object
} }
static defaultProps = { 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 => ({ @connect((state, ownProps) => {
activeUsers: state.activeUsers, if (state.login.user && ownProps.id === state.login.user.id) {
user: state.login.user, return {
status: state.login.user && state.login.user.status,
offline: !state.meteor.connected 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 = { static propTypes = {
style: ViewPropTypes.style, style: ViewPropTypes.style,
id: PropTypes.string, status: PropTypes.string,
activeUsers: PropTypes.object,
user: PropTypes.object,
offline: PropTypes.bool 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() { get status() {
const { id: userId, user, offline } = this.props; const { offline, status } = this.props;
if (user.id === userId) {
if (offline) { if (offline) {
return 'offline'; return 'offline';
} }
return user.status || 'offline'; return status;
}
return (this.props.activeUsers && this.props.activeUsers[userId] && this.props.activeUsers[userId].status) || 'offline';
} }
render() { render() {

View File

@ -109,6 +109,7 @@ export default {
changing_avatar: 'changing avatar', changing_avatar: 'changing avatar',
Channel_Name: 'Channel Name', Channel_Name: 'Channel Name',
Chats: 'Chats', Chats: 'Chats',
Close: 'Close',
Close_emoji_selector: 'Close emoji selector', Close_emoji_selector: 'Close emoji selector',
Code: 'Code', Code: 'Code',
Colaborative: 'Colaborative', Colaborative: 'Colaborative',
@ -141,6 +142,7 @@ export default {
Forgot_my_password: 'Forgot my password', 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_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',
Forgot_Password: 'Forgot Password',
Has_joined_the_channel: 'Has joined the channel', Has_joined_the_channel: 'Has joined the channel',
Has_left_the_channel: 'Has left the channel', Has_left_the_channel: 'Has left the channel',
I_have_an_account: 'I have an account', I_have_an_account: 'I have an account',
@ -164,6 +166,7 @@ export default {
Message_actions: 'Message actions', Message_actions: 'Message actions',
Message_pinned: 'Message pinned', Message_pinned: 'Message pinned',
Message_removed: 'Message removed', Message_removed: 'Message removed',
Messages: 'Messages',
Microphone_Permission_Message: 'Rocket Chat needs access to your microphone so you can send audio message.', Microphone_Permission_Message: 'Rocket Chat needs access to your microphone so you can send audio message.',
Microphone_Permission: 'Microphone Permission', Microphone_Permission: 'Microphone Permission',
Mute: 'Mute', Mute: 'Mute',
@ -294,6 +297,7 @@ export default {
Validating: 'Validating', Validating: 'Validating',
Video_call: 'Video call', Video_call: 'Video call',
Voice_call: 'Voice call', Voice_call: 'Voice call',
Welcome: 'Welcome',
Welcome_title_pt_1: 'Prepare to take off with', Welcome_title_pt_1: 'Prepare to take off with',
Welcome_title_pt_2: 'the ultimate chat platform', Welcome_title_pt_2: 'the ultimate chat platform',
Yes_action_it: 'Yes, {{action}} it!', Yes_action_it: 'Yes, {{action}} it!',

View File

@ -1,14 +1,94 @@
import React from 'react'; import { Component } from 'react';
import { Provider } from 'react-redux'; import { Linking } from 'react-native';
import { Navigation } from 'react-native-navigation';
import store from './lib/createStore'; import store from './lib/createStore';
import { appInit } from './actions';
import database from './lib/realm';
import { iconsLoaded } from './Icons';
import { registerScreens } from './views';
import { deepLinkingOpen } from './actions/deepLinking';
import parseQuery from './lib/methods/helpers/parseQuery';
import I18n from './i18n';
import { initializePushNotifications } from './push';
import Routes from './containers/Routes'; const startLogged = () => {
Navigation.startSingleScreenApp({
screen: {
screen: 'RoomsListView',
title: I18n.t('Messages')
},
drawer: {
left: {
screen: 'Sidebar'
}
},
animationType: 'fade'
});
};
const RocketChat = () => ( const startNotLogged = (route) => {
<Provider store={store}> Navigation.startSingleScreenApp({
<Routes /> screen: {
</Provider> 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 */ /* eslint-disable global-require */
const reduxImmutableStateInvariant = require('redux-immutable-state-invariant').default(); const reduxImmutableStateInvariant = require('redux-immutable-state-invariant').default();
sagaMiddleware = createSagaMiddleware({ sagaMiddleware = createSagaMiddleware({
sagaMonitor: Reactotron.createSagaMonitor() // sagaMonitor: Reactotron.createSagaMonitor()
}); });
enhancers = compose( enhancers = compose(

View File

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

View File

@ -139,7 +139,7 @@ const attachment = {
video_url: { type: 'string', optional: true }, video_url: { type: 'string', optional: true },
title: { type: 'string', optional: true }, title: { type: 'string', optional: true },
title_link: { 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 }, type: { type: 'string', optional: true },
author_icon: { type: 'string', optional: true }, author_icon: { type: 'string', optional: true },
author_name: { 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 sendMessage, { getMessage, _sendMessageCall } from './methods/sendMessage';
import { getDeviceToken } from '../push';
const TOKEN_KEY = 'reactnativemeteor_usertoken'; const TOKEN_KEY = 'reactnativemeteor_usertoken';
const call = (method, ...params) => RocketChat.ddp.call(method, ...params); // eslint-disable-line const call = (method, ...params) => RocketChat.ddp.call(method, ...params); // eslint-disable-line
const returnAnArray = obj => obj || []; const returnAnArray = obj => obj || [];
@ -119,7 +121,7 @@ const RocketChat = {
reduxStore.dispatch(setActiveUser(this.activeUsers)); reduxStore.dispatch(setActiveUser(this.activeUsers));
this._setUserTimer = null; this._setUserTimer = null;
return this.activeUsers = {}; return this.activeUsers = {};
}, 5000); }, 2000);
const activeUser = reduxStore.getState().activeUsers[ddpMessage.id]; const activeUser = reduxStore.getState().activeUsers[ddpMessage.id];
if (!ddpMessage.fields) { if (!ddpMessage.fields) {
@ -175,8 +177,8 @@ const RocketChat = {
this.ddp.on('disconnected', () => console.log('disconnected')); this.ddp.on('disconnected', () => console.log('disconnected'));
this.ddp.on('logged', protectedFunction((user) => { this.ddp.on('logged', protectedFunction((user) => {
this.getRooms().catch(e => log('logged getRooms', e));
this.loginSuccess(user); this.loginSuccess(user);
this.getRooms().catch(e => log('logged getRooms', e));
})); }));
this.ddp.once('logged', protectedFunction(({ id }) => { this.ddp.once('logged', protectedFunction(({ id }) => {
this.subscribeRooms(id); this.subscribeRooms(id);
@ -556,21 +558,24 @@ const RocketChat = {
AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`); AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`);
}, },
registerPushToken(id, token) { registerPushToken(userId) {
const deviceToken = getDeviceToken();
if (deviceToken) {
const key = Platform.OS === 'ios' ? 'apn' : 'gcm'; const key = Platform.OS === 'ios' ? 'apn' : 'gcm';
const data = { const data = {
id: `RocketChatRN${ id }`, id: `RocketChatRN${ userId }`,
token: { [key]: token }, token: { [key]: deviceToken },
appName: 'chat.rocket.reactnative', // TODO: try to get from config file appName: 'chat.rocket.reactnative', // TODO: try to get from config file
userId: id, userId,
metadata: {} metadata: {}
}; };
return call('raix:push-update', data); return call('raix:push-update', data);
}
}, },
updatePushToken(pushId) { // updatePushToken(pushId) {
return call('raix:push-setuser', pushId); // return call('raix:push-setuser', pushId);
}, // },
loadMissedMessages, loadMissedMessages,
loadMessagesForRoom, loadMessagesForRoom,
getMessage, getMessage,

View File

@ -121,7 +121,7 @@ const renderNumber = (unread, userMentions) => {
const attrs = ['name', 'unread', 'userMentions', 'alert', 'showLastMessage', 'type']; const attrs = ['name', 'unread', 'userMentions', 'alert', 'showLastMessage', 'type'];
@connect(state => ({ @connect(state => ({
user: state.login && state.login.user, username: state.login.user && state.login.user.username,
StoreLastMessage: state.settings.Store_Last_Message StoreLastMessage: state.settings.Store_Last_Message
})) }))
export default class RoomItem extends React.Component { export default class RoomItem extends React.Component {
@ -139,7 +139,7 @@ export default class RoomItem extends React.Component {
id: PropTypes.string, id: PropTypes.string,
onPress: PropTypes.func, onPress: PropTypes.func,
onLongPress: PropTypes.func, onLongPress: PropTypes.func,
user: PropTypes.object, username: PropTypes.string,
avatarSize: PropTypes.number, avatarSize: PropTypes.number,
statusStyle: ViewPropTypes.style, statusStyle: ViewPropTypes.style,
testID: PropTypes.string testID: PropTypes.string
@ -182,7 +182,7 @@ export default class RoomItem extends React.Component {
let prefix = ''; let prefix = '';
if (lastMessage.u.username === this.props.user.username) { if (lastMessage.u.username === this.props.username) {
prefix = I18n.t('You_colon'); prefix = I18n.t('You_colon');
} else if (type !== 'd') { } else if (type !== 'd') {
prefix = `${ lastMessage.u.username }: `; prefix = `${ lastMessage.u.username }: `;

View File

@ -1,16 +1,19 @@
import PushNotification from 'react-native-push-notification'; import PushNotification from 'react-native-push-notification';
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import EJSON from 'ejson'; import EJSON from 'ejson';
import { goRoom } from './containers/routes/NavigationService';
import { NavigationActions } from './Navigation';
const handleNotification = (notification) => { const handleNotification = (notification) => {
if (!notification.userInteraction) { if (notification.userInteraction) {
return;
}
const { const {
rid, name, sender, type rid, name, sender, type
} = EJSON.parse(notification.ejson || notification.data.ejson); } = EJSON.parse(notification.ejson || notification.data.ejson);
return rid && goRoom({ rid, name: type === 'd' ? sender.username : name }); NavigationActions.push({
screen: 'RoomView',
passProps: { rid, name: type === 'd' ? sender.username : name }
});
}
}; };
PushNotification.configure({ 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'; import { APP } from '../actions/actionsTypes';
const initialState = { const initialState = {
root: null,
starting: true, starting: true,
ready: false, ready: false,
inactive: false, inactive: false,
@ -31,6 +32,11 @@ export default function app(state = initialState, action) {
foreground: false, foreground: false,
background: false background: false
}; };
case APP.START:
return {
...state,
root: action.root
};
case APP.INIT: case APP.INIT:
return { return {
...state, ...state,

View File

@ -19,6 +19,7 @@ export default function login(state = initialState, action) {
...state, ...state,
isFetching: true, isFetching: true,
isAuthenticated: false, isAuthenticated: false,
isRegistering: false,
failure: false, failure: false,
error: '' 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 { AsyncStorage } from 'react-native';
import { METEOR } from '../actions/actionsTypes'; import { METEOR } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
@ -31,7 +31,8 @@ const test = function* test() {
const server = yield select(getServer); const server = yield select(getServer);
const user = yield call(getToken); const user = yield call(getToken);
// const response = // 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)); // yield put(connectSuccess(response));
} catch (err) { } catch (err) {
console.warn('test', err); console.warn('test', err);

View File

@ -1,9 +1,10 @@
import { delay } from 'redux-saga'; import { delay } from 'redux-saga';
import { select, put, call, take, takeLatest } from 'redux-saga/effects'; import { select, put, call, take, takeLatest } from 'redux-saga/effects';
import { NavigationActions } from '../Navigation';
import { CREATE_CHANNEL, LOGIN } from '../actions/actionsTypes'; import { CREATE_CHANNEL, LOGIN } from '../actions/actionsTypes';
import { createChannelSuccess, createChannelFailure } from '../actions/createChannel'; import { createChannelSuccess, createChannelFailure } from '../actions/createChannel';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { goRoom } from '../containers/routes/NavigationService';
const create = function* create(data) { const create = function* create(data) {
return yield RocketChat.createChannel(data); return yield RocketChat.createChannel(data);
@ -18,7 +19,17 @@ const handleRequest = function* handleRequest({ data }) {
} }
const result = yield call(create, data); const result = yield call(create, data);
const { rid, name } = result; 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)); yield put(createChannelSuccess(result));
} catch (err) { } catch (err) {
yield put(createChannelFailure(err)); yield put(createChannelFailure(err));

View File

@ -1,24 +1,26 @@
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { delay } from 'redux-saga'; import { takeLatest, take, select, put } from 'redux-saga/effects';
import { takeLatest, take, select, call, put } from 'redux-saga/effects';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { setServer, addServer } from '../actions/server'; import { appStart } from '../actions';
import * as NavigationService from '../containers/routes/NavigationService'; import { selectServer, addServer } from '../actions/server';
import database from '../lib/realm'; import database from '../lib/realm';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { NavigationActions } from '../Navigation';
const navigate = function* go({ server, params, sameServer = true }) { const navigate = function* go({ 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) { if (!sameServer) {
yield call(NavigationService.goRoomsList); 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; 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 { try {
yield RocketChat.testServer(host); yield RocketChat.testServer(host);
@ -43,18 +52,26 @@ const handleOpen = function* handleOpen({ params }) {
return; return;
} }
const token = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ host }`);
// TODO: needs better test // TODO: needs better test
// if deep link is from same server // if deep link is from same server
if (server === host) { if (server === host) {
yield navigate({ server, params }); if (token) {
yield navigate({ params });
}
} else { // if deep link is from a different server } else { // if deep link is from a different server
// search if deep link's server already exists // search if deep link's server already exists
const servers = yield database.databases.serversDB.objects('servers').filtered('id = $0', host); // TODO: need better test const servers = yield database.databases.serversDB.objects('servers').filtered('id = $0', host); // TODO: need better test
if (servers.length) { if (servers.length) {
// if server exists, select it const deepLinkServer = servers[0].id;
yield put(setServer(servers[0].id)); if (!token) {
yield delay(2000); yield put(appStart('outside'));
yield navigate({ server: servers[0].id, params, sameServer: false }); } else {
yield put(selectServer(deepLinkServer));
yield take(types.METEOR.REQUEST);
yield navigate({ params, sameServer: false });
}
} else { } else {
yield put(addServer(host)); yield put(addServer(host));
} }

View File

@ -2,7 +2,7 @@ import { AsyncStorage } from 'react-native';
import { call, put, takeLatest } from 'redux-saga/effects'; import { call, put, takeLatest } from 'redux-saga/effects';
import * as actions from '../actions'; import * as actions from '../actions';
import { setServer } from '../actions/server'; import { selectServer } from '../actions/server';
import { restoreToken, setUser } from '../actions/login'; import { restoreToken, setUser } from '../actions/login';
import { APP } from '../actions/actionsTypes'; import { APP } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
@ -13,11 +13,16 @@ const restore = function* restore() {
const token = yield call([AsyncStorage, 'getItem'], RocketChat.TOKEN_KEY); const token = yield call([AsyncStorage, 'getItem'], RocketChat.TOKEN_KEY);
if (token) { if (token) {
yield put(restoreToken(token)); yield put(restoreToken(token));
} else {
yield put(actions.appStart('outside'));
} }
const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer'); const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer');
if (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 }`); const login = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`);
if (login) { if (login) {

View File

@ -1,7 +1,9 @@
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { delay } from 'redux-saga';
import { put, call, take, takeLatest, select, all } from 'redux-saga/effects'; import { put, call, take, takeLatest, select, all } from 'redux-saga/effects';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { appStart } from '../actions';
import { import {
// loginRequest, // loginRequest,
// loginSubmit, // loginSubmit,
@ -18,7 +20,6 @@ import {
forgotPasswordFailure forgotPasswordFailure
} from '../actions/login'; } from '../actions/login';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import * as NavigationService from '../containers/routes/NavigationService';
import log from '../utils/log'; import log from '../utils/log';
import I18n from '../i18n'; import I18n from '../i18n';
@ -26,7 +27,6 @@ const getUser = state => state.login;
const getServer = state => state.server.server; const getServer = state => state.server.server;
const getIsConnected = state => state.meteor.connected; 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 loginCall = args => RocketChat.loginWithPassword(args);
const registerCall = args => RocketChat.register(args); const registerCall = args => RocketChat.register(args);
const setUsernameCall = args => RocketChat.setUsername(args); const setUsernameCall = args => RocketChat.setUsername(args);
@ -34,67 +34,27 @@ const loginSuccessCall = () => RocketChat.loginSuccess();
const logoutCall = args => RocketChat.logout(args); const logoutCall = args => RocketChat.logout(args);
const forgotPasswordCall = args => RocketChat.forgotPassword(args); const forgotPasswordCall = args => RocketChat.forgotPassword(args);
// const getToken = function* getToken() { const handleLoginSuccess = function* handleLoginSuccess() {
// 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() {
try { try {
const [server, user] = yield all([select(getServer), select(getUser)]); const [server, user] = yield all([select(getServer), select(getUser)]);
yield AsyncStorage.setItem(RocketChat.TOKEN_KEY, user.token); yield AsyncStorage.setItem(RocketChat.TOKEN_KEY, user.token);
yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user)); yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user));
const token = yield AsyncStorage.getItem('pushId'); // const token = yield AsyncStorage.getItem('pushId');
if (token) { // if (token) {
yield RocketChat.registerPushToken(user.user.id, token); // yield RocketChat.registerPushToken(user.user.id, token);
} // }
if (!user.user.username && !user.isRegistering) { yield RocketChat.registerPushToken(user.user.id);
if (!user.user.username || user.isRegistering) {
yield put(registerIncomplete()); yield put(registerIncomplete());
} else {
yield delay(300);
yield put(appStart('inside'));
} }
} catch (e) { } 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 }) { const handleRegisterSubmit = function* handleRegisterSubmit({ credentials }) {
yield put(registerRequest(credentials)); yield put(registerRequest(credentials));
}; };
@ -137,6 +97,8 @@ const handleLogout = function* handleLogout() {
const server = yield select(getServer); const server = yield select(getServer);
if (server) { if (server) {
try { try {
yield put(appStart('outside'));
yield delay(300);
yield call(logoutCall, { server }); yield call(logoutCall, { server });
} catch (e) { } catch (e) {
log('handleLogout', e); log('handleLogout', e);
@ -145,7 +107,7 @@ const handleLogout = function* handleLogout() {
}; };
const handleRegisterIncomplete = function* handleRegisterIncomplete() { const handleRegisterIncomplete = function* handleRegisterIncomplete() {
yield call(NavigationService.navigate, 'Register'); yield put(appStart('outside'));
}; };
const handleForgotPasswordRequest = function* handleForgotPasswordRequest({ email }) { const handleForgotPasswordRequest = function* handleForgotPasswordRequest({ email }) {
@ -183,7 +145,7 @@ const handleSetUser = function* handleSetUser(params) {
const root = function* root() { const root = function* root() {
// yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges); // yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges);
// yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest); // 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.SUBMIT, handleLoginSubmit);
yield takeLatest(types.LOGIN.REGISTER_REQUEST, handleRegisterRequest); yield takeLatest(types.LOGIN.REGISTER_REQUEST, handleRegisterRequest);
yield takeLatest(types.LOGIN.REGISTER_SUBMIT, handleRegisterSubmit); yield takeLatest(types.LOGIN.REGISTER_SUBMIT, handleRegisterSubmit);

View File

@ -1,5 +1,6 @@
import { delay } from 'redux-saga'; import { delay } from 'redux-saga';
import { takeLatest, put, call, select } from 'redux-saga/effects'; import { takeLatest, put, call, select } from 'redux-saga/effects';
import { MESSAGES } from '../actions/actionsTypes'; import { MESSAGES } from '../actions/actionsTypes';
import { import {
messagesSuccess, messagesSuccess,
@ -16,8 +17,8 @@ import {
} from '../actions/messages'; } from '../actions/messages';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/realm';
import { goRoom } from '../containers/routes/NavigationService';
import log from '../utils/log'; import log from '../utils/log';
import { NavigationActions } from '../Navigation';
const deleteMessage = message => RocketChat.deleteMessage(message); const deleteMessage = message => RocketChat.deleteMessage(message);
const editMessage = message => RocketChat.editMessage(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 }) { const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
try { try {
const { username } = message.u; const { username } = message.u;
const subscriptions = database.objects('subscriptions').filtered('name = $0', username); const subscriptions = database.objects('subscriptions').filtered('name = $0', username);
if (subscriptions.length) { if (subscriptions.length) {
goRoom({ rid: subscriptions[0].rid, name: subscriptions[0].name }); yield goRoom({ rid: subscriptions[0].rid, name: subscriptions[0].name });
} else { } else {
const room = yield RocketChat.createDirectMessage(username); 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 server = yield select(state => state.server.server);
const msg = `[ ](${ server }/direct/${ username }?msg=${ message._id }) `; const msg = `[ ](${ server }/direct/${ username }?msg=${ message._id }) `;
yield put(setInput({ msg })); yield put(setInput({ msg }));

View File

@ -9,8 +9,8 @@ import { addUserTyping, removeUserTyping, setLastOpen } from '../actions/room';
import { messagesRequest, editCancel } from '../actions/messages'; import { messagesRequest, editCancel } from '../actions/messages';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/realm';
import * as NavigationService from '../containers/routes/NavigationService';
import log from '../utils/log'; import log from '../utils/log';
import { NavigationActions } from '../Navigation';
const leaveRoom = rid => RocketChat.leaveRoom(rid); const leaveRoom = rid => RocketChat.leaveRoom(rid);
const eraseRoom = rid => RocketChat.eraseRoom(rid); const eraseRoom = rid => RocketChat.eraseRoom(rid);
@ -139,7 +139,7 @@ const updateLastOpen = function* updateLastOpen() {
}; };
const goRoomsListAndDelete = function* goRoomsListAndDelete(rid) { const goRoomsListAndDelete = function* goRoomsListAndDelete(rid) {
NavigationService.goRoomsList(); NavigationActions.popToRoot();
yield delay(1000); yield delay(1000);
try { try {
database.write(() => { 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 { delay } from 'redux-saga';
import { AsyncStorage } from 'react-native'; 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 * as actions from '../actions';
import { connectRequest } from '../actions/connect'; 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 { setRoles } from '../actions/roles';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/realm';
import { navigate } from '../containers/routes/NavigationService';
import log from '../utils/log'; import log from '../utils/log';
import I18n from '../i18n';
const validate = function* validate(server) { const validate = function* validate(server) {
return yield RocketChat.testServer(server); return yield RocketChat.testServer(server);
}; };
const selectServer = function* selectServer({ server }) { const handleSelectServer = function* handleSelectServer({ server }) {
try { try {
yield database.setActiveDB(server); yield database.setActiveDB(server);
@ -36,7 +37,7 @@ const selectServer = function* selectServer({ server }) {
yield put(connectRequest()); yield put(connectRequest());
} catch (e) { } catch (e) {
log('selectServer', e); log('handleSelectServer', e);
} }
}; };
@ -53,12 +54,12 @@ const validateServer = function* validateServer({ server }) {
const addServer = function* addServer({ server }) { const addServer = function* addServer({ server }) {
try { try {
yield put(actions.appStart('outside'));
yield call(NavigationActions.resetTo, { screen: 'ListServerView', title: I18n.t('Servers') });
database.databases.serversDB.write(() => { database.databases.serversDB.write(() => {
database.databases.serversDB.create('servers', { id: server, current: false }, true); database.databases.serversDB.create('servers', { id: server, current: false }, true);
}); });
yield put(setServer(server)); yield put(selectServer(server));
yield take(LOGIN.SET_TOKEN);
navigate('LoginSignup');
} catch (e) { } catch (e) {
log('addServer', e); log('addServer', e);
} }
@ -66,7 +67,7 @@ const addServer = function* addServer({ server }) {
const root = function* root() { const root = function* root() {
yield takeLatest(SERVER.REQUEST, validateServer); yield takeLatest(SERVER.REQUEST, validateServer);
yield takeLatest(SERVER.SELECT, selectServer); yield takeLatest(SERVER.SELECT, handleSelectServer);
yield takeLatest(SERVER.ADD, addServer); yield takeLatest(SERVER.ADD, addServer);
}; };
export default root; 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 Button from '../containers/Button';
import I18n from '../i18n'; import I18n from '../i18n';
@connect( @connect(state => ({
state => ({
createChannel: state.createChannel, createChannel: state.createChannel,
users: state.selectedUsers.users users: state.selectedUsers.users
}), }), dispatch => ({
dispatch => ({
create: data => dispatch(createChannelRequest(data)) create: data => dispatch(createChannelRequest(data))
}) }))
) /** @extends React.Component */
export default class CreateChannelView extends LoggedView { export default class CreateChannelView extends LoggedView {
static propTypes = { static propTypes = {
navigator: PropTypes.object,
create: PropTypes.func.isRequired, create: PropTypes.func.isRequired,
createChannel: PropTypes.object.isRequired, createChannel: PropTypes.object.isRequired,
users: PropTypes.array.isRequired, users: PropTypes.array.isRequired
navigation: PropTypes.object.isRequired
}; };
constructor(props) { constructor(props) {

View File

@ -20,12 +20,13 @@ import I18n from '../i18n';
forgotPasswordInit: () => dispatch(forgotPasswordInit()), forgotPasswordInit: () => dispatch(forgotPasswordInit()),
forgotPasswordRequest: email => dispatch(forgotPasswordRequest(email)) forgotPasswordRequest: email => dispatch(forgotPasswordRequest(email))
})) }))
/** @extends React.Component */
export default class ForgotPasswordView extends LoggedView { export default class ForgotPasswordView extends LoggedView {
static propTypes = { static propTypes = {
navigator: PropTypes.object,
forgotPasswordInit: PropTypes.func.isRequired, forgotPasswordInit: PropTypes.func.isRequired,
forgotPasswordRequest: PropTypes.func.isRequired, forgotPasswordRequest: PropTypes.func.isRequired,
login: PropTypes.object, login: PropTypes.object
navigation: PropTypes.object.isRequired
} }
constructor(props) { constructor(props) {
@ -44,7 +45,7 @@ export default class ForgotPasswordView extends LoggedView {
componentDidUpdate() { componentDidUpdate() {
const { login } = this.props; const { login } = this.props;
if (login.success) { if (login.success) {
this.props.navigation.goBack(); this.props.navigator.pop();
setTimeout(() => { setTimeout(() => {
showErrorAlert(I18n.t('Forgot_password_If_this_email_is_registered'), I18n.t('Alert')); 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 Icon from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
// import Zeroconf from 'react-native-zeroconf'; import { View, Text, SectionList, StyleSheet } from 'react-native';
import { View, Text, SectionList, StyleSheet, SafeAreaView } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withNavigationFocus } from 'react-navigation';
import LoggedView from './View'; import LoggedView from './View';
import { setServer } from '../actions/server'; import { selectServer } from '../actions/server';
import database from '../lib/realm'; import database from '../lib/realm';
import Fade from '../animations/fade'; import Fade from '../animations/fade';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
import I18n from '../i18n'; import I18n from '../i18n';
import { iconsMap } from '../Icons';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
view: { view: {
@ -63,22 +62,19 @@ const styles = StyleSheet.create({
} }
}); });
// const zeroconf = new Zeroconf();
@connect(state => ({ @connect(state => ({
server: state.server.server, server: state.server.server,
login: state.login, login: state.login,
connected: state.meteor.connected connected: state.meteor.connected
}), dispatch => ({ }), 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 = { static propTypes = {
navigation: PropTypes.object.isRequired, navigator: PropTypes.object,
login: PropTypes.object.isRequired, login: PropTypes.object.isRequired,
selectServer: PropTypes.func.isRequired, selectServer: PropTypes.func.isRequired,
connected: PropTypes.bool.isRequired,
server: PropTypes.string server: PropTypes.string
} }
@ -88,51 +84,36 @@ class ListServerView extends LoggedView {
sections: [] sections: []
}; };
this.data = database.databases.serversDB.objects('servers'); this.data = database.databases.serversDB.objects('servers');
// this.redirected = false;
this.data.addListener(this.updateState); 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() { componentDidMount() {
// zeroconf.on('update', this.updateState);
// zeroconf.scan('http', 'tcp', 'local.');
this.updateState(); this.updateState();
this.jumpToSelectedServer(); 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() { componentWillUnmount() {
// zeroconf.stop();
this.data.removeAllListeners(); this.data.removeAllListeners();
// zeroconf.removeListener('update', this.updateState);
} }
openLogin = () => { onNavigatorEvent(event) {
this.props.navigation.navigate({ key: 'LoginSignup', routeName: 'LoginSignup' }); if (event.type === 'NavBarButtonPress') {
if (event.id === 'addServer') {
this.props.navigator.push({
screen: 'NewServerView',
title: I18n.t('New_Server')
});
} }
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);
} }
} }
@ -145,24 +126,6 @@ class ListServerView extends LoggedView {
title: I18n.t('My_servers'), title: I18n.t('My_servers'),
data: this.data 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 { return {
...this.state, ...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 = () => { updateState = () => {
this.setState(this.getState()); this.setState(this.getState());
} }
@ -210,7 +193,7 @@ class ListServerView extends LoggedView {
render() { render() {
return ( return (
<SafeAreaView style={styles.view} testID='list-server-view'> <View style={styles.view} testID='list-server-view'>
<SectionList <SectionList
style={styles.list} style={styles.list}
sections={this.state.sections} sections={this.state.sections}
@ -219,8 +202,7 @@ class ListServerView extends LoggedView {
keyExtractor={item => item.id} keyExtractor={item => item.id}
ItemSeparatorComponent={this.renderSeparator} ItemSeparatorComponent={this.renderSeparator}
/> />
</SafeAreaView> </View>
); );
} }
} }
export default withNavigationFocus(ListServerView);

View File

@ -1,13 +1,11 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/FontAwesome'; import Icon from 'react-native-vector-icons/FontAwesome';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import Modal from 'react-native-modal';
import RocketChat from '../lib/rocketchat';
import { open, close } from '../actions/login'; import { open, close } from '../actions/login';
import LoggedView from './View'; import LoggedView from './View';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
@ -17,9 +15,6 @@ import Button from '../containers/Button';
import Loading from '../containers/Loading'; import Loading from '../containers/Loading';
import I18n from '../i18n'; 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({ const styles = StyleSheet.create({
container: { container: {
alignItems: 'center', alignItems: 'center',
@ -44,14 +39,13 @@ const styles = StyleSheet.create({
planetImage: { planetImage: {
width: 200, width: 200,
height: 162, height: 162,
marginVertical: 20, marginVertical: 20
opacity: 0.6
} }
}); });
@connect(state => ({ @connect(state => ({
server: state.server.server, server: state.server.server,
login: state.login, isFetching: state.login.isFetching,
Accounts_EmailOrUsernamePlaceholder: state.settings.Accounts_EmailOrUsernamePlaceholder, Accounts_EmailOrUsernamePlaceholder: state.settings.Accounts_EmailOrUsernamePlaceholder,
Accounts_PasswordPlaceholder: state.settings.Accounts_PasswordPlaceholder, Accounts_PasswordPlaceholder: state.settings.Accounts_PasswordPlaceholder,
Accounts_OAuth_Facebook: state.settings.Accounts_OAuth_Facebook, Accounts_OAuth_Facebook: state.settings.Accounts_OAuth_Facebook,
@ -63,17 +57,16 @@ const styles = StyleSheet.create({
Accounts_OAuth_Twitter: state.settings.Accounts_OAuth_Twitter, Accounts_OAuth_Twitter: state.settings.Accounts_OAuth_Twitter,
services: state.login.services services: state.login.services
}), dispatch => ({ }), dispatch => ({
loginOAuth: params => RocketChat.login(params),
open: () => dispatch(open()), open: () => dispatch(open()),
close: () => dispatch(close()) close: () => dispatch(close())
})) }))
/** @extends React.Component */
export default class LoginSignupView extends LoggedView { export default class LoginSignupView extends LoggedView {
static propTypes = { static propTypes = {
loginOAuth: PropTypes.func.isRequired, navigator: PropTypes.object,
open: PropTypes.func.isRequired, open: PropTypes.func.isRequired,
close: PropTypes.func.isRequired, close: PropTypes.func.isRequired,
navigation: PropTypes.object.isRequired, isFetching: PropTypes.bool,
login: PropTypes.object,
server: PropTypes.string, server: PropTypes.string,
Accounts_EmailOrUsernamePlaceholder: PropTypes.bool, Accounts_EmailOrUsernamePlaceholder: PropTypes.bool,
Accounts_PasswordPlaceholder: PropTypes.string, Accounts_PasswordPlaceholder: PropTypes.string,
@ -89,13 +82,6 @@ export default class LoginSignupView extends LoggedView {
constructor(props) { constructor(props) {
super('LoginSignupView', props); super('LoginSignupView', props);
this.state = {
modalVisible: false,
oAuthUrl: '',
showSocialButtons: false
};
this.redirectRegex = new RegExp(`(?=.*(${ this.props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g');
} }
componentDidMount() { componentDidMount() {
@ -183,19 +169,29 @@ export default class LoginSignupView extends LoggedView {
} }
openOAuth = (oAuthUrl) => { 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 = () => { register = () => {
this.props.navigation.navigate({ key: 'Register', routeName: 'Register' }); this.props.navigator.push({
} screen: 'RegisterView',
title: this.props.server,
closeOAuth = () => { backButtonTitle: I18n.t('Welcome')
this.setState({ modalVisible: false }); });
}
toggleSocialButtons = () => {
this.setState({ showSocialButtons: !this.state.showSocialButtons });
} }
renderServices = () => { renderServices = () => {
@ -279,13 +275,11 @@ export default class LoginSignupView extends LoggedView {
render() { render() {
return ( return (
[
<ScrollView <ScrollView
key='login-view'
style={[sharedStyles.container, sharedStyles.containerScrollView]} style={[sharedStyles.container, sharedStyles.containerScrollView]}
{...scrollPersistTaps} {...scrollPersistTaps}
> >
<SafeAreaView testID='welcome-view'> <View testID='welcome-view'>
<View style={styles.container}> <View style={styles.container}>
<Image <Image
source={require('../static/images/logo.png')} source={require('../static/images/logo.png')}
@ -301,44 +295,20 @@ export default class LoginSignupView extends LoggedView {
<Button <Button
title={I18n.t('I_have_an_account')} title={I18n.t('I_have_an_account')}
type='primary' type='primary'
onPress={() => this.props.navigation.navigate({ key: 'Login', routeName: 'Login' })} onPress={() => this.login()}
testID='welcome-view-login' testID='welcome-view-login'
/> />
<Button <Button
title={I18n.t('Create_account')} title={I18n.t('Create_account')}
type='secondary' type='secondary'
onPress={() => this.props.navigation.navigate({ key: 'Register', routeName: 'Register' })} onPress={() => this.register()}
testID='welcome-view-register' testID='welcome-view-register'
/> />
{this.renderServices()} {this.renderServices()}
</View> </View>
<Loading visible={this.props.login.isFetching} /> <Loading visible={this.props.isFetching} />
</SafeAreaView> </View>
</ScrollView>, </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>
]
); );
} }
} }

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import { serverRequest, addServer } from '../actions/server'; import { serverRequest, addServer } from '../actions/server';
@ -21,14 +21,15 @@ import I18n from '../i18n';
validateServer: url => dispatch(serverRequest(url)), validateServer: url => dispatch(serverRequest(url)),
addServer: url => dispatch(addServer(url)) addServer: url => dispatch(addServer(url))
})) }))
/** @extends React.Component */
export default class NewServerView extends LoggedView { export default class NewServerView extends LoggedView {
static propTypes = { static propTypes = {
navigator: PropTypes.object,
validateServer: PropTypes.func.isRequired, validateServer: PropTypes.func.isRequired,
addServer: PropTypes.func.isRequired, addServer: PropTypes.func.isRequired,
validating: PropTypes.bool.isRequired, validating: PropTypes.bool.isRequired,
validInstance: PropTypes.bool.isRequired, validInstance: PropTypes.bool.isRequired,
addingServer: PropTypes.bool.isRequired, addingServer: PropTypes.bool.isRequired
navigation: PropTypes.object.isRequired
} }
constructor(props) { constructor(props) {
@ -36,11 +37,7 @@ export default class NewServerView extends LoggedView {
this.state = { this.state = {
defaultServer: 'https://open.rocket.chat' defaultServer: 'https://open.rocket.chat'
}; };
this.props.validateServer(this.state.defaultServer); // Need to call because in case of submit with empty field props.validateServer(this.state.defaultServer); // Need to call because in case of submit with empty field
}
componentDidMount() {
this.input.focus();
} }
onChangeText = (text) => { onChangeText = (text) => {
@ -109,7 +106,7 @@ export default class NewServerView extends LoggedView {
keyboardVerticalOffset={128} keyboardVerticalOffset={128}
> >
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}> <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> <Text style={[styles.loginText, styles.loginTitle]}>{I18n.t('Sign_in_your_server')}</Text>
<TextInput <TextInput
inputRef={e => this.input = e} inputRef={e => this.input = e}
@ -132,7 +129,7 @@ export default class NewServerView extends LoggedView {
/> />
</View> </View>
<Loading visible={this.props.addingServer} /> <Loading visible={this.props.addingServer} />
</SafeAreaView> </View>
</ScrollView> </ScrollView>
</KeyboardView> </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 CANCEL_INDEX = 1;
const options = [I18n.t('Unpin'), I18n.t('Cancel')]; const options = [I18n.t('Unpin'), I18n.t('Cancel')];
@connect( @connect(state => ({
state => ({
messages: state.pinnedMessages.messages, messages: state.pinnedMessages.messages,
ready: state.pinnedMessages.ready, ready: state.pinnedMessages.ready,
user: state.login.user, 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 : '' baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}), }), dispatch => ({
dispatch => ({
openPinnedMessages: (rid, limit) => dispatch(openPinnedMessages(rid, limit)), openPinnedMessages: (rid, limit) => dispatch(openPinnedMessages(rid, limit)),
closePinnedMessages: () => dispatch(closePinnedMessages()), closePinnedMessages: () => dispatch(closePinnedMessages()),
togglePinRequest: message => dispatch(togglePinRequest(message)) togglePinRequest: message => dispatch(togglePinRequest(message))
}) }))
) /** @extends React.Component */
export default class PinnedMessagesView extends LoggedView { export default class PinnedMessagesView extends LoggedView {
static propTypes = { static propTypes = {
navigation: PropTypes.object, rid: PropTypes.string,
messages: PropTypes.array, messages: PropTypes.array,
ready: PropTypes.bool, ready: PropTypes.bool,
user: PropTypes.object, user: PropTypes.object,
@ -81,7 +83,7 @@ export default class PinnedMessagesView extends LoggedView {
} }
load = () => { load = () => {
this.props.openPinnedMessages(this.props.navigation.state.params.rid, this.limit); this.props.openPinnedMessages(this.props.rid, this.limit);
} }
moreData = () => { moreData = () => {

View File

@ -3,7 +3,10 @@ import PropTypes from 'prop-types';
import { WebView } from 'react-native'; import { WebView } from 'react-native';
import { connect } from 'react-redux'; 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 = { static propTypes = {
privacyPolicy: PropTypes.string 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 React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import Dialog from 'react-native-dialog'; import Dialog from 'react-native-dialog';
import SHA256 from 'js-sha256'; import SHA256 from 'js-sha256';
@ -22,17 +22,24 @@ import I18n from '../../i18n';
import Button from '../../containers/Button'; import Button from '../../containers/Button';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import { iconsMap } from '../../Icons';
@connect(state => ({ @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 Accounts_CustomFields: state.settings.Accounts_CustomFields
})) }))
/** @extends React.Component */
export default class ProfileView extends LoggedView { export default class ProfileView extends LoggedView {
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigator: PropTypes.object,
user: PropTypes.object, user: PropTypes.object,
Accounts_CustomFields: PropTypes.string Accounts_CustomFields: PropTypes.string
}; }
constructor(props) { constructor(props) {
super('ProfileView', props); super('ProfileView', props);
@ -49,11 +56,26 @@ export default class ProfileView extends LoggedView {
avatarSuggestions: {}, avatarSuggestions: {},
customFields: {} 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() { async componentDidMount() {
this.init(); this.init();
this.props.navigator.setDrawerEnabled({
side: 'left',
enabled: true
});
try { try {
const result = await RocketChat.getAvatarSuggestion(); const result = await RocketChat.getAvatarSuggestion();
this.setState({ avatarSuggestions: result }); 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) => { init = (user) => {
const { const {
name, username, emails, customFields name, username, emails, customFields
@ -195,10 +233,6 @@ export default class ProfileView extends LoggedView {
} }
} }
setAvatar = (avatar) => {
this.setState({ avatar });
}
resetAvatar = async() => { resetAvatar = async() => {
try { try {
await RocketChat.resetAvatar(); await RocketChat.resetAvatar();

View File

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

View File

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

View File

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

View File

@ -34,18 +34,16 @@ const PERMISSIONS_ARRAY = [
PERMISSION_DELETE_P PERMISSION_DELETE_P
]; ];
@connect(null, dispatch => ({ /** @extends React.Component */
eraseRoom: rid => dispatch(eraseRoom(rid)) class RoomInfoEditView extends LoggedView {
}))
export default class RoomInfoEditView extends LoggedView {
static propTypes = { static propTypes = {
navigation: PropTypes.object, rid: PropTypes.string,
eraseRoom: PropTypes.func eraseRoom: PropTypes.func
}; };
constructor(props) { constructor(props) {
super('RoomInfoEditView', props); super('RoomInfoEditView', props);
const { rid } = props.navigation.state.params; const { rid } = props;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.permissions = {}; this.permissions = {};
this.state = { 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 PropTypes from 'prop-types';
import { View, Text, ScrollView } from 'react-native'; import { View, Text, ScrollView } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import moment from 'moment'; import moment from 'moment';
import LoggedView from '../View'; import LoggedView from '../View';
@ -12,11 +11,11 @@ import styles from './styles';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import database from '../../lib/realm'; import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import Touch from '../../utils/touch';
import log from '../../utils/log'; import log from '../../utils/log';
import RoomTypeIcon from '../../containers/RoomTypeIcon'; import RoomTypeIcon from '../../containers/RoomTypeIcon';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { iconsMap } from '../../Icons';
const PERMISSION_EDIT_ROOM = 'edit-room'; 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> <Text testID='room-info-view-name' style={styles.roomTitle} key='room-info-name'>{room.name}</Text>
] ]
); );
@connect(state => ({ @connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', 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, activeUsers: state.activeUsers,
Message_TimeFormat: state.settings.Message_TimeFormat, Message_TimeFormat: state.settings.Message_TimeFormat,
roles: state.roles roles: state.roles
})) }))
/** @extends React.Component */
export default class RoomInfoView extends LoggedView { export default class RoomInfoView extends LoggedView {
static propTypes = { static propTypes = {
baseUrl: PropTypes.string, navigator: PropTypes.object,
user: PropTypes.object, rid: PropTypes.string,
navigation: PropTypes.object, userId: PropTypes.string,
activeUsers: PropTypes.object, activeUsers: PropTypes.object,
Message_TimeFormat: PropTypes.string, Message_TimeFormat: PropTypes.string,
roles: PropTypes.object 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) { constructor(props) {
super('RoomInfoView', props); super('RoomInfoView', props);
const { rid } = props.navigation.state.params; const { rid } = props;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.sub = { this.sub = {
unsubscribe: () => {} unsubscribe: () => {}
@ -80,6 +58,7 @@ export default class RoomInfoView extends LoggedView {
roomUser: {}, roomUser: {},
roles: [] roles: []
}; };
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
} }
async componentDidMount() { async componentDidMount() {
@ -87,9 +66,10 @@ export default class RoomInfoView extends LoggedView {
this.rooms.addListener(this.updateRoom); this.rooms.addListener(this.updateRoom);
// get user of room // get user of room
if (this.state.room) {
if (this.state.room.t === 'd') { if (this.state.room.t === 'd') {
try { try {
const roomUser = await RocketChat.getRoomMember(this.state.room.rid, this.props.user.id); const roomUser = await RocketChat.getRoomMember(this.state.room.rid, this.props.userId);
this.setState({ roomUser }); this.setState({ roomUser });
const username = this.state.room.name; const username = this.state.room.name;
@ -112,7 +92,16 @@ export default class RoomInfoView extends LoggedView {
} }
} else { } else {
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], this.state.room.rid); const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], this.state.room.rid);
this.props.navigation.setParams({ hasEditPermission: permissions[PERMISSION_EDIT_ROOM] }); if (permissions[PERMISSION_EDIT_ROOM]) {
this.props.navigator.setButtons({
rightButtons: [{
id: 'edit',
icon: iconsMap.create,
testID: 'room-info-view-edit-button'
}]
});
}
}
} }
} }
@ -121,6 +110,20 @@ export default class RoomInfoView extends LoggedView {
this.sub.unsubscribe(); 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) => { getFullUserData = async(username) => {
try { try {
const result = await RocketChat.subscribe('fullUserData', username); const result = await RocketChat.subscribe('fullUserData', username);
@ -170,7 +173,6 @@ export default class RoomInfoView extends LoggedView {
if (!utcOffset) { if (!utcOffset) {
return null; return null;
} }
// TODO: translate
return ( return (
<View style={styles.item}> <View style={styles.item}>
<Text style={styles.itemLabel}>{I18n.t('Timezone')}</Text> <Text style={styles.itemLabel}>{I18n.t('Timezone')}</Text>

View File

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

View File

@ -71,7 +71,7 @@ export class List extends React.Component {
onEndReachedThreshold={100} onEndReachedThreshold={100}
renderFooter={this.props.renderFooter} renderFooter={this.props.renderFooter}
renderHeader={() => <Typing />} renderHeader={() => <Typing />}
onEndReached={() => this.props.onEndReached(this.data[this.data.length - 1], this.data.length)} onEndReached={() => this.props.onEndReached(this.data[this.data.length - 1])}
dataSource={this.dataSource} dataSource={this.dataSource}
renderRow={(item, previousItem) => this.props.renderRow(item, previousItem)} renderRow={(item, previousItem) => this.props.renderRow(item, previousItem)}
initialListSize={20} initialListSize={20}

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