Switch push notification lib (#346)

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

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

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

View File

@ -124,8 +124,7 @@ module.exports = {
"prefer-const": 2,
"object-shorthand": 2,
"consistent-return": 0,
"global-require": "off",
"react/prop-types": [0, { skipUndeclared: true }]
"global-require": "off"
},
"globals": {
"__DEV__": true

22
.snyk
View File

@ -1,5 +1,5 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.7.1
version: v1.12.0
ignore: {}
# patches apply the minimum changes required to fix a vulnerability
patch:
@ -24,3 +24,23 @@ patch:
patched: '2017-09-29T23:29:20.238Z'
- realm > extract-zip > debug:
patched: '2017-09-29T23:29:20.238Z'
'npm:lodash:20180130':
- react-native > plist > xmlbuilder > lodash:
patched: '2018-07-02T23:20:39.933Z'
'npm:hoek:20180212':
- realm > node-pre-gyp > hawk > hoek:
patched: '2018-06-22T03:39:40.096Z'
- realm > node-pre-gyp > hawk > boom > hoek:
patched: '2018-06-22T03:39:40.096Z'
- realm > node-pre-gyp > hawk > sntp > hoek:
patched: '2018-06-22T03:39:40.096Z'
- realm > node-pre-gyp > hawk > cryptiles > boom > hoek:
patched: '2018-06-22T03:39:40.096Z'
- realm > node-pre-gyp > request > hawk > hoek:
patched: '2018-06-22T03:39:40.096Z'
- realm > node-pre-gyp > request > hawk > boom > hoek:
patched: '2018-06-22T03:39:40.096Z'
- realm > node-pre-gyp > request > hawk > sntp > hoek:
patched: '2018-06-22T03:39:40.096Z'
- realm > node-pre-gyp > request > hawk > cryptiles > boom > hoek:
patched: '2018-06-22T03:39:40.096Z'

View File

@ -618,7 +618,7 @@ exports[`render unread +999 1`] = `
undefined,
],
Object {
"backgroundColor": "#cbced1",
"backgroundColor": undefined,
},
]
}
@ -868,7 +868,7 @@ exports[`render unread 1`] = `
undefined,
],
Object {
"backgroundColor": "#cbced1",
"backgroundColor": undefined,
},
]
}
@ -1118,7 +1118,7 @@ exports[`renders correctly 1`] = `
undefined,
],
Object {
"backgroundColor": "#cbced1",
"backgroundColor": undefined,
},
]
}

View File

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

View File

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

View File

@ -10,7 +10,7 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
@ -50,27 +50,7 @@
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="${applicationId}" />
</intent-filter>
</receiver>
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
<service android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationRegistrationService"/>
<service
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="673693445664\0"/>
</application>
</manifest>

View File

@ -0,0 +1,53 @@
package chat.rocket.reactnative;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings.System;
import android.media.RingtoneManager;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import com.wix.reactnativenotifications.core.notification.PushNotification;
public class CustomPushNotification extends PushNotification {
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
}
@Override
protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
Bundle bundle = mNotificationProps.asBundle();
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
int largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
String title = bundle.getString("title");
String message = bundle.getString("message");
final Notification.Builder notification = new Notification.Builder(mContext);
notification
.setSmallIcon(smallIconResId)
.setContentIntent(intent)
.setContentTitle(title)
.setContentText(message)
.setStyle(new Notification.BigTextStyle().bigText(message))
.setPriority(Notification.PRIORITY_HIGH)
.setColor(mContext.getColor(R.color.notification_text))
.setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true);
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
notification.setLargeIcon(largeIconBitmap);
return notification;
}
}

View File

@ -1,26 +1,17 @@
package chat.rocket.reactnative;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
import org.devio.rn.splashscreen.SplashScreen;
import com.crashlytics.android.Crashlytics;
import io.fabric.sdk.android.Fabric;
import android.graphics.drawable.Drawable;
import android.support.v4.content.ContextCompat;
import android.widget.LinearLayout;
import com.reactnativenavigation.controllers.SplashActivity;
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
public class MainActivity extends SplashActivity {
@Override
protected String getMainComponentName() {
return "RocketChatRN";
}
public LinearLayout createSplashLayout() {
LinearLayout splash = new LinearLayout(this);
Drawable launch_screen_bitmap = ContextCompat.getDrawable(getApplicationContext(),R.drawable.launch_screen_bitmap);
splash.setBackground(launch_screen_bitmap);
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.show(this);
super.onCreate(savedInstanceState);
Fabric.with(this, new Crashlytics());
return splash;
}
}

View File

@ -1,41 +1,60 @@
package chat.rocket.reactnative;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import com.facebook.react.ReactApplication;
import com.AlexanderZaytsev.RNI18n.RNI18nPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
import com.balthazargronon.RCTZeroconf.ZeroconfReactPackage;
import com.brentvatne.react.ReactVideoPackage;
import com.crashlytics.android.Crashlytics;
import com.dylanvann.fastimage.FastImageViewPackage;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.horcrux.svg.SvgPackage;
import com.imagepicker.ImagePickerPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
import com.balthazargronon.RCTZeroconf.ZeroconfReactPackage;
import io.realm.react.RealmReactPackage;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage;
import com.brentvatne.react.ReactVideoPackage;
import com.reactnativenavigation.NavigationApplication;
import com.remobile.toast.RCTToastPackage;
import com.wix.reactnativekeyboardinput.KeyboardInputPackage;
import com.rnim.rn.audio.ReactNativeAudioPackage;
import com.smixx.fabric.FabricPackage;
import com.dylanvann.fastimage.FastImageViewPackage;
import com.AlexanderZaytsev.RNI18n.RNI18nPackage;
import com.wix.reactnativekeyboardinput.KeyboardInputPackage;
import com.wix.reactnativenotifications.RNNotificationsPackage;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
import com.wix.reactnativenotifications.core.notification.IPushNotification;
import java.util.Arrays;
import java.util.List;
import org.devio.rn.splashscreen.SplashScreenReactPackage;
public class MainApplication extends Application implements ReactApplication {
import io.fabric.sdk.android.Fabric;
import io.realm.react.RealmReactPackage;
public class MainApplication extends NavigationApplication implements INotificationsApplication {
private NotificationsLifecycleFacade notificationsLifecycleFacade;
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
public boolean isDebug() {
return BuildConfig.DEBUG;
}
@Override
public String getJSMainModuleName() {
return "index";
}
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(
new MainReactPackage(),
new SvgPackage(),
@ -44,28 +63,37 @@ public class MainApplication extends Application implements ReactApplication {
new RNFetchBlobPackage(),
new ZeroconfReactPackage(),
new RealmReactPackage(),
new ReactNativePushNotificationPackage(),
new ReactVideoPackage(),
new SplashScreenReactPackage(),
new RCTToastPackage(),
new ReactNativeAudioPackage(),
new KeyboardInputPackage(MainApplication.this),
new RocketChatNativePackage(),
new FabricPackage(),
new FastImageViewPackage(),
new RNI18nPackage()
new RNI18nPackage(),
new RNNotificationsPackage(MainApplication.this)
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void 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 {
repositories {
jcenter()
google()
mavenLocal()
mavenCentral()
jcenter()
}
dependencies {
// classpath 'com.android.tools.build:gradle:2.2.3'
@ -17,6 +19,7 @@ buildscript {
allprojects {
repositories {
mavenLocal()
mavenCentral()
jcenter()
google()
maven {

View File

@ -9,8 +9,6 @@ include ':react-native-audio'
project(':react-native-audio').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-audio/android')
include ':reactnativekeyboardinput'
project(':reactnativekeyboardinput').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keyboard-input/lib/android')
include ':react-native-splash-screen'
project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android')
include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
include ':react-native-svg'
@ -25,8 +23,10 @@ include ':react-native-zeroconf'
project(':react-native-zeroconf').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-zeroconf/android')
include ':realm'
project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android')
include ':react-native-push-notification'
project(':react-native-push-notification').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-push-notification/android')
include ':react-native-toast'
project(':react-native-toast').projectDir = new File(settingsDir, '../node_modules/@remobile/react-native-toast/android')
include ':react-native-navigation'
project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/')
include ':reactnativenotifications'
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android')
include ':app'

28
app/Icons.js Normal file
View File

@ -0,0 +1,28 @@
import { Platform } from 'react-native';
import Ionicons from 'react-native-vector-icons/Ionicons';
const isIOS = Platform.OS === 'ios';
const prefix = isIOS ? 'ios' : 'md';
// icon name from provider: [ size of the uri, icon provider, name to be used later ]
const icons = {
[`${ prefix }-search`]: [30, Ionicons, 'search'],
[`${ prefix }-menu`]: [30, Ionicons, 'menu'],
[`${ prefix }-star`]: [30, Ionicons, 'star'],
[`${ prefix }-star-outline`]: [30, Ionicons, 'starOutline'],
[isIOS ? 'ios-create-outline' : 'md-create']: [30, Ionicons, 'create'],
[`${ prefix }-more`]: [30, Ionicons, 'more'],
[`${ prefix }-add`]: [30, Ionicons, 'add']
};
const iconsMap = {};
const iconsLoaded = async() => {
const promises = Object.keys(icons).map((icon) => {
const Provider = icons[icon][1];
return Provider.getImageSource(icon, icons[icon][0]);
});
const sources = await Promise.all(promises);
Object.keys(icons).forEach((icon, i) => (iconsMap[icons[icon][2]] = sources[i]));
};
export { iconsLoaded, iconsMap };

13
app/Navigation.js Normal file
View File

@ -0,0 +1,13 @@
class NavigationActionsClass {
setNavigator(navigator) {
this.navigator = navigator;
}
push = params => this.navigator && this.navigator.push(params);
pop = params => this.navigator && this.navigator.pop(params);
popToRoot = params => this.navigator && this.navigator.popToRoot(params);
resetTo = params => this.navigator && this.navigator.resetTo(params);
toggleDrawer = params => this.navigator && this.navigator.toggleDrawer(params);
}
export const NavigationActions = new NavigationActionsClass();

View File

@ -1,11 +1,14 @@
/* eslint-disable */
import { NativeModules } from 'react-native';
import Reactotron from 'reactotron-react-native';
import { reactotronRedux } from 'reactotron-redux';
import sagaPlugin from 'reactotron-redux-saga'
if (__DEV__) {
const scriptURL = NativeModules.SourceCode.scriptURL;
const scriptHostname = scriptURL.split('://')[1].split(':')[0];
Reactotron
.configure()
.configure({ host: scriptHostname })
.useReactNative()
.use(reactotronRedux())
.use(sagaPlugin())

View File

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

View File

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

View File

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

0
app/app.js Normal file
View File

View File

@ -1,40 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TouchableOpacity, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { NavigationActions } from 'react-navigation';
import { COLOR_TEXT } from '../constants/colors';
const styles = StyleSheet.create({
button: {
width: 25,
height: 25,
marginTop: 5
},
icon: {
color: COLOR_TEXT,
left: -5
}
});
export default class CloseModalButton extends React.PureComponent {
static propTypes = {
navigation: PropTypes.object.isRequired
}
render() {
return (
<TouchableOpacity
onPress={() => this.props.navigation.dispatch(NavigationActions.back())}
style={styles.button}
testID='close-modal-button'
>
<Icon
style={styles.icon}
name='close'
size={25}
/>
</TouchableOpacity>
);
}
}

View File

@ -1,51 +0,0 @@
import React from 'react';
import { View, StyleSheet, Platform } from 'react-native';
import PropTypes from 'prop-types';
import { SafeAreaView } from 'react-navigation';
let platformContainerStyles;
if (Platform.OS === 'ios') {
platformContainerStyles = {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0, 0, 0, .3)'
};
} else {
platformContainerStyles = {
shadowColor: 'black',
shadowOpacity: 0.1,
shadowRadius: StyleSheet.hairlineWidth,
shadowOffset: {
height: StyleSheet.hairlineWidth
},
elevation: 4
};
}
const height = Platform.OS === 'ios' ? 44 : 56;
const backgroundColor = Platform.OS === 'ios' ? '#F7F7F7' : '#FFF';
const styles = StyleSheet.create({
container: {
backgroundColor,
...platformContainerStyles
},
appBar: {
height,
backgroundColor
}
});
export default class Header extends React.PureComponent {
static propTypes = {
subview: PropTypes.object.isRequired
}
render() {
return (
<SafeAreaView forceInset={{ bottom: 'never' }} style={styles.container}>
<View style={styles.appBar}>
{this.props.subview}
</View>
</SafeAreaView>
);
}
}

View File

@ -21,7 +21,6 @@ import I18n from '../i18n';
@connect(
state => ({
actionMessage: state.messages.actionMessage,
user: state.login.user,
Message_AllowDeleting: state.settings.Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing: state.settings.Message_AllowEditing,
@ -42,9 +41,9 @@ import I18n from '../i18n';
export default class MessageActions extends React.Component {
static propTypes = {
actionsHide: PropTypes.func.isRequired,
room: PropTypes.object,
room: PropTypes.object.isRequired,
actionMessage: PropTypes.object,
user: PropTypes.object,
user: PropTypes.object.isRequired,
deleteRequest: PropTypes.func.isRequired,
editInit: PropTypes.func.isRequired,
toggleStarRequest: PropTypes.func.isRequired,

View File

@ -1,88 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Linking } from 'react-native';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import SplashScreen from 'react-native-splash-screen';
import { appInit } from '../actions';
import { deepLinkingOpen } from '../actions/deepLinking';
import AuthRoutes from './routes/AuthRoutes';
import PublicRoutes from './routes/PublicRoutes';
import * as NavigationService from './routes/NavigationService';
import parseQuery from '../lib/methods/helpers/parseQuery';
@connect(
state => ({
login: state.login,
app: state.app,
background: state.app.background
}),
dispatch => bindActionCreators({
appInit, deepLinkingOpen
}, dispatch)
)
export default class Routes extends React.Component {
static propTypes = {
login: PropTypes.object.isRequired,
app: PropTypes.object.isRequired,
appInit: PropTypes.func.isRequired
}
constructor(props) {
super(props);
this.handleOpenURL = this.handleOpenURL.bind(this);
}
componentDidMount() {
if (this.props.app.ready) {
return SplashScreen.hide();
}
this.props.appInit();
Linking
.getInitialURL()
.then(url => this.handleOpenURL({ url }))
.catch(console.error);
Linking.addEventListener('url', this.handleOpenURL);
}
componentWillReceiveProps(nextProps) {
if (nextProps.app.ready && this.props.app.ready !== nextProps.app.ready) {
SplashScreen.hide();
}
}
componentDidUpdate() {
NavigationService.setNavigator(this.navigator);
}
componentWillUnmount() {
Linking.removeEventListener('url', this.handleOpenURL);
}
handleOpenURL({ url }) {
if (url) {
url = url.replace(/rocketchat:\/\/|https:\/\/go.rocket.chat\//, '');
const regex = /^(room|auth)\?/;
if (url.match(regex)) {
url = url.replace(regex, '');
const params = parseQuery(url);
this.props.deepLinkingOpen(params);
}
}
}
render() {
const { login } = this.props;
if (this.props.app.starting) {
return null;
}
if (!login.token || login.isRegistering) {
return (<PublicRoutes ref={nav => this.navigator = nav} />);
}
return (<AuthRoutes ref={nav => this.navigator = nav} />);
}
}

View File

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

View File

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

View File

@ -32,7 +32,7 @@ export default class Typing extends React.Component {
}
get usersTyping() {
const users = this.props.usersTyping.filter(_username => this.props.username !== _username);
return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }` : '';
return users.length ? `${ users.join(', ') } ${ users.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }` : '';
}
render() {
const { usersTyping } = this;

View File

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

View File

@ -99,7 +99,11 @@ export default class Message extends React.Component {
Message_GroupingPeriod: PropTypes.number.isRequired,
customTimeFormat: PropTypes.string,
message: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
user: PropTypes.shape({
id: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
token: PropTypes.string.isRequired
}),
editing: PropTypes.bool,
errorActionsShow: PropTypes.func,
toggleReactionPicker: PropTypes.func,
@ -109,7 +113,8 @@ export default class Message extends React.Component {
onLongPress: PropTypes.func,
_updatedAt: PropTypes.instanceOf(Date),
archived: PropTypes.bool,
broadcast: PropTypes.bool
broadcast: PropTypes.bool,
previousItem: PropTypes.object
}
static defaultProps = {

View File

@ -1,190 +0,0 @@
import React from 'react';
import { Platform, TouchableOpacity } from 'react-native';
import { createStackNavigator, createDrawerNavigator } from 'react-navigation';
import Icon from 'react-native-vector-icons/MaterialIcons';
import Sidebar from '../../containers/Sidebar';
import RoomsListView from '../../views/RoomsListView';
import RoomView from '../../views/RoomView';
import RoomActionsView from '../../views/RoomActionsView';
import CreateChannelView from '../../views/CreateChannelView';
import SelectedUsersView from '../../views/SelectedUsersView';
import NewServerView from '../../views/NewServerView';
import StarredMessagesView from '../../views/StarredMessagesView';
import PinnedMessagesView from '../../views/PinnedMessagesView';
import MentionedMessagesView from '../../views/MentionedMessagesView';
import SnippetedMessagesView from '../../views/SnippetedMessagesView';
import SearchMessagesView from '../../views/SearchMessagesView';
import RoomFilesView from '../../views/RoomFilesView';
import RoomMembersView from '../../views/RoomMembersView';
import RoomInfoView from '../../views/RoomInfoView';
import RoomInfoEditView from '../../views/RoomInfoEditView';
import ProfileView from '../../views/ProfileView';
import SettingsView from '../../views/SettingsView';
import I18n from '../../i18n';
import sharedStyles from '../../views/Styles';
const headerTintColor = '#292E35';
const AuthRoutes = createStackNavigator(
{
RoomsList: {
screen: RoomsListView
},
Room: {
screen: RoomView
},
CreateChannel: {
screen: CreateChannelView,
navigationOptions: {
title: I18n.t('Create_Channel'),
headerTintColor
}
},
SelectedUsers: {
screen: SelectedUsersView,
navigationOptions: {
title: I18n.t('Select_Users'),
headerTintColor
}
},
AddServer: {
screen: NewServerView,
navigationOptions: {
title: I18n.t('New_Server'),
headerTintColor
}
},
RoomActions: {
screen: RoomActionsView,
navigationOptions: {
title: I18n.t('Actions'),
headerTintColor
}
},
StarredMessages: {
screen: StarredMessagesView,
navigationOptions: {
title: I18n.t('Starred_Messages'),
headerTintColor
}
},
PinnedMessages: {
screen: PinnedMessagesView,
navigationOptions: {
title: I18n.t('Pinned_Messages'),
headerTintColor
}
},
MentionedMessages: {
screen: MentionedMessagesView,
navigationOptions: {
title: I18n.t('Mentioned_Messages'),
headerTintColor
}
},
SnippetedMessages: {
screen: SnippetedMessagesView,
navigationOptions: {
title: I18n.t('Snippet_Messages'),
headerTintColor
}
},
SearchMessages: {
screen: SearchMessagesView,
navigationOptions: {
title: I18n.t('Search_Messages'),
headerTintColor
}
},
RoomFiles: {
screen: RoomFilesView,
navigationOptions: {
title: I18n.t('Room_Files'),
headerTintColor
}
},
RoomMembers: {
screen: RoomMembersView,
navigationOptions: {
title: I18n.t('Room_Members'),
headerTintColor
}
},
RoomInfo: {
screen: RoomInfoView,
navigationOptions: {
title: I18n.t('Room_Info'),
headerTintColor
}
},
RoomInfoEdit: {
screen: RoomInfoEditView,
navigationOptions: {
title: I18n.t('Room_Info_Edit'),
headerTintColor
}
}
},
{
navigationOptions: {
headerTitleAllowFontScaling: false
}
}
);
const MenuButton = ({ navigation, testID }) => (
<TouchableOpacity
style={sharedStyles.headerButton}
onPress={navigation.toggleDrawer}
accessibilityLabel={I18n.t('Toggle_Drawer')}
accessibilityTraits='button'
testID={testID}
>
<Icon name='menu' size={30} color='#292E35' />
</TouchableOpacity>
);
const Routes = createDrawerNavigator(
{
Chats: {
screen: AuthRoutes,
navigationOptions: {
drawerLabel: I18n.t('Chats'),
drawerIcon: () => <Icon name='chat-bubble' size={20} />
}
},
ProfileView: {
screen: createStackNavigator({
ProfileView: {
screen: ProfileView,
navigationOptions: ({ navigation }) => ({
title: I18n.t('Profile'),
headerTintColor: '#292E35',
headerLeft: <MenuButton navigation={navigation} testID='profile-view-sidebar' />
})
}
})
},
SettingsView: {
screen: createStackNavigator({
SettingsView: {
screen: SettingsView,
navigationOptions: ({ navigation }) => ({
title: I18n.t('Settings'),
headerTintColor: '#292E35',
headerLeft: <MenuButton navigation={navigation} testID='settings-view-sidebar' />
})
}
})
}
},
{
contentComponent: Sidebar,
drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked',
initialRouteName: 'Chats',
backBehavior: 'initialRoute'
}
);
export default Routes;

View File

@ -1,58 +0,0 @@
import { NavigationActions, StackActions } from 'react-navigation';
const config = {};
export function setNavigator(nav) {
if (nav) {
config.navigator = nav;
}
}
export function navigate(routeName, params) {
if (config.navigator && routeName) {
const action = NavigationActions.navigate({ key: routeName, routeName, params });
config.navigator.dispatch(action);
}
}
export function goBack() {
if (config.navigator) {
const action = NavigationActions.back({});
config.navigator.dispatch(action);
}
}
export function goRoomsList() {
if (config.navigator) {
const action = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ key: 'RoomsList', routeName: 'RoomsList' })]
});
config.navigator.dispatch(action);
}
}
export function goRoom({ rid, name }, counter = 0) {
// about counter: we can call this method before navigator be set. so we have to wait, if we tried a lot, we give up ...
if (!rid || counter > 10) {
return;
}
if (!config.navigator) {
return setTimeout(() => goRoom({ rid, name }, counter + 1), 100);
}
const action = StackActions.reset({
index: 1,
actions: [
NavigationActions.navigate({ key: 'RoomsList', routeName: 'RoomsList' }),
NavigationActions.navigate({ key: `Room-${ rid }`, routeName: 'Room', params: { room: { rid, name }, rid, name } })
]
});
config.navigator.dispatch(action);
}
export function dispatch(action) {
if (config.navigator) {
config.navigator.dispatch(action);
}
}

