Compare commits

..

42 Commits

Author SHA1 Message Date
Elias Nahum
56e36b0468 Version Bump to 57 2017-10-05 17:44:04 -03:00
Elias Nahum
b5b57085e5 Version Bump to 57 2017-10-05 17:41:26 -03:00
Elias Nahum
e082b42947 Fix crashes when user hasn't loaded and when postDraft was undefined 2017-10-05 16:59:07 -03:00
Elias Nahum
77fdaa9058 Version Bump to 56 2017-10-05 12:24:57 -03:00
Elias Nahum
774f1a1b47 Version Bump to 56 2017-10-05 12:20:48 -03:00
Elias Nahum
192e5093c1 Fix postTextBox at_mention autocomplete 2017-10-05 11:51:41 -03:00
Elias Nahum
52ba404b8e Version Bump to 55 2017-10-05 10:03:11 -03:00
Elias Nahum
8249080304 Version Bump to 55 2017-10-05 09:59:45 -03:00
Elias Nahum
b6c0d47d18 Fix Android crash on push notification 2017-10-05 09:38:15 -03:00
enahum
031876fb77 RN-382 Refactor at_mention & channel_mention autocomplete (#988)
* RN-382 Refactor at_mention & channel_mention autocomplete

* Feedback review

* If the term changes always trigger a request
2017-10-04 13:36:51 -07:00
Harrison Healey
ac0ac22f39 RN-383 Fixed new messages indicator (#990) 2017-10-04 08:57:48 -07:00
enahum
bed81ad514 Version Bump to 54 (#985) 2017-10-02 15:53:35 -03:00
enahum
642dd299c6 Version Bump to 54 (#984) 2017-10-02 15:51:15 -03:00
enahum
2633060a7f Remove context menu for system and ephemeral messages (#981) 2017-10-02 15:27:51 -03:00
enahum
50369d0c28 RN-362 Do not crash when leaving channels (#980) 2017-10-02 15:27:38 -03:00
enahum
4ef308469d Version Bump to 53 (#979) 2017-10-02 13:27:31 -03:00
enahum
f2394ba8df IOS Version Bump to 53 (#978)
* iOS deployment target 9.3

* Version Bump to 53
2017-10-02 13:25:22 -03:00
enahum
cc55b03e75 Make Post time format respect user display setting (#977) 2017-10-02 08:39:02 -07:00
enahum
82b3dcc1f6 Set minSdkVersion to 21 (Android 5.0) (#976)
* Set minSdkVersion to 21 (Android 5.0)

* Update README
2017-10-02 12:22:28 -03:00
enahum
13922e3764 Fix drawer according to number of teams (#974) 2017-10-02 12:22:14 -03:00
enahum
0df3c7428a Fix search issues (#973) 2017-10-02 12:21:56 -03:00
Harrison Healey
8e526b61ed RN-379 Get posts since last websocket disconnect when viewing channel (#971)
* RN-379 Added websocket state to device state

* Fixed view store blacklist

* RN-379 Get posts since last websocket disconnect when viewing channel

* Used Date.now instead of new Date().getTime()
2017-10-02 12:21:39 -03:00
Harrison Healey
c93f04a708 RN-268 Fixed teams list not always rerendering when team member changes (#970)
* RN-268 Fixed teams list not always rerendering when team member changes

* Removed unused prop from ChannelDrawer

* RN-268 Passed fewer props into TeamsListItem
2017-09-29 12:46:03 -03:00
enahum
e3761fc529 Version Bump to 52 (#968) 2017-09-28 15:55:40 -03:00
enahum
72fef11496 Version Bump to 52 (#967) 2017-09-28 15:55:27 -03:00
enahum
6fdd58b481 Fix blank screen when user has no teams (#965) 2017-09-28 15:16:35 -03:00
Harrison Healey
ad2d126ec0 RN-364 Fixed switch teams button rendering with only one team (#964) 2017-09-28 14:53:41 -03:00
Harrison Healey
76eb5d06fd Fixed drawer rendering empty if the user is only on one team (#963) 2017-09-28 14:53:27 -03:00
enahum
1e434346ae Multiple performance improvements (#956)
* Update fastlane

* Multiple performance improvements

* Feedback review

* Feedback review
2017-09-28 12:54:32 -03:00
enahum
a694122ffd Fix post additional content (#962)
* Fix post additional content

* Feedback review
2017-09-28 12:02:41 -03:00
enahum
0c3bb89832 Fix SSO login (#960) 2017-09-28 11:32:26 -03:00
enahum
78e6b8d5a3 Fix image preview initial scroll (#959) 2017-09-28 11:31:44 -03:00
enahum
ae7c566375 Fix android search autocomplete (#958) 2017-09-28 11:17:58 -03:00
enahum
6f260bf4c7 RN-320 Tapping enter in channel header/purpose keeps the keyboard open (#961) 2017-09-28 11:17:34 -03:00
Harrison Healey
ff65b52618 Added selector for getting sorted teams list (#954) 2017-09-28 11:17:22 -03:00
enahum
2f47d7db2e Include iOS marketing icon (#957) 2017-09-28 06:02:41 -03:00
Harrison Healey
b8e450ba85 RN-369 Added a maximum depth to the markdown renderer (#942)
* RN-369 Added a maximum depth to the markdown renderer

* Updated yarn.lock
2017-09-27 17:49:38 -07:00
Harrison Healey
f2533bd650 RN-349 Mark channel as read when switching teams and opening the app (#953)
* RN-349 Mark current channel as read when opening the app

* RN-349 Mark channels as read when switching teams

* Moved markChannelAsRead into handleTeamChange action
2017-09-27 15:34:09 -03:00
enahum
f9419a7746 Version Bump to 51 (#938) 2017-09-27 10:41:35 -03:00
enahum
978c80bef1 Version Bump to 51 (#937) 2017-09-27 10:41:21 -03:00
enahum
6e1d8471f7 translations PR 20170925 (#940) 2017-09-27 07:47:15 -03:00
Elias Nahum
fa9110d9d7 UpsideDown required by apple 2017-09-25 15:44:35 -03:00
367 changed files with 7599 additions and 22089 deletions

View File

@@ -1,10 +1,5 @@
{
"presets": [ "react-native" ],
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
},
"plugins": [
["module-resolver", {
"root": ["./src", "."],

View File

@@ -1,48 +1,58 @@
[ignore]
; We fork some components by platform
# We fork some components by platform.
.*/*[.]android.js
; Ignore "BUCK" generated dirs
# Ignore templates with `@flow` in header
.*/local-cli/generator.*
# Ignore malformed json
.*/node_modules/y18n/test/.*\.json
# Ignore the website subdir
<PROJECT_ROOT>/website/.*
# Ignore BUCK generated dirs
<PROJECT_ROOT>/\.buckd/
; Ignore unexpected extra "@providesModule"
.*/node_modules/.*/node_modules/fbjs/.*
# Ignore unexpected extra @providesModule
.*/node_modules/commoner/test/source/widget/share.js
; Ignore duplicate module providers
; For RN Apps installed via npm, "Libraries" folder is inside
; "node_modules/react-native" but in the source repo it is in the root
# Ignore duplicate module providers
# For RN Apps installed via npm, "Libraries" folder is inside node_modules/react-native but in the source repo it is in the root
.*/Libraries/react-native/React.js
; Ignore polyfills
.*/Libraries/polyfills/.*
.*/Libraries/react-native/ReactNative.js
.*/node_modules/jest-runtime/build/__tests__/.*
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
node_modules/react-native/flow
flow/
[options]
emoji=true
module.system=haste
esproposal.class_static_fields=enable
esproposal.class_instance_fields=enable
experimental.strict_type_args=true
munge_underscores=true
module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub'
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_type=$FixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(5[0-3]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-3]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-2]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-2]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
unsafe.enable_getters_and_setters=true
[version]
^0.53.0
^0.32.0

27
.gitignore vendored
View File

@@ -1,8 +1,5 @@
assets/override
dist
*.zip
server.PID
mattermost.keystore
# OSX
#
@@ -29,27 +26,28 @@ DerivedData
*.xcuserstate
project.xcworkspace
# Android/IntelliJ
# Android/IJ
#
build/
*.iml
.idea
.gradle
local.properties
*.iml
# node.js
#
node_modules/
npm-debug.log
.npminstall
yarn-error.log
# yarn
#
.yarninstall
# BUCK
buck-out/
\.buckd/
android/app/libs
*.keystore
android/keystores/debug.keystore
# Vim
[._]*.s[a-w][a-z]
@@ -60,17 +58,10 @@ Session.vim
*~
tags
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/
fastlane/report.xml
fastlane/Preview.html
*/fastlane/screenshots
*.zip
server.PID
mattermost.keystore
# Sentry
android/sentry.properties

View File

@@ -1,31 +1,5 @@
# Mattermost Mobile Apps Changelog
## v1.4.1 Release
Release Date: Nov 15, 2017
Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed network detection issue causing some people to be unable to access the app
- Fixed issue with lag when pressing send button
- Fixed app crash when opening notification settings
- Fixed various other bugs to reduce app crashes
## v1.4 Release
- Release Date: November 6, 2017
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Performance improvements
- Various performance improvements to decrease channel load times
### Bug Fixes
- Fixed issue with Android app sometimes showing a white screen when re-opening the app
- Fixed an issue with orientation lock not working on Android
## v1.3 Release
- Release Date: October 5, 2017

View File

@@ -6,10 +6,8 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.provider.Settings;
import android.support.annotation.NonNull;
@@ -46,7 +44,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.List;
import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.modules.core.PermissionAwareActivity;
@@ -199,9 +196,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
public void doOnCancel()
{
if (this.callback != null) {
responseHelper.invokeCancel(this.callback);
}
responseHelper.invokeCancel(callback);
}
public void launchCamera()
@@ -226,7 +221,6 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
return;
}
this.callback = callback;
this.options = options;
if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_CAMERA))
@@ -257,12 +251,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
final File original = createNewFile(reactContext, this.options, false);
imageConfig = imageConfig.withOriginalFile(original);
if (imageConfig.original != null) {
cameraCaptureURI = RealPathUtil.compatUriFromFile(reactContext, imageConfig.original);
}else {
responseHelper.invokeError(callback, "Couldn't get file path for photo");
return;
}
cameraCaptureURI = RealPathUtil.compatUriFromFile(reactContext, imageConfig.original);
if (cameraCaptureURI == null)
{
responseHelper.invokeError(callback, "Couldn't get file path for photo");
@@ -277,16 +266,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
return;
}
// Workaround for Android bug.
// grantUriPermission also needed for KITKAT,
// see https://code.google.com/p/android/issues/detail?id=76683
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
List<ResolveInfo> resInfoList = reactContext.getPackageManager().queryIntentActivities(cameraIntent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
reactContext.grantUriPermission(packageName, cameraCaptureURI, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}
this.callback = callback;
try
{
@@ -314,7 +294,6 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
}
this.options = options;
this.callback = callback;
if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_LIBRARY))
{
@@ -335,7 +314,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
{
requestCode = REQUEST_LAUNCH_IMAGE_LIBRARY;
libraryIntent = new Intent(Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}
if (libraryIntent.resolveActivity(reactContext.getPackageManager()) == null)
@@ -344,6 +323,8 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
return;
}
this.callback = callback;
try
{
currentActivity.startActivityForResult(libraryIntent, requestCode);
@@ -590,9 +571,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
innerActivity.startActivityForResult(intent, 1);
}
});
if (dialog != null) {
dialog.show();
}
dialog.show();
return false;
}
else
@@ -627,7 +606,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
private boolean isCameraAvailable() {
return reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)
|| reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|| reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
}
private @NonNull String getRealPathFromURI(@NonNull final Uri uri) {

View File

@@ -60,7 +60,7 @@ check-device-ios:
@exit 1; \
fi
run-ios: | check-device-ios start-build-packager
run-ios: | check-device-ios start
@echo Running iOS app in development
@react-native run-ios --simulator="${SIMULATOR}"
@@ -82,11 +82,11 @@ endif
@exit 1; \
fi
run-android: | check-device-android start-build-packager prepare-android-build
run-android: | check-device-android start prepare-android-build
@echo Running Android app in development
@react-native run-android --no-packager
test: | pre-run check-style
test: pre-run
@yarn test
check-style: .yarninstall
@@ -120,26 +120,9 @@ post-install:
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_FULL_USER);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
@sed -i'' -e "s|var AndroidTextInput = requireNativeComponent('AndroidTextInput', null);|var AndroidTextInput = requireNativeComponent('CustomTextInput', null);|g" node_modules/react-native/Libraries/Components/TextInput/TextInput.js
@sed -i'' -e 's^getItemLayout || index <= this._highestMeasuredFrameIndex,^!getItemLayout || index !== -1,^g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
@if [ $(shell grep "const Platform" node_modules/react-native/Libraries/Lists/VirtualizedList.js | grep -civ grep) -eq 0 ]; then \
sed $ -i'' -e "s|const ReactNative = require('ReactNative');|const ReactNative = require('ReactNative');`echo $\\\\\\r;`const Platform = require('Platform');|g" node_modules/react-native/Libraries/Lists/VirtualizedList.js; \
fi
@sed -i'' -e 's|transform: \[{scaleY: -1}\],|...Platform.select({android: {transform: \[{perspective: 1}, {scaleY: -1}\]}, ios: {transform: \[{scaleY: -1}\]}}),|g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
@cd ./node_modules/react-native-svg/ios && rm -rf PerformanceBezier && git clone https://github.com/adamwulf/PerformanceBezier.git
@cd ./node_modules/mattermost-redux && yarn run build
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_SENSOR);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
start-packager:
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
node ./node_modules/react-native/local-cli/cli.js start; \
else \
echo React Native packager server already running; \
ps -e | grep -i "cli.js start" | grep -v grep | awk '{print $$1}' > server.PID; \
fi
start-build-packager:
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
node ./node_modules/react-native/local-cli/cli.js start --reset-cache & echo $$! > server.PID; \
@@ -171,10 +154,10 @@ endif
do-build-ios:
@echo "Building ios $(ios_target) app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios $(ios_target)
@cd fastlane && NODE_ENV=production bundle exec fastlane ios $(ios_target)
build-ios: | check-ios-target pre-run check-style start-build-packager do-build-ios stop-packager
build-ios: | check-ios-target pre-run check-style start-packager do-build-ios stop-packager
check-android-target:
ifeq ($(android_target), )
@@ -195,9 +178,9 @@ prepare-android-build:
do-build-android:
@echo "Building android $(android_target) app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android $(android_target)
@cd fastlane && NODE_ENV=production bundle exec fastlane android $(android_target)
build-android: | check-android-target pre-run check-style start-build-packager prepare-android-build do-build-android stop-packager
build-android: | check-android-target pre-run check-style start-packager prepare-android-build do-build-android stop-packager
do-unsigned-ios:
@echo "Building unsigned iOS app"
@@ -213,9 +196,9 @@ do-unsigned-android:
@cd fastlane && NODE_ENV=production bundle exec fastlane android unsigned
@mv android/app/build/outputs/apk/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
unsigned-android: pre-run check-style start-build-packager do-unsigned-android stop-packager
unsigned-android: pre-run check-style start-packager do-unsigned-android stop-packager
unsigned-ios: pre-run check-style start-build-packager do-unsigned-ios stop-packager
unsigned-ios: pre-run check-style start-packager do-unsigned-ios stop-packager
alpha:
@:

View File

@@ -386,6 +386,39 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
---
## react-native-keyboard-spacer
This product contains 'react-native-keyboard-spacer', a keyboard spacer view for React Native by Andrew Hurst.
* HOMEPAGE:
* https://github.com/Andr3wHur5t/react-native-keyboard-spacer
* LICENSE:
The MIT License (MIT)
Copyright (c) 2015 Andrew Hurst
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-vector-icons
This product contains 'react-native-vector-icons', a set of vector icons for use in React Native apps by Joel Arvidsson.
@@ -1379,807 +1412,3 @@ 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-video
This product contains 'react-native-video', A <Video> component for react-native.
* HOMEPAGE:
* https://github.com/react-native-community/react-native-video
* LICENSE:
MIT License
Copyright (c) 2016 Brent Vatne, Baris Sencan
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-slider
This product contains 'react-native-slider', It is a drop-in replacement for React Native Slider by Jean Regisser.
* HOMEPAGE:
* https://github.com/jeanregisser/react-native-slider
* LICENSE:
MIT License
Copyright (c) 2015-present Jean Regisser
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-doc-viewer
This product contains 'react-native-doc-viewer', A React Native bridge module: Document Viewer for files (pdf, png, jpg, xls, doc, ppt, xlsx, docx, pptx etc.)
* HOMEPAGE:
* https://github.com/philipphecht/react-native-doc-viewer
* LICENSE:
MIT License
Copyright (c) 2017 Phil Pike
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-safe-area
This product contains 'react-native-safe-area', React Native module to retrieve safe area insets for iOS 11 or later.
* HOMEPAGE:
* https://github.com/miyabi/react-native-safe-area
* LICENSE:
MIT License
Copyright (c) 2017 Masayuki Iwai
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.
---
## deep-equal
This is a Node's assert.deepEqual() algorithm as a standalone module.
* HOMEPAGE:
* https://github.com/substack/node-deep-equal
* LICENSE:
The MIT license
This software is released under the MIT license:
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.
---
## mattermost-redux
The project purpose is consolidating the storage, web utilities and logic of the webapp and React Native mobile clients into a single driver.
* HOMEPAGE:
* https://github.com/mattermost/mattermost-redux
* LICENSE:
Apache License
Copyright 2016 Mattermost, Inc.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---
## prop-types
The project is a runtime type checking for React props and similar objects.
* HOMEPAGE:
* https://github.com/facebook/prop-types
* LICENSE:
MIT License
Copyright (c) 2013-present, Facebook, Inc.
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-animatable
The project is an easy to use declarative transitions and a standard set of animations for React Native.
* HOMEPAGE:
* https://github.com/oblador/react-native-animatable
* LICENSE:
The MIT License (MIT)
Copyright (c) 2015 Joel Arvidsson
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-bottom-sheet
This is a React Native Bottom Sheet module for Android.
* HOMEPAGE:
* https://github.com/WhatAKitty/react-native-bottom-sheet
* LICENSE:
The MIT License (MIT)
Copyright (c) 2016 WhatAKitty
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-cookies
This project is a cookie manager for react native.
* HOMEPAGE:
* https://github.com/joeferraro/react-native-cookies
* LICENSE:
The MIT License (MIT)
Copyright (c) 2015 Joseph P. Ferraro
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-orientation
This product contains a modified version of 'react-native-orientation'. Allows to listen to device orientation changes in React Native applications and programmatically set preferred orientation on a per screen basis.
* HOMEPAGE:
* https://github.com/yamill/react-native-orientation
* LICENSE:
ISC License
Copyright 2017 React Native Orientation
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.
---
## reselect
This project is a simple “selector” library for Redux.
* HOMEPAGE:
* https://github.com/reactjs/reselect
* LICENSE:
The MIT License (MIT)
Copyright (c) 2015-2016 Reselect Contributors
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.
---
## shallow-equals
This project can be used to determine if an array or object is equivalent with another, not recursively.
* HOMEPAGE:
* https://github.com/hughsk/shallow-equals
* LICENSE:
The MIT License (MIT)
Copyright (c) 2014 Hugh Kennedy
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.
---
## socketcluster
SocketCluster is a fast, highly scalable HTTP + realtime server engine which lets you build multi-process realtime servers that make use of all CPU cores on a machine/instance.
* HOMEPAGE:
* https://github.com/SocketCluster/socketcluster
* LICENSE:
(The MIT License)
Copyright (c) 2013-2017 SocketCluster.io
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.
---
## commonmark
This product contains a modified version of 'commonmark'. CommonMark is a rationalized version of Markdown syntax, with a spec and BSD-licensed reference implementations in C and JavaScript.
* HOMEPAGE:
* https://github.com/commonmark/CommonMark
* LICENSE:
The CommonMark spec (spec.txt) and DTD (CommonMark.dtd) are
Copyright (C) 2014-16 John MacFarlane
Released under the Creative Commons CC-BY-SA 4.0 license:
<http://creativecommons.org/licenses/by-sa/4.0/>.
The test software in test/ and the programs in tools/ are
Copyright (c) 2014, John MacFarlane
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The normalization code in runtests.py was derived from the
markdowntest project, Copyright 2013 Karl Dubost:
The MIT License (MIT)
Copyright (c) 2013 Karl Dubost
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.
---
## commonmark-react-renderer
This project is a renderer for CommonMark which returns an array of React elements
* HOMEPAGE:
* https://github.com/rexxars/commonmark-react-renderer
* LICENSE:
The MIT License (MIT)
Copyright (c) 2015 Espen Hovlandsdal
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.
---
## semver
This project is a microservice semver registry.
* HOMEPAGE:
* https://github.com/quarterto/semserver
* LICENSE:
ISC License (ISC)
Copyright 2016 Matt Brennan
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.
---
## babel-polyfill
This project includes a custom regenerator runtime and core-js.
* HOMEPAGE:
* https://github.com/babel/babel/tree/master/packages/babel-polyfill
* LICENSE:
The MIT License (MIT)
Copyright (c) 2017 Brian Ng
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-media-controls
This product contains a modified version of 'react-native-media-controls' This project is a UI component to manipulate your media.
* HOMEPAGE:
* https://github.com/charliesbox/react-native-media-controls
* LICENSE:
he MIT License (MIT)
Copyright (c) 2017 Charlie
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-section-list-get-item-layout
This package provides a function that helps you construct the getItemLayout function for your SectionLists.
* HOMEPAGE:
* https://github.com/jsoendermann/rn-section-list-get-item-layout
* LICENSE:
Copyright (c) 2017 Jan Soendermann
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---

View File

@@ -84,8 +84,7 @@ Follow the [React Native Getting Started Guide](https://facebook.github.io/react
$ cd watchman
$ git checkout master
$ ./autogen.sh
$ ./configure
$ make
$ ./configure make
$ sudo make install
```
Configure your kernel to accept a lot of file watches, using a command like:

View File

@@ -1,3 +1,5 @@
import re
# To learn about Buck see [Docs](https://buckbuild.com/).
# To run your application with Buck:
# - install Buck
@@ -9,9 +11,8 @@
#
lib_deps = []
for jarfile in glob(['libs/*.jar']):
name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')]
name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile)
lib_deps.append(':' + name)
prebuilt_jar(
name = name,
@@ -19,7 +20,7 @@ for jarfile in glob(['libs/*.jar']):
)
for aarfile in glob(['libs/*.aar']):
name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')]
name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile)
lib_deps.append(':' + name)
android_prebuilt_aar(
name = name,
@@ -27,39 +28,39 @@ for aarfile in glob(['libs/*.aar']):
)
android_library(
name = "all-libs",
exported_deps = lib_deps,
name = 'all-libs',
exported_deps = lib_deps
)
android_library(
name = "app-code",
srcs = glob([
"src/main/java/**/*.java",
]),
deps = [
":all-libs",
":build_config",
":res",
],
name = 'app-code',
srcs = glob([
'src/main/java/**/*.java',
]),
deps = [
':all-libs',
':build_config',
':res',
],
)
android_build_config(
name = "build_config",
package = "com.mattermost-mobile",
name = 'build_config',
package = 'com.mattermost.rnbeta',
)
android_resource(
name = "res",
res = "src/main/res",
package = "com.mattermost.rnbeta",
name = 'res',
res = 'src/main/res',
package = 'com.mattermost.rnbeta',
)
android_binary(
name = "app",
keystore = "//android/keystores:debug",
manifest = "src/main/AndroidManifest.xml",
package_type = "debug",
deps = [
":app-code",
],
name = 'app',
package_type = 'debug',
manifest = 'src/main/AndroidManifest.xml',
keystore = '//android/keystores:debug',
deps = [
':app-code',
],
)

View File

@@ -33,13 +33,6 @@ import com.android.build.OutputFile
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
*
* // whether to disable dev mode in custom build variants (by default only disabled in release)
* // for example: to disable dev mode in the staging build type (if configured)
* devDisabledInStaging: true,
* // The configuration property can be in the following formats
* // 'devDisabledIn${productFlavor}${buildType}'
* // 'devDisabledIn${buildType}'
*
* // the root of your project, i.e. where "package.json" lives
* root: "../../",
*
@@ -65,21 +58,17 @@ import com.android.build.OutputFile
* inputExcludes: ["android/**", "ios/**"],
*
* // override which node gets called and with what additional arguments
* nodeExecutableAndArgs: ["node"],
* nodeExecutableAndArgs: ["node"]
*
* // supply additional arguments to the packager
* extraPackagerArgs: []
* ]
*/
project.ext.react = [
entryFile: "index.js"
]
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
if (System.getenv("SENTRY_ENABLED") == "true") {
if (System.getenv("MM_SENTRY_ENABLED") == "true") {
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
}
@@ -106,8 +95,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion 21
targetSdkVersion 23
versionCode 74
versionName "1.5.1"
versionCode 57
versionName "1.3.0"
multiDexEnabled true
ndk {
abiFilters "armeabi-v7a", "x86"
@@ -163,11 +152,8 @@ android {
}
dependencies {
compile project(':react-native-doc-viewer')
compile project(':react-native-video')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:25.0.1"
compile 'com.android.support:percent:25.3.1'
compile "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-navigation')
compile project(':react-native-image-picker')

View File

@@ -50,10 +50,6 @@
-dontwarn com.facebook.react.**
# TextLayoutBuilder uses a non-public Android constructor within StaticLayout.
# See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details.
-dontwarn android.text.StaticLayout
# okhttp
-keepattributes Signature

View File

@@ -1,30 +0,0 @@
package com.mattermost.components;
import android.content.Context;
import android.text.InputType;
import com.facebook.react.views.textinput.ReactEditText;
public class CustomTextInput extends ReactEditText {
private boolean autoScroll = false;
public CustomTextInput(Context context) {
super(context);
}
private boolean isMultiline() {
return (getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
}
@Override
public boolean isLayoutRequested() {
if (isMultiline() && !autoScroll) {
return true;
}
return false;
}
public void setAutoScroll(boolean autoScroll) {
this.autoScroll = autoScroll;
}
}

View File

@@ -1,35 +0,0 @@
package com.mattermost.components;
import android.text.InputType;
import android.util.TypedValue;
import com.facebook.react.views.textinput.ReactTextInputManager;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewDefaults;
import com.facebook.react.uimanager.annotations.ReactProp;
public class CustomTextInputManager extends ReactTextInputManager {
@Override
public String getName() {
return "CustomTextInput";
}
@Override
public CustomTextInput createViewInstance(ThemedReactContext context) {
CustomTextInput editText = new CustomTextInput(context);
int inputType = editText.getInputType();
editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
editText.setReturnKeyType("done");
editText.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
(int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP)));
return editText;
}
@ReactProp(name = "autoScroll", defaultBoolean = false)
public void setAutoScroll(CustomTextInput view, boolean autoScroll) {
view.setAutoScroll(autoScroll);
}
}

View File

@@ -16,11 +16,8 @@ import android.os.Build;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.provider.Settings.System;
import java.util.LinkedHashMap;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.lang.reflect.Field;
import com.wix.reactnativenotifications.core.notification.PushNotification;
@@ -43,7 +40,7 @@ public class CustomPushNotification extends PushNotification {
public static final String NOTIFICATION_REPLIED_EVENT_NAME = "notificationReplied";
private static LinkedHashMap<String,Integer> channelIdToNotificationCount = new LinkedHashMap<String,Integer>();
private static LinkedHashMap<String,List<Bundle>> channelIdToNotification = new LinkedHashMap<String,List<Bundle>>();
private static LinkedHashMap<String,ArrayList<Bundle>> channelIdToNotification = new LinkedHashMap<String,ArrayList<Bundle>>();
private static AppLifecycleFacade lifecycleFacade;
private static Context context;
@@ -56,21 +53,8 @@ public class CustomPushNotification extends PushNotification {
if (notificationId != -1) {
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
if (context != null) {
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
}
}
public static void clearNotification(Context mContext, int notificationId, String channelId) {
if (notificationId != -1) {
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
if (mContext != null) {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
}
@@ -90,16 +74,14 @@ public class CustomPushNotification extends PushNotification {
channelIdToNotificationCount.put(channelId, count);
Object bundleArray = channelIdToNotification.get(channelId);
List list = null;
ArrayList list = null;
if (bundleArray == null) {
list = Collections.synchronizedList(new ArrayList(0));
list = new ArrayList(0);
} else {
list = Collections.synchronizedList((List)bundleArray);
}
synchronized (list) {
list.add(0, data);
channelIdToNotification.put(channelId, list);
list = (ArrayList)bundleArray;
}
list.add(0, data);
channelIdToNotification.put(channelId, list);
}
if ("clear".equals(type)) {
@@ -210,7 +192,7 @@ public class CustomPushNotification extends PushNotification {
String summaryTitle = String.format("%s (%d)", title, numMessages);
Notification.InboxStyle style = new Notification.InboxStyle();
List<Bundle> list = new ArrayList<Bundle>(channelIdToNotification.get(channelId));
ArrayList<Bundle> list = (ArrayList<Bundle>) channelIdToNotification.get(channelId);
for (Bundle data : list){
String msg = data.getString("message");
@@ -272,14 +254,14 @@ public class CustomPushNotification extends PushNotification {
notification.setSound(Uri.parse(soundUri), AudioManager.STREAM_NOTIFICATION);
}
} else {
Uri defaultUri = System.DEFAULT_NOTIFICATION_URI;
Uri defaultUri = RingtoneManager.getActualDefaultRingtoneUri(mContext, RingtoneManager.TYPE_NOTIFICATION);
notification.setSound(defaultUri, AudioManager.STREAM_NOTIFICATION);
}
boolean vibrate = notificationPreferences.getShouldVibrate();
if (vibrate) {
// use the system default for vibration
notification.setDefaults(Notification.DEFAULT_VIBRATE);
// Each element then alternates between delay, vibrate, sleep, vibrate, sleep
notification.setVibrate(new long[] {1000, 1000, 500, 1000, 500});
}
boolean blink = notificationPreferences.getShouldBlink();

View File

@@ -2,9 +2,62 @@ package com.mattermost.rnbeta;
import com.reactnativenavigation.controllers.SplashActivity;
import java.lang.ref.WeakReference;
import android.content.Context;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.graphics.Color;
import android.widget.TextView;
import android.view.ViewGroup.LayoutParams;
import android.view.Gravity;
import android.util.TypedValue;
public class MainActivity extends SplashActivity {
@Override
public int getSplashLayout() {
return R.layout.launch_screen;
}
private static ImageView imageView;
private static WeakReference<MainActivity> wr_activity;
protected static MainActivity getActivity() {
return wr_activity.get();
}
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
// @Override
// protected String getMainComponentName() {
// return "Mattermost";
// }
@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();
NotificationsLifecycleFacade.getInstance().LoadManagedConfig(getActivity());
imageView = new ImageView(context);
imageView.setImageResource(drawableId);
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;
}
private static int getImageId() {
int drawableId = getActivity().getResources().getIdentifier("splash", "drawable", getActivity().getClass().getPackage().getName());
if (drawableId == 0) {
drawableId = getActivity().getResources().getIdentifier("splash", "drawable", getActivity().getPackageName());
}
return drawableId;
}
}

View File

@@ -6,9 +6,6 @@ import android.content.Context;
import android.os.Bundle;
import com.facebook.react.ReactApplication;
import com.reactlibrary.RNReactNativeDocViewerPackage;
import com.brentvatne.react.ReactVideoPackage;
import com.horcrux.svg.SvgPackage;
import com.inprogress.reactnativeyoutube.ReactNativeYouTube;
import io.sentry.RNSentryPackage;
import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
@@ -26,6 +23,7 @@ import com.gnet.bottomsheet.RNBottomSheetPackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.psykar.cookiemanager.CookieManagerPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.horcrux.svg.SvgPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.github.yamill.orientation.OrientationPackage;
import com.reactnativenavigation.NavigationApplication;
@@ -68,17 +66,10 @@ public class MainApplication extends NavigationApplication implements INotificat
new MattermostPackage(this),
new RNSentryPackage(this),
new ReactNativeExceptionHandlerPackage(),
new ReactNativeYouTube(),
new ReactVideoPackage(),
new RNReactNativeDocViewerPackage()
new ReactNativeYouTube()
);
}
@Override
public String getJSMainModuleName() {
return "index";
}
@Override
public void onCreate() {
super.onCreate();

View File

@@ -10,8 +10,6 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.JavaScriptModule;
import com.mattermost.components.CustomTextInputManager;
public class MattermostPackage implements ReactPackage {
private final MainApplication mApplication;
@@ -29,8 +27,6 @@ public class MattermostPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new CustomTextInputManager()
);
return Collections.emptyList();
}
}

View File

@@ -4,23 +4,20 @@ import android.content.Context;
import android.content.Intent;
import android.app.IntentService;
import android.os.Bundle;
import android.util.Log;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationDismissService extends IntentService {
private Context mContext;
public NotificationDismissService() {
super("notificationDismissService");
}
@Override
protected void onHandleIntent(Intent intent) {
mContext = getApplicationContext();
Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
String channelId = bundle.getString("channel_id");
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
Log.i("ReactNative", "Dismiss notification");
CustomPushNotification.clearNotification(notificationId, channelId);
}
}

View File

@@ -6,7 +6,6 @@ import android.app.NotificationManager;
import android.app.RemoteInput;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
@@ -16,11 +15,9 @@ import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationReplyService extends HeadlessJsTaskService {
private Context mContext;
@Override
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
mContext = getApplicationContext();
if (CustomPushNotification.KEY_TEXT_REPLY.equals(intent.getAction())) {
CharSequence message = getReplyMessage(intent);
@@ -30,9 +27,8 @@ public class NotificationReplyService extends HeadlessJsTaskService {
bundle.putInt("msg_count", CustomPushNotification.getMessageCountInChannel(channelId));
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
CustomPushNotification.clearNotification(notificationId, channelId);
Log.i("ReactNative", "Replying service");
return new HeadlessJsTaskConfig(
"notificationReplied",
Arguments.fromBundle(bundle),

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.percent.PercentRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:gravity="center_horizontal"
tools:context=".SplashScreenActivity">
<ImageView
android:id="@+id/imgLogo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:src="@drawable/splash" />
</android.support.percent.PercentRelativeLayout>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="white">#FFFFFF</color>
</resources>

View File

@@ -3,9 +3,7 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:windowIsTranslucent">false</item>
<item name="android:windowBackground">@color/white</item>
<item name="android:colorBackground">@color/white</item>
<item name="android:windowIsTranslucent">true</item>
</style>
</resources>

View File

@@ -1,8 +1,8 @@
keystore(
name = "debug",
properties = "debug.keystore.properties",
store = "debug.keystore",
visibility = [
"PUBLIC",
],
name = 'debug',
store = 'debug.keystore',
properties = 'debug.keystore.properties',
visibility = [
'PUBLIC',
],
)

View File

@@ -1,8 +1,4 @@
rootProject.name = 'Mattermost'
include ':react-native-doc-viewer'
project(':react-native-doc-viewer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-doc-viewer/android')
include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
include ':react-native-youtube'
project(':react-native-youtube').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-youtube/android')
include ':react-native-sentry'
@@ -31,9 +27,9 @@ include ':reactnativenotifications'
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android')
include ':app'
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-orientation'
project(':react-native-orientation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation/android')
include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')

View File

@@ -1,4 +0,0 @@
{
"name": "Mattermost",
"displayName": "Mattermost"
}

View File

@@ -8,9 +8,10 @@ import {ViewTypes} from 'app/constants';
import {UserTypes} from 'mattermost-redux/action_types';
import {
fetchMyChannelsAndMembers,
markChannelAsRead,
getChannelStats,
selectChannel,
leaveChannel as serviceLeaveChannel
leaveChannel as serviceLeaveChannel,
unfavoriteChannel
} from 'mattermost-redux/actions/channels';
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts';
import {getFilesForPost} from 'mattermost-redux/actions/files';
@@ -18,20 +19,18 @@ import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
import {General, Preferences} from 'mattermost-redux/constants';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {
getChannelByName,
getDirectChannelName,
getUserIdFromChannelName,
isDirectChannelVisible,
isGroupChannelVisible,
isDirectChannel,
isGroupChannel
} from 'mattermost-redux/utils/channel_utils';
import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
import {isDirectChannelVisible, isGroupChannelVisible} from 'app/utils/channels';
const MAX_POST_TRIES = 3;
export function loadChannelsIfNecessary(teamId) {
@@ -181,17 +180,15 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
};
}
export async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
for (let i = 0; i < maxTries; i++) {
const {data} = await action(dispatch, getState);
const posts = await action(dispatch, getState);
if (data) {
dispatch(setChannelRetryFailed(false));
return data;
if (posts) {
return posts;
}
}
dispatch(setChannelRetryFailed(true));
return null;
}
@@ -224,8 +221,7 @@ export function selectInitialChannel(teamId) {
const {channels, myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
const {myPreferences} = state.entities.preferences;
const lastChannelForTeam = state.views.team.lastChannelForTeam[teamId];
const lastChannelId = lastChannelForTeam && lastChannelForTeam.length ? lastChannelForTeam[0] : '';
const lastChannelId = state.views.team.lastChannelForTeam[teamId] || '';
const lastChannel = channels[lastChannelId];
const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL &&
@@ -237,27 +233,20 @@ export function selectInitialChannel(teamId) {
if (lastChannelId && myMembers[lastChannelId] &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)) {
handleSelectChannel(lastChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId)(dispatch, getState);
return;
}
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
let channelId;
if (channel) {
channelId = channel.id;
dispatch(setChannelDisplayName(''));
handleSelectChannel(channel.id)(dispatch, getState);
} else {
// Handle case when the default channel cannot be found
// so we need to get the first available channel of the team
const channelsInTeam = Object.values(channels).filter((c) => c.team_id === teamId);
const firstChannel = channelsInTeam.length ? channelsInTeam[0].id : {id: ''};
channelId = firstChannel.id;
}
if (channelId) {
dispatch(setChannelDisplayName(''));
handleSelectChannel(channelId)(dispatch, getState);
markChannelAsRead(channelId)(dispatch, getState);
handleSelectChannel(firstChannel.id)(dispatch, getState);
}
};
}
@@ -266,84 +255,28 @@ export function handleSelectChannel(channelId) {
return async (dispatch, getState) => {
const {currentTeamId} = getState().entities.teams;
loadPostsIfNecessaryWithRetry(channelId)(dispatch, getState);
selectChannel(channelId)(dispatch, getState);
dispatch(batchActions([
{
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
data: channelId
},
setChannelLoading(false),
{
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
teamId: currentTeamId,
channelId
}
]), 'BATCH_CHANNEL_LOADED');
dispatch(setChannelLoading(false));
dispatch({
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
teamId: currentTeamId,
channelId
});
getChannelStats(channelId)(dispatch, getState);
};
}
export function handlePostDraftChanged(channelId, draft) {
export function handlePostDraftChanged(channelId, postDraft) {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.POST_DRAFT_CHANGED,
channelId,
draft
postDraft
}, getState);
};
}
export function handlePostDraftSelectionChanged(channelId, cursorPosition) {
return {
type: ViewTypes.POST_DRAFT_SELECTION_CHANGED,
channelId,
cursorPosition
};
}
export function insertToDraft(value) {
return (dispatch, getState) => {
const state = getState();
const channelId = getCurrentChannelId(state);
const threadId = state.entities.posts.selectedPostId;
let draft;
let cursorPosition;
let action;
if (state.views.thread.drafts[threadId]) {
const threadDraft = state.views.thread.drafts[threadId];
draft = threadDraft.draft;
cursorPosition = threadDraft.cursorPosition;
action = {
type: ViewTypes.COMMENT_DRAFT_CHANGED,
rootId: threadId
};
} else if (state.views.channel.drafts[channelId]) {
const channelDraft = state.views.channel.drafts[channelId];
draft = channelDraft.draft;
cursorPosition = channelDraft.cursorPosition;
action = {
type: ViewTypes.POST_DRAFT_CHANGED,
channelId
};
}
let nextDraft = `${value}`;
if (cursorPosition > 0) {
const beginning = draft.slice(0, cursorPosition);
const end = draft.slice(cursorPosition);
nextDraft = `${beginning}${value}${end}`;
}
if (action && nextDraft !== draft) {
dispatch({
...action,
draft: nextDraft
});
}
};
}
export function toggleDMChannel(otherUserId, visible) {
return async (dispatch, getState) => {
const state = getState();
@@ -379,10 +312,13 @@ export function toggleGMChannel(channelId, visible) {
export function closeDMChannel(channel) {
return async (dispatch, getState) => {
const state = getState();
const currentChannelId = getCurrentChannelId(state);
if (channel.isFavorite) {
unfavoriteChannel(channel.id)(dispatch, getState);
}
toggleDMChannel(channel.teammate_id, 'false')(dispatch, getState);
if (channel.id === currentChannelId) {
if (channel.isCurrent) {
selectInitialChannel(state.entities.teams.currentTeamId)(dispatch, getState);
}
};
@@ -391,21 +327,21 @@ export function closeDMChannel(channel) {
export function closeGMChannel(channel) {
return async (dispatch, getState) => {
const state = getState();
const currentChannelId = getCurrentChannelId(state);
if (channel.isFavorite) {
unfavoriteChannel(channel.id)(dispatch, getState);
}
toggleGMChannel(channel.id, 'false')(dispatch, getState);
if (channel.id === currentChannelId) {
if (channel.isCurrent) {
selectInitialChannel(state.entities.teams.currentTeamId)(dispatch, getState);
}
};
}
export function refreshChannelWithRetry(channelId) {
return async (dispatch, getState) => {
dispatch(setChannelRefreshing(true));
const posts = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
dispatch(setChannelRefreshing(false));
return posts;
return (dispatch, getState) => {
return retryGetPostsAction(getPosts(channelId), dispatch, getState);
};
}
@@ -433,10 +369,10 @@ export function setChannelRefreshing(loading = true) {
};
}
export function setChannelRetryFailed(failed = true) {
export function setPostTooltipVisible(visible = true) {
return {
type: ViewTypes.SET_CHANNEL_RETRY_FAILED,
failed
type: ViewTypes.POST_TOOLTIP_VISIBLE,
visible
};
}
@@ -455,42 +391,26 @@ export function increasePostVisibility(channelId, focusedPostId) {
const currentPostVisibility = postVisibility[channelId] || 0;
if (loadingPosts[channelId]) {
return;
return true;
}
// Check if we already have the posts that we want to show
if (!focusedPostId) {
const loadedPostCount = state.entities.posts.postsInChannel[channelId].length;
const desiredPostVisibility = currentPostVisibility + ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
if (loadedPostCount >= desiredPostVisibility) {
// We already have the posts, so we just need to show them
dispatch({
type: ViewTypes.INCREASE_POST_VISIBILITY,
data: channelId,
amount: ViewTypes.POST_VISIBILITY_CHUNK_SIZE
});
return;
dispatch(batchActions([
{
type: ViewTypes.LOADING_POSTS,
data: true,
channelId
}
}
dispatch({
type: ViewTypes.LOADING_POSTS,
data: true,
channelId
});
]));
const page = Math.floor(currentPostVisibility / ViewTypes.POST_VISIBILITY_CHUNK_SIZE);
let result;
let posts;
if (focusedPostId) {
result = await getPostsBefore(channelId, focusedPostId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
posts = await getPostsBefore(channelId, focusedPostId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
} else {
result = await getPosts(channelId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
posts = await getPosts(channelId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
}
const posts = result.data;
if (posts) {
// make sure to increment the posts visibility
// only if we got results
@@ -506,5 +426,7 @@ export function increasePostVisibility(channelId, focusedPostId) {
data: false,
channelId
});
return posts && posts.order.length >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
};
}

View File

@@ -8,9 +8,9 @@ export function handleAddChannelMembers(channelId, members) {
try {
const requests = members.map((m) => dispatch(addChannelMember(channelId, m, getState)));
return await Promise.all(requests);
await Promise.all(requests);
} catch (error) {
return error;
// should be handled by global error handling
}
};
}

View File

@@ -8,9 +8,9 @@ export function handleRemoveChannelMembers(channelId, members) {
try {
const requests = members.map((m) => dispatch(removeChannelMember(channelId, m, getState)));
return await Promise.all(requests);
await Promise.all(requests);
} catch (error) {
return error;
// should be handled by global error handling
}
};
}

View File

@@ -1,10 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
export function setLastUpgradeCheck() {
return {
type: ViewTypes.SET_LAST_UPGRADE_CHECK
};
}

View File

@@ -1,32 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {executeCommand as executeCommandService} from 'mattermost-redux/actions/integrations';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
export function executeCommand(message, channelId, rootId) {
return async (dispatch, getState) => {
const state = getState();
const teamId = getCurrentTeamId(state);
const args = {
channel_id: channelId,
team_id: teamId,
root_id: rootId,
parent_id: rootId
};
let msg = message;
let cmdLength = msg.indexOf(' ');
if (cmdLength < 0) {
cmdLength = msg.length;
}
const cmd = msg.substring(0, cmdLength).toLowerCase();
msg = cmd + msg.substring(cmdLength, msg.length);
return await executeCommandService(msg, args)(dispatch, getState);
};
}

View File

@@ -12,7 +12,7 @@ export function handleCreateChannel(displayName, purpose, header, type) {
const state = getState();
const currentUserId = getCurrentUserId(state);
const teamId = getCurrentTeamId(state);
const channel = {
let channel = {
team_id: teamId,
name: cleanUpUrlable(displayName),
display_name: displayName,
@@ -21,10 +21,10 @@ export function handleCreateChannel(displayName, purpose, header, type) {
type
};
const {data} = await createChannel(channel, currentUserId)(dispatch, getState);
if (data && data.id) {
channel = await createChannel(channel, currentUserId)(dispatch, getState);
if (channel && channel.id) {
dispatch(setChannelDisplayName(displayName));
handleSelectChannel(data.id)(dispatch, getState);
handleSelectChannel(channel.id)(dispatch, getState);
}
};
}

View File

@@ -1,34 +1,17 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {addReaction as serviceAddReaction} from 'mattermost-redux/actions/posts';
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
import {addReaction} from 'mattermost-redux/actions/posts';
import {getPostsInCurrentChannel, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
import {ViewTypes} from 'app/constants';
const getPostIdsForThread = makeGetPostIdsForThread();
export function addReaction(postId, emoji) {
return (dispatch) => {
dispatch(serviceAddReaction(postId, emoji));
dispatch(addRecentEmoji(emoji));
};
}
const getPostsForThread = makeGetPostsForThread();
export function addReactionToLatestPost(emoji, rootId) {
return async (dispatch, getState) => {
const state = getState();
const postIds = rootId ? getPostIdsForThread(state, rootId) : getPostIdsInCurrentChannel(state);
const lastPostId = postIds[0];
const posts = rootId ? getPostsForThread(state, {rootId}) : getPostsInCurrentChannel(state);
const lastPost = posts[0];
dispatch(serviceAddReaction(lastPostId, emoji));
dispatch(addRecentEmoji(emoji));
};
}
export function addRecentEmoji(emoji) {
return {
type: ViewTypes.ADD_RECENT_EMOJI,
emoji
dispatch(addReaction(lastPost.id, emoji));
};
}

View File

@@ -16,32 +16,23 @@ export function handleUploadFiles(files, rootId) {
const channelId = state.entities.channels.currentChannelId;
const formData = new FormData();
const clientIds = [];
const re = /heic/i;
files.forEach((file) => {
let name = file.fileName;
let mimeType = lookupMimeType(name);
let extension = name.split('.').pop().replace('.', '');
const uri = file.uri;
const mimeType = lookupMimeType(file.fileName);
const extension = file.fileName.split('.').pop().replace('.', '');
const clientId = generateId();
if (re.test(extension)) {
extension = 'JPG';
name = name.replace(re, 'jpg');
mimeType = 'image/jpeg';
}
clientIds.push({
clientId,
localPath: uri,
name,
localPath: file.uri,
name: file.fileName,
type: mimeType,
extension
});
const fileData = {
uri,
name,
uri: file.uri,
name: file.fileName,
type: mimeType,
extension
};
@@ -108,14 +99,6 @@ export function handleClearFiles(channelId, rootId) {
};
}
export function handleClearFailedFiles(channelId, rootId) {
return {
type: ViewTypes.CLEAR_FAILED_FILES_FOR_POST_DRAFT,
channelId,
rootId
};
}
export function handleRemoveFile(clientId, channelId, rootId) {
return {
type: ViewTypes.REMOVE_FILE_FROM_POST_DRAFT,

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
export function initialize() {
return async (dispatch, getState) => {
setTimeout(() => {
dispatch({
type: ViewTypes.APPLICATION_INITIALIZED
}, getState);
}, 400);
};
}

View File

@@ -1,78 +1,70 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {GeneralTypes, PostTypes} from 'mattermost-redux/action_types';
import {Client4} from 'mattermost-redux/client';
import {General} from 'mattermost-redux/constants';
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'mattermost-redux/actions/general';
import {getPosts} from 'mattermost-redux/actions/posts';
import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams';
import {recordTime} from 'app/utils/segment';
import {ViewTypes} from 'app/constants';
import {
handleSelectChannel,
setChannelDisplayName,
retryGetPostsAction
setChannelDisplayName
} from 'app/actions/views/channel';
import {handleTeamChange, selectFirstAvailableTeam} from 'app/actions/views/select_team';
import {General} from 'mattermost-redux/constants';
import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
import {getChannelAndMyMember, markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
export function loadConfigAndLicense() {
return async (dispatch, getState) => {
const [configData, licenseData] = await Promise.all([
const [config, license] = await Promise.all([
getClientConfig()(dispatch, getState),
getLicenseConfig()(dispatch, getState)
]);
const config = configData.data || {};
const license = licenseData.data || {};
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
license.IsLicensed === 'true' && license.DataRetention === 'true') {
getDataRetentionPolicy()(dispatch, getState);
} else {
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
}
return {config, license};
};
}
export function loadFromPushNotification(notification) {
export function queueNotification(notification) {
return async (dispatch, getState) => {
dispatch({type: ViewTypes.NOTIFICATION_CHANGED, data: notification}, getState);
};
}
export function clearNotification() {
return async (dispatch, getState) => {
dispatch({type: ViewTypes.NOTIFICATION_CHANGED, data: null}, getState);
};
}
export function goToNotification(notification) {
return async (dispatch, getState) => {
const state = getState();
const {data} = notification;
const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams;
const {currentChannelId} = state.entities.channels;
const {currentTeamId, teams} = state.entities.teams;
const {channels, currentChannelId, myMembers} = state.entities.channels;
const channelId = data.channel_id;
// when the notification does not have a team id is because its from a DM or GM
// if the notification does not have a team id is because its from a DM or GM
const teamId = data.team_id || currentTeamId;
//verify that we have the team loaded
if (teamId && (!teams[teamId] || !myTeamMembers[teamId])) {
await Promise.all([
getMyTeams()(dispatch, getState),
getMyTeamMembers()(dispatch, getState)
]);
dispatch(setChannelDisplayName(''));
if (teamId && teamId !== currentTeamId) {
handleTeamChange(teams[teamId], false)(dispatch, getState);
} else if (!teamId) {
await selectFirstAvailableTeam()(dispatch, getState);
}
// when the notification is from a team other than the current team
if (teamId !== currentTeamId) {
selectTeam({id: teamId})(dispatch, getState);
if (!channels[channelId] || !myMembers[channelId]) {
getChannelAndMyMember(channelId)(dispatch, getState);
}
// when the notification is from the same channel as the current channel
// we should get the posts
if (channelId === currentChannelId) {
markChannelAsRead(channelId, null, false)(dispatch, getState);
await retryGetPostsAction(getPosts(channelId), dispatch, getState);
} else {
// when the notification is from a channel other than the current channel
markChannelAsRead(channelId, currentChannelId, false)(dispatch, getState);
dispatch(setChannelDisplayName(''));
if (channelId !== currentChannelId) {
handleSelectChannel(channelId)(dispatch, getState);
}
viewChannel(channelId)(dispatch, getState);
markChannelAsRead(channelId, currentChannelId)(dispatch, getState);
};
}
@@ -80,51 +72,10 @@ export function purgeOfflineStore() {
return {type: General.OFFLINE_STORE_PURGE};
}
export function createPost(post) {
return async (dispatch, getState) => {
const state = getState();
const currentUserId = state.entities.users.currentUserId;
const timestamp = Date.now();
const pendingPostId = post.pending_post_id || `${currentUserId}:${timestamp}`;
const newPost = {
...post,
pending_post_id: pendingPostId,
create_at: timestamp,
update_at: timestamp
};
try {
const payload = Client4.createPost({...newPost, create_at: 0});
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[payload.id]: payload
}
},
channelId: payload.channel_id
});
} catch (error) {
return {error};
}
return {data: true};
};
}
export function recordLoadTime(screenName, category) {
return async (dispatch, getState) => {
const {currentUserId} = getState().entities.users;
recordTime(screenName, category, currentUserId);
};
}
export default {
loadConfigAndLicense,
loadFromPushNotification,
queueNotification,
clearNotification,
goToNotification,
purgeOfflineStore
};

View File

@@ -3,7 +3,7 @@
import {batchActions} from 'redux-batched-actions';
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
@@ -12,26 +12,25 @@ import {NavigationTypes} from 'app/constants';
import {setChannelDisplayName} from './channel';
export function handleTeamChange(teamId, selectChannel = true) {
export function handleTeamChange(team, selectChannel = true) {
return async (dispatch, getState) => {
const state = getState();
const {currentTeamId} = state.entities.teams;
if (currentTeamId === teamId) {
if (currentTeamId === team.id) {
return;
}
const actions = [
setChannelDisplayName(''),
{type: TeamTypes.SELECT_TEAM, data: teamId}
{type: TeamTypes.SELECT_TEAM, data: team.id}
];
if (selectChannel) {
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''});
const lastChannels = state.views.team.lastChannelForTeam[teamId] || [];
const lastChannelId = lastChannels[0] || '';
const lastChannelId = state.views.team.lastChannelForTeam[team.id] || '';
const currentChannelId = getCurrentChannelId(state);
markChannelAsViewed(currentChannelId)(dispatch, getState);
viewChannel(lastChannelId, currentChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState);
}
@@ -46,7 +45,7 @@ export function selectFirstAvailableTeam() {
const firstTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
if (firstTeam) {
handleTeamChange(firstTeam.id)(dispatch, getState);
handleTeamChange(firstTeam)(dispatch, getState);
} else {
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
}

View File

@@ -12,11 +12,3 @@ export function handleCommentDraftChanged(rootId, draft) {
}, getState);
};
}
export function handleCommentDraftSelectionChanged(rootId, cursorPosition) {
return {
type: ViewTypes.COMMENT_DRAFT_SELECTION_CHANGED,
rootId,
cursorPosition
};
}

View File

@@ -3,29 +3,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Clipboard, Text} from 'react-native';
import {intlShape} from 'react-intl';
import {Text} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import CustomPropTypes from 'app/constants/custom_prop_types';
import mattermostManaged from 'app/mattermost_managed';
export default class AtMention extends React.PureComponent {
class AtMention extends React.PureComponent {
static propTypes = {
intl: intlShape,
isSearchResult: PropTypes.bool,
mentionName: PropTypes.string.isRequired,
mentionStyle: CustomPropTypes.Style,
navigator: PropTypes.object.isRequired,
onLongPress: PropTypes.func.isRequired,
onPostPress: PropTypes.func,
textStyle: CustomPropTypes.Style,
theme: PropTypes.object.isRequired,
usersByUsername: PropTypes.object.isRequired
};
static contextTypes = {
intl: intlShape
}
constructor(props) {
super(props);
@@ -47,8 +42,7 @@ export default class AtMention extends React.PureComponent {
}
goToUserProfile = () => {
const {navigator, theme} = this.props;
const {intl} = this.context;
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'UserProfile',
@@ -71,7 +65,7 @@ export default class AtMention extends React.PureComponent {
let mentionName = props.mentionName;
while (mentionName.length > 0) {
if (props.usersByUsername.hasOwnProperty(mentionName)) {
if (props.usersByUsername[mentionName]) {
const user = props.usersByUsername[mentionName];
return {
username: user.username,
@@ -92,30 +86,6 @@ export default class AtMention extends React.PureComponent {
};
}
handleLongPress = async () => {
const {intl} = this.context;
const config = await mattermostManaged.getLocalConfig();
let action;
if (config.copyAndPasteProtection !== 'false') {
action = {
text: intl.formatMessage({
id: 'mobile.mention.copy_mention',
defaultMessage: 'Copy Mention'
}),
onPress: this.handleCopyMention
};
}
this.props.onLongPress(action);
}
handleCopyMention = () => {
const {username} = this.state;
Clipboard.setString(`@${username}`);
}
render() {
const {isSearchResult, mentionName, mentionStyle, onPostPress, textStyle} = this.props;
const username = this.state.username;
@@ -130,7 +100,6 @@ export default class AtMention extends React.PureComponent {
<Text
style={textStyle}
onPress={isSearchResult ? onPostPress : this.goToUserProfile}
onLongPress={this.handleLongPress}
>
<Text style={mentionStyle}>
{'@' + username}
@@ -140,3 +109,5 @@ export default class AtMention extends React.PureComponent {
);
}
}
export default injectIntl(AtMention);

View File

@@ -5,14 +5,15 @@ import {connect} from 'react-redux';
import {getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import AtMention from './at_mention';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
theme: getTheme(state),
usersByUsername: getUsersByUsername(state)
usersByUsername: getUsersByUsername(state),
...ownProps
};
}

View File

@@ -9,7 +9,6 @@ import {RequestStatus} from 'mattermost-redux/constants';
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
import AtMentionItem from 'app/components/autocomplete/at_mention_item';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -25,21 +24,19 @@ export default class AtMention extends PureComponent {
defaultChannel: PropTypes.object,
inChannel: PropTypes.array,
isSearch: PropTypes.bool,
listHeight: PropTypes.number,
matchTerm: PropTypes.string,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
outChannel: PropTypes.array,
postDraft: PropTypes.string,
requestStatus: PropTypes.string.isRequired,
teamMembers: PropTypes.array,
theme: PropTypes.object.isRequired,
value: PropTypes.string
theme: PropTypes.object.isRequired
};
static defaultProps = {
defaultChannel: {},
isSearch: false,
value: ''
postDraft: '',
isSearch: false
};
constructor(props) {
@@ -58,9 +55,6 @@ export default class AtMention extends PureComponent {
mentionComplete: false,
sections: []
});
this.props.onResultCountChange(0);
return;
} else if (matchTerm === null) {
// if the terms did not change but is null then we don't need to do anything
@@ -119,8 +113,6 @@ export default class AtMention extends PureComponent {
this.setState({
sections
});
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
}
}
@@ -152,8 +144,8 @@ export default class AtMention extends PureComponent {
};
completeMention = (mention) => {
const {cursorPosition, isSearch, onChangeText, value} = this.props;
const mentionPart = value.substring(0, cursorPosition);
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
const mentionPart = postDraft.substring(0, cursorPosition);
let completedDraft;
if (isSearch) {
@@ -162,8 +154,8 @@ export default class AtMention extends PureComponent {
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
}
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
}
onChangeText(completedDraft, true);
@@ -203,7 +195,7 @@ export default class AtMention extends PureComponent {
};
render() {
const {isSearch, listHeight, theme} = this.props;
const {isSearch, theme} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
@@ -218,11 +210,10 @@ export default class AtMention extends PureComponent {
<SectionList
keyboardShouldPersistTaps='always'
keyExtractor={this.keyExtractor}
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
style={[style.listView, isSearch ? style.search : null]}
sections={sections}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
ItemSeparatorComponent={AutocompleteDivider}
initialNumToRender={10}
/>
);
@@ -235,7 +226,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
backgroundColor: theme.centerChannelBg
},
search: {
minHeight: 125
height: 250
}
};
});

View File

@@ -14,15 +14,30 @@ import {
filterMembersInCurrentTeam,
getMatchTermForAtMention
} from 'app/selectors/autocomplete';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import AtMention from './at_mention';
function mapStateToProps(state, ownProps) {
const {cursorPosition, isSearch} = ownProps;
const {cursorPosition, isSearch, rootId} = ownProps;
const currentChannelId = getCurrentChannelId(state);
const value = ownProps.value.substring(0, cursorPosition);
let postDraft = '';
if (isSearch) {
postDraft = state.views.search;
} else if (ownProps.rootId) {
const threadDraft = state.views.thread.drafts[rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
} else if (currentChannelId) {
const channelDraft = state.views.channel.drafts[currentChannelId];
if (channelDraft) {
postDraft = channelDraft.draft;
}
}
const value = postDraft.substring(0, cursorPosition);
const matchTerm = getMatchTermForAtMention(value, isSearch);
let teamMembers;
@@ -39,12 +54,14 @@ function mapStateToProps(state, ownProps) {
currentChannelId,
currentTeamId: getCurrentTeamId(state),
defaultChannel: getDefaultChannel(state),
postDraft,
matchTerm,
teamMembers,
inChannel,
outChannel,
requestStatus: state.requests.users.autocompleteUsers.status,
theme: getTheme(state)
theme: getTheme(state),
...ownProps
};
}

View File

@@ -10,7 +10,7 @@ import {
} from 'react-native';
import ProfilePicture from 'app/components/profile_picture';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class AtMentionItem extends PureComponent {
static propTypes = {
@@ -60,14 +60,19 @@ export default class AtMentionItem extends PureComponent {
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowPicture: {
marginHorizontal: 8,

View File

@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import AtMentionItem from './at_mention_item';
@@ -16,7 +16,8 @@ function mapStateToProps(state, ownProps) {
firstName: user.first_name,
lastName: user.last_name,
username: user.username,
theme: getTheme(state)
theme: getTheme(state),
...ownProps
};
}

View File

@@ -1,180 +0,0 @@
// 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 {
Keyboard,
Platform,
View
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import AtMention from './at_mention';
import ChannelMention from './channel_mention';
import EmojiSuggestion from './emoji_suggestion';
import SlashSuggestion from './slash_suggestion';
export default class Autocomplete extends PureComponent {
static propTypes = {
deviceHeight: PropTypes.number,
onChangeText: PropTypes.func.isRequired,
rootId: PropTypes.string,
isSearch: PropTypes.bool,
theme: PropTypes.object.isRequired,
value: PropTypes.string
};
static defaultProps = {
isSearch: false
};
state = {
cursorPosition: 0,
atMentionCount: 0,
channelMentionCount: 0,
emojiCount: 0,
commandCount: 0,
keyboardOffset: 0
};
handleSelectionChange = (event) => {
this.setState({
cursorPosition: event.nativeEvent.selection.end
});
};
handleAtMentionCountChange = (atMentionCount) => {
this.setState({atMentionCount});
};
handleChannelMentionCountChange = (channelMentionCount) => {
this.setState({channelMentionCount});
};
handleEmojiCountChange = (emojiCount) => {
this.setState({emojiCount});
};
handleCommandCountChange = (commandCount) => {
this.setState({commandCount});
};
componentWillMount() {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide);
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
keyboardDidShow = (e) => {
const {height} = e.endCoordinates;
this.setState({keyboardOffset: height});
}
keyboardDidHide = () => {
this.setState({keyboardOffset: 0});
}
listHeight() {
let offset = Platform.select({ios: 65, android: 75});
if (DeviceInfo.getModel() === 'iPhone X') {
offset = 90;
}
return this.props.deviceHeight - offset - this.state.keyboardOffset;
}
render() {
const style = getStyleFromTheme(this.props.theme);
const wrapperStyle = [];
const containerStyle = [];
if (this.props.isSearch) {
wrapperStyle.push(style.base, style.searchContainer);
containerStyle.push(style.content);
} else {
containerStyle.push(style.base, style.container);
}
// We always need to render something, but we only draw the borders when we have results to show
const {atMentionCount, channelMentionCount, emojiCount, commandCount} = this.state;
if (atMentionCount + channelMentionCount + emojiCount + commandCount > 0) {
if (this.props.isSearch) {
wrapperStyle.push(style.bordersSearch);
} else {
containerStyle.push(style.borders);
}
}
const listHeight = this.listHeight();
return (
<View style={wrapperStyle}>
<View style={containerStyle}>
<AtMention
listHeight={listHeight}
cursorPosition={this.state.cursorPosition}
onResultCountChange={this.handleAtMentionCountChange}
{...this.props}
/>
<ChannelMention
listHeight={listHeight}
cursorPosition={this.state.cursorPosition}
onResultCountChange={this.handleChannelMentionCountChange}
{...this.props}
/>
<EmojiSuggestion
cursorPosition={this.state.cursorPosition}
onResultCountChange={this.handleEmojiCountChange}
{...this.props}
/>
<SlashSuggestion
onResultCountChange={this.handleCommandCountChange}
{...this.props}
/>
</View>
</View>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
base: {
left: 0,
overflow: 'hidden',
position: 'absolute',
right: 0
},
borders: {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderBottomWidth: 0
},
bordersSearch: {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2)
},
container: {
bottom: 0,
maxHeight: 200
},
content: {
flex: 1
},
searchContainer: {
flex: 1,
...Platform.select({
android: {
top: 46
},
ios: {
top: 44
}
})
}
};
});

View File

@@ -1,32 +0,0 @@
// 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 {View} from 'react-native';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class AutocompleteDivider extends PureComponent {
static propTypes = {
theme: PropTypes.object.isRequired
};
render() {
const {theme} = this.props;
const style = getStyleFromTheme(theme);
return (
<View style={style.divider}/>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
divider: {
height: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2)
}
};
});

View File

@@ -1,16 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import AutocompleteDivider from './autocomplete_divider';
function mapStateToProps(state) {
return {
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(AutocompleteDivider);

View File

@@ -40,7 +40,11 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
paddingLeft: 8,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2)
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
sectionText: {
fontSize: 12,

View File

@@ -8,7 +8,6 @@ import {SectionList} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import ChannelMentionItem from 'app/components/autocomplete/channel_mention_item';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -21,22 +20,20 @@ export default class ChannelMention extends PureComponent {
currentTeamId: PropTypes.string.isRequired,
cursorPosition: PropTypes.number.isRequired,
isSearch: PropTypes.bool,
listHeight: PropTypes.number,
matchTerm: PropTypes.string,
myChannels: PropTypes.array,
otherChannels: PropTypes.array,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
postDraft: PropTypes.string,
privateChannels: PropTypes.array,
publicChannels: PropTypes.array,
requestStatus: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
value: PropTypes.string
theme: PropTypes.object.isRequired
};
static defaultProps = {
isSearch: false,
value: ''
postDraft: '',
isSearch: false
};
constructor(props) {
@@ -56,9 +53,6 @@ export default class ChannelMention extends PureComponent {
mentionComplete: false,
sections: []
});
this.props.onResultCountChange(0);
return;
} else if (matchTerm === null) {
// if the terms did not change but is null then we don't need to do anything
@@ -118,14 +112,12 @@ export default class ChannelMention extends PureComponent {
this.setState({
sections
});
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
}
}
completeMention = (mention) => {
const {cursorPosition, isSearch, onChangeText, value} = this.props;
const mentionPart = value.substring(0, cursorPosition);
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
const mentionPart = postDraft.substring(0, cursorPosition);
let completedDraft;
if (isSearch) {
@@ -135,8 +127,8 @@ export default class ChannelMention extends PureComponent {
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
}
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
}
onChangeText(completedDraft, true);
@@ -167,7 +159,7 @@ export default class ChannelMention extends PureComponent {
};
render() {
const {isSearch, listHeight, theme} = this.props;
const {isSearch, theme} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
@@ -182,11 +174,10 @@ export default class ChannelMention extends PureComponent {
<SectionList
keyboardShouldPersistTaps='always'
keyExtractor={this.keyExtractor}
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
style={[style.listView, isSearch ? style.search : null]}
sections={sections}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
ItemSeparatorComponent={AutocompleteDivider}
initialNumToRender={10}
/>
);
@@ -199,7 +190,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
backgroundColor: theme.centerChannelBg
},
search: {
minHeight: 125
height: 250
}
};
});

View File

@@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {searchChannels} from 'mattermost-redux/actions/channels';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
@@ -14,14 +15,30 @@ import {
filterPrivateChannels,
getMatchTermForChannelMention
} from 'app/selectors/autocomplete';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelMention from './channel_mention';
function mapStateToProps(state, ownProps) {
const {cursorPosition, isSearch} = ownProps;
const {cursorPosition, isSearch, rootId} = ownProps;
const currentChannelId = getCurrentChannelId(state);
const value = ownProps.value.substring(0, cursorPosition);
let postDraft = '';
if (isSearch) {
postDraft = state.views.search;
} else if (rootId) {
const threadDraft = state.views.thread.drafts[rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
} else if (currentChannelId) {
const channelDraft = state.views.channel.drafts[currentChannelId];
if (channelDraft) {
postDraft = channelDraft.draft;
}
}
const value = postDraft.substring(0, cursorPosition);
const matchTerm = getMatchTermForChannelMention(value, isSearch);
let myChannels;
@@ -37,12 +54,14 @@ function mapStateToProps(state, ownProps) {
}
return {
...ownProps,
myChannels,
otherChannels,
publicChannels,
privateChannels,
currentTeamId: getCurrentTeamId(state),
matchTerm,
postDraft,
requestStatus: state.requests.channels.getChannels.status,
theme: getTheme(state)
};

View File

@@ -8,7 +8,7 @@ import {
TouchableOpacity
} from 'react-native';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class ChannelMentionItem extends PureComponent {
static propTypes = {
@@ -46,14 +46,19 @@ export default class ChannelMentionItem extends PureComponent {
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
padding: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowDisplayName: {
fontSize: 13,

View File

@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelMentionItem from './channel_mention_item';
@@ -15,7 +15,8 @@ function mapStateToProps(state, ownProps) {
return {
displayName: channel.display_name,
name: channel.name,
theme: getTheme(state)
theme: getTheme(state),
...ownProps
};
}

View File

@@ -10,12 +10,10 @@ import {
View
} from 'react-native';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import Emoji from 'app/components/emoji';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
const EMOJI_REGEX = /(^|\s|^\+|^-)(:([^:\s]*))$/i;
const EMOJI_REGEX_WITHOUT_PREFIX = /\B(:([^:\s]*))$/i;
export default class EmojiSuggestion extends Component {
static propTypes = {
@@ -24,31 +22,25 @@ export default class EmojiSuggestion extends Component {
}).isRequired,
cursorPosition: PropTypes.number,
emojis: PropTypes.array.isRequired,
isSearch: PropTypes.bool,
postDraft: PropTypes.string,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
rootId: PropTypes.string,
value: PropTypes.string
rootId: PropTypes.string
};
static defaultProps = {
defaultChannel: {},
value: ''
postDraft: ''
};
state = {
active: false,
dataSource: []
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.isSearch) {
return;
}
const regex = EMOJI_REGEX;
const match = nextProps.value.substring(0, nextProps.cursorPosition).match(regex);
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
if (!match || this.state.emojiComplete) {
this.setState({
@@ -56,9 +48,6 @@ export default class EmojiSuggestion extends Component {
matchTerm: null,
emojiComplete: false
});
this.props.onResultCountChange(0);
return;
}
@@ -71,17 +60,7 @@ export default class EmojiSuggestion extends Component {
let data = [];
if (matchTerm.length) {
const lowerCaseMatchTerm = matchTerm.toLowerCase();
const startsWith = [];
const includes = [];
nextProps.emojis.forEach((emoji) => {
if (emoji.startsWith(lowerCaseMatchTerm)) {
startsWith.push(emoji);
} else {
includes.push(emoji);
}
});
data = [...startsWith.sort(), ...includes.sort()];
data = nextProps.emojis.filter((emoji) => emoji.startsWith(matchTerm.toLowerCase())).sort();
} else {
const initialEmojis = [...nextProps.emojis];
initialEmojis.splice(0, 300);
@@ -89,25 +68,23 @@ export default class EmojiSuggestion extends Component {
}
this.setState({
active: data.length > 0,
active: data.length,
dataSource: data
});
this.props.onResultCountChange(data.length);
}
completeSuggestion = (emoji) => {
const {actions, cursorPosition, onChangeText, value, rootId} = this.props;
const emojiPart = value.substring(0, cursorPosition);
const {actions, cursorPosition, onChangeText, postDraft, rootId} = this.props;
const emojiPart = postDraft.substring(0, cursorPosition);
if (emojiPart.startsWith('+:')) {
actions.addReactionToLatestPost(emoji, rootId);
onChangeText('');
} else {
let completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `:${emoji}: `);
let completedDraft = emojiPart.replace(EMOJI_REGEX, `:${emoji}: `);
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
}
onChangeText(completedDraft);
@@ -159,7 +136,6 @@ export default class EmojiSuggestion extends Component {
data={this.state.dataSource}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
ItemSeparatorComponent={AutocompleteDivider}
pageSize={10}
initialListSize={10}
/>
@@ -185,7 +161,13 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
backgroundColor: theme.centerChannelBg
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
}
};
});

View File

@@ -8,7 +8,7 @@ import {bindActionCreators} from 'redux';
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
import {addReactionToLatestPost} from 'app/actions/views/emoji';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {EmojiIndicesByAlias} from 'app/utils/emojis';
import EmojiSuggestion from './emoji_suggestion';
@@ -25,11 +25,26 @@ const getEmojisByName = createSelector(
}
);
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {currentChannelId} = state.entities.channels;
const emojis = getEmojisByName(state);
let postDraft;
if (ownProps.rootId) {
const threadDraft = state.views.thread.drafts[ownProps.rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
} else if (currentChannelId) {
const channelDraft = state.views.channel.drafts[currentChannelId];
if (channelDraft) {
postDraft = channelDraft.draft;
}
}
return {
emojis,
postDraft,
theme: getTheme(state)
};
}

View File

@@ -1,20 +1,88 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
StyleSheet,
View
} from 'react-native';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import AtMention from './at_mention';
import ChannelMention from './channel_mention';
import EmojiSuggestion from './emoji_suggestion';
import {getDimensions} from 'app/selectors/device';
import Autocomplete from './autocomplete';
function mapStateToProps(state) {
const {deviceHeight} = getDimensions(state);
return {
deviceHeight,
theme: getTheme(state)
export default class Autocomplete extends PureComponent {
static propTypes = {
onChangeText: PropTypes.func.isRequired,
rootId: PropTypes.string,
isSearch: PropTypes.bool
};
static defaultProps = {
isSearch: false
};
state = {
cursorPosition: 0
};
handleSelectionChange = (event) => {
this.setState({
cursorPosition: event.nativeEvent.selection.end
});
};
render() {
const searchContainer = this.props.isSearch ? style.searchContainer : null;
const container = this.props.isSearch ? null : style.container;
return (
<View style={searchContainer}>
<View style={container}>
<AtMention
cursorPosition={this.state.cursorPosition}
{...this.props}
/>
<ChannelMention
cursorPosition={this.state.cursorPosition}
{...this.props}
/>
<EmojiSuggestion
cursorPosition={this.state.cursorPosition}
{...this.props}
/>
</View>
</View>
);
}
}
export default connect(mapStateToProps, null, null, {withRef: true})(Autocomplete);
const style = StyleSheet.create({
container: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
maxHeight: 200,
overflow: 'hidden'
},
searchContainer: {
elevation: 5,
flex: 1,
left: 0,
maxHeight: 250,
overflow: 'hidden',
position: 'absolute',
right: 0,
zIndex: 5,
...Platform.select({
android: {
top: 47
},
ios: {
top: 64
}
})
}
});

View File

@@ -1,45 +0,0 @@
// 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 {createSelector} from 'reselect';
import {getAutocompleteCommands} from 'mattermost-redux/actions/integrations';
import {getAutocompleteCommandsList} from 'mattermost-redux/selectors/entities/integrations';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import SlashSuggestion from './slash_suggestion';
// TODO: Remove when all below commands have been implemented
const COMMANDS_TO_IMPLEMENT_LATER = ['collapse', 'expand', 'join', 'open', 'leave', 'logout', 'msg', 'grpmsg'];
const NON_MOBILE_COMMANDS = ['rename', 'invite_people', 'shortcuts', 'search', 'help', 'settings', 'remove'];
const COMMANDS_TO_HIDE_ON_MOBILE = [...COMMANDS_TO_IMPLEMENT_LATER, ...NON_MOBILE_COMMANDS];
const mobileCommandsSelector = createSelector(
getAutocompleteCommandsList,
(commands) => {
return commands.filter((command) => !COMMANDS_TO_HIDE_ON_MOBILE.includes(command.trigger));
}
);
function mapStateToProps(state) {
return {
commands: mobileCommandsSelector(state),
commandsRequest: state.requests.integrations.getAutocompleteCommands,
currentTeamId: getCurrentTeamId(state),
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getAutocompleteCommands
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(SlashSuggestion);

View File

@@ -1,167 +0,0 @@
// 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 {
FlatList
} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import SlashSuggestionItem from './slash_suggestion_item';
const SLASH_REGEX = /(^\/)([a-zA-Z-]*)$/;
const TIME_BEFORE_NEXT_COMMAND_REQUEST = 1000 * 60 * 5;
export default class SlashSuggestion extends Component {
static propTypes = {
actions: PropTypes.shape({
getAutocompleteCommands: PropTypes.func.isRequired
}).isRequired,
currentTeamId: PropTypes.string.isRequired,
commands: PropTypes.array,
commandsRequest: PropTypes.object.isRequired,
isSearch: PropTypes.bool,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
value: PropTypes.string
};
static defaultProps = {
defaultChannel: {},
value: ''
};
state = {
active: false,
suggestionComplete: false,
dataSource: [],
lastCommandRequest: 0
};
componentWillReceiveProps(nextProps) {
if (nextProps.isSearch) {
return;
}
const {currentTeamId} = this.props;
const {
commands: nextCommands,
commandsRequest: nextCommandsRequest,
currentTeamId: nextTeamId,
value: nextValue
} = nextProps;
if (currentTeamId !== nextTeamId) {
this.setState({
lastCommandRequest: 0
});
}
const match = nextValue.match(SLASH_REGEX);
if (!match || this.state.suggestionComplete) {
this.setState({
active: false,
matchTerm: null,
suggestionComplete: false
});
this.props.onResultCountChange(0);
return;
}
const dataIsStale = Date.now() - this.state.lastCommandRequest > TIME_BEFORE_NEXT_COMMAND_REQUEST;
if ((!nextCommands.length || dataIsStale) && nextCommandsRequest.status !== RequestStatus.STARTED) {
this.props.actions.getAutocompleteCommands(nextProps.currentTeamId);
this.setState({
lastCommandRequest: Date.now()
});
}
const matchTerm = match[2];
const data = this.filterSlashSuggestions(matchTerm, nextCommands);
this.setState({
active: data.length,
dataSource: data
});
this.props.onResultCountChange(data.length);
}
filterSlashSuggestions = (matchTerm, commands) => {
return commands.filter((command) => {
if (!command.auto_complete) {
return false;
} else if (!matchTerm) {
return true;
}
return command.display_name.startsWith(matchTerm) || command.trigger.startsWith(matchTerm);
});
}
completeSuggestion = (command) => {
const {onChangeText} = this.props;
const completedDraft = `/${command} `;
onChangeText(completedDraft);
this.setState({
active: false,
suggestionComplete: true
});
};
keyExtractor = (item) => item.id || item.trigger;
renderItem = ({item}) => (
<SlashSuggestionItem
displayName={item.display_name}
description={item.auto_complete_desc}
hint={item.auto_complete_hint}
onPress={this.completeSuggestion}
theme={this.props.theme}
trigger={item.trigger}
/>
)
render() {
if (!this.state.active) {
// If we are not in an active state return null so nothing is rendered
// other components are not blocked.
return null;
}
const style = getStyleFromTheme(this.props.theme);
return (
<FlatList
keyboardShouldPersistTaps='always'
style={style.listView}
extraData={this.state}
data={this.state.dataSource}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
pageSize={10}
initialListSize={10}
/>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg
}
};
});

View File

@@ -1,83 +0,0 @@
// 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 {
Text,
TouchableOpacity
} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class SlashSuggestionItem extends PureComponent {
static propTypes = {
displayName: PropTypes.string,
description: PropTypes.string,
hint: PropTypes.string,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
trigger: PropTypes.string
};
completeSuggestion = () => {
const {onPress, trigger} = this.props;
onPress(trigger);
};
render() {
const {
displayName,
description,
hint,
theme,
trigger
} = this.props;
const style = getStyleFromTheme(theme);
return (
<TouchableOpacity
onPress={this.completeSuggestion}
style={style.row}
>
<Text style={style.suggestionName}>{`/${displayName || trigger} ${hint}`}</Text>
<Text style={style.suggestionDescription}>{description}</Text>
</TouchableOpacity>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
height: 55,
justifyContent: 'center',
paddingHorizontal: 8,
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowDisplayName: {
fontSize: 13,
color: theme.centerChannelColor
},
rowName: {
color: theme.centerChannelColor,
opacity: 0.6
},
suggestionDescription: {
fontSize: 11,
color: changeOpacity(theme.centerChannelColor, 0.6)
},
suggestionName: {
fontSize: 13,
color: theme.centerChannelColor,
marginBottom: 5
}
};
});

View File

@@ -70,7 +70,13 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowPicture: {
marginHorizontal: 8,

View File

@@ -32,6 +32,7 @@ export default class Badge extends PureComponent {
constructor(props) {
super(props);
this.width = 0;
this.mounted = false;
}
@@ -66,8 +67,6 @@ export default class Badge extends PureComponent {
};
onLayout = (e) => {
const height = Math.max(e.nativeEvent.layout.height, this.props.minHeight);
const borderRadius = height / 2;
let width;
if (e.nativeEvent.layout.width <= e.nativeEvent.layout.height) {
@@ -76,14 +75,26 @@ export default class Badge extends PureComponent {
width = e.nativeEvent.layout.width + this.props.extraPaddingHorizontal;
}
width = Math.max(width, this.props.minWidth);
if (this.width === width) {
return;
}
this.width = width;
const height = Math.max(e.nativeEvent.layout.height, this.props.minHeight);
const borderRadius = height / 2;
this.setNativeProps({
style: {
width,
height,
borderRadius,
opacity: 1
borderRadius
}
});
setTimeout(() => {
this.setNativeProps({
style: {
opacity: 1
}
});
}, 100);
};
renderText = () => {
@@ -107,10 +118,6 @@ export default class Badge extends PureComponent {
};
render() {
if (!this.props.count) {
return null;
}
return (
<TouchableWithoutFeedback
{...this.panResponder.panHandlers}

View File

@@ -1,21 +1,18 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
BackHandler,
InteractionManager,
Keyboard,
Platform,
StyleSheet,
View
} from 'react-native';
import SafeAreaView from 'app/components/safe_area_view';
import Drawer from 'app/components/drawer';
import {alertErrorWithFallback} from 'app/utils/general';
import tracker from 'app/utils/time_tracker';
import ChannelsList from './channels_list';
import DrawerSwiper from './drawer_swipper';
@@ -27,12 +24,12 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
const DRAWER_INITIAL_OFFSET = 40;
const DRAWER_LANDSCAPE_OFFSET = 150;
export default class ChannelDrawer extends Component {
export default class ChannelDrawer extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getTeams: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
markChannelAsViewed: PropTypes.func.isRequired,
viewChannel: PropTypes.func.isRequired,
makeDirectChannel: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
@@ -40,6 +37,7 @@ export default class ChannelDrawer extends Component {
}).isRequired,
blurPostTextBox: PropTypes.func.isRequired,
children: PropTypes.node,
currentChannelId: PropTypes.string.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
isLandscape: PropTypes.bool.isRequired,
@@ -62,6 +60,7 @@ export default class ChannelDrawer extends Component {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.state = {
openDrawer: false,
openDrawerOffset
};
}
@@ -75,14 +74,15 @@ export default class ChannelDrawer extends Component {
EventEmitter.on('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
this.mounted = true;
}
componentWillReceiveProps(nextProps) {
const {isLandscape} = this.props;
if (nextProps.isLandscape !== isLandscape) {
const {isLandscape, isTablet} = this.props;
if (nextProps.isLandscape !== isLandscape || nextProps.isTablet || isTablet) {
if (this.state.openDrawerOffset !== 0) {
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (nextProps.isLandscape || this.props.isTablet) {
if (nextProps.isLandscape || nextProps.isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.setState({openDrawerOffset});
@@ -90,29 +90,17 @@ export default class ChannelDrawer extends Component {
}
}
shouldComponentUpdate(nextProps, nextState) {
const {currentTeamId, isLandscape, teamsCount} = this.props;
const {openDrawerOffset} = this.state;
if (nextState.openDrawerOffset !== openDrawerOffset) {
return true;
}
return nextProps.currentTeamId !== currentTeamId ||
nextProps.isLandscape !== isLandscape ||
nextProps.teamsCount !== teamsCount;
}
componentWillUnmount() {
EventEmitter.off('open_channel_drawer', this.openChannelDrawer);
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
this.mounted = false;
}
handleAndroidBack = () => {
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
if (this.state.openDrawer) {
this.setState({openDrawer: false});
return true;
}
@@ -120,8 +108,8 @@ export default class ChannelDrawer extends Component {
};
closeChannelDrawer = () => {
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
if (this.mounted) {
this.setState({openDrawer: false});
}
};
@@ -136,6 +124,13 @@ export default class ChannelDrawer extends Component {
InteractionManager.clearInteractionHandle(this.closeLeftHandle);
this.closeLeftHandle = null;
}
if (this.state.openDrawer && this.mounted) {
// The state doesn't get updated if you swipe to close
this.setState({
openDrawer: false
});
}
};
handleDrawerCloseStart = () => {
@@ -159,6 +154,13 @@ export default class ChannelDrawer extends Component {
if (!this.openLeftHandle) {
this.openLeftHandle = InteractionManager.createInteractionHandle();
}
if (!this.state.openDrawer && this.mounted) {
// The state doesn't get updated if you swipe to open
this.setState({
openDrawer: true
});
}
};
handleDrawerTween = (ratio) => {
@@ -190,14 +192,17 @@ export default class ChannelDrawer extends Component {
openChannelDrawer = () => {
this.props.blurPostTextBox();
if (this.refs.drawer && !this.refs.drawer.isOpened()) {
this.refs.drawer.open();
if (this.mounted) {
this.setState({
openDrawer: true
});
}
};
selectChannel = (channel, currentChannelId) => {
selectChannel = (channel) => {
const {
actions
actions,
currentChannelId
} = this.props;
const {
@@ -205,28 +210,24 @@ export default class ChannelDrawer extends Component {
markChannelAsRead,
setChannelLoading,
setChannelDisplayName,
markChannelAsViewed
viewChannel
} = actions;
tracker.channelSwitch = Date.now();
setChannelLoading(channel.id !== currentChannelId);
setChannelLoading();
setChannelDisplayName(channel.display_name);
this.closeChannelDrawer();
InteractionManager.runAfterInteractions(() => {
handleSelectChannel(channel.id);
requestAnimationFrame(() => {
// mark the channel as viewed after all the frame has flushed
markChannelAsRead(channel.id, currentChannelId);
if (channel.id !== currentChannelId) {
markChannelAsViewed(currentChannelId);
}
});
markChannelAsRead(channel.id, currentChannelId);
if (channel.id !== currentChannelId) {
viewChannel(currentChannelId);
}
});
};
joinChannel = async (channel, currentChannelId) => {
joinChannel = async (channel) => {
const {
actions,
currentTeamId,
@@ -268,7 +269,7 @@ export default class ChannelDrawer extends Component {
return;
}
this.selectChannel(result.data, currentChannelId);
this.selectChannel(result.data);
};
onPageSelected = (index) => {
@@ -278,11 +279,7 @@ export default class ChannelDrawer extends Component {
onSearchEnds = () => {
//hack to update the drawer when the offset changes
const {isLandscape, isTablet} = this.props;
if (this.refs.drawer) {
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
}
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (isLandscape || isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
@@ -291,21 +288,18 @@ export default class ChannelDrawer extends Component {
};
onSearchStart = () => {
if (this.refs.drawer) {
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
}
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
this.setState({openDrawerOffset: 0});
};
showTeams = () => {
if (this.drawerSwiper && this.swiperIndex === 1 && this.props.teamsCount > 1) {
if (this.swiperIndex === 1 && this.props.teamsCount > 1) {
this.drawerSwiper.getWrappedInstance().showTeamsPage();
}
};
resetDrawer = () => {
if (this.drawerSwiper && this.swiperIndex !== 1) {
if (this.swiperIndex !== 1) {
this.drawerSwiper.getWrappedInstance().resetPage();
}
};
@@ -359,35 +353,31 @@ export default class ChannelDrawer extends Component {
onShowTeams={this.showTeams}
onSearchStart={this.onSearchStart}
onSearchEnds={this.onSearchEnds}
theme={theme}
/>
</View>
);
return (
<SafeAreaView
backgroundColor={theme.sidebarHeaderBg}
navigator={navigator}
<DrawerSwiper
ref={this.drawerSwiperRef}
onPageSelected={this.onPageSelected}
openDrawerOffset={openDrawerOffset}
showTeams={showTeams}
theme={theme}
>
<DrawerSwiper
ref={this.drawerSwiperRef}
onPageSelected={this.onPageSelected}
openDrawerOffset={openDrawerOffset}
showTeams={showTeams}
>
{lists}
</DrawerSwiper>
</SafeAreaView>
{lists}
</DrawerSwiper>
);
};
render() {
const {children} = this.props;
const {openDrawerOffset} = this.state;
const {openDrawer, openDrawerOffset} = this.state;
return (
<Drawer
ref='drawer'
open={openDrawer}
onOpenStart={this.handleDrawerOpenStart}
onOpen={this.handleDrawerOpen}
onClose={this.handleDrawerClose}
@@ -395,7 +385,7 @@ export default class ChannelDrawer extends Component {
captureGestures='open'
type='static'
acceptTap={true}
acceptPanOnDrawer={false}
acceptPanOnDrawer={true}
disabled={false}
content={this.renderContent()}
tapToClose={true}
@@ -410,8 +400,6 @@ export default class ChannelDrawer extends Component {
tweenDuration={100}
tweenHandler={this.handleDrawerTween}
elevation={-5}
bottomPanOffset={Platform.OS === 'ios' ? 46 : 64}
topPanOffset={Platform.OS === 'ios' ? 64 : 46}
styles={{
main: {
shadowColor: '#000000',

View File

@@ -0,0 +1,163 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
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';
export default class ChannelItem extends PureComponent {
static propTypes = {
channel: PropTypes.object.isRequired,
onSelectChannel: PropTypes.func.isRequired,
isActive: PropTypes.bool.isRequired,
hasUnread: PropTypes.bool.isRequired,
mentions: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired
};
onPress = () => {
const {channel, onSelectChannel} = this.props;
requestAnimationFrame(() => {
preventDoubleTap(onSelectChannel, this, channel);
});
};
render() {
const {
channel,
theme,
mentions,
hasUnread,
isActive
} = this.props;
const style = getStyleSheet(theme);
let activeItem;
let activeText;
let unreadText;
let activeBorder;
let badge;
if (mentions && !isActive) {
badge = (
<Badge
style={style.badge}
countStyle={style.mention}
count={mentions}
minHeight={20}
minWidth={20}
onPress={this.onPress}
/>
);
}
if (hasUnread) {
unreadText = style.textUnread;
}
if (isActive) {
activeItem = style.itemActive;
activeText = style.textActive;
activeBorder = (
<View style={style.borderActive}/>
);
}
const icon = (
<ChanneIcon
isActive={isActive}
hasUnread={hasUnread}
membersCount={channel.display_name.split(',').length}
size={16}
status={channel.status}
theme={theme}
type={channel.type}
/>
);
return (
<TouchableHighlight
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={this.onPress}
>
<View style={style.container}>
{activeBorder}
<View style={[style.item, activeItem]}>
{icon}
<Text
style={[style.text, unreadText, activeText]}
ellipsizeMode='tail'
numberOfLines={1}
>
{channel.display_name}
</Text>
{badge}
</View>
</View>
</TouchableHighlight>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
flexDirection: 'row',
height: 44
},
borderActive: {
backgroundColor: theme.sidebarTextActiveBorder,
width: 5
},
item: {
alignItems: 'center',
height: 44,
flex: 1,
flexDirection: 'row',
paddingLeft: 16
},
itemActive: {
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.1),
paddingLeft: 11
},
text: {
color: changeOpacity(theme.sidebarText, 0.4),
flex: 1,
fontSize: 14,
fontWeight: '600',
lineHeight: 16,
paddingRight: 40
},
textActive: {
color: theme.sidebarTextActiveColor
},
textUnread: {
color: theme.sidebarUnreadText
},
badge: {
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
padding: 3,
position: 'relative',
right: 16
},
mention: {
color: theme.mentionColor,
fontSize: 10
}
};
});

View File

@@ -1,206 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
Platform,
TouchableHighlight,
Text,
View
} from 'react-native';
import {intlShape} from 'react-intl';
import Badge from 'app/components/badge';
import ChannelIcon from 'app/components/channel_icon';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const {View: AnimatedView} = Animated;
export default class ChannelItem extends PureComponent {
static propTypes = {
channelId: PropTypes.string.isRequired,
currentChannelId: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
fake: PropTypes.bool,
isUnread: PropTypes.bool,
mentions: PropTypes.number.isRequired,
navigator: PropTypes.object,
onSelectChannel: PropTypes.func.isRequired,
status: PropTypes.string,
type: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired
};
static contextTypes = {
intl: intlShape
};
onPress = wrapWithPreventDoubleTap(() => {
const {channelId, currentChannelId, displayName, fake, onSelectChannel, type} = this.props;
requestAnimationFrame(() => {
onSelectChannel({id: channelId, display_name: displayName, fake, type}, currentChannelId);
});
});
onPreview = () => {
const {channelId, navigator} = this.props;
if (Platform.OS === 'ios' && navigator && this.previewRef) {
const {intl} = this.context;
navigator.push({
screen: 'ChannelPeek',
previewCommit: false,
previewView: this.previewRef,
previewActions: [{
id: 'action-mark-as-read',
title: intl.formatMessage({id: 'mobile.channel.markAsRead', defaultMessage: 'Mark As Read'})
}],
passProps: {
channelId
}
});
}
};
setPreviewRef = (ref) => {
this.previewRef = ref;
};
render() {
const {
channelId,
currentChannelId,
displayName,
isUnread,
mentions,
status,
theme,
type
} = this.props;
const style = getStyleSheet(theme);
const isActive = channelId === currentChannelId;
let extraItemStyle;
let extraTextStyle;
let extraBorder;
if (isActive) {
extraItemStyle = style.itemActive;
extraTextStyle = style.textActive;
extraBorder = (
<View style={style.borderActive}/>
);
} else if (isUnread) {
extraTextStyle = style.textUnread;
}
let badge;
if (mentions) {
badge = (
<Badge
style={style.badge}
countStyle={style.mention}
count={mentions}
minHeight={20}
minWidth={20}
onPress={this.onPress}
/>
);
}
const icon = (
<ChannelIcon
isActive={isActive}
channelId={channelId}
isUnread={isUnread}
membersCount={displayName.split(',').length}
size={16}
status={status}
theme={theme}
type={type}
/>
);
return (
<AnimatedView ref={this.setPreviewRef}>
<TouchableHighlight
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={this.onPress}
onLongPress={this.onPreview}
>
<View style={style.container}>
{extraBorder}
<View style={[style.item, extraItemStyle]}>
{icon}
<Text
style={[style.text, extraTextStyle]}
ellipsizeMode='tail'
numberOfLines={1}
>
{displayName}
</Text>
{badge}
</View>
</View>
</TouchableHighlight>
</AnimatedView>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
flexDirection: 'row',
height: 44
},
borderActive: {
backgroundColor: theme.sidebarTextActiveBorder,
width: 5
},
item: {
alignItems: 'center',
height: 44,
flex: 1,
flexDirection: 'row',
paddingLeft: 16
},
itemActive: {
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.1),
paddingLeft: 11
},
text: {
color: changeOpacity(theme.sidebarText, 0.4),
flex: 1,
fontSize: 14,
fontWeight: '600',
lineHeight: 16,
paddingRight: 40
},
textActive: {
color: theme.sidebarTextActiveColor
},
textUnread: {
color: theme.sidebarUnreadText
},
badge: {
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
padding: 3,
position: 'relative',
right: 16
},
mention: {
color: theme.mentionColor,
fontSize: 10
}
};
});

View File

@@ -1,33 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getCurrentChannelId, makeGetChannel, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelItem from './channel_item';
function makeMapStateToProps() {
const getChannel = makeGetChannel();
return (state, ownProps) => {
const channel = ownProps.channel || getChannel(state, {id: ownProps.channelId});
let member;
if (ownProps.isUnread) {
member = getMyChannelMember(state, ownProps.channelId);
}
return {
currentChannelId: getCurrentChannelId(state),
displayName: channel.display_name,
fake: channel.fake,
mentions: member ? member.mention_count : 0,
status: channel.status,
theme: getTheme(state),
type: channel.type
};
};
}
export default connect(makeMapStateToProps)(ChannelItem);

View File

@@ -18,7 +18,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import FilteredList from './filtered_list';
import List from './list';
import SwitchTeamsButton from './switch_teams_button';
import SwitchTeams from './switch_teams';
class ChannelsList extends React.PureComponent {
static propTypes = {
@@ -34,7 +34,7 @@ class ChannelsList extends React.PureComponent {
constructor(props) {
super(props);
this.firstUnreadChannel = null;
this.state = {
searching: false,
term: ''
@@ -45,16 +45,14 @@ class ChannelsList extends React.PureComponent {
});
}
onSelectChannel = (channel, currentChannelId) => {
onSelectChannel = (channel) => {
if (channel.fake) {
this.props.onJoinChannel(channel, currentChannelId);
this.props.onJoinChannel(channel);
} else {
this.props.onSelectChannel(channel, currentChannelId);
this.props.onSelectChannel(channel);
}
if (this.refs.search_bar) {
this.refs.search_bar.cancel();
}
this.refs.search_bar.cancel();
};
openSettingsModal = wrapWithPreventDoubleTap(() => {
@@ -140,24 +138,21 @@ class ChannelsList extends React.PureComponent {
);
}
const searchBarInput = {
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
color: theme.sidebarHeaderTextColor,
fontSize: 15,
lineHeight: 66
};
const title = (
<View style={styles.searchContainer}>
<SearchBar
ref='search_bar'
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to...'})}
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to a conversation'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
inputStyle={{
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
color: theme.sidebarHeaderTextColor,
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.8)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
titleCancelColor={theme.sidebarHeaderTextColor}
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
@@ -176,12 +171,10 @@ class ChannelsList extends React.PureComponent {
>
<View style={styles.statusBar}>
<View style={styles.headerContainer}>
<View style={styles.switchContainer}>
<SwitchTeamsButton
searching={searching}
onShowTeams={onShowTeams}
/>
</View>
<SwitchTeams
searching={searching}
showTeams={onShowTeams}
/>
{title}
{settings}
</View>
@@ -199,7 +192,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flex: 1
},
statusBar: {
backgroundColor: theme.sidebarHeaderBg
backgroundColor: theme.sidebarHeaderBg,
...Platform.select({
ios: {
paddingTop: 20
}
})
},
headerContainer: {
alignItems: 'center',
@@ -224,10 +222,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
fontWeight: 'normal',
paddingLeft: 16
},
switchContainer: {
position: 'relative',
top: -1
},
settingsContainer: {
alignItems: 'center',
justifyContent: 'center',
@@ -294,6 +288,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
above: {
backgroundColor: theme.mentionBj,
top: 9
},
indicatorText: {
backgroundColor: 'transparent',
color: theme.mentionColor,
fontSize: 14,
paddingVertical: 2,
paddingHorizontal: 4,
textAlign: 'center',
textAlignVertical: 'center'
}
};
});

View File

@@ -95,25 +95,25 @@ class FilteredList extends Component {
}
onSelectChannel = (channel) => {
const {actions, currentChannel} = this.props;
const {makeGroupMessageVisibleIfNecessary} = actions;
const {makeGroupMessageVisibleIfNecessary} = this.props.actions;
if (channel.type === General.GM_CHANNEL) {
makeGroupMessageVisibleIfNecessary(channel.id);
}
this.props.onSelectChannel(channel, currentChannel.id);
this.props.onSelectChannel(channel);
};
createChannelElement = (channel) => {
return (
<ChannelDrawerItem
ref={channel.id}
channelId={channel.id}
channel={channel}
isUnread={false}
hasUnread={false}
mentions={0}
onSelectChannel={this.onSelectChannel}
isActive={channel.isCurrent || false}
theme={this.props.theme}
/>
);
};
@@ -147,7 +147,7 @@ class FilteredList extends Component {
},
channels: {
builder: this.buildChannelsForSearch,
id: 'mobile.channel_list.channels',
id: 'sidebar.channels',
defaultMessage: 'CHANNELS'
},
dms: {

View File

@@ -94,7 +94,7 @@ const getGroupChannelMemberDetails = createSelector(
getGroupDetails
);
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {currentUserId} = state.entities.users;
const profiles = getUsers(state);
@@ -119,7 +119,8 @@ function mapStateToProps(state) {
searchOrder,
pastDirectMessages: pastDirectMessages(state),
restrictDms,
theme: getTheme(state)
theme: getTheme(state),
...ownProps
};
}

View File

@@ -3,12 +3,13 @@
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelsList from './channels_list';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
...ownProps,
theme: getTheme(state)
};
}

View File

@@ -4,48 +4,26 @@
import {connect} from 'react-redux';
import {General} from 'mattermost-redux/constants';
import {
getSortedUnreadChannelIds,
getSortedFavoriteChannelIds,
getSortedPublicChannelIds,
getSortedPrivateChannelIds,
getSortedDirectChannelIds
} from 'mattermost-redux/selectors/entities/channels';
import {getChannelsWithUnreadSection, getCurrentChannel, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {getTheme, getFavoritesPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
import List from './list';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {config, license} = state.entities.general;
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
const unreadChannelIds = getSortedUnreadChannelIds(state);
const favoriteChannelIds = getSortedFavoriteChannelIds(state);
const publicChannelIds = getSortedPublicChannelIds(state);
const privateChannelIds = getSortedPrivateChannelIds(state);
const directChannelIds = getSortedDirectChannelIds(state);
return {
canCreatePrivateChannels: showCreateOption(config, license, General.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)),
unreadChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
directChannelIds,
theme: getTheme(state)
channelMembers: getMyChannelMemberships(state),
channels: getChannelsWithUnreadSection(state),
currentChannel: getCurrentChannel(state),
theme: getTheme(state),
...ownProps
};
}
function areStatesEqual(next, prev) {
const equalRoles = getCurrentUserRoles(prev) === getCurrentUserRoles(next);
const equalChannels = next.entities.channels === prev.entities.channels;
const equalConfig = next.entities.general.config === prev.entities.general.config;
const equalUsers = next.entities.users.profiles === prev.entities.users.profiles;
const equalFav = getFavoritesPreferences(next) === getFavoritesPreferences(prev);
return equalChannels && equalConfig && equalRoles && equalUsers && equalFav;
}
export default connect(mapStateToProps, null, null, {pure: true, areStatesEqual})(List);
export default connect(mapStateToProps, null)(List);

View File

@@ -1,51 +1,50 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import deepEqual from 'deep-equal';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
InteractionManager,
SectionList,
FlatList,
Text,
TouchableHighlight,
View
} from 'react-native';
import {intlShape} from 'react-intl';
import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {General} from 'mattermost-redux/constants';
import {debounce} from 'mattermost-redux/actions/helpers';
import ChannelItem from 'app/components/channel_drawer/channels_list/channel_item';
import UnreadIndicator from 'app/components/channel_drawer/channels_list/unread_indicator';
import FormattedText from 'app/components/formatted_text';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
export default class List extends PureComponent {
import {General} from 'mattermost-redux/constants';
import ChannelItem from 'app/components/channel_drawer/channels_list/channel_item';
import UnreadIndicator from 'app/components/channel_drawer/channels_list/unread_indicator';
class List extends Component {
static propTypes = {
canCreatePrivateChannels: PropTypes.bool.isRequired,
directChannelIds: PropTypes.array.isRequired,
favoriteChannelIds: PropTypes.array.isRequired,
channels: PropTypes.object.isRequired,
channelMembers: PropTypes.object,
currentChannel: PropTypes.object,
intl: intlShape.isRequired,
navigator: PropTypes.object,
onSelectChannel: PropTypes.func.isRequired,
publicChannelIds: PropTypes.array.isRequired,
privateChannelIds: PropTypes.array.isRequired,
styles: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
unreadChannelIds: PropTypes.array.isRequired
theme: PropTypes.object.isRequired
};
static contextTypes = {
intl: intlShape
static defaultProps = {
currentChannel: {}
};
constructor(props) {
super(props);
this.firstUnreadChannel = null;
this.state = {
sections: this.buildSections(props),
showIndicator: false,
width: 0
dataSource: this.buildData(props),
showAbove: false
};
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
@@ -53,98 +52,107 @@ export default class List extends PureComponent {
});
}
shouldComponentUpdate(nextProps, nextState) {
return !deepEqual(this.props, nextProps, {strict: true}) || !deepEqual(this.state, nextState, {strict: true});
}
componentWillReceiveProps(nextProps) {
const {
canCreatePrivateChannels,
directChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
unreadChannelIds
} = this.props;
if (nextProps.canCreatePrivateChannels !== canCreatePrivateChannels ||
nextProps.directChannelIds !== directChannelIds || nextProps.favoriteChannelIds !== favoriteChannelIds ||
nextProps.publicChannelIds !== publicChannelIds || nextProps.privateChannelIds !== privateChannelIds ||
nextProps.unreadChannelIds !== unreadChannelIds) {
this.setState({sections: this.buildSections(nextProps)});
}
this.setState({
dataSource: this.buildData(nextProps)
}, () => {
if (this.refs.list) {
this.refs.list.recordInteraction();
this.updateUnreadIndicators({
viewableItems: Array.from(this.refs.list._listRef._viewabilityHelper._viewableItems.values()) //eslint-disable-line
});
}
});
}
componentDidUpdate(prevProps, prevState) {
if (prevState.sections !== this.state.sections && this.refs.list._wrapperListRef.getListRef()._viewabilityHelper) { //eslint-disable-line
this.refs.list.recordInteraction();
this.updateUnreadIndicators({
viewableItems: Array.from(this.refs.list._wrapperListRef.getListRef()._viewabilityHelper._viewableItems.values()) //eslint-disable-line
updateUnreadIndicators = ({viewableItems}) => {
let showAbove = false;
const visibleIndexes = viewableItems.map((v) => v.index);
if (visibleIndexes.length) {
const {dataSource} = this.state;
const firstVisible = parseInt(visibleIndexes[0], 10);
if (this.firstUnreadChannel) {
const index = dataSource.findIndex((item) => {
return item.display_name === this.firstUnreadChannel;
});
showAbove = index < firstVisible;
}
this.setState({
showAbove
});
}
}
buildSections = (props) => {
const {
canCreatePrivateChannels,
directChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
unreadChannelIds
} = props;
const sections = [];
if (unreadChannelIds.length) {
sections.push({
id: 'mobile.channel_list.unreads',
defaultMessage: 'UNREADS',
data: unreadChannelIds,
renderItem: this.renderUnreadItem,
topSeparator: false,
bottomSeparator: true
});
}
if (favoriteChannelIds.length) {
sections.push({
id: 'sidebar.favorite',
defaultMessage: 'FAVORITES',
data: favoriteChannelIds,
topSeparator: unreadChannelIds.length > 0,
bottomSeparator: true
});
}
sections.push({
action: this.goToMoreChannels,
id: 'sidebar.channels',
defaultMessage: 'PUBLIC CHANNELS',
data: publicChannelIds,
topSeparator: favoriteChannelIds.length > 0 || unreadChannelIds.length > 0,
bottomSeparator: publicChannelIds.length > 0
});
sections.push({
action: canCreatePrivateChannels ? this.goToCreatePrivateChannel : null,
id: 'sidebar.pg',
defaultMessage: 'PRIVATE CHANNELS',
data: privateChannelIds,
topSeparator: true,
bottomSeparator: privateChannelIds.length > 0
});
sections.push({
action: this.goToDirectMessages,
id: 'sidebar.direct',
defaultMessage: 'DIRECT MESSAGES',
data: directChannelIds,
topSeparator: true,
bottomSeparator: directChannelIds.length > 0
});
return sections;
};
goToCreatePrivateChannel = wrapWithPreventDoubleTap(() => {
const {navigator, theme} = this.props;
const {intl} = this.context;
onSelectChannel = (channel) => {
this.props.onSelectChannel(channel);
};
onLayout = (event) => {
const {width} = event.nativeEvent.layout;
this.width = width;
};
getUnreadMessages = (channel) => {
const member = this.props.channelMembers[channel.id];
let mentions = 0;
let unreadCount = 0;
if (member && channel) {
mentions = member.mention_count;
unreadCount = channel.total_msg_count - member.msg_count;
if (member.notify_props && member.notify_props.mark_unread === General.MENTION) {
unreadCount = 0;
}
}
return {
mentions,
unreadCount
};
};
findUnreadChannels = (data) => {
data.forEach((c) => {
if (c.id) {
const {mentions, unreadCount} = this.getUnreadMessages(c);
const unread = (mentions + unreadCount) > 0;
if (unread && c.id !== this.props.currentChannel.id) {
if (!this.firstUnreadChannel) {
this.firstUnreadChannel = c.display_name;
}
}
}
});
};
createChannelElement = (channel) => {
const {mentions, unreadCount} = this.getUnreadMessages(channel);
const msgCount = mentions + unreadCount;
const unread = msgCount > 0;
return (
<ChannelItem
ref={channel.id}
channel={channel}
hasUnread={unread}
mentions={mentions}
onSelectChannel={this.onSelectChannel}
isActive={channel.isCurrent}
theme={this.props.theme}
/>
);
};
createPrivateChannel = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'CreateChannel',
@@ -165,9 +173,76 @@ export default class List extends PureComponent {
});
});
goToDirectMessages = wrapWithPreventDoubleTap(() => {
const {navigator, theme} = this.props;
const {intl} = this.context;
buildChannels = (props) => {
const {canCreatePrivateChannels, styles} = props;
const {
unreadChannels,
favoriteChannels,
publicChannels,
privateChannels,
directAndGroupChannels
} = props.channels;
const data = [];
if (unreadChannels.length) {
data.push(
this.renderTitle(styles, 'mobile.channel_list.unreads', 'UNREADS', null, false, true),
...unreadChannels
);
}
if (favoriteChannels.length) {
data.push(
this.renderTitle(styles, 'sidebar.favorite', 'FAVORITES', null, unreadChannels.length > 0, true),
...favoriteChannels
);
}
data.push(
this.renderTitle(styles, 'sidebar.channels', 'CHANNELS', this.showMoreChannelsModal, favoriteChannels.length > 0, publicChannels.length > 0),
...publicChannels
);
let createPrivateChannel;
if (canCreatePrivateChannels) {
createPrivateChannel = this.createPrivateChannel;
}
data.push(
this.renderTitle(styles, 'sidebar.pg', 'PRIVATE CHANNELS', createPrivateChannel, true, privateChannels.length > 0),
...privateChannels
);
data.push(
this.renderTitle(styles, 'sidebar.direct', 'DIRECT MESSAGES', this.showDirectMessagesModal, true, directAndGroupChannels.length > 0),
...directAndGroupChannels
);
return data;
};
buildData = (props) => {
if (!props.currentChannel) {
return null;
}
const data = this.buildChannels(props);
this.firstUnreadChannel = null;
this.findUnreadChannels(data);
return data;
};
scrollToTop = () => {
this.refs.list.scrollToOffset({
x: 0,
y: 0,
animated: true
});
}
showDirectMessagesModal = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'MoreDirectMessages',
@@ -190,9 +265,8 @@ export default class List extends PureComponent {
});
});
goToMoreChannels = wrapWithPreventDoubleTap(() => {
const {navigator, theme} = this.props;
const {intl} = this.context;
showMoreChannelsModal = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'MoreChannels',
@@ -212,18 +286,6 @@ export default class List extends PureComponent {
});
});
keyExtractor = (item) => item.id || item;
onSelectChannel = (channel, currentChannelId) => {
const {onSelectChannel} = this.props;
onSelectChannel(channel, currentChannelId);
};
onLayout = (event) => {
const {width} = event.nativeEvent.layout;
this.setState({width: width - 40});
};
renderSectionAction = (styles, action) => {
const {theme} = this.props;
return (
@@ -240,118 +302,86 @@ export default class List extends PureComponent {
);
};
renderSectionSeparator = () => {
const {styles} = this.props;
renderDivider = (styles, marginLeft) => {
return (
<View style={[styles.divider]}/>
<View
style={[styles.divider, {marginLeft}]}
/>
);
};
renderItem = ({item}) => {
return (
<ChannelItem
channelId={item}
navigator={this.props.navigator}
onSelectChannel={this.onSelectChannel}
/>
);
};
renderUnreadItem = ({item}) => {
return (
<ChannelItem
channelId={item}
isUnread={true}
navigator={this.props.navigator}
onSelectChannel={this.onSelectChannel}
/>
);
};
renderSectionHeader = ({section}) => {
const {styles} = this.props;
const {intl} = this.context;
const {
action,
bottomSeparator,
defaultMessage,
id,
topSeparator
} = section;
return (
<View>
{topSeparator && this.renderSectionSeparator()}
<View style={styles.titleContainer}>
<Text style={styles.title}>
{intl.formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
{action && this.renderSectionAction(styles, action)}
</View>
{bottomSeparator && this.renderSectionSeparator()}
</View>
);
};
scrollToTop = () => {
if (this.refs.list) {
this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
x: 0,
y: 0,
animated: true
});
if (!item.isTitle) {
return this.createChannelElement(item);
}
return item.title;
};
emitUnreadIndicatorChange = debounce((showIndicator) => {
this.setState({showIndicator});
}, 100);
renderTitle = (styles, id, defaultMessage, action, topDivider, bottomDivider) => {
const {formatMessage} = this.props.intl;
updateUnreadIndicators = ({viewableItems}) => {
InteractionManager.runAfterInteractions(() => {
const {unreadChannelIds} = this.props;
const firstUnread = unreadChannelIds.length && unreadChannelIds[0];
if (firstUnread && viewableItems.length) {
const isVisible = viewableItems.find((v) => v.item === firstUnread);
return this.emitUnreadIndicatorChange(!isVisible);
}
return this.emitUnreadIndicatorChange(false);
});
return {
id,
isTitle: true,
title: (
<View>
{topDivider && this.renderDivider(styles, 0)}
<View style={styles.titleContainer}>
<Text style={styles.title}>
{formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
{action && this.renderSectionAction(styles, action)}
</View>
{bottomDivider && this.renderDivider(styles, 0)}
</View>
)
};
};
render() {
const {styles, theme} = this.props;
const {sections, width, showIndicator} = this.state;
const {styles} = this.props;
const {dataSource, showAbove} = this.state;
let above;
if (showAbove) {
above = (
<UnreadIndicator
style={[styles.above, {width: (this.width - 40)}]}
onPress={this.scrollToTop}
text={(
<FormattedText
style={styles.indicatorText}
id='sidebar.unreadAbove'
defaultMessage='Unread post(s) above'
/>
)}
/>
);
}
return (
<View
style={styles.container}
onLayout={this.onLayout}
>
<SectionList
<FlatList
ref='list'
sections={sections}
data={dataSource}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
keyExtractor={this.keyExtractor}
keyExtractor={(item) => item.id}
onViewableItemsChanged={this.updateUnreadIndicators}
keyboardDismissMode='on-drag'
maxToRenderPerBatch={10}
stickySectionHeadersEnabled={false}
viewabilityConfig={{
viewAreaCoveragePercentThreshold: 3,
waitForInteraction: true
waitForInteraction: false
}}
/>
<UnreadIndicator
show={showIndicator}
style={[styles.above, {width}]}
onPress={this.scrollToTop}
theme={theme}
/>
{above}
</View>
);
}
}
export default injectIntl(List);

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import SwitchTeams from './switch_teams';
function mapStateToProps(state, ownProps) {
return {
currentTeam: getCurrentTeam(state),
teamMembers: getTeamMemberships(state),
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(SwitchTeams);

View File

@@ -14,50 +14,96 @@ import Badge from 'app/components/badge';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class SwitchTeamsButton extends React.PureComponent {
export default class SwitchTeams extends React.PureComponent {
static propTypes = {
currentTeamId: PropTypes.string,
displayName: PropTypes.string,
currentTeam: PropTypes.object,
searching: PropTypes.bool.isRequired,
onShowTeams: PropTypes.func.isRequired,
mentionCount: PropTypes.number.isRequired,
teamsCount: PropTypes.number.isRequired,
showTeams: PropTypes.func.isRequired,
teamMembers: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.state = {
badgeCount: this.getBadgeCount(props)
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.currentTeam !== this.props.currentTeam || nextProps.teamMembers !== this.props.teamMembers) {
this.setState({
badgeCount: this.getBadgeCount(nextProps)
});
}
}
getBadgeCount = (props) => {
const {
currentTeam,
teamMembers
} = props;
let mentionCount = 0;
let messageCount = 0;
Object.values(teamMembers).forEach((m) => {
if (m.team_id !== currentTeam.id) {
mentionCount = mentionCount + (m.mention_count || 0);
messageCount = messageCount + (m.msg_count || 0);
}
});
let badgeCount;
if (mentionCount) {
badgeCount = mentionCount;
} else if (messageCount) {
badgeCount = -1;
} else {
badgeCount = 0;
}
return badgeCount;
};
showTeams = wrapWithPreventDoubleTap(() => {
this.props.onShowTeams();
this.props.showTeams();
});
render() {
const {
currentTeamId,
displayName,
mentionCount,
currentTeam,
searching,
teamsCount,
teamMembers,
theme
} = this.props;
if (!currentTeamId) {
if (!currentTeam) {
return null;
}
if (searching || teamsCount < 2) {
const {
badgeCount
} = this.state;
if (searching || Object.keys(teamMembers).length < 2) {
return null;
}
const styles = getStyleSheet(theme);
const badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={mentionCount}
minHeight={20}
minWidth={20}
/>
);
let badge;
if (badgeCount) {
badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
return (
<View>
@@ -73,7 +119,7 @@ export default class SwitchTeamsButton extends React.PureComponent {
/>
<View style={styles.switcherDivider}/>
<Text style={styles.switcherTeam}>
{displayName.substr(0, 2).toUpperCase()}
{currentTeam.display_name.substr(0, 2).toUpperCase()}
</Text>
</View>
</TouchableHighlight>
@@ -93,7 +139,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
height: 32,
justifyContent: 'center',
marginLeft: 6,
marginRight: 5,
marginRight: 10,
paddingHorizontal: 6
},
switcherDivider: {

View File

@@ -1,23 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeam, getMyTeamsCount, getChannelDrawerBadgeCount} from 'mattermost-redux/selectors/entities/teams';
import SwitchTeamsButton from './switch_teams_button';
function mapStateToProps(state) {
const team = getCurrentTeam(state);
return {
currentTeamId: team.id,
displayName: team.display_name,
mentionCount: getChannelDrawerBadgeCount(state),
teamsCount: getMyTeamsCount(state),
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(SwitchTeamsButton);

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
TouchableWithoutFeedback,
View,
ViewPropTypes
} from 'react-native';
export default class UnreadIndicator extends PureComponent {
static propTypes = {
style: ViewPropTypes.style,
textStyle: Text.propTypes.style,
text: PropTypes.node.isRequired,
onPress: PropTypes.func
};
static defaultProps = {
onPress: () => true
};
render() {
return (
<TouchableWithoutFeedback onPress={this.props.onPress}>
<View
style={[Styles.container, this.props.style]}
>
{this.props.text}
</View>
</TouchableWithoutFeedback>
);
}
}
const Styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
position: 'absolute',
borderRadius: 15,
marginHorizontal: 15,
height: 25
}
});

View File

@@ -1,84 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
TouchableWithoutFeedback,
View,
ViewPropTypes
} from 'react-native';
import IonIcon from 'react-native-vector-icons/Ionicons';
import FormattedText from 'app/components/formatted_text';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class UnreadIndicator extends PureComponent {
static propTypes = {
show: PropTypes.bool,
style: ViewPropTypes.style,
onPress: PropTypes.func,
theme: PropTypes.object.isRequired
};
static defaultProps = {
onPress: () => true
};
render() {
const {onPress, show, theme} = this.props;
if (!show) {
return null;
}
const style = getStyleSheet(theme);
return (
<TouchableWithoutFeedback onPress={onPress}>
<View
style={[style.container, this.props.style]}
>
<FormattedText
style={style.indicatorText}
id='sidebar.unreads'
defaultMessage='More unreads'
/>
<IonIcon
size={14}
name='md-arrow-round-up'
color={theme.mentionColor}
style={style.arrow}
/>
</View>
</TouchableWithoutFeedback>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
position: 'absolute',
borderRadius: 15,
marginHorizontal: 15,
height: 25
},
indicatorText: {
backgroundColor: 'transparent',
color: theme.mentionColor,
fontSize: 14,
paddingVertical: 2,
paddingHorizontal: 4,
textAlign: 'center',
textAlignVertical: 'center'
},
arrow: {
position: 'relative',
bottom: -1
}
};
});

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {StyleSheet} from 'react-native';
@@ -9,7 +9,7 @@ import {changeOpacity} from 'app/utils/theme';
import Swiper from 'app/components/swiper';
export default class DrawerSwiper extends Component {
export default class DrawerSwiper extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
deviceWidth: PropTypes.number.isRequired,
@@ -24,28 +24,16 @@ export default class DrawerSwiper extends Component {
openDrawerOffset: 0
};
shouldComponentUpdate(nextProps) {
const {deviceWidth, showTeams, theme} = this.props;
return nextProps.deviceWidth !== deviceWidth ||
nextProps.showTeams !== showTeams || nextProps.theme !== theme;
}
runOnLayout = (shouldRun = true) => {
if (this.refs.swiper) {
this.refs.swiper.runOnLayout = shouldRun;
}
this.refs.swiper.runOnLayout = shouldRun;
};
resetPage = () => {
if (this.refs.swiper) {
this.refs.swiper.scrollToIndex(1, false);
}
this.refs.swiper.scrollToIndex(1, false);
};
scrollToStart = () => {
if (this.refs.swiper) {
this.refs.swiper.scrollToStart();
}
this.refs.swiper.scrollToStart();
};
swiperPageSelected = (index) => {
@@ -53,9 +41,7 @@ export default class DrawerSwiper extends Component {
};
showTeamsPage = () => {
if (this.refs.swiper) {
this.refs.swiper.scrollToIndex(0, true);
}
this.refs.swiper.scrollToIndex(0, true);
};
render() {

View File

@@ -3,16 +3,15 @@
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getDimensions} from 'app/selectors/device';
import {getDimensions, isLandscape} from 'app/selectors/device';
import DraweSwiper from './drawer_swiper';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
...ownProps,
...getDimensions(state),
theme: getTheme(state)
isLandscape: isLandscape(state)
};
}

View File

@@ -4,26 +4,29 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {joinChannel, markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {joinChannel, viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getTeams} from 'mattermost-redux/actions/teams';
import {getCurrentTeamId, getMyTeamsCount} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import {handleSelectChannel, setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {isLandscape, isTablet} from 'app/selectors/device';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelDrawer from './channel_drawer.js';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {currentUserId} = state.entities.users;
return {
...ownProps,
currentTeamId: getCurrentTeamId(state),
currentChannelId: getCurrentChannelId(state),
currentUserId,
isLandscape: isLandscape(state),
isTablet: isTablet(state),
teamsCount: getMyTeamsCount(state),
teamsCount: Object.keys(getTeamMemberships(state)).length,
theme: getTheme(state)
};
}
@@ -34,7 +37,7 @@ function mapDispatchToProps(dispatch) {
getTeams,
handleSelectChannel,
joinChannel,
markChannelAsViewed,
viewChannel,
makeDirectChannel,
markChannelAsRead,
setChannelDisplayName,

View File

@@ -5,24 +5,23 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getCurrentTeamId, getJoinableTeamIds, getMySortedTeamIds} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId, getJoinableTeams} from 'mattermost-redux/selectors/entities/teams';
import {handleTeamChange} from 'app/actions/views/select_team';
import {getCurrentLocale} from 'app/selectors/i18n';
import {getTheme} from 'app/selectors/preferences';
import {getMySortedTeams} from 'app/selectors/teams';
import {removeProtocol} from 'app/utils/url';
import TeamsList from './teams_list';
function mapStateToProps(state) {
const locale = getCurrentLocale(state);
function mapStateToProps(state, ownProps) {
return {
canJoinOtherTeams: getJoinableTeamIds(state).length > 0,
joinableTeams: getJoinableTeams(state),
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
teamIds: getMySortedTeamIds(state, locale),
theme: getTheme(state)
teams: getMySortedTeams(state),
theme: getTheme(state),
...ownProps
};
}

View File

@@ -16,7 +16,6 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import tracker from 'app/utils/time_tracker';
import TeamsListItem from './teams_list_item';
@@ -25,13 +24,13 @@ class TeamsList extends PureComponent {
actions: PropTypes.shape({
handleTeamChange: PropTypes.func.isRequired
}).isRequired,
canJoinOtherTeams: PropTypes.bool.isRequired,
closeChannelDrawer: PropTypes.func.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
intl: intlShape.isRequired,
joinableTeams: PropTypes.object.isRequired,
navigator: PropTypes.object.isRequired,
teamIds: PropTypes.array.isRequired,
teams: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired
};
@@ -43,12 +42,11 @@ class TeamsList extends PureComponent {
});
}
selectTeam = (teamId) => {
selectTeam = (team) => {
requestAnimationFrame(() => {
const {actions, closeChannelDrawer, currentTeamId} = this.props;
if (teamId !== currentTeamId) {
tracker.teamSwitch = Date.now();
actions.handleTeamChange(teamId);
if (team.id !== currentTeamId) {
actions.handleTeamChange(team);
}
closeChannelDrawer();
@@ -83,25 +81,25 @@ class TeamsList extends PureComponent {
});
});
keyExtractor = (item) => {
return item;
};
keyExtractor = (team) => {
return team.id;
}
renderItem = ({item}) => {
return (
<TeamsListItem
selectTeam={this.selectTeam}
teamId={item}
team={item}
/>
);
};
render() {
const {canJoinOtherTeams, teamIds, theme} = this.props;
const {joinableTeams, teams, theme} = this.props;
const styles = getStyleSheet(theme);
let moreAction;
if (canJoinOtherTeams) {
if (Object.keys(joinableTeams).length) {
moreAction = (
<TouchableHighlight
style={styles.moreActionContainer}
@@ -130,7 +128,7 @@ class TeamsList extends PureComponent {
</View>
</View>
<FlatList
data={teamIds}
data={teams}
renderItem={this.renderItem}
keyExtractor={this.keyExtractor}
viewabilityConfig={{
@@ -150,7 +148,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flex: 1
},
statusBar: {
backgroundColor: theme.sidebarHeaderBg
backgroundColor: theme.sidebarHeaderBg,
...Platform.select({
ios: {
paddingTop: 20
}
})
},
headerContainer: {
alignItems: 'center',

View File

@@ -5,27 +5,20 @@ import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId, getTeam, makeGetBadgeCountForTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import {removeProtocol} from 'app/utils/url';
import TeamsListItem from './teams_list_item.js';
function makeMapStateToProps() {
const getMentionCount = makeGetBadgeCountForTeamId();
return function mapStateToProps(state, ownProps) {
const team = getTeam(state, ownProps.teamId);
return {
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
displayName: team.display_name,
mentionCount: getMentionCount(state, ownProps.teamId),
name: team.name,
theme: getTheme(state)
};
function mapStateToProps(state, ownProps) {
return {
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
teamMember: getTeamMemberships(state)[ownProps.team.id],
theme: getTheme(state),
...ownProps
};
}
export default connect(makeMapStateToProps)(TeamsListItem);
export default connect(mapStateToProps)(TeamsListItem);

View File

@@ -18,32 +18,29 @@ export default class TeamsListItem extends React.PureComponent {
static propTypes = {
currentTeamId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
mentionCount: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
selectTeam: PropTypes.func.isRequired,
teamId: PropTypes.string.isRequired,
team: PropTypes.object.isRequired,
teamMember: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
selectTeam = wrapWithPreventDoubleTap(() => {
this.props.selectTeam(this.props.teamId);
this.props.selectTeam(this.props.team);
});
render() {
const {
currentTeamId,
currentUrl,
displayName,
mentionCount,
name,
teamId,
team,
teamMember,
theme
} = this.props;
const styles = getStyleSheet(theme);
let current;
if (teamId === currentTeamId) {
let badge;
if (team.id === currentTeamId) {
current = (
<View style={styles.checkmarkContainer}>
<IonIcon
@@ -54,15 +51,24 @@ export default class TeamsListItem extends React.PureComponent {
);
}
const badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={mentionCount}
minHeight={20}
minWidth={20}
/>
);
let badgeCount = 0;
if (teamMember.mention_count) {
badgeCount = teamMember.mention_count;
} else if (teamMember.msg_count) {
badgeCount = -1;
}
if (badgeCount) {
badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
return (
<View style={styles.teamWrapper}>
@@ -73,7 +79,7 @@ export default class TeamsListItem extends React.PureComponent {
<View style={styles.teamContainer}>
<View style={styles.teamIconContainer}>
<Text style={styles.teamIcon}>
{displayName.substr(0, 2).toUpperCase()}
{team.display_name.substr(0, 2).toUpperCase()}
</Text>
</View>
<View style={styles.teamNameContainer}>
@@ -82,14 +88,14 @@ export default class TeamsListItem extends React.PureComponent {
ellipsizeMode='tail'
style={styles.teamName}
>
{displayName}
{team.display_name}
</Text>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.teamUrl}
>
{`${currentUrl}/${name}`}
{`${currentUrl}/${team.name}`}
</Text>
</View>
{current}

View File

@@ -9,7 +9,7 @@ import {
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {AwayAvatar, DndAvatar, OfflineAvatar, OnlineAvatar} from 'app/components/status_icons';
import {OnlineStatus, AwayStatus, OfflineStatus} from 'app/components/status_icons';
import {General} from 'mattermost-redux/constants';
@@ -19,7 +19,7 @@ export default class ChannelIcon extends React.PureComponent {
static propTypes = {
isActive: PropTypes.bool,
isInfo: PropTypes.bool,
isUnread: PropTypes.bool,
hasUnread: PropTypes.bool,
membersCount: PropTypes.number,
size: PropTypes.number,
status: PropTypes.string,
@@ -30,12 +30,12 @@ export default class ChannelIcon extends React.PureComponent {
static defaultProps = {
isActive: false,
isInfo: false,
isUnread: false,
hasUnread: false,
size: 12
};
render() {
const {isActive, isUnread, isInfo, membersCount, size, status, theme, type} = this.props;
const {isActive, hasUnread, isInfo, membersCount, size, status, theme, type} = this.props;
const style = getStyleSheet(theme);
let activeIcon;
@@ -46,7 +46,7 @@ export default class ChannelIcon extends React.PureComponent {
let unreadGroup;
let offlineColor = changeOpacity(theme.sidebarText, 0.5);
if (isUnread) {
if (hasUnread) {
unreadIcon = style.iconUnread;
unreadGroupBox = style.groupBoxUnread;
unreadGroup = style.groupUnread;
@@ -90,43 +90,30 @@ export default class ChannelIcon extends React.PureComponent {
</View>
);
} else if (type === General.DM_CHANNEL) {
switch (status) {
case General.AWAY:
if (status === General.ONLINE) {
icon = (
<AwayAvatar
width={size}
height={size}
color={theme.awayIndicator}
/>
);
break;
case General.DND:
icon = (
<DndAvatar
width={size}
height={size}
color={theme.dndIndicator}
/>
);
break;
case General.ONLINE:
icon = (
<OnlineAvatar
<OnlineStatus
width={size}
height={size}
color={theme.onlineIndicator}
/>
);
break;
default:
} else if (status === General.AWAY) {
icon = (
<OfflineAvatar
<AwayStatus
width={size}
height={size}
color={theme.awayIndicator}
/>
);
} else {
icon = (
<OfflineStatus
width={size}
height={size}
color={offlineColor}
/>
);
break;
}
}

View File

@@ -12,7 +12,6 @@ import {getFullName} from 'mattermost-redux/utils/user_utils';
import {General} from 'mattermost-redux/constants';
import {injectIntl, intlShape} from 'react-intl';
import Loading from 'app/components/loading';
import ProfilePicture from 'app/components/profile_picture';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -23,7 +22,6 @@ class ChannelIntro extends PureComponent {
currentChannel: PropTypes.object.isRequired,
currentChannelMembers: PropTypes.array.isRequired,
intl: intlShape.isRequired,
isLoadingPosts: PropTypes.bool,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
@@ -77,6 +75,7 @@ class ChannelIntro extends PureComponent {
size={64}
statusBorderWidth={2}
statusSize={25}
statusIconSize={15}
/>
</TouchableOpacity>
));
@@ -305,35 +304,18 @@ class ChannelIntro extends PureComponent {
};
render() {
const {currentChannel, isLoadingPosts, theme} = this.props;
const {theme} = this.props;
const style = getStyleSheet(theme);
const channelType = currentChannel.type;
if (isLoadingPosts) {
return (
<View style={style.container}>
<Loading/>
</View>
);
}
let profiles;
if (channelType === General.DM_CHANNEL || channelType === General.GM_CHANNEL) {
profiles = (
<View>
<View style={style.profilesContainer}>
{this.buildProfiles()}
</View>
<View style={style.namesContainer}>
{this.buildNames()}
</View>
</View>
);
}
return (
<View style={style.container}>
{profiles}
<View style={style.profilesContainer}>
{this.buildProfiles()}
</View>
<View style={style.namesContainer}>
{this.buildNames()}
</View>
<View style={style.contentContainer}>
{this.buildContent()}
</View>

View File

@@ -1,47 +1,47 @@
// 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 {General, RequestStatus} from 'mattermost-redux/constants';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUser, makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
import {General} from 'mattermost-redux/constants';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUser, getProfilesInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelIntro from './channel_intro';
function makeMapStateToProps() {
const getChannel = makeGetChannel();
const getProfilesInChannel = makeGetProfilesInChannel();
function mapStateToProps(state) {
const currentChannel = getCurrentChannel(state) || {};
const currentUser = getCurrentUser(state) || {};
return function mapStateToProps(state, ownProps) {
const currentChannel = getChannel(state, {id: ownProps.channelId}) || {};
const currentUser = getCurrentUser(state) || {};
const {status: getPostsRequestStatus} = state.requests.posts.getPosts;
let currentChannelMembers = [];
if (currentChannel.type === General.DM_CHANNEL) {
const otherChannelMember = currentChannel.name.split('__').find((m) => m !== currentUser.id);
const otherProfile = state.entities.users.profiles[otherChannelMember];
if (otherProfile) {
currentChannelMembers.push(otherProfile);
}
} else {
currentChannelMembers = getProfilesInChannel(state, ownProps.channelId) || [];
currentChannelMembers = currentChannelMembers.filter((m) => m.id !== currentUser.id);
let currentChannelMembers = [];
if (currentChannel.type === General.DM_CHANNEL) {
const otherChannelMember = currentChannel.name.split('__').find((m) => m !== currentUser.id);
const otherProfile = state.entities.users.profiles[otherChannelMember];
if (otherProfile) {
currentChannelMembers.push(otherProfile);
}
} else {
currentChannelMembers = getProfilesInCurrentChannel(state) || [];
currentChannelMembers = currentChannelMembers.filter((m) => m.id !== currentUser.id);
}
const creator = currentChannel.creator_id === currentUser.id ? currentUser : state.entities.users.profiles[currentChannel.creator_id];
const postsInChannel = state.entities.posts.postsInChannel[ownProps.channelId] || [];
const creator = currentChannel.creator_id === currentUser.id ? currentUser : state.entities.users.profiles[currentChannel.creator_id];
return {
creator,
currentChannel,
currentChannelMembers,
isLoadingPosts: !postsInChannel.length && getPostsRequestStatus === RequestStatus.STARTED,
theme: getTheme(state)
};
return {
creator,
currentChannel,
currentChannelMembers,
theme: getTheme(state)
};
}
export default connect(makeMapStateToProps)(ChannelIntro);
function mapDispatchToProps(dispatch) {
// placeholder for invite and set header actions
return {
actions: bindActionCreators({}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelIntro);

View File

@@ -10,9 +10,10 @@ import {handleSelectChannel, setChannelDisplayName} from 'app/actions/views/chan
import ChannelLink from './channel_link';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
channelsByName: getChannelsNameMapInCurrentTeam(state)
channelsByName: getChannelsNameMapInCurrentTeam(state),
...ownProps
};
}

View File

@@ -2,13 +2,14 @@
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelLoader from './channel_loader';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {deviceWidth} = state.device.dimension;
return {
...ownProps,
channelIsLoading: state.views.channel.loading,
deviceWidth,
theme: getTheme(state)

View File

@@ -1,271 +0,0 @@
// 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 {
Alert,
Animated,
Linking,
TouchableOpacity,
View
} from 'react-native';
import {intlShape} from 'react-intl';
import DeviceInfo from 'react-native-device-info';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import {UpgradeTypes} from 'app/constants/view';
import checkUpgradeType from 'app/utils/client_upgrade';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const {View: AnimatedView} = Animated;
const UPDATE_TIMEOUT = 60000;
export default class ClientUpgradeListener extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
logError: PropTypes.func.isRequired,
setLastUpgradeCheck: PropTypes.func.isRequired
}).isRequired,
currentVersion: PropTypes.string,
downloadLink: PropTypes.string,
forceUpgrade: PropTypes.bool,
isLandscape: PropTypes.bool,
lastUpgradeCheck: PropTypes.number,
latestVersion: PropTypes.string,
minVersion: PropTypes.string,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired
};
static contextTypes = {
intl: intlShape
};
constructor(props) {
super(props);
this.isX = DeviceInfo.getModel() === 'iPhone X';
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
this.closeButton = source;
});
this.state = {
top: new Animated.Value(-100)
};
}
componentDidMount() {
const {forceUpgrade, isLandscape, lastUpgradeCheck, latestVersion, minVersion} = this.props;
if (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT) {
this.checkUpgrade(minVersion, latestVersion, isLandscape);
}
}
componentWillReceiveProps(nextProps) {
const {forceUpgrade, latestVersion, minVersion} = this.props;
const {latestVersion: nextLatestVersion, minVersion: nextMinVersion, lastUpgradeCheck} = nextProps;
const versionMismatch = latestVersion !== nextLatestVersion || minVersion !== nextMinVersion;
if (versionMismatch && (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT)) {
this.checkUpgrade(minVersion, latestVersion, nextProps.isLandscape);
} else if (this.props.isLandscape !== nextProps.isLandscape &&
this.state.upgradeType !== UpgradeTypes.NO_UPGRADE && this.isX) {
const newTop = nextProps.isLandscape ? 45 : 100;
this.setState({top: new Animated.Value(newTop)});
}
}
checkUpgrade = (minVersion, latestVersion, isLandscape) => {
const {actions, currentVersion} = this.props;
const upgradeType = checkUpgradeType(currentVersion, minVersion, latestVersion, actions.logError);
this.setState({upgradeType});
if (upgradeType === UpgradeTypes.NO_UPGRADE) {
return;
}
setTimeout(() => {
this.toggleUpgradeMessage(true, isLandscape);
}, 500);
actions.setLastUpgradeCheck();
};
toggleUpgradeMessage = (show = true, isLandscape) => {
let toValue = -100;
if (show) {
if (this.isX && isLandscape) {
toValue = 45;
} else {
toValue = this.isX ? 100 : 75;
}
}
Animated.timing(this.state.top, {
toValue,
duration: 300
}).start();
};
handleDismiss = () => {
this.toggleUpgradeMessage(false);
};
handleDownload = () => {
const {downloadLink} = this.props;
const {intl} = this.context;
Linking.canOpenURL(downloadLink).then((supported) => {
if (supported) {
return Linking.openURL(downloadLink);
}
Alert.alert(
intl.formatMessage({
id: 'mobile.client_upgrade.download_error.title',
defaultMessage: 'Upgrade Error'
}),
intl.formatMessage({
id: 'mobile.client_upgrade.download_error.message',
defaultMessage: 'An error occurred while trying to open the download link.'
})
);
return false;
});
this.toggleUpgradeMessage(false);
};
handleLearnMore = () => {
const {intl} = this.context;
this.props.navigator.dismissModal({animationType: 'none'});
this.props.navigator.showModal({
screen: 'ClientUpgrade',
title: intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Upgrade App'}),
navigatorStyle: {
navBarHidden: false,
statusBarHidden: false,
statusBarHideWithNavBar: false
},
navigatorButtons: {
leftButtons: [{
id: 'close-upgrade',
icon: this.closeButton
}]
},
passProps: {
upgradeType: this.state.upgradeType
}
});
this.toggleUpgradeMessage(false);
};
render() {
if (this.state.upgradeType === UpgradeTypes.NO_UPGRADE) {
return null;
}
const {forceUpgrade, theme} = this.props;
const styles = getStyleSheet(theme);
return (
<AnimatedView
style={[styles.wrapper, {top: this.state.top}]}
>
<View style={styles.container}>
<View style={styles.message}>
<FormattedText
id='mobile.client_upgrade.listener.message'
defaultMessage='A client upgrade is available!'
style={styles.messageText}
/>
</View>
<View style={styles.bottom}>
<TouchableOpacity onPress={this.handleDownload}>
<FormattedText
style={styles.button}
id='mobile.client_upgrade.listener.upgrade_button'
defaultMessage='Upgrade'
/>
</TouchableOpacity>
<TouchableOpacity onPress={this.handleLearnMore}>
<FormattedText
style={styles.button}
id='mobile.client_upgrade.listener.learn_more_button'
defaultMessage='Learn More'
/>
</TouchableOpacity>
{!forceUpgrade &&
<TouchableOpacity onPress={this.handleDismiss}>
<FormattedText
style={styles.button}
id='mobile.client_upgrade.listener.dismiss_button'
defaultMessage='Dismiss'
/>
</TouchableOpacity>
}
</View>
</View>
</AnimatedView>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
bottom: {
flexDirection: 'row',
justifyContent: 'space-around',
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
borderTopWidth: 1
},
button: {
color: theme.linkColor,
fontSize: 13,
paddingHorizontal: 5,
paddingVertical: 5
},
container: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelBg, 0.8),
borderRadius: 5
},
message: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
messageText: {
fontSize: 16,
color: changeOpacity(theme.centerChannelColor, 0.8),
fontWeight: '600'
},
wrapper: {
position: 'absolute',
elevation: 5,
left: 30,
right: 30,
height: 75,
backgroundColor: 'white',
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderWidth: 2,
borderRadius: 5,
shadowColor: theme.centerChannelColor,
shadowOffset: {
width: 0,
height: 3
},
shadowOpacity: 0.2,
shadowRadius: 2
}
};
});

View File

@@ -1,37 +0,0 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {logError} from 'mattermost-redux/actions/errors';
import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade';
import getClientUpgrade from 'app/selectors/client_upgrade';
import {isLandscape} from 'app/selectors/device';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ClientUpgradeListener from './client_upgrade_listener';
function mapStateToProps(state) {
const {currentVersion, downloadLink, forceUpgrade, latestVersion, minVersion} = getClientUpgrade(state);
return {
currentVersion,
downloadLink,
forceUpgrade,
isLandscape: isLandscape(state),
lastUpgradeCheck: state.views.clientUpgrade.lastUpdateCheck,
latestVersion,
minVersion,
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
logError,
setLastUpgradeCheck
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ClientUpgradeListener);

View File

@@ -3,7 +3,7 @@
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
@@ -15,7 +15,8 @@ function makeMapStateToProps() {
return (state, ownProps) => {
return {
theme: getTheme(state),
channel: getChannel(state, ownProps)
channel: getChannel(state, ownProps),
...ownProps
};
};
}

View File

@@ -3,7 +3,9 @@
import {connect} from 'react-redux';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import UserListRow from './user_list_row';
@@ -12,7 +14,8 @@ function mapStateToProps(state, ownProps) {
return {
theme: getTheme(state),
user: getUser(state, ownProps.id),
teammateNameDisplay: getTeammateNameDisplaySetting(state)
teammateNameDisplay: getTeammateNameDisplaySetting(state),
...ownProps
};
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {Keyboard, Dimensions} from 'react-native';
import PropTypes from 'prop-types';
import BaseDrawer from 'react-native-drawer';
@@ -9,49 +8,20 @@ import BaseDrawer from 'react-native-drawer';
export default class Drawer extends BaseDrawer {
static propTypes = {
...BaseDrawer.propTypes,
onRequestClose: PropTypes.func.isRequired,
bottomPanOffset: PropTypes.number,
topPanOffset: PropTypes.number
onRequestClose: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.keyboardHeight = 0;
}
componentDidMount() {
Keyboard.addListener('keyboardDidShow', this.keyboardDidShow);
Keyboard.addListener('keyboardDidHide', this.keyboardDidHide);
}
componentWillUnmount() {
Keyboard.removeListener('keyboardDidShow', this.keyboardDidShow);
Keyboard.removeListener('keyboardDidHide', this.keyboardDidHide);
}
// To fix the android onLayout issue give this a value of 100% as it does not need another one
getMainHeight = () => '100%';
keyboardDidShow = (e) => {
this.keyboardHeight = e.endCoordinates.height;
};
keyboardDidHide = () => {
this.keyboardHeight = 0;
};
isOpened = () => {
return this._open; //eslint-disable-line no-underscore-dangle
};
processTapGestures = () => {
// Note that we explicitly don't support tap to open or double tap because I didn't copy them over
if (this._activeTween) { //eslint-disable-line no-underscore-dangle
if (this._activeTween) { // eslint-disable-line no-underscore-dangle
return false;
}
if (this.props.tapToClose && this._open) { //eslint-disable-line no-underscore-dangle
if (this.props.tapToClose && this._open) { // eslint-disable-line no-underscore-dangle
this.props.onRequestClose();
return true;
@@ -59,41 +29,4 @@ export default class Drawer extends BaseDrawer {
return false;
};
testPanResponderMask = (e) => {
if (this.props.disabled) {
return false;
}
// Disable if parent or child drawer exist and are open
if (this.context.drawer && this.context.drawer._open) { //eslint-disable-line no-underscore-dangle
return false;
}
if (this._childDrawer && this._childDrawer._open) { //eslint-disable-line no-underscore-dangle
return false;
}
const topPanOffset = this.props.topPanOffset || 0;
const bottomPanOffset = this.props.bottomPanOffset || 0;
const height = Dimensions.get('window').height;
if ((this.props.topPanOffset && e.nativeEvent.pageY < topPanOffset) ||
(this.props.bottomPanOffset && e.nativeEvent.pageY > (height - (bottomPanOffset + this.keyboardHeight)))) {
return false;
}
const pos0 = this.isLeftOrRightSide() ? e.nativeEvent.pageX : e.nativeEvent.pageY;
const deltaOpen = this.isLeftOrTopSide() ? this.getDeviceLength() - pos0 : pos0;
const deltaClose = this.isLeftOrTopSide() ? pos0 : this.getDeviceLength() - pos0;
if (this._open && deltaOpen > this.getOpenMask()) { //eslint-disable-line no-underscore-dangle
return false;
}
if (!this._open && deltaClose > this.getClosedMask()) { //eslint-disable-line no-underscore-dangle
return false;
}
return true;
};
}

View File

@@ -1,423 +0,0 @@
// 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 {
Platform,
TouchableWithoutFeedback,
View,
Text,
findNodeHandle
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {General} from 'mattermost-redux/constants';
import {getShortenedURL} from 'app/utils/url';
export default class EditChannelInfo extends PureComponent {
static propTypes = {
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
deviceWidth: PropTypes.number.isRequired,
deviceHeight: PropTypes.number.isRequired,
channelType: PropTypes.string,
enableRightButton: PropTypes.func,
saving: PropTypes.bool.isRequired,
editing: PropTypes.bool,
error: PropTypes.string,
displayName: PropTypes.string,
currentTeamUrl: PropTypes.string,
channelURL: PropTypes.string,
purpose: PropTypes.string,
header: PropTypes.string,
onDisplayNameChange: PropTypes.func,
onChannelURLChange: PropTypes.func,
onPurposeChange: PropTypes.func,
onHeaderChange: PropTypes.func,
oldDisplayName: PropTypes.string,
oldChannelURL: PropTypes.string,
oldHeader: PropTypes.string,
oldPurpose: PropTypes.string
};
static defaultProps = {
editing: false
};
blur = () => {
if (this.nameInput) {
this.nameInput.refs.wrappedInstance.blur();
}
// TODO: uncomment below once the channel URL field is added
// if (this.urlInput) {
// this.urlInput.refs.wrappedInstance.blur();
// }
if (this.purposeInput) {
this.purposeInput.refs.wrappedInstance.blur();
}
if (this.headerInput) {
this.headerInput.refs.wrappedInstance.blur();
}
if (this.scroll) {
this.scroll.scrollToPosition(0, 0, true);
}
};
channelNameRef = (ref) => {
this.nameInput = ref;
};
channelURLRef = (ref) => {
this.urlInput = ref;
};
channelPurposeRef = (ref) => {
this.purposeInput = ref;
};
channelHeaderRef = (ref) => {
this.headerInput = ref;
};
close = (goBack = false) => {
EventEmitter.emit('closing-create-channel', false);
if (goBack) {
this.props.navigator.pop({animated: true});
} else {
this.props.navigator.dismissModal({
animationType: 'slide-down'
});
}
};
lastTextRef = (ref) => {
this.lastText = ref;
};
canUpdate = (displayName, channelURL, purpose, header) => {
const {
oldDisplayName,
oldChannelURL,
oldPurpose,
oldHeader
} = this.props;
return displayName !== oldDisplayName || channelURL !== oldChannelURL ||
purpose !== oldPurpose || header !== oldHeader;
};
enableRightButton = (enable = false) => {
this.props.enableRightButton(enable);
};
onDisplayNameChangeText = (displayName) => {
const {editing, onDisplayNameChange} = this.props;
onDisplayNameChange(displayName);
if (editing) {
const {channelURL, purpose, header} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
return;
}
const displayNameExists = displayName && displayName.length >= 2;
this.props.enableRightButton(displayNameExists);
};
onDisplayURLChangeText = (channelURL) => {
const {editing, onChannelURLChange} = this.props;
onChannelURLChange(channelURL);
if (editing) {
const {displayName, purpose, header} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
}
};
onPurposeChangeText = (purpose) => {
const {editing, onPurposeChange} = this.props;
onPurposeChange(purpose);
if (editing) {
const {displayName, channelURL, header} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
}
};
onHeaderChangeText = (header) => {
const {editing, onHeaderChange} = this.props;
onHeaderChange(header);
if (editing) {
const {displayName, channelURL, purpose} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
}
};
scrollRef = (ref) => {
this.scroll = ref;
};
scrollToEnd = () => {
if (this.scroll && this.lastText) {
this.scroll.scrollToFocusedInput(findNodeHandle(this.lastText));
}
};
render() {
const {
theme,
editing,
channelType,
currentTeamUrl,
deviceWidth,
deviceHeight,
displayName,
channelURL,
header,
purpose
} = this.props;
const {error, saving} = this.props;
const fullUrl = currentTeamUrl + '/channels';
const shortUrl = getShortenedURL(fullUrl, 35);
const style = getStyleSheet(theme);
const displayHeaderOnly = channelType === General.DM_CHANNEL ||
channelType === General.GM_CHANNEL;
if (saving) {
return (
<View style={style.container}>
<StatusBar/>
<Loading/>
</View>
);
}
let displayError;
if (error) {
displayError = (
<View style={[style.errorContainer, {deviceWidth}]}>
<View style={style.errorWrapper}>
<ErrorText error={error}/>
</View>
</View>
);
}
return (
<View style={style.container}>
<StatusBar/>
<KeyboardAwareScrollView
ref={this.scrollRef}
style={style.container}
>
{displayError}
<TouchableWithoutFeedback onPress={this.blur}>
<View style={[style.scrollView, {height: deviceHeight + (Platform.OS === 'android' ? 200 : 0)}]}>
{!displayHeaderOnly && (
<View>
<View>
<FormattedText
style={style.title}
id='channel_modal.name'
defaultMessage='Name'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelNameRef}
value={displayName}
onChangeText={this.onDisplayNameChangeText}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.nameEx', defaultMessage: 'E.g.: "Bugs", "Marketing", "客户支持"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
underlineColorAndroid='transparent'
/>
</View>
</View>
)}
{/*TODO: Hide channel url field until it's added to CreateChannel */}
{false && editing && !displayHeaderOnly && (
<View>
<View style={style.titleContainer30}>
<FormattedText
style={style.title}
id='rename_channel.url'
defaultMessage='URL'
/>
<Text style={style.optional}>
{shortUrl}
</Text>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelURLRef}
value={channelURL}
onChangeText={this.onDisplayURLChangeText}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'rename_channel.handleHolder', defaultMessage: 'lowercase alphanumeric characters'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
underlineColorAndroid='transparent'
/>
</View>
</View>
)}
{!displayHeaderOnly && (
<View>
<View style={style.titleContainer30}>
<FormattedText
style={style.title}
id='channel_modal.purpose'
defaultMessage='Purpose'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelPurposeRef}
value={purpose}
onChangeText={this.onPurposeChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.purposeEx', defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
blurOnSubmit={false}
textAlignVertical='top'
underlineColorAndroid='transparent'
/>
</View>
<View>
<FormattedText
style={style.helpText}
id='channel_modal.descriptionHelp'
defaultMessage='Describe how this channel should be used.'
/>
</View>
</View>
)}
<View style={style.titleContainer15}>
<FormattedText
style={style.title}
id='channel_modal.header'
defaultMessage='Header'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelHeaderRef}
value={header}
onChangeText={this.onHeaderChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.headerEx', defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
blurOnSubmit={false}
onFocus={this.scrollToEnd}
textAlignVertical='top'
underlineColorAndroid='transparent'
/>
</View>
<View ref={this.lastTextRef}>
<FormattedText
style={style.helpText}
id='channel_modal.headerHelp'
defaultMessage={'Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com).'}
/>
</View>
</View>
</TouchableWithoutFeedback>
</KeyboardAwareScrollView>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
paddingTop: 10
},
errorContainer: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
},
errorWrapper: {
justifyContent: 'center',
alignItems: 'center'
},
inputContainer: {
marginTop: 10,
backgroundColor: '#fff'
},
input: {
color: '#333',
fontSize: 14,
height: 40,
paddingHorizontal: 15
},
titleContainer30: {
flexDirection: 'row',
marginTop: 30
},
titleContainer15: {
flexDirection: 'row',
marginTop: 15
},
title: {
fontSize: 14,
color: theme.centerChannelColor,
marginLeft: 15
},
optional: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 14,
marginLeft: 5
},
helpText: {
fontSize: 14,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginTop: 10,
marginHorizontal: 15
}
};
});

View File

@@ -138,7 +138,7 @@ export default class Emoji extends React.PureComponent {
let marginTop = 0;
if (fontSize) {
// Center the image vertically on iOS (does nothing on Android)
marginTop = (height - 16) / 2;
marginTop = (height - fontSize) / 2;
// hack to get the vertical alignment looking better
if (fontSize === 17) {

View File

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

View File

@@ -3,40 +3,28 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {injectIntl, intlShape} from 'react-intl';
import {
FlatList,
KeyboardAvoidingView,
Platform,
SectionList,
Text,
TouchableOpacity,
View
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
import sectionListGetItemLayout from 'react-native-section-list-get-item-layout';
import Emoji from 'app/components/emoji';
import FormattedText from 'app/components/formatted_text';
import SafeAreaView from 'app/components/safe_area_view';
import SearchBar from 'app/components/search_bar';
import {emptyFunction} from 'app/utils/general';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import EmojiPickerRow from './emoji_picker_row';
const EMOJI_SIZE = 30;
const EMOJI_GUTTER = 7.5;
const SECTION_MARGIN = 15;
const SECTION_HEADER_HEIGHT = 28;
export default class EmojiPicker extends PureComponent {
class EmojiPicker extends PureComponent {
static propTypes = {
emojisByName: PropTypes.array.isRequired,
emojisBySection: PropTypes.array.isRequired,
emojis: PropTypes.array.isRequired,
deviceWidth: PropTypes.number.isRequired,
isLandscape: PropTypes.bool.isRequired,
intl: intlShape.isRequired,
onEmojiPress: PropTypes.func,
theme: PropTypes.object.isRequired
};
@@ -45,115 +33,46 @@ export default class EmojiPicker extends PureComponent {
onEmojiPress: emptyFunction
};
static contextTypes = {
intl: intlShape.isRequired
leftButton = {
id: 'close-edit-post'
};
constructor(props) {
super(props);
this.sectionListGetItemLayout = sectionListGetItemLayout({
getItemHeight: () => {
return EMOJI_SIZE + (EMOJI_GUTTER * 2);
},
getSectionHeaderHeight: () => SECTION_HEADER_HEIGHT
});
const emojis = this.renderableEmojis(props.emojisBySection, props.deviceWidth);
const emojiSectionIndexByOffset = this.measureEmojiSections(emojis);
this.isX = DeviceInfo.getModel() === 'iPhone X';
this.state = {
emojis,
emojiSectionIndexByOffset,
filteredEmojis: [],
emojis: props.emojis,
searchTerm: '',
currentSectionIndex: 0
width: props.deviceWidth - (SECTION_MARGIN * 2)
};
}
componentWillReceiveProps(nextProps) {
if (this.props.deviceWidth !== nextProps.deviceWidth) {
if (nextProps.deviceWidth !== this.props.deviceWidth) {
this.setState({
emojis: this.renderableEmojis(this.props.emojisBySection, nextProps.deviceWidth)
width: nextProps.deviceWidth - (SECTION_MARGIN * 2)
});
if (this.refs.search_bar) {
this.refs.search_bar.blur();
}
}
}
renderableEmojis = (emojis, deviceWidth) => {
const numberOfColumns = this.getNumberOfColumns(deviceWidth);
const nextEmojis = emojis.map((section) => {
const data = [];
let row = {
key: `${section.key}-0`,
items: []
};
section.data.forEach((emoji, index) => {
if (index % numberOfColumns === 0 && index !== 0) {
data.push(row);
row = {
key: `${section.key}-${index}`,
items: []
};
}
row.items.push(emoji);
});
if (row.items.length) {
if (row.items.length < numberOfColumns) {
// push some empty items to make sure flexbox can justfiy content correctly
const emptyEmojis = new Array(numberOfColumns - row.items.length);
row.items.push(...emptyEmojis);
}
data.push(row);
}
return {
...section,
data
};
});
return nextEmojis;
};
measureEmojiSections = (emojiSections) => {
let lastOffset = 0;
return emojiSections.map((section) => {
const start = lastOffset;
const nextOffset = (section.data.length * (EMOJI_SIZE + (EMOJI_GUTTER * 2))) + SECTION_HEADER_HEIGHT;
lastOffset += nextOffset;
return start;
});
};
changeSearchTerm = (text) => {
this.setState({
searchTerm: text
});
clearTimeout(this.searchTermTimeout);
const timeout = text ? 100 : 0;
const timeout = text ? 350 : 0;
this.searchTermTimeout = setTimeout(() => {
const filteredEmojis = this.searchEmojis(text);
const emojis = this.searchEmojis(text);
this.setState({
filteredEmojis
emojis
});
}, timeout);
};
cancelSearch = () => {
this.setState({
filteredEmojis: [],
emojis: this.props.emojis,
searchTerm: ''
});
};
@@ -163,105 +82,38 @@ export default class EmojiPicker extends PureComponent {
};
searchEmojis = (searchTerm) => {
const {emojisByName} = this.props;
const {emojis} = this.props;
const searchTermLowerCase = searchTerm.toLowerCase();
if (!searchTerm) {
return [];
return emojis;
}
const startsWith = [];
const includes = [];
emojisByName.forEach((emoji) => {
if (emoji.startsWith(searchTermLowerCase)) {
startsWith.push(emoji);
} else if (emoji.includes(searchTermLowerCase)) {
includes.push(emoji);
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 [...startsWith.sort(), ...includes.sort()];
};
getNumberOfColumns = (deviceWidth) => {
return Math.floor(Number(((deviceWidth - (SECTION_MARGIN * 2)) / (EMOJI_SIZE + (EMOJI_GUTTER * 2)))));
};
renderItem = ({item}) => {
return (
<EmojiPickerRow
key={item.key}
emojiGutter={EMOJI_GUTTER}
emojiSize={EMOJI_SIZE}
items={item.items}
onEmojiPress={this.props.onEmojiPress}
/>
);
};
flatListKeyExtractor = (item) => item;
flatListRenderItem = ({item}) => {
const style = getStyleSheetFromTheme(this.props.theme);
return (
<TouchableOpacity
onPress={() => this.props.onEmojiPress(item)}
style={style.flatListRow}
>
<View style={style.flatListEmoji}>
<Emoji
emojiName={item}
size={20}
/>
</View>
<Text style={style.flatListEmojiName}>{`:${item}:`}</Text>
</TouchableOpacity>
);
};
onScroll = (e) => {
if (this.state.jumpToSection) {
return;
}
clearTimeout(this.setIndexTimeout);
const {contentOffset} = e.nativeEvent;
let nextIndex = this.state.emojiSectionIndexByOffset.findIndex((offset) => contentOffset.y <= offset);
if (nextIndex === -1) {
nextIndex = this.state.emojiSectionIndexByOffset.length - 1;
} else if (nextIndex !== 0) {
nextIndex -= 1;
}
if (nextIndex !== this.state.currentSectionIndex) {
this.setState({
currentSectionIndex: nextIndex
});
}
};
onMomentumScrollEnd = () => {
if (this.state.jumpToSection) {
this.setState({
jumpToSection: false
});
}
};
scrollToSection = (index) => {
this.setState({
jumpToSection: true,
currentSectionIndex: index
}, () => {
this.sectionList.scrollToLocation({
sectionIndex: index,
itemIndex: 0,
viewOffset: 25
});
});
return nextEmojis;
};
renderSectionHeader = ({section}) => {
@@ -269,10 +121,7 @@ export default class EmojiPicker extends PureComponent {
const styles = getStyleSheetFromTheme(theme);
return (
<View
style={styles.sectionTitleContainer}
key={section.title}
>
<View key={section.title}>
<FormattedText
style={styles.sectionTitle}
id={section.id}
@@ -282,143 +131,111 @@ export default class EmojiPicker extends PureComponent {
);
};
renderSectionIcons = () => {
renderEmojis = (emojis, index) => {
const {theme} = this.props;
const styles = getStyleSheetFromTheme(theme);
return this.state.emojis.map((section, index) => {
const onPress = () => this.scrollToSection(index);
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={section.key}
onPress={onPress}
style={styles.sectionIconContainer}
>
<FontAwesomeIcon
name={section.icon}
size={17}
style={[styles.sectionIcon, (index === this.state.currentSectionIndex && styles.sectionIconHighlight)]}
/>
</TouchableOpacity>
);
});
return (
<TouchableOpacity
key={emoji.name}
style={style}
onPress={() => {
this.props.onEmojiPress(emoji.name);
}}
>
<Emoji
emojiName={emoji.name}
size={EMOJI_SIZE}
/>
</TouchableOpacity>
);
})}
</View>
);
};
attachSectionList = (c) => {
this.sectionList = c;
renderItem = ({item}) => {
const {theme} = this.props;
const styles = getStyleSheetFromTheme(theme);
const numColumns = Number((this.state.width / (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 {deviceWidth, isLandscape, theme} = this.props;
const {emojis, filteredEmojis, searchTerm} = this.state;
const {intl} = this.context;
const {intl, theme} = this.props;
const {emojis, searchTerm} = this.state;
const {formatMessage} = intl;
const styles = getStyleSheetFromTheme(theme);
let listComponent;
if (searchTerm) {
listComponent = (
<FlatList
keyboardShouldPersistTaps='always'
style={styles.flatList}
data={filteredEmojis}
keyExtractor={this.flatListKeyExtractor}
renderItem={this.flatListRenderItem}
pageSize={10}
initialListSize={10}
/>
);
} else {
listComponent = (
<SectionList
ref={this.attachSectionList}
showsVerticalScrollIndicator={false}
style={[styles.listView, {width: deviceWidth - (SECTION_MARGIN * 2)}]}
sections={emojis}
renderSectionHeader={this.renderSectionHeader}
renderItem={this.renderItem}
keyboardShouldPersistTaps='always'
getItemLayout={this.sectionListGetItemLayout}
removeClippedSubviews={true}
onScroll={this.onScroll}
onMomentumScrollEnd={this.onMomentumScrollEnd}
pageSize={30}
/>
);
}
let keyboardOffset = 64;
if (Platform.OS === 'android') {
keyboardOffset = -200;
} else if (this.isX) {
keyboardOffset = isLandscape ? 35 : 107;
} else if (isLandscape) {
keyboardOffset = 52;
}
return (
<SafeAreaView excludeHeader={true}>
<KeyboardAvoidingView
behavior='padding'
style={{flex: 1}}
keyboardVerticalOffset={keyboardOffset}
>
<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}>
{listComponent}
{!searchTerm &&
<View style={styles.bottomContentWrapper}>
<View style={styles.bottomContent}>
{this.renderSectionIcons()}
</View>
</View>
}
</View>
</KeyboardAvoidingView>
</SafeAreaView>
<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 {
bottomContent: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopColor: changeOpacity(theme.centerChannelColor, 0.3),
borderTopWidth: 1,
flexDirection: 'row',
justifyContent: 'space-between'
},
bottomContentWrapper: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 35,
width: '100%',
backgroundColor: 'white'
},
columnStyle: {
alignSelf: 'stretch',
flexDirection: 'row',
@@ -443,34 +260,8 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
emojiRight: {
marginRight: 0
},
flatList: {
flex: 1,
backgroundColor: theme.centerChannelBg,
alignSelf: 'stretch'
},
flatListEmoji: {
marginRight: 5
},
flatListEmojiName: {
fontSize: 13,
color: theme.centerChannelColor
},
flatListRow: {
height: 40,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
listView: {
backgroundColor: theme.centerChannelBg,
marginBottom: 35
backgroundColor: theme.centerChannelBg
},
searchBar: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
@@ -479,30 +270,16 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
section: {
alignItems: 'center'
},
sectionIcon: {
color: changeOpacity(theme.centerChannelColor, 0.3)
},
sectionIconContainer: {
flex: 1,
height: 35,
alignItems: 'center',
justifyContent: 'center'
},
sectionIconHighlight: {
color: theme.centerChannelColor
},
sectionTitle: {
color: changeOpacity(theme.centerChannelColor, 0.2),
fontSize: 15,
fontWeight: '700'
},
sectionTitleContainer: {
height: SECTION_HEADER_HEIGHT,
justifyContent: 'center',
backgroundColor: theme.centerChannelBg
fontWeight: '700',
paddingVertical: 5
},
wrapper: {
flex: 1
}
};
});
export default injectIntl(EmojiPicker);

View File

@@ -1,95 +0,0 @@
// 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 {
StyleSheet,
TouchableOpacity,
View
} from 'react-native';
import Emoji from 'app/components/emoji';
export default class EmojiPickerRow extends Component {
static propTypes = {
emojiGutter: PropTypes.number.isRequired,
emojiSize: PropTypes.number.isRequired,
items: PropTypes.array.isRequired,
onEmojiPress: PropTypes.func.isRequired
}
shouldComponentUpdate(nextProps) {
return this.props.items.length !== nextProps.items.length;
}
renderEmojis = (emoji, index, emojis) => {
const {emojiGutter, emojiSize} = this.props;
const style = [
styles.emoji,
{
width: emojiSize,
height: emojiSize,
marginHorizontal: emojiGutter
}
];
if (index === 0) {
style.push(styles.emojiLeft);
} else if (index === emojis.length - 1) {
style.push(styles.emojiRight);
}
if (!emoji) {
return (
<View
key={index}
style={style}
/>
);
}
return (
<TouchableOpacity
key={emoji.name}
style={style}
onPress={() => {
this.props.onEmojiPress(emoji.name);
}}
>
<Emoji
emojiName={emoji.name}
size={emojiSize}
/>
</TouchableOpacity>
);
}
render() {
const {emojiGutter, items} = this.props;
return (
<View style={[styles.columnStyle, {marginVertical: emojiGutter}]}>
{items.map(this.renderEmojis)}
</View>
);
}
}
const styles = StyleSheet.create({
columnStyle: {
alignSelf: 'stretch',
flexDirection: 'row',
justifyContent: 'space-between'
},
emoji: {
alignItems: 'center',
justifyContent: 'center'
},
emojiLeft: {
marginLeft: 0
},
emojiRight: {
marginRight: 0
}
});

View File

@@ -6,62 +6,48 @@ import {createSelector} from 'reselect';
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
import {getDimensions, isLandscape} from 'app/selectors/device';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {CategoryNames, Emojis, EmojiIndicesByAlias, EmojiIndicesByCategory} from 'app/utils/emojis';
import {getDimensions} from 'app/selectors/device';
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',
icon: 'futbol-o'
defaultMessage: 'ACTIVITY'
},
custom: {
id: 'mobile.emoji_picker.custom',
defaultMessage: 'CUSTOM',
icon: 'at'
defaultMessage: 'CUSTOM'
},
flags: {
id: 'mobile.emoji_picker.flags',
defaultMessage: 'FLAGS',
icon: 'flag-o'
defaultMessage: 'FLAGS'
},
foods: {
id: 'mobile.emoji_picker.foods',
defaultMessage: 'FOODS',
icon: 'cutlery'
defaultMessage: 'FOODS'
},
nature: {
id: 'mobile.emoji_picker.nature',
defaultMessage: 'NATURE',
icon: 'leaf'
defaultMessage: 'NATURE'
},
objects: {
id: 'mobile.emoji_picker.objects',
defaultMessage: 'OBJECTS',
icon: 'lightbulb-o'
defaultMessage: 'OBJECTS'
},
people: {
id: 'mobile.emoji_picker.people',
defaultMessage: 'PEOPLE',
icon: 'smile-o'
defaultMessage: 'PEOPLE'
},
places: {
id: 'mobile.emoji_picker.places',
defaultMessage: 'PLACES',
icon: 'plane'
},
recent: {
id: 'mobile.emoji_picker.recent',
defaultMessage: 'RECENTLY USED',
icon: 'clock-o'
defaultMessage: 'PLACES'
},
symbols: {
id: 'mobile.emoji_picker.symbols',
defaultMessage: 'SYMBOLS',
icon: 'heart-o'
defaultMessage: 'SYMBOLS'
}
};
@@ -75,24 +61,28 @@ function fillEmoji(indice) {
const getEmojisBySection = createSelector(
getCustomEmojisByName,
(state) => state.views.recentEmojis,
(customEmojis, recentEmojis) => {
(customEmojis) => {
const emoticons = CategoryNames.filter((name) => name !== 'custom').map((category) => {
const items = EmojiIndicesByCategory.get(category).map(fillEmoji);
const section = {
...categoryToI18n[category],
key: category,
data: items
data: [{
key: `${category}-emojis`,
items: EmojiIndicesByCategory.get(category).map(fillEmoji)
}]
};
return section;
});
const customEmojiItems = [];
const customEmojiData = {
key: 'custom-emojis',
title: 'CUSTOM',
items: []
};
for (const [key] of customEmojis) {
customEmojiItems.push({
customEmojiData.items.push({
name: key
});
}
@@ -100,45 +90,20 @@ const getEmojisBySection = createSelector(
emoticons.push({
...categoryToI18n.custom,
key: 'custom',
data: customEmojiItems
data: [customEmojiData]
});
if (recentEmojis.length) {
const items = recentEmojis.map((emoji) => ({name: emoji}));
emoticons.unshift({
...categoryToI18n.recent,
key: 'recent',
data: items
});
}
return emoticons;
}
);
const getEmojisByName = createSelector(
getCustomEmojisByName,
(customEmojis) => {
const emoticons = [];
for (const [key] of [...EmojiIndicesByAlias.entries(), ...customEmojis.entries()]) {
emoticons.push(key);
}
return emoticons;
}
);
function mapStateToProps(state) {
const emojisBySection = getEmojisBySection(state);
const emojisByName = getEmojisByName(state);
const emojis = getEmojisBySection(state);
const {deviceWidth} = getDimensions(state);
return {
emojisByName,
emojisBySection,
emojis,
deviceWidth,
isLandscape: isLandscape(state),
theme: getTheme(state)
};
}

View File

@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
import {Text} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {GlobalStyles} from 'app/styles';
import {makeStyleSheetFromTheme} from 'app/utils/theme';

View File

@@ -12,7 +12,6 @@ import {
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import * as Utils from 'mattermost-redux/utils/file_utils.js';
import FileAttachmentDocument, {SUPPORTED_DOCS_FORMAT} from './file_attachment_document';
import FileAttachmentIcon from './file_attachment_icon';
import FileAttachmentImage from './file_attachment_image';
@@ -29,11 +28,7 @@ export default class FileAttachment extends PureComponent {
static defaultProps = {
onInfoPress: () => true,
onPreviewPress: () => true
};
handlePreviewPress = () => {
this.props.onPreviewPress(this.props.file);
};
}
renderFileInfo() {
const {file, theme} = this.props;
@@ -60,50 +55,40 @@ export default class FileAttachment extends PureComponent {
);
}
render() {
const {file, onInfoPress, theme} = this.props;
const style = getStyleSheet(theme);
handlePreviewPress = () => {
this.props.onPreviewPress(this.props.file);
}
let mime = file.mime_type;
if (mime && mime.includes(';')) {
mime = mime.split(';')[0];
}
render() {
const {file, theme} = this.props;
const style = getStyleSheet(theme);
let fileAttachmentComponent;
if (file.has_preview_image || file.loading || file.mime_type === 'image/gif') {
fileAttachmentComponent = (
<TouchableOpacity onPress={this.handlePreviewPress}>
<FileAttachmentImage
addFileToFetchCache={this.props.addFileToFetchCache}
fetchCache={this.props.fetchCache}
file={file}
theme={theme}
/>
</TouchableOpacity>
);
} else if (SUPPORTED_DOCS_FORMAT.includes(mime)) {
fileAttachmentComponent = (
<FileAttachmentDocument
<FileAttachmentImage
addFileToFetchCache={this.props.addFileToFetchCache}
fetchCache={this.props.fetchCache}
file={file}
theme={theme}
/>
);
} else {
fileAttachmentComponent = (
<TouchableOpacity onPress={this.handlePreviewPress}>
<FileAttachmentIcon
file={file}
theme={theme}
/>
</TouchableOpacity>
<FileAttachmentIcon
file={file}
theme={theme}
/>
);
}
return (
<View style={style.fileWrapper}>
{fileAttachmentComponent}
<TouchableOpacity onPress={this.handlePreviewPress}>
{fileAttachmentComponent}
</TouchableOpacity>
<TouchableOpacity
onPress={onInfoPress}
onPress={this.props.onInfoPress}
style={style.fileInfoContainer}
>
{this.renderFileInfo()}
@@ -148,21 +133,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
marginTop: 10,
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2)
},
circularProgress: {
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center'
},
circularProgressContent: {
position: 'absolute',
height: '100%',
width: '100%',
top: 0,
left: 0,
alignItems: 'center',
justifyContent: 'center'
}
};
});

View File

@@ -1,320 +0,0 @@
// 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 {
Alert,
Platform,
StyleSheet,
TouchableOpacity,
View
} from 'react-native';
import OpenFile from 'react-native-doc-viewer';
import RNFetchBlob from 'react-native-fetch-blob';
import {AnimatedCircularProgress} from 'react-native-circular-progress';
import {intlShape} from 'react-intl';
import {changeOpacity} from 'app/utils/theme';
import {getFileUrl} from 'mattermost-redux/utils/file_utils.js';
import {DeviceTypes} from 'app/constants/';
import FileAttachmentIcon from './file_attachment_icon';
const {DOCUMENTS_PATH} = DeviceTypes;
export const SUPPORTED_DOCS_FORMAT = [
'application/pdf',
'application/msword',
'application/rtf',
'application/vnd.ms-excel',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/xml',
'text/csv'
];
export default class FileAttachmentDocument extends PureComponent {
static propTypes = {
iconHeight: PropTypes.number,
iconWidth: PropTypes.number,
file: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
wrapperHeight: PropTypes.number,
wrapperWidth: PropTypes.number
};
static defaultProps = {
iconHeight: 65,
iconWidth: 65,
wrapperHeight: 100,
wrapperWidth: 100
};
static contextTypes = {
intl: intlShape
};
state = {
didCancel: false,
downloading: false,
progress: 0
};
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
}
cancelDownload = () => {
if (this.mounted) {
this.setState({didCancel: true});
}
if (this.downloadTask) {
this.downloadTask.cancel();
}
};
downloadAndPreviewFile = async (file) => {
const path = `${DOCUMENTS_PATH}/${file.name}`;
this.setState({didCancel: false});
try {
const isDir = await RNFetchBlob.fs.isDir(DOCUMENTS_PATH);
if (!isDir) {
try {
await RNFetchBlob.fs.mkdir(DOCUMENTS_PATH);
} catch (error) {
this.showDownloadFailedAlert();
return;
}
}
const options = {
session: file.id,
timeout: 10000,
indicator: true,
overwrite: true,
path
};
const exist = await RNFetchBlob.fs.exists(path);
if (exist) {
this.openDocument(file, 0);
} else {
this.setState({downloading: true});
this.downloadTask = RNFetchBlob.config(options).fetch('GET', getFileUrl(file.id)).
progress((received, total) => {
const progress = (received / total) * 100;
if (this.mounted) {
this.setState({progress});
}
});
await this.downloadTask;
if (this.mounted) {
this.setState({
progress: 100
}, () => {
// need to wait a bit for the progress circle UI to update to the give progress
this.openDocument(file);
});
}
}
} catch (error) {
RNFetchBlob.fs.unlink(path);
if (this.mounted) {
this.setState({downloading: false, progress: 0});
if (error.message !== 'cancelled') {
const {intl} = this.context;
Alert.alert(
intl.formatMessage({
id: 'mobile.downloader.failed_title',
defaultMessage: 'Download failed'
}),
intl.formatMessage({
id: 'mobile.downloader.failed_description',
defaultMessage: 'An error occurred while downloading the file. Please check your internet connection and try again.\n'
}),
[{
text: intl.formatMessage({
id: 'mobile.server_upgrade.button',
defaultMessage: 'OK'
})
}]
);
}
}
}
};
handlePreviewPress = async () => {
const {file} = this.props;
const {downloading, progress} = this.state;
if (downloading && progress < 100) {
this.cancelDownload();
} else if (downloading) {
this.resetViewState();
} else {
this.downloadAndPreviewFile(file);
}
};
openDocument = (file, delay = 2000) => {
// The animation for the progress circle takes about 2 seconds to finish
// therefore we are delaying the opening of the document to have the UI
// shown nicely and smooth
setTimeout(() => {
if (!this.state.didCancel && this.mounted) {
const prefix = Platform.OS === 'android' ? 'file:/' : '';
const path = `${DOCUMENTS_PATH}/${file.name}`;
OpenFile.openDoc([{
url: `${prefix}${path}`,
fileName: file.name,
fileType: file.extension,
cache: false
}], (error) => {
if (error) {
const {intl} = this.context;
Alert.alert(
intl.formatMessage({
id: 'mobile.document_preview.failed_title',
defaultMessage: 'Open Document failed'
}),
intl.formatMessage({
id: 'mobile.document_preview.failed_description',
defaultMessage: 'An error occurred while opening the document. Please make sure you have a {fileType} viewer installed and try again.\n'
}, {
fileType: file.extension.toUpperCase()
}),
[{
text: intl.formatMessage({
id: 'mobile.server_upgrade.button',
defaultMessage: 'OK'
})
}]
);
RNFetchBlob.fs.unlink(path);
}
this.setState({downloading: false, progress: 0});
});
}
}, delay);
};
resetViewState = () => {
if (this.mounted) {
this.setState({
progress: 0,
didCancel: true
}, () => {
// need to wait a bit for the progress circle UI to update to the give progress
setTimeout(() => this.setState({downloading: false}), 2000);
});
}
};
renderProgress = () => {
const {iconHeight, iconWidth, file, theme, wrapperWidth} = this.props;
return (
<View style={[style.circularProgressContent, {width: wrapperWidth}]}>
<FileAttachmentIcon
file={file}
iconHeight={iconHeight}
iconWidth={iconWidth}
theme={theme}
wrapperHeight={iconHeight}
wrapperWidth={iconWidth}
/>
</View>
);
};
showDownloadFailedAlert = () => {
const {intl} = this.context;
Alert.alert(
intl.formatMessage({
id: 'mobile.downloader.failed_title',
defaultMessage: 'Download failed'
}),
intl.formatMessage({
id: 'mobile.downloader.failed_description',
defaultMessage: 'An error occurred while downloading the file. Please check your internet connection and try again.\n'
}),
[{
text: intl.formatMessage({
id: 'mobile.server_upgrade.button',
defaultMessage: 'OK'
}),
onPress: () => this.downloadDidCancel()
}]
);
};
render() {
const {iconHeight, iconWidth, file, theme, wrapperHeight, wrapperWidth} = this.props;
const {downloading, progress} = this.state;
let fileAttachmentComponent;
if (downloading) {
fileAttachmentComponent = (
<AnimatedCircularProgress
size={wrapperHeight}
fill={progress}
width={4}
backgroundColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColor={theme.linkColor}
rotation={0}
style={style.circularProgress}
>
{this.renderProgress}
</AnimatedCircularProgress>
);
} else {
fileAttachmentComponent = (
<FileAttachmentIcon
file={file}
theme={theme}
iconHeight={iconHeight}
iconWidth={iconWidth}
wrapperHeight={wrapperHeight}
wrapperWidth={wrapperWidth}
/>
);
}
return (
<TouchableOpacity onPress={this.handlePreviewPress}>
{fileAttachmentComponent}
</TouchableOpacity>
);
}
}
const style = StyleSheet.create({
circularProgress: {
alignItems: 'center',
height: '100%',
justifyContent: 'center',
width: '100%'
},
circularProgressContent: {
alignItems: 'center',
height: '100%',
justifyContent: 'center',
left: 0,
position: 'absolute',
top: 0
}
});

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