forked from Ivasoft/mattermost-mobile
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93265b3de0 | ||
|
|
0d70372a3c | ||
|
|
72087391dc | ||
|
|
9c89fe2907 | ||
|
|
9684328123 | ||
|
|
c3ef5e6f38 | ||
|
|
e36f63c84d | ||
|
|
ce05b9c98b | ||
|
|
388294a124 | ||
|
|
1a12abfe50 | ||
|
|
0210d6e1eb | ||
|
|
49bcf185e6 | ||
|
|
61ecf7d159 | ||
|
|
ead5f2860f | ||
|
|
09ac903630 | ||
|
|
a471379cb2 | ||
|
|
eaf128b2a0 | ||
|
|
96f5cd2c11 | ||
|
|
6b23c230ed | ||
|
|
63a3e4eb89 |
48
.flowconfig
48
.flowconfig
@@ -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
27
.gitignore
vendored
@@ -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
|
||||
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -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
|
||||
|
||||
27
Makefile
27
Makefile
@@ -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,7 +82,7 @@ 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
|
||||
|
||||
@@ -121,25 +121,10 @@ post-install:
|
||||
@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
|
||||
|
||||
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; \
|
||||
@@ -174,7 +159,7 @@ do-build-ios:
|
||||
@cd fastlane && BABEL_ENV=production 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), )
|
||||
@@ -197,7 +182,7 @@ do-build-android:
|
||||
@echo "Building android $(android_target) app"
|
||||
@cd fastlane && BABEL_ENV=production 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 +198,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:
|
||||
@:
|
||||
|
||||
837
NOTICE.txt
837
NOTICE.txt
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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 83
|
||||
versionName "1.5.3"
|
||||
versionCode 63
|
||||
versionName "1.4.0"
|
||||
multiDexEnabled true
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
@@ -163,8 +152,6 @@ 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'
|
||||
|
||||
4
android/app/proguard-rules.pro
vendored
4
android/app/proguard-rules.pro
vendored
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -56,21 +56,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,15 +197,9 @@ public class CustomPushNotification extends PushNotification {
|
||||
String summaryTitle = String.format("%s (%d)", title, numMessages);
|
||||
|
||||
Notification.InboxStyle style = new Notification.InboxStyle();
|
||||
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
|
||||
List<Bundle> list;
|
||||
if (bundleArray != null) {
|
||||
list = new ArrayList<Bundle>(bundleArray);
|
||||
} else {
|
||||
list = new ArrayList<Bundle>();
|
||||
}
|
||||
List<Bundle> list = new ArrayList<Bundle>(channelIdToNotification.get(channelId));
|
||||
|
||||
for (Bundle data : list) {
|
||||
for (Bundle data : list){
|
||||
String msg = data.getString("message");
|
||||
if (msg != message) {
|
||||
style.addLine(data.getString("message"));
|
||||
@@ -284,8 +265,8 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
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();
|
||||
|
||||
@@ -6,8 +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;
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,9 +69,7 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
Uri defaultUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_NOTIFICATION);
|
||||
if (defaultUri != null) {
|
||||
result.putString("defaultUri", Uri.decode(defaultUri.toString()));
|
||||
}
|
||||
result.putString("defaultUri", Uri.decode(defaultUri.toString()));
|
||||
result.putString("selectedUri", mNotificationPreference.getNotificationSound());
|
||||
result.putBoolean("shouldVibrate", mNotificationPreference.getShouldVibrate());
|
||||
result.putBoolean("shouldBlink", mNotificationPreference.getShouldBlink());
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -8,9 +8,9 @@ import {ViewTypes} from 'app/constants';
|
||||
import {UserTypes} from 'mattermost-redux/action_types';
|
||||
import {
|
||||
fetchMyChannelsAndMembers,
|
||||
markChannelAsRead,
|
||||
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';
|
||||
@@ -19,7 +19,6 @@ 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,
|
||||
@@ -237,27 +236,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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -379,10 +371,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,10 +386,13 @@ 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);
|
||||
}
|
||||
};
|
||||
@@ -455,31 +453,16 @@ 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);
|
||||
|
||||
@@ -506,5 +489,7 @@ export function increasePostVisibility(channelId, focusedPostId) {
|
||||
data: false,
|
||||
channelId
|
||||
});
|
||||
|
||||
return posts && posts.order.length >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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 {addReaction} from 'mattermost-redux/actions/posts';
|
||||
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} 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));
|
||||
};
|
||||
}
|
||||
|
||||
export function addReactionToLatestPost(emoji, rootId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const postIds = rootId ? getPostIdsForThread(state, rootId) : getPostIdsInCurrentChannel(state);
|
||||
const lastPostId = postIds[0];
|
||||
|
||||
dispatch(serviceAddReaction(lastPostId, emoji));
|
||||
dispatch(addRecentEmoji(emoji));
|
||||
};
|
||||
}
|
||||
|
||||
export function addRecentEmoji(emoji) {
|
||||
return {
|
||||
type: ViewTypes.ADD_RECENT_EMOJI,
|
||||
emoji
|
||||
dispatch(addReaction(lastPostId, emoji));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,37 +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 || file.path || file.uri;
|
||||
|
||||
if (name.includes('/')) {
|
||||
name = name.split('/').pop();
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
@@ -113,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,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {getDataRetentionPolicy} from 'mattermost-redux/actions/general';
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {Client, Client4} from 'mattermost-redux/client';
|
||||
|
||||
@@ -27,10 +25,8 @@ export function handlePasswordChanged(password) {
|
||||
|
||||
export function handleSuccessfulLogin() {
|
||||
return async (dispatch, getState) => {
|
||||
const {config, license} = getState().entities.general;
|
||||
const token = Client4.getToken();
|
||||
const url = Client4.getUrl();
|
||||
|
||||
dispatch({
|
||||
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
|
||||
data: {
|
||||
@@ -42,13 +38,6 @@ export function handleSuccessfulLogin() {
|
||||
Client.setToken(token);
|
||||
Client.setUrl(url);
|
||||
|
||||
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 true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
// 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 {getClientConfig, 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 {
|
||||
handleSelectChannel,
|
||||
setChannelDisplayName,
|
||||
@@ -19,24 +14,11 @@ import {
|
||||
|
||||
export function loadConfigAndLicense() {
|
||||
return async (dispatch, getState) => {
|
||||
const {currentUserId} = getState().entities.users;
|
||||
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 (currentUserId) {
|
||||
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};
|
||||
};
|
||||
}
|
||||
@@ -68,11 +50,9 @@ export function loadFromPushNotification(notification) {
|
||||
// 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(''));
|
||||
handleSelectChannel(channelId)(dispatch, getState);
|
||||
}
|
||||
@@ -83,49 +63,6 @@ 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,
|
||||
|
||||
@@ -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';
|
||||
@@ -28,10 +28,9 @@ export function handleTeamChange(teamId, selectChannel = true) {
|
||||
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[teamId] || '';
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
markChannelAsViewed(currentChannelId)(dispatch, getState);
|
||||
viewChannel(lastChannelId, currentChannelId)(dispatch, getState);
|
||||
markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -9,10 +9,11 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
usersByUsername: getUsersByUsername(state)
|
||||
usersByUsername: getUsersByUsername(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -19,10 +19,25 @@ import {getTheme} from 'mattermost-redux/selectors/entities/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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
@@ -19,9 +20,25 @@ import {getTheme} from 'mattermost-redux/selectors/entities/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)
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,7 +15,8 @@ function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
displayName: channel.display_name,
|
||||
name: channel.name,
|
||||
theme: getTheme(state)
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -32,8 +32,8 @@ export default class Badge extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.width = 0;
|
||||
this.mounted = false;
|
||||
this.layoutReady = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
@@ -50,12 +50,6 @@ export default class Badge extends PureComponent {
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.count !== this.props.count) {
|
||||
this.layoutReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
@@ -73,27 +67,34 @@ export default class Badge extends PureComponent {
|
||||
};
|
||||
|
||||
onLayout = (e) => {
|
||||
if (!this.layoutReady) {
|
||||
const height = Math.max(e.nativeEvent.layout.height, this.props.minHeight);
|
||||
const borderRadius = height / 2;
|
||||
let width;
|
||||
let width;
|
||||
|
||||
if (e.nativeEvent.layout.width <= e.nativeEvent.layout.height) {
|
||||
width = e.nativeEvent.layout.height;
|
||||
} else {
|
||||
width = e.nativeEvent.layout.width + this.props.extraPaddingHorizontal;
|
||||
if (e.nativeEvent.layout.width <= e.nativeEvent.layout.height) {
|
||||
width = e.nativeEvent.layout.height;
|
||||
} else {
|
||||
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
|
||||
}
|
||||
width = Math.max(width, this.props.minWidth);
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.setNativeProps({
|
||||
style: {
|
||||
width,
|
||||
height,
|
||||
borderRadius,
|
||||
opacity: 1
|
||||
}
|
||||
});
|
||||
this.layoutReady = true;
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
renderText = () => {
|
||||
|
||||
@@ -11,11 +11,9 @@ import {
|
||||
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';
|
||||
@@ -32,7 +30,7 @@ export default class ChannelDrawer extends Component {
|
||||
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,
|
||||
@@ -205,10 +203,9 @@ export default class ChannelDrawer extends Component {
|
||||
markChannelAsRead,
|
||||
setChannelLoading,
|
||||
setChannelDisplayName,
|
||||
markChannelAsViewed
|
||||
viewChannel
|
||||
} = actions;
|
||||
|
||||
tracker.channelSwitch = Date.now();
|
||||
setChannelLoading(channel.id !== currentChannelId);
|
||||
setChannelDisplayName(channel.display_name);
|
||||
|
||||
@@ -220,7 +217,7 @@ export default class ChannelDrawer extends Component {
|
||||
// mark the channel as viewed after all the frame has flushed
|
||||
markChannelAsRead(channel.id, currentChannelId);
|
||||
if (channel.id !== currentChannelId) {
|
||||
markChannelAsViewed(currentChannelId);
|
||||
viewChannel(currentChannelId);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -365,19 +362,14 @@ export default class ChannelDrawer extends Component {
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
backgroundColor={theme.sidebarHeaderBg}
|
||||
navigator={navigator}
|
||||
<DrawerSwiper
|
||||
ref={this.drawerSwiperRef}
|
||||
onPageSelected={this.onPageSelected}
|
||||
openDrawerOffset={openDrawerOffset}
|
||||
showTeams={showTeams}
|
||||
>
|
||||
<DrawerSwiper
|
||||
ref={this.drawerSwiperRef}
|
||||
onPageSelected={this.onPageSelected}
|
||||
openDrawerOffset={openDrawerOffset}
|
||||
showTeams={showTeams}
|
||||
>
|
||||
{lists}
|
||||
</DrawerSwiper>
|
||||
</SafeAreaView>
|
||||
{lists}
|
||||
</DrawerSwiper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -395,7 +387,7 @@ export default class ChannelDrawer extends Component {
|
||||
captureGestures='open'
|
||||
type='static'
|
||||
acceptTap={true}
|
||||
acceptPanOnDrawer={false}
|
||||
acceptPanOnDrawer={true}
|
||||
disabled={false}
|
||||
content={this.renderContent()}
|
||||
tapToClose={true}
|
||||
|
||||
@@ -4,21 +4,16 @@
|
||||
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,
|
||||
@@ -27,17 +22,12 @@ export default class ChannelItem extends PureComponent {
|
||||
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(() => {
|
||||
@@ -45,30 +35,6 @@ export default class ChannelItem extends PureComponent {
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -127,28 +93,25 @@ export default class ChannelItem extends PureComponent {
|
||||
);
|
||||
|
||||
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>
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<View style={style.container}>
|
||||
{extraBorder}
|
||||
<View style={[style.item, extraItemStyle]}>
|
||||
{icon}
|
||||
<Text
|
||||
style={[style.text, extraTextStyle]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
{badge}
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
</AnimatedView>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class ChannelsList extends React.PureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.firstUnreadChannel = null;
|
||||
this.state = {
|
||||
searching: false,
|
||||
term: ''
|
||||
@@ -140,13 +140,6 @@ 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
|
||||
@@ -155,7 +148,12 @@ class ChannelsList extends React.PureComponent {
|
||||
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: 15,
|
||||
lineHeight: 66
|
||||
}}
|
||||
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
|
||||
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
|
||||
@@ -199,7 +197,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flex: 1
|
||||
},
|
||||
statusBar: {
|
||||
backgroundColor: theme.sidebarHeaderBg
|
||||
backgroundColor: theme.sidebarHeaderBg,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
paddingTop: 20
|
||||
}
|
||||
})
|
||||
},
|
||||
headerContainer: {
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -147,7 +147,7 @@ class FilteredList extends Component {
|
||||
},
|
||||
channels: {
|
||||
builder: this.buildChannelsForSearch,
|
||||
id: 'mobile.channel_list.channels',
|
||||
id: 'sidebar.channels',
|
||||
defaultMessage: 'CHANNELS'
|
||||
},
|
||||
dms: {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
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';
|
||||
@@ -21,11 +21,12 @@ import UnreadIndicator from 'app/components/channel_drawer/channels_list/unread_
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class List extends PureComponent {
|
||||
class List extends PureComponent {
|
||||
static propTypes = {
|
||||
canCreatePrivateChannels: PropTypes.bool.isRequired,
|
||||
directChannelIds: PropTypes.array.isRequired,
|
||||
favoriteChannelIds: PropTypes.array.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
onSelectChannel: PropTypes.func.isRequired,
|
||||
publicChannelIds: PropTypes.array.isRequired,
|
||||
@@ -35,10 +36,6 @@ export default class List extends PureComponent {
|
||||
unreadChannelIds: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -72,10 +69,10 @@ export default class List extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.sections !== this.state.sections && this.refs.list._wrapperListRef.getListRef()._viewabilityHelper) { //eslint-disable-line
|
||||
if (prevState.sections !== this.state.sections && this.refs.list) {
|
||||
this.refs.list.recordInteraction();
|
||||
this.updateUnreadIndicators({
|
||||
viewableItems: Array.from(this.refs.list._wrapperListRef.getListRef()._viewabilityHelper._viewableItems.values()) //eslint-disable-line
|
||||
viewableItems: Array.from(this.refs.list._wrapperListRef._listRef._viewabilityHelper._viewableItems.values()) //eslint-disable-line
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -143,8 +140,7 @@ export default class List extends PureComponent {
|
||||
};
|
||||
|
||||
goToCreatePrivateChannel = wrapWithPreventDoubleTap(() => {
|
||||
const {navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {intl, navigator, theme} = this.props;
|
||||
|
||||
navigator.showModal({
|
||||
screen: 'CreateChannel',
|
||||
@@ -166,8 +162,7 @@ export default class List extends PureComponent {
|
||||
});
|
||||
|
||||
goToDirectMessages = wrapWithPreventDoubleTap(() => {
|
||||
const {navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {intl, navigator, theme} = this.props;
|
||||
|
||||
navigator.showModal({
|
||||
screen: 'MoreDirectMessages',
|
||||
@@ -191,8 +186,7 @@ export default class List extends PureComponent {
|
||||
});
|
||||
|
||||
goToMoreChannels = wrapWithPreventDoubleTap(() => {
|
||||
const {navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {intl, navigator, theme} = this.props;
|
||||
|
||||
navigator.showModal({
|
||||
screen: 'MoreChannels',
|
||||
@@ -251,7 +245,6 @@ export default class List extends PureComponent {
|
||||
return (
|
||||
<ChannelItem
|
||||
channelId={item}
|
||||
navigator={this.props.navigator}
|
||||
onSelectChannel={this.onSelectChannel}
|
||||
/>
|
||||
);
|
||||
@@ -262,15 +255,13 @@ export default class List extends PureComponent {
|
||||
<ChannelItem
|
||||
channelId={item}
|
||||
isUnread={true}
|
||||
navigator={this.props.navigator}
|
||||
onSelectChannel={this.onSelectChannel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
const {styles} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {intl, styles} = this.props;
|
||||
const {
|
||||
action,
|
||||
bottomSeparator,
|
||||
@@ -295,7 +286,7 @@ export default class List extends PureComponent {
|
||||
|
||||
scrollToTop = () => {
|
||||
if (this.refs.list) {
|
||||
this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
|
||||
this.refs.list._wrapperListRef._listRef.scrollToOffset({ //eslint-disable-line no-underscore-dangle
|
||||
x: 0,
|
||||
y: 0,
|
||||
animated: true
|
||||
@@ -355,3 +346,5 @@ export default class List extends PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(List);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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, View} from 'react-native';
|
||||
import Svg, {
|
||||
G,
|
||||
Path
|
||||
} from 'react-native-svg';
|
||||
|
||||
export default class AboveIcon extends PureComponent {
|
||||
static propTypes = {
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const {color, height, width} = this.props;
|
||||
|
||||
return (
|
||||
<View style={[style.container, {height, width}]}>
|
||||
<Svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox='0 0 10 10'
|
||||
>
|
||||
<G transform='matrix(1,0,0,1,-20,-18)'>
|
||||
<G transform='matrix(0.0330723,0,0,0.0322634,15.8132,12.3164)'>
|
||||
<Path
|
||||
d='M245.803,377.493C245.803,377.493 204.794,336.485 179.398,311.088C168.55,300.24 150.962,300.24 140.114,311.088C138.327,312.875 136.517,314.686 134.73,316.473C123.882,327.321 123.882,344.908 134.73,355.756C167.972,388.998 233.949,454.975 256.949,477.975C262.158,483.184 269.223,486.111 276.591,486.111C277.38,486.111 278.176,486.111 278.965,486.111C286.332,486.111 293.397,483.184 298.607,477.975C321.607,454.975 387.584,388.998 420.826,355.756C431.674,344.908 431.674,327.321 420.826,316.473C419.039,314.686 417.228,312.875 415.441,311.088C404.593,300.24 387.005,300.24 376.158,311.088C350.761,336.485 309.753,377.493 309.753,377.493C309.753,377.493 309.753,279.687 309.753,203.94C309.753,196.573 306.826,189.508 301.617,184.298C296.408,179.089 289.342,176.162 281.975,176.162C279.191,176.162 276.364,176.162 273.58,176.162C266.213,176.162 259.148,179.089 253.939,184.298C248.729,189.508 245.803,196.573 245.803,203.94L245.803,377.493Z'
|
||||
fill={color}
|
||||
/>
|
||||
</G>
|
||||
</G>
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'flex-start',
|
||||
transform: [{rotate: '180deg'}]
|
||||
}
|
||||
});
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
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';
|
||||
|
||||
import AboveIcon from './above_icon';
|
||||
|
||||
export default class UnreadIndicator extends PureComponent {
|
||||
static propTypes = {
|
||||
show: PropTypes.bool,
|
||||
@@ -44,11 +45,10 @@ export default class UnreadIndicator extends PureComponent {
|
||||
id='sidebar.unreads'
|
||||
defaultMessage='More unreads'
|
||||
/>
|
||||
<IonIcon
|
||||
size={14}
|
||||
name='md-arrow-round-up'
|
||||
<AboveIcon
|
||||
width={12}
|
||||
height={12}
|
||||
color={theme.mentionColor}
|
||||
style={style.arrow}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
@@ -75,10 +75,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
paddingHorizontal: 4,
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center'
|
||||
},
|
||||
arrow: {
|
||||
position: 'relative',
|
||||
bottom: -1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
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';
|
||||
|
||||
@@ -34,7 +34,7 @@ function mapDispatchToProps(dispatch) {
|
||||
getTeams,
|
||||
handleSelectChannel,
|
||||
joinChannel,
|
||||
markChannelAsViewed,
|
||||
viewChannel,
|
||||
makeDirectChannel,
|
||||
markChannelAsRead,
|
||||
setChannelDisplayName,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -47,7 +46,6 @@ class TeamsList extends PureComponent {
|
||||
requestAnimationFrame(() => {
|
||||
const {actions, closeChannelDrawer, currentTeamId} = this.props;
|
||||
if (teamId !== currentTeamId) {
|
||||
tracker.teamSwitch = Date.now();
|
||||
actions.handleTeamChange(teamId);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -11,21 +11,18 @@ import {removeProtocol} from 'app/utils/url';
|
||||
|
||||
import TeamsListItem from './teams_list_item.js';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const team = getTeam(state, ownProps.teamId);
|
||||
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)
|
||||
};
|
||||
return {
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
currentUrl: removeProtocol(getCurrentUrl(state)),
|
||||
displayName: team.display_name,
|
||||
mentionCount: getMentionCount(state, ownProps.teamId),
|
||||
name: team.name,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(TeamsListItem);
|
||||
export default connect(mapStateToProps)(TeamsListItem);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ class ChannelIntro extends PureComponent {
|
||||
size={64}
|
||||
statusBorderWidth={2}
|
||||
statusSize={25}
|
||||
statusIconSize={15}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
));
|
||||
|
||||
@@ -2,79 +2,41 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {General, RequestStatus} from 'mattermost-redux/constants';
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId, getUser, makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
|
||||
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 ChannelIntro from './channel_intro';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getChannel = makeGetChannel();
|
||||
const getProfilesInChannel = makeGetProfilesInChannel();
|
||||
function mapStateToProps(state) {
|
||||
const currentChannel = getCurrentChannel(state) || {};
|
||||
const currentUser = getCurrentUser(state) || {};
|
||||
const {status: getPostsRequestStatus} = state.requests.posts.getPosts;
|
||||
|
||||
const getOtherUserIdForDm = createSelector(
|
||||
(state, channel) => channel,
|
||||
getCurrentUserId,
|
||||
(channel, currentUserId) => {
|
||||
if (!channel) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return channel.name.split('__').find((m) => m !== currentUserId);
|
||||
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 getChannelMembers = createSelector(
|
||||
getCurrentUserId,
|
||||
(state, channel) => getProfilesInChannel(state, channel.id),
|
||||
(currentUserId, profilesInChannel) => {
|
||||
const currentChannelMembers = profilesInChannel || [];
|
||||
return currentChannelMembers.filter((m) => m.id !== currentUserId);
|
||||
}
|
||||
);
|
||||
const creator = currentChannel.creator_id === currentUser.id ? currentUser : state.entities.users.profiles[currentChannel.creator_id];
|
||||
const postsInChannel = state.entities.posts.postsInChannel[currentChannel.id] || [];
|
||||
|
||||
const getChannelMembersForDm = createSelector(
|
||||
(state, channel) => getUser(state, getOtherUserIdForDm(state, channel)),
|
||||
(otherUser) => {
|
||||
if (!otherUser) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [otherUser];
|
||||
}
|
||||
);
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const currentChannel = getChannel(state, {id: ownProps.channelId}) || {};
|
||||
const {status: getPostsRequestStatus} = state.requests.posts.getPosts;
|
||||
|
||||
let currentChannelMembers;
|
||||
let creator;
|
||||
let postsInChannel;
|
||||
|
||||
if (currentChannel) {
|
||||
if (currentChannel.type === General.DM_CHANNEL) {
|
||||
currentChannelMembers = getChannelMembersForDm(state, currentChannel);
|
||||
} else {
|
||||
currentChannelMembers = getChannelMembers(state, currentChannel);
|
||||
}
|
||||
|
||||
creator = getUser(state, currentChannel.creator_id);
|
||||
postsInChannel = state.entities.posts.postsInChannel[currentChannel.Id];
|
||||
}
|
||||
|
||||
return {
|
||||
creator,
|
||||
currentChannel,
|
||||
currentChannelMembers,
|
||||
isLoadingPosts: (!postsInChannel || postsInChannel.length === 0) && getPostsRequestStatus === RequestStatus.STARTED,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
return {
|
||||
creator,
|
||||
currentChannel,
|
||||
currentChannelMembers,
|
||||
isLoadingPosts: !postsInChannel.length && getPostsRequestStatus === RequestStatus.STARTED,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(ChannelIntro);
|
||||
export default connect(mapStateToProps)(ChannelIntro);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@ import {
|
||||
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 {injectIntl, intlShape} from 'react-intl';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {UpgradeTypes} from 'app/constants/view';
|
||||
@@ -23,7 +21,7 @@ const {View: AnimatedView} = Animated;
|
||||
|
||||
const UPDATE_TIMEOUT = 60000;
|
||||
|
||||
export default class ClientUpgradeListener extends PureComponent {
|
||||
class ClientUpgradeListener extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
logError: PropTypes.func.isRequired,
|
||||
@@ -32,7 +30,7 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
currentVersion: PropTypes.string,
|
||||
downloadLink: PropTypes.string,
|
||||
forceUpgrade: PropTypes.bool,
|
||||
isLandscape: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
lastUpgradeCheck: PropTypes.number,
|
||||
latestVersion: PropTypes.string,
|
||||
minVersion: PropTypes.string,
|
||||
@@ -40,28 +38,14 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
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)
|
||||
};
|
||||
state = {
|
||||
top: new Animated.Value(-100)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {forceUpgrade, isLandscape, lastUpgradeCheck, latestVersion, minVersion} = this.props;
|
||||
const {forceUpgrade, lastUpgradeCheck, latestVersion, minVersion} = this.props;
|
||||
if (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT) {
|
||||
this.checkUpgrade(minVersion, latestVersion, isLandscape);
|
||||
this.checkUpgrade(minVersion, latestVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,54 +55,40 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
|
||||
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)});
|
||||
this.checkUpgrade(minVersion, latestVersion);
|
||||
}
|
||||
}
|
||||
|
||||
checkUpgrade = (minVersion, latestVersion, isLandscape) => {
|
||||
checkUpgrade = (minVersion, latestVersion) => {
|
||||
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);
|
||||
this.setState({upgradeType});
|
||||
|
||||
setTimeout(this.toggleUpgradeMessage, 500);
|
||||
|
||||
actions.setLastUpgradeCheck();
|
||||
};
|
||||
}
|
||||
|
||||
toggleUpgradeMessage = (show = true, isLandscape) => {
|
||||
let toValue = -100;
|
||||
if (show) {
|
||||
if (this.isX && isLandscape) {
|
||||
toValue = 45;
|
||||
} else {
|
||||
toValue = this.isX ? 100 : 75;
|
||||
}
|
||||
}
|
||||
toggleUpgradeMessage = (show = true) => {
|
||||
const toValue = show ? 75 : -100;
|
||||
Animated.timing(this.state.top, {
|
||||
toValue,
|
||||
duration: 300
|
||||
}).start();
|
||||
};
|
||||
}
|
||||
|
||||
handleDismiss = () => {
|
||||
this.toggleUpgradeMessage(false);
|
||||
};
|
||||
}
|
||||
|
||||
handleDownload = () => {
|
||||
const {downloadLink} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {downloadLink, intl} = this.props;
|
||||
|
||||
Linking.canOpenURL(downloadLink).then((supported) => {
|
||||
if (supported) {
|
||||
@@ -140,25 +110,17 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
});
|
||||
|
||||
this.toggleUpgradeMessage(false);
|
||||
};
|
||||
}
|
||||
|
||||
handleLearnMore = () => {
|
||||
const {intl} = this.context;
|
||||
this.props.navigator.dismissModal({animationType: 'none'});
|
||||
this.props.navigator.dismissAllModals({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
|
||||
}]
|
||||
navBarHidden: true,
|
||||
statusBarHidden: true,
|
||||
statusBarHideWithNavBar: true
|
||||
},
|
||||
passProps: {
|
||||
upgradeType: this.state.upgradeType
|
||||
@@ -166,13 +128,9 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
});
|
||||
|
||||
this.toggleUpgradeMessage(false);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.upgradeType === UpgradeTypes.NO_UPGRADE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {forceUpgrade, theme} = this.props;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
@@ -269,3 +227,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(ClientUpgradeListener);
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
@@ -17,7 +16,6 @@ function mapStateToProps(state) {
|
||||
currentVersion,
|
||||
downloadLink,
|
||||
forceUpgrade,
|
||||
isLandscape: isLandscape(state),
|
||||
lastUpgradeCheck: state.views.clientUpgrade.lastUpdateCheck,
|
||||
latestVersion,
|
||||
minVersion,
|
||||
|
||||
@@ -15,7 +15,8 @@ function makeMapStateToProps() {
|
||||
return (state, ownProps) => {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
channel: getChannel(state, ownProps)
|
||||
channel: getChannel(state, ownProps),
|
||||
...ownProps
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
user: getUser(state, ownProps.id),
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state)
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -88,11 +88,13 @@ export default class CustomSectionList extends React.PureComponent {
|
||||
initialNumToRender: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultKeyExtractor = (item) => {
|
||||
return item.id;
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showNoResults: true,
|
||||
keyExtractor: (item) => {
|
||||
return item.id;
|
||||
},
|
||||
keyExtractor: CustomSectionList.defaultKeyExtractor,
|
||||
onListEndReached: () => true,
|
||||
onListEndReachedThreshold: 50,
|
||||
loadingText: null,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
@@ -6,62 +6,48 @@ import {createSelector} from 'reselect';
|
||||
|
||||
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
|
||||
|
||||
import {getDimensions, isLandscape} from 'app/selectors/device';
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {CategoryNames, Emojis, EmojiIndicesByAlias, EmojiIndicesByCategory} from 'app/utils/emojis';
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
View,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
@@ -58,7 +57,7 @@ export default class FileAttachmentList extends Component {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'black',
|
||||
screenBackgroundColor: 'transparent',
|
||||
modalPresentationStyle: 'overCurrentContext'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ function makeMapStateToProps() {
|
||||
const getFilesForPost = makeGetFilesForPost();
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
fetchCache: state.views.fetchCache,
|
||||
files: getFilesForPost(state, ownProps.postId),
|
||||
theme: getTheme(state),
|
||||
|
||||
@@ -14,6 +14,7 @@ import Icon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image';
|
||||
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
|
||||
import KeyboardLayout from 'app/components/layout/keyboard_layout';
|
||||
|
||||
export default class FileUploadPreview extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -45,7 +46,7 @@ export default class FileUploadPreview extends PureComponent {
|
||||
buildFilePreviews = () => {
|
||||
return this.props.files.map((file) => {
|
||||
let filePreviewComponent;
|
||||
if (file.loading | (file.has_preview_image || file.mime_type === 'image/gif')) {
|
||||
if (file.loading | file.has_preview_image) {
|
||||
filePreviewComponent = (
|
||||
<FileAttachmentImage
|
||||
addFileToFetchCache={this.props.actions.addFileToFetchCache}
|
||||
@@ -103,7 +104,7 @@ export default class FileUploadPreview extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<KeyboardLayout>
|
||||
<View style={[style.container, {height: this.props.deviceHeight}]}>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
@@ -113,7 +114,7 @@ export default class FileUploadPreview extends PureComponent {
|
||||
{this.buildFilePreviews()}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ function mapStateToProps(state, ownProps) {
|
||||
const {deviceHeight} = getDimensions(state);
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
createPostRequestStatus: state.requests.posts.createPost.status,
|
||||
deviceHeight,
|
||||
|
||||
@@ -1,31 +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 Svg, {Path, G} from 'react-native-svg';
|
||||
|
||||
export default class FlagIcon extends PureComponent {
|
||||
static propTypes = {
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Svg
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
viewBox='0 0 16 16'
|
||||
>
|
||||
<G fill={this.props.color}>
|
||||
<Path
|
||||
d='M8,1 L2,1 C2,0.447 1.553,0 1,0 C0.447,0 0,0.447 0,1 L0,15.5 C0,15.776 0.224,16 0.5,16 L1.5,16 C1.776,16 2,15.776 2,15.5 L2,11 L7,11 L7,12 C7,12.553 7.447,13 8,13 L15,13 C15.553,13 16,12.553 16,12 L16,4 C16,3.447 15.553,3 15,3 L9,3 L9,2 C9,1.447 8.553,1 8,1 Z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
</G>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
157
app/components/inverted_flat_list/index.js
Normal file
157
app/components/inverted_flat_list/index.js
Normal file
@@ -0,0 +1,157 @@
|
||||
// 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 {FlatList, Platform, ScrollView, StyleSheet, View} from 'react-native';
|
||||
|
||||
import RefreshList from 'app/components/refresh_list';
|
||||
|
||||
import VirtualList from './virtual_list';
|
||||
|
||||
export default class InvertibleFlatList extends PureComponent {
|
||||
static propTypes = {
|
||||
horizontal: PropTypes.bool,
|
||||
inverted: PropTypes.bool,
|
||||
ListFooterComponent: PropTypes.func,
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
horizontal: false,
|
||||
inverted: true
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.inversionDirection = props.horizontal ? styles.horizontal : styles.vertical;
|
||||
}
|
||||
|
||||
getMetrics = () => {
|
||||
return this.flatListRef.getMetrics();
|
||||
};
|
||||
|
||||
recordInteraction = () => {
|
||||
this.flatListRef.recordInteraction();
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
const {ListFooterComponent: footer} = this.props;
|
||||
if (!footer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, this.inversionDirection]}>
|
||||
{footer()}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderItem = (info) => {
|
||||
return (
|
||||
<View style={[styles.container, this.inversionDirection]}>
|
||||
{this.props.renderItem(info)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderScrollComponent = (props) => {
|
||||
const {theme} = this.props;
|
||||
|
||||
if (props.onRefresh) {
|
||||
return (
|
||||
<ScrollView
|
||||
{...props}
|
||||
refreshControl={
|
||||
<RefreshList
|
||||
refreshing={props.refreshing}
|
||||
onRefresh={props.onRefresh}
|
||||
tintColor={theme.centerChannelColor}
|
||||
colors={[theme.centerChannelColor]}
|
||||
style={this.inversionDirection}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ScrollView {...props}/>;
|
||||
};
|
||||
|
||||
scrollToEnd = (params) => {
|
||||
this.flatListRef.scrollToEnd(params);
|
||||
};
|
||||
|
||||
scrollToIndex = (params) => {
|
||||
this.flatListRef.scrollToIndex(params);
|
||||
};
|
||||
|
||||
scrollToItem = (params) => {
|
||||
this.flatListRef.scrollToItem(params);
|
||||
};
|
||||
|
||||
scrollToOffset = (params) => {
|
||||
this.flatListRef.scrollToOffset(params);
|
||||
};
|
||||
|
||||
setFlatListRef = (flatListRef) => {
|
||||
this.flatListRef = flatListRef;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {inverted, ...forwardedProps} = this.props;
|
||||
|
||||
// If not inverted, render as an ordinary FlatList
|
||||
if (!inverted) {
|
||||
return (
|
||||
<FlatList
|
||||
{...forwardedProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, this.inversionDirection]}>
|
||||
<VirtualList
|
||||
ref={this.setFlatListRef}
|
||||
{...forwardedProps}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
renderItem={this.renderItem}
|
||||
renderScrollComponent={this.renderScrollComponent}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
vertical: Platform.select({
|
||||
android: {
|
||||
transform: [
|
||||
{perspective: 1},
|
||||
{scaleY: -1}
|
||||
]
|
||||
},
|
||||
ios: {
|
||||
transform: [{scaleY: -1}]
|
||||
}
|
||||
}),
|
||||
horizontal: Platform.select({
|
||||
android: {
|
||||
transform: [
|
||||
{perspective: 1},
|
||||
{scaleY: -1}
|
||||
]
|
||||
},
|
||||
ios: {
|
||||
transform: [{scaleX: -1}]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
54
app/components/inverted_flat_list/virtual_list.js
Normal file
54
app/components/inverted_flat_list/virtual_list.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {VirtualizedList} from 'react-native';
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export default class Virtualized extends VirtualizedList {
|
||||
_onScroll = (e) => {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll(e);
|
||||
}
|
||||
const timestamp = e.timeStamp;
|
||||
const visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement);
|
||||
const contentLength = this._selectLength(e.nativeEvent.contentSize);
|
||||
const offset = this._selectOffset(e.nativeEvent.contentOffset);
|
||||
const dt = Math.max(1, timestamp - this._scrollMetrics.timestamp);
|
||||
const dOffset = offset - this._scrollMetrics.offset;
|
||||
const velocity = dOffset / dt;
|
||||
this._scrollMetrics = {contentLength, dt, offset, timestamp, velocity, visibleLength};
|
||||
const {data, getItemCount, onEndReached, onEndReachedThreshold, windowSize} = this.props;
|
||||
this._updateViewableItems(data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const distanceFromEnd = contentLength - visibleLength - offset;
|
||||
const itemCount = getItemCount(data);
|
||||
if (this.state.last === itemCount - 1 &&
|
||||
distanceFromEnd <= onEndReachedThreshold &&
|
||||
(this._hasDataChangedSinceEndReached ||
|
||||
this._scrollMetrics.contentLength !== this._sentEndForContentLength)) {
|
||||
// Only call onEndReached once for a given dataset + content length.
|
||||
this._hasDataChangedSinceEndReached = false;
|
||||
this._sentEndForContentLength = this._scrollMetrics.contentLength;
|
||||
onEndReached({distanceFromEnd});
|
||||
}
|
||||
const {first, last} = this.state;
|
||||
if ((first > 0 && velocity < 0) || (last < itemCount - 1 && velocity > 0)) {
|
||||
const distanceToContentEdge = Math.min(
|
||||
Math.abs(this._getFrameMetricsApprox(first).offset - offset),
|
||||
Math.abs(this._getFrameMetricsApprox(last).offset - (offset + visibleLength)),
|
||||
);
|
||||
const hiPri = distanceToContentEdge < (windowSize * visibleLength / 4);
|
||||
if (hiPri) {
|
||||
// Don't worry about interactions when scrolling quickly; focus on filling content as fast
|
||||
// as possible.
|
||||
this._updateCellsToRenderBatcher.dispose({abort: true});
|
||||
this._updateCellsToRender();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._updateCellsToRenderBatcher.schedule();
|
||||
};
|
||||
}
|
||||
@@ -3,16 +3,13 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {getStatusBarHeight} from 'app/selectors/device';
|
||||
|
||||
import KeyboardLayout from './keyboard_layout';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
statusBarHeight: getStatusBarHeight(state),
|
||||
theme: getTheme(state)
|
||||
statusBarHeight: getStatusBarHeight(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,98 +3,46 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Animated, Keyboard, Platform, View} from 'react-native';
|
||||
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
import {KeyboardAvoidingView, Platform, View} from 'react-native';
|
||||
|
||||
export default class KeyboardLayout extends PureComponent {
|
||||
static propTypes = {
|
||||
behaviour: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
statusBarHeight: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired
|
||||
keyboardVerticalOffset: PropTypes.number,
|
||||
statusBarHeight: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
keyboardVerticalOffset: 0
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.subscriptions = [];
|
||||
this.count = 0;
|
||||
this.state = {
|
||||
bottom: new Animated.Value(0)
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (Platform.OS === 'ios') {
|
||||
this.subscriptions = [
|
||||
Keyboard.addListener('keyboardWillChangeFrame', this.onKeyboardChange),
|
||||
Keyboard.addListener('keyboardWillHide', this.onKeyboardWillHide)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscriptions.forEach((sub) => sub.remove());
|
||||
}
|
||||
|
||||
onKeyboardWillHide = (e) => {
|
||||
const {duration} = e;
|
||||
Animated.timing(this.state.bottom, {
|
||||
toValue: 0,
|
||||
duration
|
||||
}).start();
|
||||
};
|
||||
|
||||
onKeyboardChange = (e) => {
|
||||
if (!e) {
|
||||
this.setState({bottom: new Animated.Value(0)});
|
||||
return;
|
||||
}
|
||||
|
||||
const {endCoordinates, duration} = e;
|
||||
const {height} = endCoordinates;
|
||||
Animated.timing(this.state.bottom, {
|
||||
toValue: height,
|
||||
duration
|
||||
}).start();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {children, theme, ...otherProps} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
const {behaviour, children, keyboardVerticalOffset, statusBarHeight, ...otherProps} = this.props;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return (
|
||||
<View
|
||||
style={style.keyboardLayout}
|
||||
{...otherProps}
|
||||
>
|
||||
<View {...otherProps}>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let height = 0;
|
||||
if (statusBarHeight > 20) {
|
||||
height = (statusBarHeight - 20) + keyboardVerticalOffset;
|
||||
} else {
|
||||
height = keyboardVerticalOffset;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatedView
|
||||
style={[style.keyboardLayout, {bottom: this.state.bottom}]}
|
||||
<KeyboardAvoidingView
|
||||
behaviour={behaviour}
|
||||
keyboardVerticalOffset={height}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</AnimatedView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
keyboardLayout: {
|
||||
position: 'relative',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,16 +1,317 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {Parser} from 'commonmark';
|
||||
import Renderer from 'commonmark-react-renderer';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import AtMention from 'app/components/at_mention';
|
||||
import ChannelLink from 'app/components/channel_link';
|
||||
import Emoji from 'app/components/emoji';
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {concatStyles} from 'app/utils/theme';
|
||||
|
||||
import Markdown from './markdown';
|
||||
import MarkdownBlockQuote from './markdown_block_quote';
|
||||
import MarkdownCodeBlock from './markdown_code_block';
|
||||
import MarkdownLink from './markdown_link';
|
||||
import MarkdownList from './markdown_list';
|
||||
import MarkdownListItem from './markdown_list_item';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state)
|
||||
export default class Markdown extends PureComponent {
|
||||
static propTypes = {
|
||||
baseTextStyle: CustomPropTypes.Style,
|
||||
blockStyles: PropTypes.object,
|
||||
emojiSizes: PropTypes.object,
|
||||
fontSizes: PropTypes.object,
|
||||
isSearchResult: PropTypes.bool,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onLongPress: PropTypes.func,
|
||||
onPostPress: PropTypes.func,
|
||||
textStyles: PropTypes.object,
|
||||
value: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
textStyles: {},
|
||||
blockStyles: {},
|
||||
emojiSizes: {
|
||||
...Platform.select({
|
||||
ios: {
|
||||
heading1: 25,
|
||||
heading2: 25,
|
||||
heading3: 25,
|
||||
heading4: 25,
|
||||
heading5: 25,
|
||||
heading6: 25,
|
||||
text: 20
|
||||
},
|
||||
android: {
|
||||
heading1: 60,
|
||||
heading2: 60,
|
||||
heading3: 60,
|
||||
heading4: 60,
|
||||
heading5: 60,
|
||||
heading6: 60,
|
||||
text: 45
|
||||
}
|
||||
})
|
||||
},
|
||||
fontSizes: {
|
||||
heading1: 17,
|
||||
heading2: 17,
|
||||
heading3: 17,
|
||||
heading4: 17,
|
||||
heading5: 17,
|
||||
heading6: 17,
|
||||
text: 15
|
||||
},
|
||||
onLongPress: () => true
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.parser = new Parser();
|
||||
this.renderer = this.createRenderer();
|
||||
}
|
||||
|
||||
createRenderer = () => {
|
||||
return new Renderer({
|
||||
renderers: {
|
||||
text: this.renderText,
|
||||
|
||||
emph: Renderer.forwardChildren,
|
||||
strong: Renderer.forwardChildren,
|
||||
del: Renderer.forwardChildren,
|
||||
code: this.renderCodeSpan,
|
||||
link: this.renderLink,
|
||||
image: this.renderImage,
|
||||
atMention: this.renderAtMention,
|
||||
channelLink: this.renderChannelLink,
|
||||
emoji: this.renderEmoji,
|
||||
|
||||
paragraph: this.renderParagraph,
|
||||
heading: this.renderHeading,
|
||||
codeBlock: this.renderCodeBlock,
|
||||
blockQuote: this.renderBlockQuote,
|
||||
|
||||
list: this.renderList,
|
||||
item: this.renderListItem,
|
||||
|
||||
hardBreak: this.renderHardBreak,
|
||||
thematicBreak: this.renderThematicBreak,
|
||||
softBreak: this.renderSoftBreak,
|
||||
|
||||
htmlBlock: this.renderHtml,
|
||||
htmlInline: this.renderHtml
|
||||
},
|
||||
renderParagraphsInLists: true
|
||||
});
|
||||
}
|
||||
|
||||
computeTextStyle = (baseStyle, context) => {
|
||||
return concatStyles(baseStyle, context.map((type) => this.props.textStyles[type]));
|
||||
}
|
||||
|
||||
renderText = ({context, literal}) => {
|
||||
// Construct the text style based off of the parents of this node since RN's inheritance is limited
|
||||
return <Text style={this.computeTextStyle(this.props.baseTextStyle, context)}>{literal}</Text>;
|
||||
}
|
||||
|
||||
renderCodeSpan = ({context, literal}) => {
|
||||
return <Text style={this.computeTextStyle([this.props.baseTextStyle, this.props.textStyles.code], context)}>{literal}</Text>;
|
||||
}
|
||||
|
||||
renderImage = ({children, context, src}) => {
|
||||
// TODO This will be properly implemented for PLT-5736
|
||||
return (
|
||||
<Text style={this.computeTextStyle(this.props.baseTextStyle, context)}>
|
||||
{''}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
renderAtMention = ({context, mentionName}) => {
|
||||
return (
|
||||
<AtMention
|
||||
mentionStyle={this.props.textStyles.mention}
|
||||
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
|
||||
isSearchResult={this.props.isSearchResult}
|
||||
mentionName={mentionName}
|
||||
onPostPress={this.props.onPostPress}
|
||||
navigator={this.props.navigator}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderChannelLink = ({context, channelName}) => {
|
||||
return (
|
||||
<ChannelLink
|
||||
linkStyle={this.props.textStyles.link}
|
||||
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
|
||||
channelName={channelName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmoji = ({context, emojiName, literal}) => {
|
||||
let size;
|
||||
let fontSize;
|
||||
const headingType = context.find((type) => type.startsWith('heading'));
|
||||
if (headingType) {
|
||||
size = this.props.emojiSizes[headingType];
|
||||
fontSize = this.props.fontSizes[headingType];
|
||||
} else {
|
||||
size = this.props.emojiSizes.text;
|
||||
fontSize = this.props.fontSizes.text;
|
||||
}
|
||||
|
||||
return (
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
literal={literal}
|
||||
size={size}
|
||||
fontSize={fontSize}
|
||||
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderParagraph = ({children, first}) => {
|
||||
const blockStyle = [style.block];
|
||||
if (!first) {
|
||||
blockStyle.push(this.props.blockStyles.adjacentParagraph);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={blockStyle}>
|
||||
<Text>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeading = ({children, level}) => {
|
||||
return (
|
||||
<View style={[style.block, this.props.blockStyles[`heading${level}`]]}>
|
||||
<Text>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderCodeBlock = (props) => {
|
||||
// These sometimes include a trailing newline
|
||||
const content = props.literal.replace(/\n$/, '');
|
||||
|
||||
return (
|
||||
<MarkdownCodeBlock
|
||||
navigator={this.props.navigator}
|
||||
content={content}
|
||||
language={props.language}
|
||||
textStyle={this.props.textStyles.codeBlock}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderBlockQuote = ({children, ...otherProps}) => {
|
||||
return (
|
||||
<MarkdownBlockQuote
|
||||
iconStyle={this.props.blockStyles.quoteBlockIcon}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</MarkdownBlockQuote>
|
||||
);
|
||||
}
|
||||
|
||||
renderList = ({children, start, tight, type}) => {
|
||||
return (
|
||||
<MarkdownList
|
||||
ordered={type !== 'bullet'}
|
||||
startAt={start}
|
||||
tight={tight}
|
||||
>
|
||||
{children}
|
||||
</MarkdownList>
|
||||
);
|
||||
}
|
||||
|
||||
renderListItem = ({children, context, ...otherProps}) => {
|
||||
const level = context.filter((type) => type === 'list').length;
|
||||
|
||||
return (
|
||||
<MarkdownListItem
|
||||
bulletStyle={this.props.baseTextStyle}
|
||||
level={level}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</MarkdownListItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderHardBreak = () => {
|
||||
return <Text>{'\n'}</Text>;
|
||||
}
|
||||
|
||||
renderThematicBreak = () => {
|
||||
return <View style={this.props.blockStyles.horizontalRule}/>;
|
||||
}
|
||||
|
||||
renderSoftBreak = () => {
|
||||
return <Text>{'\n'}</Text>;
|
||||
}
|
||||
|
||||
renderHtml = (props) => {
|
||||
let rendered = this.renderText(props);
|
||||
|
||||
if (props.isBlock) {
|
||||
rendered = (
|
||||
<View style={style.block}>
|
||||
{rendered}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
renderLink = ({children, href}) => {
|
||||
return (
|
||||
<MarkdownLink
|
||||
href={href}
|
||||
onLongPress={this.props.onLongPress}
|
||||
>
|
||||
{children}
|
||||
</MarkdownLink>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const ast = this.parser.parse(this.props.value);
|
||||
|
||||
return <View>{this.renderer.render(ast)}</View>;
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Markdown);
|
||||
const style = StyleSheet.create({
|
||||
block: {
|
||||
alignItems: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,411 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {Parser, Node} from 'commonmark';
|
||||
import Renderer from 'commonmark-react-renderer';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import AtMention from 'app/components/at_mention';
|
||||
import ChannelLink from 'app/components/channel_link';
|
||||
import Emoji from 'app/components/emoji';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {blendColors, concatStyles, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import MarkdownBlockQuote from './markdown_block_quote';
|
||||
import MarkdownCodeBlock from './markdown_code_block';
|
||||
import MarkdownImage from './markdown_image';
|
||||
import MarkdownLink from './markdown_link';
|
||||
import MarkdownList from './markdown_list';
|
||||
import MarkdownListItem from './markdown_list_item';
|
||||
import {addListItemIndices, pullOutImages} from './transform';
|
||||
|
||||
export default class Markdown extends PureComponent {
|
||||
static propTypes = {
|
||||
baseTextStyle: CustomPropTypes.Style,
|
||||
blockStyles: PropTypes.object,
|
||||
emojiSizes: PropTypes.object,
|
||||
fontSizes: PropTypes.object,
|
||||
isEdited: PropTypes.bool,
|
||||
isSearchResult: PropTypes.bool,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onLongPress: PropTypes.func,
|
||||
onPostPress: PropTypes.func,
|
||||
textStyles: PropTypes.object,
|
||||
theme: PropTypes.object.isRequired,
|
||||
value: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
textStyles: {},
|
||||
blockStyles: {},
|
||||
emojiSizes: {
|
||||
...Platform.select({
|
||||
ios: {
|
||||
heading1: 25,
|
||||
heading2: 25,
|
||||
heading3: 25,
|
||||
heading4: 25,
|
||||
heading5: 25,
|
||||
heading6: 25,
|
||||
text: 20
|
||||
},
|
||||
android: {
|
||||
heading1: 60,
|
||||
heading2: 60,
|
||||
heading3: 60,
|
||||
heading4: 60,
|
||||
heading5: 60,
|
||||
heading6: 60,
|
||||
text: 45
|
||||
}
|
||||
})
|
||||
},
|
||||
fontSizes: {
|
||||
heading1: 17,
|
||||
heading2: 17,
|
||||
heading3: 17,
|
||||
heading4: 17,
|
||||
heading5: 17,
|
||||
heading6: 17,
|
||||
text: 15
|
||||
},
|
||||
onLongPress: () => true
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.parser = new Parser();
|
||||
this.renderer = this.createRenderer();
|
||||
}
|
||||
|
||||
createRenderer = () => {
|
||||
return new Renderer({
|
||||
renderers: {
|
||||
text: this.renderText,
|
||||
|
||||
emph: Renderer.forwardChildren,
|
||||
strong: Renderer.forwardChildren,
|
||||
del: Renderer.forwardChildren,
|
||||
code: this.renderCodeSpan,
|
||||
link: this.renderLink,
|
||||
image: this.renderImage,
|
||||
atMention: this.renderAtMention,
|
||||
channelLink: this.renderChannelLink,
|
||||
emoji: this.renderEmoji,
|
||||
|
||||
paragraph: this.renderParagraph,
|
||||
heading: this.renderHeading,
|
||||
codeBlock: this.renderCodeBlock,
|
||||
blockQuote: this.renderBlockQuote,
|
||||
|
||||
list: this.renderList,
|
||||
item: this.renderListItem,
|
||||
|
||||
hardBreak: this.renderHardBreak,
|
||||
thematicBreak: this.renderThematicBreak,
|
||||
softBreak: this.renderSoftBreak,
|
||||
|
||||
htmlBlock: this.renderHtml,
|
||||
htmlInline: this.renderHtml,
|
||||
|
||||
editedIndicator: this.renderEditedIndicator
|
||||
},
|
||||
renderParagraphsInLists: true,
|
||||
getExtraPropsForNode: this.getExtraPropsForNode
|
||||
});
|
||||
}
|
||||
|
||||
getExtraPropsForNode = (node) => {
|
||||
const extraProps = {
|
||||
continue: node.continue,
|
||||
index: node.index
|
||||
};
|
||||
|
||||
if (node.type === 'image') {
|
||||
extraProps.reactChildren = node.react.children;
|
||||
extraProps.linkDestination = node.linkDestination;
|
||||
}
|
||||
|
||||
return extraProps;
|
||||
}
|
||||
|
||||
computeTextStyle = (baseStyle, context) => {
|
||||
return concatStyles(baseStyle, context.map((type) => this.props.textStyles[type]));
|
||||
}
|
||||
|
||||
renderText = ({context, literal}) => {
|
||||
if (context.indexOf('image') !== -1) {
|
||||
// If this text is displayed, it will be styled by the image component
|
||||
return <Text>{literal}</Text>;
|
||||
}
|
||||
|
||||
// Construct the text style based off of the parents of this node since RN's inheritance is limited
|
||||
return <Text style={this.computeTextStyle(this.props.baseTextStyle, context)}>{literal}</Text>;
|
||||
}
|
||||
|
||||
renderCodeSpan = ({context, literal}) => {
|
||||
return <Text style={this.computeTextStyle([this.props.baseTextStyle, this.props.textStyles.code], context)}>{literal}</Text>;
|
||||
}
|
||||
|
||||
renderImage = ({linkDestination, reactChildren, context, src}) => {
|
||||
return (
|
||||
<MarkdownImage
|
||||
linkDestination={linkDestination}
|
||||
onLongPress={this.props.onLongPress}
|
||||
source={src}
|
||||
errorTextStyle={[this.computeTextStyle(this.props.baseTextStyle, context), this.props.textStyles.error]}
|
||||
>
|
||||
{reactChildren}
|
||||
</MarkdownImage>
|
||||
);
|
||||
}
|
||||
|
||||
renderAtMention = ({context, mentionName}) => {
|
||||
return (
|
||||
<AtMention
|
||||
mentionStyle={this.props.textStyles.mention}
|
||||
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
|
||||
isSearchResult={this.props.isSearchResult}
|
||||
mentionName={mentionName}
|
||||
onLongPress={this.props.onLongPress}
|
||||
onPostPress={this.props.onPostPress}
|
||||
navigator={this.props.navigator}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderChannelLink = ({context, channelName}) => {
|
||||
return (
|
||||
<ChannelLink
|
||||
linkStyle={this.props.textStyles.link}
|
||||
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
|
||||
channelName={channelName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmoji = ({context, emojiName, literal}) => {
|
||||
let size;
|
||||
let fontSize;
|
||||
const headingType = context.find((type) => type.startsWith('heading'));
|
||||
if (headingType) {
|
||||
size = this.props.emojiSizes[headingType];
|
||||
fontSize = this.props.fontSizes[headingType];
|
||||
} else {
|
||||
size = this.props.emojiSizes.text;
|
||||
fontSize = this.props.fontSizes.text;
|
||||
}
|
||||
|
||||
return (
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
literal={literal}
|
||||
size={size}
|
||||
fontSize={fontSize}
|
||||
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderParagraph = ({children, first}) => {
|
||||
if (!children || children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
const blockStyle = [style.block];
|
||||
if (!first) {
|
||||
blockStyle.push(this.props.blockStyles.adjacentParagraph);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={blockStyle}>
|
||||
<Text>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeading = ({children, level}) => {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
|
||||
return (
|
||||
<View style={[style.block, this.props.blockStyles[`heading${level}`]]}>
|
||||
<Text>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderCodeBlock = (props) => {
|
||||
// These sometimes include a trailing newline
|
||||
const content = props.literal.replace(/\n$/, '');
|
||||
|
||||
return (
|
||||
<MarkdownCodeBlock
|
||||
navigator={this.props.navigator}
|
||||
content={content}
|
||||
language={props.language}
|
||||
textStyle={this.props.textStyles.codeBlock}
|
||||
onLongPress={this.props.onLongPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderBlockQuote = ({children, ...otherProps}) => {
|
||||
return (
|
||||
<MarkdownBlockQuote
|
||||
iconStyle={this.props.blockStyles.quoteBlockIcon}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</MarkdownBlockQuote>
|
||||
);
|
||||
}
|
||||
|
||||
renderList = ({children, tight, type}) => {
|
||||
return (
|
||||
<MarkdownList
|
||||
ordered={type !== 'bullet'}
|
||||
tight={tight}
|
||||
>
|
||||
{children}
|
||||
</MarkdownList>
|
||||
);
|
||||
}
|
||||
|
||||
renderListItem = ({children, context, ...otherProps}) => {
|
||||
const level = context.filter((type) => type === 'list').length;
|
||||
|
||||
return (
|
||||
<MarkdownListItem
|
||||
bulletStyle={this.props.baseTextStyle}
|
||||
level={level}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</MarkdownListItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderHardBreak = () => {
|
||||
return <Text>{'\n'}</Text>;
|
||||
}
|
||||
|
||||
renderThematicBreak = () => {
|
||||
return <View style={this.props.blockStyles.horizontalRule}/>;
|
||||
}
|
||||
|
||||
renderSoftBreak = () => {
|
||||
return <Text>{'\n'}</Text>;
|
||||
}
|
||||
|
||||
renderHtml = (props) => {
|
||||
let rendered = this.renderText(props);
|
||||
|
||||
if (props.isBlock) {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
|
||||
rendered = (
|
||||
<View style={style.block}>
|
||||
{rendered}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
renderLink = ({children, href}) => {
|
||||
return (
|
||||
<MarkdownLink
|
||||
href={href}
|
||||
onLongPress={this.props.onLongPress}
|
||||
>
|
||||
{children}
|
||||
</MarkdownLink>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditedIndicator = ({context}) => {
|
||||
let spacer = '';
|
||||
if (context[0] === 'paragraph') {
|
||||
spacer = ' ';
|
||||
}
|
||||
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
const styles = [
|
||||
this.props.baseTextStyle,
|
||||
style.editedIndicatorText
|
||||
];
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={styles}
|
||||
>
|
||||
{spacer}
|
||||
<FormattedText
|
||||
id='post_message_view.edited'
|
||||
defaultMessage='(edited)'
|
||||
/>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let ast = this.parser.parse(this.props.value);
|
||||
|
||||
ast = addListItemIndices(ast);
|
||||
ast = pullOutImages(ast);
|
||||
|
||||
if (this.props.isEdited) {
|
||||
const editIndicatorNode = new Node('edited_indicator');
|
||||
if (['heading', 'paragraph'].includes(ast.lastChild.type)) {
|
||||
ast.lastChild.appendChild(editIndicatorNode);
|
||||
} else {
|
||||
const node = new Node('paragraph');
|
||||
node.appendChild(editIndicatorNode);
|
||||
|
||||
ast.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
return <View>{this.renderer.render(ast)}</View>;
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
// Android has trouble giving text transparency depending on how it's nested,
|
||||
// so we calculate the resulting colour manually
|
||||
const editedOpacity = Platform.select({
|
||||
ios: 0.3,
|
||||
android: 1.0
|
||||
});
|
||||
const editedColor = Platform.select({
|
||||
ios: theme.centerChannelColor,
|
||||
android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3)
|
||||
});
|
||||
|
||||
return {
|
||||
block: {
|
||||
alignItems: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap'
|
||||
},
|
||||
editedIndicatorText: {
|
||||
color: editedColor,
|
||||
fontSize: 14,
|
||||
opacity: editedOpacity
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -2,7 +2,6 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
View
|
||||
@@ -13,27 +12,19 @@ import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
|
||||
export default class MarkdownBlockQuote extends PureComponent {
|
||||
static propTypes = {
|
||||
continue: PropTypes.bool,
|
||||
iconStyle: CustomPropTypes.Style,
|
||||
children: CustomPropTypes.Children.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
let icon;
|
||||
if (!this.props.continue) {
|
||||
icon = (
|
||||
<Icon
|
||||
name='quote-left'
|
||||
style={this.props.iconStyle}
|
||||
size={14}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<View style={style.icon}>
|
||||
{icon}
|
||||
<View>
|
||||
<Icon
|
||||
name='quote-left'
|
||||
style={this.props.iconStyle}
|
||||
size={14}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.childContainer}>
|
||||
{this.props.children}
|
||||
@@ -50,8 +41,5 @@ const style = StyleSheet.create({
|
||||
},
|
||||
childContainer: {
|
||||
flex: 1
|
||||
},
|
||||
icon: {
|
||||
width: 23
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,9 +7,10 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import MarkdownCodeBlock from './markdown_code_block';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
theme: getTheme(state)
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {PropTypes} from 'prop-types';
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Clipboard,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
@@ -17,7 +16,6 @@ import FormattedText from 'app/components/formatted_text';
|
||||
import {getDisplayNameForLanguage} from 'app/utils/markdown';
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
const MAX_LINES = 4;
|
||||
|
||||
@@ -28,8 +26,7 @@ class MarkdownCodeBlock extends React.PureComponent {
|
||||
theme: PropTypes.object.isRequired,
|
||||
language: PropTypes.string,
|
||||
content: PropTypes.string.isRequired,
|
||||
textStyle: CustomPropTypes.Style,
|
||||
onLongPress: PropTypes.func.isRequired
|
||||
textStyle: CustomPropTypes.Style
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -75,26 +72,6 @@ class MarkdownCodeBlock extends React.PureComponent {
|
||||
});
|
||||
});
|
||||
|
||||
handleLongPress = async () => {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
const config = await mattermostManaged.getLocalConfig();
|
||||
|
||||
let action;
|
||||
if (config.copyAndPasteProtection !== 'true') {
|
||||
action = {
|
||||
text: formatMessage({id: 'mobile.markdown.code.copy_code', defaultMessage: 'Copy Code'}),
|
||||
onPress: this.handleCopyCode
|
||||
};
|
||||
}
|
||||
|
||||
this.props.onLongPress(action);
|
||||
}
|
||||
|
||||
handleCopyCode = () => {
|
||||
Clipboard.setString(this.props.content);
|
||||
}
|
||||
|
||||
trimContent = (content) => {
|
||||
const lines = content.split('\n');
|
||||
const numberOfLines = lines.length;
|
||||
@@ -154,10 +131,7 @@ class MarkdownCodeBlock extends React.PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={this.handlePress}
|
||||
onLongPress={this.handleLongPress}
|
||||
>
|
||||
<TouchableOpacity onPress={this.handlePress}>
|
||||
<View style={style.container}>
|
||||
<View style={style.lineNumbers}>
|
||||
<Text style={style.lineNumbersText}>
|
||||
|
||||
@@ -1,90 +1,36 @@
|
||||
// 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 React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Clipboard,
|
||||
Image,
|
||||
Linking,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View
|
||||
} from 'react-native';
|
||||
import {Image} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import {normalizeProtocol} from 'app/utils/url';
|
||||
|
||||
const MAX_IMAGE_HEIGHT = 150;
|
||||
|
||||
const ANDROID_MAX_HEIGHT = 4096;
|
||||
const ANDROID_MAX_WIDTH = 4096;
|
||||
|
||||
export default class MarkdownImage extends React.Component {
|
||||
export default class MarkdownLink extends PureComponent {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
linkDestination: PropTypes.string,
|
||||
onLongPress: PropTypes.func,
|
||||
source: PropTypes.string.isRequired,
|
||||
errorTextStyle: CustomPropTypes.Style
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired
|
||||
src: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
maxWidth: Math.MAX_INT,
|
||||
failed: false
|
||||
width: 10000,
|
||||
maxWidth: 10000,
|
||||
height: 0
|
||||
};
|
||||
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.loadImageSize(this.props.source);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
Image.getSize(this.props.src, this.handleSizeReceived, this.handleSizeFailed);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.source !== nextProps.source) {
|
||||
this.setState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
failed: false
|
||||
});
|
||||
|
||||
this.loadImageSize(nextProps.source);
|
||||
if (this.props.src !== nextProps.src) {
|
||||
Image.getSize(nextProps.src, this.handleSizeReceived, this.handleSizeFailed);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
loadImageSize = (source) => {
|
||||
Image.getSize(source, this.handleSizeReceived, this.handleSizeFailed);
|
||||
};
|
||||
|
||||
handleSizeReceived = (width, height) => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
width,
|
||||
height
|
||||
@@ -92,140 +38,33 @@ export default class MarkdownImage extends React.Component {
|
||||
};
|
||||
|
||||
handleSizeFailed = () => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
failed: true
|
||||
width: 0,
|
||||
height: 0
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
handleLayout = (event) => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
maxWidth: event.nativeEvent.layout.width
|
||||
});
|
||||
};
|
||||
|
||||
handleLinkPress = () => {
|
||||
const url = normalizeProtocol(this.props.linkDestination);
|
||||
|
||||
Linking.canOpenURL(url).then((supported) => {
|
||||
if (supported) {
|
||||
Linking.openURL(url);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleLinkLongPress = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
const config = await mattermostManaged.getLocalConfig();
|
||||
|
||||
let action;
|
||||
if (config.copyAndPasteProtection !== 'true') {
|
||||
action = {
|
||||
text: formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'}),
|
||||
onPress: this.handleLinkCopy
|
||||
};
|
||||
}
|
||||
|
||||
this.props.onLongPress(action);
|
||||
};
|
||||
|
||||
handleLinkCopy = () => {
|
||||
Clipboard.setString(this.props.linkDestination);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let image = null;
|
||||
let {width, maxWidth, height} = this.state; // eslint-disable-line prefer-const
|
||||
|
||||
if (this.state.width && this.state.height && this.state.maxWidth) {
|
||||
let {width, height} = this.state;
|
||||
|
||||
if (Platform.OS === 'android' && (width > ANDROID_MAX_WIDTH || height > ANDROID_MAX_HEIGHT)) {
|
||||
// Android has a cap on the max image size that can be displayed
|
||||
|
||||
image = (
|
||||
<Text style={this.props.errorTextStyle}>
|
||||
<FormattedText
|
||||
id='mobile.markdown.image.too_large'
|
||||
defaultMessage='Image exceeds max dimensions of {maxWidth} by {maxHeight}:'
|
||||
values={{
|
||||
maxWidth: ANDROID_MAX_WIDTH,
|
||||
maxHeight: ANDROID_MAX_HEIGHT
|
||||
}}
|
||||
/>
|
||||
{' '}
|
||||
{this.props.children}
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
const maxWidth = this.state.maxWidth;
|
||||
if (width > maxWidth) {
|
||||
height = height * (maxWidth / width);
|
||||
width = maxWidth;
|
||||
}
|
||||
|
||||
const maxHeight = MAX_IMAGE_HEIGHT;
|
||||
if (height > maxHeight) {
|
||||
width = width * (maxHeight / height);
|
||||
height = maxHeight;
|
||||
}
|
||||
|
||||
// React Native complains if we try to pass resizeMode as a style
|
||||
image = (
|
||||
<Image
|
||||
source={{uri: this.props.source}}
|
||||
resizeMode='contain'
|
||||
style={[{width, height}, style.image]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (this.state.failed) {
|
||||
image = (
|
||||
<Text style={this.props.errorTextStyle}>
|
||||
<FormattedText
|
||||
id='mobile.markdown.image.error'
|
||||
defaultMessage='Image failed to load:'
|
||||
/>
|
||||
{' '}
|
||||
{this.props.children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (image && this.props.linkDestination) {
|
||||
image = (
|
||||
<TouchableHighlight
|
||||
onPress={this.handleLinkPress}
|
||||
onLongPress={this.handleLinkLongPress}
|
||||
>
|
||||
{image}
|
||||
</TouchableHighlight>
|
||||
);
|
||||
if (width > maxWidth) {
|
||||
height = height * (maxWidth / width);
|
||||
width = maxWidth;
|
||||
}
|
||||
|
||||
// React Native complains if we try to pass resizeMode into a StyleSheet
|
||||
return (
|
||||
<View
|
||||
style={style.container}
|
||||
<Image
|
||||
source={{uri: this.props.src}}
|
||||
onLayout={this.handleLayout}
|
||||
>
|
||||
{image}
|
||||
</View>
|
||||
style={{width, height, flexShrink: 1, resizeMode: 'cover'}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
image: {
|
||||
marginVertical: 5
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,17 +3,12 @@
|
||||
|
||||
import React, {Children, PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Clipboard, Linking, Text} from 'react-native';
|
||||
import {Linking, Text} from 'react-native';
|
||||
import urlParse from 'url-parse';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
import Config from 'assets/config';
|
||||
|
||||
import {normalizeProtocol} from 'app/utils/url';
|
||||
|
||||
export default class MarkdownLink extends PureComponent {
|
||||
static propTypes = {
|
||||
children: CustomPropTypes.Children.isRequired,
|
||||
@@ -23,14 +18,11 @@ export default class MarkdownLink extends PureComponent {
|
||||
|
||||
static defaultProps = {
|
||||
onLongPress: () => true
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
handlePress = () => {
|
||||
const url = normalizeProtocol(this.props.href);
|
||||
// Android doesn't like the protocol being upper case
|
||||
const url = this.props.href;
|
||||
|
||||
Linking.canOpenURL(url).then((supported) => {
|
||||
if (supported) {
|
||||
@@ -73,33 +65,13 @@ export default class MarkdownLink extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
handleLongPress = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
const config = await mattermostManaged.getLocalConfig();
|
||||
|
||||
let action;
|
||||
if (config.copyAndPasteProtection !== 'true') {
|
||||
action = {
|
||||
text: formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'}),
|
||||
onPress: this.handleCopyURL
|
||||
};
|
||||
}
|
||||
|
||||
this.props.onLongPress(action);
|
||||
}
|
||||
|
||||
handleCopyURL = () => {
|
||||
Clipboard.setString(this.props.href);
|
||||
}
|
||||
|
||||
render() {
|
||||
const children = Config.ExperimentalNormalizeMarkdownLinks ? this.parseChildren() : this.props.children;
|
||||
|
||||
return (
|
||||
<Text
|
||||
onPress={this.handlePress}
|
||||
onLongPress={this.handleLongPress}
|
||||
onLongPress={this.props.onLongPress}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
|
||||
@@ -3,33 +3,30 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import {View} from 'react-native';
|
||||
|
||||
export default class MarkdownList extends PureComponent {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf([PropTypes.node])]).isRequired,
|
||||
ordered: PropTypes.bool.isRequired,
|
||||
startAt: PropTypes.number,
|
||||
tight: PropTypes.bool
|
||||
};
|
||||
|
||||
render() {
|
||||
const children = React.Children.map(this.props.children, (child) => {
|
||||
const children = React.Children.map(this.props.children, (child, i) => {
|
||||
return React.cloneElement(child, {
|
||||
ordered: this.props.ordered,
|
||||
startAt: this.props.startAt,
|
||||
index: i,
|
||||
tight: this.props.tight
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={style.indent}>
|
||||
<View style={{marginRight: 20}}>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
indent: {
|
||||
marginRight: 20
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,18 +15,21 @@ export default class MarkdownListItem extends PureComponent {
|
||||
static propTypes = {
|
||||
children: CustomPropTypes.Children.isRequired,
|
||||
ordered: PropTypes.bool.isRequired,
|
||||
continue: PropTypes.bool,
|
||||
startAt: PropTypes.number,
|
||||
index: PropTypes.number.isRequired,
|
||||
tight: PropTypes.bool,
|
||||
bulletStyle: CustomPropTypes.Style,
|
||||
level: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
startAt: 1
|
||||
};
|
||||
|
||||
render() {
|
||||
let bullet;
|
||||
if (this.props.continue) {
|
||||
bullet = '';
|
||||
} else if (this.props.ordered) {
|
||||
bullet = this.props.index + '. ';
|
||||
if (this.props.ordered) {
|
||||
bullet = (this.props.startAt + this.props.index) + '. ';
|
||||
} else if (this.props.level % 2 === 0) {
|
||||
bullet = '◦';
|
||||
} else {
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
// Add indices to the items of every list
|
||||
export function addListItemIndices(ast) {
|
||||
const walker = ast.walker();
|
||||
|
||||
let e;
|
||||
while ((e = walker.next())) {
|
||||
if (e.entering) {
|
||||
const node = e.node;
|
||||
|
||||
if (node.type === 'list') {
|
||||
let i = node.listStart || 1; // List indices match what would be displayed in the UI
|
||||
|
||||
for (let child = node.firstChild; child; child = child.next) {
|
||||
child.index = i;
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
// Take all images and move them to be children of the root document node. When this happens, their
|
||||
// parent nodes are split into two, if necessary, with the version that follows the image having its
|
||||
// "continue" field set to true to indicate that things like bullet points don't need to be rendered.
|
||||
export function pullOutImages(ast) {
|
||||
for (let block = ast.firstChild; block !== null; block = block.next) {
|
||||
let node = block.firstChild;
|
||||
|
||||
let cameFromChild = false;
|
||||
|
||||
while (node && node !== block) {
|
||||
if (node.type === 'image' && node.parent.type !== 'document') {
|
||||
const image = node;
|
||||
|
||||
let parent = image.parent;
|
||||
let prev = image.prev;
|
||||
let next = image.next;
|
||||
|
||||
// Remove image from its siblings
|
||||
if (prev) {
|
||||
prev._next = next;
|
||||
}
|
||||
if (next) {
|
||||
next._prev = prev;
|
||||
|
||||
// Since the following text will be on a new line, a preceding space would cause the
|
||||
// alignment to be incorrect
|
||||
if (next.type === 'text' && next.literal.startsWith(' ')) {
|
||||
next.literal = next.literal.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
// And from its parents
|
||||
if (parent._firstChild === image) {
|
||||
// image was the first child (ie prev is null), so the next sibling is now the first child
|
||||
parent._firstChild = next;
|
||||
}
|
||||
if (parent._lastChild === image) {
|
||||
// image was the last child (ie next is null), so the previous sibling is now the last child
|
||||
parent._lastChild = prev;
|
||||
}
|
||||
|
||||
// Split the tree between the previous and next siblings, where the image would've been
|
||||
while (parent && parent.type !== 'document') {
|
||||
// We only need to split the parent if there's anything on the right of where we're splitting
|
||||
// in the current branch
|
||||
let parentCopy = null;
|
||||
|
||||
// Split if we have children to the right of the split (next) or if we have any siblings to the
|
||||
// right of the parent (parent.next)
|
||||
if (next) {
|
||||
parentCopy = copyNodeWithoutNeighbors(parent);
|
||||
|
||||
// Set an additional flag so we know not to re-render things like bullet points
|
||||
parentCopy.continue = true;
|
||||
|
||||
// Re-assign the children to the right of the split to belong to the copy
|
||||
parentCopy._firstChild = next;
|
||||
parentCopy._lastChild = getLastSibling(next);
|
||||
|
||||
if (parent._firstChild === next) {
|
||||
parent._firstChild = null;
|
||||
parent._lastChild = null;
|
||||
} else {
|
||||
parent._lastChild = prev;
|
||||
}
|
||||
|
||||
// And re-assign the parent of all of those to be the copy
|
||||
for (let child = parentCopy.firstChild; child; child = child.next) {
|
||||
child._parent = parentCopy;
|
||||
}
|
||||
|
||||
// Insert the copy as parent's next sibling
|
||||
if (parent.next) {
|
||||
parent.next._prev = parentCopy;
|
||||
parentCopy._next = parent.next;
|
||||
parent._next = parentCopy;
|
||||
} else /* if (parent.parent.lastChild === parent) */ {
|
||||
// Since parent has no next sibling, parent is the last child of its parent, so
|
||||
// we need to set the copy as the last child
|
||||
parent.parent.lastChild = parentCopy;
|
||||
}
|
||||
}
|
||||
|
||||
// Change prev and next to no longer be siblings
|
||||
if (prev) {
|
||||
prev._next = null;
|
||||
}
|
||||
|
||||
if (next) {
|
||||
next._prev = null;
|
||||
}
|
||||
|
||||
// This image is part of a link so include the destination along with it
|
||||
if (parent.type === 'link') {
|
||||
image.linkDestination = parent.destination;
|
||||
}
|
||||
|
||||
// Move up the tree
|
||||
next = parentCopy || parent.next;
|
||||
prev = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
// Re-insert the image now that we have a tree split down to the root with the image's ancestors.
|
||||
// Note that parent is the root node, prev is the ancestor of image, and next is the ancestor of the copy
|
||||
|
||||
// Add image to its parent
|
||||
image._parent = parent;
|
||||
if (next) {
|
||||
parent._lastChild = next;
|
||||
} else {
|
||||
// image is the last child of the root node now
|
||||
parent._lastChild = image;
|
||||
}
|
||||
|
||||
// Add image to its siblings
|
||||
image._prev = prev;
|
||||
prev._next = image;
|
||||
|
||||
image._next = next;
|
||||
if (next) {
|
||||
next._prev = image;
|
||||
}
|
||||
|
||||
// The copy still needs its parent set to the root node
|
||||
if (next) {
|
||||
next._parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk through tree to next node
|
||||
if (node.firstChild && !cameFromChild) {
|
||||
node = node.firstChild;
|
||||
cameFromChild = false;
|
||||
} else if (node.next) {
|
||||
node = node.next;
|
||||
cameFromChild = false;
|
||||
} else {
|
||||
node = node.parent;
|
||||
cameFromChild = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
// Copies a Node without its parent, children, or siblings
|
||||
function copyNodeWithoutNeighbors(node) {
|
||||
// commonmark uses classes so it takes a bit of work to copy them
|
||||
const copy = Object.assign(Object.create(Reflect.getPrototypeOf(node)), node);
|
||||
|
||||
copy._parent = null;
|
||||
copy._firstChild = null;
|
||||
copy._lastChild = null;
|
||||
copy._prev = null;
|
||||
copy._next = null;
|
||||
|
||||
// Deep copy list data since it's an object
|
||||
copy._listData = {...node._listData};
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
// Gets the last sibling of a given node
|
||||
function getLastSibling(node) {
|
||||
let sibling = node;
|
||||
|
||||
while (sibling && sibling.next) {
|
||||
sibling = sibling.next;
|
||||
}
|
||||
|
||||
return sibling;
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
// 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 {init as initWebSocket} from 'mattermost-redux/actions/websocket';
|
||||
|
||||
import {connection} from 'app/actions/device';
|
||||
import {getConnection, isLandscape} from 'app/selectors/device';
|
||||
import {getConnection} from 'app/selectors/device';
|
||||
|
||||
import OfflineIndicator from './offline_indicator';
|
||||
|
||||
@@ -18,19 +14,9 @@ function mapStateToProps(state) {
|
||||
|
||||
return {
|
||||
isConnecting,
|
||||
isLandscape: isLandscape(state),
|
||||
isOnline: getConnection(state),
|
||||
webSocketStatus
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
connection,
|
||||
initWebSocket
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(OfflineIndicator);
|
||||
export default connect(mapStateToProps)(OfflineIndicator);
|
||||
|
||||
@@ -8,30 +8,24 @@ import {
|
||||
Animated,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import checkNetwork from 'app/utils/network';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
const HEIGHT = 38;
|
||||
const NAVBAR = Platform.OS === 'ios' ? 64 : 46;
|
||||
const INITIAL_TOP = NAVBAR - HEIGHT;
|
||||
const OFFLINE = 'offline';
|
||||
const CONNECTING = 'connecting';
|
||||
const CONNECTED = 'connected';
|
||||
|
||||
export default class OfflineIndicator extends Component {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
connection: PropTypes.func.isRequired,
|
||||
initWebSocket: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
isConnecting: PropTypes.bool,
|
||||
isLandscape: PropTypes.bool,
|
||||
isOnline: PropTypes.bool,
|
||||
webSocketStatus: PropTypes.string
|
||||
};
|
||||
@@ -43,27 +37,16 @@ export default class OfflineIndicator extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.isX = DeviceInfo.getModel() === 'iPhone X';
|
||||
const navBar = this.getNavBarHeight(props.isLandscape);
|
||||
|
||||
this.state = {
|
||||
network: null,
|
||||
navBar,
|
||||
top: new Animated.Value(navBar - HEIGHT)
|
||||
top: new Animated.Value(INITIAL_TOP)
|
||||
};
|
||||
|
||||
this.backgroundColor = new Animated.Value(0);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {isLandscape, webSocketStatus} = this.props;
|
||||
|
||||
if (nextProps.isLandscape !== isLandscape && this.state.network) {
|
||||
const navBar = this.getNavBarHeight(nextProps.isLandscape);
|
||||
const top = new Animated.Value(navBar - HEIGHT);
|
||||
this.setState({navBar, top});
|
||||
}
|
||||
|
||||
const {webSocketStatus} = this.props;
|
||||
if (nextProps.isOnline) {
|
||||
if (this.state.network && webSocketStatus === RequestStatus.STARTED && nextProps.webSocketStatus === RequestStatus.SUCCESS) {
|
||||
// Show the connected animation only if we had a previous network status
|
||||
@@ -78,19 +61,21 @@ export default class OfflineIndicator extends Component {
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return (nextState.network !== this.state.network || nextProps.isLandscape !== this.props.isLandscape);
|
||||
return nextState.network !== this.state.network && nextState.network;
|
||||
}
|
||||
|
||||
connect = () => {
|
||||
checkNetwork((result) => {
|
||||
this.setState({network: CONNECTING}, () => {
|
||||
if (result) {
|
||||
this.props.actions.connection(true);
|
||||
this.props.actions.initWebSocket(Platform.OS);
|
||||
} else {
|
||||
this.setState({network: OFFLINE});
|
||||
}
|
||||
});
|
||||
offline = () => {
|
||||
this.setState({network: OFFLINE}, () => {
|
||||
this.show();
|
||||
});
|
||||
};
|
||||
|
||||
connecting = () => {
|
||||
const prevState = this.state.network;
|
||||
this.setState({network: CONNECTING}, () => {
|
||||
if (prevState !== OFFLINE) {
|
||||
this.show();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -105,7 +90,7 @@ export default class OfflineIndicator extends Component {
|
||||
),
|
||||
Animated.timing(
|
||||
this.state.top, {
|
||||
toValue: (this.state.navBar - HEIGHT),
|
||||
toValue: INITIAL_TOP,
|
||||
duration: 300,
|
||||
delay: 500
|
||||
}
|
||||
@@ -116,42 +101,10 @@ export default class OfflineIndicator extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
connecting = () => {
|
||||
const prevState = this.state.network;
|
||||
this.setState({network: CONNECTING}, () => {
|
||||
if (prevState !== OFFLINE) {
|
||||
this.show();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getNavBarHeight = (isLandscape) => {
|
||||
let navBar = 46;
|
||||
if (Platform.OS === 'ios') {
|
||||
if (this.isX && isLandscape) {
|
||||
navBar = 32;
|
||||
} else if (this.isX) {
|
||||
navBar = 88;
|
||||
} else if (isLandscape) {
|
||||
navBar = 52;
|
||||
} else {
|
||||
navBar = 64;
|
||||
}
|
||||
}
|
||||
|
||||
return navBar;
|
||||
};
|
||||
|
||||
offline = () => {
|
||||
this.setState({network: OFFLINE}, () => {
|
||||
this.show();
|
||||
});
|
||||
};
|
||||
|
||||
show = () => {
|
||||
Animated.timing(
|
||||
this.state.top, {
|
||||
toValue: this.state.navBar,
|
||||
toValue: NAVBAR,
|
||||
duration: 300
|
||||
}
|
||||
).start();
|
||||
@@ -173,19 +126,7 @@ export default class OfflineIndicator extends Component {
|
||||
switch (this.state.network) {
|
||||
case OFFLINE:
|
||||
i18nId = 'mobile.offlineIndicator.offline';
|
||||
defaultMessage = 'Cannot connect to the server';
|
||||
action = (
|
||||
<TouchableOpacity
|
||||
onPress={this.connect}
|
||||
style={[styles.actionContainer, styles.actionButton]}
|
||||
>
|
||||
<IonIcon
|
||||
color='#FFFFFF'
|
||||
name='ios-refresh'
|
||||
size={20}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
defaultMessage = 'No internet connection';
|
||||
break;
|
||||
case CONNECTING:
|
||||
i18nId = 'mobile.offlineIndicator.connecting';
|
||||
@@ -200,7 +141,6 @@ export default class OfflineIndicator extends Component {
|
||||
);
|
||||
break;
|
||||
case CONNECTED:
|
||||
default:
|
||||
i18nId = 'mobile.offlineIndicator.connected';
|
||||
defaultMessage = 'Connected';
|
||||
action = (
|
||||
|
||||
@@ -20,22 +20,16 @@ export default class OptionsContext extends PureComponent {
|
||||
cancelText: 'Cancel'
|
||||
};
|
||||
|
||||
show = (additionalAction) => {
|
||||
show = () => {
|
||||
const {actions, cancelText} = this.props;
|
||||
const nextActions = [...actions];
|
||||
if (additionalAction && !additionalAction.nativeEvent && additionalAction.text) {
|
||||
const copyPostIndex = nextActions.findIndex((action) => action.copyPost);
|
||||
nextActions.splice(copyPostIndex + 1, 0, additionalAction);
|
||||
}
|
||||
|
||||
if (nextActions.length) {
|
||||
const actionsText = nextActions.map((a) => a.text);
|
||||
if (actions.length) {
|
||||
const actionsText = actions.map((a) => a.text);
|
||||
RNBottomSheet.showBottomSheetWithOptions({
|
||||
options: [...actionsText, cancelText],
|
||||
cancelButtonIndex: nextActions.length
|
||||
cancelButtonIndex: actions.length
|
||||
}, (value) => {
|
||||
if (value !== nextActions.length) {
|
||||
const selectedOption = nextActions[value];
|
||||
if (value !== actions.length) {
|
||||
const selectedOption = actions[value];
|
||||
if (selectedOption && selectedOption.onPress) {
|
||||
selectedOption.onPress();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user