View File

@ -1,120 +0,0 @@
import React from 'react';
import { TouchableOpacity } from 'react-native';
import { createStackNavigator } from 'react-navigation';
import Icon from 'react-native-vector-icons/FontAwesome';
import ListServerView from '../../views/ListServerView';
import NewServerView from '../../views/NewServerView';
import LoginSignupView from '../../views/LoginSignupView';
import LoginView from '../../views/LoginView';
import RegisterView from '../../views/RegisterView';
import TermsServiceView from '../../views/TermsServiceView';
import PrivacyPolicyView from '../../views/PrivacyPolicyView';
import ForgotPasswordView from '../../views/ForgotPasswordView';
import database from '../../lib/realm';
import I18n from '../../i18n';
const hasServers = () => {
const db = database.databases.serversDB.objects('servers');
return db.length > 0;
};
const ServerStack = createStackNavigator({
ListServer: {
screen: ListServerView,
navigationOptions({ navigation }) {
return {
title: I18n.t('Servers'),
headerRight: (
<TouchableOpacity
onPress={() => navigation.navigate({ key: 'AddServer', routeName: 'AddServer' })}
style={{ width: 50, alignItems: 'center' }}
accessibilityLabel={I18n.t('Add_Server')}
accessibilityTraits='button'
>
<Icon name='plus' size={16} />
</TouchableOpacity>
)
};
}
},
AddServer: {
screen: NewServerView,
navigationOptions: {
header: null
}
},
LoginSignup: {
screen: LoginSignupView,
navigationOptions: {
header: null
}
}
}, {
headerMode: 'screen',
initialRouteName: hasServers() ? 'ListServer' : 'AddServer'
});
const LoginStack = createStackNavigator({
Login: {
screen: LoginView,
navigationOptions: {
header: null
}
},
ForgotPassword: {
screen: ForgotPasswordView,
navigationOptions: {
title: I18n.t('Forgot_my_password'),
headerTintColor: '#292E35'
}
}
}, {
headerMode: 'screen'
});
const RegisterStack = createStackNavigator({
Register: {
screen: RegisterView,
navigationOptions: {
header: null
}
},
TermsService: {
screen: TermsServiceView,
navigationOptions: {
title: I18n.t('Terms_of_Service'),
headerTintColor: '#292E35'
}
},
PrivacyPolicy: {
screen: PrivacyPolicyView,
navigationOptions: {
title: I18n.t('Privacy_Policy'),
headerTintColor: '#292E35'
}
}
}, {
headerMode: 'screen'
});
const PublicRoutes = createStackNavigator(
{
Server: {
screen: ServerStack
},
Login: {
screen: LoginStack
},
Register: {
screen: RegisterStack
}
},
{
mode: 'modal',
headerMode: 'none'
}
);
export default PublicRoutes;

