Compare commits

..

20 Commits

Author SHA1 Message Date
Elias Nahum
93265b3de0 Keep post openGraph data 2017-11-06 14:21:01 -03:00
enahum
0d70372a3c Version Bump to 63 (#1095) 2017-11-03 17:51:08 -03:00
enahum
72087391dc Version Bump to 63 (#1094) 2017-11-03 17:20:37 -03:00
enahum
9c89fe2907 Set the section list to wait for interactions before updating the unread indicator (#1093) 2017-11-03 14:47:31 -03:00
enahum
9684328123 set appSarted as false when logging out and resetting cache (#1092) 2017-11-03 14:47:18 -03:00
enahum
c3ef5e6f38 Version Bump to 62 (#1089) 2017-11-02 17:50:26 -03:00
enahum
e36f63c84d Version Bump to 62 (#1088) 2017-11-02 17:44:18 -03:00
enahum
ce05b9c98b RN-456 when a channel is left we update content and title (#1087) 2017-11-02 17:16:08 -03:00
enahum
388294a124 Update Mattermost redux (#1086)
* Fix middleware

* Upgrade mattermost-redux

* another middleware fix
2017-11-02 16:09:53 -03:00
enahum
1a12abfe50 Fix bugs reported by sentry (#1081) 2017-11-02 16:09:01 -03:00
Elias Nahum
0210d6e1eb Use ImageBackground for youtube videos instead of nested Images 2017-11-02 16:08:43 -03:00
enahum
49bcf185e6 Prevent More unreads indicators when canceling jump to... (#1082) 2017-11-01 18:50:28 -03:00
enahum
61ecf7d159 Version Bump to 61 (#1078) 2017-11-01 16:32:01 -03:00
enahum
ead5f2860f Version Bump to 61 (#1077) 2017-11-01 16:31:48 -03:00
enahum
09ac903630 Keep user statuses for current channel and all post visibilities (#1080) 2017-11-01 16:26:24 -03:00
enahum
a471379cb2 Perform validations to avoid crash on data cleanup (#1075)
* Perform validations to avoid crash on data cleanup

* Remove postId from postsInChannel
2017-10-31 12:23:27 -07:00
enahum
eaf128b2a0 Show profile images and names only for DMs in channel intro (#1076) 2017-10-31 12:23:17 -07:00
enahum
96f5cd2c11 translations PR 20171030 (#1074) 2017-10-31 10:42:31 -03:00
enahum
6b23c230ed Version Bump to 60 (#1073) 2017-10-30 14:37:35 -03:00
enahum
63a3e4eb89 Version Bump to 60 (#1072) 2017-10-30 14:37:18 -03:00
299 changed files with 5748 additions and 16842 deletions

View File

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

27
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,13 +33,6 @@ import com.android.build.OutputFile
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
*
* // whether to disable dev mode in custom build variants (by default only disabled in release)
* // for example: to disable dev mode in the staging build type (if configured)
* devDisabledInStaging: true,
* // The configuration property can be in the following formats
* // 'devDisabledIn${productFlavor}${buildType}'
* // 'devDisabledIn${buildType}'
*
* // the root of your project, i.e. where "package.json" lives
* root: "../../",
*
@@ -65,21 +58,17 @@ import com.android.build.OutputFile
* inputExcludes: ["android/**", "ios/**"],
*
* // override which node gets called and with what additional arguments
* nodeExecutableAndArgs: ["node"],
* nodeExecutableAndArgs: ["node"]
*
* // supply additional arguments to the packager
* extraPackagerArgs: []
* ]
*/
project.ext.react = [
entryFile: "index.js"
]
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
if (System.getenv("SENTRY_ENABLED") == "true") {
if (System.getenv("MM_SENTRY_ENABLED") == "true") {
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
}
@@ -106,8 +95,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion 21
targetSdkVersion 23
versionCode 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'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,4 @@
rootProject.name = 'Mattermost'
include ':react-native-doc-viewer'
project(':react-native-doc-viewer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-doc-viewer/android')
include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
include ':react-native-youtube'
project(':react-native-youtube').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-youtube/android')
include ':react-native-sentry'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
import {batchActions} from 'redux-batched-actions';
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
@@ -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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,8 @@ function mapStateToProps(state, ownProps) {
firstName: user.first_name,
lastName: user.last_name,
username: user.username,
theme: getTheme(state)
theme: getTheme(state),
...ownProps
};
}

View File

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

View File

@@ -1,32 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class AutocompleteDivider extends PureComponent {
static propTypes = {
theme: PropTypes.object.isRequired
};
render() {
const {theme} = this.props;
const style = getStyleFromTheme(theme);
return (
<View style={style.divider}/>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
divider: {
height: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2)
}
};
});

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {searchChannels} from 'mattermost-redux/actions/channels';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
@@ -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)
};

View File

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

View File

@@ -15,7 +15,8 @@ function mapStateToProps(state, ownProps) {
return {
displayName: channel.display_name,
name: channel.name,
theme: getTheme(state)
theme: getTheme(state),
...ownProps
};
}

View File

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

View File

@@ -25,11 +25,26 @@ const getEmojisByName = createSelector(
}
);
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {currentChannelId} = state.entities.channels;
const emojis = getEmojisByName(state);
let postDraft;
if (ownProps.rootId) {
const threadDraft = state.views.thread.drafts[ownProps.rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
} else if (currentChannelId) {
const channelDraft = state.views.channel.drafts[currentChannelId];
if (channelDraft) {
postDraft = channelDraft.draft;
}
}
return {
emojis,
postDraft,
theme: getTheme(state)
};
}

View File

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

View File

@@ -1,45 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {getAutocompleteCommands} from 'mattermost-redux/actions/integrations';
import {getAutocompleteCommandsList} from 'mattermost-redux/selectors/entities/integrations';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import SlashSuggestion from './slash_suggestion';
// TODO: Remove when all below commands have been implemented
const COMMANDS_TO_IMPLEMENT_LATER = ['collapse', 'expand', 'join', 'open', 'leave', 'logout', 'msg', 'grpmsg'];
const NON_MOBILE_COMMANDS = ['rename', 'invite_people', 'shortcuts', 'search', 'help', 'settings', 'remove'];
const COMMANDS_TO_HIDE_ON_MOBILE = [...COMMANDS_TO_IMPLEMENT_LATER, ...NON_MOBILE_COMMANDS];
const mobileCommandsSelector = createSelector(
getAutocompleteCommandsList,
(commands) => {
return commands.filter((command) => !COMMANDS_TO_HIDE_ON_MOBILE.includes(command.trigger));
}
);
function mapStateToProps(state) {
return {
commands: mobileCommandsSelector(state),
commandsRequest: state.requests.integrations.getAutocompleteCommands,
currentTeamId: getCurrentTeamId(state),
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getAutocompleteCommands
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(SlashSuggestion);

View File

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

View File

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

View File

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

View File

@@ -32,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 = () => {

View File

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

View File

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

View File

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

View File

@@ -147,7 +147,7 @@ class FilteredList extends Component {
},
channels: {
builder: this.buildChannelsForSearch,
id: 'mobile.channel_list.channels',
id: 'sidebar.channels',
defaultMessage: 'CHANNELS'
},
dms: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,6 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import tracker from 'app/utils/time_tracker';
import TeamsListItem from './teams_list_item';
@@ -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',

View File

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

View File

@@ -9,7 +9,7 @@ import {
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {AwayAvatar, DndAvatar, OfflineAvatar, OnlineAvatar} from 'app/components/status_icons';
import {OnlineStatus, AwayStatus, OfflineStatus} from 'app/components/status_icons';
import {General} from 'mattermost-redux/constants';
@@ -90,43 +90,30 @@ export default class ChannelIcon extends React.PureComponent {
</View>
);
} else if (type === General.DM_CHANNEL) {
switch (status) {
case General.AWAY:
if (status === General.ONLINE) {
icon = (
<AwayAvatar
width={size}
height={size}
color={theme.awayIndicator}
/>
);
break;
case General.DND:
icon = (
<DndAvatar
width={size}
height={size}
color={theme.dndIndicator}
/>
);
break;
case General.ONLINE:
icon = (
<OnlineAvatar
<OnlineStatus
width={size}
height={size}
color={theme.onlineIndicator}
/>
);
break;
default:
} else if (status === General.AWAY) {
icon = (
<OfflineAvatar
<AwayStatus
width={size}
height={size}
color={theme.awayIndicator}
/>
);
} else {
icon = (
<OfflineStatus
width={size}
height={size}
color={offlineColor}
/>
);
break;
}
}

View File

@@ -77,6 +77,7 @@ class ChannelIntro extends PureComponent {
size={64}
statusBorderWidth={2}
statusSize={25}
statusIconSize={15}
/>
</TouchableOpacity>
));

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,8 @@ function makeMapStateToProps() {
return (state, ownProps) => {
return {
theme: getTheme(state),
channel: getChannel(state, ownProps)
channel: getChannel(state, ownProps),
...ownProps
};
};
}

View File

@@ -12,7 +12,8 @@ function mapStateToProps(state, ownProps) {
return {
theme: getTheme(state),
user: getUser(state, ownProps.id),
teammateNameDisplay: getTeammateNameDisplaySetting(state)
teammateNameDisplay: getTeammateNameDisplaySetting(state),
...ownProps
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,95 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
TouchableOpacity,
View
} from 'react-native';
import Emoji from 'app/components/emoji';
export default class EmojiPickerRow extends Component {
static propTypes = {
emojiGutter: PropTypes.number.isRequired,
emojiSize: PropTypes.number.isRequired,
items: PropTypes.array.isRequired,
onEmojiPress: PropTypes.func.isRequired
}
shouldComponentUpdate(nextProps) {
return this.props.items.length !== nextProps.items.length;
}
renderEmojis = (emoji, index, emojis) => {
const {emojiGutter, emojiSize} = this.props;
const style = [
styles.emoji,
{
width: emojiSize,
height: emojiSize,
marginHorizontal: emojiGutter
}
];
if (index === 0) {
style.push(styles.emojiLeft);
} else if (index === emojis.length - 1) {
style.push(styles.emojiRight);
}
if (!emoji) {
return (
<View
key={index}
style={style}
/>
);
}
return (
<TouchableOpacity
key={emoji.name}
style={style}
onPress={() => {
this.props.onEmojiPress(emoji.name);
}}
>
<Emoji
emojiName={emoji.name}
size={emojiSize}
/>
</TouchableOpacity>
);
}
render() {
const {emojiGutter, items} = this.props;
return (
<View style={[styles.columnStyle, {marginVertical: emojiGutter}]}>
{items.map(this.renderEmojis)}
</View>
);
}
}
const styles = StyleSheet.create({
columnStyle: {
alignSelf: 'stretch',
flexDirection: 'row',
justifyContent: 'space-between'
},
emoji: {
alignItems: 'center',
justifyContent: 'center'
},
emojiLeft: {
marginLeft: 0
},
emojiRight: {
marginRight: 0
}
});

View File

@@ -6,62 +6,48 @@ import {createSelector} from 'reselect';
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
import {getDimensions, isLandscape} from 'app/selectors/device';
import {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)
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -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)}>
{'!['}
{children}
{']('}
{src}
{')'}
</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'
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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