Compare commits

..

38 Commits

Author SHA1 Message Date
enahum
fc179a6516 translations PR 20170905 (#882) 2017-09-05 09:46:41 -03:00
lfbrock
fea6372819 Update minimum server version (#878) 2017-09-02 21:29:47 -03:00
enahum
87e89de854 Version Bump to 49 (#876) 2017-09-01 14:50:01 -03:00
enahum
534af426c9 Version Bump to 49 (#875) 2017-09-01 14:49:48 -03:00
enahum
553f3796b1 RN-220 Add "in:" and "from:" search modifiers (#869)
* Fix search and search preview

* Add "in:" and "from:" search modifiers
2017-08-24 18:07:07 -03:00
enahum
18b3d6eec9 Fix badge display on Android (#867) 2017-08-24 11:31:03 -03:00
Harrison Healey
73c81bb863 RN-73 Fixed code block text colour being incorrect (#868) 2017-08-24 10:59:03 -03:00
enahum
6e4abacb4a Bump app version to 1.2.0 (#865) 2017-08-23 16:37:34 -03:00
enahum
9de87abdae Version Bump to 48 (#864) 2017-08-23 16:33:53 -03:00
enahum
d4b3e2ffc3 Version Bump to 48 (#863) 2017-08-23 16:33:42 -03:00
Chris Duarte
bffc75f8d1 RN-223 Emoji picker for adding new emoji reactions (#854)
* RN-223 Emoji picker for adding new emoji reactions

* Remove redundant setServerVersion and fetch custom emojis on reset

* Review feedback

* Review feedback 2

* Review feedback 3

* Review feedback 4

* Change aliases to array and add header to image source

* Remove aliases from custom emojis
2017-08-23 15:29:09 -03:00
enahum
b8d8be78d5 Fetch images using APIv4 (#862) 2017-08-22 22:23:10 -03:00
enahum
ecec78b2e0 translations PR 20170821 (#860) 2017-08-22 10:59:50 -03:00
enahum
f0f9195903 Include title and description for Android AppConfig (#859)
* Include title and description for Android AppConfig

* Update strings.xml

Fix descriptions
2017-08-21 11:41:07 -03:00
Harrison Healey
a291fdc933 RN-74 Added updated markdown code block view (#855)
* RN-74 Added updated markdown code block view

* Set default channelIsLoading prop to PostList

* RN-74 Fixed uneven line numbers on Android

* RN-74 Prevented double tapping on markdown code blocks
2017-08-21 11:40:42 -03:00
enahum
86261dad30 RN-303 fix small screen error overlap (#857)
* RN-303 fix small screen error overlap

* feeback review
2017-08-18 12:19:21 -04:00
enahum
cd5fb71681 Appconfig support for iOS and Android (#856)
* AppConfig support for iOS

* AppConfig support for Android

* Fix typo

* Java feedback review
2017-08-18 12:19:00 -04:00
Harrison Healey
2b33618f31 Reverted change to keyboardShouldPersistTaps for post list (#858) 2017-08-17 22:44:17 -03:00
Harrison Healey
57c602eb79 RN-113/RN-114 Updated markdown styles for lists and code (#851)
* RN-114 Updated markdown list style

* RN-113 Updated code font
2017-08-17 00:43:11 -03:00
enahum
3c32d5a9ed translations PR 20170814 (#853) 2017-08-16 10:02:16 -03:00
Harrison Healey
c6ae1090f7 Changed makeStyleSheetFromTheme to take a function that returns a plain object (#852)
* Changed makeStyleSheetFromTheme to take a function that returns a plain object

* Fixed style issues
2017-08-15 18:26:55 -03:00
Harrison Healey
f720b96f88 RN-177 Updated commonmark.js to match web app list behaviour (#848) 2017-08-11 16:45:39 -04:00
Harrison Healey
321a2bf576 RN-283 Properly render uppercase theme colours (#849) 2017-08-11 16:45:27 -04:00
Harrison Healey
d88f59bc6b RN-282 Switched usage of splice to slice (#850) 2017-08-11 16:45:13 -04:00
enahum
010336bb64 RN-306 Don't scroll to results when recent search is removed (#834)
* RN-306 Don't scroll to results when recent search is removed

* Feedback review
2017-08-11 11:42:52 -04:00
enahum
5422b5b472 Add LDAP login to the login options (#846) 2017-08-11 10:37:44 -04:00
enahum
f7cbf3afd0 Ensure reset of version and badge when logout (#832) 2017-08-11 10:37:25 -04:00
Chris Duarte
aeb3691696 Implement new image preview components (#838)
* Implement new image preview components

* Review feedback
2017-08-11 10:33:11 -04:00
enahum
523a66e023 Fix mention badge wrap text (#833) 2017-08-11 10:30:28 -04:00
lfbrock
9d4562841a Update full_description.txt (#837)
* Update full_description.txt

* Update full_description.txt
2017-08-10 11:40:11 -04:00
Chris Duarte
0dbbf65477 Fix empty serverVersion dispatch (#839) 2017-08-09 21:14:16 -04:00
lfbrock
404ac82ca2 Update README.md (#836) 2017-08-08 17:04:35 -04:00
enahum
8107dff4fe translations PR 20170807 (#835) 2017-08-08 12:46:28 -04:00
enahum
d9b76386c0 RN-288 Add analytics with segment.io (#828)
* Add analytics with segment.io

* Feedback review
2017-08-08 12:43:41 -04:00
Harrison Healey
a0a1678939 RN-174 Added support for strikethrough text in markdown (#816) 2017-08-07 10:37:29 -04:00
enahum
4928355dae Update fastlane (#826) 2017-08-04 17:43:11 -04:00
enahum
b7b817a973 Release 1.1 (#829)
* Updated yarn.lock

* Fastlane ensure branch for cutting builds (#815)

* Version Bump to 45 (#818)

* Android Version Bump to 45 (#819)

* Updated yarn.lock (#817)

* Version Bump to 45

* Fix Create on Create channel to not push another Create Channel screen (#820)

* Version Bump to 46 (#821)

* Version Bump to 46 (#822)

* RN-205/RN-210 Allow tapping buttons on post and DM lists without closing keyboard (#823)

* Version Bump to 47 (#824)

* Version Bump to 47 (#825)

* Release 1.1 changelog (#827)

* Release 1.1 changelog

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

Set supported server version

* update fastlane

* Update fastlane android release
2017-08-04 17:42:55 -04:00
Harrison Healey
0eeb9453c9 Updated yarn.lock (#817) 2017-08-03 16:25:40 -04:00
133 changed files with 7146 additions and 1686 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 its set to background to protect any confidential on-screen information, it also prevents taking screenshots of the app.</string>
<string name="jailbreakProtection_title">Jailbreak &#x2F; Root Detection</string>
<string name="jailbreakProtection_description">Disable app launch on Jailbroken or rooted devices.</string>
<string name="copyAndPasteProtection_title">Copy&amp;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>

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,9 @@ import Emoji from './emoji';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
customEmojis: getCustomEmojisByName(state),
...ownProps
token: state.entities.general.credentials.token
};
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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