View File

@ -12,41 +12,33 @@ const styles = StyleSheet.create({
}
});
@connect(state => ({
activeUsers: state.activeUsers,
user: state.login.user,
@connect((state, ownProps) => {
if (state.login.user && ownProps.id === state.login.user.id) {
return {
status: state.login.user && state.login.user.status,
offline: !state.meteor.connected
}))
};
}
export default class Status extends React.Component {
const user = state.activeUsers[ownProps.id];
return {
status: (user && user.status) || 'offline'
};
})
export default class Status extends React.PureComponent {
static propTypes = {
style: ViewPropTypes.style,
id: PropTypes.string,
activeUsers: PropTypes.object,
user: PropTypes.object,
status: PropTypes.string,
offline: PropTypes.bool
};
shouldComponentUpdate(nextProps) {
const { id: userId, user } = this.props;
if (user.id === userId) {
if (nextProps.offline !== this.props.offline) {
return true;
}
return (nextProps.user && nextProps.user.status !== user.status);
}
return (nextProps.activeUsers[userId] && nextProps.activeUsers[userId].status) !== this.status;
}
get status() {
const { id: userId, user, offline } = this.props;
if (user.id === userId) {
const { offline, status } = this.props;
if (offline) {
return 'offline';
}
return user.status || 'offline';
}
return (this.props.activeUsers && this.props.activeUsers[userId] && this.props.activeUsers[userId].status) || 'offline';
return status;
}
render() {

View File

@ -109,6 +109,7 @@ export default {
changing_avatar: 'changing avatar',
Channel_Name: 'Channel Name',
Chats: 'Chats',
Close: 'Close',
Close_emoji_selector: 'Close emoji selector',
Code: 'Code',
Colaborative: 'Colaborative',
@ -141,6 +142,7 @@ export default {
Forgot_my_password: 'Forgot my password',
Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.',
Forgot_password: 'Forgot password',
Forgot_Password: 'Forgot Password',
Has_joined_the_channel: 'Has joined the channel',
Has_left_the_channel: 'Has left the channel',
I_have_an_account: 'I have an account',
@ -164,6 +166,7 @@ export default {
Message_actions: 'Message actions',
Message_pinned: 'Message pinned',
Message_removed: 'Message removed',
Messages: 'Messages',
Microphone_Permission_Message: 'Rocket Chat needs access to your microphone so you can send audio message.',
Microphone_Permission: 'Microphone Permission',
Mute: 'Mute',
@ -294,6 +297,7 @@ export default {
Validating: 'Validating',
Video_call: 'Video call',
Voice_call: 'Voice call',
Welcome: 'Welcome',
Welcome_title_pt_1: 'Prepare to take off with',
Welcome_title_pt_2: 'the ultimate chat platform',
Yes_action_it: 'Yes, {{action}} it!',

View File

@ -1,14 +1,94 @@
import React from 'react';
import { Provider } from 'react-redux';
import { Component } from 'react';
import { Linking } from 'react-native';
import { Navigation } from 'react-native-navigation';
import store from './lib/createStore';
import { appInit } from './actions';
import database from './lib/realm';
import { iconsLoaded } from './Icons';
import { registerScreens } from './views';
import { deepLinkingOpen } from './actions/deepLinking';
import parseQuery from './lib/methods/helpers/parseQuery';
import I18n from './i18n';
import { initializePushNotifications } from './push';
import Routes from './containers/Routes';
const startLogged = () => {
Navigation.startSingleScreenApp({
screen: {
screen: 'RoomsListView',
title: I18n.t('Messages')
},
drawer: {
left: {
screen: 'Sidebar'
}
},
animationType: 'fade'
});
};
const RocketChat = () => (
<Provider store={store}>
<Routes />
</Provider>
);
const startNotLogged = (route) => {
Navigation.startSingleScreenApp({
screen: {
screen: route,
title: route === 'NewServerView' ? I18n.t('New_Server') : I18n.t('Servers')
},
animationType: 'fade'
});
};
export default RocketChat;
const hasServers = () => {
const db = database.databases.serversDB.objects('servers');
return db.length > 0;
};
const handleOpenURL = ({ url }) => {
if (url) {
url = url.replace(/rocketchat:\/\/|https:\/\/go.rocket.chat\//, '');
const regex = /^(room|auth)\?/;
if (url.match(regex)) {
url = url.replace(regex, '');
const params = parseQuery(url);
store.dispatch(deepLinkingOpen(params));
}
}
};
registerScreens(store);
iconsLoaded();
export default class App extends Component {
constructor(props) {
super(props);
store.dispatch(appInit());
store.subscribe(this.onStoreUpdate.bind(this));
initializePushNotifications();
Linking
.getInitialURL()
.then(url => handleOpenURL({ url }))
.catch(e => console.warn(e));
Linking.addEventListener('url', handleOpenURL);
}
onStoreUpdate = () => {
const { root } = store.getState().app;
if (this.currentRoot !== root) {
this.currentRoot = root;
if (root === 'outside') {
if (hasServers()) {
startNotLogged('ListServerView');
} else {
startNotLogged('NewServerView');
}
} else if (root === 'inside') {
startLogged();
}
}
}
setDeviceToken(deviceToken) {
this.deviceToken = deviceToken;
}
}

View File

@ -14,7 +14,7 @@ if (__DEV__) {
/* eslint-disable global-require */
const reduxImmutableStateInvariant = require('redux-immutable-state-invariant').default();
sagaMiddleware = createSagaMiddleware({
sagaMonitor: Reactotron.createSagaMonitor()
// sagaMonitor: Reactotron.createSagaMonitor()
});
enhancers = compose(

View File

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

View File

@ -139,7 +139,7 @@ const attachment = {
video_url: { type: 'string', optional: true },
title: { type: 'string', optional: true },
title_link: { type: 'string', optional: true },
title_link_download: { type: 'bool', optional: true },
// title_link_download: { type: 'bool', optional: true },
type: { type: 'string', optional: true },
author_icon: { type: 'string', optional: true },
author_name: { type: 'string', optional: true },

View File

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

View File

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

View File

@ -1,16 +1,19 @@
import PushNotification from 'react-native-push-notification';
import { AsyncStorage } from 'react-native';
import EJSON from 'ejson';
import { goRoom } from './containers/routes/NavigationService';
import { NavigationActions } from './Navigation';
const handleNotification = (notification) => {
if (!notification.userInteraction) {
return;
}
if (notification.userInteraction) {
const {
rid, name, sender, type
} = EJSON.parse(notification.ejson || notification.data.ejson);
return rid && goRoom({ rid, name: type === 'd' ? sender.username : name });
NavigationActions.push({
screen: 'RoomView',
passProps: { rid, name: type === 'd' ? sender.username : name }
});
}
};
PushNotification.configure({

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

@ -0,0 +1,42 @@
import EJSON from 'ejson';
import PushNotification from './push';
import store from '../lib/createStore';
import { deepLinkingOpen } from '../actions/deepLinking';
const onNotification = (notification) => {
if (notification) {
const data = notification.getData();
if (data) {
try {
const {
rid, name, sender, type, host
} = EJSON.parse(data.ejson);
const types = {
c: 'channel', d: 'direct', p: 'group'
};
const roomName = type === 'd' ? sender.username : name;
const params = {
host,
rid,
path: `${ types[type] }/${ roomName }`
};
store.dispatch(deepLinkingOpen(params));
} catch (e) {
console.warn(e);
}
}
}
};
const initializePushNotifications = () => {
PushNotification.configure({
onNotification
});
};
const getDeviceToken = () => PushNotification.getDeviceToken();
export { initializePushNotifications, getDeviceToken };

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

@ -0,0 +1,34 @@
import { NotificationsAndroid, PendingNotifications } from 'react-native-notifications';
class PushNotification {
constructor() {
this.onRegister = null;
this.onNotification = null;
this.deviceToken = null;
NotificationsAndroid.setRegistrationTokenUpdateListener((deviceToken) => {
this.deviceToken = deviceToken;
});
NotificationsAndroid.setNotificationOpenedListener((notification) => {
this.onNotification(notification);
});
}
getDeviceToken() {
return this.deviceToken;
}
configure(params) {
this.onRegister = params.onRegister;
this.onNotification = params.onNotification;
PendingNotifications.getInitialNotification()
.then((notification) => {
this.onNotification(notification);
})
.catch(e => console.warn(e));
}
}
export default new PushNotification();

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

@ -0,0 +1,31 @@
import NotificationsIOS from 'react-native-notifications';
class PushNotification {
constructor() {
this.onRegister = null;
this.onNotification = null;
this.deviceToken = null;
NotificationsIOS.addEventListener('remoteNotificationsRegistered', (deviceToken) => {
this.deviceToken = deviceToken;
});
NotificationsIOS.addEventListener('notificationOpened', (notification) => {
this.onNotification(notification);
});
NotificationsIOS.requestPermissions();
}
getDeviceToken() {
return this.deviceToken;
}
configure(params) {
this.onRegister = params.onRegister;
this.onNotification = params.onNotification;
NotificationsIOS.consumeBackgroundQueue();
}
}
export default new PushNotification();

View File

@ -2,6 +2,7 @@ import { FOREGROUND, BACKGROUND, INACTIVE } from 'redux-enhancer-react-native-ap
import { APP } from '../actions/actionsTypes';
const initialState = {
root: null,
starting: true,
ready: false,
inactive: false,
@ -31,6 +32,11 @@ export default function app(state = initialState, action) {
foreground: false,
background: false
};
case APP.START:
return {
...state,
root: action.root
};
case APP.INIT:
return {
...state,

View File

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

View File

@ -1,4 +1,4 @@
import { call, takeLatest, select, put, all } from 'redux-saga/effects';
import { call, takeLatest, select, put } from 'redux-saga/effects';
import { AsyncStorage } from 'react-native';
import { METEOR } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat';
@ -31,7 +31,8 @@ const test = function* test() {
const server = yield select(getServer);
const user = yield call(getToken);
// const response =
yield all([call(connect, server, user && user.token ? { resume: user.token, ...user.user } : undefined)]);// , put(loginRequest({ resume: user.token }))]);
// yield all([call(connect, server, user && user.token ? { resume: user.token, ...user.user } : undefined)]);// , put(loginRequest({ resume: user.token }))]);
yield call(connect, server, user && user.token ? { resume: user.token, ...user.user } : undefined);
// yield put(connectSuccess(response));
} catch (err) {
console.warn('test', err);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,8 +9,8 @@ import { addUserTyping, removeUserTyping, setLastOpen } from '../actions/room';
import { messagesRequest, editCancel } from '../actions/messages';
import RocketChat from '../lib/rocketchat';
import database from '../lib/realm';
import * as NavigationService from '../containers/routes/NavigationService';
import log from '../utils/log';
import { NavigationActions } from '../Navigation';
const leaveRoom = rid => RocketChat.leaveRoom(rid);
const eraseRoom = rid => RocketChat.eraseRoom(rid);
@ -139,7 +139,7 @@ const updateLastOpen = function* updateLastOpen() {
};
const goRoomsListAndDelete = function* goRoomsListAndDelete(rid) {
NavigationService.goRoomsList();
NavigationActions.popToRoot();
yield delay(1000);
try {
database.write(() => {

View File

@ -1,22 +1,23 @@
import { put, call, takeLatest, take } from 'redux-saga/effects';
import { put, call, takeLatest } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import { AsyncStorage } from 'react-native';
import { SERVER, LOGIN } from '../actions/actionsTypes';
import { NavigationActions } from '../Navigation';
import { SERVER } from '../actions/actionsTypes';
import * as actions from '../actions';
import { connectRequest } from '../actions/connect';
import { serverSuccess, serverFailure, setServer } from '../actions/server';
import { serverSuccess, serverFailure, selectServer } from '../actions/server';
import { setRoles } from '../actions/roles';
import RocketChat from '../lib/rocketchat';
import database from '../lib/realm';
import { navigate } from '../containers/routes/NavigationService';
import log from '../utils/log';
import I18n from '../i18n';
const validate = function* validate(server) {
return yield RocketChat.testServer(server);
};
const selectServer = function* selectServer({ server }) {
const handleSelectServer = function* handleSelectServer({ server }) {
try {
yield database.setActiveDB(server);
@ -36,7 +37,7 @@ const selectServer = function* selectServer({ server }) {
yield put(connectRequest());
} catch (e) {
log('selectServer', e);
log('handleSelectServer', e);
}
};
@ -53,12 +54,12 @@ const validateServer = function* validateServer({ server }) {
const addServer = function* addServer({ server }) {
try {
yield put(actions.appStart('outside'));
yield call(NavigationActions.resetTo, { screen: 'ListServerView', title: I18n.t('Servers') });
database.databases.serversDB.write(() => {
database.databases.serversDB.create('servers', { id: server, current: false }, true);
});
yield put(setServer(server));
yield take(LOGIN.SET_TOKEN);
navigate('LoginSignup');
yield put(selectServer(server));
} catch (e) {
log('addServer', e);
}
@ -66,7 +67,7 @@ const addServer = function* addServer({ server }) {
const root = function* root() {
yield takeLatest(SERVER.REQUEST, validateServer);
yield takeLatest(SERVER.SELECT, selectServer);
yield takeLatest(SERVER.SELECT, handleSelectServer);
yield takeLatest(SERVER.ADD, addServer);
};
export default root;

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -13,21 +13,19 @@ import scrollPersistTaps from '../utils/scrollPersistTaps';
import Button from '../containers/Button';
import I18n from '../i18n';
@connect(
state => ({
@connect(state => ({
createChannel: state.createChannel,
users: state.selectedUsers.users
}),
dispatch => ({
}), dispatch => ({
create: data => dispatch(createChannelRequest(data))
})
)
}))
/** @extends React.Component */
export default class CreateChannelView extends LoggedView {
static propTypes = {
navigator: PropTypes.object,
create: PropTypes.func.isRequired,
createChannel: PropTypes.object.isRequired,
users: PropTypes.array.isRequired,
navigation: PropTypes.object.isRequired
users: PropTypes.array.isRequired
};
constructor(props) {

View File

@ -20,12 +20,13 @@ import I18n from '../i18n';
forgotPasswordInit: () => dispatch(forgotPasswordInit()),
forgotPasswordRequest: email => dispatch(forgotPasswordRequest(email))
}))
/** @extends React.Component */
export default class ForgotPasswordView extends LoggedView {
static propTypes = {
navigator: PropTypes.object,
forgotPasswordInit: PropTypes.func.isRequired,
forgotPasswordRequest: PropTypes.func.isRequired,
login: PropTypes.object,
navigation: PropTypes.object.isRequired
login: PropTypes.object
}
constructor(props) {
@ -44,7 +45,7 @@ export default class ForgotPasswordView extends LoggedView {
componentDidUpdate() {
const { login } = this.props;
if (login.success) {
this.props.navigation.goBack();
this.props.navigator.pop();
setTimeout(() => {
showErrorAlert(I18n.t('Forgot_password_If_this_email_is_registered'), I18n.t('Alert'));
});

View File

@ -2,17 +2,16 @@ import React from 'react';
import Icon from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types';
// import Zeroconf from 'react-native-zeroconf';
import { View, Text, SectionList, StyleSheet, SafeAreaView } from 'react-native';
import { View, Text, SectionList, StyleSheet } from 'react-native';
import { connect } from 'react-redux';
import { withNavigationFocus } from 'react-navigation';
import LoggedView from './View';
import { setServer } from '../actions/server';
import { selectServer } from '../actions/server';
import database from '../lib/realm';
import Fade from '../animations/fade';
import Touch from '../utils/touch';
import I18n from '../i18n';
import { iconsMap } from '../Icons';
const styles = StyleSheet.create({
view: {
@ -63,22 +62,19 @@ const styles = StyleSheet.create({
}
});
// const zeroconf = new Zeroconf();
@connect(state => ({
server: state.server.server,
login: state.login,
connected: state.meteor.connected
}), dispatch => ({
selectServer: server => dispatch(setServer(server))
selectServer: server => dispatch(selectServer(server))
}))
class ListServerView extends LoggedView {
/** @extends React.Component */
export default class ListServerView extends LoggedView {
static propTypes = {
navigation: PropTypes.object.isRequired,
navigator: PropTypes.object,
login: PropTypes.object.isRequired,
selectServer: PropTypes.func.isRequired,
connected: PropTypes.bool.isRequired,
server: PropTypes.string
}
@ -88,51 +84,36 @@ class ListServerView extends LoggedView {
sections: []
};
this.data = database.databases.serversDB.objects('servers');
// this.redirected = false;
this.data.addListener(this.updateState);
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
async componentWillMount() {
this.props.navigator.setButtons({
rightButtons: [{
id: 'addServer',
icon: iconsMap.add
}]
});
}
componentDidMount() {
// zeroconf.on('update', this.updateState);
// zeroconf.scan('http', 'tcp', 'local.');
this.updateState();
this.jumpToSelectedServer();
}
// componentDidUpdate() {
// if (this.props.connected &&
// this.props.server &&
// !this.props.login.token &&
// !this.redirected) {
// this.redirected = true;
// this.props.navigation.navigate({ key: 'LoginSignup', routeName: 'LoginSignup' });
// } else if (!this.props.connected) {
// this.redirected = false;
// }
// }
componentWillUnmount() {
// zeroconf.stop();
this.data.removeAllListeners();
// zeroconf.removeListener('update', this.updateState);
}
openLogin = () => {
this.props.navigation.navigate({ key: 'LoginSignup', routeName: 'LoginSignup' });
onNavigatorEvent(event) {
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'),
data: this.data
}];
//
// this.state.nearBy = zeroconf.getServices();
// if (this.state.nearBy) {
// const nearBy = Object.keys(this.state.nearBy)
// .filter(key => this.state.nearBy[key].addresses);
// if (nearBy.length) {
// sections.push({
// title: 'Nearby',
// data: nearBy.map((key) => {
// const server = this.state.nearBy[key];
// const address = `http://${ server.addresses[0] }:${ server.port }`;
// return {
// id: address
// };
// })
// });
// }
// }
return {
...this.state,
@ -170,6 +133,26 @@ class ListServerView extends LoggedView {
};
};
openLogin = (server) => {
this.props.navigator.push({
screen: 'LoginSignupView',
title: server
});
}
selectAndNavigateTo = (server) => {
this.props.selectServer(server);
this.openLogin(server);
}
jumpToSelectedServer() {
if (this.props.server && !this.props.login.isRegistering) {
setTimeout(() => {
this.openLogin(this.props.server);
}, 1000);
}
}
updateState = () => {
this.setState(this.getState());
}
@ -210,7 +193,7 @@ class ListServerView extends LoggedView {
render() {
return (
<SafeAreaView style={styles.view} testID='list-server-view'>
<View style={styles.view} testID='list-server-view'>
<SectionList
style={styles.list}
sections={this.state.sections}
@ -219,8 +202,7 @@ class ListServerView extends LoggedView {
keyExtractor={item => item.id}
ItemSeparatorComponent={this.renderSeparator}
/>
</SafeAreaView>
</View>
);
}
}
export default withNavigationFocus(ListServerView);

View File

@ -1,13 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, View, ScrollView, TouchableOpacity, SafeAreaView, WebView, Platform, LayoutAnimation, Image, StyleSheet } from 'react-native';
import { Text, View, ScrollView, TouchableOpacity, LayoutAnimation, Image, StyleSheet } from 'react-native';
import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/FontAwesome';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { Base64 } from 'js-base64';
import Modal from 'react-native-modal';
import RocketChat from '../lib/rocketchat';
import { open, close } from '../actions/login';
import LoggedView from './View';
import sharedStyles from './Styles';
@ -17,9 +15,6 @@ import Button from '../containers/Button';
import Loading from '../containers/Loading';
import I18n from '../i18n';
const userAgentAndroid = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1';
const userAgent = Platform.OS === 'ios' ? 'UserAgent' : userAgentAndroid;
const styles = StyleSheet.create({
container: {
alignItems: 'center',
@ -44,14 +39,13 @@ const styles = StyleSheet.create({
planetImage: {
width: 200,
height: 162,
marginVertical: 20,
opacity: 0.6
marginVertical: 20
}
});
@connect(state => ({
server: state.server.server,
login: state.login,
isFetching: state.login.isFetching,
Accounts_EmailOrUsernamePlaceholder: state.settings.Accounts_EmailOrUsernamePlaceholder,
Accounts_PasswordPlaceholder: state.settings.Accounts_PasswordPlaceholder,
Accounts_OAuth_Facebook: state.settings.Accounts_OAuth_Facebook,
@ -63,17 +57,16 @@ const styles = StyleSheet.create({
Accounts_OAuth_Twitter: state.settings.Accounts_OAuth_Twitter,
services: state.login.services
}), dispatch => ({
loginOAuth: params => RocketChat.login(params),
open: () => dispatch(open()),
close: () => dispatch(close())
}))
/** @extends React.Component */
export default class LoginSignupView extends LoggedView {
static propTypes = {
loginOAuth: PropTypes.func.isRequired,
navigator: PropTypes.object,
open: PropTypes.func.isRequired,
close: PropTypes.func.isRequired,
navigation: PropTypes.object.isRequired,
login: PropTypes.object,
isFetching: PropTypes.bool,
server: PropTypes.string,
Accounts_EmailOrUsernamePlaceholder: PropTypes.bool,
Accounts_PasswordPlaceholder: PropTypes.string,
@ -89,13 +82,6 @@ export default class LoginSignupView extends LoggedView {
constructor(props) {
super('LoginSignupView', props);
this.state = {
modalVisible: false,
oAuthUrl: '',
showSocialButtons: false
};
this.redirectRegex = new RegExp(`(?=.*(${ this.props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g');
}
componentDidMount() {
@ -183,19 +169,29 @@ export default class LoginSignupView extends LoggedView {
}
openOAuth = (oAuthUrl) => {
this.setState({ oAuthUrl, modalVisible: true });
this.props.navigator.showModal({
screen: 'OAuthView',
title: 'OAuth',
passProps: {
oAuthUrl
}
});
}
login = () => {
this.props.navigator.push({
screen: 'LoginView',
title: this.props.server,
backButtonTitle: I18n.t('Welcome')
});
}
register = () => {
this.props.navigation.navigate({ key: 'Register', routeName: 'Register' });
}
closeOAuth = () => {
this.setState({ modalVisible: false });
}
toggleSocialButtons = () => {
this.setState({ showSocialButtons: !this.state.showSocialButtons });
this.props.navigator.push({
screen: 'RegisterView',
title: this.props.server,
backButtonTitle: I18n.t('Welcome')
});
}
renderServices = () => {
@ -279,13 +275,11 @@ export default class LoginSignupView extends LoggedView {
render() {
return (
[
<ScrollView
key='login-view'
style={[sharedStyles.container, sharedStyles.containerScrollView]}
{...scrollPersistTaps}
>
<SafeAreaView testID='welcome-view'>
<View testID='welcome-view'>
<View style={styles.container}>
<Image
source={require('../static/images/logo.png')}
@ -301,44 +295,20 @@ export default class LoginSignupView extends LoggedView {
<Button
title={I18n.t('I_have_an_account')}
type='primary'
onPress={() => this.props.navigation.navigate({ key: 'Login', routeName: 'Login' })}
onPress={() => this.login()}
testID='welcome-view-login'
/>
<Button
title={I18n.t('Create_account')}
type='secondary'
onPress={() => this.props.navigation.navigate({ key: 'Register', routeName: 'Register' })}
onPress={() => this.register()}
testID='welcome-view-register'
/>
{this.renderServices()}
</View>
<Loading visible={this.props.login.isFetching} />
</SafeAreaView>
</ScrollView>,
<Modal
key='modal-oauth'
visible={this.state.modalVisible}
animationType='slide'
style={sharedStyles.oAuthModal}
onBackButtonPress={this.closeOAuth}
useNativeDriver
>
<WebView
source={{ uri: this.state.oAuthUrl }}
userAgent={userAgent}
onNavigationStateChange={(webViewState) => {
const url = decodeURIComponent(webViewState.url);
if (this.redirectRegex.test(url)) {
const parts = url.split('#');
const credentials = JSON.parse(parts[1]);
this.props.loginOAuth({ oauth: { ...credentials } });
this.setState({ modalVisible: false });
}
}}
/>
<Icon name='close' size={30} style={sharedStyles.closeOAuth} onPress={this.closeOAuth} />
</Modal>
]
<Loading visible={this.props.isFetching} />
</View>
</ScrollView>
);
}
}

View File

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

View File

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

View File

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

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

@ -0,0 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import { WebView, Platform } from 'react-native';
import { connect } from 'react-redux';
import RocketChat from '../lib/rocketchat';
import I18n from '../i18n';
const userAgentAndroid = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1';
const userAgent = Platform.OS === 'ios' ? 'UserAgent' : userAgentAndroid;
@connect(state => ({
server: state.server.server
}))
export default class TermsServiceView extends React.PureComponent {
static navigatorButtons = {
leftButtons: [{
id: 'close',
title: I18n.t('Close')
}]
}
static propTypes = {
navigator: PropTypes.object,
oAuthUrl: PropTypes.string,
server: PropTypes.string
}
constructor(props) {
super(props);
this.redirectRegex = new RegExp(`(?=.*(${ props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g');
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
onNavigatorEvent(event) {
const { navigator } = this.props;
if (event.type === 'NavBarButtonPress') {
if (event.id === 'close') {
navigator.dismissModal();
}
}
}
login = async(params) => {
try {
await RocketChat.login(params);
} catch (e) {
console.warn(e);
}
}
render() {
return (
<WebView
source={{ uri: this.props.oAuthUrl }}
userAgent={userAgent}
onNavigationStateChange={(webViewState) => {
const url = decodeURIComponent(webViewState.url);
if (this.redirectRegex.test(url)) {
const parts = url.split('#');
const credentials = JSON.parse(parts[1]);
this.login({ oauth: { ...credentials } });
this.props.navigator.dismissModal();
}
}}
/>
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, ScrollView } from 'react-native';
import { connect } from 'react-redux';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import moment from 'moment';
import LoggedView from '../View';
@ -12,11 +11,11 @@ import styles from './styles';
import sharedStyles from '../Styles';
import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat';
import Touch from '../../utils/touch';
import log from '../../utils/log';
import RoomTypeIcon from '../../containers/RoomTypeIcon';
import I18n from '../../i18n';
import { iconsMap } from '../../Icons';
const PERMISSION_EDIT_ROOM = 'edit-room';
@ -28,49 +27,28 @@ const getRoomTitle = room => (room.t === 'd' ?
<Text testID='room-info-view-name' style={styles.roomTitle} key='room-info-name'>{room.name}</Text>
]
);
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
user: state.login.user,
userId: state.login.user && state.login.user.id,
activeUsers: state.activeUsers,
Message_TimeFormat: state.settings.Message_TimeFormat,
roles: state.roles
}))
/** @extends React.Component */
export default class RoomInfoView extends LoggedView {
static propTypes = {
baseUrl: PropTypes.string,
user: PropTypes.object,
navigation: PropTypes.object,
navigator: PropTypes.object,
rid: PropTypes.string,
userId: PropTypes.string,
activeUsers: PropTypes.object,
Message_TimeFormat: PropTypes.string,
roles: PropTypes.object
}
static navigationOptions = ({ navigation }) => {
const params = navigation.state.params || {};
if (!params.hasEditPermission) {
return;
}
return {
headerRight: (
<Touch
onPress={() => navigation.navigate({ key: 'RoomInfoEdit', routeName: 'RoomInfoEdit', params: { rid: navigation.state.params.rid } })}
underlayColor='#ffffff'
activeOpacity={0.5}
accessibilityLabel={I18n.t('edit')}
accessibilityTraits='button'
testID='room-info-view-edit-button'
>
<View style={sharedStyles.headerButton}>
<MaterialIcon name='edit' size={20} />
</View>
</Touch>
)
};
};
constructor(props) {
super('RoomInfoView', props);
const { rid } = props.navigation.state.params;
const { rid } = props;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.sub = {
unsubscribe: () => {}
@ -80,6 +58,7 @@ export default class RoomInfoView extends LoggedView {
roomUser: {},
roles: []
};
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
async componentDidMount() {
@ -87,9 +66,10 @@ export default class RoomInfoView extends LoggedView {
this.rooms.addListener(this.updateRoom);
// get user of room
if (this.state.room) {
if (this.state.room.t === 'd') {
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 });
const username = this.state.room.name;
@ -112,7 +92,16 @@ export default class RoomInfoView extends LoggedView {
}
} else {
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();
}
onNavigatorEvent(event) {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'edit') {
this.props.navigator.push({
screen: 'RoomInfoEditView',
title: I18n.t('Room_Info_Edit'),
passProps: {
rid: this.props.rid
}
});
}
}
}
getFullUserData = async(username) => {
try {
const result = await RocketChat.subscribe('fullUserData', username);
@ -170,7 +173,6 @@ export default class RoomInfoView extends LoggedView {
if (!utcOffset) {
return null;
}
// TODO: translate
return (
<View style={styles.item}>
<Text style={styles.itemLabel}>{I18n.t('Timezone')}</Text>

View File

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

View File

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

View File

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

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