forked from Ivasoft/mattermost-mobile
Compare commits
38 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc179a6516 | ||
|
|
fea6372819 | ||
|
|
87e89de854 | ||
|
|
534af426c9 | ||
|
|
553f3796b1 | ||
|
|
18b3d6eec9 | ||
|
|
73c81bb863 | ||
|
|
6e4abacb4a | ||
|
|
9de87abdae | ||
|
|
d4b3e2ffc3 | ||
|
|
bffc75f8d1 | ||
|
|
b8d8be78d5 | ||
|
|
ecec78b2e0 | ||
|
|
f0f9195903 | ||
|
|
a291fdc933 | ||
|
|
86261dad30 | ||
|
|
cd5fb71681 | ||
|
|
2b33618f31 | ||
|
|
57c602eb79 | ||
|
|
3c32d5a9ed | ||
|
|
c6ae1090f7 | ||
|
|
f720b96f88 | ||
|
|
321a2bf576 | ||
|
|
d88f59bc6b | ||
|
|
010336bb64 | ||
|
|
5422b5b472 | ||
|
|
f7cbf3afd0 | ||
|
|
aeb3691696 | ||
|
|
523a66e023 | ||
|
|
9d4562841a | ||
|
|
0dbbf65477 | ||
|
|
404ac82ca2 | ||
|
|
8107dff4fe | ||
|
|
d9b76386c0 | ||
|
|
a0a1678939 | ||
|
|
4928355dae | ||
|
|
b7b817a973 | ||
|
|
0eeb9453c9 |
156
NOTICE.txt
156
NOTICE.txt
@@ -1123,3 +1123,159 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## analytics-react-native
|
||||
|
||||
This product contains 'analytics-react-native', A React Native client for Segment. The hassle-free way to integrate analytics into any application.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/neiker/analytics-react-native
|
||||
|
||||
* LICENSE:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Javier Alvarez
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-passcode-status
|
||||
|
||||
This product contains 'react-native-passcode-status', A thin wrapper around UIDevice-PasscodeStatus.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/tradle/react-native-passcode-status
|
||||
|
||||
* LICENSE:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Tradle
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-local-auth
|
||||
|
||||
This product contains 'react-native-local-auth', Authenticate users with Touch ID, with optional fallback to passcode.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/tradle/react-native-local-auth
|
||||
|
||||
* LICENSE:
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2015, [Tradle, Inc](http://tradle.io/)
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## jail-monkey
|
||||
|
||||
This product contains 'jail-monkey', Identify if a phone has been jail-broken or rooted for iOS/Android.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/GantMan/jail-monkey/
|
||||
|
||||
* LICENSE:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Gant Laborde
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-fast-image
|
||||
|
||||
FastImage, performant React Native image component.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/DylanVann/react-native-fast-image
|
||||
|
||||
* LICENSE:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Dylan Vann
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Mattermost Mobile
|
||||
|
||||
**Supported Server Versions:** 3.8+
|
||||
**Supported Server Versions:** 4.0+
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 11 languages. Learn more at https://mattermost.com.
|
||||
|
||||
@@ -132,9 +132,11 @@ We plan to add support for tablets in the future, but the timeline depends on ho
|
||||
|
||||
### I keep getting a message "Cannot connect to the server. Please check your server URL and internet connection."
|
||||
|
||||
Our second generation mobile apps only support server versions 3.8+. If your server version is too old, you might see this error message come up.
|
||||
This sometimes appears when there is an issue with the SSL certitificate configuration.
|
||||
|
||||
To check your server version, log on to the site on desktop and go to Main Menu > About Mattermost.
|
||||
To check that your SSL certificate is set up correctly, test the SSL certificate by visiting a site such as https://www.ssllabs.com/ssltest/index.html. If there’s an error about the missing chain or certificate path, there is likely an intermediate certificate missing that needs to be included.
|
||||
|
||||
Please note that the apps cannot connect to servers with self-signed certificates, consider using [Let's Encrypt](https://docs.mattermost.com/install/config-ssl-http2-nginx.html) instead.
|
||||
|
||||
### I see a “Connecting…” bar that does not go away
|
||||
|
||||
|
||||
@@ -91,8 +91,8 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 23
|
||||
versionCode 47
|
||||
versionName "1.1.0"
|
||||
versionCode 49
|
||||
versionName "1.2.0"
|
||||
multiDexEnabled true
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
@@ -160,6 +160,8 @@ dependencies {
|
||||
compile project(':react-native-linear-gradient')
|
||||
compile project(':react-native-vector-icons')
|
||||
compile project(':react-native-svg')
|
||||
compile project(':react-native-local-auth')
|
||||
compile project(':jail-monkey')
|
||||
}
|
||||
|
||||
// Run this once to be able to run the application with BUCK
|
||||
|
||||
@@ -29,7 +29,10 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
>
|
||||
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\0"/>
|
||||
<meta-data android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
|
||||
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\0"/>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.github.yamill.orientation.OrientationPackage;
|
||||
import com.psykar.cookiemanager.CookieManagerPackage;
|
||||
import com.BV.LinearGradient.LinearGradientPackage;
|
||||
|
||||
import com.reactnativenavigation.controllers.SplashActivity;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
@@ -37,23 +33,23 @@ public class MainActivity extends SplashActivity {
|
||||
@Override
|
||||
public LinearLayout createSplashLayout() {
|
||||
wr_activity = new WeakReference<>(this);
|
||||
LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||
Context context = getActivity();
|
||||
final int drawableId = getImageId();
|
||||
|
||||
Context context = getActivity();
|
||||
NotificationsLifecycleFacade.getInstance().LoadManagedConfig(getActivity());
|
||||
|
||||
imageView = new ImageView(context);
|
||||
imageView.setImageResource(drawableId);
|
||||
|
||||
LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||
imageView.setLayoutParams(layoutParams);
|
||||
|
||||
imageView.setScaleType(ImageView.ScaleType.CENTER);
|
||||
|
||||
LinearLayout view = new LinearLayout(this);
|
||||
|
||||
view.setBackgroundColor(Color.parseColor("#FFFFFF"));
|
||||
view.setGravity(Gravity.CENTER);
|
||||
|
||||
view.addView(imageView);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Application;
|
||||
import android.util.Log;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.gantix.JailMonkey.JailMonkeyPackage;
|
||||
import io.tradle.react.LocalAuthPackage;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
@@ -33,8 +34,7 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class MainApplication extends NavigationApplication implements INotificationsApplication {
|
||||
|
||||
NotificationsLifecycleFacade notificationsLifecycleFacade;
|
||||
public NotificationsLifecycleFacade notificationsLifecycleFacade;
|
||||
|
||||
@Override
|
||||
public boolean isDebug() {
|
||||
@@ -55,20 +55,22 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
new SvgPackage(),
|
||||
new LinearGradientPackage(),
|
||||
new OrientationPackage(),
|
||||
new RNNotificationsPackage(MainApplication.this)
|
||||
new RNNotificationsPackage(this),
|
||||
new LocalAuthPackage(),
|
||||
new JailMonkeyPackage(),
|
||||
new MattermostManagedPackage()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
instance = this;
|
||||
// Create an object of the custom facade impl
|
||||
notificationsLifecycleFacade = new NotificationsLifecycleFacade();
|
||||
notificationsLifecycleFacade = NotificationsLifecycleFacade.getInstance();
|
||||
// Attach it to react-native-navigation
|
||||
setActivityCallbacks(notificationsLifecycleFacade);
|
||||
|
||||
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
|
||||
public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
private static MattermostManagedModule instance;
|
||||
|
||||
private boolean shouldBlurAppScreen = false;
|
||||
|
||||
private MattermostManagedModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
public static MattermostManagedModule getInstance(ReactApplicationContext reactContext) {
|
||||
if (instance == null) {
|
||||
instance = new MattermostManagedModule(reactContext);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static MattermostManagedModule getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "MattermostManaged";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void blurAppScreen(boolean enabled) {
|
||||
shouldBlurAppScreen = enabled;
|
||||
}
|
||||
|
||||
public boolean isBlurAppScreenEnabled() {
|
||||
return shouldBlurAppScreen;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getConfig(final Promise promise) {
|
||||
try {
|
||||
Bundle config = NotificationsLifecycleFacade.getInstance().getManagedConfig();
|
||||
|
||||
if (config != null) {
|
||||
Object result = Arguments.fromBundle(config);
|
||||
promise.resolve(result);
|
||||
} else {
|
||||
throw new Exception("The MDM vendor has not sent any Managed configuration");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
promise.reject("no managed configuration", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
|
||||
public class MattermostManagedPackage implements ReactPackage {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(MattermostManagedModule.getInstance(reactContext));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,20 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.RestrictionsManager;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.ArraySet;
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
import com.reactnativenavigation.NavigationApplication;
|
||||
import com.reactnativenavigation.controllers.ActivityCallbacks;
|
||||
import com.reactnativenavigation.react.ReactGateway;
|
||||
@@ -12,16 +23,72 @@ 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 static NotificationsLifecycleFacade instance;
|
||||
|
||||
private Bundle managedConfig = null;
|
||||
private Activity mVisibleActivity;
|
||||
private Set<AppVisibilityListener> mListeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
private final IntentFilter restrictionsFilter =
|
||||
new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
|
||||
|
||||
private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() {
|
||||
@Override public void onReceive(Context context, Intent intent) {
|
||||
|
||||
if (mVisibleActivity != null) {
|
||||
// Get the current configuration bundle
|
||||
RestrictionsManager myRestrictionsMgr =
|
||||
(RestrictionsManager) mVisibleActivity
|
||||
.getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
|
||||
|
||||
// Check current configuration settings, change your app's UI and
|
||||
// functionality as necessary.
|
||||
Log.i("ReactNative", "Managed Configuration Changed");
|
||||
sendConfigChanged(managedConfig);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static NotificationsLifecycleFacade getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new NotificationsLifecycleFacade();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
|
||||
MattermostManagedModule managedModule = MattermostManagedModule.getInstance();
|
||||
if (managedModule != null && managedModule.isBlurAppScreenEnabled()) {
|
||||
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
|
||||
LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
if (managedConfig!= null && managedConfig.size() > 0) {
|
||||
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) {
|
||||
switchToVisible(activity);
|
||||
|
||||
if (managedConfig != null && managedConfig.size() > 0) {
|
||||
RestrictionsManager myRestrictionsMgr =
|
||||
(RestrictionsManager) activity
|
||||
.getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
|
||||
Bundle newConfig = myRestrictionsMgr.getApplicationRestrictions();
|
||||
if (!equalBundles(newConfig ,managedConfig)) {
|
||||
Log.i("ReactNative", "onResumed Managed Configuration Changed");
|
||||
managedConfig = newConfig;
|
||||
sendConfigChanged(managedConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -32,6 +99,13 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
|
||||
@Override
|
||||
public void onActivityStopped(Activity activity) {
|
||||
switchToInvisible(activity);
|
||||
if (managedConfig != null && managedConfig.size() > 0) {
|
||||
try {
|
||||
activity.unregisterReceiver(restrictionsReceiver);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Just ignore this cause the receiver wasn't registered for this activity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,4 +162,67 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void LoadManagedConfig(Activity activity) {
|
||||
RestrictionsManager myRestrictionsMgr =
|
||||
(RestrictionsManager) activity
|
||||
.getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
|
||||
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
|
||||
myRestrictionsMgr = null;
|
||||
}
|
||||
|
||||
public synchronized Bundle getManagedConfig() {
|
||||
if (managedConfig!= null && managedConfig.size() > 0) {
|
||||
return managedConfig;
|
||||
}
|
||||
|
||||
if (mVisibleActivity != null) {
|
||||
LoadManagedConfig(mVisibleActivity);
|
||||
return managedConfig;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void sendConfigChanged(Bundle config) {
|
||||
Object result = Arguments.fromBundle(config);
|
||||
getRunningReactContext().
|
||||
getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
|
||||
emit("managedConfigDidChange", result);
|
||||
}
|
||||
|
||||
private boolean equalBundles(Bundle one, Bundle two) {
|
||||
if (one == null || two == null)
|
||||
return false;
|
||||
|
||||
if(one.size() != two.size())
|
||||
return false;
|
||||
|
||||
Set<String> setOne = new ArraySet<String>();
|
||||
setOne.addAll(one.keySet());
|
||||
setOne.addAll(two.keySet());
|
||||
Object valueOne;
|
||||
Object valueTwo;
|
||||
|
||||
for(String key : setOne) {
|
||||
if (!one.containsKey(key) || !two.containsKey(key))
|
||||
return false;
|
||||
|
||||
valueOne = one.get(key);
|
||||
valueTwo = two.get(key);
|
||||
if(valueOne instanceof Bundle && valueTwo instanceof Bundle &&
|
||||
!equalBundles((Bundle) valueOne, (Bundle) valueTwo)) {
|
||||
return false;
|
||||
}
|
||||
else if(valueOne == null) {
|
||||
if(valueTwo != null)
|
||||
return false;
|
||||
}
|
||||
else if(!valueOne.equals(valueTwo))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<?xml version="1.0"?>
|
||||
<resources>
|
||||
|
||||
|
||||
<string name="app_name">Mattermost Beta</string>
|
||||
<string name="inAppPinCode_title">in-App Pincode</string>
|
||||
<string name="inAppPinCode_description">Require users to authenticate as the owner of the phone before using the app. Prompts for fingerprint or passcode when the app first opens and when the app has been in the background for more than 5 minutes.</string>
|
||||
<string name="blurApplicationScreen_title">Blur Application Screen</string>
|
||||
<string name="blurApplicationScreen_description">Blur the app when it’s set to background to protect any confidential on-screen information, it also prevents taking screenshots of the app.</string>
|
||||
<string name="jailbreakProtection_title">Jailbreak / Root Detection</string>
|
||||
<string name="jailbreakProtection_description">Disable app launch on Jailbroken or rooted devices.</string>
|
||||
<string name="copyAndPasteProtection_title">Copy&Paste Protection</string>
|
||||
<string name="copyAndPasteProtection_description">Disable the ability to copy from or paste into any text inputs in the app.</string>
|
||||
<string name="serverUrl_title">Mattermost Server URL</string>
|
||||
<string name="serverUrl_description">Set a default Mattermost server URL.</string>
|
||||
<string name="allowOtherServers_title">Allow Other Servers</string>
|
||||
<string name="allowOtherServers_description">Allow the user to change the above server URL.</string>
|
||||
<string name="username_title">Default Username</string>
|
||||
<string name="username_description">Set the username or email address to use to authenticate against the Mattermost Server.</string>
|
||||
<string name="vendor_title">EMM Vendor or Company Name</string>
|
||||
<string name="vendor_description">Name of the EMM vendor or company deploying the app. Used in help text when prompting for passcodes so users are aware why the app is being protected.</string>
|
||||
</resources>
|
||||
|
||||
53
android/app/src/main/res/xml/app_restrictions.xml
Normal file
53
android/app/src/main/res/xml/app_restrictions.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<restrictions xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<restriction
|
||||
android:key="inAppPinCode"
|
||||
android:title="@string/inAppPinCode_title"
|
||||
android:description="@string/inAppPinCode_description"
|
||||
android:restrictionType="string"
|
||||
android:defaultValue="false" />
|
||||
<restriction
|
||||
android:key="blurApplicationScreen"
|
||||
android:title="@string/blurApplicationScreen_title"
|
||||
android:description="@string/blurApplicationScreen_description"
|
||||
android:restrictionType="string"
|
||||
android:defaultValue="false" />
|
||||
<restriction
|
||||
android:key="jailbreakProtection"
|
||||
android:title="@string/jailbreakProtection_title"
|
||||
android:description="@string/jailbreakProtection_description"
|
||||
android:restrictionType="string"
|
||||
android:defaultValue="false" />
|
||||
<restriction
|
||||
android:key="copyAndPasteProtection"
|
||||
android:title="@string/copyAndPasteProtection_title"
|
||||
android:description="@string/copyAndPasteProtection_description"
|
||||
android:restrictionType="string"
|
||||
android:defaultValue="false" />
|
||||
<restriction
|
||||
android:key="serverUrl"
|
||||
android:title="@string/serverUrl_title"
|
||||
android:description="@string/serverUrl_description"
|
||||
android:restrictionType="string"
|
||||
android:defaultValue="" />
|
||||
<restriction
|
||||
android:key="allowOtherServers"
|
||||
android:title="@string/allowOtherServers_title"
|
||||
android:description="@string/allowOtherServers_description"
|
||||
android:restrictionType="string"
|
||||
android:defaultValue="true" />
|
||||
<restriction
|
||||
android:key="username"
|
||||
android:title="@string/username_title"
|
||||
android:description="@string/username_description"
|
||||
android:restrictionType="string"
|
||||
android:defaultValue="" />
|
||||
<restriction
|
||||
android:key="vendor"
|
||||
android:title="@string/vendor_title"
|
||||
android:description="@string/vendor_description"
|
||||
android:restrictionType="string"
|
||||
android:defaultValue="" />
|
||||
|
||||
</restrictions>
|
||||
@@ -1,4 +1,8 @@
|
||||
rootProject.name = 'Mattermost'
|
||||
include ':jail-monkey'
|
||||
project(':jail-monkey').projectDir = new File(rootProject.projectDir, '../node_modules/jail-monkey/android')
|
||||
include ':react-native-local-auth'
|
||||
project(':react-native-local-auth').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-local-auth/android')
|
||||
include ':react-native-navigation'
|
||||
project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/')
|
||||
include ':react-native-image-picker'
|
||||
|
||||
@@ -167,13 +167,14 @@ export function loadFilesForPostIfNecessary(postId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadThreadIfNecessary(rootId) {
|
||||
export function loadThreadIfNecessary(rootId, channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {posts} = state.entities.posts;
|
||||
const {posts, postsInChannel} = state.entities.posts;
|
||||
const channelPosts = postsInChannel[channelId];
|
||||
|
||||
if (rootId && !posts[rootId]) {
|
||||
getPostThread(rootId)(dispatch, getState);
|
||||
if (rootId && (!posts[rootId] || !channelPosts || !channelPosts[rootId])) {
|
||||
getPostThread(rootId, false)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,14 +11,17 @@ import {
|
||||
import {handleTeamChange, selectFirstAvailableTeam} from 'app/actions/views/select_team';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getClientConfig, getLicenseConfig, setServerVersion} from 'mattermost-redux/actions/general';
|
||||
import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
|
||||
import {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
|
||||
|
||||
export function loadConfigAndLicense(serverVersion) {
|
||||
export function loadConfigAndLicense() {
|
||||
return async (dispatch, getState) => {
|
||||
getClientConfig()(dispatch, getState);
|
||||
getLicenseConfig()(dispatch, getState);
|
||||
setServerVersion(serverVersion)(dispatch, getState);
|
||||
const [config, license] = await Promise.all([
|
||||
getClientConfig()(dispatch, getState),
|
||||
getLicenseConfig()(dispatch, getState)
|
||||
]);
|
||||
|
||||
return {config, license};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ListView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
@@ -300,7 +299,7 @@ export default class AtMention extends Component {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
section: {
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 8,
|
||||
@@ -368,5 +367,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
flexWrap: 'wrap',
|
||||
paddingRight: 8
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ListView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
@@ -238,7 +237,7 @@ export default class ChannelMention extends Component {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
section: {
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 8,
|
||||
@@ -291,5 +290,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
color: theme.centerChannelColor,
|
||||
opacity: 0.6
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
@@ -135,7 +134,7 @@ export default class EmojiSuggestion extends Component {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
emoji: {
|
||||
marginRight: 5
|
||||
},
|
||||
@@ -160,5 +159,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ export default class Badge extends PureComponent {
|
||||
setTimeout(() => {
|
||||
this.setNativeProps({
|
||||
style: {
|
||||
display: 'flex'
|
||||
opacity: 1
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
@@ -125,7 +125,7 @@ export default class Badge extends PureComponent {
|
||||
>
|
||||
<View
|
||||
ref='badgeContainer'
|
||||
style={[styles.badge, this.props.style, {display: 'none'}]}
|
||||
style={[styles.badge, this.props.style, {opacity: 0}]}
|
||||
>
|
||||
<View style={styles.wrapper}>
|
||||
{this.renderText()}
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
TouchableHighlight,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import Badge from 'app/components/badge';
|
||||
import ChanneIcon from 'app/components/channel_icon';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
@@ -50,11 +50,14 @@ export default class ChannelItem extends PureComponent {
|
||||
|
||||
if (mentions && !isActive) {
|
||||
badge = (
|
||||
<View style={style.badgeContainer}>
|
||||
<Text style={style.badge}>
|
||||
{mentions}
|
||||
</Text>
|
||||
</View>
|
||||
<Badge
|
||||
style={style.badge}
|
||||
countStyle={style.mention}
|
||||
count={mentions}
|
||||
minHeight={20}
|
||||
minWidth={20}
|
||||
onPress={this.onPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +111,7 @@ export default class ChannelItem extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
@@ -143,19 +146,18 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
textUnread: {
|
||||
color: theme.sidebarUnreadText
|
||||
},
|
||||
badgeContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.mentionBj,
|
||||
borderRadius: 7,
|
||||
height: 15,
|
||||
justifyContent: 'center',
|
||||
marginRight: 16,
|
||||
width: 16
|
||||
},
|
||||
badge: {
|
||||
backgroundColor: theme.mentionBj,
|
||||
borderColor: theme.sidebarHeaderBg,
|
||||
borderRadius: 10,
|
||||
borderWidth: 1,
|
||||
padding: 3,
|
||||
position: 'relative',
|
||||
right: 16
|
||||
},
|
||||
mention: {
|
||||
color: theme.mentionColor,
|
||||
fontSize: 10,
|
||||
fontWeight: '600'
|
||||
fontSize: 10
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View
|
||||
@@ -245,7 +244,7 @@ class ChannelsList extends Component {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
flex: 1
|
||||
@@ -401,7 +400,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center'
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(ChannelsList);
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
InteractionManager,
|
||||
FlatList,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View
|
||||
@@ -43,8 +42,7 @@ class TeamsList extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).
|
||||
then((source) => {
|
||||
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => {
|
||||
this.closeButton = source;
|
||||
});
|
||||
}
|
||||
@@ -218,7 +216,7 @@ class TeamsList extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
flex: 1
|
||||
@@ -328,7 +326,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
color: theme.mentionColor,
|
||||
fontSize: 10
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(TeamsList);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -127,7 +126,7 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
marginRight: 12,
|
||||
alignItems: 'center'
|
||||
@@ -174,5 +173,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
groupInfo: {
|
||||
color: theme.centerChannelColor
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
@@ -301,7 +300,6 @@ class ChannelIntro extends PureComponent {
|
||||
|
||||
case General.PRIVATE_CHANNEL:
|
||||
return this.buildPrivateChannelContent();
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@@ -327,7 +325,7 @@ class ChannelIntro extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
channelTitle: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 19,
|
||||
@@ -368,7 +366,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-start'
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(ChannelIntro);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
@@ -76,7 +75,7 @@ channelLoader.propTypes = {
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
avatar: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderRadius: 16,
|
||||
@@ -102,5 +101,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
marginLeft: 12,
|
||||
flex: 1
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -68,7 +67,7 @@ export default class ChannelListRow extends React.PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
@@ -91,5 +90,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
fontSize: 13,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
@@ -57,7 +56,7 @@ export default class CustomListRow extends React.PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: 65,
|
||||
@@ -91,5 +90,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
backgroundColor: '#378FD2',
|
||||
borderWidth: 0
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See License.txt for license information.
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {ListView, Platform, StyleSheet, Text, View} from 'react-native';
|
||||
import {ListView, Platform, Text, View} from 'react-native';
|
||||
|
||||
import Loading from 'app/components/loading';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
@@ -239,7 +239,7 @@ export default class CustomList extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
@@ -285,5 +285,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
fontSize: 26,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -66,7 +65,7 @@ export default class UserListRow extends React.PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: 65,
|
||||
@@ -113,5 +112,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
backgroundColor: '#378FD2',
|
||||
borderWidth: 0
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
SectionList,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -255,7 +254,7 @@ export default class CustomSectionList extends React.PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
@@ -307,5 +306,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
fontSize: 26,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Image, Text} from 'react-native';
|
||||
import {Image, Platform, Text} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
|
||||
|
||||
import {Client} from 'mattermost-redux/client';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
export default class Emoji extends React.PureComponent {
|
||||
static propTypes = {
|
||||
@@ -17,14 +18,15 @@ export default class Emoji extends React.PureComponent {
|
||||
literal: PropTypes.string,
|
||||
padding: PropTypes.number,
|
||||
size: PropTypes.number.isRequired,
|
||||
textStyle: CustomPropTypes.Style
|
||||
}
|
||||
textStyle: CustomPropTypes.Style,
|
||||
token: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
customEmojis: new Map(),
|
||||
literal: '',
|
||||
padding: 10
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
@@ -33,26 +35,40 @@ export default class Emoji extends React.PureComponent {
|
||||
literal,
|
||||
padding,
|
||||
size,
|
||||
textStyle
|
||||
textStyle,
|
||||
token
|
||||
} = this.props;
|
||||
|
||||
let imageUrl;
|
||||
if (EmojiIndicesByAlias.has(emojiName)) {
|
||||
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)];
|
||||
imageUrl = Client.getSystemEmojiImageUrl(emoji.filename);
|
||||
imageUrl = Client4.getSystemEmojiImageUrl(emoji.filename);
|
||||
} else if (customEmojis.has(emojiName)) {
|
||||
const emoji = customEmojis.get(emojiName);
|
||||
imageUrl = Client.getCustomEmojiImageUrl(emoji.id);
|
||||
imageUrl = Client4.getCustomEmojiImageUrl(emoji.id);
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
return <Text style={textStyle}>{literal}</Text>;
|
||||
}
|
||||
|
||||
let ImageComponent = FastImage;
|
||||
const source = {
|
||||
uri: imageUrl,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
};
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
ImageComponent = Image;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
<ImageComponent
|
||||
style={{width: size, height: size, padding}}
|
||||
source={{uri: imageUrl}}
|
||||
source={source}
|
||||
onError={this.onError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ import Emoji from './emoji';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
customEmojis: getCustomEmojisByName(state),
|
||||
...ownProps
|
||||
token: state.entities.general.credentials.token
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
278
app/components/emoji_picker/emoji_picker.js
Normal file
278
app/components/emoji_picker/emoji_picker.js
Normal file
@@ -0,0 +1,278 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Dimensions,
|
||||
SectionList,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import SearchBar from 'app/components/search_bar';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
const {width: deviceWidth} = Dimensions.get('window');
|
||||
const EMOJI_SIZE = 30;
|
||||
const EMOJI_GUTTER = 7.5;
|
||||
const SECTION_MARGIN = 15;
|
||||
|
||||
class EmojiPicker extends PureComponent {
|
||||
static propTypes = {
|
||||
emojis: PropTypes.array.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
onEmojiPress: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onEmojiPress: emptyFunction
|
||||
};
|
||||
|
||||
leftButton = {
|
||||
id: 'close-edit-post'
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
emojis: props.emojis,
|
||||
searchTerm: ''
|
||||
};
|
||||
}
|
||||
|
||||
changeSearchTerm = (text) => {
|
||||
this.setState({
|
||||
searchTerm: text
|
||||
});
|
||||
|
||||
clearTimeout(this.searchTermTimeout);
|
||||
const timeout = text ? 350 : 0;
|
||||
this.searchTermTimeout = setTimeout(() => {
|
||||
const emojis = this.searchEmojis(text);
|
||||
this.setState({
|
||||
emojis
|
||||
});
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
cancelSearch = () => {
|
||||
this.setState({
|
||||
emojis: this.props.emojis,
|
||||
searchTerm: ''
|
||||
});
|
||||
}
|
||||
|
||||
filterEmojiAliases = (aliases, searchTerm) => {
|
||||
return aliases.findIndex((alias) => alias.includes(searchTerm)) !== -1;
|
||||
}
|
||||
|
||||
searchEmojis = (searchTerm) => {
|
||||
const {emojis} = this.props;
|
||||
const searchTermLowerCase = searchTerm.toLowerCase();
|
||||
|
||||
if (!searchTerm) {
|
||||
return emojis;
|
||||
}
|
||||
|
||||
const nextEmojis = [];
|
||||
emojis.forEach((section) => {
|
||||
const {data, ...otherProps} = section;
|
||||
const {key, items} = data[0];
|
||||
|
||||
const nextData = {
|
||||
key,
|
||||
items: items.filter((item) => {
|
||||
if (item.aliases) {
|
||||
return this.filterEmojiAliases(item.aliases, searchTermLowerCase);
|
||||
}
|
||||
|
||||
return item.name.includes(searchTermLowerCase);
|
||||
})
|
||||
};
|
||||
|
||||
if (nextData.items.length) {
|
||||
nextEmojis.push({
|
||||
...otherProps,
|
||||
data: [nextData]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return nextEmojis;
|
||||
}
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
const {theme} = this.props;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View key={section.title}>
|
||||
<FormattedText
|
||||
style={styles.sectionTitle}
|
||||
id={section.id}
|
||||
defaultMessage={section.defaultMessage}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmojis = (emojis, index) => {
|
||||
const {theme} = this.props;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
style={styles.columnStyle}
|
||||
>
|
||||
{emojis.map((emoji, emojiIndex) => {
|
||||
const style = [styles.emoji];
|
||||
if (emojiIndex === 0) {
|
||||
style.push(styles.emojiLeft);
|
||||
} else if (emojiIndex === emojis.length - 1) {
|
||||
style.push(styles.emojiRight);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={emoji.name}
|
||||
style={style}
|
||||
onPress={() => {
|
||||
this.props.onEmojiPress(emoji.name);
|
||||
}}
|
||||
>
|
||||
<Emoji
|
||||
emojiName={emoji.name}
|
||||
size={EMOJI_SIZE}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderItem = ({item}) => {
|
||||
const {theme} = this.props;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
const numColumns = Number(((deviceWidth - (SECTION_MARGIN * 2)) / (EMOJI_SIZE + (EMOJI_GUTTER * 2))).toFixed(0));
|
||||
|
||||
const slices = item.items.reduce((slice, emoji, emojiIndex) => {
|
||||
if (emojiIndex % numColumns === 0 && emojiIndex !== 0) {
|
||||
slice.push([]);
|
||||
}
|
||||
|
||||
slice[slice.length - 1].push(emoji);
|
||||
|
||||
return slice;
|
||||
}, [[]]);
|
||||
|
||||
return (
|
||||
<View style={styles.section}>
|
||||
{slices.map(this.renderEmojis)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {intl, theme} = this.props;
|
||||
const {emojis, searchTerm} = this.state;
|
||||
const {formatMessage} = intl;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.searchBar}>
|
||||
<SearchBar
|
||||
ref='search_bar'
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={33}
|
||||
inputStyle={{
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 13
|
||||
}}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={this.changeSearchTerm}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
value={searchTerm}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.container}>
|
||||
<SectionList
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={styles.listView}
|
||||
sections={emojis}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
renderItem={this.renderItem}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
columnStyle: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
marginVertical: EMOJI_GUTTER,
|
||||
justifyContent: 'flex-start'
|
||||
},
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1
|
||||
},
|
||||
emoji: {
|
||||
width: EMOJI_SIZE,
|
||||
height: EMOJI_SIZE,
|
||||
marginHorizontal: EMOJI_GUTTER,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
emojiLeft: {
|
||||
marginLeft: 0
|
||||
},
|
||||
emojiRight: {
|
||||
marginRight: 0
|
||||
},
|
||||
listView: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
width: deviceWidth - (SECTION_MARGIN * 2)
|
||||
},
|
||||
searchBar: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
paddingVertical: 5
|
||||
},
|
||||
section: {
|
||||
alignItems: 'center'
|
||||
},
|
||||
sectionTitle: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
paddingVertical: 5
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(EmojiPicker);
|
||||
108
app/components/emoji_picker/index.js
Normal file
108
app/components/emoji_picker/index.js
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {CategoryNames, Emojis, EmojiIndicesByCategory} from 'app/utils/emojis';
|
||||
|
||||
import EmojiPicker from './emoji_picker';
|
||||
|
||||
const categoryToI18n = {
|
||||
activity: {
|
||||
id: 'mobile.emoji_picker.activity',
|
||||
defaultMessage: 'ACTIVITY'
|
||||
},
|
||||
custom: {
|
||||
id: 'mobile.emoji_picker.custom',
|
||||
defaultMessage: 'CUSTOM'
|
||||
},
|
||||
flags: {
|
||||
id: 'mobile.emoji_picker.flags',
|
||||
defaultMessage: 'FLAGS'
|
||||
},
|
||||
foods: {
|
||||
id: 'mobile.emoji_picker.foods',
|
||||
defaultMessage: 'FOODS'
|
||||
},
|
||||
nature: {
|
||||
id: 'mobile.emoji_picker.nature',
|
||||
defaultMessage: 'NATURE'
|
||||
},
|
||||
objects: {
|
||||
id: 'mobile.emoji_picker.objects',
|
||||
defaultMessage: 'OBJECTS'
|
||||
},
|
||||
people: {
|
||||
id: 'mobile.emoji_picker.people',
|
||||
defaultMessage: 'PEOPLE'
|
||||
},
|
||||
places: {
|
||||
id: 'mobile.emoji_picker.places',
|
||||
defaultMessage: 'PLACES'
|
||||
},
|
||||
symbols: {
|
||||
id: 'mobile.emoji_picker.symbols',
|
||||
defaultMessage: 'SYMBOLS'
|
||||
}
|
||||
};
|
||||
|
||||
function fillEmoji(indice) {
|
||||
const emoji = Emojis[indice];
|
||||
return {
|
||||
name: emoji.aliases[0],
|
||||
aliases: emoji.aliases
|
||||
};
|
||||
}
|
||||
|
||||
const getEmojisBySection = createSelector(
|
||||
getCustomEmojisByName,
|
||||
(customEmojis) => {
|
||||
const emoticons = CategoryNames.filter((name) => name !== 'custom').map((category) => {
|
||||
const section = {
|
||||
...categoryToI18n[category],
|
||||
key: category,
|
||||
data: [{
|
||||
key: `${category}-emojis`,
|
||||
items: EmojiIndicesByCategory.get(category).map(fillEmoji)
|
||||
}]
|
||||
};
|
||||
|
||||
return section;
|
||||
});
|
||||
|
||||
const customEmojiData = {
|
||||
key: 'custom-emojis',
|
||||
title: 'CUSTOM',
|
||||
items: []
|
||||
};
|
||||
|
||||
for (const [key] of customEmojis) {
|
||||
customEmojiData.items.push({
|
||||
name: key
|
||||
});
|
||||
}
|
||||
|
||||
emoticons.push({
|
||||
...categoryToI18n.custom,
|
||||
key: 'custom',
|
||||
data: [customEmojiData]
|
||||
});
|
||||
|
||||
return emoticons;
|
||||
}
|
||||
);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const emojis = getEmojisBySection(state);
|
||||
|
||||
return {
|
||||
emojis,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(EmojiPicker);
|
||||
@@ -4,7 +4,7 @@
|
||||
import {connect} from 'react-redux';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {StyleSheet, Text} from 'react-native';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
@@ -51,11 +51,11 @@ class ErrorText extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
errorLabel: {
|
||||
color: (theme.errorTextColor || '#DA4A4A')
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
|
||||
@@ -6,8 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
StyleSheet
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
@@ -100,7 +99,7 @@ export default class FileAttachment extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
downloadIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.7),
|
||||
marginRight: 5
|
||||
@@ -135,5 +134,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
StyleSheet
|
||||
} from 'react-native';
|
||||
|
||||
import {Client} from 'mattermost-redux/client';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import imageIcon from 'assets/images/icons/image.png';
|
||||
|
||||
@@ -105,12 +105,12 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
|
||||
switch (imageSize) {
|
||||
case IMAGE_SIZE.Fullsize:
|
||||
return Client.getFileUrl(file.id, this.state.timestamp);
|
||||
return Client4.getFileUrl(file.id, this.state.timestamp);
|
||||
case IMAGE_SIZE.Preview:
|
||||
return Client.getFilePreviewUrl(file.id, this.state.timestamp);
|
||||
return Client4.getFilePreviewUrl(file.id, this.state.timestamp);
|
||||
case IMAGE_SIZE.Thumbnail:
|
||||
default:
|
||||
return Client.getFileThumbnailUrl(file.id, this.state.timestamp);
|
||||
return Client4.getFileThumbnailUrl(file.id, this.state.timestamp);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
}
|
||||
|
||||
return newWidth;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
||||
@@ -79,6 +79,7 @@ export default class Markdown extends PureComponent {
|
||||
|
||||
emph: Renderer.forwardChildren,
|
||||
strong: Renderer.forwardChildren,
|
||||
del: Renderer.forwardChildren,
|
||||
code: this.renderCodeSpan,
|
||||
link: this.renderLink,
|
||||
image: this.renderImage,
|
||||
@@ -200,15 +201,15 @@ export default class Markdown extends PureComponent {
|
||||
|
||||
renderCodeBlock = (props) => {
|
||||
// These sometimes include a trailing newline
|
||||
const contents = props.literal.replace(/\n$/, '');
|
||||
const content = props.literal.replace(/\n$/, '');
|
||||
|
||||
return (
|
||||
<MarkdownCodeBlock
|
||||
blockStyle={this.props.blockStyles.codeBlock}
|
||||
textStyle={concatStyles(this.props.baseTextStyle, this.props.textStyles.codeBlock)}
|
||||
>
|
||||
{contents}
|
||||
</MarkdownCodeBlock>
|
||||
navigator={this.props.navigator}
|
||||
content={content}
|
||||
language={props.language}
|
||||
textStyle={this.props.textStyles.codeBlock}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -235,10 +236,13 @@ export default class Markdown extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
renderListItem = ({children, ...otherProps}) => {
|
||||
renderListItem = ({children, context, ...otherProps}) => {
|
||||
const level = context.filter((type) => type === 'list').length;
|
||||
|
||||
return (
|
||||
<MarkdownListItem
|
||||
bulletStyle={this.props.baseTextStyle}
|
||||
level={level}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {ScrollView, Text} from 'react-native';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
|
||||
export default class MarkdownCodeBlock extends PureComponent {
|
||||
static propTypes = {
|
||||
children: CustomPropTypes.Children,
|
||||
blockStyle: CustomPropTypes.Style,
|
||||
textStyle: CustomPropTypes.Style
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ScrollView
|
||||
style={this.props.blockStyle}
|
||||
horizontal={true}
|
||||
>
|
||||
<Text style={this.props.textStyle}>
|
||||
{this.props.children}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
17
app/components/markdown/markdown_code_block/index.js
Normal file
17
app/components/markdown/markdown_code_block/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import MarkdownCodeBlock from './markdown_code_block';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(MarkdownCodeBlock);
|
||||
@@ -0,0 +1,216 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {PropTypes} from 'prop-types';
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {getDisplayNameForLanguage} from 'app/utils/markdown';
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const MAX_LINES = 4;
|
||||
|
||||
class MarkdownCodeBlock extends React.PureComponent {
|
||||
static propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
language: PropTypes.string,
|
||||
content: PropTypes.string.isRequired,
|
||||
textStyle: CustomPropTypes.Style
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
language: ''
|
||||
};
|
||||
|
||||
handlePress = wrapWithPreventDoubleTap(() => {
|
||||
const {intl, navigator, theme} = this.props;
|
||||
|
||||
const languageDisplayName = getDisplayNameForLanguage(this.props.language);
|
||||
let title;
|
||||
if (languageDisplayName) {
|
||||
title = intl.formatMessage(
|
||||
{
|
||||
id: 'mobile.routes.code',
|
||||
defaultMessage: '{language} Code'
|
||||
},
|
||||
{
|
||||
language: languageDisplayName
|
||||
}
|
||||
);
|
||||
} else {
|
||||
title = intl.formatMessage({
|
||||
id: 'mobile.routes.code.noLanguage',
|
||||
defaultMessage: 'Code'
|
||||
});
|
||||
}
|
||||
|
||||
navigator.push({
|
||||
screen: 'Code',
|
||||
title,
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
passProps: {
|
||||
content: this.props.content
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
trimContent = (content) => {
|
||||
const lines = content.split('\n');
|
||||
const numberOfLines = lines.length;
|
||||
|
||||
if (numberOfLines > MAX_LINES) {
|
||||
return {
|
||||
content: lines.slice(0, MAX_LINES).join('\n'),
|
||||
numberOfLines
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
numberOfLines
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
|
||||
let language = null;
|
||||
if (this.props.language) {
|
||||
const languageDisplayName = getDisplayNameForLanguage(this.props.language);
|
||||
|
||||
if (languageDisplayName) {
|
||||
language = (
|
||||
<View style={style.language}>
|
||||
<Text style={style.languageText}>
|
||||
{languageDisplayName}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const {content, numberOfLines} = this.trimContent(this.props.content);
|
||||
|
||||
let lineNumbers = '1';
|
||||
for (let i = 1; i < Math.min(numberOfLines, MAX_LINES); i++) {
|
||||
const line = (i + 1).toString();
|
||||
|
||||
lineNumbers += '\n' + line;
|
||||
}
|
||||
|
||||
let plusMoreLines = null;
|
||||
if (numberOfLines > MAX_LINES) {
|
||||
plusMoreLines = (
|
||||
<FormattedText
|
||||
style={style.plusMoreLinesText}
|
||||
id='mobile.markdown.code.plusMoreLines'
|
||||
defaultMessage='+{count, number} more lines'
|
||||
values={{
|
||||
count: numberOfLines - MAX_LINES
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={this.handlePress}>
|
||||
<View style={style.container}>
|
||||
<View style={style.lineNumbers}>
|
||||
<Text style={style.lineNumbersText}>
|
||||
{lineNumbers}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={style.rightColumn}>
|
||||
<View style={style.code}>
|
||||
<Text style={[style.codeText, this.props.textStyle]}>
|
||||
{content}
|
||||
</Text>
|
||||
</View>
|
||||
{plusMoreLines}
|
||||
</View>
|
||||
{language}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRadius: 3,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
lineNumbers: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.05),
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-start',
|
||||
paddingVertical: 4,
|
||||
width: 21
|
||||
},
|
||||
lineNumbersText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 12,
|
||||
lineHeight: 18
|
||||
},
|
||||
rightColumn: {
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 4
|
||||
},
|
||||
code: {
|
||||
flexDirection: 'row',
|
||||
overflow: 'scroll' // Doesn't actually cause a scrollbar, but stops text from wrapping
|
||||
},
|
||||
codeText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.65),
|
||||
fontSize: 12,
|
||||
lineHeight: 18
|
||||
},
|
||||
plusMoreLinesText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
fontSize: 11,
|
||||
marginTop: 2
|
||||
},
|
||||
language: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.sidebarHeaderBg,
|
||||
justifyContent: 'center',
|
||||
opacity: 0.8,
|
||||
padding: 6,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0
|
||||
},
|
||||
languageText: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
fontSize: 12
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(MarkdownCodeBlock);
|
||||
@@ -18,7 +18,8 @@ export default class MarkdownListItem extends PureComponent {
|
||||
startAt: PropTypes.number,
|
||||
index: PropTypes.number.isRequired,
|
||||
tight: PropTypes.bool,
|
||||
bulletStyle: CustomPropTypes.Style
|
||||
bulletStyle: CustomPropTypes.Style,
|
||||
level: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -29,13 +30,15 @@ export default class MarkdownListItem extends PureComponent {
|
||||
let bullet;
|
||||
if (this.props.ordered) {
|
||||
bullet = (this.props.startAt + this.props.index) + '. ';
|
||||
} else if (this.props.level % 2 === 0) {
|
||||
bullet = '◦';
|
||||
} else {
|
||||
bullet = '• ';
|
||||
bullet = '•';
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<View>
|
||||
<View style={style.bullet}>
|
||||
<Text style={this.props.bulletStyle}>
|
||||
{bullet}
|
||||
</Text>
|
||||
@@ -49,6 +52,9 @@ export default class MarkdownListItem extends PureComponent {
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
bullet: {
|
||||
width: 15
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {createPost, deletePost, removePost} from 'mattermost-redux/actions/posts';
|
||||
import {addReaction, createPost, deletePost, removePost} from 'mattermost-redux/actions/posts';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
@@ -38,6 +38,7 @@ function makeMapStateToProps() {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
addReaction,
|
||||
createPost,
|
||||
deletePost,
|
||||
removePost,
|
||||
|
||||
@@ -5,7 +5,6 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Alert,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewPropTypes
|
||||
} from 'react-native';
|
||||
@@ -29,6 +28,7 @@ import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
class Post extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
addReaction: PropTypes.func.isRequired,
|
||||
createPost: PropTypes.func.isRequired,
|
||||
deletePost: PropTypes.func.isRequired,
|
||||
removePost: PropTypes.func.isRequired,
|
||||
@@ -128,8 +128,7 @@ class Post extends PureComponent {
|
||||
|
||||
handlePostEdit = () => {
|
||||
const {intl, navigator, post, theme} = this.props;
|
||||
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).
|
||||
then((source) => {
|
||||
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => {
|
||||
navigator.showModal({
|
||||
screen: 'EditPost',
|
||||
title: intl.formatMessage({id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'}),
|
||||
@@ -148,6 +147,35 @@ class Post extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
handleAddReactionToPost = (emoji) => {
|
||||
const {post} = this.props;
|
||||
this.props.actions.addReaction(post.id, emoji);
|
||||
}
|
||||
|
||||
handleAddReaction = () => {
|
||||
const {intl, navigator, post, theme} = this.props;
|
||||
|
||||
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).
|
||||
then((source) => {
|
||||
navigator.showModal({
|
||||
screen: 'AddReaction',
|
||||
title: intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
|
||||
animated: true,
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg
|
||||
},
|
||||
passProps: {
|
||||
post,
|
||||
closeButton: source,
|
||||
onEmojiPress: this.handleAddReactionToPost
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleFailedPostPress = () => {
|
||||
const options = {
|
||||
title: {
|
||||
@@ -306,6 +334,7 @@ class Post extends PureComponent {
|
||||
canEdit={this.state.canEdit}
|
||||
isSearchResult={isSearchResult}
|
||||
navigator={this.props.navigator}
|
||||
onAddReaction={this.handleAddReaction}
|
||||
onFailedPostPress={this.handleFailedPostPress}
|
||||
onPostDelete={this.handlePostDelete}
|
||||
onPostEdit={this.handlePostEdit}
|
||||
@@ -319,11 +348,10 @@ class Post extends PureComponent {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flexDirection: 'row'
|
||||
@@ -368,7 +396,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
highlight: {
|
||||
backgroundColor: changeOpacity(theme.mentionHighlightBg, 0.5)
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(Post);
|
||||
|
||||
@@ -5,7 +5,6 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableHighlight,
|
||||
TouchableOpacity,
|
||||
View
|
||||
@@ -44,6 +43,7 @@ class PostBody extends PureComponent {
|
||||
isSystemMessage: PropTypes.bool,
|
||||
message: PropTypes.string,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onAddReaction: PropTypes.func,
|
||||
onFailedPostPress: PropTypes.func,
|
||||
onPostDelete: PropTypes.func,
|
||||
onPostEdit: PropTypes.func,
|
||||
@@ -56,6 +56,7 @@ class PostBody extends PureComponent {
|
||||
|
||||
static defaultProps = {
|
||||
fileIds: [],
|
||||
onAddReaction: emptyFunction,
|
||||
onFailedPostPress: emptyFunction,
|
||||
onPostDelete: emptyFunction,
|
||||
onPostEdit: emptyFunction,
|
||||
@@ -193,6 +194,11 @@ class PostBody extends PureComponent {
|
||||
if (canDelete && !hasBeenDeleted) {
|
||||
actions.push({text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), onPress: onPostDelete});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
text: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
|
||||
onPress: this.props.onAddReaction
|
||||
});
|
||||
}
|
||||
|
||||
let messageComponent;
|
||||
@@ -283,7 +289,7 @@ class PostBody extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
message: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15
|
||||
@@ -298,7 +304,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
systemMessage: {
|
||||
opacity: 0.6
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(PostBody);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
@@ -187,7 +186,7 @@ export default class PostHeader extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
commentedOn: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.65),
|
||||
marginBottom: 3,
|
||||
@@ -242,5 +241,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
marginRight: 5,
|
||||
marginBottom: 3
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ DateHeader.propTypes = {
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
@@ -60,7 +60,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
fontSize: 14,
|
||||
fontWeight: '600'
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default DateHeader;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewPropTypes
|
||||
@@ -58,7 +57,7 @@ export default class LoadMorePosts extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -71,5 +70,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
fontWeight: '600',
|
||||
color: theme.linkColor
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@ NewMessagesDivider.propTypes = {
|
||||
};
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
@@ -54,7 +54,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
fontSize: 14,
|
||||
color: theme.newMessageSeparator
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default NewMessagesDivider;
|
||||
|
||||
@@ -43,7 +43,8 @@ export default class PostList extends PureComponent {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
channel: {}
|
||||
channel: {},
|
||||
channelIsLoading: false
|
||||
};
|
||||
|
||||
getPostsWithDates = () => {
|
||||
@@ -176,7 +177,6 @@ export default class PostList extends PureComponent {
|
||||
{...refreshControl}
|
||||
renderItem={this.renderItem}
|
||||
theme={theme}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -49,7 +48,7 @@ export default class PostListRetry extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
buttonContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
@@ -80,5 +79,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
marginTop: 15,
|
||||
color: theme.linkColor
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
BackHandler,
|
||||
Keyboard,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
@@ -527,7 +526,7 @@ class PostTextbox extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
buttonContainer: {
|
||||
height: Platform.select({
|
||||
ios: 34,
|
||||
@@ -609,7 +608,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
color: theme.centerChannelColor,
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(PostTextbox, {withRef: true});
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Image, Platform, StyleSheet, View} from 'react-native';
|
||||
import {Image, Platform, View} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import placeholder from 'assets/images/profile.jpg';
|
||||
|
||||
import {Client} from 'mattermost-redux/client';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
const statusToIcon = {
|
||||
online: 'check',
|
||||
@@ -54,7 +54,7 @@ export default class ProfilePicture extends PureComponent {
|
||||
|
||||
let pictureUrl;
|
||||
if (this.props.user) {
|
||||
pictureUrl = Client.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update);
|
||||
pictureUrl = Client4.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update);
|
||||
}
|
||||
|
||||
let statusIcon;
|
||||
@@ -112,8 +112,8 @@ export default class ProfilePicture extends PureComponent {
|
||||
borderRadius: (this.props.statusSize - this.props.statusBorderWidth) / 2,
|
||||
padding: this.props.statusBorderWidth
|
||||
},
|
||||
style[this.props.status
|
||||
]]}
|
||||
style[this.props.status]
|
||||
]}
|
||||
>
|
||||
{statusIcon}
|
||||
</View>
|
||||
@@ -125,7 +125,7 @@ export default class ProfilePicture extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
statusWrapper: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
@@ -154,5 +154,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
offlineIcon: {
|
||||
borderColor: '#bababa'
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
@@ -47,7 +46,7 @@ export default class Reaction extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
count: {
|
||||
color: theme.linkColor,
|
||||
marginLeft: 6
|
||||
@@ -66,5 +65,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 6
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
Dimensions,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
@@ -53,9 +52,17 @@ export default class SearchPreview extends PureComponent {
|
||||
};
|
||||
|
||||
state = {
|
||||
showPosts: false
|
||||
showPosts: false,
|
||||
animationEnded: false
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {animationEnded, showPosts} = this.state;
|
||||
if (animationEnded && !showPosts && nextProps.posts.length) {
|
||||
this.setState({showPosts: true});
|
||||
}
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.refs.view.zoomOut().then(() => {
|
||||
if (this.props.onClose) {
|
||||
@@ -75,6 +82,7 @@ export default class SearchPreview extends PureComponent {
|
||||
};
|
||||
|
||||
showPostList = () => {
|
||||
this.setState({animationEnded: true});
|
||||
if (!this.state.showPosts && this.props.posts.length) {
|
||||
this.setState({showPosts: true});
|
||||
}
|
||||
@@ -158,7 +166,7 @@ export default class SearchPreview extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
position: 'absolute',
|
||||
backgroundColor: changeOpacity('#000', 0.3),
|
||||
@@ -230,5 +238,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
fontWeight: '600',
|
||||
textAlignVertical: 'center'
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
Image,
|
||||
Linking,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -335,7 +334,7 @@ export default class SlackAttachment extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderWidth: 1,
|
||||
@@ -407,5 +406,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flex: 1,
|
||||
height: 50
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import 'babel-polyfill';
|
||||
import Analytics from 'analytics-react-native';
|
||||
import Orientation from 'react-native-orientation';
|
||||
import {Provider} from 'react-redux';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
@@ -27,19 +28,30 @@ import {close as closeWebSocket} from 'mattermost-redux/actions/websocket';
|
||||
import {Client, Client4} from 'mattermost-redux/client';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {goToNotification, loadConfigAndLicense, queueNotification, setStatusBarHeight, purgeOfflineStore} from 'app/actions/views/root';
|
||||
import {
|
||||
goToNotification,
|
||||
loadConfigAndLicense,
|
||||
queueNotification,
|
||||
setStatusBarHeight,
|
||||
purgeOfflineStore
|
||||
} from 'app/actions/views/root';
|
||||
import {setChannelDisplayName} from 'app/actions/views/channel';
|
||||
import {handleLoginIdChanged} from 'app/actions/views/login';
|
||||
import {handleServerUrlChanged} from 'app/actions/views/select_server';
|
||||
import {NavigationTypes, ViewTypes} from 'app/constants';
|
||||
import {getTranslations} from 'app/i18n';
|
||||
import initialState from 'app/initial_state';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import {registerScreens} from 'app/screens';
|
||||
import configureStore from 'app/store';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
import Config from 'assets/config';
|
||||
|
||||
const {StatusBarManager} = NativeModules;
|
||||
const store = configureStore(initialState);
|
||||
const AUTHENTICATION_TIMEOUT = 5 * 60 * 1000;
|
||||
|
||||
registerScreens(store, Provider);
|
||||
|
||||
export default class Mattermost {
|
||||
@@ -51,6 +63,7 @@ export default class Mattermost {
|
||||
console.ignoredYellowBox = ['`scaleY`']; //eslint-disable-line
|
||||
}
|
||||
this.isConfigured = false;
|
||||
this.allowOtherServers = true;
|
||||
setJSExceptionHandler(this.errorHandler, false);
|
||||
Orientation.lockToPortrait();
|
||||
this.unsubscribeFromStore = store.subscribe(this.listenForHydration);
|
||||
@@ -60,6 +73,8 @@ export default class Mattermost {
|
||||
EventEmitter.on(General.DEFAULT_CHANNEL, this.handleResetDisplayName);
|
||||
EventEmitter.on(NavigationTypes.RESTART_APP, this.restartApp);
|
||||
|
||||
mattermostManaged.addEventListener('managedConfigDidChange', this.handleManagedConfig);
|
||||
|
||||
this.handleAppStateChange(AppState.currentState);
|
||||
Client4.setUserAgent(DeviceInfo.getUserAgent());
|
||||
|
||||
@@ -106,12 +121,88 @@ export default class Mattermost {
|
||||
return intl;
|
||||
};
|
||||
|
||||
handleAppStateChange = (appState) => {
|
||||
const {dispatch, getState} = store;
|
||||
setAppState(appState === 'active')(dispatch, getState);
|
||||
configureAnalytics = (config) => {
|
||||
if (config && config.DiagnosticsEnabled === 'true' && config.DiagnosticId && Config.SegmentApiKey) {
|
||||
if (!global.analytics) {
|
||||
global.analytics = new Analytics(Config.SegmentApiKey);
|
||||
global.analytics.identify({
|
||||
userId: config.DiagnosticId,
|
||||
context: {
|
||||
ip: '0.0.0.0'
|
||||
},
|
||||
page: {
|
||||
path: '',
|
||||
referrer: '',
|
||||
search: '',
|
||||
title: '',
|
||||
url: ''
|
||||
},
|
||||
anonymousId: '00000000000000000000000000'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
global.analytics = null;
|
||||
}
|
||||
};
|
||||
|
||||
handleConfigChanged = (serverVersion) => {
|
||||
configurePushNotifications = () => {
|
||||
PushNotifications.configure({
|
||||
onRegister: this.onRegisterDevice,
|
||||
onNotification: this.onPushNotification,
|
||||
popInitialNotification: true,
|
||||
requestPermissions: true
|
||||
});
|
||||
};
|
||||
|
||||
handleAppStateChange = async (appState) => {
|
||||
const {dispatch, getState} = store;
|
||||
const isActive = appState === 'active';
|
||||
setAppState(isActive)(dispatch, getState);
|
||||
try {
|
||||
if (!isActive && !this.inBackgroundSince) {
|
||||
this.inBackgroundSince = Date.now();
|
||||
} else if (isActive && this.inBackgroundSince && (Date.now() - this.inBackgroundSince) >= AUTHENTICATION_TIMEOUT) {
|
||||
this.inBackgroundSince = null;
|
||||
if (this.mdmEnabled) {
|
||||
const config = await mattermostManaged.getConfig();
|
||||
const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true';
|
||||
if (authNeeded) {
|
||||
const authenticated = await this.handleAuthentication(config.vendor);
|
||||
if (!authenticated) {
|
||||
mattermostManaged.quitApp();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
|
||||
handleAuthentication = async (vendor) => {
|
||||
const isSecured = await mattermostManaged.isDeviceSecure();
|
||||
|
||||
const intl = this.getIntl();
|
||||
if (isSecured) {
|
||||
try {
|
||||
await mattermostManaged.authenticate({
|
||||
reason: intl.formatMessage({
|
||||
id: 'mobile.managed.secured_by',
|
||||
defaultMessage: 'Secured by {vendor}'
|
||||
}, {vendor}),
|
||||
fallbackToPasscode: true,
|
||||
suppressEnterPassword: true
|
||||
});
|
||||
} catch (err) {
|
||||
mattermostManaged.quitApp();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
handleConfigChanged = async (serverVersion) => {
|
||||
const {dispatch, getState} = store;
|
||||
const version = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g)[0];
|
||||
const intl = this.getIntl();
|
||||
@@ -128,19 +219,103 @@ export default class Mattermost {
|
||||
{cancelable: false}
|
||||
);
|
||||
} else {
|
||||
setServerVersion('')(dispatch, getState);
|
||||
loadConfigAndLicense(serverVersion)(dispatch, getState);
|
||||
setServerVersion(serverVersion)(dispatch, getState);
|
||||
const data = await loadConfigAndLicense()(dispatch, getState);
|
||||
this.configureAnalytics(data.config);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleReset = () => {
|
||||
handleManagedConfig = async (serverConfig) => {
|
||||
const {dispatch, getState} = store;
|
||||
Client4.serverVersion = '';
|
||||
Client.serverVersion = '';
|
||||
Client.token = null;
|
||||
PushNotifications.cancelAllLocalNotifications();
|
||||
setServerVersion('')(dispatch, getState);
|
||||
const state = getState();
|
||||
|
||||
let authNeeded = false;
|
||||
let blurApplicationScreen = false;
|
||||
let jailbreakProtection = false;
|
||||
let vendor = null;
|
||||
let serverUrl = null;
|
||||
let username = null;
|
||||
|
||||
try {
|
||||
const config = await mattermostManaged.getConfig();
|
||||
if (config) {
|
||||
this.mdmEnabled = true;
|
||||
authNeeded = config.inAppPinCode && config.inAppPinCode === 'true';
|
||||
blurApplicationScreen = config.blurApplicationScreen && config.blurApplicationScreen === 'true';
|
||||
jailbreakProtection = config.jailbreakProtection && config.jailbreakProtection === 'true';
|
||||
vendor = config.vendor || 'Mattermost';
|
||||
|
||||
if (!state.entities.general.credentials.token) {
|
||||
serverUrl = config.serverUrl;
|
||||
username = config.username;
|
||||
|
||||
if (config.allowOtherServers && config.allowOtherServers === 'false') {
|
||||
this.allowOtherServers = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.mdmEnabled) {
|
||||
if (jailbreakProtection) {
|
||||
const isTrusted = mattermostManaged.isTrustedDevice();
|
||||
|
||||
if (!isTrusted) {
|
||||
const intl = this.getIntl();
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.managed.blocked_by',
|
||||
defaultMessage: 'Blocked by {vendor}'
|
||||
}, {vendor}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.managed.jailbreak',
|
||||
defaultMessage: 'Jailbroken devices are not trusted by {vendor}, please exit the app.'
|
||||
}, {vendor}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.managed.exit', defaultMessage: 'Exit'}),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
mattermostManaged.quitApp();
|
||||
}
|
||||
}],
|
||||
{cancelable: false}
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (authNeeded && !serverConfig) {
|
||||
if (Platform.OS === 'android') {
|
||||
//Start a fake app as we need at least one activity for android
|
||||
await this.startFakeApp();
|
||||
}
|
||||
const authenticated = await this.handleAuthentication(vendor);
|
||||
if (!authenticated) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (blurApplicationScreen) {
|
||||
mattermostManaged.blurAppScreen(true);
|
||||
}
|
||||
|
||||
if (serverUrl) {
|
||||
handleServerUrlChanged(serverUrl)(dispatch, getState);
|
||||
}
|
||||
|
||||
if (username) {
|
||||
handleLoginIdChanged(username)(dispatch, getState);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
handleReset = () => {
|
||||
this.resetBadgeAndVersion();
|
||||
this.startApp('fade');
|
||||
};
|
||||
|
||||
@@ -162,6 +337,8 @@ export default class Mattermost {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
logout()(dispatch, getState);
|
||||
});
|
||||
} else {
|
||||
this.resetBadgeAndVersion();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -170,19 +347,14 @@ export default class Mattermost {
|
||||
const state = store.getState();
|
||||
if (state.views.root.hydrationComplete) {
|
||||
this.unsubscribeFromStore();
|
||||
this.startApp();
|
||||
this.handleManagedConfig().then((shouldStart) => {
|
||||
if (shouldStart) {
|
||||
this.startApp();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
configurePushNotifications = () => {
|
||||
PushNotifications.configure({
|
||||
onRegister: this.onRegisterDevice,
|
||||
onNotification: this.onPushNotification,
|
||||
popInitialNotification: true,
|
||||
requestPermissions: true
|
||||
});
|
||||
};
|
||||
|
||||
onRegisterDevice = (data) => {
|
||||
const {dispatch, getState} = store;
|
||||
let prefix;
|
||||
@@ -235,11 +407,41 @@ export default class Mattermost {
|
||||
}
|
||||
};
|
||||
|
||||
restartApp = () => {
|
||||
resetBadgeAndVersion = () => {
|
||||
const {dispatch, getState} = store;
|
||||
Client4.serverVersion = '';
|
||||
Client.serverVersion = '';
|
||||
Client.token = null;
|
||||
Client4.userId = '';
|
||||
PushNotifications.setApplicationIconBadgeNumber(0);
|
||||
PushNotifications.cancelAllLocalNotifications();
|
||||
setServerVersion('')(dispatch, getState);
|
||||
};
|
||||
|
||||
restartApp = async () => {
|
||||
Navigation.dismissModal({animationType: 'none'});
|
||||
|
||||
const {dispatch, getState} = store;
|
||||
await loadConfigAndLicense()(dispatch, getState);
|
||||
this.startApp('fade');
|
||||
};
|
||||
|
||||
startFakeApp = async () => {
|
||||
return Navigation.startSingleScreenApp({
|
||||
screen: {
|
||||
screen: 'Root',
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false
|
||||
}
|
||||
},
|
||||
passProps: {
|
||||
justInit: true
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
startApp = (animationType = 'none') => {
|
||||
Navigation.startSingleScreenApp({
|
||||
screen: {
|
||||
@@ -250,6 +452,9 @@ export default class Mattermost {
|
||||
statusBarHideWithNavBar: false
|
||||
}
|
||||
},
|
||||
passProps: {
|
||||
allowOtherServers: this.allowOtherServers
|
||||
},
|
||||
animationType
|
||||
});
|
||||
|
||||
|
||||
6
app/mattermost_managed/index.js
Normal file
6
app/mattermost_managed/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// Used to leverage the platform specific components
|
||||
import MattermostManaged from './mattermost-managed';
|
||||
export default MattermostManaged;
|
||||
36
app/mattermost_managed/mattermost-managed.android.js
Normal file
36
app/mattermost_managed/mattermost-managed.android.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {BackHandler, NativeModules, DeviceEventEmitter} from 'react-native';
|
||||
import LocalAuth from 'react-native-local-auth';
|
||||
import JailMonkey from 'jail-monkey';
|
||||
|
||||
const {MattermostManaged} = NativeModules;
|
||||
|
||||
export default {
|
||||
addEventListener: (name, callback) => {
|
||||
DeviceEventEmitter.addListener(name, (config) => {
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback(config);
|
||||
}
|
||||
});
|
||||
},
|
||||
authenticate: LocalAuth.authenticate,
|
||||
blurAppScreen: MattermostManaged.blurAppScreen,
|
||||
getConfig: MattermostManaged.getConfig,
|
||||
isDeviceSecure: async () => {
|
||||
try {
|
||||
return await LocalAuth.isDeviceSecure();
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isTrustedDevice: () => {
|
||||
if (__DEV__) { //eslint-disable-line no-undef
|
||||
return true;
|
||||
}
|
||||
|
||||
return JailMonkey.trustFall();
|
||||
},
|
||||
quitApp: BackHandler.exitApp
|
||||
};
|
||||
36
app/mattermost_managed/mattermost-managed.ios.js
Normal file
36
app/mattermost_managed/mattermost-managed.ios.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
import {NativeModules, NativeEventEmitter} from 'react-native';
|
||||
import LocalAuth from 'react-native-local-auth';
|
||||
import JailMonkey from 'jail-monkey';
|
||||
|
||||
const {BlurAppScreen, MattermostManaged} = NativeModules;
|
||||
const MattermostManagedEvents = new NativeEventEmitter(MattermostManaged);
|
||||
|
||||
export default {
|
||||
addEventListener: (name, callback) => {
|
||||
MattermostManagedEvents.addListener(name, (config) => {
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback(config);
|
||||
}
|
||||
});
|
||||
},
|
||||
authenticate: LocalAuth.authenticate,
|
||||
blurAppScreen: BlurAppScreen.enabled,
|
||||
getConfig: MattermostManaged.getConfig,
|
||||
isDeviceSecure: async () => {
|
||||
try {
|
||||
return await LocalAuth.isDeviceSecure();
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isTrustedDevice: () => {
|
||||
if (__DEV__) { //eslint-disable-line no-undef
|
||||
return true;
|
||||
}
|
||||
|
||||
return JailMonkey.trustFall();
|
||||
},
|
||||
quitApp: MattermostManaged.quitApp
|
||||
};
|
||||
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
Linking,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
@@ -282,7 +281,7 @@ export default class About extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
@@ -366,5 +365,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
fontSize: 11,
|
||||
lineHeight: 13
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Keyboard,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
@@ -573,7 +572,7 @@ class AccountNotifications extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
@@ -596,7 +595,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
scrollViewContent: {
|
||||
paddingBottom: 30
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(AccountNotifications);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
@@ -12,7 +11,7 @@ import FormattedText from 'app/components/formatted_text';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
marginTop: 30
|
||||
},
|
||||
@@ -35,7 +34,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1)
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
function section(props) {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Switch,
|
||||
TouchableWithoutFeedback,
|
||||
View
|
||||
@@ -21,7 +20,7 @@ const ActionTypes = {
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
@@ -35,7 +34,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
wrapper: {
|
||||
paddingHorizontal: 15
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
function sectionItem(props) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -108,7 +107,7 @@ class AccountSettings extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
|
||||
@@ -154,7 +153,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(AccountSettings);
|
||||
|
||||
72
app/screens/add_reaction/add_reaction.js
Normal file
72
app/screens/add_reaction/add_reaction.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import EmojiPicker from 'app/components/emoji_picker';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
|
||||
export default class AddReaction extends PureComponent {
|
||||
static propTypes = {
|
||||
closeButton: PropTypes.object,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onEmojiPress: PropTypes.func
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onEmojiPress: emptyFunction
|
||||
};
|
||||
|
||||
leftButton = {
|
||||
id: 'close-edit-post'
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
|
||||
props.navigator.setButtons({
|
||||
leftButtons: [{...this.leftButton, icon: props.closeButton}]
|
||||
});
|
||||
}
|
||||
|
||||
close = () => {
|
||||
this.props.navigator.dismissModal({
|
||||
animationType: 'slide-down'
|
||||
});
|
||||
};
|
||||
|
||||
onNavigatorEvent = (event) => {
|
||||
if (event.type === 'NavBarButtonPress') {
|
||||
switch (event.id) {
|
||||
case 'close-edit-post':
|
||||
this.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleEmojiPress = (emoji) => {
|
||||
this.props.onEmojiPress(emoji);
|
||||
this.close();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<EmojiPicker onEmojiPress={this.handleEmojiPress}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
}
|
||||
});
|
||||
6
app/screens/add_reaction/index.js
Normal file
6
app/screens/add_reaction/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import AddReaction from './add_reaction';
|
||||
|
||||
export default AddReaction;
|
||||
@@ -7,7 +7,6 @@ import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
@@ -108,7 +107,7 @@ class AdvancedSettings extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
|
||||
@@ -154,7 +153,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(AdvancedSettings);
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Dimensions,
|
||||
NetInfo,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
@@ -250,7 +249,7 @@ class Channel extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
headerContainer: {
|
||||
flex: 1,
|
||||
position: 'absolute'
|
||||
@@ -292,7 +291,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
flex: 1,
|
||||
paddingBottom: 0
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(Channel);
|
||||
|
||||
@@ -7,7 +7,6 @@ import {connect} from 'react-redux';
|
||||
import {
|
||||
PanResponder,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -144,7 +143,7 @@ class ChannelDrawerButton extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
width: 40
|
||||
},
|
||||
@@ -180,7 +179,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
color: theme.mentionColor,
|
||||
fontSize: 10
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
function mapStateToProps(state) {
|
||||
|
||||
@@ -133,7 +133,7 @@ class ChannelPostList extends PureComponent {
|
||||
const channelId = post.channel_id;
|
||||
const rootId = (post.root_id || post.id);
|
||||
|
||||
actions.loadThreadIfNecessary(post.root_id);
|
||||
actions.loadThreadIfNecessary(post.root_id, channelId);
|
||||
actions.selectPost(rootId);
|
||||
|
||||
let title;
|
||||
|
||||
@@ -7,7 +7,6 @@ import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
InteractionManager,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
@@ -86,7 +85,7 @@ class ChannelAddMembers extends PureComponent {
|
||||
nextProps.loadMoreRequestStatus === RequestStatus.SUCCESS) {
|
||||
const {page} = this.state;
|
||||
const profiles = markSelectedProfiles(
|
||||
nextProps.membersNotInChannel.splice(0, (page + 1) * General.PROFILE_CHUNK_SIZE),
|
||||
nextProps.membersNotInChannel.slice(0, (page + 1) * General.PROFILE_CHUNK_SIZE),
|
||||
this.state.selectedMembers
|
||||
);
|
||||
this.setState({profiles, showNoResults: true});
|
||||
@@ -279,12 +278,12 @@ class ChannelAddMembers extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(ChannelAddMembers);
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Alert,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
@@ -367,7 +366,7 @@ class ChannelInfo extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
@@ -395,7 +394,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
height: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1)
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(ChannelInfo);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -122,7 +121,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
marginBottom: 40,
|
||||
@@ -161,5 +160,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
section: {
|
||||
marginTop: 15
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Switch,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
@@ -89,7 +88,7 @@ channelInfoRow.defaultProps = {
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
paddingHorizontal: 15,
|
||||
@@ -113,7 +112,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
rightIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default channelInfoRow;
|
||||
|
||||
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
Alert,
|
||||
InteractionManager,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
@@ -86,7 +85,7 @@ class ChannelMembers extends PureComponent {
|
||||
nextProps.requestStatus === RequestStatus.SUCCESS) {
|
||||
const {page} = this.state;
|
||||
const profiles = markSelectedProfiles(
|
||||
nextProps.currentChannelMembers.splice(0, (page + 1) * General.PROFILE_CHUNK_SIZE),
|
||||
nextProps.currentChannelMembers.slice(0, (page + 1) * General.PROFILE_CHUNK_SIZE),
|
||||
this.state.selectedMembers
|
||||
);
|
||||
this.setState({profiles, showNoResults: true});
|
||||
@@ -325,12 +324,12 @@ class ChannelMembers extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(ChannelMembers);
|
||||
|
||||
116
app/screens/code/code.js
Normal file
116
app/screens/code/code.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
Dimensions,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import {getCodeFont} from 'app/utils/markdown';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const {
|
||||
width: deviceWidth
|
||||
} = Dimensions.get('window');
|
||||
|
||||
export default class Code extends React.PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
content: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
countLines = (content) => {
|
||||
return content.split('\n').length;
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
|
||||
const numberOfLines = this.countLines(this.props.content);
|
||||
let lineNumbers = '1';
|
||||
for (let i = 1; i < numberOfLines; i++) {
|
||||
const line = (i + 1).toString();
|
||||
|
||||
lineNumbers += '\n' + line;
|
||||
}
|
||||
|
||||
let lineNumbersStyle;
|
||||
if (numberOfLines >= 10) {
|
||||
lineNumbersStyle = [style.lineNumbers, style.lineNumbersRight];
|
||||
} else {
|
||||
lineNumbersStyle = style.lineNumbers;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={style.scrollContainer}
|
||||
contentContainerStyle={style.container}
|
||||
>
|
||||
<View style={lineNumbersStyle}>
|
||||
<Text style={style.lineNumbersText}>
|
||||
{lineNumbers}
|
||||
</Text>
|
||||
</View>
|
||||
<ScrollView
|
||||
style={style.codeContainer}
|
||||
contentContainerStyle={style.code}
|
||||
horizontal={true}
|
||||
>
|
||||
<Text style={style.codeText}>
|
||||
{this.props.content}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
scrollContainer: {
|
||||
flex: 1
|
||||
},
|
||||
container: {
|
||||
minHeight: '100%',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
lineNumbers: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.05),
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-start',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 4
|
||||
},
|
||||
lineNumbersRight: {
|
||||
alignItems: 'flex-end'
|
||||
},
|
||||
lineNumbersText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 12,
|
||||
lineHeight: 18
|
||||
},
|
||||
codeContainer: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
width: deviceWidth
|
||||
},
|
||||
code: {
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 4
|
||||
},
|
||||
codeText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.65),
|
||||
fontFamily: getCodeFont(),
|
||||
fontSize: 12,
|
||||
lineHeight: 18
|
||||
}
|
||||
};
|
||||
});
|
||||
25
app/screens/code/index.js
Normal file
25
app/screens/code/index.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import Code from './code';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Code);
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Keyboard,
|
||||
InteractionManager,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
findNodeHandle
|
||||
@@ -244,12 +243,12 @@ class CreateChannel extends PureComponent {
|
||||
ref={this.scrollRef}
|
||||
style={style.container}
|
||||
>
|
||||
{displayError}
|
||||
<TouchableWithoutFeedback onPress={this.blur}>
|
||||
<View style={[style.scrollView, {height: height + (Platform.OS === 'android' ? 200 : 0)}]}>
|
||||
{displayError}
|
||||
<View>
|
||||
<FormattedText
|
||||
style={[style.title, {marginTop: (error ? 10 : 0)}]}
|
||||
style={style.title}
|
||||
id='channel_modal.name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
@@ -343,7 +342,7 @@ class CreateChannel extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
@@ -351,15 +350,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
|
||||
paddingTop: 30
|
||||
paddingTop: 10
|
||||
},
|
||||
errorContainer: {
|
||||
position: 'absolute'
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
|
||||
},
|
||||
errorWrapper: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 10
|
||||
alignItems: 'center'
|
||||
},
|
||||
inputContainer: {
|
||||
marginTop: 10,
|
||||
@@ -406,7 +404,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(CreateChannel);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Dimensions,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
@@ -210,7 +209,7 @@ class EditPost extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
@@ -239,7 +238,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
padding: 15,
|
||||
textAlignVertical: 'top'
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(EditPost);
|
||||
|
||||
@@ -22,7 +22,7 @@ import Orientation from 'react-native-orientation';
|
||||
|
||||
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
|
||||
|
||||
import ZoomableImage from './zoomable_image';
|
||||
import Previewer from './previewer';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
const {height: deviceHeight, width: deviceWidth} = Dimensions.get('window');
|
||||
@@ -144,21 +144,13 @@ export default class ImagePreview extends PureComponent {
|
||||
};
|
||||
|
||||
handleImageTap = () => {
|
||||
/*if (!this.lastPress) {
|
||||
this.lastPress = Date.now();
|
||||
} else if (Date.now() - this.lastPress < 400) {
|
||||
if (this.zoomableImages.hasOwnProperty(this.state.currentFile)) {
|
||||
this.zoomableImages[this.state.currentFile].zoomIn();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.lastPress = Date.now();
|
||||
|
||||
}*/
|
||||
|
||||
this.setHeaderAndFileInfoVisible(!this.state.showFileInfo);
|
||||
};
|
||||
|
||||
handleImageDoubleTap = (x, y) => {
|
||||
this.zoomableImages[this.state.currentFile].toggleZoom(x, y);
|
||||
}
|
||||
|
||||
setHeaderAndFileInfoVisible = (show) => {
|
||||
this.setState({
|
||||
showFileInfo: show
|
||||
@@ -180,7 +172,12 @@ export default class ImagePreview extends PureComponent {
|
||||
if (event.nativeEvent.contentOffset.x % this.state.deviceWidth === 0) {
|
||||
this.setState({
|
||||
currentFile: (event.nativeEvent.contentOffset.x / this.state.deviceWidth),
|
||||
pagingEnabled: true
|
||||
pagingEnabled: true,
|
||||
shouldShrinkImages: false
|
||||
});
|
||||
} else if (!this.state.shouldShrinkImages && !this.state.isZooming) {
|
||||
this.setState({
|
||||
shouldShrinkImages: true
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -223,7 +220,7 @@ export default class ImagePreview extends PureComponent {
|
||||
|
||||
return (
|
||||
<View
|
||||
style={style.wrapper}
|
||||
style={[style.wrapper, {height: this.state.deviceHeight, width: this.state.deviceWidth}]}
|
||||
onLayout={this.onLayout}
|
||||
>
|
||||
<AnimatedView
|
||||
@@ -246,7 +243,7 @@ export default class ImagePreview extends PureComponent {
|
||||
let component;
|
||||
if (file.has_preview_image) {
|
||||
component = (
|
||||
<ZoomableImage
|
||||
<Previewer
|
||||
ref={(c) => {
|
||||
this.zoomableImages[index] = c;
|
||||
}}
|
||||
@@ -256,9 +253,11 @@ export default class ImagePreview extends PureComponent {
|
||||
theme={this.props.theme}
|
||||
imageHeight={Math.min(maxImageHeight, file.height)}
|
||||
imageWidth={Math.min(this.state.deviceWidth, file.width)}
|
||||
shrink={this.state.shouldShrinkImages}
|
||||
wrapperHeight={this.state.deviceHeight}
|
||||
wrapperWidth={this.state.deviceWidth}
|
||||
onImageTap={this.handleImageTap}
|
||||
onImageDoubleTap={this.handleImageDoubleTap}
|
||||
onZoom={this.imageIsZooming}
|
||||
/>
|
||||
);
|
||||
@@ -378,12 +377,11 @@ const style = StyleSheet.create({
|
||||
justifyContent: 'center'
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1
|
||||
flex: 1,
|
||||
backgroundColor: '#000'
|
||||
},
|
||||
scrollViewContent: {
|
||||
backgroundColor: '#000',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
backgroundColor: '#000'
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
@@ -393,7 +391,9 @@ const style = StyleSheet.create({
|
||||
textAlign: 'center'
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
View,
|
||||
PanResponder
|
||||
} from 'react-native';
|
||||
|
||||
import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image';
|
||||
const {Image: AnimatedImage} = Animated;
|
||||
|
||||
function calcDistance(x1, y1, x2, y2) {
|
||||
const dx = Math.abs(x1 - x2);
|
||||
@@ -45,22 +46,21 @@ function calcOffsetByZoom(width, height, imageWidth, imageHeight, zoom) {
|
||||
};
|
||||
}
|
||||
|
||||
class ZoomableImage extends Component {
|
||||
export default class ImageView extends Component {
|
||||
static propTypes = {
|
||||
addFileToFetchCache: PropTypes.func.isRequired,
|
||||
fetchCache: PropTypes.object.isRequired,
|
||||
file: PropTypes.object.isRequired,
|
||||
imageHeight: PropTypes.number.isRequired,
|
||||
imageWidth: PropTypes.number.isRequired,
|
||||
imageHeight: PropTypes.number,
|
||||
imageWidth: PropTypes.number,
|
||||
maximumZoomScale: PropTypes.number,
|
||||
minimumZoomScale: PropTypes.number,
|
||||
onImageTap: PropTypes.func,
|
||||
onZoom: PropTypes.func.isRequired,
|
||||
onZoom: PropTypes.func,
|
||||
style: PropTypes.object.isRequired,
|
||||
wrapperHeight: PropTypes.number.isRequired,
|
||||
wrapperWidth: PropTypes.number.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
wrapperWidth: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onImageTap: () => false
|
||||
onZoom: () => false
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -94,33 +94,8 @@ class ZoomableImage extends Component {
|
||||
|
||||
componentWillMount() {
|
||||
this.panResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => {
|
||||
this.tap = Date.now();
|
||||
|
||||
return true;
|
||||
},
|
||||
onStartShouldSetPanResponderCapture: (evt, gestureState) => {
|
||||
if (gestureState.numberActiveTouches === 2 || this.state.zoom > 1) {
|
||||
// Store each press for double tap detection
|
||||
/*if (!this.lastPress) {
|
||||
this.lastPress = Date.now();
|
||||
} else if (Date.now() - this.lastPress < 400 && Date.now() - this.lastPress > 100) {
|
||||
this.setState({
|
||||
zoom: 1,
|
||||
top: 0,
|
||||
offsetTop: 0,
|
||||
left: 0,
|
||||
offsetLeft: 0
|
||||
});
|
||||
this.props.onZoom(false);
|
||||
this.lastPress = null;
|
||||
|
||||
return false;
|
||||
}*/
|
||||
|
||||
this.lastPress = Date.now();
|
||||
|
||||
this.props.onZoom(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -174,17 +149,18 @@ class ZoomableImage extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn = (zoom = 2) => {
|
||||
setZoom = (zoom = true) => {
|
||||
const zoomScale = zoom ? this.props.maximumZoomScale : this.props.minimumZoomScale;
|
||||
const offsetByZoom = calcOffsetByZoom(this.state.width, this.state.height,
|
||||
this.props.wrapperWidth, this.props.wrapperHeight, zoom);
|
||||
this.props.wrapperWidth, this.props.wrapperHeight, zoomScale);
|
||||
|
||||
this.setState({
|
||||
zoom,
|
||||
zoom: zoomScale,
|
||||
left: offsetByZoom.left,
|
||||
top: offsetByZoom.top,
|
||||
initialX: this.state.width / 2,
|
||||
initialY: this.state.height / 2,
|
||||
initialZoom: zoom,
|
||||
initialZoom: zoomScale,
|
||||
initialTopWithoutZoom: this.state.top - offsetByZoom.top,
|
||||
initialLeftWithoutZoom: this.state.left - offsetByZoom.left
|
||||
});
|
||||
@@ -271,16 +247,19 @@ class ZoomableImage extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
addFileToFetchCache,
|
||||
fetchCache,
|
||||
file,
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
theme,
|
||||
wrapperHeight,
|
||||
wrapperWidth
|
||||
style,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
let height = style.height;
|
||||
let width = style.width;
|
||||
if (this.state.zoom > 1) {
|
||||
height = imageHeight * this.state.zoom;
|
||||
width = imageWidth * this.state.zoom;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
{...this.panResponder.panHandlers}
|
||||
@@ -303,23 +282,11 @@ class ZoomableImage extends Component {
|
||||
height: this.state.height * this.state.zoom
|
||||
}}
|
||||
>
|
||||
<FileAttachmentImage
|
||||
addFileToFetchCache={addFileToFetchCache}
|
||||
fetchCache={fetchCache}
|
||||
file={file}
|
||||
theme={theme}
|
||||
imageHeight={imageHeight * this.state.zoom}
|
||||
imageSize='fullsize'
|
||||
imageWidth={imageWidth * this.state.zoom}
|
||||
loadingBackgroundColor='#000'
|
||||
resizeMode='contain'
|
||||
wrapperBackgroundColor='#000'
|
||||
wrapperHeight={wrapperHeight * this.state.zoom}
|
||||
wrapperWidth={wrapperWidth * this.state.zoom}
|
||||
<AnimatedImage
|
||||
{...otherProps}
|
||||
style={{height, width}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ZoomableImage;
|
||||
89
app/screens/image_preview/image_view.ios.js
Normal file
89
app/screens/image_preview/image_view.ios.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
ScrollView
|
||||
} from 'react-native';
|
||||
|
||||
const {Image: AnimatedImage} = Animated;
|
||||
|
||||
export default class ImageView extends PureComponent {
|
||||
static propTypes = {
|
||||
maximumZoomScale: PropTypes.number,
|
||||
minimumZoomScale: PropTypes.number,
|
||||
onZoom: PropTypes.func,
|
||||
showsHorizontalScrollIndicator: PropTypes.bool,
|
||||
showsVerticalScrollIndicator: PropTypes.bool,
|
||||
wrapperHeight: PropTypes.number.isRequired,
|
||||
wrapperWidth: PropTypes.number.isRequired
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
maximumZoomScale: 3,
|
||||
minimumZoomScale: 1,
|
||||
onZoom: () => true,
|
||||
showsHorizontalScrollIndicator: false,
|
||||
showsVerticalScrollIndicator: false
|
||||
}
|
||||
|
||||
attachScrollView = (c) => {
|
||||
if (c) {
|
||||
this.scrollView = c;
|
||||
this.scrollResponder = c.getScrollResponder();
|
||||
}
|
||||
}
|
||||
|
||||
setZoom = (zoom, x, y) => {
|
||||
const rect = {};
|
||||
if (zoom) {
|
||||
rect.x = x;
|
||||
rect.y = y;
|
||||
} else {
|
||||
rect.height = this.props.wrapperHeight;
|
||||
rect.width = this.props.wrapperWidth;
|
||||
}
|
||||
|
||||
this.scrollResponder.scrollResponderZoomTo({
|
||||
...rect,
|
||||
animated: true
|
||||
});
|
||||
}
|
||||
|
||||
handleScroll = (evt) => {
|
||||
const {nativeEvent} = evt;
|
||||
|
||||
clearTimeout(this.scrollEventTimeout);
|
||||
this.scrollEventTimeout = setTimeout(() => {
|
||||
this.props.onZoom(nativeEvent.zoomScale > 1);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
maximumZoomScale,
|
||||
minimumZoomScale,
|
||||
showsHorizontalScrollIndicator,
|
||||
showsVerticalScrollIndicator,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={this.attachScrollView}
|
||||
alwaysBounceHorizontal={false}
|
||||
alwaysBounceVertical={false}
|
||||
bounces={false}
|
||||
contentContainerStyle={{alignItems: 'center', justifyContent: 'center'}}
|
||||
centerContent={true}
|
||||
maximumZoomScale={maximumZoomScale}
|
||||
minimumZoomScale={minimumZoomScale}
|
||||
onScroll={this.handleScroll}
|
||||
scrollEventThrottle={16}
|
||||
showsHorizontalScrollIndicator={showsHorizontalScrollIndicator}
|
||||
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
|
||||
>
|
||||
<AnimatedImage {...otherProps}/>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
279
app/screens/image_preview/previewer.js
Normal file
279
app/screens/image_preview/previewer.js
Normal file
@@ -0,0 +1,279 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
PanResponder,
|
||||
Platform,
|
||||
View,
|
||||
StyleSheet
|
||||
} from 'react-native';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import imageIcon from 'assets/images/icons/image.png';
|
||||
|
||||
import ImageView from './image_view';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
|
||||
const DOUBLE_CLICK_THRESHOLD = 250;
|
||||
|
||||
export default class Previewer extends Component {
|
||||
static propTypes = {
|
||||
addFileToFetchCache: PropTypes.func.isRequired,
|
||||
fetchCache: PropTypes.object.isRequired,
|
||||
file: PropTypes.object,
|
||||
gutter: PropTypes.number,
|
||||
imageHeight: PropTypes.number,
|
||||
imageWidth: PropTypes.number,
|
||||
onImageTap: PropTypes.func,
|
||||
onImageDoubleTap: PropTypes.func,
|
||||
onZoom: PropTypes.func,
|
||||
shrink: PropTypes.bool,
|
||||
wrapperHeight: PropTypes.number,
|
||||
wrapperWidth: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
fadeInOnLoad: false,
|
||||
gutter: 20,
|
||||
loading: false,
|
||||
onImageTap: () => true,
|
||||
onImageDoubleTap: () => true,
|
||||
onZoom: () => true
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
imageHeight: new Animated.Value(props.imageHeight),
|
||||
imageWidth: new Animated.Value(props.imageWidth),
|
||||
opacity: new Animated.Value(0),
|
||||
requesting: true,
|
||||
retry: 0,
|
||||
zooming: false
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.panResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponderCapture: (evt, gestureState) => {
|
||||
const {numberActiveTouches} = gestureState;
|
||||
if (numberActiveTouches === 1 && !this.state.isZooming) {
|
||||
return true;
|
||||
} else if (numberActiveTouches === 1) {
|
||||
this.handleResponderRelease(evt);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
onPanResponderGrant: () => {
|
||||
return;
|
||||
},
|
||||
onPanResponderTerminate: () => {
|
||||
return;
|
||||
},
|
||||
onShouldBlockNativeResponder: () => false
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.shrink && !nextProps.shrink) {
|
||||
this.setShrink();
|
||||
} else if (!this.props.shrink && nextProps.shrink) {
|
||||
this.setShrink(true);
|
||||
}
|
||||
}
|
||||
|
||||
setShrink = (shrink = false) => {
|
||||
const {gutter, imageHeight, imageWidth} = this.props;
|
||||
|
||||
let height = imageHeight;
|
||||
let width = imageWidth;
|
||||
const duration = 150;
|
||||
if (shrink) {
|
||||
height = height - gutter;
|
||||
width = width - gutter;
|
||||
}
|
||||
|
||||
const animations = [
|
||||
Animated.timing(this.state.imageWidth, {
|
||||
toValue: width,
|
||||
duration
|
||||
})
|
||||
];
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
animations.push(
|
||||
Animated.timing(this.state.imageHeight, {
|
||||
toValue: height,
|
||||
duration
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Animated.parallel(animations).start();
|
||||
}
|
||||
|
||||
handleResponderRelease = (evt) => {
|
||||
clearTimeout(this.singleTap);
|
||||
let cancelSingleTap = false;
|
||||
if (this.lastTap && Date.now() - this.lastTap < DOUBLE_CLICK_THRESHOLD) {
|
||||
cancelSingleTap = true;
|
||||
} else {
|
||||
this.lastTap = Date.now();
|
||||
}
|
||||
|
||||
if (cancelSingleTap) {
|
||||
const {nativeEvent} = evt;
|
||||
const x = nativeEvent.locationX;
|
||||
const y = nativeEvent.locationY;
|
||||
|
||||
cancelSingleTap = false;
|
||||
this.lastTap = null;
|
||||
|
||||
this.props.onImageDoubleTap(x, y);
|
||||
} else if (!this.state.isZooming) {
|
||||
this.singleTap = setTimeout(() => {
|
||||
this.props.onImageTap();
|
||||
}, DOUBLE_CLICK_THRESHOLD);
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadError = () => {
|
||||
if (this.state.retry < 4) {
|
||||
setTimeout(() => {
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
retry: (prevState.retry + 1),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
handleLoad = () => {
|
||||
this.setState({
|
||||
requesting: false
|
||||
});
|
||||
|
||||
Animated.timing(this.state.opacity, {
|
||||
toValue: 1,
|
||||
duration: 300
|
||||
}).start(() => {
|
||||
this.props.addFileToFetchCache(this.handleGetImageURL());
|
||||
});
|
||||
};
|
||||
|
||||
handleLoadStart = () => {
|
||||
this.setState({
|
||||
requesting: true
|
||||
});
|
||||
};
|
||||
|
||||
handleGetImageURL = () => {
|
||||
const {file} = this.props;
|
||||
|
||||
return Client4.getFilePreviewUrl(file.id, this.state.timestamp);
|
||||
};
|
||||
|
||||
attachImageView = (c) => {
|
||||
this.imageView = c;
|
||||
};
|
||||
|
||||
handleZoom = (zoom) => {
|
||||
this.setState({
|
||||
isZooming: zoom
|
||||
});
|
||||
|
||||
this.props.onZoom(zoom);
|
||||
};
|
||||
|
||||
toggleZoom = (x, y) => {
|
||||
const zoom = !this.state.isZooming;
|
||||
|
||||
this.imageView.setZoom(zoom, x, y);
|
||||
this.handleZoom(zoom);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
fetchCache,
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
wrapperHeight,
|
||||
wrapperWidth
|
||||
} = this.props;
|
||||
|
||||
let source = {};
|
||||
let usingIcon = false;
|
||||
if (this.state.retry === 4) {
|
||||
source = imageIcon;
|
||||
usingIcon = true;
|
||||
} else {
|
||||
source = {uri: this.handleGetImageURL()};
|
||||
}
|
||||
|
||||
let isInFetchCache = fetchCache[source.uri];
|
||||
if (usingIcon) {
|
||||
isInFetchCache = true;
|
||||
}
|
||||
|
||||
const imageComponentLoaders = {
|
||||
onError: (isInFetchCache) ? null : this.handleLoadError,
|
||||
onLoadStart: isInFetchCache ? null : this.handleLoadStart,
|
||||
onLoad: isInFetchCache ? null : this.handleLoad
|
||||
};
|
||||
|
||||
const opacity = isInFetchCache ? 1 : this.state.opacity;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...this.panResponder.panHandlers}
|
||||
onResponderRelease={this.handleResponderRelease}
|
||||
style={[style.fileImageWrapper, {height: wrapperHeight, width: wrapperWidth}]}
|
||||
>
|
||||
<AnimatedView style={{height: imageHeight, width: this.state.imageWidth, backgroundColor: '#000', opacity}}>
|
||||
<ImageView
|
||||
ref={this.attachImageView}
|
||||
source={source}
|
||||
minimumZoomScale={1}
|
||||
maximumZoomScale={3}
|
||||
onZoom={this.handleZoom}
|
||||
resizeMode='contain'
|
||||
imageHeight={imageHeight}
|
||||
imageWidth={imageWidth}
|
||||
style={{height: this.state.imageHeight, width: this.state.imageWidth}}
|
||||
wrapperHeight={wrapperHeight}
|
||||
wrapperWidth={wrapperWidth}
|
||||
{...imageComponentLoaders}
|
||||
/>
|
||||
</AnimatedView>
|
||||
{(!isInFetchCache && this.state.requesting) &&
|
||||
<View style={[style.loaderContainer, {backgroundColor: 'white'}]}>
|
||||
<ActivityIndicator size='small'/>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
fileImageWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
loaderContainer: {
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
@@ -7,11 +7,13 @@ import {Navigation} from 'react-native-navigation';
|
||||
import About from 'app/screens/about';
|
||||
import AccountSettings from 'app/screens/account_settings';
|
||||
import AccountNotifications from 'app/screens/account_notifications';
|
||||
import AddReaction from 'app/screens/add_reaction';
|
||||
import AdvancedSettings from 'app/screens/advanced_settings';
|
||||
import Channel from 'app/screens/channel';
|
||||
import ChannelAddMembers from 'app/screens/channel_add_members';
|
||||
import ChannelInfo from 'app/screens/channel_info';
|
||||
import ChannelMembers from 'app/screens/channel_members';
|
||||
import Code from 'app/screens/code';
|
||||
import CreateChannel from 'app/screens/create_channel';
|
||||
import EditPost from 'app/screens/edit_post';
|
||||
import ImagePreview from 'app/screens/image_preview';
|
||||
@@ -52,11 +54,13 @@ export function registerScreens(store, Provider) {
|
||||
Navigation.registerComponent('About', () => wrapWithContextProvider(About), store, Provider);
|
||||
Navigation.registerComponent('AccountSettings', () => wrapWithContextProvider(AccountSettings), store, Provider);
|
||||
Navigation.registerComponent('AccountNotifications', () => wrapWithContextProvider(AccountNotifications), store, Provider);
|
||||
Navigation.registerComponent('AddReaction', () => wrapWithContextProvider(AddReaction), store, Provider);
|
||||
Navigation.registerComponent('AdvancedSettings', () => wrapWithContextProvider(AdvancedSettings), store, Provider);
|
||||
Navigation.registerComponent('Channel', () => wrapWithContextProvider(Channel), store, Provider);
|
||||
Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(ChannelAddMembers), store, Provider);
|
||||
Navigation.registerComponent('ChannelInfo', () => wrapWithContextProvider(ChannelInfo), store, Provider);
|
||||
Navigation.registerComponent('ChannelMembers', () => wrapWithContextProvider(ChannelMembers), store, Provider);
|
||||
Navigation.registerComponent('Code', () => wrapWithContextProvider(Code), store, Provider);
|
||||
Navigation.registerComponent('CreateChannel', () => wrapWithContextProvider(CreateChannel), store, Provider);
|
||||
Navigation.registerComponent('EditPost', () => wrapWithContextProvider(EditPost), store, Provider);
|
||||
Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(ImagePreview), store, Provider);
|
||||
|
||||
@@ -87,6 +87,40 @@ class LoginOptions extends PureComponent {
|
||||
return null;
|
||||
};
|
||||
|
||||
renderLdapOption = () => {
|
||||
const {config, license} = this.props;
|
||||
if (license.IsLicensed === 'true' && config.EnableLdap === 'true') {
|
||||
let buttonText;
|
||||
if (config.LdapLoginFieldName) {
|
||||
buttonText = (
|
||||
<Text style={[GlobalStyles.signupButtonText, {color: 'white'}]}>
|
||||
{config.LdapLoginFieldName}
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
buttonText = (
|
||||
<FormattedText
|
||||
id='login.ldapUsernameLower'
|
||||
defaultMessage='AD/LDAP username'
|
||||
style={[GlobalStyles.signupButtonText, {color: 'white'}]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key='ldap'
|
||||
onPress={() => preventDoubleTap(this.goToLogin, this)}
|
||||
containerStyle={[GlobalStyles.signupButton, {backgroundColor: '#2389d7'}]}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
renderGitlabOption = () => {
|
||||
const {config, serverVersion} = this.props;
|
||||
const match = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g);
|
||||
@@ -159,6 +193,7 @@ class LoginOptions extends PureComponent {
|
||||
defaultMessage='Choose your login method'
|
||||
/>
|
||||
{this.renderEmailOption()}
|
||||
{this.renderLdapOption()}
|
||||
{this.renderGitlabOption()}
|
||||
{this.renderSamlOption()}
|
||||
</View>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Platform,
|
||||
InteractionManager,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
@@ -57,7 +56,7 @@ class MoreChannels extends PureComponent {
|
||||
this.searchTimeoutId = 0;
|
||||
|
||||
this.state = {
|
||||
channels: props.channels.splice(0, General.CHANNELS_CHUNK_SIZE),
|
||||
channels: props.channels.slice(0, General.CHANNELS_CHUNK_SIZE),
|
||||
createScreenVisible: false,
|
||||
page: 0,
|
||||
adding: false,
|
||||
@@ -102,7 +101,7 @@ class MoreChannels extends PureComponent {
|
||||
} else if (requestStatus.status === RequestStatus.STARTED &&
|
||||
nextProps.requestStatus.status === RequestStatus.SUCCESS) {
|
||||
const {page} = this.state;
|
||||
const channels = nextProps.channels.splice(0, (page + 1) * General.CHANNELS_CHUNK_SIZE);
|
||||
const channels = nextProps.channels.slice(0, (page + 1) * General.CHANNELS_CHUNK_SIZE);
|
||||
this.setState({channels, showNoResults: true});
|
||||
}
|
||||
|
||||
@@ -330,7 +329,7 @@ class MoreChannels extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
@@ -346,7 +345,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(MoreChannels);
|
||||
|
||||
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
InteractionManager,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
@@ -426,7 +425,7 @@ class MoreDirectMessages extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
@@ -434,7 +433,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
searchContainer: {
|
||||
marginVertical: 5
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(MoreDirectMessages);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
@@ -67,7 +66,7 @@ export default class SelectedUser extends React.PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
@@ -86,5 +85,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 13
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import SelectedUser from 'app/screens/more_dms/selected_users/selected_user';
|
||||
@@ -115,7 +115,7 @@ export default class SelectedUsers extends React.PureComponent {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
marginLeft: 5,
|
||||
marginBottom: 5
|
||||
@@ -132,5 +132,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
marginTop: 10,
|
||||
marginBottom: 2
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -12,7 +12,9 @@ import {stripTrailingSlashes} from 'app/utils/url';
|
||||
|
||||
export default class Root extends PureComponent {
|
||||
static propTypes = {
|
||||
allowOtherServers: PropTypes.bool,
|
||||
credentials: PropTypes.object,
|
||||
justInit: PropTypes.bool,
|
||||
loginRequest: PropTypes.object,
|
||||
navigator: PropTypes.object,
|
||||
theme: PropTypes.object,
|
||||
@@ -22,7 +24,9 @@ export default class Root extends PureComponent {
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.loadStoreAndScene();
|
||||
if (!this.props.justInit) {
|
||||
this.loadStoreAndScene();
|
||||
}
|
||||
}
|
||||
|
||||
goToLoadTeam = () => {
|
||||
@@ -44,7 +48,9 @@ export default class Root extends PureComponent {
|
||||
};
|
||||
|
||||
goToSelectServer = () => {
|
||||
this.props.navigator.resetTo({
|
||||
const {allowOtherServers, navigator} = this.props;
|
||||
|
||||
navigator.resetTo({
|
||||
screen: 'SelectServer',
|
||||
animated: false,
|
||||
navigatorStyle: {
|
||||
@@ -52,6 +58,9 @@ export default class Root extends PureComponent {
|
||||
navBarBackgroundColor: 'black',
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false
|
||||
},
|
||||
passProps: {
|
||||
allowOtherServers
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Dimensions,
|
||||
Keyboard,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {General, RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
@@ -32,6 +34,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
const SECTION_HEIGHT = 20;
|
||||
const RECENT_LABEL_HEIGHT = 42;
|
||||
const RECENT_SEPARATOR_HEIGHT = 3;
|
||||
const MODIFIER_LABEL_HEIGHT = 58;
|
||||
const POSTS_PER_PAGE = ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
const SEARCHING = 'searching';
|
||||
const NO_RESULTS = 'no results';
|
||||
@@ -89,16 +92,17 @@ class Search extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {searchingStatus, recent} = this.props;
|
||||
componentDidUpdate(prevProps) {
|
||||
const {searchingStatus: status, recent} = this.props;
|
||||
const {searchingStatus: prevStatus} = prevProps;
|
||||
const recentLenght = recent.length;
|
||||
const shouldScroll = searchingStatus === RequestStatus.SUCCESS || searchingStatus === RequestStatus.STARTED;
|
||||
const shouldScroll = prevStatus !== status && (status === RequestStatus.SUCCESS || status === RequestStatus.STARTED);
|
||||
|
||||
if (shouldScroll && !this.state.isFocused) {
|
||||
setTimeout(() => {
|
||||
this.refs.list.getWrapperRef().getListRef().scrollToOffset({
|
||||
animated: true,
|
||||
offset: SECTION_HEIGHT + (recentLenght * RECENT_LABEL_HEIGHT) + ((recentLenght - 1) * RECENT_SEPARATOR_HEIGHT)
|
||||
offset: SECTION_HEIGHT + (2 * MODIFIER_LABEL_HEIGHT) + (recentLenght * RECENT_LABEL_HEIGHT) + ((recentLenght + 1) * RECENT_SEPARATOR_HEIGHT)
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
@@ -120,7 +124,8 @@ class Search extends Component {
|
||||
const channel = channels.find((c) => c.id === channelId);
|
||||
const rootId = (post.root_id || post.id);
|
||||
|
||||
actions.loadThreadIfNecessary(post.root_id);
|
||||
Keyboard.dismiss();
|
||||
actions.loadThreadIfNecessary(rootId, channelId);
|
||||
actions.selectPost(rootId);
|
||||
|
||||
let title;
|
||||
@@ -148,11 +153,7 @@ class Search extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
navigator.showModal(options);
|
||||
} else {
|
||||
navigator.push(options);
|
||||
}
|
||||
navigator.push(options);
|
||||
};
|
||||
|
||||
handleSelectionChange = (event) => {
|
||||
@@ -172,6 +173,10 @@ class Search extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
keyModifierExtractor = (item) => {
|
||||
return `modifier-${item.value}`;
|
||||
};
|
||||
|
||||
keyRecentExtractor = (item) => {
|
||||
return `recent-${item.terms}`;
|
||||
};
|
||||
@@ -204,7 +209,8 @@ class Search extends Component {
|
||||
const focusedPostId = post.id;
|
||||
const channelId = post.channel_id;
|
||||
|
||||
actions.getPostThread(focusedPostId);
|
||||
Keyboard.dismiss();
|
||||
actions.getPostThread(focusedPostId, false);
|
||||
actions.getPostsBefore(channelId, focusedPostId, 0, POSTS_PER_PAGE);
|
||||
actions.getPostsAfter(channelId, focusedPostId, 0, POSTS_PER_PAGE);
|
||||
|
||||
@@ -223,6 +229,40 @@ class Search extends Component {
|
||||
actions.removeSearchTerms(currentTeamId, item.terms);
|
||||
};
|
||||
|
||||
renderModifiers = ({item}) => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
key={item.modifier}
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={() => preventDoubleTap(this.setModifierValue, this, item.value)}
|
||||
>
|
||||
<View style={style.modifierItemContainer}>
|
||||
<View style={style.modifierItemWrapper}>
|
||||
<View style={style.modifierItemLabelContainer}>
|
||||
<View style={style.modifierLabelIconContainer}>
|
||||
<AwesomeIcon
|
||||
style={style.modifierLabelIcon}
|
||||
name='plus-square-o'
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={style.modifierItemLabel}
|
||||
>
|
||||
{item.modifier}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={style.modifierItemDescription}>
|
||||
{item.description}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
renderPost = ({item, index}) => {
|
||||
const {channels, posts, theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
@@ -296,15 +336,19 @@ class Search extends Component {
|
||||
const {title} = section;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={style.sectionContainer}>
|
||||
<Text style={style.sectionLabel}>
|
||||
{title}
|
||||
</Text>
|
||||
if (title) {
|
||||
return (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={style.sectionContainer}>
|
||||
<Text style={style.sectionLabel}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
return <View/>;
|
||||
};
|
||||
|
||||
renderRecentItem = ({item}) => {
|
||||
@@ -352,6 +396,17 @@ class Search extends Component {
|
||||
actions.searchPosts(currentTeamId, terms, isOrSearch);
|
||||
};
|
||||
|
||||
setModifierValue = (modifier) => {
|
||||
const {value} = this.state;
|
||||
if (!value) {
|
||||
this.handleTextChanged(modifier);
|
||||
} else if (value.endsWith(' ')) {
|
||||
this.handleTextChanged(`${value}${modifier}`);
|
||||
} else {
|
||||
this.handleTextChanged(`${value} ${modifier}`);
|
||||
}
|
||||
};
|
||||
|
||||
setRecentValue = (recent) => {
|
||||
const {terms, isOrSearch} = recent;
|
||||
this.handleTextChanged(terms);
|
||||
@@ -404,7 +459,28 @@ class Search extends Component {
|
||||
|
||||
const {channelName, postId, preview, value} = this.state;
|
||||
const style = getStyleFromTheme(theme);
|
||||
const sections = [];
|
||||
const sections = [{
|
||||
data: [{
|
||||
value: 'from:',
|
||||
modifier: `from:${intl.formatMessage({id: 'mobile.search.from_modifier_title', defaultMessage: 'username'})}`,
|
||||
description: intl.formatMessage({
|
||||
id: 'mobile.search.from_modifier_description',
|
||||
defaultMessage: 'to find posts from specific users'
|
||||
})
|
||||
}, {
|
||||
value: 'in:',
|
||||
modifier: `in:${intl.formatMessage({id: 'mobile.search.in_modifier_title', defaultMessage: 'channel-name'})}`,
|
||||
description: intl.formatMessage({
|
||||
id: 'mobile.search.in_modifier_description',
|
||||
defaultMessage: 'to find posts in specific channels'
|
||||
})
|
||||
}],
|
||||
key: 'modifiers',
|
||||
title: '',
|
||||
renderItem: this.renderModifiers,
|
||||
keyExtractor: this.keyModifierExtractor,
|
||||
ItemSeparatorComponent: this.renderRecentSeparator
|
||||
}];
|
||||
|
||||
if (recent.length) {
|
||||
sections.push({
|
||||
@@ -519,7 +595,8 @@ class Search extends Component {
|
||||
style={style.sectionList}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
sections={sections}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='interactive'
|
||||
stickySectionHeadersEnabled={Platform.OS === 'ios'}
|
||||
/>
|
||||
{previewComponent}
|
||||
@@ -529,7 +606,7 @@ class Search extends Component {
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
header: {
|
||||
backgroundColor: theme.sidebarHeaderBg,
|
||||
width: Dimensions.get('window').width,
|
||||
@@ -558,6 +635,38 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
fontSize: 12,
|
||||
fontWeight: '600'
|
||||
},
|
||||
modifierItemContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: MODIFIER_LABEL_HEIGHT
|
||||
},
|
||||
modifierItemWrapper: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
paddingHorizontal: 16
|
||||
},
|
||||
modifierItemLabelContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
modifierLabelIconContainer: {
|
||||
alignItems: 'center',
|
||||
marginRight: 5
|
||||
},
|
||||
modifierLabelIcon: {
|
||||
fontSize: 16,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
},
|
||||
modifierItemLabel: {
|
||||
fontSize: 14,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
modifierItemDescription: {
|
||||
fontSize: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
marginTop: 5
|
||||
},
|
||||
recentItemContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
@@ -616,7 +725,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
searching: {
|
||||
marginTop: 25
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(Search);
|
||||
|
||||
@@ -32,6 +32,7 @@ import logo from 'assets/images/logo.png';
|
||||
|
||||
class SelectServer extends PureComponent {
|
||||
static propTypes = {
|
||||
allowOtherServers: PropTypes.bool,
|
||||
navigator: PropTypes.object,
|
||||
intl: intlShape.isRequired,
|
||||
config: PropTypes.object,
|
||||
@@ -59,6 +60,13 @@ class SelectServer extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {allowOtherServers, pingRequest, serverUrl} = this.props;
|
||||
if (pingRequest.status === RequestStatus.NOT_STARTED && !allowOtherServers && serverUrl) {
|
||||
// If the app is managed, the server url is set and the user can't change it
|
||||
// we automatically trigger the ping to move to the next screen
|
||||
this.onClick();
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard);
|
||||
}
|
||||
@@ -151,7 +159,7 @@ class SelectServer extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {serverUrl, pingRequest, configRequest, licenseRequest} = this.props;
|
||||
const {allowOtherServers, serverUrl, pingRequest, configRequest, licenseRequest} = this.props;
|
||||
const isLoading = pingRequest.status === RequestStatus.STARTED ||
|
||||
configRequest.status === RequestStatus.STARTED ||
|
||||
licenseRequest.status === RequestStatus.STARTED;
|
||||
@@ -203,9 +211,10 @@ class SelectServer extends PureComponent {
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
ref={this.inputRef}
|
||||
value={serverUrl}
|
||||
editable={allowOtherServers}
|
||||
onChangeText={this.props.actions.handleServerUrlChanged}
|
||||
onSubmitEditing={this.onClick}
|
||||
style={GlobalStyles.inputBox}
|
||||
style={[GlobalStyles.inputBox, allowOtherServers ? {} : {backgroundColor: '#e3e3e3'}]}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
keyboardType='url'
|
||||
|
||||
@@ -234,7 +234,7 @@ export default class SelectTeam extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1
|
||||
@@ -297,5 +297,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 12
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
InteractionManager,
|
||||
Linking,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
@@ -259,7 +258,7 @@ class Settings extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
leftComponent: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
@@ -287,7 +286,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(Settings);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native';
|
||||
import {TouchableOpacity, View} from 'react-native';
|
||||
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
@@ -104,7 +104,7 @@ export default class SettingsItem extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: 51,
|
||||
@@ -139,5 +139,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
height: 1,
|
||||
marginHorizontal: 16
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
InteractionManager,
|
||||
Text,
|
||||
StyleSheet,
|
||||
View,
|
||||
WebView
|
||||
} from 'react-native';
|
||||
@@ -139,9 +138,9 @@ class SSO extends PureComponent {
|
||||
|
||||
Client4.setToken(token);
|
||||
setStoreFromLocalData({url: this.props.serverUrl, token}).
|
||||
then(handleSuccessfulLogin).
|
||||
then(getSession).
|
||||
then(this.goToLoadTeam);
|
||||
then(handleSuccessfulLogin).
|
||||
then(getSession).
|
||||
then(this.goToLoadTeam);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -191,7 +190,7 @@ class SSO extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
errorContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
@@ -204,7 +203,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
lineHeight: 23,
|
||||
paddingHorizontal: 30
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(SSO);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
import KeyboardLayout from 'app/components/layout/keyboard_layout';
|
||||
import PostList from 'app/components/post_list';
|
||||
@@ -91,10 +90,10 @@ export default class Thread extends PureComponent {
|
||||
}
|
||||
|
||||
const getStyle = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -146,7 +145,7 @@ class UserProfile extends PureComponent {
|
||||
}
|
||||
|
||||
const createStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
@@ -185,7 +184,7 @@ const createStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(UserProfile);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Switch,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
@@ -16,7 +15,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
const createStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelBg, 0.7),
|
||||
paddingHorizontal: 15,
|
||||
@@ -50,7 +49,7 @@ const createStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
wrapper: {
|
||||
backgroundColor: '#ddd'
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
function createTouchableComponent(children, action) {
|
||||
|
||||
@@ -45,6 +45,11 @@ export const getTheme = createSelector(
|
||||
|
||||
// At this point, the theme should be a plain object
|
||||
|
||||
// Fix a case where upper case theme colours are rendered as black
|
||||
for (const key of Object.keys(theme)) {
|
||||
theme[key] = theme[key].toLowerCase();
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user